Compare commits

...

12 Commits

Author SHA1 Message Date
cwilvx
80a0bdbbf1 remove root dir draft settings 2025-01-29 12:30:42 +03:00
cwilvx
2e27da3f9f fix cardscroller 2025-01-29 12:28:51 +03:00
cwilvx
74bf8f5d78 context menu on artist 2025-01-29 12:05:27 +03:00
cwilvx
bfdefc37fd album card context menu 2025-01-29 11:43:02 +03:00
cwilvx
44a877b9c9 enable delete 2025-01-28 10:44:52 +03:00
cwilvx
db93fd554e first draft 2025-01-28 09:17:37 +03:00
cwilvx
40a7ad084c revert bottom bar test bitrate 2025-01-28 06:41:47 +03:00
cwilvx
e44aa01d63 move /home to /nothome 2025-01-07 23:21:44 +03:00
cwilvx
192e705890 add explicit flag 2025-01-06 00:18:34 +03:00
cwilvx
50f92b65ab revert default settings page 2024-12-30 21:10:07 +03:00
cwilvx
a5aea45cd6 Merge branch 'lastfm' 2024-12-30 21:01:04 +03:00
Mungai Njoroge
cc93fe7419 Merge pull request #39 from swingmx/another-one
Recommendations and misc stuff
2024-12-28 16:04:52 +03:00
31 changed files with 1132 additions and 569 deletions

View File

@@ -24,7 +24,7 @@
"pinia-plugin-persistedstate": "^3.2.0", "pinia-plugin-persistedstate": "^3.2.0",
"qr-code-styling": "^1.6.0-rc.1", "qr-code-styling": "^1.6.0-rc.1",
"v-wave": "^1.5.0", "v-wave": "^1.5.0",
"vue": "^v3.2.45", "vue": "^v3.5.13",
"vue-boring-avatars": "^1.4.0", "vue-boring-avatars": "^1.4.0",
"vue-debounce": "^3.0.2", "vue-debounce": "^3.0.2",
"vue-router": "^4.1.3", "vue-router": "^4.1.3",

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.84421 21.8972H18.0295C20.5685 21.8972 21.8737 20.5919 21.8737 18.0914V3.82921C21.8737 1.32656 20.5685 0.0234375 18.0295 0.0234375H3.84421C1.31484 0.0234375 0 1.31695 0 3.82921V18.0914C0 20.6016 1.31484 21.8972 3.84421 21.8972Z" fill="#aeaeaf"/>
<path d="M8.24921 16.3608C7.44976 16.3608 7.04688 15.8618 7.04688 15.0368V6.67026C7.04688 5.84948 7.45187 5.34839 8.24921 5.34839H13.795C14.3777 5.34839 14.7607 5.68026 14.7607 6.26643C14.7607 6.8376 14.3777 7.19619 13.795 7.19619H9.33695V9.92808H13.5377C14.0824 9.92808 14.4464 10.2356 14.4464 10.7923C14.4464 11.3222 14.0824 11.6255 13.5377 11.6255H9.33695V14.513H13.795C14.3777 14.513 14.7607 14.8545 14.7607 15.4406C14.7607 16.0118 14.3777 16.3608 13.795 16.3608H8.24921Z" fill="#111111"/>
</svg>

After

Width:  |  Height:  |  Size: 831 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.42607 18.5857L16.593 5.42412L14.344 3.16546L1.1674 16.3366L0.0267015 19.0816C-0.10197 19.4303 0.258496 19.8049 0.592479 19.6708L3.42607 18.5857ZM17.715 4.32139L18.9829 3.07476C19.6122 2.44546 19.6378 1.7482 19.0703 1.16906L18.6128 0.709452C18.0454 0.139922 17.3439 0.200625 16.7125 0.808593L15.4467 2.06273L17.715 4.32139Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 439 B

View File

@@ -268,3 +268,8 @@ button {
background-color: $blue; background-color: $blue;
opacity: 1; opacity: 1;
} }
.explicit-icon {
width: 0.9rem;
margin-left: $smaller;
}

View File

@@ -32,6 +32,7 @@
<span class="ellip"> <span class="ellip">
{{ queue.currenttrack?.title || 'Hello there' }} {{ queue.currenttrack?.title || 'Hello there' }}
</span> </span>
<ExplicitIcon class="explicit-icon" v-if="queue.currenttrack?.explicit" />
<MasterFlag :bitrate="queue.currenttrack?.bitrate || 0" /> <MasterFlag :bitrate="queue.currenttrack?.bitrate || 0" />
</div> </div>
<ArtistName <ArtistName
@@ -61,6 +62,7 @@ import HotKeys from '../LeftSidebar/NP/HotKeys.vue'
import HeartSvg from '../shared/HeartSvg.vue' import HeartSvg from '../shared/HeartSvg.vue'
import MasterFlag from '../shared/MasterFlag.vue' import MasterFlag from '../shared/MasterFlag.vue'
import Actions from './Right.vue' import Actions from './Right.vue'
import ExplicitIcon from '@/assets/icons/explicit.svg'
const queue = useQStore() const queue = useQStore()
const settings = useSettingsStore() const settings = useSettingsStore()

View File

@@ -57,6 +57,10 @@ onMounted(async () => {
date.value = res.data.dates date.value = res.data.dates
} }
}) })
defineOptions({
inheritAttrs: false,
})
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -22,6 +22,12 @@
@hideModal="hideModal" @hideModal="hideModal"
@setTitle="setTitle" @setTitle="setTitle"
/> />
<CrudPage
v-if="modal.component == modal.options.page"
@hideModal="hideModal"
@setTitle="setTitle"
v-bind="modal.props"
/>
<UpdatePlaylist <UpdatePlaylist
v-if="modal.component == modal.options.updatePlaylist" v-if="modal.component == modal.options.updatePlaylist"
v-bind="modal.props" v-bind="modal.props"
@@ -49,6 +55,7 @@ import { useRouter } from 'vue-router'
import AuthLogin from './modals/AuthLogin.vue' import AuthLogin from './modals/AuthLogin.vue'
import ConfirmModal from './modals/ConfirmModal.vue' import ConfirmModal from './modals/ConfirmModal.vue'
import CrudPage from './modals/CrudPage.vue'
import NewPlaylist from './modals/NewPlaylist.vue' import NewPlaylist from './modals/NewPlaylist.vue'
import RootDirsPrompt from './modals/RootDirsPrompt.vue' import RootDirsPrompt from './modals/RootDirsPrompt.vue'
import SetRootDirs from './modals/SetRootDirs.vue' import SetRootDirs from './modals/SetRootDirs.vue'

View File

@@ -0,0 +1,81 @@
<template>
<form action="" v-if="delete">
<div>Are you sure you want to delete this page?</div>
<br />
<button @click.prevent="submit" class="critical">Yes, Delete</button>
</form>
<form class="playlist-modal" @submit.prevent="submit" v-else>
<label for="name">Page name</label>
<br />
<input type="search" class="rounded-sm" id="name" :value="page?.name" />
<br />
<label for="description">Description</label>
<br />
<input type="search" class="rounded-sm" id="description" :value="page?.extra.description" />
<br /><br />
<button type="submit">{{ page ? 'Update' : 'Create' }}</button>
</form>
</template>
<script setup lang="ts">
import { Page } from '@/interfaces'
import { createNewPage, deletePage, updatePage } from '@/requests/pages'
import { router } from '@/router';
import { NotifType, Notification } from '@/stores/notification'
const props = defineProps<{
page?: Page
hash?: string
type?: string
extra?: any
delete?: boolean
}>()
const emit = defineEmits<{
(e: 'hideModal'): void
(e: 'setTitle', title: string): void
}>()
emit('setTitle', props.page ? (props.delete ? 'Delete Page' : 'Update Page') : 'New Page')
async function submit(e: Event) {
if (props.delete && props.page) {
const deleted = await deletePage(props.page.id)
if (deleted) {
new Notification('Page deleted', NotifType.Success)
emit('hideModal')
router.push('/')
}
return
}
e.preventDefault()
const name = (e.target as any).elements['name'].value
const description = (e.target as any).elements['description'].value
// If the page is null, we are creating a new page
if (props.page == null) {
const created = await createNewPage(name, description, [
{
hash: props.hash as string,
type: props.type as string,
extra: props.extra,
},
])
if (created) {
new Notification('New page created', NotifType.Success)
emit('hideModal')
}
} else {
const updatedPage = await updatePage(props.page, name, description)
if (updatedPage) {
props.page.name = updatedPage.name
props.page.extra.description = updatedPage.extra.description
new Notification('Page updated', NotifType.Success)
emit('hideModal')
}
}
}
</script>

View File

