PlaylistTabBase.vue 27 KB

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