Edit.vue 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  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">
  8. Total Length
  9. </p>
  10. <p class="title">
  11. {{ totalLength() }}
  12. </p>
  13. </div>
  14. </div>
  15. </nav>
  16. <hr />
  17. <aside
  18. v-if="playlist.songs && playlist.songs.length > 0"
  19. class="menu"
  20. >
  21. <ul class="menu-list">
  22. <li v-for="(song, index) in playlist.songs" :key="index">
  23. <a href="#" target="_blank">{{ song.title }}</a>
  24. <div class="controls">
  25. <a href="#" v-on:click="promoteSong(song.songId)">
  26. <i class="material-icons" v-if="$index > 0"
  27. >keyboard_arrow_up</i
  28. >
  29. <i
  30. v-else
  31. class="material-icons"
  32. style="opacity: 0"
  33. >error</i
  34. >
  35. </a>
  36. <a href="#" v-on:click="demoteSong(song.songId)">
  37. <i
  38. v-if="playlist.songs.length - 1 !== $index"
  39. class="material-icons"
  40. >keyboard_arrow_down</i
  41. >
  42. <i
  43. v-else
  44. class="material-icons"
  45. style="opacity: 0"
  46. >error</i
  47. >
  48. </a>
  49. <a
  50. href="#"
  51. @click="removeSongFromPlaylist(song.songId)"
  52. >
  53. <i class="material-icons">delete</i>
  54. </a>
  55. </div>
  56. </li>
  57. </ul>
  58. <br />
  59. </aside>
  60. <div class="control is-grouped">
  61. <p class="control is-expanded">
  62. <input
  63. v-model="songQuery"
  64. class="input"
  65. type="text"
  66. placeholder="Search for Song to add"
  67. autofocus
  68. @keyup.enter="searchForSongs()"
  69. />
  70. </p>
  71. <p class="control">
  72. <a class="button is-info" @click="searchForSongs()" href="#"
  73. >Search</a
  74. >
  75. </p>
  76. </div>
  77. <table v-if="songQueryResults.length > 0" class="table">
  78. <tbody>
  79. <tr
  80. v-for="(result, index) in songQueryResults"
  81. :key="index"
  82. >
  83. <td>
  84. <img :src="result.thumbnail" />
  85. </td>
  86. <td>{{ result.title }}</td>
  87. <td>
  88. <a
  89. class="button is-success"
  90. href="#"
  91. @click="addSongToPlaylist(result.id)"
  92. >Add</a
  93. >
  94. </td>
  95. </tr>
  96. </tbody>
  97. </table>
  98. <div class="control is-grouped">
  99. <p class="control is-expanded">
  100. <input
  101. v-model="importQuery"
  102. class="input"
  103. type="text"
  104. placeholder="YouTube Playlist URL"
  105. @keyup.enter="importPlaylist()"
  106. />
  107. </p>
  108. <p class="control">
  109. <a class="button is-info" @click="importPlaylist()" href="#"
  110. >Import</a
  111. >
  112. </p>
  113. </div>
  114. <h5>Edit playlist details:</h5>
  115. <div class="control is-grouped">
  116. <p class="control is-expanded">
  117. <input
  118. v-model="playlist.displayName"
  119. class="input"
  120. type="text"
  121. placeholder="Playlist Display Name"
  122. @keyup.enter="renamePlaylist()"
  123. />
  124. </p>
  125. <p class="control">
  126. <a class="button is-info" @click="renamePlaylist()" href="#"
  127. >Rename</a
  128. >
  129. </p>
  130. </div>
  131. </div>
  132. <div slot="footer">
  133. <a class="button is-danger" v-on:click="removePlaylist()" href="#"
  134. >Remove Playlist</a
  135. >
  136. </div>
  137. </modal>
  138. </template>
  139. <script>
  140. import { mapState } from "vuex";
  141. import { Toast } from "vue-roaster";
  142. import Modal from "../Modal.vue";
  143. import io from "../../../io";
  144. import validation from "../../../validation";
  145. export default {
  146. components: { Modal },
  147. data() {
  148. return {
  149. playlist: { songs: [] },
  150. songQueryResults: [],
  151. songQuery: "",
  152. importQuery: ""
  153. };
  154. },
  155. computed: mapState("user/playlists", {
  156. editing: state => state.editing
  157. }),
  158. mounted: function() {
  159. let _this = this;
  160. io.getSocket(socket => {
  161. _this.socket = socket;
  162. _this.socket.emit("playlists.getPlaylist", _this.editing, res => {
  163. if (res.status === "success") _this.playlist = res.data;
  164. _this.playlist.oldId = res.data._id;
  165. });
  166. _this.socket.on("event:playlist.addSong", data => {
  167. if (_this.playlist._id === data.playlistId)
  168. _this.playlist.songs.push(data.song);
  169. });
  170. _this.socket.on("event:playlist.removeSong", data => {
  171. if (_this.playlist._id === data.playlistId) {
  172. _this.playlist.songs.forEach((song, index) => {
  173. if (song.songId === data.songId)
  174. _this.playlist.songs.splice(index, 1);
  175. });
  176. }
  177. });
  178. _this.socket.on("event:playlist.updateDisplayName", data => {
  179. if (_this.playlist._id === data.playlistId)
  180. _this.playlist.displayName = data.displayName;
  181. });
  182. _this.socket.on("event:playlist.moveSongToBottom", data => {
  183. if (_this.playlist._id === data.playlistId) {
  184. let songIndex;
  185. _this.playlist.songs.forEach((song, index) => {
  186. if (song.songId === data.songId) songIndex = index;
  187. });
  188. let song = _this.playlist.songs.splice(songIndex, 1)[0];
  189. _this.playlist.songs.push(song);
  190. }
  191. });
  192. _this.socket.on("event:playlist.moveSongToTop", data => {
  193. if (_this.playlist._id === data.playlistId) {
  194. let songIndex;
  195. _this.playlist.songs.forEach((song, index) => {
  196. if (song.songId === data.songId) songIndex = index;
  197. });
  198. let song = _this.playlist.songs.splice(songIndex, 1)[0];
  199. _this.playlist.songs.unshift(song);
  200. }
  201. });
  202. });
  203. },
  204. methods: {
  205. formatTime: function(length) {
  206. let duration = moment.duration(length, "seconds");
  207. function getHours() {
  208. return Math.floor(duration.asHours());
  209. }
  210. if (length <= 0) return "0 seconds";
  211. else
  212. return (
  213. (getHours() > 0
  214. ? getHours() > 1
  215. ? getHours() < 10
  216. ? "0" + getHours() + " hours "
  217. : getHours() + " hours "
  218. : "0" + getHours() + " hour "
  219. : "") +
  220. (duration.minutes() > 0
  221. ? duration.minutes() > 1
  222. ? duration.minutes() < 10
  223. ? "0" + duration.minutes() + " minutes "
  224. : duration.minutes() + " minutes "
  225. : "0" + duration.minutes() + " minute "
  226. : "") +
  227. (duration.seconds() > 0
  228. ? duration.seconds() > 1
  229. ? duration.seconds() < 10
  230. ? "0" + duration.seconds() + " seconds "
  231. : duration.seconds() + " seconds "
  232. : "0" + duration.seconds() + " second "
  233. : "")
  234. );
  235. },
  236. totalLength: function() {
  237. let length = 0;
  238. this.playlist.songs.forEach(song => {
  239. length += song.duration;
  240. });
  241. return this.formatTime(length);
  242. },
  243. searchForSongs: function() {
  244. let _this = this;
  245. let query = _this.songQuery;
  246. if (query.indexOf("&index=") !== -1) {
  247. query = query.split("&index=");
  248. query.pop();
  249. query = query.join("");
  250. }
  251. if (query.indexOf("&list=") !== -1) {
  252. query = query.split("&list=");
  253. query.pop();
  254. query = query.join("");
  255. }
  256. _this.socket.emit("apis.searchYoutube", query, res => {
  257. if (res.status == "success") {
  258. _this.songQueryResults = [];
  259. for (let i = 0; i < res.data.items.length; i++) {
  260. _this.songQueryResults.push({
  261. id: res.data.items[i].id.videoId,
  262. url: `https://www.youtube.com/watch?v=${this.id}`,
  263. title: res.data.items[i].snippet.title,
  264. thumbnail:
  265. res.data.items[i].snippet.thumbnails.default.url
  266. });
  267. }
  268. } else if (res.status === "error")
  269. Toast.methods.addToast(res.message, 3000);
  270. });
  271. },
  272. addSongToPlaylist: function(id) {
  273. let _this = this;
  274. _this.socket.emit(
  275. "playlists.addSongToPlaylist",
  276. id,
  277. _this.playlist._id,
  278. res => {
  279. Toast.methods.addToast(res.message, 4000);
  280. }
  281. );
  282. },
  283. importPlaylist: function() {
  284. let _this = this;
  285. Toast.methods.addToast(
  286. "Starting to import your playlist. This can take some time to do.",
  287. 4000
  288. );
  289. this.socket.emit(
  290. "playlists.addSetToPlaylist",
  291. _this.importQuery,
  292. _this.playlist._id,
  293. res => {
  294. if (res.status === "success")
  295. _this.playlist.songs = res.data;
  296. Toast.methods.addToast(res.message, 4000);
  297. }
  298. );
  299. },
  300. removeSongFromPlaylist: function(id) {
  301. let _this = this;
  302. this.socket.emit(
  303. "playlists.removeSongFromPlaylist",
  304. id,
  305. _this.playlist._id,
  306. res => {
  307. Toast.methods.addToast(res.message, 4000);
  308. }
  309. );
  310. },
  311. renamePlaylist: function() {
  312. const displayName = this.playlist.displayName;
  313. if (!validation.isLength(displayName, 2, 32))
  314. return Toast.methods.addToast(
  315. "Display name must have between 2 and 32 characters.",
  316. 8000
  317. );
  318. if (!validation.regex.azAZ09_.test(displayName))
  319. return Toast.methods.addToast(
  320. "Invalid display name format. Allowed characters: a-z, A-Z, 0-9 and _.",
  321. 8000
  322. );
  323. this.socket.emit(
  324. "playlists.updateDisplayName",
  325. this.playlist._id,
  326. this.playlist.displayName,
  327. res => {
  328. Toast.methods.addToast(res.message, 4000);
  329. }
  330. );
  331. },
  332. removePlaylist: function() {
  333. let _this = this;
  334. _this.socket.emit("playlists.remove", _this.playlist._id, res => {
  335. Toast.methods.addToast(res.message, 3000);
  336. if (res.status === "success") {
  337. _this.$parent.modals.editPlaylist = !_this.$parent.modals
  338. .editPlaylist;
  339. }
  340. });
  341. },
  342. promoteSong: function(songId) {
  343. let _this = this;
  344. _this.socket.emit(
  345. "playlists.moveSongToTop",
  346. _this.playlist._id,
  347. songId,
  348. res => {
  349. Toast.methods.addToast(res.message, 4000);
  350. }
  351. );
  352. },
  353. demoteSong: function(songId) {
  354. let _this = this;
  355. _this.socket.emit(
  356. "playlists.moveSongToBottom",
  357. _this.playlist._id,
  358. songId,
  359. res => {
  360. Toast.methods.addToast(res.message, 4000);
  361. }
  362. );
  363. }
  364. }
  365. };
  366. </script>
  367. <style lang="scss" scoped>
  368. .menu {
  369. padding: 0 20px;
  370. }
  371. .menu-list li {
  372. display: flex;
  373. justify-content: space-between;
  374. }
  375. .menu-list a:hover {
  376. color: #000 !important;
  377. }
  378. li a {
  379. display: flex;
  380. align-items: center;
  381. }
  382. .controls {
  383. display: flex;
  384. a {
  385. display: flex;
  386. align-items: center;
  387. }
  388. }
  389. .table {
  390. margin-bottom: 0;
  391. }
  392. h5 {
  393. padding: 20px 0;
  394. }
  395. </style>