EditSong.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  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.songId'>
  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 Information</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 />
  110. <article class="media" v-for='song in spotify.songs'>
  111. <figure class="media-left">
  112. <p class="image is-64x64">
  113. <img :src="song.thumbnail" onerror="this.src='/assets/notes-transparent.png'">
  114. </p>
  115. </figure>
  116. <div class="media-content">
  117. <div class="content">
  118. <p>
  119. <strong>{{song.title}}</strong>
  120. <br />
  121. <small>Artists: {{song.artists}}</small>, <small>Duration: {{song.duration}}</small>, <small>Explicit: {{song.explicit}}</small>
  122. <br />
  123. <small>Thumbnail: {{song.thumbnail}}</small>
  124. </p>
  125. </div>
  126. </div>
  127. </article>
  128. </div>
  129. <div slot='footer'>
  130. <button class='button is-success' @click='save(editing.song, false)'>
  131. <i class='material-icons save-changes'>done</i>
  132. <span>&nbsp;Save</span>
  133. </button>
  134. <button class='button is-success' @click='save(editing.song, true)'>
  135. <i class='material-icons save-changes'>done</i>
  136. <span>&nbsp;Save and close</span>
  137. </button>
  138. <button class='button is-danger' @click='$parent.toggleModal()'>
  139. <span>&nbsp;Close</span>
  140. </button>
  141. </div>
  142. </modal>
  143. </div>
  144. </template>
  145. <script>
  146. import io from '../../io';
  147. import { Toast } from 'vue-roaster';
  148. import Modal from './Modal.vue';
  149. export default {
  150. components: { Modal },
  151. data() {
  152. return {
  153. editing: {
  154. index: 0,
  155. song: {},
  156. type: ''
  157. },
  158. reports: 0,
  159. video: {
  160. player: null,
  161. paused: false,
  162. playerReady: false
  163. },
  164. spotify: {
  165. title: '',
  166. artist: '',
  167. songs: []
  168. }
  169. }
  170. },
  171. methods: {
  172. save: function (song, close) {
  173. let _this = this;
  174. this.socket.emit(`${_this.editing.type}.update`, song._id, song, res => {
  175. Toast.methods.addToast(res.message, 4000);
  176. if (res.status === 'success') {
  177. _this.$parent.songs.forEach(lSong => {
  178. if (song._id === lSong._id) {
  179. for (let n in song) {
  180. lSong[n] = song[n];
  181. }
  182. }
  183. });
  184. }
  185. if (close) _this.$parent.toggleModal();
  186. });
  187. },
  188. settings: function (type) {
  189. let _this = this;
  190. switch(type) {
  191. case 'stop':
  192. _this.video.player.stopVideo();
  193. _this.video.paused = true;
  194. break;
  195. case 'pause':
  196. _this.video.player.pauseVideo();
  197. _this.video.paused = true;
  198. break;
  199. case 'play':
  200. _this.video.player.playVideo();
  201. _this.video.paused = false;
  202. break;
  203. case 'skipToLast10Secs':
  204. _this.video.player.seekTo((_this.editing.song.duration - 10) + _this.editing.song.skipDuration);
  205. break;
  206. }
  207. },
  208. changeVolume: function () {
  209. let local = this;
  210. let volume = $("#volumeSlider").val();
  211. localStorage.setItem("volume", volume);
  212. local.video.player.setVolume(volume);
  213. if (volume > 0) local.video.player.unMute();
  214. },
  215. addTag: function (type) {
  216. if (type == 'genres') {
  217. let genre = $('#new-genre').val().toLowerCase().trim();
  218. if (this.editing.song.genres.indexOf(genre) !== -1) return Toast.methods.addToast('Genre already exists', 3000);
  219. if (genre) {
  220. this.editing.song.genres.push(genre);
  221. $('#new-genre').val('');
  222. } else Toast.methods.addToast('Genre cannot be empty', 3000);
  223. } else if (type == 'artists') {
  224. let artist = $('#new-artist').val();
  225. if (this.editing.song.artists.indexOf(artist) !== -1) return Toast.methods.addToast('Artist already exists', 3000);
  226. if ($('#new-artist').val() !== '') {
  227. this.editing.song.artists.push(artist);
  228. $('#new-artist').val('');
  229. } else Toast.methods.addToast('Artist cannot be empty', 3000);
  230. }
  231. },
  232. removeTag: function (type, index) {
  233. if (type == 'genres') this.editing.song.genres.splice(index, 1);
  234. else if (type == 'artists') this.editing.song.artists.splice(index, 1);
  235. },
  236. getSpotifySongs: function() {
  237. this.socket.emit('apis.getSpotifySongs', this.spotify.title, this.spotify.artist, (res) => {
  238. if (res.status === 'success') {
  239. this.spotify.songs = res.songs;
  240. }
  241. });
  242. }
  243. },
  244. ready: function () {
  245. let _this = this;
  246. io.getSocket(socket => {
  247. _this.socket = socket;
  248. });
  249. setInterval(() => {
  250. if (_this.video.paused === false && _this.playerReady && _this.video.player.getCurrentTime() - _this.editing.song.skipDuration > _this.editing.song.duration) {
  251. _this.video.paused = false;
  252. _this.video.player.stopVideo();
  253. }
  254. }, 200);
  255. this.video.player = new YT.Player('player', {
  256. height: 315,
  257. width: 560,
  258. videoId: this.editing.song.songId,
  259. playerVars: { controls: 0, iv_load_policy: 3, rel: 0, showinfo: 0 },
  260. startSeconds: _this.editing.song.skipDuration,
  261. events: {
  262. 'onReady': () => {
  263. let volume = parseInt(localStorage.getItem("volume"));
  264. volume = (typeof volume === "number") ? volume : 20;
  265. _this.video.player.seekTo(_this.editing.song.skipDuration);
  266. _this.video.player.setVolume(volume);
  267. if (volume > 0) _this.video.player.unMute();
  268. _this.playerReady = true;
  269. },
  270. 'onStateChange': event => {
  271. if (event.data === 1) {
  272. _this.video.paused = false;
  273. let youtubeDuration = _this.video.player.getDuration();
  274. youtubeDuration -= _this.editing.song.skipDuration;
  275. if (_this.editing.song.duration > youtubeDuration) {
  276. this.video.player.stopVideo();
  277. _this.video.paused = true;
  278. Toast.methods.addToast("Video can't play. Specified duration is bigger than the YouTube song duration.", 4000);
  279. } else if (_this.editing.song.duration <= 0) {
  280. this.video.player.stopVideo();
  281. _this.video.paused = true;
  282. Toast.methods.addToast("Video can't play. Specified duration has to be more than 0 seconds.", 4000);
  283. }
  284. if (_this.video.player.getCurrentTime() < _this.editing.song.skipDuration) {
  285. _this.video.player.seekTo(10);
  286. }
  287. } else if (event.data === 2) {
  288. this.video.paused = true;
  289. }
  290. }
  291. }
  292. });
  293. let volume = parseInt(localStorage.getItem("volume"));
  294. volume = (typeof volume === "number") ? volume : 20;
  295. $("#volumeSlider").val(volume);
  296. },
  297. events: {
  298. closeModal: function () {
  299. this.$parent.modals.editSong = false;
  300. this.video.player.stopVideo();
  301. },
  302. editSong: function (song, index, type) {
  303. let _this = this;
  304. this.video.player.loadVideoById(song.songId, this.editing.song.skipDuration);
  305. let newSong = {};
  306. for (let n in song) {
  307. newSong[n] = song[n];
  308. }
  309. this.editing = {
  310. index,
  311. song: newSong,
  312. type
  313. };
  314. _this.socket.emit('reports.getReportsForSong', song._id, res => {
  315. if (res.status === 'success') _this.reports = res.data;
  316. });
  317. this.$parent.toggleModal();
  318. },
  319. stopVideo: function () {
  320. this.video.player.stopVideo();
  321. }
  322. }
  323. }
  324. </script>
  325. <style type='scss' scoped>
  326. input[type=range] {
  327. -webkit-appearance: none;
  328. width: 100%;
  329. margin: 7.3px 0;
  330. }
  331. input[type=range]:focus {
  332. outline: none;
  333. }
  334. input[type=range]::-webkit-slider-runnable-track {
  335. width: 100%;
  336. height: 5.2px;
  337. cursor: pointer;
  338. box-shadow: 0;
  339. background: #c2c0c2;
  340. border-radius: 0;
  341. border: 0;
  342. }
  343. input[type=range]::-webkit-slider-thumb {
  344. box-shadow: 0;
  345. border: 0;
  346. height: 19px;
  347. width: 19px;
  348. border-radius: 15px;
  349. background: #03a9f4;
  350. cursor: pointer;
  351. -webkit-appearance: none;
  352. margin-top: -6.5px;
  353. }
  354. input[type=range]::-moz-range-track {
  355. width: 100%;
  356. height: 5.2px;
  357. cursor: pointer;
  358. box-shadow: 0;
  359. background: #c2c0c2;
  360. border-radius: 0;
  361. border: 0;
  362. }
  363. input[type=range]::-moz-range-thumb {
  364. box-shadow: 0;
  365. border: 0;
  366. height: 19px;
  367. width: 19px;
  368. border-radius: 15px;
  369. background: #03a9f4;
  370. cursor: pointer;
  371. -webkit-appearance: none;
  372. margin-top: -6.5px;
  373. }
  374. input[type=range]::-ms-track {
  375. width: 100%;
  376. height: 5.2px;
  377. cursor: pointer;
  378. box-shadow: 0;
  379. background: #c2c0c2;
  380. border-radius: 1.3px;
  381. }
  382. input[type=range]::-ms-fill-lower {
  383. background: #c2c0c2;
  384. border: 0;
  385. border-radius: 0;
  386. box-shadow: 0;
  387. }
  388. input[type=range]::-ms-fill-upper {
  389. background: #c2c0c2;
  390. border: 0;
  391. border-radius: 0;
  392. box-shadow: 0;
  393. }
  394. input[type=range]::-ms-thumb {
  395. box-shadow: 0;
  396. border: 0;
  397. height: 15px;
  398. width: 15px;
  399. border-radius: 15px;
  400. background: #03a9f4;
  401. cursor: pointer;
  402. -webkit-appearance: none;
  403. margin-top: 1.5px;
  404. }
  405. .controls {
  406. display: flex;
  407. flex-direction: column;
  408. align-items: center;
  409. }
  410. .artist-genres {
  411. display: flex;
  412. justify-content: space-between;
  413. }
  414. #volumeSlider { margin-bottom: 15px; }
  415. .has-text-centered { padding: 10px; }
  416. .thumbnail-preview {
  417. display: flex;
  418. margin: 0 auto 25px auto;
  419. max-width: 200px;
  420. width: 100%;
  421. }
  422. .modal-card-body, .modal-card-foot { border-top: 0; }
  423. .label, .checkbox, h5 {
  424. font-weight: normal;
  425. }
  426. .video-container {
  427. display: flex;
  428. flex-direction: column;
  429. align-items: center;
  430. padding: 10px;
  431. iframe { pointer-events: none; }
  432. }
  433. .save-changes { color: #fff; }
  434. .tag:not(:last-child) { margin-right: 5px; }
  435. .reports {
  436. margin: 0 auto;
  437. display: flex;
  438. }
  439. </style>