EditSong.vue 11 KB

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