|
@@ -1,531 +0,0 @@
|
|
|
-<template>
|
|
|
- <div>
|
|
|
- <modal title='Edit Song'>
|
|
|
- <div slot='body'>
|
|
|
- <h5 class='has-text-centered'>Video Preview</h5>
|
|
|
- <div class='video-container'>
|
|
|
- <div id='player'></div>
|
|
|
- <div class="controls">
|
|
|
- <form action="#">
|
|
|
- <p style="margin-top: 0; position: relative;">
|
|
|
- <input type="range" id="volumeSlider" min="0" max="100" class="active" v-on:change="changeVolume()" v-on:input="changeVolume()">
|
|
|
- </p>
|
|
|
- </form>
|
|
|
- <p class='control has-addons'>
|
|
|
- <button class='button' @click='settings("pause")' v-if='!video.paused'>
|
|
|
- <i class='material-icons'>pause</i>
|
|
|
- </button>
|
|
|
- <button class='button' @click='settings("play")' v-if='video.paused'>
|
|
|
- <i class='material-icons'>play_arrow</i>
|
|
|
- </button>
|
|
|
- <button class='button' @click='settings("stop")'>
|
|
|
- <i class='material-icons'>stop</i>
|
|
|
- </button>
|
|
|
- <button class='button' @click='settings("skipToLast10Secs")'>
|
|
|
- <i class='material-icons'>fast_forward</i>
|
|
|
- </button>
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <h5 class='has-text-centered'>Thumbnail Preview</h5>
|
|
|
- <img class='thumbnail-preview' :src='editing.song.thumbnail' onerror="this.src='/assets/notes-transparent.png'">
|
|
|
-
|
|
|
- <div class="control is-horizontal">
|
|
|
- <div class="control-label">
|
|
|
- <label class="label">Thumbnail URL</label>
|
|
|
- </div>
|
|
|
- <div class="control">
|
|
|
- <input class='input' type='text' v-model='editing.song.thumbnail'>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <h5 class='has-text-centered'>Edit Information</h5>
|
|
|
-
|
|
|
- <p class='control'>
|
|
|
- <label class='checkbox'>
|
|
|
- <input type='checkbox' v-model='editing.song.explicit'>
|
|
|
- Explicit
|
|
|
- </label>
|
|
|
- </p>
|
|
|
- <label class='label'>Song ID & Title</label>
|
|
|
- <div class="control is-horizontal">
|
|
|
- <div class="control is-grouped">
|
|
|
- <p class='control is-expanded'>
|
|
|
- <input class='input' type='text' v-model='editing.song.songId'>
|
|
|
- </p>
|
|
|
- <p class='control is-expanded'>
|
|
|
- <input class='input' type='text' v-model='editing.song.title' autofocus>
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <label class='label'>Artists & Genres</label>
|
|
|
- <div class='control is-horizontal'>
|
|
|
- <div class='control is-grouped artist-genres'>
|
|
|
- <div>
|
|
|
- <p class='control has-addons'>
|
|
|
- <input class='input' id='new-artist' type='text' placeholder='Artist'>
|
|
|
- <button class='button is-info' @click='addTag("artists")'>Add Artist</button>
|
|
|
- </p>
|
|
|
- <span class='tag is-info' v-for='(index, artist) in editing.song.artists' track-by='$index'>
|
|
|
- {{ artist }}
|
|
|
- <button class='delete is-info' @click='removeTag("artists", index)'></button>
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <div>
|
|
|
- <p class='control has-addons'>
|
|
|
- <input class='input' id='new-genre' type='text' placeholder='Genre'>
|
|
|
- <button class='button is-info' @click='addTag("genres")'>Add Genre</button>
|
|
|
- </p>
|
|
|
- <span class='tag is-info' v-for='(index, genre) in editing.song.genres' track-by='$index'>
|
|
|
- {{ genre }}
|
|
|
- <button class='delete is-info' @click='removeTag("genres", index)'></button>
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <label class='label'>Song Duration</label>
|
|
|
- <p class='control'>
|
|
|
- <input class='input' type='text' v-model='editing.song.duration'>
|
|
|
- </p>
|
|
|
- <label class='label'>Skip Duration</label>
|
|
|
- <p class='control'>
|
|
|
- <input class='input' type='text' v-model='editing.song.skipDuration'>
|
|
|
- </p>
|
|
|
- <article class="message" v-if="editing.type === 'songs'">
|
|
|
- <div class="message-body">
|
|
|
- <span class="reports-length">
|
|
|
- {{ reports.length }}
|
|
|
- <span v-if="reports.length > 1 || reports.length <= 0"> Reports</span>
|
|
|
- <span v-else> Report</span>
|
|
|
- </span>
|
|
|
- <div v-for='report in reports'>
|
|
|
- <a :href='`/admin/reports?id=${report}`' class='report-link'>Report - {{ report }}</a>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </article>
|
|
|
- <hr />
|
|
|
- <h5 class='has-text-centered'>Spotify Information</h5>
|
|
|
- <label class='label'>Song title</label>
|
|
|
- <p class='control'>
|
|
|
- <input class='input' type='text' v-model='spotify.title'>
|
|
|
- </p>
|
|
|
- <label class='label'>Song artist (1 artist full name)</label>
|
|
|
- <p class='control'>
|
|
|
- <input class='input' type='text' v-model='spotify.artist'>
|
|
|
- </p>
|
|
|
- <button class='button is-success' @click='getSpotifySongs()'>
|
|
|
- Get Spotify songs
|
|
|
- </button>
|
|
|
- <hr />
|
|
|
- <article class="media" v-for='song in spotify.songs'>
|
|
|
- <figure class="media-left">
|
|
|
- <p class="image is-64x64">
|
|
|
- <img :src="song.thumbnail" onerror="this.src='/assets/notes-transparent.png'">
|
|
|
- </p>
|
|
|
- </figure>
|
|
|
- <div class="media-content">
|
|
|
- <div class="content">
|
|
|
- <p>
|
|
|
- <strong>{{song.title}}</strong>
|
|
|
- <br />
|
|
|
- <small>Artists: {{song.artists}}</small>, <small>Duration: {{song.duration}}</small>, <small>Explicit: {{song.explicit}}</small>
|
|
|
- <br />
|
|
|
- <small>Thumbnail: {{song.thumbnail}}</small>
|
|
|
- </p>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </article>
|
|
|
- </div>
|
|
|
- <div slot='footer'>
|
|
|
- <button class='button is-success' @click='save(editing.song, false)'>
|
|
|
- <i class='material-icons save-changes'>done</i>
|
|
|
- <span> Save</span>
|
|
|
- </button>
|
|
|
- <button class='button is-success' @click='save(editing.song, true)'>
|
|
|
- <i class='material-icons save-changes'>done</i>
|
|
|
- <span> Save and close</span>
|
|
|
- </button>
|
|
|
- <button class='button is-danger' @click='$parent.toggleModal()'>
|
|
|
- <span> Close</span>
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </modal>
|
|
|
- </div>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script>
|
|
|
- import io from '../../io';
|
|
|
- import validation from '../../validation';
|
|
|
- import { Toast } from 'vue-roaster';
|
|
|
- import Modal from './Modal.vue';
|
|
|
-
|
|
|
- export default {
|
|
|
- components: { Modal },
|
|
|
- data() {
|
|
|
- return {
|
|
|
- editing: {
|
|
|
- index: 0,
|
|
|
- song: {},
|
|
|
- type: ''
|
|
|
- },
|
|
|
- reports: 0,
|
|
|
- video: {
|
|
|
- player: null,
|
|
|
- paused: true,
|
|
|
- playerReady: false,
|
|
|
- autoPlayed: false
|
|
|
- },
|
|
|
- spotify: {
|
|
|
- title: '',
|
|
|
- artist: '',
|
|
|
- songs: []
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- methods: {
|
|
|
- save: function (song, close) {
|
|
|
- let _this = this;
|
|
|
-
|
|
|
- if (!song.title) return Toast.methods.addToast('Please fill in all fields', 8000);
|
|
|
- if (!song.thumbnail) return Toast.methods.addToast('Please fill in all fields', 8000);
|
|
|
-
|
|
|
-
|
|
|
- // Title
|
|
|
- if (!validation.isLength(song.title, 1, 64)) return Toast.methods.addToast('Title must have between 1 and 64 characters.', 8000);
|
|
|
- if (!validation.regex.ascii.test(song.title)) return Toast.methods.addToast('Invalid title format. Only ascii characters are allowed.', 8000);
|
|
|
-
|
|
|
-
|
|
|
- // Artists
|
|
|
- if (song.artists.length < 1 || song.artists.length > 10) return Toast.methods.addToast('Invalid artists. You must have at least 1 artist and a maximum of 10 artists.', 8000);
|
|
|
- let error;
|
|
|
- song.artists.forEach((artist) => {
|
|
|
- if (!validation.isLength(artist, 1, 32)) return error = 'Artist must have between 1 and 32 characters.';
|
|
|
- if (!validation.regex.ascii.test(artist)) return error = 'Invalid artist format. Only ascii characters are allowed.';
|
|
|
- if (artist === 'NONE') return error = 'Invalid artist format. Artists are not allowed to be named "NONE".';
|
|
|
- });
|
|
|
- if (error) return Toast.methods.addToast(error, 8000);
|
|
|
-
|
|
|
-
|
|
|
- // Genres
|
|
|
- error = undefined;
|
|
|
- song.genres.forEach((genre) => {
|
|
|
- if (!validation.isLength(genre, 1, 16)) return error = 'Genre must have between 1 and 16 characters.';
|
|
|
- if (!validation.regex.az09_.test(genre)) return error = 'Invalid genre format. Only ascii characters are allowed.';
|
|
|
- });
|
|
|
- if (error) return Toast.methods.addToast(error, 8000);
|
|
|
-
|
|
|
-
|
|
|
- // Thumbnail
|
|
|
- if (!validation.isLength(song.thumbnail, 8, 256)) return Toast.methods.addToast('Thumbnail must have between 8 and 256 characters.', 8000);
|
|
|
- if (song.thumbnail.indexOf('https://') !== 0) return Toast.methods.addToast('Thumbnail must start with "https://".', 8000);
|
|
|
-
|
|
|
-
|
|
|
- this.socket.emit(`${_this.editing.type}.update`, song._id, song, res => {
|
|
|
- Toast.methods.addToast(res.message, 4000);
|
|
|
- if (res.status === 'success') {
|
|
|
- _this.$parent.songs.forEach(lSong => {
|
|
|
- if (song._id === lSong._id) {
|
|
|
- for (let n in song) {
|
|
|
- lSong[n] = song[n];
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
- if (close) _this.$parent.toggleModal();
|
|
|
- });
|
|
|
- },
|
|
|
- settings: function (type) {
|
|
|
- let _this = this;
|
|
|
- switch(type) {
|
|
|
- case 'stop':
|
|
|
- _this.video.player.stopVideo();
|
|
|
- _this.video.paused = true;
|
|
|
- break;
|
|
|
- case 'pause':
|
|
|
- _this.video.player.pauseVideo();
|
|
|
- _this.video.paused = true;
|
|
|
- break;
|
|
|
- case 'play':
|
|
|
- _this.video.player.playVideo();
|
|
|
- _this.video.paused = false;
|
|
|
- break;
|
|
|
- case 'skipToLast10Secs':
|
|
|
- _this.video.player.seekTo((_this.editing.song.duration - 10) + _this.editing.song.skipDuration);
|
|
|
- break;
|
|
|
- }
|
|
|
- },
|
|
|
- changeVolume: function () {
|
|
|
- let local = this;
|
|
|
- let volume = $("#volumeSlider").val();
|
|
|
- localStorage.setItem("volume", volume);
|
|
|
- local.video.player.setVolume(volume);
|
|
|
- if (volume > 0) local.video.player.unMute();
|
|
|
- },
|
|
|
- addTag: function (type) {
|
|
|
- if (type == 'genres') {
|
|
|
- let genre = $('#new-genre').val().toLowerCase().trim();
|
|
|
- if (this.editing.song.genres.indexOf(genre) !== -1) return Toast.methods.addToast('Genre already exists', 3000);
|
|
|
- if (genre) {
|
|
|
- this.editing.song.genres.push(genre);
|
|
|
- $('#new-genre').val('');
|
|
|
- } else Toast.methods.addToast('Genre cannot be empty', 3000);
|
|
|
- } else if (type == 'artists') {
|
|
|
- let artist = $('#new-artist').val();
|
|
|
- if (this.editing.song.artists.indexOf(artist) !== -1) return Toast.methods.addToast('Artist already exists', 3000);
|
|
|
- if ($('#new-artist').val() !== '') {
|
|
|
- this.editing.song.artists.push(artist);
|
|
|
- $('#new-artist').val('');
|
|
|
- } else Toast.methods.addToast('Artist cannot be empty', 3000);
|
|
|
- }
|
|
|
- },
|
|
|
- removeTag: function (type, index) {
|
|
|
- if (type == 'genres') this.editing.song.genres.splice(index, 1);
|
|
|
- else if (type == 'artists') this.editing.song.artists.splice(index, 1);
|
|
|
- },
|
|
|
- getSpotifySongs: function() {
|
|
|
- this.socket.emit('apis.getSpotifySongs', this.spotify.title, this.spotify.artist, (res) => {
|
|
|
- if (res.status === 'success') {
|
|
|
- Toast.methods.addToast(`Succesfully got ${res.songs.length} song${(res.songs.length !== 1) ? 's' : ''}.`, 3000);
|
|
|
- this.spotify.songs = res.songs;
|
|
|
- } else Toast.methods.addToast(`Failed to get songs. ${res.message}`, 3000);
|
|
|
- });
|
|
|
- }
|
|
|
- },
|
|
|
- ready: function () {
|
|
|
-
|
|
|
- let _this = this;
|
|
|
-
|
|
|
- io.getSocket(socket => {
|
|
|
- _this.socket = socket;
|
|
|
- });
|
|
|
-
|
|
|
- setInterval(() => {
|
|
|
- if (_this.video.paused === false && _this.playerReady && _this.video.player.getCurrentTime() - _this.editing.song.skipDuration > _this.editing.song.duration) {
|
|
|
- _this.video.paused = false;
|
|
|
- _this.video.player.stopVideo();
|
|
|
- }
|
|
|
- }, 200);
|
|
|
-
|
|
|
- this.video.player = new YT.Player('player', {
|
|
|
- height: 315,
|
|
|
- width: 560,
|
|
|
- videoId: this.editing.song.songId,
|
|
|
- playerVars: { controls: 0, iv_load_policy: 3, rel: 0, showinfo: 0, autoplay: 1 },
|
|
|
- startSeconds: _this.editing.song.skipDuration,
|
|
|
- events: {
|
|
|
- 'onReady': () => {
|
|
|
- let volume = parseInt(localStorage.getItem("volume"));
|
|
|
- volume = (typeof volume === "number") ? volume : 20;
|
|
|
- _this.video.player.seekTo(_this.editing.song.skipDuration);
|
|
|
- _this.video.player.setVolume(volume);
|
|
|
- if (volume > 0) _this.video.player.unMute();
|
|
|
- _this.playerReady = true;
|
|
|
- },
|
|
|
- 'onStateChange': event => {
|
|
|
- if (event.data === 1) {
|
|
|
- if (!_this.video.autoPlayed) {
|
|
|
- _this.video.autoPlayed = true;
|
|
|
- return _this.video.player.stopVideo();
|
|
|
- }
|
|
|
-
|
|
|
- _this.video.paused = false;
|
|
|
- let youtubeDuration = _this.video.player.getDuration();
|
|
|
- youtubeDuration -= _this.editing.song.skipDuration;
|
|
|
- if (_this.editing.song.duration > youtubeDuration) {
|
|
|
- this.video.player.stopVideo();
|
|
|
- _this.video.paused = true;
|
|
|
- Toast.methods.addToast("Video can't play. Specified duration is bigger than the YouTube song duration.", 4000);
|
|
|
- } else if (_this.editing.song.duration <= 0) {
|
|
|
- this.video.player.stopVideo();
|
|
|
- _this.video.paused = true;
|
|
|
- Toast.methods.addToast("Video can't play. Specified duration has to be more than 0 seconds.", 4000);
|
|
|
- }
|
|
|
-
|
|
|
- if (_this.video.player.getCurrentTime() < _this.editing.song.skipDuration) {
|
|
|
- _this.video.player.seekTo(10);
|
|
|
- }
|
|
|
- } else if (event.data === 2) {
|
|
|
- this.video.paused = true;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- let volume = parseInt(localStorage.getItem("volume"));
|
|
|
- volume = (typeof volume === "number") ? volume : 20;
|
|
|
- $("#volumeSlider").val(volume);
|
|
|
-
|
|
|
- },
|
|
|
- events: {
|
|
|
- closeModal: function () {
|
|
|
- this.$parent.modals.editSong = false;
|
|
|
- this.video.player.stopVideo();
|
|
|
- },
|
|
|
- editSong: function (song, index, type) {
|
|
|
- let _this = this;
|
|
|
- this.video.player.loadVideoById(song.songId, this.editing.song.skipDuration);
|
|
|
- let newSong = {};
|
|
|
- for (let n in song) {
|
|
|
- newSong[n] = song[n];
|
|
|
- }
|
|
|
- this.editing = {
|
|
|
- index,
|
|
|
- song: newSong,
|
|
|
- type
|
|
|
- };
|
|
|
- if (type === 'songs') {
|
|
|
- _this.socket.emit('reports.getReportsForSong', song.songId, res => {
|
|
|
- if (res.status === 'success') _this.reports = res.data;
|
|
|
- });
|
|
|
- }
|
|
|
- this.$parent.toggleModal();
|
|
|
- },
|
|
|
- stopVideo: function () {
|
|
|
- this.video.player.stopVideo();
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-</script>
|
|
|
-
|
|
|
-<style type='scss' scoped>
|
|
|
- input[type=range] {
|
|
|
- -webkit-appearance: none;
|
|
|
- width: 100%;
|
|
|
- margin: 7.3px 0;
|
|
|
- }
|
|
|
-
|
|
|
- input[type=range]:focus {
|
|
|
- outline: none;
|
|
|
- }
|
|
|
-
|
|
|
- input[type=range]::-webkit-slider-runnable-track {
|
|
|
- width: 100%;
|
|
|
- height: 5.2px;
|
|
|
- cursor: pointer;
|
|
|
- box-shadow: 0;
|
|
|
- background: #c2c0c2;
|
|
|
- border-radius: 0;
|
|
|
- border: 0;
|
|
|
- }
|
|
|
-
|
|
|
- input[type=range]::-webkit-slider-thumb {
|
|
|
- box-shadow: 0;
|
|
|
- border: 0;
|
|
|
- height: 19px;
|
|
|
- width: 19px;
|
|
|
- border-radius: 15px;
|
|
|
- background: #03a9f4;
|
|
|
- cursor: pointer;
|
|
|
- -webkit-appearance: none;
|
|
|
- margin-top: -6.5px;
|
|
|
- }
|
|
|
-
|
|
|
- input[type=range]::-moz-range-track {
|
|
|
- width: 100%;
|
|
|
- height: 5.2px;
|
|
|
- cursor: pointer;
|
|
|
- box-shadow: 0;
|
|
|
- background: #c2c0c2;
|
|
|
- border-radius: 0;
|
|
|
- border: 0;
|
|
|
- }
|
|
|
-
|
|
|
- input[type=range]::-moz-range-thumb {
|
|
|
- box-shadow: 0;
|
|
|
- border: 0;
|
|
|
- height: 19px;
|
|
|
- width: 19px;
|
|
|
- border-radius: 15px;
|
|
|
- background: #03a9f4;
|
|
|
- cursor: pointer;
|
|
|
- -webkit-appearance: none;
|
|
|
- margin-top: -6.5px;
|
|
|
- }
|
|
|
-
|
|
|
- input[type=range]::-ms-track {
|
|
|
- width: 100%;
|
|
|
- height: 5.2px;
|
|
|
- cursor: pointer;
|
|
|
- box-shadow: 0;
|
|
|
- background: #c2c0c2;
|
|
|
- border-radius: 1.3px;
|
|
|
- }
|
|
|
-
|
|
|
- input[type=range]::-ms-fill-lower {
|
|
|
- background: #c2c0c2;
|
|
|
- border: 0;
|
|
|
- border-radius: 0;
|
|
|
- box-shadow: 0;
|
|
|
- }
|
|
|
-
|
|
|
- input[type=range]::-ms-fill-upper {
|
|
|
- background: #c2c0c2;
|
|
|
- border: 0;
|
|
|
- border-radius: 0;
|
|
|
- box-shadow: 0;
|
|
|
- }
|
|
|
-
|
|
|
- input[type=range]::-ms-thumb {
|
|
|
- box-shadow: 0;
|
|
|
- border: 0;
|
|
|
- height: 15px;
|
|
|
- width: 15px;
|
|
|
- border-radius: 15px;
|
|
|
- background: #03a9f4;
|
|
|
- cursor: pointer;
|
|
|
- -webkit-appearance: none;
|
|
|
- margin-top: 1.5px;
|
|
|
- }
|
|
|
-
|
|
|
- .controls {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- align-items: center;
|
|
|
- }
|
|
|
-
|
|
|
- .artist-genres {
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- }
|
|
|
-
|
|
|
- #volumeSlider { margin-bottom: 15px; }
|
|
|
-
|
|
|
- .has-text-centered { padding: 10px; }
|
|
|
-
|
|
|
- .thumbnail-preview {
|
|
|
- display: flex;
|
|
|
- margin: 0 auto 25px auto;
|
|
|
- max-width: 200px;
|
|
|
- width: 100%;
|
|
|
- }
|
|
|
-
|
|
|
- .modal-card-body, .modal-card-foot { border-top: 0; }
|
|
|
-
|
|
|
- .label, .checkbox, h5 {
|
|
|
- font-weight: normal;
|
|
|
- }
|
|
|
-
|
|
|
- .video-container {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- align-items: center;
|
|
|
- padding: 10px;
|
|
|
-
|
|
|
- iframe { pointer-events: none; }
|
|
|
- }
|
|
|
-
|
|
|
- .save-changes { color: #fff; }
|
|
|
-
|
|
|
- .tag:not(:last-child) { margin-right: 5px; }
|
|
|
-
|
|
|
- .reports-length {
|
|
|
- color: #03A9F4;
|
|
|
- font-weight: bold;
|
|
|
- display: flex;
|
|
|
- justify-content: center;
|
|
|
- }
|
|
|
-
|
|
|
- .report-link {
|
|
|
- color: #000;
|
|
|
- }
|
|
|
-</style>
|