PlaylistTabBase.vue 25 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093
  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. autoRequest.value.length === 0
  268. ) {
  269. const autorequestParsedItem = JSON.parse(
  270. autorequestLocalStorageItem
  271. );
  272. const autorequestUpdatedAt = new Date(
  273. autorequestParsedItem.updatedAt
  274. );
  275. const fiveMinutesAgo = new Date(
  276. new Date().getTime() - 5 * 60 * 1000
  277. );
  278. if (autorequestUpdatedAt > fiveMinutesAgo) {
  279. const playlists = [];
  280. const promises = autorequestParsedItem.playlistIds.map(
  281. playlistId =>
  282. new Promise<void>(resolve => {
  283. socket.dispatch(
  284. "playlists.getPlaylist",
  285. playlistId,
  286. res => {
  287. if (res.status === "success") {
  288. playlists.push(res.data.playlist);
  289. }
  290. resolve();
  291. }
  292. );
  293. })
  294. );
  295. Promise.all(promises).then(() => {
  296. addAutorequestPlaylists(playlists);
  297. });
  298. } else updateAutorequestLocalStorage();
  299. }
  300. });
  301. });
  302. </script>
  303. <template>
  304. <div class="playlist-tab-base">
  305. <div v-if="$slots.info" class="top-info has-text-centered">
  306. <slot name="info" />
  307. </div>
  308. <div class="tabs-container">
  309. <div class="tab-selection">
  310. <button
  311. v-if="
  312. type === 'autorequest' || station.type === 'community'
  313. "
  314. class="button is-default"
  315. :ref="el => (tabs['my-playlists-tab'] = el)"
  316. :class="{ selected: tab === 'my-playlists' }"
  317. @click="showTab('my-playlists')"
  318. >
  319. My Playlists
  320. </button>
  321. <button
  322. class="button is-default"
  323. :ref="el => (tabs['search-tab'] = el)"
  324. :class="{ selected: tab === 'search' }"
  325. @click="showTab('search')"
  326. >
  327. Search
  328. </button>
  329. <button
  330. class="button is-default"
  331. :ref="el => (tabs['current-tab'] = el)"
  332. :class="{ selected: tab === 'current' }"
  333. @click="showTab('current')"
  334. >
  335. Current
  336. <span class="tag" v-if="selectedPlaylists().length > 0">{{
  337. selectedPlaylists().length
  338. }}</span>
  339. </button>
  340. </div>
  341. <div class="tab" v-show="tab === 'search'">
  342. <div v-if="featuredPlaylists.length > 0">
  343. <label class="label"> Featured playlists </label>
  344. <playlist-item
  345. v-for="featuredPlaylist in featuredPlaylists"
  346. :key="`featuredKey-${featuredPlaylist._id}`"
  347. :playlist="featuredPlaylist"
  348. :show-owner="true"
  349. >
  350. <template #item-icon>
  351. <i
  352. class="material-icons blacklisted-icon"
  353. v-if="
  354. isSelected(
  355. featuredPlaylist._id,
  356. 'blacklist'
  357. )
  358. "
  359. :content="`This playlist is currently ${label(
  360. 'past',
  361. 'blacklist'
  362. )}`"
  363. v-tippy
  364. >
  365. block
  366. </i>
  367. <i
  368. class="material-icons"
  369. v-else-if="isSelected(featuredPlaylist._id)"
  370. :content="`This playlist is currently ${label(
  371. 'past'
  372. )}`"
  373. v-tippy
  374. >
  375. play_arrow
  376. </i>
  377. <i
  378. class="material-icons"
  379. v-else
  380. :content="`This playlist is currently not ${label(
  381. 'past'
  382. )}`"
  383. v-tippy
  384. >
  385. {{
  386. type === "blacklist"
  387. ? "block"
  388. : "play_disabled"
  389. }}
  390. </i>
  391. </template>
  392. <template #actions>
  393. <i
  394. v-if="
  395. type !== 'blacklist' &&
  396. isSelected(
  397. featuredPlaylist._id,
  398. 'blacklist'
  399. )
  400. "
  401. class="material-icons stop-icon"
  402. :content="`This playlist is ${label(
  403. 'past',
  404. 'blacklist'
  405. )} in this station`"
  406. v-tippy="{ theme: 'info' }"
  407. >play_disabled</i
  408. >
  409. <quick-confirm
  410. v-if="
  411. type !== 'blacklist' &&
  412. isSelected(featuredPlaylist._id)
  413. "
  414. @confirm="
  415. deselectPlaylist(featuredPlaylist._id)
  416. "
  417. >
  418. <i
  419. class="material-icons stop-icon"
  420. :content="`Stop ${label(
  421. 'present'
  422. )} songs from this playlist`"
  423. v-tippy
  424. >
  425. stop
  426. </i>
  427. </quick-confirm>
  428. <i
  429. v-if="
  430. type !== 'blacklist' &&
  431. !isSelected(featuredPlaylist._id) &&
  432. !isSelected(
  433. featuredPlaylist._id,
  434. 'blacklist'
  435. )
  436. "
  437. @click="selectPlaylist(featuredPlaylist)"
  438. class="material-icons play-icon"
  439. :content="`${label(
  440. 'future',
  441. null,
  442. true
  443. )} songs from this playlist`"
  444. v-tippy
  445. >play_arrow</i
  446. >
  447. <quick-confirm
  448. v-if="
  449. type === 'blacklist' &&
  450. !isSelected(
  451. featuredPlaylist._id,
  452. 'blacklist'
  453. )
  454. "
  455. @confirm="
  456. selectPlaylist(
  457. featuredPlaylist,
  458. 'blacklist'
  459. )
  460. "
  461. >
  462. <i
  463. class="material-icons stop-icon"
  464. :content="`${label(
  465. 'future',
  466. null,
  467. true
  468. )} Playlist`"
  469. v-tippy
  470. >block</i
  471. >
  472. </quick-confirm>
  473. <quick-confirm
  474. v-if="
  475. type === 'blacklist' &&
  476. isSelected(
  477. featuredPlaylist._id,
  478. 'blacklist'
  479. )
  480. "
  481. @confirm="
  482. deselectPlaylist(featuredPlaylist._id)
  483. "
  484. >
  485. <i
  486. class="material-icons stop-icon"
  487. :content="`Stop ${label(
  488. 'present'
  489. )} songs from this playlist`"
  490. v-tippy
  491. >
  492. stop
  493. </i>
  494. </quick-confirm>
  495. <i
  496. v-if="featuredPlaylist.createdBy === myUserId"
  497. @click="
  498. openModal({
  499. modal: 'editPlaylist',
  500. props: {
  501. playlistId: featuredPlaylist._id
  502. }
  503. })
  504. "
  505. class="material-icons edit-icon"
  506. content="Edit Playlist"
  507. v-tippy
  508. >edit</i
  509. >
  510. <i
  511. v-if="
  512. featuredPlaylist.createdBy !== myUserId &&
  513. (featuredPlaylist.privacy === 'public' ||
  514. hasPermission('playlists.view.others'))
  515. "
  516. @click="
  517. openModal({
  518. modal: 'editPlaylist',
  519. props: {
  520. playlistId: featuredPlaylist._id
  521. }
  522. })
  523. "
  524. class="material-icons edit-icon"
  525. content="View Playlist"
  526. v-tippy
  527. >visibility</i
  528. >
  529. </template>
  530. </playlist-item>
  531. <br />
  532. </div>
  533. <label class="label">Search for a playlist</label>
  534. <div class="control is-grouped input-with-button">
  535. <p class="control is-expanded">
  536. <input
  537. class="input"
  538. type="text"
  539. placeholder="Enter your playlist query here..."
  540. v-model="search.query"
  541. @keyup.enter="searchForPlaylists(1)"
  542. />
  543. </p>
  544. <p class="control">
  545. <a class="button is-info" @click="searchForPlaylists(1)"
  546. ><i class="material-icons icon-with-button"
  547. >search</i
  548. >Search</a
  549. >
  550. </p>
  551. </div>
  552. <div v-if="search.results.length > 0">
  553. <playlist-item
  554. v-for="playlist in search.results"
  555. :key="`searchKey-${playlist._id}`"
  556. :playlist="playlist"
  557. :show-owner="true"
  558. >
  559. <template #item-icon>
  560. <i
  561. class="material-icons blacklisted-icon"
  562. v-if="isSelected(playlist._id, 'blacklist')"
  563. :content="`This playlist is currently ${label(
  564. 'past',
  565. 'blacklist'
  566. )}`"
  567. v-tippy
  568. >
  569. block
  570. </i>
  571. <i
  572. class="material-icons"
  573. v-else-if="isSelected(playlist._id)"
  574. :content="`This playlist is currently ${label(
  575. 'past'
  576. )}`"
  577. v-tippy
  578. >
  579. play_arrow
  580. </i>
  581. <i
  582. class="material-icons"
  583. v-else
  584. :content="`This playlist is currently not ${label(
  585. 'past'
  586. )}`"
  587. v-tippy
  588. >
  589. {{
  590. type === "blacklist"
  591. ? "block"
  592. : "play_disabled"
  593. }}
  594. </i>
  595. </template>
  596. <template #actions>
  597. <i
  598. v-if="
  599. type !== 'blacklist' &&
  600. isSelected(playlist._id, 'blacklist')
  601. "
  602. class="material-icons stop-icon"
  603. :content="`This playlist is ${label(
  604. 'past',
  605. 'blacklist'
  606. )} in this station`"
  607. v-tippy="{ theme: 'info' }"
  608. >play_disabled</i
  609. >
  610. <quick-confirm
  611. v-if="
  612. type !== 'blacklist' &&
  613. isSelected(playlist._id)
  614. "
  615. @confirm="deselectPlaylist(playlist._id)"
  616. >
  617. <i
  618. class="material-icons stop-icon"
  619. :content="`Stop ${label(
  620. 'present'
  621. )} songs from this playlist`"
  622. v-tippy
  623. >
  624. stop
  625. </i>
  626. </quick-confirm>
  627. <i
  628. v-if="
  629. type !== 'blacklist' &&
  630. !isSelected(playlist._id) &&
  631. !isSelected(playlist._id, 'blacklist')
  632. "
  633. @click="selectPlaylist(playlist)"
  634. class="material-icons play-icon"
  635. :content="`${label(
  636. 'future',
  637. null,
  638. true
  639. )} songs from this playlist`"
  640. v-tippy
  641. >play_arrow</i
  642. >
  643. <quick-confirm
  644. v-if="
  645. type === 'blacklist' &&
  646. !isSelected(playlist._id, 'blacklist')
  647. "
  648. @confirm="selectPlaylist(playlist, 'blacklist')"
  649. >
  650. <i
  651. class="material-icons stop-icon"
  652. :content="`${label(
  653. 'future',
  654. null,
  655. true
  656. )} Playlist`"
  657. v-tippy
  658. >block</i
  659. >
  660. </quick-confirm>
  661. <quick-confirm
  662. v-if="
  663. type === 'blacklist' &&
  664. isSelected(playlist._id, 'blacklist')
  665. "
  666. @confirm="deselectPlaylist(playlist._id)"
  667. >
  668. <i
  669. class="material-icons stop-icon"
  670. :content="`Stop ${label(
  671. 'present'
  672. )} songs from this playlist`"
  673. v-tippy
  674. >
  675. stop
  676. </i>
  677. </quick-confirm>
  678. <i
  679. v-if="playlist.createdBy === myUserId"
  680. @click="
  681. openModal({
  682. modal: 'editPlaylist',
  683. props: { playlistId: playlist._id }
  684. })
  685. "
  686. class="material-icons edit-icon"
  687. content="Edit Playlist"
  688. v-tippy
  689. >edit</i
  690. >
  691. <i
  692. v-if="
  693. playlist.createdBy !== myUserId &&
  694. (playlist.privacy === 'public' ||
  695. hasPermission('playlists.view.others'))
  696. "
  697. @click="
  698. openModal({
  699. modal: 'editPlaylist',
  700. props: { playlistId: playlist._id }
  701. })
  702. "
  703. class="material-icons edit-icon"
  704. content="View Playlist"
  705. v-tippy
  706. >visibility</i
  707. >
  708. </template>
  709. </playlist-item>
  710. <button
  711. v-if="resultsLeftCount > 0"
  712. class="button is-primary load-more-button"
  713. @click="searchForPlaylists(search.page + 1)"
  714. >
  715. Load {{ nextPageResultsCount }} more results
  716. </button>
  717. </div>
  718. </div>
  719. <div class="tab" v-show="tab === 'current'">
  720. <div v-if="selectedPlaylists().length > 0">
  721. <playlist-item
  722. v-for="playlist in selectedPlaylists()"
  723. :key="`key-${playlist._id}`"
  724. :playlist="playlist"
  725. :show-owner="true"
  726. >
  727. <template #item-icon>
  728. <i
  729. class="material-icons"
  730. :class="{
  731. 'blacklisted-icon': type === 'blacklist'
  732. }"
  733. :content="`This playlist is currently ${label(
  734. 'past'
  735. )}`"
  736. v-tippy
  737. >
  738. {{
  739. type === "blacklist"
  740. ? "block"
  741. : "play_arrow"
  742. }}
  743. </i>
  744. </template>
  745. <template #actions>
  746. <quick-confirm
  747. @confirm="deselectPlaylist(playlist._id)"
  748. >
  749. <i
  750. class="material-icons stop-icon"
  751. :content="`Stop ${label(
  752. 'present'
  753. )} songs from this playlist`"
  754. v-tippy
  755. >
  756. stop
  757. </i>
  758. </quick-confirm>
  759. <i
  760. v-if="playlist.createdBy === myUserId"
  761. @click="
  762. openModal({
  763. modal: 'editPlaylist',
  764. props: { playlistId: playlist._id }
  765. })
  766. "
  767. class="material-icons edit-icon"
  768. content="Edit Playlist"
  769. v-tippy
  770. >edit</i
  771. >
  772. <i
  773. v-if="
  774. playlist.createdBy !== myUserId &&
  775. (playlist.privacy === 'public' ||
  776. hasPermission('playlists.view.others'))
  777. "
  778. @click="
  779. openModal({
  780. modal: 'editPlaylist',
  781. props: { playlistId: playlist._id }
  782. })
  783. "
  784. class="material-icons edit-icon"
  785. content="View Playlist"
  786. v-tippy
  787. >visibility</i
  788. >
  789. </template>
  790. </playlist-item>
  791. </div>
  792. <p v-else class="has-text-centered scrollable-list">
  793. No playlists currently {{ label("present") }}.
  794. </p>
  795. </div>
  796. <div
  797. v-if="type === 'autorequest' || station.type === 'community'"
  798. class="tab"
  799. v-show="tab === 'my-playlists'"
  800. >
  801. <div
  802. class="menu-list scrollable-list"
  803. v-if="playlists.length > 0"
  804. >
  805. <draggable-list
  806. v-model:list="playlists"
  807. item-key="_id"
  808. @start="drag = true"
  809. @end="drag = false"
  810. @update="savePlaylistOrder"
  811. >
  812. <template #item="{ element }">
  813. <playlist-item :playlist="element">
  814. <template #item-icon>
  815. <i
  816. class="material-icons blacklisted-icon"
  817. v-if="
  818. isSelected(element._id, 'blacklist')
  819. "
  820. :content="`This playlist is currently ${label(
  821. 'past',
  822. 'blacklist'
  823. )}`"
  824. v-tippy
  825. >
  826. block
  827. </i>
  828. <i
  829. class="material-icons"
  830. v-else-if="isSelected(element._id)"
  831. :content="`This playlist is currently ${label(
  832. 'past'
  833. )}`"
  834. v-tippy
  835. >
  836. play_arrow
  837. </i>
  838. <i
  839. class="material-icons"
  840. v-else
  841. :content="`This playlist is currently not ${label(
  842. 'past'
  843. )}`"
  844. v-tippy
  845. >
  846. {{
  847. type === "blacklist"
  848. ? "block"
  849. : "play_disabled"
  850. }}
  851. </i>
  852. </template>
  853. <template #actions>
  854. <i
  855. v-if="
  856. type !== 'blacklist' &&
  857. isSelected(element._id, 'blacklist')
  858. "
  859. class="material-icons stop-icon"
  860. :content="`This playlist is ${label(
  861. 'past',
  862. 'blacklist'
  863. )} in this station`"
  864. v-tippy="{ theme: 'info' }"
  865. >play_disabled</i
  866. >
  867. <quick-confirm
  868. v-if="
  869. type !== 'blacklist' &&
  870. isSelected(element._id)
  871. "
  872. @confirm="deselectPlaylist(element._id)"
  873. >
  874. <i
  875. class="material-icons stop-icon"
  876. :content="`Stop ${label(
  877. 'present'
  878. )} songs from this playlist`"
  879. v-tippy
  880. >
  881. stop
  882. </i>
  883. </quick-confirm>
  884. <i
  885. v-if="
  886. type !== 'blacklist' &&
  887. !isSelected(element._id) &&
  888. !isSelected(
  889. element._id,
  890. 'blacklist'
  891. )
  892. "
  893. @click="selectPlaylist(element)"
  894. class="material-icons play-icon"
  895. :content="`${label(
  896. 'future',
  897. null,
  898. true
  899. )} songs from this playlist`"
  900. v-tippy
  901. >play_arrow</i
  902. >
  903. <quick-confirm
  904. v-if="
  905. type === 'blacklist' &&
  906. !isSelected(
  907. element._id,
  908. 'blacklist'
  909. )
  910. "
  911. @confirm="
  912. selectPlaylist(element, 'blacklist')
  913. "
  914. >
  915. <i
  916. class="material-icons stop-icon"
  917. :content="`${label(
  918. 'future',
  919. null,
  920. true
  921. )} Playlist`"
  922. v-tippy
  923. >block</i
  924. >
  925. </quick-confirm>
  926. <quick-confirm
  927. v-if="
  928. type === 'blacklist' &&
  929. isSelected(element._id, 'blacklist')
  930. "
  931. @confirm="deselectPlaylist(element._id)"
  932. >
  933. <i
  934. class="material-icons stop-icon"
  935. :content="`Stop ${label(
  936. 'present'
  937. )} songs from this playlist`"
  938. v-tippy
  939. >
  940. stop
  941. </i>
  942. </quick-confirm>
  943. <i
  944. @click="
  945. openModal({
  946. modal: 'editPlaylist',
  947. props: {
  948. playlistId: element._id
  949. }
  950. })
  951. "
  952. class="material-icons edit-icon"
  953. content="Edit Playlist"
  954. v-tippy
  955. >edit</i
  956. >
  957. </template>
  958. </playlist-item>
  959. </template>
  960. </draggable-list>
  961. </div>
  962. <p v-else class="has-text-centered scrollable-list">
  963. You don't have any playlists
  964. </p>
  965. <button
  966. class="button is-primary"
  967. id="create-new-playlist-button"
  968. @click="openModal('createPlaylist')"
  969. >
  970. Create new playlist
  971. </button>
  972. </div>
  973. </div>
  974. </div>
  975. </template>
  976. <style lang="less" scoped>
  977. .night-mode {
  978. .tabs-container .tab-selection .button {
  979. background: var(--dark-grey) !important;
  980. color: var(--white) !important;
  981. }
  982. }
  983. .blacklisted-icon {
  984. color: var(--dark-red);
  985. }
  986. .playlist-tab-base {
  987. .top-info {
  988. font-size: 15px;
  989. margin-bottom: 15px;
  990. }
  991. .tabs-container {
  992. .tab-selection {
  993. display: flex;
  994. overflow-x: auto;
  995. .button {
  996. border-radius: 0;
  997. border: 0;
  998. text-transform: uppercase;
  999. font-size: 14px;
  1000. color: var(--dark-grey-3);
  1001. background-color: var(--light-grey-2);
  1002. flex-grow: 1;
  1003. height: 32px;
  1004. &:not(:first-of-type) {
  1005. margin-left: 5px;
  1006. }
  1007. }
  1008. .selected {
  1009. background-color: var(--primary-color) !important;
  1010. color: var(--white) !important;
  1011. font-weight: 600;
  1012. }
  1013. }
  1014. .tab {
  1015. padding: 15px 0;
  1016. border-radius: 0;
  1017. .playlist-item:not(:last-of-type) {
  1018. margin-bottom: 10px;
  1019. }
  1020. .load-more-button {
  1021. width: 100%;
  1022. margin-top: 10px;
  1023. }
  1024. }
  1025. }
  1026. }
  1027. .draggable-list-transition-move {
  1028. transition: transform 0.5s;
  1029. }
  1030. .draggable-list-ghost {
  1031. opacity: 0.5;
  1032. filter: brightness(95%);
  1033. }
  1034. </style>