@@ -59,7 +59,7 @@ const currentGroup = computed(() => {
// select default tab // select default tab
for (const group of settingGroups) { for (const group of settingGroups) {
for (const settings of group.groups) { for (const settings of group.groups) {
if (settings.title === 'Last.fm') { if (settings.title === 'Appearance') {
return settings return settings
} }
} }

View File

@@ -5,6 +5,8 @@
params: { albumhash: album.albumhash }, params: { albumhash: album.albumhash },
}" }"
class="album-card" class="album-card"
@contextmenu.prevent="showMenu"
:class="{ 'context-menu-open': contextMenuFlag }"
> >
<div class="with-img rounded-sm no-scroll"> <div class="with-img rounded-sm no-scroll">
<div <div
@@ -56,7 +58,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Routes } from '@/router' import { Routes } from '@/router'
import { computed } from 'vue' import { computed, ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { Album } from '../../interfaces' import { Album } from '../../interfaces'
@@ -66,9 +68,11 @@ import { playSources } from '@/enums'
import useAlbumStore from '@/stores/pages/album' import useAlbumStore from '@/stores/pages/album'
import { paths } from '../../config' import { paths } from '../../config'
import MasterFlag from './MasterFlag.vue' import MasterFlag from './MasterFlag.vue'
import { showAlbumContextMenu } from '@/helpers/contextMenuHandler'
const imguri = paths.images.thumb.medium
const route = useRoute() const route = useRoute()
const contextMenuFlag = ref(false)
const imguri = paths.images.thumb.medium
const props = defineProps<{ const props = defineProps<{
album: Album album: Album
@@ -94,6 +98,10 @@ const artists = computed(() => {
return albumartists return albumartists
}) })
function showMenu(e: MouseEvent) {
showAlbumContextMenu(e, contextMenuFlag, props.album)
}
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -105,6 +113,10 @@ const artists = computed(() => {
height: max-content; height: max-content;
transition: background-color 0.2s ease-out; transition: background-color 0.2s ease-out;
&.context-menu-open {
background-color: $gray5;
}
.with-img { .with-img {
position: relative; position: relative;
@@ -130,10 +142,6 @@ const artists = computed(() => {
opacity: 1; opacity: 1;
} }
img {
/* border-radius: 0 0 $medium $medium; Not sure why this one was added, fugly with animation */
}
.gradient { .gradient {
opacity: 1; opacity: 1;
} }

View File

@@ -7,6 +7,8 @@
}, },
}" }"
class="artist-card" class="artist-card"
@contextmenu.prevent="showContextMenu"
:class="{ 'context-menu-open': contextMenuFlag }"
> >
<div class="image circular"> <div class="image circular">
<img class="artist-image circular" :src="imguri + artist.image" /> <img class="artist-image circular" :src="imguri + artist.image" />
@@ -38,12 +40,19 @@ import { Routes } from '@/router'
import { playSources } from '@/enums' import { playSources } from '@/enums'
import PlayBtn from './PlayBtn.vue' import PlayBtn from './PlayBtn.vue'
import { ref } from 'vue'
import { showArtistContextMenu } from '@/helpers/contextMenuHandler'
const imguri = paths.images.artist.medium const imguri = paths.images.artist.medium
const contextMenuFlag = ref(false)
defineProps<{ const props = defineProps<{
artist: Artist artist: Artist
}>() }>()
const showContextMenu = (e: MouseEvent) => {
showArtistContextMenu(e, contextMenuFlag, props.artist.artisthash, props.artist.name)
}
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -59,6 +68,10 @@ defineProps<{
height: max-content; height: max-content;
transition: background-color 0.2s ease-out; transition: background-color 0.2s ease-out;
&.context-menu-open {
background-color: $gray5;
}
.image { .image {
position: relative; position: relative;

View File

@@ -1,25 +1,57 @@
<template> <template>
<div v-if="type == 'album'" class="cardlistrow"> <div class="cardlistrow">
<AlbumCard v-for="item in items" :key="item.albumhash" class="hlistitem" :album="(item as Album)" /> <component v-for="item in items" :key="item.key" :is="item.component" v-bind="item.props" />
</div>
<div v-else-if="type == 'artist'" class="cardlistrow">
<ArtistCard v-for="item in items" :key="item.artisthash" class="hlistitem" :artist="(item as Artist)" />
</div>
<div v-else-if="type == 'mix'" class="cardlistrow">
<MixCard v-for="item in items" :key="item.sourcehash" class="hlistitem" :mix="(item as Mix)" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Album, Artist, Mix } from "@/interfaces"; import { Album, Artist, Mix } from '@/interfaces'
import AlbumCard from "./AlbumCard.vue"; import AlbumCard from './AlbumCard.vue'
import ArtistCard from "./ArtistCard.vue"; import ArtistCard from './ArtistCard.vue'
import MixCard from "../Mixes/MixCard.vue"; import MixCard from '../Mixes/MixCard.vue'
import { computed } from 'vue'
defineProps<{ const props = defineProps<{
type: string | "album" | "artist" | "mix"; items: Album[] | Artist[] | Mix[]
items: Album[] | Artist[] | Mix[]; }>()
}>();
const items = computed(() => {
return props.items.map((item: any) => {
const i = {
component: <any>null,
props: {},
key: '',
}
switch (item['type']) {
case 'album':
i.component = AlbumCard
i.key = item.albumhash
i.props = {
album: item,
}
break
case 'artist':
i.component = ArtistCard
i.key = item.artisthash
i.props = {
artist: item,
}
break
case 'mix':
i.component = MixCard
i.key = item.sourcehash
i.props = {
mix: item,
}
break
}
return i
})
})
console.log(props)
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -2,11 +2,17 @@
<div class="cardscroller"> <div class="cardscroller">
<div class="rinfo"> <div class="rinfo">
<div class="rtitle"> <div class="rtitle">
<b>{{ title }}</b> <b>
<RouterLink :to="route || ''">
{{ title }}
</RouterLink>
</b>
<SeeAll v-if="route && itemlist.length >= maxAbumCards" :route="route" :text="seeAllText" /> <SeeAll v-if="route && itemlist.length >= maxAbumCards" :route="route" :text="seeAllText" />
</div> </div>
<div v-if="description" class="rdesc"> <div v-if="description" class="rdesc">
<RouterLink :to="route || ''">
{{ description }} {{ description }}
</RouterLink>
</div> </div>
</div> </div>
<div class="recentitems"> <div class="recentitems">
@@ -26,7 +32,7 @@
import { playSources } from '@/enums' import { playSources } from '@/enums'
import { maxAbumCards } from '@/stores/content-width' import { maxAbumCards } from '@/stores/content-width'
import { computed } from 'vue' import { computed, onMounted } from 'vue'
import PlaylistCard from '../PlaylistsList/PlaylistCard.vue' import PlaylistCard from '../PlaylistsList/PlaylistCard.vue'
import SeeAll from '../shared/SeeAll.vue' import SeeAll from '../shared/SeeAll.vue'
import AlbumCard from './AlbumCard.vue' import AlbumCard from './AlbumCard.vue'
@@ -131,6 +137,10 @@ function getProps(item: { type: string; item?: any; with_helptext?: boolean }) {
} }
} }
} }
onMounted(() => {
console.log(props.items)
})
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@@ -30,6 +30,12 @@
grid-template-columns: 1fr max-content; grid-template-columns: 1fr max-content;
} }
.right {
display: flex;
align-items: center;
height: 100%;
}
.after { .after {
margin-top: 2rem; margin-top: 2rem;
margin-left: -$medium; margin-left: -$medium;

View File

@@ -28,7 +28,7 @@ defineProps<{
font-weight: 600; font-weight: 600;
margin-left: $smaller; margin-left: $smaller;
padding: 2px 5px; padding: 2px 5px;
border-radius: 5px; border-radius: 4px;
opacity: 0.75; opacity: 0.75;
text-transform: uppercase; text-transform: uppercase;
} }

View File

@@ -25,6 +25,7 @@
<span class="title ellip"> <span class="title ellip">
{{ track.title }} {{ track.title }}
</span> </span>
<ExplicitIcon class="explicit-icon" v-if="track.explicit" />
<MasterFlag :bitrate="track.bitrate" /> <MasterFlag :bitrate="track.bitrate" />
</div> </div>
<div class="isSmallArtists"> <div class="isSmallArtists">
@@ -40,6 +41,7 @@ const imguri = paths.images.thumb.small;
import ArtistName from "../ArtistName.vue"; import ArtistName from "../ArtistName.vue";
import MasterFlag from "../MasterFlag.vue"; import MasterFlag from "../MasterFlag.vue";
import ExplicitIcon from "@/assets/icons/explicit.svg";
import { paths } from "@/config"; import { paths } from "@/config";
@@ -59,6 +61,10 @@ defineEmits<{
position: relative; position: relative;
align-items: center; align-items: center;
.explicit-icon {
margin-left: $small;
}
.thumbnail { .thumbnail {
margin-right: $medium; margin-right: $medium;
display: flex; display: flex;

View File

@@ -85,6 +85,9 @@ export const paths = {
return this.base + '/artists' return this.base + '/artists'
}, },
}, },
pages: {
base: base_url + '/pages',
},
search: { search: {
base: base_url + '/search', base: base_url + '/search',
get top() { get top() {
@@ -144,7 +147,7 @@ export const paths = {
}, },
files: base_url + '/file', files: base_url + '/file',
home: { home: {
base: base_url + '/home', base: base_url + '/nothome',
get recentlyAdded() { get recentlyAdded() {
return this.base + '/recents/added' return this.base + '/recents/added'
}, },

View File

@@ -1,56 +1,109 @@
import useModal from "@/stores/modal"; import { router, Routes } from '@/router'
import useAlbum from "@/stores/pages/album";
import useTracklist from "@/stores/queue/tracklist";
import { Option, Playlist } from "@/interfaces"; import useAlbum from '@/stores/pages/album'
import { addAlbumToPlaylist } from "@/requests/playlists"; import useTracklist from '@/stores/queue/tracklist'
import { getAddToPlaylistOptions, get_find_on_social } from "./utils"; import usePage from '@/stores/pages/page'
import { AddToQueueIcon, PlayNextIcon, PlaylistIcon, PlusIcon } from "@/icons";
export default async () => { import { getAlbumTracks } from '@/requests/album'
const album = useAlbum(); import { addAlbumToPlaylist } from '@/requests/playlists'
import { addOrRemoveItemFromPage } from '@/requests/pages'
import { Album, Option, Page, Playlist, Track } from '@/interfaces'
import { AddToQueueIcon, DeleteIcon, PlayNextIcon, PlusIcon } from '@/icons'
import { getAddToPageOptions, getAddToPlaylistOptions, get_find_on_social } from './utils'
export default async (album?: Album) => {
const albumStore = useAlbum()
if (!album) {
album = albumStore.info
}
const play_next = <Option>{ const play_next = <Option>{
label: "Play next", label: 'Play next',
action: () => { action: async () => {
const tracks = album.tracks.filter( let tracks: Track[] = []
(track) => !track.is_album_disc_number
); if (album) {
useTracklist().insertAfterCurrent(tracks); tracks = await getAlbumTracks(album.albumhash)
} else {
tracks = albumStore.tracks.filter(track => !track.is_album_disc_number)
}
useTracklist().insertAfterCurrent(tracks)
}, },
icon: PlayNextIcon, icon: PlayNextIcon,
}; }
const add_to_queue = <Option>{ const add_to_queue = <Option>{
label: "Add to queue", label: 'Add to queue',
action: () => { action: async () => {
const tracks = album.tracks.filter( let tracks: Track[] = []
(track) => !track.is_album_disc_number
); if (album) {
useTracklist().addTracks(tracks); tracks = await getAlbumTracks(album.albumhash)
} else {
tracks = albumStore.tracks.filter(track => !track.is_album_disc_number)
}
useTracklist().addTracks(tracks)
}, },
icon: AddToQueueIcon, icon: AddToQueueIcon,
}; }
// Action for each playlist option // Action for each playlist option
const AddToPlaylistAction = (playlist: Playlist) => { const AddToPlaylistAction = (playlist: Playlist) => {
const store = album; addAlbumToPlaylist(playlist, album.albumhash)
addAlbumToPlaylist(playlist, store.info.albumhash); }
};
const add_to_playlist: Option = { const add_to_playlist: Option = {
label: "Add to Playlist", label: 'Add to Playlist',
children: () => getAddToPlaylistOptions(AddToPlaylistAction, { children: () =>
albumhash: album.info.albumhash, getAddToPlaylistOptions(AddToPlaylistAction, {
playlist_name: album.info.title, albumhash: album.albumhash,
playlist_name: album.title,
}), }),
icon: PlusIcon, icon: PlusIcon,
}; }
const addToPageAction = (page: Page) => {
addOrRemoveItemFromPage(page.id, album, 'album', 'add')
}
const add_to_page: Option = {
label: 'Add to Page',
children: () =>
getAddToPageOptions(addToPageAction, {
page: null,
hash: album.albumhash,
type: 'album',
extra: {},
}),
icon: PlusIcon,
}
const remove_from_page: Option = {
label: 'Remove from Page',
action: async () => {
const success = await addOrRemoveItemFromPage(
parseInt(router.currentRoute.value.params.page as string),
album,
'album',
'remove'
)
if (success) {
usePage().removeLocalItem(album, 'album')
}
},
icon: DeleteIcon,
}
return [ return [
play_next, play_next,
add_to_queue, add_to_queue,
add_to_playlist, add_to_playlist,
get_find_on_social(), ...[router.currentRoute.value.name === Routes.Page ? remove_from_page : add_to_page],
]; get_find_on_social('album', '', album),
}; ]
}

View File

@@ -1,54 +1,102 @@
import modal from "@/stores/modal"; import { Routes } from '@/router'
import useTracklist from "@/stores/queue/tracklist"; import { router } from '@/router'
import { getArtistTracks } from "@/requests/artists"; import usePage from '@/stores/pages/page'
import { addArtistToPlaylist } from "@/requests/playlists"; import useTracklist from '@/stores/queue/tracklist'
import { Option, Playlist } from "@/interfaces"; import { getArtistTracks } from '@/requests/artists'
import { getAddToPlaylistOptions, get_find_on_social } from "./utils"; import { addArtistToPlaylist } from '@/requests/playlists'
import { AddToQueueIcon, PlayNextIcon, PlaylistIcon, PlusIcon } from "@/icons"; import { addOrRemoveItemFromPage } from '@/requests/pages'
import { Artist, Option, Page, Playlist } from '@/interfaces'
import { AddToQueueIcon, DeleteIcon, PlayNextIcon, PlusIcon } from '@/icons'
import { getAddToPageOptions, getAddToPlaylistOptions, get_find_on_social } from './utils'
export default async (artisthash: string, artistname: string) => { export default async (artisthash: string, artistname: string) => {
const play_next = <Option>{ const play_next = <Option>{
label: "Play next", label: 'Play next',
action: () => { action: () => {
getArtistTracks(artisthash).then((tracks) => { getArtistTracks(artisthash).then(tracks => {
const store = useTracklist(); const store = useTracklist()
store.insertAfterCurrent(tracks); store.insertAfterCurrent(tracks)
}); })
}, },
icon: PlayNextIcon, icon: PlayNextIcon,
}; }
const add_to_queue = <Option>{ const add_to_queue = <Option>{
label: "Add to queue", label: 'Add to queue',
action: () => { action: () => {
getArtistTracks(artisthash).then((tracks) => { getArtistTracks(artisthash).then(tracks => {
const store = useTracklist(); const store = useTracklist()
store.addTracks(tracks); store.addTracks(tracks)
}); })
}, },
icon: AddToQueueIcon, icon: AddToQueueIcon,
}; }
// Action for each playlist option // Action for each playlist option
const AddToPlaylistAction = (playlist: Playlist) => { const AddToPlaylistAction = (playlist: Playlist) => {
addArtistToPlaylist(playlist, artisthash); addArtistToPlaylist(playlist, artisthash)
}; }
const add_to_playlist: Option = { const add_to_playlist: Option = {
label: "Add to Playlist", label: 'Add to Playlist',
children: () => getAddToPlaylistOptions(AddToPlaylistAction, { children: () =>
getAddToPlaylistOptions(AddToPlaylistAction, {
artisthash, artisthash,
playlist_name: `This is ${artistname}`, playlist_name: `This is ${artistname}`,
}), }),
icon: PlusIcon, icon: PlusIcon,
}; }
const addToPageAction = (page: Page) => {
addOrRemoveItemFromPage(
page.id,
{
artisthash,
} as Artist,
'artist',
'add'
)
}
const add_to_page: Option = {
label: 'Add to Page',
children: () =>
getAddToPageOptions(addToPageAction, {
page: null,
hash: artisthash,
type: 'artist',
extra: {},
}),
icon: PlusIcon,
}
const remove_from_page: Option = {
label: 'Remove from Page',
action: async () => {
const success = await addOrRemoveItemFromPage(
parseInt(router.currentRoute.value.params.page as string),
{
artisthash,
} as Artist,
'artist',
'remove'
)
if (success) {
usePage().removeLocalItem({ artisthash } as Artist, 'artist')
}
},
icon: DeleteIcon,
}
return [ return [
play_next, play_next,
add_to_queue, add_to_queue,
add_to_playlist, add_to_playlist,
get_find_on_social("artist"), ...[router.currentRoute.value.name === Routes.Page ? remove_from_page : add_to_page],
]; get_find_on_social('artist'),
}; ]
}

View File

@@ -3,16 +3,15 @@ import useAlbum from '@/stores/pages/album'
import useArtist from '@/stores/pages/artist' import useArtist from '@/stores/pages/artist'
import { SearchIcon } from '@/icons' import { SearchIcon } from '@/icons'
import { Option, Playlist } from '@/interfaces' import { Album, Option, Page, Playlist } from '@/interfaces'
import { getAllPages } from '@/requests/pages'
import { getAllPlaylists } from '@/requests/playlists' import { getAllPlaylists } from '@/requests/playlists'
export const separator: Option = { export const separator: Option = {
type: 'separator', type: 'separator',
} }
export function get_new_playlist_option( export function get_new_playlist_option(new_playlist_modal_props: any = {}): Option {
new_playlist_modal_props: any = {}
): Option {
return { return {
label: 'New playlist', label: 'New playlist',
action: () => { action: () => {
@@ -21,6 +20,15 @@ export function get_new_playlist_option(
} }
} }
export function get_new_page_option(new_playlist_modal_props: any = {}): Option {
return {
label: 'New page',
action: () => {
modal().showPageModal(new_playlist_modal_props)
},
}
}
type action = (playlist: Playlist) => void type action = (playlist: Playlist) => void
/** /**
@@ -29,10 +37,7 @@ type action = (playlist: Playlist) => void
* @param new_playlist_modal_props Props to be passed to the modal when creating a new playlist * @param new_playlist_modal_props Props to be passed to the modal when creating a new playlist
* @returns A list of options to be used in a context menu * @returns A list of options to be used in a context menu
*/ */
export async function getAddToPlaylistOptions( export async function getAddToPlaylistOptions(addToPlaylist: action, new_playlist_modal_props: any = {}) {
addToPlaylist: action,
new_playlist_modal_props: any = {}
) {
const new_playlist = get_new_playlist_option(new_playlist_modal_props) const new_playlist = get_new_playlist_option(new_playlist_modal_props)
const p = await getAllPlaylists(true) const p = await getAllPlaylists(true)
@@ -44,7 +49,7 @@ export async function getAddToPlaylistOptions(
let playlists = <Option[]>[] let playlists = <Option[]>[]
playlists = p.map((playlist) => { playlists = p.map(playlist => {
return <Option>{ return <Option>{
label: playlist.name, label: playlist.name,
action: () => { action: () => {
@@ -56,20 +61,42 @@ export async function getAddToPlaylistOptions(
return [...items, separator, ...playlists] return [...items, separator, ...playlists]
} }
export const get_find_on_social = (page = 'album', query = '') => { /**
*
* @param addToPlaylist Function to be called when a playlist is selected
* @param new_playlist_modal_props Props to be passed to the modal when creating a new playlist
* @returns A list of options to be used in a context menu
*/
export async function getAddToPageOptions(addToPage: (page: Page) => void, new_page_modal_props: any = {}) {
const new_page = get_new_page_option(new_page_modal_props)
const p = await getAllPages()
let items = [new_page]
if (p.length === 0) {
return items
}
let pages = <Option[]>[]
pages = p.map(playlist => {
return <Option>{
label: playlist.name,
action: () => {
addToPage(playlist)
},
}
})
return [...items, separator, ...pages]
}
export const get_find_on_social = (page = 'album', query = '', album?: Album) => {
const is_album = page === 'album' const is_album = page === 'album'
const getAlbumSearchTerm = () => { const getAlbumSearchTerm = () => {
const store = useAlbum() return `${album?.title} - ${album?.albumartists.map(a => a.name).join(', ')}`
return `${store.info.title} - ${store.info.albumartists
.map((a) => a.name)
.join(', ')}`
} }
const search_term = query const search_term = query ? query : is_album ? getAlbumSearchTerm() : useArtist().info.name
? query
: is_album
? getAlbumSearchTerm()
: useArtist().info.name
return <Option>{ return <Option>{
label: 'Search on', label: 'Search on',
@@ -77,67 +104,36 @@ export const get_find_on_social = (page = 'album', query = '') => {
children: async () => [ children: async () => [
{ {
label: 'Google', label: 'Google',
action: () => action: () => window.open(`https://www.google.com/search?q=${search_term}`, '_blank'),
window.open(
`https://www.google.com/search?q=${search_term}`,
'_blank'
),
}, },
{ {
label: 'YouTube', label: 'YouTube',
action: () => action: () => window.open(`https://www.youtube.com/results?search_query=${search_term}`, '_blank'),
window.open(
`https://www.youtube.com/results?search_query=${search_term}`,
'_blank'
),
}, },
{ {
label: 'Spotify', label: 'Spotify',
action: () => action: () => window.open(`https://open.spotify.com/search/${search_term}/${page}s`, '_blank'),
window.open(
`https://open.spotify.com/search/${search_term}/${page}s`,
'_blank'
),
}, },
{ {
label: 'Tidal', label: 'Tidal',
action: () => action: () => window.open(`https://listen.tidal.com/search/${page}s?q=${search_term}`, '_blank'),
window.open(
`https://listen.tidal.com/search/${page}s?q=${search_term}`,
'_blank'
),
}, },
{ {
label: 'Apple Music', label: 'Apple Music',
action: () => action: () => window.open(`https://music.apple.com/search?term=${search_term}`, '_blank'),
window.open(
`https://music.apple.com/search?term=${search_term}`,
'_blank'
),
}, },
{ {
label: 'Deezer', label: 'Deezer',
action: () => action: () => window.open(`https://www.deezer.com/search/${search_term}/${page}`, '_blank'),
window.open(
`https://www.deezer.com/search/${search_term}/${page}`,
'_blank'
),
}, },
{ {
label: 'Wikipedia', label: 'Wikipedia',
action: () => action: () =>
window.open( window.open(`https://en.wikipedia.org/wiki/Special:Search?search=${search_term}`, '_blank'),
`https://en.wikipedia.org/wiki/Special:Search?search=${search_term}`,
'_blank'
),
}, },
{ {
label: 'Last.fm', label: 'Last.fm',
action: () => action: () => window.open(`https://www.last.fm/search/${page}s?q=${search_term}`, '_blank'),
window.open(
`https://www.last.fm/search/${page}s?q=${search_term}`,
'_blank'
),
}, },
], ],
} }

View File

@@ -1,88 +1,78 @@
import { Store } from "pinia"; import { Store } from 'pinia'
import { Ref } from "vue"; import { Ref } from 'vue'
import { useRoute } from "vue-router"; import { useRoute } from 'vue-router'
import { ContextSrc } from "@/enums"; import { ContextSrc } from '@/enums'
import { Track } from "@/interfaces"; import { Album, Track } from '@/interfaces'
import useContextStore from "@/stores/context"; import useContextStore from '@/stores/context'
import albumContextItems from "@/context_menus/album"; import albumContextItems from '@/context_menus/album'
import artistContextItems from "@/context_menus/artist"; import artistContextItems from '@/context_menus/artist'
import folderContextItems from "@/context_menus/folder"; import folderContextItems from '@/context_menus/folder'
import trackContextItems from "@/context_menus/track"; import trackContextItems from '@/context_menus/track'
import queueContextItems from "@/context_menus/queue"; import queueContextItems from '@/context_menus/queue'
let stop_prev_watcher = () => {}; let stop_prev_watcher = () => {}
function flagWatcher(menu: Store, flag: Ref<boolean>) { function flagWatcher(menu: Store, flag: Ref<boolean>) {
stop_prev_watcher(); stop_prev_watcher()
if (flag.value) { if (flag.value) {
return (flag.value = false); return (flag.value = false)
} }
// watch for context menu visibility and reset flag // watch for context menu visibility and reset flag
stop_prev_watcher = menu.$subscribe((mutation, state) => { stop_prev_watcher = menu.$subscribe((mutation, state) => {
//@ts-ignore //@ts-ignore
flag.value = state.visible; flag.value = state.visible
}); })
} }
export const showTrackContextMenu = ( export const showTrackContextMenu = (e: MouseEvent, track: Track, flag: Ref<boolean>) => {
e: MouseEvent, const menu = useContextStore()
track: Track, const options = () => trackContextItems(track)
flag: Ref<boolean>,
) => {
const menu = useContextStore();
const options = () => trackContextItems(track);
menu.showContextMenu(e, options, ContextSrc.Track); menu.showContextMenu(e, options, ContextSrc.Track)
flagWatcher(menu, flag); flagWatcher(menu, flag)
}; }
export const showAlbumContextMenu = (e: MouseEvent, flag: Ref<boolean>) => { export const showAlbumContextMenu = (e: MouseEvent, flag: Ref<boolean>, album?: Album) => {
const menu = useContextStore(); const menu = useContextStore()
const options = () => albumContextItems(); const options = () => albumContextItems(album)
menu.showContextMenu(e, options, ContextSrc.AlbumHeader); menu.showContextMenu(e, options, ContextSrc.AlbumHeader)
flagWatcher(menu, flag); flagWatcher(menu, flag)
}; }
export const showFolderContextMenu = ( export const showFolderContextMenu = (e: MouseEvent, flag: Ref<boolean>, source: ContextSrc, path: string) => {
e: MouseEvent, const menu = useContextStore()
flag: Ref<boolean>,
source: ContextSrc,
path: string
) => {
const menu = useContextStore();
const options = () => folderContextItems(path); const options = () => folderContextItems(path)
menu.showContextMenu(e, options, source); menu.showContextMenu(e, options, source)
flagWatcher(menu, flag); flagWatcher(menu, flag)
}; }
export const showArtistContextMenu = ( export const showArtistContextMenu = (e: MouseEvent, flag: Ref<boolean>, artisthash: string, artistname: string) => {
e: MouseEvent, const menu = useContextStore()
flag: Ref<boolean>,
artisthash: string,
artistname: string
) => {
const menu = useContextStore();
const options = () => artistContextItems(artisthash, artistname); const options = () => artistContextItems(artisthash, artistname)
menu.showContextMenu(e, options, ContextSrc.ArtistHeader); menu.showContextMenu(e, options, ContextSrc.ArtistHeader)
flagWatcher(menu, flag); flagWatcher(menu, flag)
}; }
export const showQueueContextMenu = (e: MouseEvent, flag: Ref<boolean>) => { export const showQueueContextMenu = (e: MouseEvent, flag: Ref<boolean>) => {
const menu = useContextStore(); const menu = useContextStore()
const options = () => queueContextItems(); const options = () => queueContextItems()
menu.showContextMenu(e, options, ContextSrc.Queue); menu.showContextMenu(e, options, ContextSrc.Queue)
flagWatcher(menu, flag); flagWatcher(menu, flag)
}; }
// export const showAlbumCardContextMenu = (e: MouseEvent, flag: Ref<boolean>, album: Album) => {
// }

View File

@@ -28,6 +28,7 @@ export interface Track extends AlbumDisc {
trackhash: string trackhash: string
filetype: string filetype: string
is_favorite: boolean is_favorite: boolean
explicit: boolean
og_title: string og_title: string
og_album: string og_album: string
@@ -117,6 +118,7 @@ export interface HomePageItem {
items: { type: string; item?: any; with_helptext?: boolean }[] items: { type: string; item?: any; with_helptext?: boolean }[]
path?: string path?: string
seeAllText?: string seeAllText?: string
url?: string
} }
export interface Artist { export interface Artist {
@@ -178,6 +180,15 @@ export interface Playlist {
}[] }[]
} }
export interface Page {
id: number
name: string
items: { item: Album | Artist | Mix | Playlist; type: string }[]
extra: {
description: string
}
}
export interface Radio { export interface Radio {
name: string name: string
image: string image: string

138
src/requests/pages.ts Normal file
View File

@@ -0,0 +1,138 @@
import { paths } from '@/config'
import { Album, Artist, Mix, Page, Playlist } from '@/interfaces'
import { Notification, NotifType } from '@/stores/notification'
import useAxios from './useAxios'
const { base: basePageUrl } = paths.api.pages
export async function getAllPages() {
const { data, status } = await useAxios({
url: basePageUrl,
method: 'GET',
})
if (status == 200) {
return data as Page[]
}
return []
}
export async function getPage(page_id: string) {
const { data, status } = await useAxios({
url: basePageUrl + `/${page_id}`,
method: 'GET',
})
return data as Page
}
export async function createNewPage(
name: string,
description: string,
items?: { hash: string; type: string; extra: any }[]
) {
const { data, status } = await useAxios({
url: basePageUrl,
props: {
name,
description,
items,
},
method: 'POST',
})
if (status == 201) {
return true
}
return false
}
export async function updatePage(page: Page, name: string, description: string) {
const { data, status } = await useAxios({
url: basePageUrl + `/${page.id}`,
props: {
name,
description,
},
method: 'PUT',
})
if (status == 200) {
return data.page as Page
}
return null
}
export async function addOrRemoveItemFromPage(
page_number: number,
item: Album | Artist | Mix | Playlist,
type: string,
command: 'add' | 'remove'
) {
const payload = {
type: type,
hash: '',
extra: {},
}
switch (type) {
case 'album':
payload.hash = (item as Album).albumhash
break
case 'artist':
payload.hash = (item as Artist).artisthash
break
case 'mix':
payload.hash = (item as Mix).sourcehash
break
case 'playlist':
payload.hash = (item as Playlist).id.toString()
break
}
if (payload.hash === '') {
throw new Error('Invalid item type. Item not added to page.')
}
const { data, status } = await useAxios({
url: basePageUrl + `/${page_number}/items`,
props: {
item: payload,
},
method: command == 'add' ? 'POST' : 'DELETE',
})
if (status == 200) {
new Notification(
`${payload.type[0].toUpperCase() + payload.type.slice(1)} ${
command == 'add' ? 'added' : 'removed'
} to page`,
NotifType.Success
)
return true
}
if (status == 400) {
new Notification(`${payload.type[0].toUpperCase() + payload.type.slice(1)} already in page`, NotifType.Error)
return false
}
new Notification('Failed: ' + data.error, NotifType.Error)
return false
}
export async function deletePage(page_number: number) {
const { data, status } = await useAxios({
url: basePageUrl + `/${page_number}`,
method: 'DELETE',
})
if (status == 200) {
return true
}
return false
}

View File

@@ -28,6 +28,7 @@ const FavoriteCardScroller = () => import("@/views/FavoriteCardScroller.vue");
const StatsView = () => import("@/views/Stats/main.vue"); const StatsView = () => import("@/views/Stats/main.vue");
const MixView = () => import("@/views/MixView.vue"); const MixView = () => import("@/views/MixView.vue");
const MixListView = () => import("@/views/MixListView.vue"); const MixListView = () => import("@/views/MixListView.vue");
const Page = () => import("@/views/Pages/Page.vue");
const folder = { const folder = {
path: "/folder/:path", path: "/folder/:path",
@@ -202,6 +203,12 @@ const MixList = {
component: MixListView, component: MixListView,
}; };
const PageView = {
path: "/pages/:page",
name: "Page",
component: Page,
};
const routes = [ const routes = [
folder, folder,
playlists, playlists,
@@ -225,6 +232,7 @@ const routes = [
Stats, Stats,
Mix, Mix,
MixList, MixList,
PageView,
]; ];
const Routes = { const Routes = {
@@ -250,6 +258,7 @@ const Routes = {
Stats: Stats.name, Stats: Stats.name,
Mix: Mix.name, Mix: Mix.name,
MixList: MixList.name, MixList: MixList.name,
Page: PageView.name,
}; };
const router = createRouter({ const router = createRouter({

View File

@@ -1,63 +1,61 @@
import { Setting } from "@/interfaces/settings"; import { Setting } from '@/interfaces/settings'
import { import { addRootDirs as editRootDirs, triggerScan } from '@/requests/settings/rootdirs'
addRootDirs as editRootDirs, import { SettingType } from '../enums'
triggerScan, import { manageRootDirsStrings as data } from '../strings'
} from "@/requests/settings/rootdirs";
import { SettingType } from "../enums";
import { manageRootDirsStrings as data } from "../strings";
import useModalStore from "@/stores/modal"; import useModalStore from '@/stores/modal'
import useSettingsStore from "@/stores/settings"; import useSettingsStore from '@/stores/settings'
const text = data.settings; const text = data.settings
const change_root_dirs: Setting = { const change_root_dirs: Setting = {
title: text.change, title: text.change,
type: SettingType.button, type: SettingType.button,
state: null, state: null,
button_text: () => button_text: () => `\xa0 \xa0 ${useSettingsStore().root_dirs.length ? 'Modify' : 'Configure'} \xa0 \xa0`,
`\xa0 \xa0 ${
useSettingsStore().root_dirs.length ? "Modify" : "Configure"
} \xa0 \xa0`,
action: () => useModalStore().showRootDirsPromptModal(), action: () => useModalStore().showRootDirsPromptModal(),
}; }
const list_root_dirs: Setting = { const list_root_dirs: Setting = {
title: text.list_root_dirs, title: text.list_root_dirs,
type: SettingType.root_dirs, type: SettingType.root_dirs,
state: () => state: () =>
useSettingsStore().root_dirs.map((d) => ({ useSettingsStore().root_dirs.map(d => ({
title: d, title: d,
action: () => { action: () => {
editRootDirs([], [d]).then((all_dirs) => { editRootDirs([], [d]).then(all_dirs => {
useSettingsStore().setRootDirs(all_dirs); useSettingsStore().setRootDirs(all_dirs)
}); })
}, },
})), })),
defaultAction: () => {}, defaultAction: () => {},
action: () => triggerScan(), action: () => triggerScan(),
}; }
const enable_scans: Setting = { // const enable_scans: Setting = {
title: "Enable periodic scans", // title: "Enable periodic scans",
type: SettingType.binary, // type: SettingType.binary,
state: () => useSettingsStore().enablePeriodicScans, // state: () => useSettingsStore().enablePeriodicScans,
action: () => useSettingsStore().togglePeriodicScans(), // action: () => useSettingsStore().togglePeriodicScans(),
}; // };
const useWatchdog: Setting = { // const useWatchdog: Setting = {
title: "Watch root dirs for new music", // title: "Watch root dirs for new music",
experimental: true, // experimental: true,
type: SettingType.binary, // type: SettingType.binary,
state: () => useSettingsStore().enableWatchDog, // state: () => useSettingsStore().enableWatchDog,
action: () => useSettingsStore().toggleWatchdog(), // action: () => useSettingsStore().toggleWatchdog(),
}; // };
const periodicScanInterval: Setting = { // const periodicScanInterval: Setting = {
title: "Periodic scan interval (minutes)", // title: "Periodic scan interval (minutes)",
type: SettingType.free_number_input, // type: SettingType.free_number_input,
state: () => useSettingsStore().periodicInterval, // state: () => useSettingsStore().periodicInterval,
action: (newValue: number) => useSettingsStore().updatePeriodicInterval(newValue), // action: (newValue: number) => useSettingsStore().updatePeriodicInterval(newValue),
}; // };
export default [change_root_dirs, list_root_dirs, useWatchdog, enable_scans, periodicScanInterval]; export default [
change_root_dirs,
list_root_dirs,
// useWatchdog, enable_scans, periodicScanInterval
]

View File

@@ -30,9 +30,11 @@ export default defineStore('homepage', () => {
async function fetchAll() { async function fetchAll() {
const data: { [key: string]: HomePageItem }[] = await getHomePageData(maxAbumCards.value) const data: { [key: string]: HomePageItem }[] = await getHomePageData(maxAbumCards.value)
let keys = []
for (const [index, item] of data.entries()) { for (const [index, item] of data.entries()) {
const key = Object.keys(item)[0] const key = Object.keys(item)[0]
keys.push(key)
// @ts-ignore // @ts-ignore
homepageData[key] = item[key] homepageData[key] = item[key]
// @ts-ignore // @ts-ignore
@@ -41,6 +43,18 @@ export default defineStore('homepage', () => {
homepageData[key].path = routes[key] homepageData[key].path = routes[key]
// @ts-ignore // @ts-ignore
homepageData[key].seeAllText = seeAllTexts[key] homepageData[key].seeAllText = seeAllTexts[key]
if (item[key].url) {
// @ts-ignore
homepageData[key].path = item[key].url
}
}
// remove keys not in response
for (const key in homepageData) {
if (!keys.includes(key)) {
delete homepageData[key]
}
} }
} }

View File

@@ -1,7 +1,8 @@
import { defineStore } from "pinia"; import { defineStore } from 'pinia'
export enum ModalOptions { export enum ModalOptions {
newPlaylist, newPlaylist,
page,
updatePlaylist, updatePlaylist,
deletePlaylist, deletePlaylist,
SetIP, SetIP,
@@ -9,12 +10,12 @@ export enum ModalOptions {
setRootDirs, setRootDirs,
saveFolderAsPlaylist, saveFolderAsPlaylist,
login, login,
settings settings,
} }
export default defineStore("newModal", { export default defineStore('newModal', {
state: () => ({ state: () => ({
title: "", title: '',
options: ModalOptions, options: ModalOptions,
component: <any>null, component: <any>null,
props: <any>{}, props: <any>{},
@@ -22,65 +23,68 @@ export default defineStore("newModal", {
}), }),
actions: { actions: {
showModal(modalOption: ModalOptions, props: any = {}) { showModal(modalOption: ModalOptions, props: any = {}) {
this.component = modalOption; this.component = modalOption
this.visible = true; this.visible = true
this.props = props; this.props = props
}, },
showNewPlaylistModal(props: any = {}) { showNewPlaylistModal(props: any = {}) {
this.showModal(ModalOptions.newPlaylist, props); this.showModal(ModalOptions.newPlaylist, props)
},
showPageModal(props: any = {}) {
this.showModal(ModalOptions.page, props)
}, },
showSaveFolderAsPlaylistModal(path: string) { showSaveFolderAsPlaylistModal(path: string) {
const playlist_name = path.split("/").pop(); const playlist_name = path.split('/').pop()
const props = { const props = {
playlist_name, playlist_name,
path, path,
}; }
this.showModal(ModalOptions.newPlaylist, props); this.showModal(ModalOptions.newPlaylist, props)
}, },
showSaveArtistAsPlaylistModal(name: string, artisthash: string) { showSaveArtistAsPlaylistModal(name: string, artisthash: string) {
const props = { const props = {
artisthash, artisthash,
playlist_name: `This is ${name}`, playlist_name: `This is ${name}`,
}; }
this.showModal(ModalOptions.newPlaylist, props); this.showModal(ModalOptions.newPlaylist, props)
}, },
showSaveQueueAsPlaylistModal(name: string) { showSaveQueueAsPlaylistModal(name: string) {
const props = { const props = {
is_queue: true, is_queue: true,
playlist_name: name, playlist_name: name,
}; }
this.showModal(ModalOptions.newPlaylist, props); this.showModal(ModalOptions.newPlaylist, props)
}, },
showEditPlaylistModal() { showEditPlaylistModal() {
this.showModal(ModalOptions.updatePlaylist); this.showModal(ModalOptions.updatePlaylist)
}, },
showDeletePlaylistModal(pid: number) { showDeletePlaylistModal(pid: number) {
const props = { const props = {
pid: pid, pid: pid,
}; }
this.showModal(ModalOptions.deletePlaylist, props); this.showModal(ModalOptions.deletePlaylist, props)
}, },
showSetIPModal() { showSetIPModal() {
this.showModal(ModalOptions.SetIP); this.showModal(ModalOptions.SetIP)
}, },
showRootDirsPromptModal() { showRootDirsPromptModal() {
this.showModal(ModalOptions.rootDirsPrompt); this.showModal(ModalOptions.rootDirsPrompt)
}, },
showSetRootDirsModal() { showSetRootDirsModal() {
this.showModal(ModalOptions.setRootDirs); this.showModal(ModalOptions.setRootDirs)
}, },
showLoginModal(){ showLoginModal() {
this.showModal(ModalOptions.login); this.showModal(ModalOptions.login)
}, },
showSettingsModal(){ showSettingsModal() {
this.showModal(ModalOptions.settings); this.showModal(ModalOptions.settings)
}, },
hideModal() { hideModal() {
this.visible = false; this.visible = false
this.setTitle(""); this.setTitle('')
}, },
setTitle(new_title: string) { setTitle(new_title: string) {
this.title = new_title; this.title = new_title
}, },
}, },
}); })

25
src/stores/pages/page.ts Normal file
View File

@@ -0,0 +1,25 @@
import { Artist, Album, Page } from '@/interfaces'
import { getPage } from '@/requests/pages'
import { defineStore } from 'pinia'
export default defineStore('page', {
state: () => ({
page: <Page | null>null,
}),
actions: {
async fetchPage(page_no: string) {
this.page = await getPage(page_no)
},
async removeLocalItem(item: Album | Artist, type: 'album' | 'artist') {
if (!this.page) return
if (type == 'album') {
this.page.items = this.page.items.filter(i => (i.item as Album).albumhash != (item as Album).albumhash)
} else {
this.page.items = this.page.items.filter(
i => (i.item as Artist).artisthash != (item as Artist).artisthash
)
}
},
},
})

68
src/views/Pages/Page.vue Normal file
View File

@@ -0,0 +1,68 @@
<template>
<CardGridPage :items="page.page?.items || []">
<template #header>
<GenericHeader>
<template #name>
<span @click="updatePage">
{{ page.page?.name }} <span><PencilSvg height="0.8rem" width="0.8rem" /></span
></span>
</template>
<template #description v-if="page.page?.extra.description">
<span @click="updatePage"> {{ page.page?.extra.description }} </span>
</template>
<template #right>
<button @click="deletePage"><DeleteSvg height="1.2rem" width="1.2rem" /> Delete</button>
</template>
</GenericHeader>
</template>
</CardGridPage>
</template>
<script setup lang="ts">
import { Page } from '@/interfaces'
import { getPage } from '@/requests/pages'
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import DeleteSvg from '@/assets/icons/delete.svg'
import PencilSvg from '@/assets/icons/pencil.svg'
import GenericHeader from '@/components/shared/GenericHeader.vue'
import CardGridPage from '@/views/SearchView/CardGridPage.vue'
import useModal from '@/stores/modal'
import usePage from '@/stores/pages/page'
const modal = useModal()
const page = usePage()
onMounted(async () => {
const route = useRoute()
const page_id = route.params.page as string
page.fetchPage(page_id)
})
function updatePage() {
console.log('update page')
modal.showPageModal({
page: page.page,
})
}
function deletePage() {
modal.showPageModal({
page: page.page,
delete: true,
})
}
</script>
<style scoped lang="scss">
span {
cursor: text;
margin-right: $smaller;
}
.generichead {
margin-top: 2rem;
}
</style>

View File

@@ -7,12 +7,7 @@
v-if="showNoItemsComponent" v-if="showNoItemsComponent"
/> />
<div class="v-scroll-page" style="height: 100%"> <div class="v-scroll-page" style="height: 100%">
<DynamicScroller <DynamicScroller style="height: 100%" class="scroller" :min-item-size="64" :items="scrollerItems">
style="height: 100%"
class="scroller"
:min-item-size="64"
:items="scrollerItems"
>
<template #before> <template #before>
<slot name="header"></slot> <slot name="header"></slot>
</template> </template>
@@ -23,11 +18,7 @@
:size-dependencies="[item.props]" :size-dependencies="[item.props]"
:data-index="index" :data-index="index"
> >
<component <component :is="item.component" :key="index" v-bind="item.props"></component>
:is="item.component"
:key="index"
v-bind="item.props"
></component>
</DynamicScrollerItem> </DynamicScrollerItem>
</template> </template>
</DynamicScroller> </DynamicScroller>
@@ -35,51 +26,50 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import { computed } from 'vue'
import useSearchStore from "@/stores/search"; import useSearchStore from '@/stores/search'
import { maxAbumCards } from "@/stores/content-width"; import { maxAbumCards } from '@/stores/content-width'
import SearchSvg from "@/assets/icons/search.svg"; import SearchSvg from '@/assets/icons/search.svg'
import NoItems from "@/components/shared/NoItems.vue"; import NoItems from '@/components/shared/NoItems.vue'
import CardRow from "@/components/shared/CardRow.vue"; import CardRow from '@/components/shared/CardRow.vue'
import AlbumsFetcher from "@/components/ArtistView/AlbumsFetcher.vue"; import AlbumsFetcher from '@/components/ArtistView/AlbumsFetcher.vue'
const props = defineProps<{ const props = defineProps<{
page: "album" | "artist" | "mix"; page?: 'album' | 'artist' | 'mix'
fetch_callback?: () => Promise<void>; fetch_callback?: () => Promise<void>
items: any[]; items: any[]
outside_route?: boolean; outside_route?: boolean
showNoItemsComponent?: boolean; showNoItemsComponent?: boolean
}>(); }>()
const search = useSearchStore(); const search = useSearchStore()
const desc = computed(() => const desc = computed(() =>
search.query === "" search.query === ''
? `Start typing to search for ${props.page}s` ? `Start typing to search for ${props.page}s`
: `Results for '${search.query}' should appear here` : `Results for '${search.query}' should appear here`
); )
const scrollerItems = computed(() => { const scrollerItems = computed(() => {
let maxCards = maxAbumCards.value; let maxCards = maxAbumCards.value
if (props.outside_route) { if (props.outside_route) {
maxCards = 6; maxCards = 6
} }
const groups = Math.ceil(props.items.length / maxCards); const groups = Math.ceil(props.items.length / maxCards)
const items = []; const items = []
for (let i = 0; i < groups; i++) { for (let i = 0; i < groups; i++) {
items.push({ items.push({
id: i, id: i,
component: CardRow, component: CardRow,
props: { props: {
type: props.page,
items: props.items.slice(i * maxCards, (i + 1) * maxCards), items: props.items.slice(i * maxCards, (i + 1) * maxCards),
}, },
}); })
} }
const moreItems = props.page === 'album' ? search.albums.more : search.artists.more const moreItems = props.page === 'album' ? search.albums.more : search.artists.more
@@ -92,9 +82,9 @@ const scrollerItems = computed(() => {
fetch_callback: props.fetch_callback, fetch_callback: props.fetch_callback,
outside_route: props.outside_route, outside_route: props.outside_route,
}, },
}); })
} }
return items; return items
}); })
</script> </script>

232
yarn.lock
View File

@@ -295,11 +295,21 @@
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f"
integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==
"@babel/helper-string-parser@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c"
integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==
"@babel/helper-validator-identifier@^7.22.5": "@babel/helper-validator-identifier@^7.22.5":
version "7.22.5" version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193"
integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==
"@babel/helper-validator-identifier@^7.25.9":
version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7"
integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
"@babel/helper-validator-option@^7.22.5": "@babel/helper-validator-option@^7.22.5":
version "7.22.5" version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac"
@@ -342,11 +352,6 @@
chalk "^2.0.0" chalk "^2.0.0"
js-tokens "^4.0.0" js-tokens "^4.0.0"
"@babel/parser@^7.16.4":
version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.7.tgz#66fe23b3c8569220817d5feb8b9dcdc95bb4f71b"
integrity sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==
"@babel/parser@^7.20.15", "@babel/parser@^7.21.3", "@babel/parser@^7.22.7": "@babel/parser@^7.20.15", "@babel/parser@^7.21.3", "@babel/parser@^7.22.7":
version "7.22.7" version "7.22.7"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.7.tgz#df8cf085ce92ddbdbf668a7f186ce848c9036cae" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.7.tgz#df8cf085ce92ddbdbf668a7f186ce848c9036cae"
@@ -357,6 +362,13 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.5.tgz#721fd042f3ce1896238cf1b341c77eb7dee7dbea" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.5.tgz#721fd042f3ce1896238cf1b341c77eb7dee7dbea"
integrity sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q== integrity sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==
"@babel/parser@^7.25.3":
version "7.26.7"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.7.tgz#e114cd099e5f7d17b05368678da0fb9f69b3385c"
integrity sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==
dependencies:
"@babel/types" "^7.26.7"
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5": "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5":
version "7.22.5" version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz#87245a21cd69a73b0b81bcda98d443d6df08f05e" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz#87245a21cd69a73b0b81bcda98d443d6df08f05e"
@@ -1072,6 +1084,14 @@
"@babel/helper-validator-identifier" "^7.22.5" "@babel/helper-validator-identifier" "^7.22.5"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@babel/types@^7.26.7":
version "7.26.7"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.7.tgz#5e2b89c0768e874d4d061961f3a5a153d71dc17a"
integrity sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==
dependencies:
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@canvas/image-data@^1.0.0": "@canvas/image-data@^1.0.0":
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/@canvas/image-data/-/image-data-1.0.0.tgz#3bd2cd856e13fc9e2c25feff360a4056857b0367" resolved "https://registry.yarnpkg.com/@canvas/image-data/-/image-data-1.0.0.tgz#3bd2cd856e13fc9e2c25feff360a4056857b0367"
@@ -1652,16 +1672,6 @@
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.2.0.tgz#a1484089dd85d6528f435743f84cdd0d215bbb54" resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.2.0.tgz#a1484089dd85d6528f435743f84cdd0d215bbb54"
integrity sha512-E0tnaL4fr+qkdCNxJ+Xd0yM31UwMkQje76fsDVBBUCoGOUPexu2VDUYHL8P4CwV+zMvWw6nlRw19OnRKmYAJpw== integrity sha512-E0tnaL4fr+qkdCNxJ+Xd0yM31UwMkQje76fsDVBBUCoGOUPexu2VDUYHL8P4CwV+zMvWw6nlRw19OnRKmYAJpw==
"@vue/compiler-core@3.2.45":
version "3.2.45"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.45.tgz#d9311207d96f6ebd5f4660be129fb99f01ddb41b"
integrity sha512-rcMj7H+PYe5wBV3iYeUgbCglC+pbpN8hBLTJvRiK2eKQiWqu+fG9F+8sW99JdL4LQi7Re178UOxn09puSXvn4A==
dependencies:
"@babel/parser" "^7.16.4"
"@vue/shared" "3.2.45"
estree-walker "^2.0.2"
source-map "^0.6.1"
"@vue/compiler-core@3.3.4": "@vue/compiler-core@3.3.4":
version "3.3.4" version "3.3.4"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.3.4.tgz#7fbf591c1c19e1acd28ffd284526e98b4f581128" resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.3.4.tgz#7fbf591c1c19e1acd28ffd284526e98b4f581128"
@@ -1672,13 +1682,16 @@
estree-walker "^2.0.2" estree-walker "^2.0.2"
source-map-js "^1.0.2" source-map-js "^1.0.2"
"@vue/compiler-dom@3.2.45": "@vue/compiler-core@3.5.13":
version "3.2.45" version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.45.tgz#c43cc15e50da62ecc16a42f2622d25dc5fd97dce" resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.13.tgz#b0ae6c4347f60c03e849a05d34e5bf747c9bda05"
integrity sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw== integrity sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==
dependencies: dependencies:
"@vue/compiler-core" "3.2.45" "@babel/parser" "^7.25.3"
"@vue/shared" "3.2.45" "@vue/shared" "3.5.13"
entities "^4.5.0"
estree-walker "^2.0.2"
source-map-js "^1.2.0"
"@vue/compiler-dom@3.3.4": "@vue/compiler-dom@3.3.4":
version "3.3.4" version "3.3.4"
@@ -1688,21 +1701,28 @@
"@vue/compiler-core" "3.3.4" "@vue/compiler-core" "3.3.4"
"@vue/shared" "3.3.4" "@vue/shared" "3.3.4"
"@vue/compiler-sfc@3.2.45": "@vue/compiler-dom@3.5.13":
version "3.2.45" version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.45.tgz#7f7989cc04ec9e7c55acd406827a2c4e96872c70" resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz#bb1b8758dbc542b3658dda973b98a1c9311a8a58"
integrity sha512-1jXDuWah1ggsnSAOGsec8cFjT/K6TMZ0sPL3o3d84Ft2AYZi2jWJgRMjw4iaK0rBfA89L5gw427H4n1RZQBu6Q== integrity sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==
dependencies: dependencies:
"@babel/parser" "^7.16.4" "@vue/compiler-core" "3.5.13"
"@vue/compiler-core" "3.2.45" "@vue/shared" "3.5.13"
"@vue/compiler-dom" "3.2.45"
"@vue/compiler-ssr" "3.2.45" "@vue/compiler-sfc@3.5.13":
"@vue/reactivity-transform" "3.2.45" version "3.5.13"
"@vue/shared" "3.2.45" resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz#461f8bd343b5c06fac4189c4fef8af32dea82b46"
integrity sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==
dependencies:
"@babel/parser" "^7.25.3"
"@vue/compiler-core" "3.5.13"
"@vue/compiler-dom" "3.5.13"
"@vue/compiler-ssr" "3.5.13"
"@vue/shared" "3.5.13"
estree-walker "^2.0.2" estree-walker "^2.0.2"
magic-string "^0.25.7" magic-string "^0.30.11"
postcss "^8.1.10" postcss "^8.4.48"
source-map "^0.6.1" source-map-js "^1.2.0"
"@vue/compiler-sfc@^3.2.20": "@vue/compiler-sfc@^3.2.20":
version "3.3.4" version "3.3.4"
@@ -1720,14 +1740,6 @@
postcss "^8.1.10" postcss "^8.1.10"
source-map-js "^1.0.2" source-map-js "^1.0.2"
"@vue/compiler-ssr@3.2.45":
version "3.2.45"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.45.tgz#bd20604b6e64ea15344d5b6278c4141191c983b2"
integrity sha512-6BRaggEGqhWht3lt24CrIbQSRD5O07MTmd+LjAn5fJj568+R9eUD2F7wMQJjX859seSlrYog7sUtrZSd7feqrQ==
dependencies:
"@vue/compiler-dom" "3.2.45"
"@vue/shared" "3.2.45"
"@vue/compiler-ssr@3.3.4": "@vue/compiler-ssr@3.3.4":
version "3.3.4" version "3.3.4"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz#9d1379abffa4f2b0cd844174ceec4a9721138777" resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz#9d1379abffa4f2b0cd844174ceec4a9721138777"
@@ -1736,22 +1748,19 @@
"@vue/compiler-dom" "3.3.4" "@vue/compiler-dom" "3.3.4"
"@vue/shared" "3.3.4" "@vue/shared" "3.3.4"
"@vue/compiler-ssr@3.5.13":
version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz#e771adcca6d3d000f91a4277c972a996d07f43ba"
integrity sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==
dependencies:
"@vue/compiler-dom" "3.5.13"
"@vue/shared" "3.5.13"
"@vue/devtools-api@^6.4.5": "@vue/devtools-api@^6.4.5":
version "6.4.5" version "6.4.5"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.4.5.tgz#d54e844c1adbb1e677c81c665ecef1a2b4bb8380" resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.4.5.tgz#d54e844c1adbb1e677c81c665ecef1a2b4bb8380"
integrity sha512-JD5fcdIuFxU4fQyXUu3w2KpAJHzTVdN+p4iOX2lMWSHMOoQdMAcpFLZzm9Z/2nmsoZ1a96QEhZ26e50xLBsgOQ== integrity sha512-JD5fcdIuFxU4fQyXUu3w2KpAJHzTVdN+p4iOX2lMWSHMOoQdMAcpFLZzm9Z/2nmsoZ1a96QEhZ26e50xLBsgOQ==
"@vue/reactivity-transform@3.2.45":
version "3.2.45"
resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.45.tgz#07ac83b8138550c83dfb50db43cde1e0e5e8124d"
integrity sha512-BHVmzYAvM7vcU5WmuYqXpwaBHjsS8T63jlKGWVtHxAHIoMIlmaMyurUSEs1Zcg46M4AYT5MtB1U274/2aNzjJQ==
dependencies:
"@babel/parser" "^7.16.4"
"@vue/compiler-core" "3.2.45"
"@vue/shared" "3.2.45"
estree-walker "^2.0.2"
magic-string "^0.25.7"
"@vue/reactivity-transform@3.3.4": "@vue/reactivity-transform@3.3.4":
version "3.3.4" version "3.3.4"
resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz#52908476e34d6a65c6c21cd2722d41ed8ae51929" resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz#52908476e34d6a65c6c21cd2722d41ed8ae51929"
@@ -1763,48 +1772,49 @@
estree-walker "^2.0.2" estree-walker "^2.0.2"
magic-string "^0.30.0" magic-string "^0.30.0"
"@vue/reactivity@3.2.45": "@vue/reactivity@3.5.13":
version "3.2.45" version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.45.tgz#412a45b574de601be5a4a5d9a8cbd4dee4662ff0" resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.13.tgz#b41ff2bb865e093899a22219f5b25f97b6fe155f"
integrity sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A== integrity sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==
dependencies: dependencies:
"@vue/shared" "3.2.45" "@vue/shared" "3.5.13"
"@vue/runtime-core@3.2.45": "@vue/runtime-core@3.5.13":
version "3.2.45" version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.45.tgz#7ad7ef9b2519d41062a30c6fa001ec43ac549c7f" resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.13.tgz#1fafa4bf0b97af0ebdd9dbfe98cd630da363a455"
integrity sha512-gzJiTA3f74cgARptqzYswmoQx0fIA+gGYBfokYVhF8YSXjWTUA2SngRzZRku2HbGbjzB6LBYSbKGIaK8IW+s0A== integrity sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==
dependencies: dependencies:
"@vue/reactivity" "3.2.45" "@vue/reactivity" "3.5.13"
"@vue/shared" "3.2.45" "@vue/shared" "3.5.13"
"@vue/runtime-dom@3.2.45": "@vue/runtime-dom@3.5.13":
version "3.2.45" version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.45.tgz#1a2ef6ee2ad876206fbbe2a884554bba2d0faf59" resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz#610fc795de9246300e8ae8865930d534e1246215"
integrity sha512-cy88YpfP5Ue2bDBbj75Cb4bIEZUMM/mAkDMfqDTpUYVgTf/kuQ2VQ8LebuZ8k6EudgH8pYhsGWHlY0lcxlvTwA== integrity sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==
dependencies: dependencies:
"@vue/runtime-core" "3.2.45" "@vue/reactivity" "3.5.13"
"@vue/shared" "3.2.45" "@vue/runtime-core" "3.5.13"
csstype "^2.6.8" "@vue/shared" "3.5.13"
csstype "^3.1.3"
"@vue/server-renderer@3.2.45": "@vue/server-renderer@3.5.13":
version "3.2.45" version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.45.tgz#ca9306a0c12b0530a1a250e44f4a0abac6b81f3f" resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.13.tgz#429ead62ee51de789646c22efe908e489aad46f7"
integrity sha512-ebiMq7q24WBU1D6uhPK//2OTR1iRIyxjF5iVq/1a5I1SDMDyDu4Ts6fJaMnjrvD3MqnaiFkKQj+LKAgz5WIK3g== integrity sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==
dependencies: dependencies:
"@vue/compiler-ssr" "3.2.45" "@vue/compiler-ssr" "3.5.13"
"@vue/shared" "3.2.45" "@vue/shared" "3.5.13"
"@vue/shared@3.2.45":
version "3.2.45"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.45.tgz#a3fffa7489eafff38d984e23d0236e230c818bc2"
integrity sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==
"@vue/shared@3.3.4": "@vue/shared@3.3.4":
version "3.3.4" version "3.3.4"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.3.4.tgz#06e83c5027f464eef861c329be81454bc8b70780" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.3.4.tgz#06e83c5027f464eef861c329be81454bc8b70780"
integrity sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ== integrity sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==
"@vue/shared@3.5.13":
version "3.5.13"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.13.tgz#87b309a6379c22b926e696893237826f64339b6f"
integrity sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==
"@vueuse/components@^9.2.0": "@vueuse/components@^9.2.0":
version "9.10.0" version "9.10.0"
resolved "https://registry.yarnpkg.com/@vueuse/components/-/components-9.10.0.tgz#5c9686048119227d1cd59549e4a543dabfe860a0" resolved "https://registry.yarnpkg.com/@vueuse/components/-/components-9.10.0.tgz#5c9686048119227d1cd59549e4a543dabfe860a0"
@@ -2582,16 +2592,16 @@ csso@^5.0.5:
dependencies: dependencies:
css-tree "~2.2.0" css-tree "~2.2.0"
csstype@^2.6.8:
version "2.6.21"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.21.tgz#2efb85b7cc55c80017c66a5ad7cbd931fda3a90e"
integrity sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==
csstype@^3.1.2: csstype@^3.1.2:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
csstype@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
de-indent@^1.0.2: de-indent@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
@@ -2810,7 +2820,7 @@ enhanced-resolve@^4.1.1:
memory-fs "^0.5.0" memory-fs "^0.5.0"
tapable "^1.0.0" tapable "^1.0.0"
entities@^4.2.0: entities@^4.2.0, entities@^4.5.0:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
@@ -4203,7 +4213,7 @@ magic-string@^0.30.0:
dependencies: dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15" "@jridgewell/sourcemap-codec" "^1.4.15"
magic-string@^0.30.3: magic-string@^0.30.11, magic-string@^0.30.3:
version "0.30.17" version "0.30.17"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA== integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==
@@ -4393,6 +4403,11 @@ nanoid@^3.3.4:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
nanoid@^3.3.8:
version "3.3.8"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
nanoid@^4.0.2: nanoid@^4.0.2:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-4.0.2.tgz#140b3c5003959adbebf521c170f282c5e7f9fb9e" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-4.0.2.tgz#140b3c5003959adbebf521c170f282c5e7f9fb9e"
@@ -4687,6 +4702,11 @@ picocolors@^1.0.0:
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1: picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
@@ -4777,6 +4797,15 @@ postcss@^8.1.10, postcss@^8.4.18:
picocolors "^1.0.0" picocolors "^1.0.0"
source-map-js "^1.0.2" source-map-js "^1.0.2"
postcss@^8.4.48:
version "8.5.1"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.1.tgz#e2272a1f8a807fafa413218245630b5db10a3214"
integrity sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==
dependencies:
nanoid "^3.3.8"
picocolors "^1.1.1"
source-map-js "^1.2.1"
prebuild-install@^7.1.1: prebuild-install@^7.1.1:
version "7.1.1" version "7.1.1"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45"
@@ -5323,6 +5352,11 @@ sortable-dnd@0.2.7:
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
source-map-js@^1.2.0, source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
source-map-support@~0.5.20: source-map-support@~0.5.20:
version "0.5.21" version "0.5.21"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
@@ -5331,7 +5365,7 @@ source-map-support@~0.5.20:
buffer-from "^1.0.0" buffer-from "^1.0.0"
source-map "^0.6.0" source-map "^0.6.0"
source-map@^0.6.0, source-map@^0.6.1: source-map@^0.6.0:
version "0.6.1" version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
@@ -6000,16 +6034,16 @@ vue-wrap-balancer@^1.0.0:
dependencies: dependencies:
nanoid "^4.0.2" nanoid "^4.0.2"
vue@^v3.2.45: vue@^v3.5.13:
version "3.2.45" version "3.5.13"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.45.tgz#94a116784447eb7dbd892167784619fef379b3c8" resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.13.tgz#9f760a1a982b09c0c04a867903fc339c9f29ec0a"
integrity sha512-9Nx/Mg2b2xWlXykmCwiTUCWHbWIj53bnkizBxKai1g61f2Xit700A1ljowpTIM11e3uipOeiPcSqnmBg6gyiaA== integrity sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==
dependencies: dependencies:
"@vue/compiler-dom" "3.2.45" "@vue/compiler-dom" "3.5.13"
"@vue/compiler-sfc" "3.2.45" "@vue/compiler-sfc" "3.5.13"
"@vue/runtime-dom" "3.2.45" "@vue/runtime-dom" "3.5.13"
"@vue/server-renderer" "3.2.45" "@vue/server-renderer" "3.5.13"
"@vue/shared" "3.2.45" "@vue/shared" "3.5.13"
webidl-conversions@^4.0.2: webidl-conversions@^4.0.2:
version "4.0.2" version "4.0.2"