PlaylistTabBase.vue 25 KB

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