index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. <script setup lang="ts">
  2. import {
  3. defineAsyncComponent,
  4. ref,
  5. watch,
  6. onMounted,
  7. onBeforeUnmount
  8. } from "vue";
  9. import Toast from "toasters";
  10. import { storeToRefs } from "pinia";
  11. import { useWebsocketsStore } from "@/stores/websockets";
  12. import { useUserAuthStore } from "@/stores/userAuth";
  13. import { useModalsStore } from "@/stores/modals";
  14. import { useManageStationStore } from "@/stores/manageStation";
  15. const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
  16. const Queue = defineAsyncComponent(() => import("@/components/Queue.vue"));
  17. const SongItem = defineAsyncComponent(
  18. () => import("@/components/SongItem.vue")
  19. );
  20. const StationInfoBox = defineAsyncComponent(
  21. () => import("@/components/StationInfoBox.vue")
  22. );
  23. const Settings = defineAsyncComponent(() => import("./Settings.vue"));
  24. const PlaylistTabBase = defineAsyncComponent(
  25. () => import("@/components/PlaylistTabBase.vue")
  26. );
  27. const Request = defineAsyncComponent(() => import("@/components/Request.vue"));
  28. const QuickConfirm = defineAsyncComponent(
  29. () => import("@/components/QuickConfirm.vue")
  30. );
  31. const props = defineProps({
  32. modalUuid: { type: String, default: "" }
  33. });
  34. const tabs = ref([]);
  35. const userAuthStore = useUserAuthStore();
  36. const { loggedIn, userId } = storeToRefs(userAuthStore);
  37. const { socket } = useWebsocketsStore();
  38. const manageStationStore = useManageStationStore(props);
  39. const {
  40. stationId,
  41. sector,
  42. tab,
  43. station,
  44. stationPlaylist,
  45. autofill,
  46. blacklist,
  47. stationPaused,
  48. currentSong
  49. } = storeToRefs(manageStationStore);
  50. const {
  51. editStation,
  52. setAutofillPlaylists,
  53. setBlacklist,
  54. clearStation,
  55. updateSongsList,
  56. updateStationPlaylist,
  57. repositionSongInList,
  58. updateStationPaused,
  59. updateCurrentSong,
  60. updateStation,
  61. updateIsFavorited,
  62. hasPermission,
  63. addDj,
  64. removeDj,
  65. updatePermissions
  66. } = manageStationStore;
  67. const { closeCurrentModal } = useModalsStore();
  68. const showTab = payload => {
  69. if (tabs.value[`${payload}-tab`])
  70. tabs.value[`${payload}-tab`].scrollIntoView({ block: "nearest" });
  71. manageStationStore.showTab(payload);
  72. };
  73. const canRequest = () =>
  74. station.value &&
  75. loggedIn.value &&
  76. station.value.requests &&
  77. station.value.requests.enabled &&
  78. (station.value.requests.access === "user" ||
  79. (station.value.requests.access === "owner" &&
  80. hasPermission("stations.request")));
  81. const removeStation = () => {
  82. socket.dispatch("stations.remove", stationId.value, res => {
  83. new Toast(res.message);
  84. });
  85. };
  86. const resetQueue = () => {
  87. socket.dispatch("stations.resetQueue", stationId.value, res => {
  88. if (res.status !== "success")
  89. new Toast({
  90. content: `Error: ${res.message}`,
  91. timeout: 8000
  92. });
  93. else new Toast({ content: res.message, timeout: 4000 });
  94. });
  95. };
  96. const findTabOrClose = () => {
  97. if (hasPermission("stations.update")) return showTab("settings");
  98. if (canRequest()) return showTab("request");
  99. if (hasPermission("stations.autofill")) return showTab("autofill");
  100. if (hasPermission("stations.blacklist")) return showTab("blacklist");
  101. if (!(sector.value === "home" && hasPermission("stations.view")))
  102. return closeCurrentModal();
  103. return null;
  104. };
  105. watch(
  106. () => hasPermission("stations.update"),
  107. value => {
  108. if (!value && tab.value === "settings") findTabOrClose();
  109. }
  110. );
  111. watch(
  112. () => hasPermission("stations.request") && station.value.requests.enabled,
  113. value => {
  114. if (!value && tab.value === "request") findTabOrClose();
  115. }
  116. );
  117. watch(
  118. () => hasPermission("stations.autofill") && station.value.autofill.enabled,
  119. value => {
  120. if (!value && tab.value === "autofill") findTabOrClose();
  121. }
  122. );
  123. watch(
  124. () => hasPermission("stations.blacklist"),
  125. value => {
  126. if (!value && tab.value === "blacklist") findTabOrClose();
  127. }
  128. );
  129. onMounted(() => {
  130. socket.dispatch(`stations.getStationById`, stationId.value, async res => {
  131. if (res.status === "success") {
  132. editStation(res.data.station);
  133. await updatePermissions();
  134. findTabOrClose();
  135. const currentSong = res.data.station.currentSong
  136. ? res.data.station.currentSong
  137. : {};
  138. updateCurrentSong(currentSong);
  139. updateStationPaused(res.data.station.paused);
  140. socket.dispatch(
  141. "stations.getStationAutofillPlaylistsById",
  142. stationId.value,
  143. res => {
  144. if (res.status === "success")
  145. setAutofillPlaylists(res.data.playlists);
  146. }
  147. );
  148. socket.dispatch(
  149. "stations.getStationBlacklistById",
  150. stationId.value,
  151. res => {
  152. if (res.status === "success")
  153. setBlacklist(res.data.playlists);
  154. }
  155. );
  156. if (hasPermission("stations.view")) {
  157. socket.dispatch(
  158. "playlists.getPlaylistForStation",
  159. stationId.value,
  160. true,
  161. res => {
  162. if (res.status === "success") {
  163. updateStationPlaylist(res.data.playlist);
  164. }
  165. }
  166. );
  167. }
  168. socket.dispatch("stations.getQueue", stationId.value, res => {
  169. if (res.status === "success") updateSongsList(res.data.queue);
  170. });
  171. socket.dispatch(
  172. "apis.joinRoom",
  173. `manage-station.${stationId.value}`
  174. );
  175. socket.on(
  176. "event:station.updated",
  177. res => {
  178. updateStation(res.data.station);
  179. },
  180. { modalUuid: props.modalUuid }
  181. );
  182. socket.on(
  183. "event:station.autofillPlaylist",
  184. res => {
  185. const { playlist } = res.data;
  186. const playlistIndex = autofill.value
  187. .map(autofillPlaylist => autofillPlaylist._id)
  188. .indexOf(playlist._id);
  189. if (playlistIndex === -1) autofill.value.push(playlist);
  190. },
  191. { modalUuid: props.modalUuid }
  192. );
  193. socket.on(
  194. "event:station.blacklistedPlaylist",
  195. res => {
  196. const { playlist } = res.data;
  197. const playlistIndex = blacklist.value
  198. .map(blacklistedPlaylist => blacklistedPlaylist._id)
  199. .indexOf(playlist._id);
  200. if (playlistIndex === -1) blacklist.value.push(playlist);
  201. },
  202. { modalUuid: props.modalUuid }
  203. );
  204. socket.on(
  205. "event:station.removedAutofillPlaylist",
  206. res => {
  207. const { playlistId } = res.data;
  208. const playlistIndex = autofill.value
  209. .map(playlist => playlist._id)
  210. .indexOf(playlistId);
  211. if (playlistIndex >= 0)
  212. autofill.value.splice(playlistIndex, 1);
  213. },
  214. { modalUuid: props.modalUuid }
  215. );
  216. socket.on(
  217. "event:station.removedBlacklistedPlaylist",
  218. res => {
  219. const { playlistId } = res.data;
  220. const playlistIndex = blacklist.value
  221. .map(playlist => playlist._id)
  222. .indexOf(playlistId);
  223. if (playlistIndex >= 0)
  224. blacklist.value.splice(playlistIndex, 1);
  225. },
  226. { modalUuid: props.modalUuid }
  227. );
  228. socket.on(
  229. "event:station.deleted",
  230. () => {
  231. new Toast(`The station you were editing was deleted.`);
  232. closeCurrentModal();
  233. },
  234. { modalUuid: props.modalUuid }
  235. );
  236. socket.on(
  237. "event:user.station.favorited",
  238. res => {
  239. if (res.data.stationId === stationId.value)
  240. updateIsFavorited(true);
  241. },
  242. { modalUuid: props.modalUuid }
  243. );
  244. socket.on(
  245. "event:user.station.unfavorited",
  246. res => {
  247. if (res.data.stationId === stationId.value)
  248. updateIsFavorited(false);
  249. },
  250. { modalUuid: props.modalUuid }
  251. );
  252. } else {
  253. new Toast(`Station with that ID not found`);
  254. closeCurrentModal();
  255. }
  256. });
  257. socket.on(
  258. "event:manageStation.queue.updated",
  259. res => {
  260. if (res.data.stationId === stationId.value)
  261. updateSongsList(res.data.queue);
  262. },
  263. { modalUuid: props.modalUuid }
  264. );
  265. socket.on(
  266. "event:manageStation.queue.song.repositioned",
  267. res => {
  268. if (res.data.stationId === stationId.value)
  269. repositionSongInList(res.data.song);
  270. },
  271. { modalUuid: props.modalUuid }
  272. );
  273. socket.on(
  274. "event:station.pause",
  275. res => {
  276. if (res.data.stationId === stationId.value)
  277. updateStationPaused(true);
  278. },
  279. { modalUuid: props.modalUuid }
  280. );
  281. socket.on(
  282. "event:station.resume",
  283. res => {
  284. if (res.data.stationId === stationId.value)
  285. updateStationPaused(false);
  286. },
  287. { modalUuid: props.modalUuid }
  288. );
  289. socket.on(
  290. "event:station.nextSong",
  291. res => {
  292. if (res.data.stationId === stationId.value)
  293. updateCurrentSong(res.data.currentSong || {});
  294. },
  295. { modalUuid: props.modalUuid }
  296. );
  297. socket.on("event:manageStation.djs.added", res => {
  298. if (res.data.stationId === stationId.value) {
  299. if (res.data.user._id === userId.value) updatePermissions();
  300. addDj(res.data.user);
  301. }
  302. });
  303. socket.on("event:manageStation.djs.removed", res => {
  304. if (res.data.stationId === stationId.value) {
  305. if (res.data.user._id === userId.value) updatePermissions();
  306. removeDj(res.data.user);
  307. }
  308. });
  309. socket.on("keep.event:user.role.updated", () => {
  310. updatePermissions();
  311. });
  312. if (hasPermission("stations.view")) {
  313. socket.on(
  314. "event:playlist.song.added",
  315. res => {
  316. if (stationPlaylist.value._id === res.data.playlistId)
  317. stationPlaylist.value.songs.push(res.data.song);
  318. },
  319. {
  320. modalUuid: props.modalUuid
  321. }
  322. );
  323. socket.on(
  324. "event:playlist.song.removed",
  325. res => {
  326. if (stationPlaylist.value._id === res.data.playlistId) {
  327. // remove song from array of playlists
  328. stationPlaylist.value.songs.forEach((song, index) => {
  329. if (song.youtubeId === res.data.youtubeId)
  330. stationPlaylist.value.songs.splice(index, 1);
  331. });
  332. }
  333. },
  334. {
  335. modalUuid: props.modalUuid
  336. }
  337. );
  338. socket.on(
  339. "event:playlist.songs.repositioned",
  340. res => {
  341. if (stationPlaylist.value._id === res.data.playlistId) {
  342. // for each song that has a new position
  343. res.data.songsBeingChanged.forEach(changedSong => {
  344. stationPlaylist.value.songs.forEach((song, index) => {
  345. // find song locally
  346. if (song.youtubeId === changedSong.youtubeId) {
  347. // change song position attribute
  348. stationPlaylist.value.songs[index].position =
  349. changedSong.position;
  350. // reposition in array if needed
  351. if (index !== changedSong.position - 1)
  352. stationPlaylist.value.songs.splice(
  353. changedSong.position - 1,
  354. 0,
  355. stationPlaylist.value.songs.splice(
  356. index,
  357. 1
  358. )[0]
  359. );
  360. }
  361. });
  362. });
  363. }
  364. },
  365. {
  366. modalUuid: props.modalUuid
  367. }
  368. );
  369. }
  370. });
  371. onBeforeUnmount(() => {
  372. socket.dispatch(
  373. "apis.leaveRoom",
  374. `manage-station.${stationId.value}`,
  375. () => {}
  376. );
  377. if (hasPermission("stations.update")) showTab("settings");
  378. clearStation();
  379. // Delete the Pinia store that was created for this modal, after all other cleanup tasks are performed
  380. manageStationStore.$dispose();
  381. });
  382. </script>
  383. <template>
  384. <modal
  385. v-if="station"
  386. :title="
  387. sector === 'home' && !hasPermission('stations.view.manage')
  388. ? 'View Queue'
  389. : !hasPermission('stations.view.manage')
  390. ? 'Add Song to Queue'
  391. : 'Manage Station'
  392. "
  393. :style="`--primary-color: var(--${station.theme})`"
  394. class="manage-station-modal"
  395. :size="
  396. hasPermission('stations.view.manage') || sector !== 'home'
  397. ? 'wide'
  398. : null
  399. "
  400. :split="hasPermission('stations.view.manage') || sector !== 'home'"
  401. >
  402. <template #body v-if="station && station._id">
  403. <div class="left-section">
  404. <div class="section">
  405. <div class="station-info-box-wrapper">
  406. <station-info-box
  407. :station="station"
  408. :station-paused="stationPaused"
  409. :show-go-to-station="sector !== 'station'"
  410. :sector="'manageStation'"
  411. :modal-uuid="modalUuid"
  412. />
  413. </div>
  414. <div
  415. v-if="
  416. hasPermission('stations.view.manage') ||
  417. sector !== 'home'
  418. "
  419. >
  420. <div class="tab-selection">
  421. <button
  422. v-if="hasPermission('stations.update')"
  423. class="button is-default"
  424. :class="{ selected: tab === 'settings' }"
  425. :ref="el => (tabs['settings-tab'] = el)"
  426. @click="showTab('settings')"
  427. >
  428. Settings
  429. </button>
  430. <button
  431. v-if="canRequest()"
  432. class="button is-default"
  433. :class="{ selected: tab === 'request' }"
  434. :ref="el => (tabs['request-tab'] = el)"
  435. @click="showTab('request')"
  436. >
  437. Request
  438. </button>
  439. <button
  440. v-if="
  441. hasPermission('stations.autofill') &&
  442. station.autofill.enabled
  443. "
  444. class="button is-default"
  445. :class="{ selected: tab === 'autofill' }"
  446. :ref="el => (tabs['autofill-tab'] = el)"
  447. @click="showTab('autofill')"
  448. >
  449. Autofill
  450. </button>
  451. <button
  452. v-if="hasPermission('stations.blacklist')"
  453. class="button is-default"
  454. :class="{ selected: tab === 'blacklist' }"
  455. :ref="el => (tabs['blacklist-tab'] = el)"
  456. @click="showTab('blacklist')"
  457. >
  458. Blacklist
  459. </button>
  460. </div>
  461. <settings
  462. v-if="hasPermission('stations.update')"
  463. class="tab"
  464. v-show="tab === 'settings'"
  465. :modal-uuid="modalUuid"
  466. ref="settingsTabComponent"
  467. />
  468. <request
  469. v-if="canRequest()"
  470. class="tab"
  471. v-show="tab === 'request'"
  472. :sector="'manageStation'"
  473. :disable-auto-request="sector !== 'station'"
  474. :modal-uuid="modalUuid"
  475. />
  476. <playlist-tab-base
  477. v-if="
  478. hasPermission('stations.autofill') &&
  479. station.autofill.enabled
  480. "
  481. class="tab"
  482. v-show="tab === 'autofill'"
  483. :type="'autofill'"
  484. :modal-uuid="modalUuid"
  485. >
  486. <template #info>
  487. <p>
  488. Select playlists to automatically add songs
  489. within to the queue
  490. </p>
  491. </template>
  492. </playlist-tab-base>
  493. <playlist-tab-base
  494. v-if="hasPermission('stations.blacklist')"
  495. class="tab"
  496. v-show="tab === 'blacklist'"
  497. :type="'blacklist'"
  498. :modal-uuid="modalUuid"
  499. >
  500. <template #info>
  501. <p>
  502. Blacklist a playlist to prevent all songs
  503. within from playing in this station
  504. </p>
  505. </template>
  506. </playlist-tab-base>
  507. </div>
  508. </div>
  509. </div>
  510. <div class="right-section">
  511. <div class="section">
  512. <div class="queue-title">
  513. <h4 class="section-title">Queue</h4>
  514. </div>
  515. <hr class="section-horizontal-rule" />
  516. <song-item
  517. v-if="currentSong.youtubeId"
  518. :song="currentSong"
  519. :requested-by="true"
  520. header="Currently Playing.."
  521. class="currently-playing"
  522. />
  523. <queue :modal-uuid="modalUuid" sector="manageStation" />
  524. </div>
  525. </div>
  526. </template>
  527. <template #footer>
  528. <div class="right">
  529. <quick-confirm
  530. v-if="hasPermission('stations.queue.reset')"
  531. @confirm="resetQueue()"
  532. >
  533. <a class="button is-danger">Reset queue</a>
  534. </quick-confirm>
  535. <quick-confirm
  536. v-if="hasPermission('stations.remove')"
  537. @confirm="removeStation()"
  538. >
  539. <button class="button is-danger">Delete station</button>
  540. </quick-confirm>
  541. </div>
  542. </template>
  543. </modal>
  544. </template>
  545. <style lang="less">
  546. .manage-station-modal.modal .modal-card {
  547. .tab > button {
  548. width: 100%;
  549. margin-bottom: 10px;
  550. }
  551. .currently-playing.song-item {
  552. .thumbnail {
  553. min-width: 130px;
  554. width: 130px;
  555. height: 130px;
  556. }
  557. }
  558. }
  559. </style>
  560. <style lang="less" scoped>
  561. .night-mode {
  562. .manage-station-modal.modal .modal-card-body {
  563. .left-section {
  564. .station-info-box-wrapper {
  565. border: 0;
  566. }
  567. .section {
  568. background-color: transparent !important;
  569. }
  570. .tab-selection .button {
  571. background: var(--dark-grey);
  572. color: var(--white);
  573. }
  574. .tab {
  575. background-color: var(--dark-grey-3);
  576. border: 0;
  577. }
  578. }
  579. .right-section .section,
  580. #queue {
  581. border-radius: @border-radius;
  582. background-color: transparent !important;
  583. }
  584. }
  585. }
  586. .manage-station-modal.modal .modal-card-body {
  587. display: flex;
  588. flex-wrap: wrap;
  589. height: 100%;
  590. .left-section {
  591. .station-info-box-wrapper {
  592. border-radius: @border-radius;
  593. border: 1px solid var(--light-grey-3);
  594. overflow: hidden;
  595. margin-bottom: 20px;
  596. }
  597. .tab-selection {
  598. display: flex;
  599. overflow-x: auto;
  600. .button {
  601. border-radius: @border-radius @border-radius 0 0;
  602. border: 0;
  603. text-transform: uppercase;
  604. font-size: 14px;
  605. color: var(--dark-grey-3);
  606. background-color: var(--light-grey-2);
  607. flex-grow: 1;
  608. height: 32px;
  609. &:not(:first-of-type) {
  610. margin-left: 5px;
  611. }
  612. }
  613. .selected {
  614. background-color: var(--primary-color) !important;
  615. color: var(--white) !important;
  616. font-weight: 600;
  617. }
  618. }
  619. .tab {
  620. border: 1px solid var(--light-grey-3);
  621. padding: 15px 10px;
  622. border-radius: 0 0 @border-radius @border-radius;
  623. }
  624. }
  625. .right-section {
  626. .section {
  627. .queue-title {
  628. display: flex;
  629. line-height: 30px;
  630. .material-icons {
  631. margin-left: 5px;
  632. margin-bottom: 5px;
  633. font-size: 28px;
  634. cursor: pointer;
  635. &:first-of-type {
  636. margin-left: auto;
  637. }
  638. &.skip-station {
  639. color: var(--dark-red);
  640. }
  641. &.resume-station,
  642. &.pause-station {
  643. color: var(--primary-color);
  644. }
  645. }
  646. }
  647. .currently-playing {
  648. margin-bottom: 10px;
  649. }
  650. }
  651. }
  652. &.modal-wide .left-section .section:first-child {
  653. padding: 0 15px 15px !important;
  654. }
  655. }
  656. </style>