EditSong.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. <template>
  2. <div>
  3. <modal title='Edit Song'>
  4. <div slot='body'>
  5. <span class="tag is-info is-medium reports" v-if="reports > 0">
  6. {{ reports }}
  7. <span v-if="reports > 1">&nbsp;Reports&nbsp;</span>
  8. <span v-else>&nbsp;Report&nbsp;</span>
  9. </span>
  10. <h5 class='has-text-centered'>Video Preview</h5>
  11. <div class='video-container'>
  12. <div id='player'></div>
  13. <div class="controls">
  14. <form action="#" class="column is-7-desktop is-4-mobile">
  15. <p style="margin-top: 0; position: relative;">
  16. <input type="range" id="volumeSlider" min="0" max="100" class="active" v-on:change="changeVolume()" v-on:input="changeVolume()">
  17. </p>
  18. </form>
  19. <p class='control has-addons'>
  20. <button class='button' @click='settings("pause")' v-if='!video.paused'>
  21. <i class='material-icons'>pause</i>
  22. </button>
  23. <button class='button' @click='settings("play")' v-if='video.paused'>
  24. <i class='material-icons'>play_arrow</i>
  25. </button>
  26. <button class='button' @click='settings("stop")'>
  27. <i class='material-icons'>stop</i>
  28. </button>
  29. <button class='button' @click='settings("skipToLast10Secs")'>
  30. <i class='material-icons'>fast_forward</i>
  31. </button>
  32. </p>
  33. </div>
  34. </div>
  35. <h5 class='has-text-centered'>Thumbnail Preview</h5>
  36. <img class='thumbnail-preview' :src='editing.song.thumbnail' onerror="this.src='/assets/notes-transparent.png'">
  37. <div class="control is-horizontal">
  38. <div class="control-label">
  39. <label class="label">Thumbnail URL</label>
  40. </div>
  41. <div class="control">
  42. <input class='input' type='text' v-model='editing.song.thumbnail'>
  43. </div>
  44. </div>
  45. <h5 class='has-text-centered'>Edit Info</h5>
  46. <p class='control'>
  47. <label class='checkbox'>
  48. <input type='checkbox' v-model='editing.song.explicit'>
  49. Explicit
  50. </label>
  51. </p>
  52. <label class='label'>Song ID & Title</label>
  53. <div class="control is-horizontal">
  54. <div class="control is-grouped">
  55. <p class='control is-expanded'>
  56. <input class='input' type='text' v-model='editing.song._id'>
  57. </p>
  58. <p class='control is-expanded'>
  59. <input class='input' type='text' v-model='editing.song.title' autofocus>
  60. </p>
  61. </div>
  62. </div>
  63. <label class='label'>Artists & Genres</label>
  64. <div class='control is-horizontal'>
  65. <div class='control is-grouped artist-genres'>
  66. <div>
  67. <p class='control has-addons'>
  68. <input class='input' id='new-artist' type='text' placeholder='Artist'>
  69. <button class='button is-info' @click='addTag("artists")'>Add Artist</button>
  70. </p>
  71. <span class='tag is-info' v-for='(index, artist) in editing.song.artists' track-by='$index'>
  72. {{ artist }}
  73. <button class='delete is-info' @click='removeTag("artists", index)'></button>
  74. </span>
  75. </div>
  76. <div>
  77. <p class='control has-addons'>
  78. <input class='input' id='new-genre' type='text' placeholder='Genre'>
  79. <button class='button is-info' @click='addTag("genres")'>Add Genre</button>
  80. </p>
  81. <span class='tag is-info' v-for='(index, genre) in editing.song.genres' track-by='$index'>
  82. {{ genre }}
  83. <button class='delete is-info' @click='removeTag("genres", index)'></button>
  84. </span>
  85. </div>
  86. </div>
  87. </div>
  88. <label class='label'>Song Duration</label>
  89. <p class='control'>
  90. <input class='input' type='text' v-model='editing.song.duration'>
  91. </p>
  92. <label class='label'>Skip Duration</label>
  93. <p class='control'>
  94. <input class='input' type='text' v-model='editing.song.skipDuration'>
  95. </p>
  96. <hr>
  97. <h5 class='has-text-centered'>Spotify info</h5>
  98. <label class='label'>Song title</label>
  99. <p class='control'>
  100. <input class='input' type='text' v-model='spotify.title'>
  101. </p>
  102. <label class='label'>Song artist (1 artist full name)</label>
  103. <p class='control'>
  104. <input class='input' type='text' v-model='spotify.artist'>
  105. </p>
  106. <button class='button is-success' @click='getSpotifySongs()'>
  107. Get Spotify songs
  108. </button>
  109. <hr v-if="spotify.songs.length > 0">
  110. <h5 class='has-text-centered' v-if="spotify.songs.length > 0">Spotify results</h5>
  111. <div v-for='song in spotify.songs'>
  112. <p><b>Title:</b> {{song.title}}</p>
  113. <p>
  114. <b>Artists:</b>
  115. <ul>
  116. <li v-for='artist in song.artists'>{{artist}}</li>
  117. </ul>
  118. </p>
  119. <p><b>Duration:</b> {{song.duration}}</p>
  120. <p><b>Explicit:</b> {{song.explicit}}</p>
  121. <p>
  122. <b>Thumbnail:</b> {{song.thumbnail}}
  123. <img :src='song.thumbnail' onerror="this.src='/assets/notes-transparent.png'">
  124. </p>
  125. <hr>
  126. </div>
  127. </div>
  128. <div slot='footer'>
  129. <button class='button is-success' @click='save(editing.song, false)'>
  130. <i class='material-icons save-changes'>done</i>
  131. <span>&nbsp;Save</span>
  132. </button>
  133. <button class='button is-success' @click='save(editing.song, true)'>
  134. <i class='material-icons save-changes'>done</i>
  135. <span>&nbsp;Save and close</span>
  136. </button>
  137. <button class='button is-danger' @click='$parent.toggleModal()'>
  138. <span>&nbsp;Close</span>
  139. </button>
  140. </div>
  141. </modal>
  142. </div>
  143. </template>
  144. <script>
  145. import io from '../../io';
  146. import { Toast } from 'vue-roaster';
  147. import Modal from './Modal.vue';
  148. export default {
  149. components: { Modal },
  150. data() {
  151. return {
  152. editing: {
  153. index: 0,
  154. song: {},
  155. type: ''
  156. },
  157. reports: 0,
  158. video: {
  159. player: null,
  160. paused: false,
  161. playerReady: false
  162. },
  163. spotify: {
  164. title: '',
  165. artist: '',
  166. songs: []
  167. }
  168. }
  169. },
  170. methods: {
  171. save: function (song, close) {
  172. let _this = this;
  173. this.socket.emit(`${_this.editing.type}.update`, song._id, song, res => {
  174. Toast.methods.addToast(res.message, 4000);
  175. if (res.status === 'success') {
  176. _this.$parent.songs.forEach(lSong => {
  177. if (song._id === lSong._id) {
  178. for (let n in song) {
  179. lSong[n] = song[n];
  180. }
  181. }
  182. });
  183. }
  184. if (close) _this.$parent.toggleModal();
  185. });
  186. },
  187. settings: function (type) {
  188. let _this = this;
  189. switch(type) {
  190. case 'stop':
  191. _this.video.player.stopVideo();
  192. _this.video.paused = true;
  193. break;
  194. case 'pause':
  195. _this.video.player.pauseVideo();
  196. _this.video.paused = true;
  197. break;
  198. case 'play':
  199. _this.video.player.playVideo();
  200. _this.video.paused = false;
  201. break;
  202. case 'skipToLast10Secs':
  203. _this.video.player.seekTo((_this.editing.song.duration - 10) + _this.editing.song.skipDuration);
  204. break;
  205. }
  206. },
  207. changeVolume: function () {
  208. let local = this;
  209. let volume = $("#volumeSlider").val();
  210. localStorage.setItem("volume", volume);
  211. local.video.player.setVolume(volume);
  212. if (volume > 0) local.video.player.unMute();
  213. },
  214. addTag: function (type) {
  215. if (type == 'genres') {
  216. let genre = $('#new-genre').val().toLowerCase().trim();
  217. if (this.editing.song.genres.indexOf(genre) !== -1) return Toast.methods.addToast('Genre already exists', 3000);
  218. if (genre) {
  219. this.editing.song.genres.push(genre);
  220. $('#new-genre').val('');
  221. } else Toast.methods.addToast('Genre cannot be empty', 3000);
  222. } else if (type == 'artists') {
  223. let artist = $('#new-artist').val();
  224. if (this.editing.song.artists.indexOf(artist) !== -1) return Toast.methods.addToast('Artist already exists', 3000);
  225. if ($('#new-artist').val() !== '') {
  226. this.editing.song.artists.push(artist);
  227. $('#new-artist').val('');
  228. } else Toast.methods.addToast('Artist cannot be empty', 3000);
  229. }
  230. },
  231. removeTag: function (type, index) {
  232. if (type == 'genres') this.editing.song.genres.splice(index, 1);
  233. else if (type == 'artists') this.editing.song.artists.splice(index, 1);
  234. },
  235. getSpotifySongs: function() {
  236. this.socket.emit('apis.getSpotifySongs', this.spotify.title, this.spotify.artist, (res) => {
  237. if (res.status === 'success') {
  238. this.spotify.songs = res.songs;
  239. }
  240. });
  241. }
  242. },
  243. ready: function () {
  244. let _this = this;
  245. io.getSocket(socket => {
  246. _this.socket = socket;
  247. });
  248. setInterval(() => {
  249. if (_this.video.paused === false && _this.playerReady && _this.video.player.getCurrentTime() - _this.editing.song.skipDuration > _this.editing.song.duration) {
  250. _this.video.paused = false;
  251. _this.video.player.stopVideo();
  252. }
  253. }, 200);
  254. this.video.player = new YT.Player('player', {
  255. height: 315,
  256. width: 560,
  257. videoId: this.editing.song._id,
  258. playerVars: { controls: 0, iv_load_policy: 3, rel: 0, showinfo: 0 },
  259. startSeconds: _this.editing.song.skipDuration,
  260. events: {
  261. 'onReady': () => {
  262. let volume = parseInt(localStorage.getItem("volume"));
  263. volume = (typeof volume === "number") ? volume : 20;
  264. _this.video.player.seekTo(_this.editing.song.skipDuration);
  265. _this.video.player.setVolume(volume);
  266. if (volume > 0) _this.video.player.unMute();
  267. _this.playerReady = true;
  268. },
  269. 'onStateChange': event => {
  270. if (event.data === 1) {
  271. _this.video.paused = false;
  272. let youtubeDuration = _this.video.player.getDuration();
  273. youtubeDuration -= _this.editing.song.skipDuration;
  274. if (_this.editing.song.duration > youtubeDuration) {
  275. this.video.player.stopVideo();
  276. _this.video.paused = true;
  277. Toast.methods.addToast("Video can't play. Specified duration is bigger than the YouTube song duration.", 4000);
  278. } else if (_this.editing.song.duration <= 0) {
  279. this.video.player.stopVideo();
  280. _this.video.paused = true;
  281. Toast.methods.addToast("Video can't play. Specified duration has to be more than 0 seconds.", 4000);
  282. }
  283. if (_this.video.player.getCurrentTime() < _this.editing.song.skipDuration) {
  284. _this.video.player.seekTo(10);
  285. }
  286. } else if (event.data === 2) {
  287. this.video.paused = true;
  288. }
  289. }
  290. }
  291. });
  292. let volume = parseInt(localStorage.getItem("volume"));
  293. volume = (typeof volume === "number") ? volume : 20;
  294. $("#volumeSlider").val(volume);
  295. },
  296. events: {
  297. closeModal: function () {
  298. this.$parent.modals.editSong = false;
  299. this.video.player.stopVideo();
  300. },
  301. editSong: function (song, index, type) {
  302. let _this = this;
  303. this.video.player.loadVideoById(song._id, this.editing.song.skipDuration);
  304. let newSong = {};
  305. for (let n in song) {
  306. newSong[n] = song[n];
  307. }
  308. this.editing = {
  309. index,
  310. song: newSong,
  311. type
  312. };
  313. _this.socket.emit('reports.getReportsForSong', song._id, res => {
  314. if (res.status === 'success') _this.reports = res.data;
  315. });
  316. this.$parent.toggleModal();
  317. },
  318. stopVideo: function () {
  319. this.video.player.stopVideo();
  320. }
  321. }
  322. }
  323. </script>
  324. <style type='scss' scoped>
  325. input[type=range] {
  326. -webkit-appearance: none;
  327. width: 100%;
  328. margin: 7.3px 0;
  329. }
  330. input[type=range]:focus {
  331. outline: none;
  332. }
  333. input[type=range]::-webkit-slider-runnable-track {
  334. width: 100%;
  335. height: 5.2px;
  336. cursor: pointer;
  337. box-shadow: 0;
  338. background: #c2c0c2;
  339. border-radius: 0;
  340. border: 0;
  341. }
  342. input[type=range]::-webkit-slider-thumb {
  343. box-shadow: 0;
  344. border: 0;
  345. height: 19px;
  346. width: 19px;
  347. border-radius: 15px;
  348. background: #03a9f4;
  349. cursor: pointer;
  350. -webkit-appearance: none;
  351. margin-top: -6.5px;
  352. }
  353. input[type=range]::-moz-range-track {
  354. width: 100%;
  355. height: 5.2px;
  356. cursor: pointer;
  357. box-shadow: 0;
  358. background: #c2c0c2;
  359. border-radius: 0;
  360. border: 0;
  361. }
  362. input[type=range]::-moz-range-thumb {
  363. box-shadow: 0;
  364. border: 0;
  365. height: 19px;
  366. width: 19px;
  367. border-radius: 15px;
  368. background: #03a9f4;
  369. cursor: pointer;
  370. -webkit-appearance: none;
  371. margin-top: -6.5px;
  372. }
  373. input[type=range]::-ms-track {
  374. width: 100%;
  375. height: 5.2px;
  376. cursor: pointer;
  377. box-shadow: 0;
  378. background: #c2c0c2;
  379. border-radius: 1.3px;
  380. }
  381. input[type=range]::-ms-fill-lower {
  382. background: #c2c0c2;
  383. border: 0;
  384. border-radius: 0;
  385. box-shadow: 0;
  386. }
  387. input[type=range]::-ms-fill-upper {
  388. background: #c2c0c2;
  389. border: 0;
  390. border-radius: 0;
  391. box-shadow: 0;
  392. }
  393. input[type=range]::-ms-thumb {
  394. box-shadow: 0;
  395. border: 0;
  396. height: 15px;
  397. width: 15px;
  398. border-radius: 15px;
  399. background: #03a9f4;
  400. cursor: pointer;
  401. -webkit-appearance: none;
  402. margin-top: 1.5px;
  403. }
  404. .controls {
  405. display: flex;
  406. flex-direction: column;
  407. align-items: center;
  408. }
  409. .artist-genres {
  410. display: flex;
  411. justify-content: space-between;
  412. }
  413. #volumeSlider { margin-bottom: 15px; }
  414. .has-text-centered { padding: 10px; }
  415. .thumbnail-preview {
  416. display: flex;
  417. margin: 0 auto 25px auto;
  418. max-width: 200px;
  419. width: 100%;
  420. }
  421. .modal-card-body, .modal-card-foot { border-top: 0; }
  422. .label, .checkbox, h5 {
  423. font-weight: normal;
  424. }
  425. .video-container {
  426. display: flex;
  427. flex-direction: column;
  428. align-items: center;
  429. padding: 10px;
  430. iframe { pointer-events: none; }
  431. }
  432. .save-changes { color: #fff; }
  433. .tag:not(:last-child) { margin-right: 5px; }
  434. .reports {
  435. margin: 0 auto;
  436. display: flex;
  437. }
  438. </style>