EditPlaylist.vue 11 KB

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