PlaylistTabBase.vue 24 KB

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