123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760 |
- <script setup lang="ts">
- import {
- defineAsyncComponent,
- ref,
- watch,
- onMounted,
- onBeforeUnmount
- } from "vue";
- import Toast from "toasters";
- import { storeToRefs } from "pinia";
- import { useWebsocketsStore } from "@/stores/websockets";
- import { useUserAuthStore } from "@/stores/userAuth";
- import { useModalsStore } from "@/stores/modals";
- import { useManageStationStore } from "@/stores/manageStation";
- const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
- const Queue = defineAsyncComponent(() => import("@/components/Queue.vue"));
- const MediaItem = defineAsyncComponent(
- () => import("@/components/MediaItem.vue")
- );
- const StationInfoBox = defineAsyncComponent(
- () => import("@/components/StationInfoBox.vue")
- );
- const Settings = defineAsyncComponent(() => import("./Settings.vue"));
- const PlaylistTabBase = defineAsyncComponent(
- () => import("@/components/PlaylistTabBase.vue")
- );
- const Request = defineAsyncComponent(() => import("@/components/Request.vue"));
- const QuickConfirm = defineAsyncComponent(
- () => import("@/components/QuickConfirm.vue")
- );
- const props = defineProps({
- modalUuid: { type: String, required: true },
- stationId: { type: String, required: true },
- sector: { type: String, default: "admin" }
- });
- const tabs = ref([]);
- const userAuthStore = useUserAuthStore();
- const { loggedIn, userId } = storeToRefs(userAuthStore);
- const { socket } = useWebsocketsStore();
- const manageStationStore = useManageStationStore({
- modalUuid: props.modalUuid
- });
- const {
- stationId,
- sector,
- tab,
- station,
- stationPlaylist,
- autofill,
- blacklist,
- stationPaused,
- currentSong
- } = storeToRefs(manageStationStore);
- const {
- editStation,
- setAutofillPlaylists,
- setBlacklist,
- clearStation,
- updateSongsList,
- updateStationPlaylist,
- repositionSongInList,
- updateStationPaused,
- updateCurrentSong,
- updateStation,
- updateIsFavorited,
- hasPermission,
- addDj,
- removeDj,
- updatePermissions
- } = manageStationStore;
- const { closeCurrentModal } = useModalsStore();
- const showTab = payload => {
- if (tabs.value[`${payload}-tab`])
- tabs.value[`${payload}-tab`].scrollIntoView({ block: "nearest" });
- manageStationStore.showTab(payload);
- };
- const canRequest = () =>
- station.value &&
- loggedIn.value &&
- station.value.requests &&
- station.value.requests.enabled &&
- (station.value.requests.access === "user" ||
- (station.value.requests.access === "owner" &&
- hasPermission("stations.request")));
- const removeStation = () => {
- socket.dispatch("stations.remove", stationId.value, res => {
- new Toast(res.message);
- });
- };
- const resetQueue = () => {
- socket.dispatch("stations.resetQueue", stationId.value, res => {
- if (res.status !== "success")
- new Toast({
- content: `Error: ${res.message}`,
- timeout: 8000
- });
- else new Toast({ content: res.message, timeout: 4000 });
- });
- };
- const findTabOrClose = () => {
- if (hasPermission("stations.update")) return showTab("settings");
- if (canRequest()) return showTab("request");
- if (hasPermission("stations.autofill")) return showTab("autofill");
- if (hasPermission("stations.blacklist")) return showTab("blacklist");
- if (
- !(
- sector.value === "home" &&
- (hasPermission("stations.view") ||
- station.value.privacy === "public")
- )
- )
- return closeCurrentModal(true);
- return null;
- };
- watch(
- () => hasPermission("stations.update"),
- value => {
- if (!value && tab.value === "settings") findTabOrClose();
- }
- );
- watch(
- () => hasPermission("stations.request") && station.value.requests.enabled,
- value => {
- if (!value && tab.value === "request") findTabOrClose();
- }
- );
- watch(
- () => hasPermission("stations.autofill") && station.value.autofill.enabled,
- value => {
- if (!value && tab.value === "autofill") findTabOrClose();
- }
- );
- watch(
- () => hasPermission("stations.blacklist"),
- value => {
- if (!value && tab.value === "blacklist") findTabOrClose();
- }
- );
- onMounted(() => {
- manageStationStore.init({
- stationId: props.stationId,
- sector: props.sector
- });
- socket.onConnect(() => {
- socket.dispatch(
- `stations.getStationById`,
- stationId.value,
- async res => {
- if (res.status === "success") {
- editStation(res.data.station);
- await updatePermissions();
- findTabOrClose();
- const currentSong = res.data.station.currentSong
- ? res.data.station.currentSong
- : {};
- updateCurrentSong(currentSong);
- updateStationPaused(res.data.station.paused);
- socket.dispatch(
- "stations.getStationAutofillPlaylistsById",
- stationId.value,
- res => {
- if (res.status === "success")
- setAutofillPlaylists(res.data.playlists);
- }
- );
- socket.dispatch(
- "stations.getStationBlacklistById",
- stationId.value,
- res => {
- if (res.status === "success")
- setBlacklist(res.data.playlists);
- }
- );
- if (hasPermission("stations.view")) {
- socket.dispatch(
- "playlists.getPlaylistForStation",
- stationId.value,
- true,
- res => {
- if (res.status === "success") {
- updateStationPlaylist(res.data.playlist);
- }
- }
- );
- }
- socket.dispatch(
- "stations.getQueue",
- stationId.value,
- res => {
- if (res.status === "success")
- updateSongsList(res.data.queue);
- }
- );
- socket.dispatch(
- "apis.joinRoom",
- `manage-station.${stationId.value}`
- );
- } else {
- new Toast(`Station with that ID not found`);
- closeCurrentModal();
- }
- }
- );
- socket.on(
- "event:station.updated",
- res => {
- updateStation(res.data.station);
- },
- { modalUuid: props.modalUuid }
- );
- socket.on(
- "event:station.autofillPlaylist",
- res => {
- const { playlist } = res.data;
- const playlistIndex = autofill.value
- .map(autofillPlaylist => autofillPlaylist._id)
- .indexOf(playlist._id);
- if (playlistIndex === -1) autofill.value.push(playlist);
- },
- { modalUuid: props.modalUuid }
- );
- socket.on(
- "event:station.blacklistedPlaylist",
- res => {
- const { playlist } = res.data;
- const playlistIndex = blacklist.value
- .map(blacklistedPlaylist => blacklistedPlaylist._id)
- .indexOf(playlist._id);
- if (playlistIndex === -1) blacklist.value.push(playlist);
- },
- { modalUuid: props.modalUuid }
- );
- socket.on(
- "event:station.removedAutofillPlaylist",
- res => {
- const { playlistId } = res.data;
- const playlistIndex = autofill.value
- .map(playlist => playlist._id)
- .indexOf(playlistId);
- if (playlistIndex >= 0) autofill.value.splice(playlistIndex, 1);
- },
- { modalUuid: props.modalUuid }
- );
- socket.on(
- "event:station.removedBlacklistedPlaylist",
- res => {
- const { playlistId } = res.data;
- const playlistIndex = blacklist.value
- .map(playlist => playlist._id)
- .indexOf(playlistId);
- if (playlistIndex >= 0)
- blacklist.value.splice(playlistIndex, 1);
- },
- { modalUuid: props.modalUuid }
- );
- socket.on(
- "event:station.deleted",
- () => {
- new Toast(`The station you were editing was deleted.`);
- closeCurrentModal(true);
- },
- { modalUuid: props.modalUuid }
- );
- socket.on(
- "event:user.station.favorited",
- res => {
- if (res.data.stationId === stationId.value)
- updateIsFavorited(true);
- },
- { modalUuid: props.modalUuid }
- );
- socket.on(
- "event:user.station.unfavorited",
- res => {
- if (res.data.stationId === stationId.value)
- updateIsFavorited(false);
- },
- { modalUuid: props.modalUuid }
- );
- socket.on(
- "event:manageStation.queue.updated",
- res => {
- if (res.data.stationId === stationId.value)
- updateSongsList(res.data.queue);
- },
- { modalUuid: props.modalUuid }
- );
- socket.on(
- "event:manageStation.queue.song.repositioned",
- res => {
- if (res.data.stationId === stationId.value)
- repositionSongInList(res.data.song);
- },
- { modalUuid: props.modalUuid }
- );
- socket.on(
- "event:station.pause",
- res => {
- if (res.data.stationId === stationId.value)
- updateStationPaused(true);
- },
- { modalUuid: props.modalUuid }
- );
- socket.on(
- "event:station.resume",
- res => {
- if (res.data.stationId === stationId.value)
- updateStationPaused(false);
- },
- { modalUuid: props.modalUuid }
- );
- socket.on(
- "event:station.nextSong",
- res => {
- if (res.data.stationId === stationId.value)
- updateCurrentSong(res.data.currentSong || {});
- },
- { modalUuid: props.modalUuid }
- );
- socket.on("event:manageStation.djs.added", res => {
- if (res.data.stationId === stationId.value) {
- if (res.data.user._id === userId.value) updatePermissions();
- addDj(res.data.user);
- }
- });
- socket.on("event:manageStation.djs.removed", res => {
- if (res.data.stationId === stationId.value) {
- if (res.data.user._id === userId.value) updatePermissions();
- removeDj(res.data.user);
- }
- });
- socket.on("keep.event:user.role.updated", () => {
- updatePermissions();
- });
- if (hasPermission("stations.view")) {
- socket.on(
- "event:playlist.song.added",
- res => {
- if (stationPlaylist.value._id === res.data.playlistId)
- stationPlaylist.value.songs.push(res.data.song);
- },
- {
- modalUuid: props.modalUuid
- }
- );
- socket.on(
- "event:playlist.song.removed",
- res => {
- if (stationPlaylist.value._id === res.data.playlistId) {
- // remove song from array of playlists
- stationPlaylist.value.songs.forEach((song, index) => {
- if (song.mediaSource === res.data.mediaSource)
- stationPlaylist.value.songs.splice(index, 1);
- });
- }
- },
- {
- modalUuid: props.modalUuid
- }
- );
- socket.on(
- "event:playlist.song.replaced",
- res => {
- if (stationPlaylist.value._id === res.data.playlistId)
- stationPlaylist.value.songs =
- stationPlaylist.value.songs.map(song =>
- song.mediaSource === res.data.oldMediaSource
- ? res.data.song
- : song
- );
- },
- {
- modalUuid: props.modalUuid
- }
- );
- socket.on(
- "event:playlist.songs.repositioned",
- res => {
- if (stationPlaylist.value._id === res.data.playlistId) {
- // for each song that has a new position
- res.data.songsBeingChanged.forEach(changedSong => {
- stationPlaylist.value.songs.forEach(
- (song, index) => {
- // find song locally
- if (
- song.mediaSource ===
- changedSong.mediaSource
- ) {
- // change song position attribute
- stationPlaylist.value.songs[
- index
- ].position = changedSong.position;
- // reposition in array if needed
- if (index !== changedSong.position - 1)
- stationPlaylist.value.songs.splice(
- changedSong.position - 1,
- 0,
- stationPlaylist.value.songs.splice(
- index,
- 1
- )[0]
- );
- }
- }
- );
- });
- }
- },
- {
- modalUuid: props.modalUuid
- }
- );
- }
- });
- });
- onBeforeUnmount(() => {
- socket.dispatch(
- "apis.leaveRoom",
- `manage-station.${stationId.value}`,
- () => {}
- );
- if (hasPermission("stations.update")) showTab("settings");
- clearStation();
- // Delete the Pinia store that was created for this modal, after all other cleanup tasks are performed
- manageStationStore.$dispose();
- });
- </script>
- <template>
- <modal
- v-if="station"
- :title="
- sector === 'home' && !hasPermission('stations.view.manage')
- ? 'View Queue'
- : !hasPermission('stations.view.manage')
- ? 'Add Song to Queue'
- : 'Manage Station'
- "
- :style="`--primary-color: var(--${station.theme})`"
- class="manage-station-modal"
- :size="
- hasPermission('stations.view.manage') || sector !== 'home'
- ? 'wide'
- : null
- "
- :split="hasPermission('stations.view.manage') || sector !== 'home'"
- >
- <template #body v-if="station && station._id">
- <div class="left-section">
- <div class="section">
- <div class="station-info-box-wrapper">
- <station-info-box
- :station="station"
- :station-paused="stationPaused"
- :show-go-to-station="sector !== 'station'"
- :sector="'manageStation'"
- :modal-uuid="modalUuid"
- />
- </div>
- <div
- v-if="
- hasPermission('stations.view.manage') ||
- sector !== 'home'
- "
- >
- <div class="tab-selection">
- <button
- v-if="hasPermission('stations.update')"
- class="button is-default"
- :class="{ selected: tab === 'settings' }"
- :ref="el => (tabs['settings-tab'] = el)"
- @click="showTab('settings')"
- >
- Settings
- </button>
- <button
- v-if="canRequest()"
- class="button is-default"
- :class="{ selected: tab === 'request' }"
- :ref="el => (tabs['request-tab'] = el)"
- @click="showTab('request')"
- >
- Request
- </button>
- <button
- v-if="
- hasPermission('stations.autofill') &&
- station.autofill.enabled
- "
- class="button is-default"
- :class="{ selected: tab === 'autofill' }"
- :ref="el => (tabs['autofill-tab'] = el)"
- @click="showTab('autofill')"
- >
- Autofill
- </button>
- <button
- v-if="hasPermission('stations.blacklist')"
- class="button is-default"
- :class="{ selected: tab === 'blacklist' }"
- :ref="el => (tabs['blacklist-tab'] = el)"
- @click="showTab('blacklist')"
- >
- Blacklist
- </button>
- </div>
- <settings
- v-if="hasPermission('stations.update')"
- class="tab"
- v-show="tab === 'settings'"
- :modal-uuid="modalUuid"
- ref="settingsTabComponent"
- />
- <request
- v-if="canRequest()"
- class="tab"
- v-show="tab === 'request'"
- :sector="'manageStation'"
- :disable-auto-request="sector !== 'station'"
- :modal-uuid="modalUuid"
- />
- <playlist-tab-base
- v-if="
- hasPermission('stations.autofill') &&
- station.autofill.enabled
- "
- class="tab"
- v-show="tab === 'autofill'"
- :type="'autofill'"
- :modal-uuid="modalUuid"
- >
- <template #info>
- <p>
- Select playlists to automatically add songs
- within to the queue
- </p>
- </template>
- </playlist-tab-base>
- <playlist-tab-base
- v-if="hasPermission('stations.blacklist')"
- class="tab"
- v-show="tab === 'blacklist'"
- :type="'blacklist'"
- :modal-uuid="modalUuid"
- >
- <template #info>
- <p>
- Blacklist a playlist to prevent all songs
- within from playing in this station
- </p>
- </template>
- </playlist-tab-base>
- </div>
- </div>
- </div>
- <div class="right-section">
- <div class="section">
- <div class="queue-title">
- <h4 class="section-title">Queue</h4>
- </div>
- <hr class="section-horizontal-rule" />
- <media-item
- v-if="currentSong.mediaSource"
- :song="currentSong"
- :requested-by="true"
- header="Currently Playing.."
- class="currently-playing"
- />
- <queue :modal-uuid="modalUuid" sector="manageStation" />
- </div>
- </div>
- </template>
- <template #footer>
- <div class="right">
- <quick-confirm
- v-if="hasPermission('stations.queue.reset')"
- @confirm="resetQueue()"
- >
- <a class="button is-danger">Reset queue</a>
- </quick-confirm>
- <quick-confirm
- v-if="hasPermission('stations.remove')"
- @confirm="removeStation()"
- >
- <button class="button is-danger">Delete station</button>
- </quick-confirm>
- </div>
- </template>
- </modal>
- </template>
- <style lang="less">
- .manage-station-modal.modal .modal-card {
- .tab > button {
- width: 100%;
- margin-top: 10px;
- }
- .currently-playing.song-item {
- height: 130px !important;
- .thumbnail-and-info .thumbnail {
- min-width: 130px;
- width: 130px;
- }
- }
- }
- </style>
- <style lang="less" scoped>
- .night-mode {
- .manage-station-modal.modal .modal-card-body {
- .left-section {
- .station-info-box-wrapper {
- border: 0;
- }
- .section {
- background-color: transparent !important;
- }
- .tab-selection .button {
- background: var(--dark-grey);
- color: var(--white);
- }
- .tab {
- background-color: var(--dark-grey-3);
- border: 0;
- }
- }
- .right-section .section,
- #queue {
- border-radius: @border-radius;
- background-color: transparent !important;
- }
- }
- }
- .manage-station-modal.modal .modal-card-body {
- display: flex;
- flex-wrap: wrap;
- height: 100%;
- .left-section {
- .station-info-box-wrapper {
- border-radius: @border-radius;
- border: 1px solid var(--light-grey-3);
- overflow: hidden;
- margin-bottom: 20px;
- }
- .tab-selection {
- display: flex;
- overflow-x: auto;
- .button {
- border-radius: @border-radius @border-radius 0 0;
- border: 0;
- text-transform: uppercase;
- font-size: 14px;
- color: var(--dark-grey-3);
- background-color: var(--light-grey-2);
- flex-grow: 1;
- height: 32px;
- &:not(:first-of-type) {
- margin-left: 5px;
- }
- }
- .selected {
- background-color: var(--primary-color) !important;
- color: var(--white) !important;
- font-weight: 600;
- }
- }
- .tab {
- border: 1px solid var(--light-grey-3);
- padding: 15px 10px;
- border-radius: 0 0 @border-radius @border-radius;
- }
- }
- .right-section {
- .section {
- .queue-title {
- display: flex;
- line-height: 30px;
- .material-icons {
- margin-left: 5px;
- margin-bottom: 5px;
- font-size: 28px;
- cursor: pointer;
- &:first-of-type {
- margin-left: auto;
- }
- &.skip-station {
- color: var(--dark-red);
- }
- &.resume-station,
- &.pause-station {
- color: var(--primary-color);
- }
- }
- }
- .currently-playing {
- margin-bottom: 10px;
- }
- }
- }
- &.modal-wide .left-section .section:first-child {
- padding: 0 15px 15px !important;
- }
- }
- </style>
|