PlaylistTabBase.vue 24 KB

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