Edit.vue 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. <template>
  2. <modal title='Edit Playlist'>
  3. <div slot='body'>
  4. <nav class="level">
  5. <div class="level-item has-text-centered">
  6. <div>
  7. <p class="heading">Total Length</p>
  8. <p class="title">{{ formatTime(totalLength) }}</p>
  9. </div>
  10. </div>
  11. </nav>
  12. <aside class='menu' v-if='playlist.songs && playlist.songs.length > 0'>
  13. <ul class='menu-list'>
  14. <li v-for='song in playlist.songs' track-by='$index'>
  15. <a :href='' target='_blank'>{{ song.title }}</a>
  16. <div class='controls'>
  17. <a href='#' @click='promoteSong(song.songId)'>
  18. <i class='material-icons' v-if='$index > 0'>keyboard_arrow_up</i>
  19. <i class='material-icons' style='opacity: 0' v-else>error</i>
  20. </a>
  21. <a href='#' @click='demoteSong(song.songId)'>
  22. <i class='material-icons' v-if='playlist.songs.length - 1 !== $index'>keyboard_arrow_down</i>
  23. <i class='material-icons' style='opacity: 0' v-else>error</i>
  24. </a>
  25. <a href='#' @click='removeSongFromPlaylist(song.songId)'><i class='material-icons'>delete</i></a>
  26. </div>
  27. </li>
  28. </ul>
  29. <br />
  30. </aside>
  31. <div class='control is-grouped'>
  32. <p class='control is-expanded'>
  33. <input class='input' type='text' placeholder='Search for Song to add' v-model='songQuery' autofocus @keyup.enter='searchForSongs()'>
  34. </p>
  35. <p class='control'>
  36. <a class='button is-info' @click='searchForSongs()' href="#">Search</a>
  37. </p>
  38. </div>
  39. <table class='table' v-if='songQueryResults.length > 0'>
  40. <tbody>
  41. <tr v-for='result in songQueryResults'>
  42. <td>
  43. <img :src='result.thumbnail' />
  44. </td>
  45. <td>{{ result.title }}</td>
  46. <td>
  47. <a class='button is-success' @click='addSongToPlaylist(result.id)' href='#'>
  48. Add
  49. </a>
  50. </td>
  51. </tr>
  52. </tbody>
  53. </table>
  54. <div class='control is-grouped'>
  55. <p class='control is-expanded'>
  56. <input class='input' type='text' placeholder='YouTube Playlist URL' v-model='importQuery' @keyup.enter="importPlaylist()">
  57. </p>
  58. <p class='control'>
  59. <a class='button is-info' @click='importPlaylist()' href="#">Import</a>
  60. </p>
  61. </div>
  62. <h5>Edit playlist details:</h5>
  63. <div class='control is-grouped'>
  64. <p class='control is-expanded'>
  65. <input class='input' type='text' placeholder='Playlist Display Name' v-model='playlist.displayName' @keyup.enter="renamePlaylist()">
  66. </p>
  67. <p class='control'>
  68. <a class='button is-info' @click='renamePlaylist()' href="#">Rename</a>
  69. </p>
  70. </div>
  71. </div>
  72. <div slot='footer'>
  73. <a class='button is-danger' @click='removePlaylist()' href="#">Remove Playlist</a>
  74. </div>
  75. </modal>
  76. </template>
  77. <script>
  78. import { Toast } from 'vue-roaster';
  79. import Modal from '../Modal.vue';
  80. import io from '../../../io';
  81. import validation from '../../../validation';
  82. export default {
  83. components: { Modal },
  84. data() {
  85. return {
  86. playlist: {},
  87. totalLength: 0,
  88. songQueryResults: [],
  89. songQuery: '',
  90. importQuery: ''
  91. }
  92. },
  93. methods: {
  94. formatTime: function () {
  95. let duration = moment.duration(this.totalLength, 'seconds');
  96. if (this.totalLength < 0) return '0 minutes';
  97. return ((duration.hours() > 0) ? (duration.hours() < 10 ? ('0' + duration.hours() + ' hours ') : (duration.hours() + ' hours ')) : '') + (duration.minutes() + ' minutes ') + (duration.seconds() > 0 ? (duration.seconds() < 10 ? ('0' + duration.seconds() + ' seconds') : duration.seconds() + ' seconds') : (duration.seconds() + ' seconds'));
  98. },
  99. searchForSongs: function () {
  100. let _this = this;
  101. let query = _this.songQuery;
  102. if (query.indexOf('&index=') !== -1) {
  103. query = query.split('&index=');
  104. query.pop();
  105. query = query.join('');
  106. }
  107. if (query.indexOf('&list=') !== -1) {
  108. query = query.split('&list=');
  109. query.pop();
  110. query = query.join('');
  111. }
  112. _this.socket.emit('apis.searchYoutube', query, res => {
  113. if (res.status == 'success') {
  114. _this.songQueryResults = [];
  115. for (let i = 0; i < res.data.items.length; i++) {
  116. _this.songQueryResults.push({
  117. id: res.data.items[i].id.videoId,
  118. url: `https://www.youtube.com/watch?v=${this.id}`,
  119. title: res.data.items[i].snippet.title,
  120. thumbnail: res.data.items[i].snippet.thumbnails.default.url
  121. });
  122. }
  123. } else if (res.status === 'error') Toast.methods.addToast(res.message, 3000);
  124. });
  125. },
  126. addSongToPlaylist: function (id) {
  127. let _this = this;
  128. _this.socket.emit('playlists.addSongToPlaylist', id, _this.playlist._id, res => {
  129. Toast.methods.addToast(res.message, 4000);
  130. });
  131. },
  132. importPlaylist: function () {
  133. let _this = this;
  134. Toast.methods.addToast('Starting to import your playlist. This can take some time to do.', 4000);
  135. this.socket.emit('playlists.addSetToPlaylist', _this.importQuery, _this.playlist._id, res => {
  136. if (res.status === 'success') _this.playlist.songs = res.data;
  137. Toast.methods.addToast(res.message, 4000);
  138. });
  139. },
  140. removeSongFromPlaylist: function (id) {
  141. let _this = this;
  142. this.socket.emit('playlists.removeSongFromPlaylist', id, _this.playlist._id, res => {
  143. Toast.methods.addToast(res.message, 4000);
  144. });
  145. },
  146. renamePlaylist: function () {
  147. const displayName = this.playlist.displayName;
  148. if (!validation.isLength(displayName, 2, 32)) return Toast.methods.addToast('Display name must have between 2 and 32 characters.', 8000);
  149. if (!validation.regex.azAZ09_.test(displayName)) return Toast.methods.addToast('Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.', 8000);
  150. this.socket.emit('playlists.updateDisplayName', this.playlist._id, this.playlist.displayName, res => {
  151. Toast.methods.addToast(res.message, 4000);
  152. });
  153. },
  154. removePlaylist: function () {
  155. let _this = this;
  156. _this.socket.emit('playlists.remove', _this.playlist._id, res => {
  157. Toast.methods.addToast(res.message, 3000);
  158. if (res.status === 'success') {
  159. _this.$parent.modals.editPlaylist = !_this.$parent.modals.editPlaylist;
  160. }
  161. });
  162. },
  163. promoteSong: function (songId) {
  164. let _this = this;
  165. _this.socket.emit('playlists.moveSongToTop', _this.playlist._id, songId, res => {
  166. Toast.methods.addToast(res.message, 4000);
  167. });
  168. },
  169. demoteSong: function (songId) {
  170. let _this = this;
  171. _this.socket.emit('playlists.moveSongToBottom', _this.playlist._id, songId, res => {
  172. Toast.methods.addToast(res.message, 4000);
  173. });
  174. }
  175. },
  176. ready: function () {
  177. let _this = this;
  178. io.getSocket((socket) => {
  179. _this.socket = socket;
  180. _this.socket.emit('playlists.getPlaylist', _this.$parent.playlistBeingEdited, res => {
  181. if (res.status === 'success') _this.playlist = res.data; _this.playlist.oldId = res.data._id;
  182. });
  183. _this.socket.emit('playlists.totalLength', _this.$parent.playlistBeingEdited, res => {
  184. if (res.status === 'success') _this.totalLength = res.data;
  185. });
  186. _this.socket.on('event:playlist.updateTotalLength', res => {
  187. if (res.status === 'success') _this.totalLength = res.data;
  188. });
  189. _this.socket.on('event:playlist.addSong', data => {
  190. if (_this.playlist._id === data.playlistId) _this.playlist.songs.push(data.song);
  191. });
  192. _this.socket.on('event:playlist.removeSong', data => {
  193. if (_this.playlist._id === data.playlistId) {
  194. _this.playlist.songs.forEach((song, index) => {
  195. if (song.songId === data.songId) _this.playlist.songs.splice(index, 1);
  196. });
  197. }
  198. });
  199. _this.socket.on('event:playlist.updateDisplayName', data => {
  200. if (_this.playlist._id === data.playlistId) _this.playlist.displayName = data.displayName;
  201. });
  202. _this.socket.on('event:playlist.moveSongToBottom', data => {
  203. if (_this.playlist._id === data.playlistId) {
  204. let songIndex;
  205. _this.playlist.songs.forEach((song, index) => {
  206. if (song.songId === data.songId) songIndex = index;
  207. });
  208. let song = _this.playlist.songs.splice(songIndex, 1)[0];
  209. _this.playlist.songs.push(song);
  210. }
  211. });
  212. _this.socket.on('event:playlist.moveSongToTop', (data) => {
  213. if (_this.playlist._id === data.playlistId) {
  214. let songIndex;
  215. _this.playlist.songs.forEach((song, index) => {
  216. if (song.songId === data.songId) songIndex = index;
  217. });
  218. let song = _this.playlist.songs.splice(songIndex, 1)[0];
  219. _this.playlist.songs.unshift(song);
  220. }
  221. });
  222. });
  223. },
  224. events: {
  225. closeModal: function() {
  226. this.$parent.modals.editPlaylist = !this.$parent.modals.editPlaylist;
  227. }
  228. }
  229. }
  230. </script>
  231. <style type='scss' scoped>
  232. .menu { padding: 0 20px; }
  233. .menu-list li {
  234. display: flex;
  235. justify-content: space-between;
  236. }
  237. .menu-list a:hover { color: #000 !important; }
  238. li a {
  239. display: flex;
  240. align-items: center;
  241. }
  242. .controls {
  243. display: flex;
  244. a {
  245. display: flex;
  246. align-items: center;
  247. }
  248. }
  249. .table {
  250. margin-bottom: 0;
  251. }
  252. h5 { padding: 20px 0; }
  253. </style>