PlaylistTabBase.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173
  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="
  765. selectedPlaylists().length > 0 && type === 'autorequest'
  766. "
  767. >
  768. You are currently autorequesting a mix of
  769. {{ totalUniqueAutorequestableYoutubeIds.length }} different
  770. songs. Of these, we can currently autorequest
  771. {{ actuallyAutorequestingYoutubeIds.length }} songs.
  772. <br />
  773. Songs that
  774. <span
  775. v-if="
  776. station.requests
  777. .autorequestDisallowRecentlyPlayedEnabled
  778. "
  779. >were played recently or</span
  780. >
  781. are currently in the queue or playing will not be
  782. autorequested.
  783. <br />
  784. <br />
  785. </p>
  786. <div v-if="selectedPlaylists().length > 0">
  787. <playlist-item
  788. v-for="playlist in selectedPlaylists()"
  789. :key="`key-${playlist._id}`"
  790. :playlist="playlist"
  791. :show-owner="true"
  792. >
  793. <template #item-icon>
  794. <i
  795. class="material-icons"
  796. :class="{
  797. 'blacklisted-icon': type === 'blacklist'
  798. }"
  799. :content="`This playlist is currently ${label(
  800. 'past'
  801. )}`"
  802. v-tippy
  803. >
  804. {{
  805. type === "blacklist"
  806. ? "block"
  807. : "play_arrow"
  808. }}
  809. </i>
  810. </template>
  811. <template #actions>
  812. <quick-confirm
  813. @confirm="deselectPlaylist(playlist._id)"
  814. >
  815. <i
  816. class="material-icons stop-icon"
  817. :content="`Stop ${label(
  818. 'present'
  819. )} songs from this playlist`"
  820. v-tippy
  821. >
  822. stop
  823. </i>
  824. </quick-confirm>
  825. <i
  826. v-if="playlist.createdBy === myUserId"
  827. @click="
  828. openModal({
  829. modal: 'editPlaylist',
  830. props: { playlistId: playlist._id }
  831. })
  832. "
  833. class="material-icons edit-icon"
  834. content="Edit Playlist"
  835. v-tippy
  836. >edit</i
  837. >
  838. <i
  839. v-if="
  840. playlist.createdBy !== myUserId &&
  841. (playlist.privacy === 'public' ||
  842. hasPermission('playlists.view.others'))
  843. "
  844. @click="
  845. openModal({
  846. modal: 'editPlaylist',
  847. props: { playlistId: playlist._id }
  848. })
  849. "
  850. class="material-icons edit-icon"
  851. content="View Playlist"
  852. v-tippy
  853. >visibility</i
  854. >
  855. </template>
  856. </playlist-item>
  857. </div>
  858. <p v-else class="has-text-centered scrollable-list">
  859. No playlists currently {{ label("present") }}.
  860. </p>
  861. </div>
  862. <div
  863. v-if="type === 'autorequest' || station.type === 'community'"
  864. class="tab"
  865. v-show="tab === 'my-playlists'"
  866. >
  867. <div
  868. class="menu-list scrollable-list"
  869. v-if="playlists.length > 0"
  870. >
  871. <draggable-list
  872. v-model:list="playlists"
  873. item-key="_id"
  874. @start="drag = true"
  875. @end="drag = false"
  876. @update="savePlaylistOrder"
  877. >
  878. <template #item="{ element }">
  879. <playlist-item :playlist="element">
  880. <template #item-icon>
  881. <i
  882. class="material-icons blacklisted-icon"
  883. v-if="
  884. isSelected(element._id, 'blacklist')
  885. "
  886. :content="`This playlist is currently ${label(
  887. 'past',
  888. 'blacklist'
  889. )}`"
  890. v-tippy
  891. >
  892. block
  893. </i>
  894. <i
  895. class="material-icons"
  896. v-else-if="isSelected(element._id)"
  897. :content="`This playlist is currently ${label(
  898. 'past'
  899. )}`"
  900. v-tippy
  901. >
  902. play_arrow
  903. </i>
  904. <i
  905. class="material-icons"
  906. v-else
  907. :content="`This playlist is currently not ${label(
  908. 'past'
  909. )}`"
  910. v-tippy
  911. >
  912. {{
  913. type === "blacklist"
  914. ? "block"
  915. : "play_disabled"
  916. }}
  917. </i>
  918. </template>
  919. <template #actions>
  920. <i
  921. v-if="
  922. type !== 'blacklist' &&
  923. isSelected(element._id, 'blacklist')
  924. "
  925. class="material-icons stop-icon"
  926. :content="`This playlist is ${label(
  927. 'past',
  928. 'blacklist'
  929. )} in this station`"
  930. v-tippy="{ theme: 'info' }"
  931. >play_disabled</i
  932. >
  933. <quick-confirm
  934. v-if="
  935. type !== 'blacklist' &&
  936. isSelected(element._id)
  937. "
  938. @confirm="deselectPlaylist(element._id)"
  939. >
  940. <i
  941. class="material-icons stop-icon"
  942. :content="`Stop ${label(
  943. 'present'
  944. )} songs from this playlist`"
  945. v-tippy
  946. >
  947. stop
  948. </i>
  949. </quick-confirm>
  950. <i
  951. v-if="
  952. type !== 'blacklist' &&
  953. !isSelected(element._id) &&
  954. !isSelected(
  955. element._id,
  956. 'blacklist'
  957. )
  958. "
  959. @click="selectPlaylist(element)"
  960. class="material-icons play-icon"
  961. :content="`${label(
  962. 'future',
  963. null,
  964. true
  965. )} songs from this playlist`"
  966. v-tippy
  967. >play_arrow</i
  968. >
  969. <quick-confirm
  970. v-if="
  971. type === 'blacklist' &&
  972. !isSelected(
  973. element._id,
  974. 'blacklist'
  975. )
  976. "
  977. @confirm="
  978. selectPlaylist(element, 'blacklist')
  979. "
  980. >
  981. <i
  982. class="material-icons stop-icon"
  983. :content="`${label(
  984. 'future',
  985. null,
  986. true
  987. )} Playlist`"
  988. v-tippy
  989. >block</i
  990. >
  991. </quick-confirm>
  992. <quick-confirm
  993. v-if="
  994. type === 'blacklist' &&
  995. isSelected(element._id, 'blacklist')
  996. "
  997. @confirm="deselectPlaylist(element._id)"
  998. >
  999. <i
  1000. class="material-icons stop-icon"
  1001. :content="`Stop ${label(
  1002. 'present'
  1003. )} songs from this playlist`"
  1004. v-tippy
  1005. >
  1006. stop
  1007. </i>
  1008. </quick-confirm>
  1009. <i
  1010. @click="
  1011. openModal({
  1012. modal: 'editPlaylist',
  1013. props: {
  1014. playlistId: element._id
  1015. }
  1016. })
  1017. "
  1018. class="material-icons edit-icon"
  1019. content="Edit Playlist"
  1020. v-tippy
  1021. >edit</i
  1022. >
  1023. </template>
  1024. </playlist-item>
  1025. </template>
  1026. </draggable-list>
  1027. </div>
  1028. <p v-else class="has-text-centered scrollable-list">
  1029. You don't have any playlists
  1030. </p>
  1031. <button
  1032. class="button is-primary"
  1033. id="create-new-playlist-button"
  1034. @click="openModal('createPlaylist')"
  1035. >
  1036. Create new playlist
  1037. </button>
  1038. </div>
  1039. </div>
  1040. </div>
  1041. </template>
  1042. <style lang="less" scoped>
  1043. .night-mode {
  1044. .tabs-container .tab-selection .button {
  1045. background: var(--dark-grey) !important;
  1046. color: var(--white) !important;
  1047. }
  1048. }
  1049. .blacklisted-icon {
  1050. color: var(--dark-red);
  1051. }
  1052. .playlist-tab-base {
  1053. .top-info {
  1054. font-size: 15px;
  1055. margin-bottom: 15px;
  1056. }
  1057. .tabs-container {
  1058. .tab-selection {
  1059. display: flex;
  1060. overflow-x: auto;
  1061. .button {
  1062. border-radius: 0;
  1063. border: 0;
  1064. text-transform: uppercase;
  1065. font-size: 14px;
  1066. color: var(--dark-grey-3);
  1067. background-color: var(--light-grey-2);
  1068. flex-grow: 1;
  1069. height: 32px;
  1070. &:not(:first-of-type) {
  1071. margin-left: 5px;
  1072. }
  1073. }
  1074. .selected {
  1075. background-color: var(--primary-color) !important;
  1076. color: var(--white) !important;
  1077. font-weight: 600;
  1078. }
  1079. }
  1080. .tab {
  1081. padding: 15px 0;
  1082. border-radius: 0;
  1083. .playlist-item:not(:last-of-type) {
  1084. margin-bottom: 10px;
  1085. }
  1086. .load-more-button {
  1087. width: 100%;
  1088. margin-top: 10px;
  1089. }
  1090. }
  1091. }
  1092. }
  1093. .draggable-list-transition-move {
  1094. transition: transform 0.5s;
  1095. }
  1096. .draggable-list-ghost {
  1097. opacity: 0.5;
  1098. filter: brightness(95%);
  1099. }
  1100. </style>