PlaylistTabBase.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089
  1. <script setup lang="ts">
  2. import { defineAsyncComponent, ref, reactive, computed, onMounted } from "vue";
  3. import Toast from "toasters";
  4. import { storeToRefs } from "pinia";
  5. import { useWebsocketsStore } from "@/stores/websockets";
  6. import { useStationStore } from "@/stores/station";
  7. import { useUserPlaylistsStore } from "@/stores/userPlaylists";
  8. import { useModalsStore } from "@/stores/modals";
  9. import { useManageStationStore } from "@/stores/manageStation";
  10. import { useSortablePlaylists } from "@/composables/useSortablePlaylists";
  11. const PlaylistItem = defineAsyncComponent(
  12. () => import("@/components/PlaylistItem.vue")
  13. );
  14. const QuickConfirm = defineAsyncComponent(
  15. () => import("@/components/QuickConfirm.vue")
  16. );
  17. const props = defineProps({
  18. modalUuid: { type: String, default: null },
  19. type: {
  20. type: String,
  21. default: ""
  22. },
  23. sector: {
  24. type: String,
  25. default: "manageStation"
  26. }
  27. });
  28. const emit = defineEmits(["selected"]);
  29. const { socket } = useWebsocketsStore();
  30. const stationStore = useStationStore();
  31. const tab = ref("current");
  32. const search = reactive({
  33. query: "",
  34. searchedQuery: "",
  35. page: 0,
  36. count: 0,
  37. resultsLeft: 0,
  38. pageSize: 0,
  39. results: []
  40. });
  41. const featuredPlaylists = ref([]);
  42. const tabs = ref({});
  43. const {
  44. DraggableList,
  45. drag,
  46. playlists,
  47. savePlaylistOrder,
  48. orderOfPlaylists,
  49. myUserId,
  50. calculatePlaylistOrder
  51. } = useSortablePlaylists();
  52. const { autoRequest } = storeToRefs(stationStore);
  53. const manageStationStore = useManageStationStore({
  54. modalUuid: props.modalUuid
  55. });
  56. const { autofill } = storeToRefs(manageStationStore);
  57. const station = computed({
  58. get() {
  59. if (props.sector === "manageStation") return manageStationStore.station;
  60. return stationStore.station;
  61. },
  62. set(value) {
  63. if (props.sector === "manageStation")
  64. manageStationStore.updateStation(value);
  65. else stationStore.updateStation(value);
  66. }
  67. });
  68. const blacklist = computed({
  69. get() {
  70. if (props.sector === "manageStation")
  71. return manageStationStore.blacklist;
  72. return stationStore.blacklist;
  73. },
  74. set(value) {
  75. if (props.sector === "manageStation")
  76. manageStationStore.setBlacklist(value);
  77. else stationStore.setBlacklist(value);
  78. }
  79. });
  80. const resultsLeftCount = computed(() => search.count - search.results.length);
  81. const nextPageResultsCount = computed(() =>
  82. Math.min(search.pageSize, resultsLeftCount.value)
  83. );
  84. const hasPermission = permission =>
  85. props.sector === "manageStation"
  86. ? manageStationStore.hasPermission(permission)
  87. : stationStore.hasPermission(permission);
  88. const { openModal } = useModalsStore();
  89. const { setPlaylists } = useUserPlaylistsStore();
  90. const {
  91. addAutorequestPlaylists,
  92. addPlaylistToAutoRequest,
  93. removePlaylistFromAutoRequest,
  94. updateAutorequestLocalStorage
  95. } = stationStore;
  96. const showTab = _tab => {
  97. tabs.value[`${_tab}-tab`].scrollIntoView({ block: "nearest" });
  98. tab.value = _tab;
  99. };
  100. const label = (tense = "future", typeOverwrite = null, capitalize = false) => {
  101. let label = typeOverwrite || props.type;
  102. if (tense === "past") label = `${label}ed`;
  103. if (tense === "present") label = `${label}ing`;
  104. if (capitalize) label = `${label.charAt(0).toUpperCase()}${label.slice(1)}`;
  105. return label;
  106. };
  107. const selectedPlaylists = (typeOverwrite?: string) => {
  108. const type = typeOverwrite || props.type;
  109. if (type === "autofill") return autofill.value;
  110. if (type === "blacklist") return blacklist.value;
  111. if (type === "autorequest") return autoRequest.value;
  112. return [];
  113. };
  114. const isSelected = (playlistId, typeOverwrite?: string) => {
  115. const type = typeOverwrite || props.type;
  116. let selected = false;
  117. selectedPlaylists(type).forEach(playlist => {
  118. if (playlist._id === playlistId) selected = true;
  119. });
  120. return selected;
  121. };
  122. const deselectPlaylist = (playlistId, typeOverwrite?: string) => {
  123. const type = typeOverwrite || props.type;
  124. if (type === "autofill")
  125. return new Promise(resolve => {
  126. socket.dispatch(
  127. "stations.removeAutofillPlaylist",
  128. station.value._id,
  129. playlistId,
  130. res => {
  131. new Toast(res.message);
  132. resolve(true);
  133. }
  134. );
  135. });
  136. if (type === "blacklist")
  137. return new Promise(resolve => {
  138. socket.dispatch(
  139. "stations.removeBlacklistedPlaylist",
  140. station.value._id,
  141. playlistId,
  142. res => {
  143. new Toast(res.message);
  144. resolve(true);
  145. }
  146. );
  147. });
  148. if (type === "autorequest")
  149. return new Promise(resolve => {
  150. removePlaylistFromAutoRequest(playlistId);
  151. new Toast("Successfully deselected playlist.");
  152. resolve(true);
  153. });
  154. return false;
  155. };
  156. const selectPlaylist = async (playlist, typeOverwrite?: string) => {
  157. const type = typeOverwrite || props.type;
  158. if (isSelected(playlist._id, type))
  159. return new Toast(`Error: Playlist already ${label("past", type)}.`);
  160. if (type === "autofill")
  161. return new Promise(resolve => {
  162. socket.dispatch(
  163. "stations.autofillPlaylist",
  164. station.value._id,
  165. playlist._id,
  166. res => {
  167. new Toast(res.message);
  168. emit("selected");
  169. resolve(true);
  170. }
  171. );
  172. });
  173. if (type === "blacklist") {
  174. if (props.type !== "blacklist" && isSelected(playlist._id))
  175. await deselectPlaylist(playlist._id);
  176. return new Promise(resolve => {
  177. socket.dispatch(
  178. "stations.blacklistPlaylist",
  179. station.value._id,
  180. playlist._id,
  181. res => {
  182. new Toast(res.message);
  183. emit("selected");
  184. resolve(true);
  185. }
  186. );
  187. });
  188. }
  189. if (type === "autorequest")
  190. return new Promise(resolve => {
  191. addPlaylistToAutoRequest(playlist);
  192. new Toast("Successfully selected playlist to auto request songs.");
  193. emit("selected");
  194. resolve(true);
  195. });
  196. return false;
  197. };
  198. const searchForPlaylists = page => {
  199. if (search.page >= page || search.searchedQuery !== search.query) {
  200. search.results = [];
  201. search.page = 0;
  202. search.count = 0;
  203. search.resultsLeft = 0;
  204. search.pageSize = 0;
  205. }
  206. const { query } = search;
  207. const action =
  208. station.value.type === "official" && props.type !== "autorequest"
  209. ? "playlists.searchOfficial"
  210. : "playlists.searchCommunity";
  211. search.searchedQuery = search.query;
  212. socket.dispatch(action, query, page, res => {
  213. const { data } = res;
  214. if (res.status === "success") {
  215. const { count, pageSize, playlists } = data;
  216. search.results = [...search.results, ...playlists];
  217. search.page = page;
  218. search.count = count;
  219. search.resultsLeft = count - search.results.length;
  220. search.pageSize = pageSize;
  221. } else if (res.status === "error") {
  222. search.results = [];
  223. search.page = 0;
  224. search.count = 0;
  225. search.resultsLeft = 0;
  226. search.pageSize = 0;
  227. new Toast(res.message);
  228. }
  229. });
  230. };
  231. onMounted(() => {
  232. showTab("my-playlists");
  233. socket.onConnect(() => {
  234. socket.dispatch("playlists.indexMyPlaylists", res => {
  235. if (res.status === "success") setPlaylists(res.data.playlists);
  236. orderOfPlaylists.value = calculatePlaylistOrder(); // order in regards to the database
  237. });
  238. socket.dispatch("playlists.indexFeaturedPlaylists", res => {
  239. if (res.status === "success")
  240. featuredPlaylists.value = res.data.playlists;
  241. });
  242. if (props.type === "autofill")
  243. socket.dispatch(
  244. `stations.getStationAutofillPlaylistsById`,
  245. station.value._id,
  246. res => {
  247. if (res.status === "success") {
  248. station.value.autofill.playlists = res.data.playlists;
  249. }
  250. }
  251. );
  252. socket.dispatch(
  253. `stations.getStationBlacklistById`,
  254. station.value._id,
  255. res => {
  256. if (res.status === "success") {
  257. station.value.blacklist = res.data.playlists;
  258. }
  259. }
  260. );
  261. const autorequestLocalStorageItem = localStorage.getItem(
  262. `autorequest-${station.value._id}`
  263. );
  264. if (
  265. autorequestLocalStorageItem &&
  266. station.value.requests.allowAutorequest
  267. ) {
  268. const autorequestParsedItem = JSON.parse(
  269. autorequestLocalStorageItem
  270. );
  271. const autorequestUpdatedAt = new Date(
  272. autorequestParsedItem.updatedAt
  273. );
  274. const fiveMinutesAgo = new Date(
  275. new Date().getTime() - 5 * 60 * 1000
  276. );
  277. if (autorequestUpdatedAt > fiveMinutesAgo) {
  278. const playlists = [];
  279. const promises = autorequestParsedItem.playlistIds.map(
  280. playlistId =>
  281. new Promise<void>(resolve => {
  282. socket.dispatch(
  283. "playlists.getPlaylist",
  284. playlistId,
  285. res => {
  286. if (res.status === "success") {
  287. playlists.push(res.data.playlist);
  288. }
  289. resolve();
  290. }
  291. );
  292. })
  293. );
  294. Promise.all(promises).then(() => {
  295. addAutorequestPlaylists(playlists);
  296. });
  297. } else updateAutorequestLocalStorage();
  298. }
  299. });
  300. });
  301. </script>
  302. <template>
  303. <div class="playlist-tab-base">
  304. <div v-if="$slots.info" class="top-info has-text-centered">
  305. <slot name="info" />
  306. </div>
  307. <div class="tabs-container">
  308. <div class="tab-selection">
  309. <button
  310. v-if="
  311. type === 'autorequest' || station.type === 'community'
  312. "
  313. class="button is-default"
  314. :ref="el => (tabs['my-playlists-tab'] = el)"
  315. :class="{ selected: tab === 'my-playlists' }"
  316. @click="showTab('my-playlists')"
  317. >
  318. My Playlists
  319. </button>
  320. <button
  321. class="button is-default"
  322. :ref="el => (tabs['search-tab'] = el)"
  323. :class="{ selected: tab === 'search' }"
  324. @click="showTab('search')"
  325. >
  326. Search
  327. </button>
  328. <button
  329. class="button is-default"
  330. :ref="el => (tabs['current-tab'] = el)"
  331. :class="{ selected: tab === 'current' }"
  332. @click="showTab('current')"
  333. >
  334. Current
  335. </button>
  336. </div>
  337. <div class="tab" v-show="tab === 'search'">
  338. <div v-if="featuredPlaylists.length > 0">
  339. <label class="label"> Featured playlists </label>
  340. <playlist-item
  341. v-for="featuredPlaylist in featuredPlaylists"
  342. :key="`featuredKey-${featuredPlaylist._id}`"
  343. :playlist="featuredPlaylist"
  344. :show-owner="true"
  345. >
  346. <template #item-icon>
  347. <i
  348. class="material-icons blacklisted-icon"
  349. v-if="
  350. isSelected(
  351. featuredPlaylist._id,
  352. 'blacklist'
  353. )
  354. "
  355. :content="`This playlist is currently ${label(
  356. 'past',
  357. 'blacklist'
  358. )}`"
  359. v-tippy
  360. >
  361. block
  362. </i>
  363. <i
  364. class="material-icons"
  365. v-else-if="isSelected(featuredPlaylist._id)"
  366. :content="`This playlist is currently ${label(
  367. 'past'
  368. )}`"
  369. v-tippy
  370. >
  371. play_arrow
  372. </i>
  373. <i
  374. class="material-icons"
  375. v-else
  376. :content="`This playlist is currently not ${label(
  377. 'past'
  378. )}`"
  379. v-tippy
  380. >
  381. {{
  382. type === "blacklist"
  383. ? "block"
  384. : "play_disabled"
  385. }}
  386. </i>
  387. </template>
  388. <template #actions>
  389. <i
  390. v-if="
  391. type !== 'blacklist' &&
  392. isSelected(
  393. featuredPlaylist._id,
  394. 'blacklist'
  395. )
  396. "
  397. class="material-icons stop-icon"
  398. :content="`This playlist is ${label(
  399. 'past',
  400. 'blacklist'
  401. )} in this station`"
  402. v-tippy="{ theme: 'info' }"
  403. >play_disabled</i
  404. >
  405. <quick-confirm
  406. v-if="
  407. type !== 'blacklist' &&
  408. isSelected(featuredPlaylist._id)
  409. "
  410. @confirm="
  411. deselectPlaylist(featuredPlaylist._id)
  412. "
  413. >
  414. <i
  415. class="material-icons stop-icon"
  416. :content="`Stop ${label(
  417. 'present'
  418. )} songs from this playlist`"
  419. v-tippy
  420. >
  421. stop
  422. </i>
  423. </quick-confirm>
  424. <i
  425. v-if="
  426. type !== 'blacklist' &&
  427. !isSelected(featuredPlaylist._id) &&
  428. !isSelected(
  429. featuredPlaylist._id,
  430. 'blacklist'
  431. )
  432. "
  433. @click="selectPlaylist(featuredPlaylist)"
  434. class="material-icons play-icon"
  435. :content="`${label(
  436. 'future',
  437. null,
  438. true
  439. )} songs from this playlist`"
  440. v-tippy
  441. >play_arrow</i
  442. >
  443. <quick-confirm
  444. v-if="
  445. type === 'blacklist' &&
  446. !isSelected(
  447. featuredPlaylist._id,
  448. 'blacklist'
  449. )
  450. "
  451. @confirm="
  452. selectPlaylist(
  453. featuredPlaylist,
  454. 'blacklist'
  455. )
  456. "
  457. >
  458. <i
  459. class="material-icons stop-icon"
  460. :content="`${label(
  461. 'future',
  462. null,
  463. true
  464. )} Playlist`"
  465. v-tippy
  466. >block</i
  467. >
  468. </quick-confirm>
  469. <quick-confirm
  470. v-if="
  471. type === 'blacklist' &&
  472. isSelected(
  473. featuredPlaylist._id,
  474. 'blacklist'
  475. )
  476. "
  477. @confirm="
  478. deselectPlaylist(featuredPlaylist._id)
  479. "
  480. >
  481. <i
  482. class="material-icons stop-icon"
  483. :content="`Stop ${label(
  484. 'present'
  485. )} songs from this playlist`"
  486. v-tippy
  487. >
  488. stop
  489. </i>
  490. </quick-confirm>
  491. <i
  492. v-if="featuredPlaylist.createdBy === myUserId"
  493. @click="
  494. openModal({
  495. modal: 'editPlaylist',
  496. props: {
  497. playlistId: featuredPlaylist._id
  498. }
  499. })
  500. "
  501. class="material-icons edit-icon"
  502. content="Edit Playlist"
  503. v-tippy
  504. >edit</i
  505. >
  506. <i
  507. v-if="
  508. featuredPlaylist.createdBy !== myUserId &&
  509. (featuredPlaylist.privacy === 'public' ||
  510. hasPermission('playlists.view.others'))
  511. "
  512. @click="
  513. openModal({
  514. modal: 'editPlaylist',
  515. props: {
  516. playlistId: featuredPlaylist._id
  517. }
  518. })
  519. "
  520. class="material-icons edit-icon"
  521. content="View Playlist"
  522. v-tippy
  523. >visibility</i
  524. >
  525. </template>
  526. </playlist-item>
  527. <br />
  528. </div>
  529. <label class="label">Search for a playlist</label>
  530. <div class="control is-grouped input-with-button">
  531. <p class="control is-expanded">
  532. <input
  533. class="input"
  534. type="text"
  535. placeholder="Enter your playlist query here..."
  536. v-model="search.query"
  537. @keyup.enter="searchForPlaylists(1)"
  538. />
  539. </p>
  540. <p class="control">
  541. <a class="button is-info" @click="searchForPlaylists(1)"
  542. ><i class="material-icons icon-with-button"
  543. >search</i
  544. >Search</a
  545. >
  546. </p>
  547. </div>
  548. <div v-if="search.results.length > 0">
  549. <playlist-item
  550. v-for="playlist in search.results"
  551. :key="`searchKey-${playlist._id}`"
  552. :playlist="playlist"
  553. :show-owner="true"
  554. >
  555. <template #item-icon>
  556. <i
  557. class="material-icons blacklisted-icon"
  558. v-if="isSelected(playlist._id, 'blacklist')"
  559. :content="`This playlist is currently ${label(
  560. 'past',
  561. 'blacklist'
  562. )}`"
  563. v-tippy
  564. >
  565. block
  566. </i>
  567. <i
  568. class="material-icons"
  569. v-else-if="isSelected(playlist._id)"
  570. :content="`This playlist is currently ${label(
  571. 'past'
  572. )}`"
  573. v-tippy
  574. >
  575. play_arrow
  576. </i>
  577. <i
  578. class="material-icons"
  579. v-else
  580. :content="`This playlist is currently not ${label(
  581. 'past'
  582. )}`"
  583. v-tippy
  584. >
  585. {{
  586. type === "blacklist"
  587. ? "block"
  588. : "play_disabled"
  589. }}
  590. </i>
  591. </template>
  592. <template #actions>
  593. <i
  594. v-if="
  595. type !== 'blacklist' &&
  596. isSelected(playlist._id, 'blacklist')
  597. "
  598. class="material-icons stop-icon"
  599. :content="`This playlist is ${label(
  600. 'past',
  601. 'blacklist'
  602. )} in this station`"
  603. v-tippy="{ theme: 'info' }"
  604. >play_disabled</i
  605. >
  606. <quick-confirm
  607. v-if="
  608. type !== 'blacklist' &&
  609. isSelected(playlist._id)
  610. "
  611. @confirm="deselectPlaylist(playlist._id)"
  612. >
  613. <i
  614. class="material-icons stop-icon"
  615. :content="`Stop ${label(
  616. 'present'
  617. )} songs from this playlist`"
  618. v-tippy
  619. >
  620. stop
  621. </i>
  622. </quick-confirm>
  623. <i
  624. v-if="
  625. type !== 'blacklist' &&
  626. !isSelected(playlist._id) &&
  627. !isSelected(playlist._id, 'blacklist')
  628. "
  629. @click="selectPlaylist(playlist)"
  630. class="material-icons play-icon"
  631. :content="`${label(
  632. 'future',
  633. null,
  634. true
  635. )} songs from this playlist`"
  636. v-tippy
  637. >play_arrow</i
  638. >
  639. <quick-confirm
  640. v-if="
  641. type === 'blacklist' &&
  642. !isSelected(playlist._id, 'blacklist')
  643. "
  644. @confirm="selectPlaylist(playlist, 'blacklist')"
  645. >
  646. <i
  647. class="material-icons stop-icon"
  648. :content="`${label(
  649. 'future',
  650. null,
  651. true
  652. )} Playlist`"
  653. v-tippy
  654. >block</i
  655. >
  656. </quick-confirm>
  657. <quick-confirm
  658. v-if="
  659. type === 'blacklist' &&
  660. isSelected(playlist._id, 'blacklist')
  661. "
  662. @confirm="deselectPlaylist(playlist._id)"
  663. >
  664. <i
  665. class="material-icons stop-icon"
  666. :content="`Stop ${label(
  667. 'present'
  668. )} songs from this playlist`"
  669. v-tippy
  670. >
  671. stop
  672. </i>
  673. </quick-confirm>
  674. <i
  675. v-if="playlist.createdBy === myUserId"
  676. @click="
  677. openModal({
  678. modal: 'editPlaylist',
  679. props: { playlistId: playlist._id }
  680. })
  681. "
  682. class="material-icons edit-icon"
  683. content="Edit Playlist"
  684. v-tippy
  685. >edit</i
  686. >
  687. <i
  688. v-if="
  689. playlist.createdBy !== myUserId &&
  690. (playlist.privacy === 'public' ||
  691. hasPermission('playlists.view.others'))
  692. "
  693. @click="
  694. openModal({
  695. modal: 'editPlaylist',
  696. props: { playlistId: playlist._id }
  697. })
  698. "
  699. class="material-icons edit-icon"
  700. content="View Playlist"
  701. v-tippy
  702. >visibility</i
  703. >
  704. </template>
  705. </playlist-item>
  706. <button
  707. v-if="resultsLeftCount > 0"
  708. class="button is-primary load-more-button"
  709. @click="searchForPlaylists(search.page + 1)"
  710. >
  711. Load {{ nextPageResultsCount }} more results
  712. </button>
  713. </div>
  714. </div>
  715. <div class="tab" v-show="tab === 'current'">
  716. <div v-if="selectedPlaylists().length > 0">
  717. <playlist-item
  718. v-for="playlist in selectedPlaylists()"
  719. :key="`key-${playlist._id}`"
  720. :playlist="playlist"
  721. :show-owner="true"
  722. >
  723. <template #item-icon>
  724. <i
  725. class="material-icons"
  726. :class="{
  727. 'blacklisted-icon': type === 'blacklist'
  728. }"
  729. :content="`This playlist is currently ${label(
  730. 'past'
  731. )}`"
  732. v-tippy
  733. >
  734. {{
  735. type === "blacklist"
  736. ? "block"
  737. : "play_arrow"
  738. }}
  739. </i>
  740. </template>
  741. <template #actions>
  742. <quick-confirm
  743. @confirm="deselectPlaylist(playlist._id)"
  744. >
  745. <i
  746. class="material-icons stop-icon"
  747. :content="`Stop ${label(
  748. 'present'
  749. )} songs from this playlist`"
  750. v-tippy
  751. >
  752. stop
  753. </i>
  754. </quick-confirm>
  755. <i
  756. v-if="playlist.createdBy === myUserId"
  757. @click="
  758. openModal({
  759. modal: 'editPlaylist',
  760. props: { playlistId: playlist._id }
  761. })
  762. "
  763. class="material-icons edit-icon"
  764. content="Edit Playlist"
  765. v-tippy
  766. >edit</i
  767. >
  768. <i
  769. v-if="
  770. playlist.createdBy !== myUserId &&
  771. (playlist.privacy === 'public' ||
  772. hasPermission('playlists.view.others'))
  773. "
  774. @click="
  775. openModal({
  776. modal: 'editPlaylist',
  777. props: { playlistId: playlist._id }
  778. })
  779. "
  780. class="material-icons edit-icon"
  781. content="View Playlist"
  782. v-tippy
  783. >visibility</i
  784. >
  785. </template>
  786. </playlist-item>
  787. </div>
  788. <p v-else class="has-text-centered scrollable-list">
  789. No playlists currently {{ label("present") }}.
  790. </p>
  791. </div>
  792. <div
  793. v-if="type === 'autorequest' || station.type === 'community'"
  794. class="tab"
  795. v-show="tab === 'my-playlists'"
  796. >
  797. <div
  798. class="menu-list scrollable-list"
  799. v-if="playlists.length > 0"
  800. >
  801. <draggable-list
  802. v-model:list="playlists"
  803. item-key="_id"
  804. @start="drag = true"
  805. @end="drag = false"
  806. @update="savePlaylistOrder"
  807. >
  808. <template #item="{ element }">
  809. <playlist-item :playlist="element">
  810. <template #item-icon>
  811. <i
  812. class="material-icons blacklisted-icon"
  813. v-if="
  814. isSelected(element._id, 'blacklist')
  815. "
  816. :content="`This playlist is currently ${label(
  817. 'past',
  818. 'blacklist'
  819. )}`"
  820. v-tippy
  821. >
  822. block
  823. </i>
  824. <i
  825. class="material-icons"
  826. v-else-if="isSelected(element._id)"
  827. :content="`This playlist is currently ${label(
  828. 'past'
  829. )}`"
  830. v-tippy
  831. >
  832. play_arrow
  833. </i>
  834. <i
  835. class="material-icons"
  836. v-else
  837. :content="`This playlist is currently not ${label(
  838. 'past'
  839. )}`"
  840. v-tippy
  841. >
  842. {{
  843. type === "blacklist"
  844. ? "block"
  845. : "play_disabled"
  846. }}
  847. </i>
  848. </template>
  849. <template #actions>
  850. <i
  851. v-if="
  852. type !== 'blacklist' &&
  853. isSelected(element._id, 'blacklist')
  854. "
  855. class="material-icons stop-icon"
  856. :content="`This playlist is ${label(
  857. 'past',
  858. 'blacklist'
  859. )} in this station`"
  860. v-tippy="{ theme: 'info' }"
  861. >play_disabled</i
  862. >
  863. <quick-confirm
  864. v-if="
  865. type !== 'blacklist' &&
  866. isSelected(element._id)
  867. "
  868. @confirm="deselectPlaylist(element._id)"
  869. >
  870. <i
  871. class="material-icons stop-icon"
  872. :content="`Stop ${label(
  873. 'present'
  874. )} songs from this playlist`"
  875. v-tippy
  876. >
  877. stop
  878. </i>
  879. </quick-confirm>
  880. <i
  881. v-if="
  882. type !== 'blacklist' &&
  883. !isSelected(element._id) &&
  884. !isSelected(
  885. element._id,
  886. 'blacklist'
  887. )
  888. "
  889. @click="selectPlaylist(element)"
  890. class="material-icons play-icon"
  891. :content="`${label(
  892. 'future',
  893. null,
  894. true
  895. )} songs from this playlist`"
  896. v-tippy
  897. >play_arrow</i
  898. >
  899. <quick-confirm
  900. v-if="
  901. type === 'blacklist' &&
  902. !isSelected(
  903. element._id,
  904. 'blacklist'
  905. )
  906. "
  907. @confirm="
  908. selectPlaylist(element, 'blacklist')
  909. "
  910. >
  911. <i
  912. class="material-icons stop-icon"
  913. :content="`${label(
  914. 'future',
  915. null,
  916. true
  917. )} Playlist`"
  918. v-tippy
  919. >block</i
  920. >
  921. </quick-confirm>
  922. <quick-confirm
  923. v-if="
  924. type === 'blacklist' &&
  925. isSelected(element._id, 'blacklist')
  926. "
  927. @confirm="deselectPlaylist(element._id)"
  928. >
  929. <i
  930. class="material-icons stop-icon"
  931. :content="`Stop ${label(
  932. 'present'
  933. )} songs from this playlist`"
  934. v-tippy
  935. >
  936. stop
  937. </i>
  938. </quick-confirm>
  939. <i
  940. @click="
  941. openModal({
  942. modal: 'editPlaylist',
  943. props: {
  944. playlistId: element._id
  945. }
  946. })
  947. "
  948. class="material-icons edit-icon"
  949. content="Edit Playlist"
  950. v-tippy
  951. >edit</i
  952. >
  953. </template>
  954. </playlist-item>
  955. </template>
  956. </draggable-list>
  957. </div>
  958. <p v-else class="has-text-centered scrollable-list">
  959. You don't have any playlists
  960. </p>
  961. <button
  962. class="button is-primary"
  963. id="create-new-playlist-button"
  964. @click="openModal('createPlaylist')"
  965. >
  966. Create new playlist
  967. </button>
  968. </div>
  969. </div>
  970. </div>
  971. </template>
  972. <style lang="less" scoped>
  973. .night-mode {
  974. .tabs-container .tab-selection .button {
  975. background: var(--dark-grey) !important;
  976. color: var(--white) !important;
  977. }
  978. }
  979. .blacklisted-icon {
  980. color: var(--dark-red);
  981. }
  982. .playlist-tab-base {
  983. .top-info {
  984. font-size: 15px;
  985. margin-bottom: 15px;
  986. }
  987. .tabs-container {
  988. .tab-selection {
  989. display: flex;
  990. overflow-x: auto;
  991. .button {
  992. border-radius: 0;
  993. border: 0;
  994. text-transform: uppercase;
  995. font-size: 14px;
  996. color: var(--dark-grey-3);
  997. background-color: var(--light-grey-2);
  998. flex-grow: 1;
  999. height: 32px;
  1000. &:not(:first-of-type) {
  1001. margin-left: 5px;
  1002. }
  1003. }
  1004. .selected {
  1005. background-color: var(--primary-color) !important;
  1006. color: var(--white) !important;
  1007. font-weight: 600;
  1008. }
  1009. }
  1010. .tab {
  1011. padding: 15px 0;
  1012. border-radius: 0;
  1013. .playlist-item:not(:last-of-type) {
  1014. margin-bottom: 10px;
  1015. }
  1016. .load-more-button {
  1017. width: 100%;
  1018. margin-top: 10px;
  1019. }
  1020. }
  1021. }
  1022. }
  1023. .draggable-list-transition-move {
  1024. transition: transform 0.5s;
  1025. }
  1026. .draggable-list-ghost {
  1027. opacity: 0.5;
  1028. filter: brightness(95%);
  1029. }
  1030. </style>