PlaylistTabBase.vue 24 KB

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