PlaylistTabBase.vue 25 KB

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