PlaylistTabBase.vue 27 KB

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