PlaylistTabBase.vue 29 KB

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