mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat: revamp the router and routing logic (#1519)
This commit is contained in:
parent
279f23d4e1
commit
d038b001d4
81 changed files with 776 additions and 632 deletions
|
@ -19,7 +19,6 @@
|
|||
"@typescript-eslint"
|
||||
],
|
||||
"globals": {
|
||||
"KOEL_ENV": "readonly",
|
||||
"FileReader": "readonly",
|
||||
"defineProps": "readonly",
|
||||
"defineEmits": "readonly",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<MessageToaster ref="toaster"/>
|
||||
<GlobalEventListeners/>
|
||||
|
||||
<div id="main" v-if="authenticated" @dragover="onDragOver" @drop="onDrop" @dragend="onDragEnd">
|
||||
<div v-if="authenticated" id="main" @dragend="onDragEnd" @dragover="onDragOver" @drop="onDrop">
|
||||
<Hotkeys/>
|
||||
<AppHeader/>
|
||||
<MainWrapper/>
|
||||
|
@ -19,17 +19,17 @@
|
|||
<DropZone v-show="showDropZone"/>
|
||||
</div>
|
||||
|
||||
<div class="login-wrapper" v-else>
|
||||
<div v-else class="login-wrapper">
|
||||
<LoginForm @loggedin="onUserLoggedIn"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, nextTick, onMounted, provide, ref } from 'vue'
|
||||
import { eventBus, hideOverlay, showOverlay } from '@/utils'
|
||||
import { eventBus, hideOverlay, requireInjection, showOverlay } from '@/utils'
|
||||
import { commonStore, preferenceStore as preferences } from '@/stores'
|
||||
import { authService, playbackService, socketListener, socketService, uploadService } from '@/services'
|
||||
import { ActiveScreenKey, DialogBoxKey, MessageToasterKey } from '@/symbols'
|
||||
import { DialogBoxKey, MessageToasterKey, RouterKey } from '@/symbols'
|
||||
|
||||
import DialogBox from '@/components/ui/DialogBox.vue'
|
||||
import MessageToaster from '@/components/ui/MessageToaster.vue'
|
||||
|
@ -59,7 +59,6 @@ const dialog = ref<InstanceType<typeof DialogBox>>()
|
|||
const toaster = ref<InstanceType<typeof MessageToaster>>()
|
||||
const authenticated = ref(false)
|
||||
const showDropZone = ref(false)
|
||||
const activeScreen = ref<ScreenName>()
|
||||
|
||||
/**
|
||||
* Request for notification permission if it's not provided and the user is OK with notifications.
|
||||
|
@ -116,18 +115,15 @@ const init = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const onDragOver = (e: DragEvent) => {
|
||||
showDropZone.value = Boolean(e.dataTransfer?.types.includes('Files')) && activeScreen.value !== 'Upload'
|
||||
showDropZone.value = Boolean(e.dataTransfer?.types.includes('Files')) && router.$currentRoute.value.screen !== 'Upload'
|
||||
}
|
||||
|
||||
const onDragEnd = () => (showDropZone.value = false)
|
||||
const onDrop = () => (showDropZone.value = false)
|
||||
|
||||
onMounted(() => {
|
||||
eventBus.on('ACTIVATE_SCREEN', (screen: ScreenName) => (activeScreen.value = screen))
|
||||
})
|
||||
|
||||
provide(ActiveScreenKey, activeScreen)
|
||||
provide(DialogBoxKey, dialog)
|
||||
provide(MessageToasterKey, toaster)
|
||||
</script>
|
||||
|
|
|
@ -6,8 +6,10 @@ import { clickaway, focus } from '@/directives'
|
|||
import { defineComponent, nextTick } from 'vue'
|
||||
import { commonStore, userStore } from '@/stores'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { DialogBoxKey, MessageToasterKey } from '@/symbols'
|
||||
import { DialogBoxKey, MessageToasterKey, RouterKey } from '@/symbols'
|
||||
import { DialogBoxStub, MessageToasterStub } from '@/__tests__/stubs'
|
||||
import { routes } from '@/config'
|
||||
import Router from '@/router'
|
||||
|
||||
// A deep-merge function that
|
||||
// - supports symbols as keys (_.merge doesn't)
|
||||
|
@ -24,8 +26,10 @@ const deepMerge = (first: object, second: object) => {
|
|||
|
||||
export default abstract class UnitTestCase {
|
||||
private backupMethods = new Map()
|
||||
protected router: Router
|
||||
|
||||
public constructor () {
|
||||
this.router = new Router(routes)
|
||||
this.beforeEach()
|
||||
this.afterEach()
|
||||
this.test()
|
||||
|
@ -107,6 +111,12 @@ export default abstract class UnitTestCase {
|
|||
options.global.provide[MessageToasterKey] = MessageToasterStub
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (!options.global.provide.hasOwnProperty(RouterKey)) {
|
||||
// @ts-ignore
|
||||
options.global.provide[RouterKey] = this.router
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Ref, ref } from 'vue'
|
||||
import { noop } from '@/utils'
|
||||
|
||||
import MessageToaster from '@/components/ui/MessageToaster.vue'
|
||||
import DialogBox from '@/components/ui/DialogBox.vue'
|
||||
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import 'plyr/dist/plyr.js'
|
||||
import { createApp } from 'vue'
|
||||
import { clickaway, focus } from '@/directives'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { RouterKey } from '@/symbols'
|
||||
import { routes } from '@/config'
|
||||
import Router from '@/router'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App)
|
||||
.provide(RouterKey, new Router(routes))
|
||||
.component('icon', FontAwesomeIcon)
|
||||
.directive('koel-focus', focus)
|
||||
.directive('koel-clickaway', clickaway)
|
||||
|
|
|
@ -56,13 +56,16 @@
|
|||
<script lang="ts" setup>
|
||||
import { faDownload, faRandom } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed, toRef, toRefs } from 'vue'
|
||||
import { eventBus, pluralize, secondsToHis } from '@/utils'
|
||||
import { eventBus, pluralize, requireInjection, secondsToHis } from '@/utils'
|
||||
import { albumStore, artistStore, commonStore, songStore } from '@/stores'
|
||||
import { downloadService, playbackService } from '@/services'
|
||||
import { useDraggable } from '@/composables'
|
||||
import { RouterKey } from '@/symbols'
|
||||
|
||||
import AlbumThumbnail from '@/components/ui/AlbumArtistThumbnail.vue'
|
||||
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const { startDragging } = useDraggable('album')
|
||||
|
||||
const props = withDefaults(defineProps<{ album: Album, layout?: ArtistAlbumCardLayout }>(), { layout: 'full' })
|
||||
|
@ -76,6 +79,7 @@ const showing = computed(() => !albumStore.isUnknown(album.value))
|
|||
|
||||
const shuffle = async () => {
|
||||
await playbackService.queueAndPlay(await songStore.fetchForAlbum(album.value), true /* shuffled */)
|
||||
router.go('queue')
|
||||
}
|
||||
|
||||
const download = () => downloadService.fromAlbum(album.value)
|
||||
|
|
|
@ -4,7 +4,6 @@ import factory from '@/__tests__/factory'
|
|||
import { eventBus } from '@/utils'
|
||||
import { downloadService, playbackService } from '@/services'
|
||||
import { commonStore, songStore } from '@/stores'
|
||||
import router from '@/router'
|
||||
import AlbumContextMenu from './AlbumContextMenu.vue'
|
||||
|
||||
let album: Album
|
||||
|
@ -58,12 +57,12 @@ new class extends UnitTestCase {
|
|||
})
|
||||
|
||||
it('downloads', async () => {
|
||||
const mock = this.mock(downloadService, 'fromAlbum')
|
||||
const downloadMock = this.mock(downloadService, 'fromAlbum')
|
||||
|
||||
const { getByText } = await this.renderComponent()
|
||||
await getByText('Download').click()
|
||||
|
||||
expect(mock).toHaveBeenCalledWith(album)
|
||||
expect(downloadMock).toHaveBeenCalledWith(album)
|
||||
})
|
||||
|
||||
it('does not have an option to download if downloading is disabled', async () => {
|
||||
|
@ -74,7 +73,7 @@ new class extends UnitTestCase {
|
|||
})
|
||||
|
||||
it('goes to album', async () => {
|
||||
const mock = this.mock(router, 'go')
|
||||
const mock = this.mock(this.router, 'go')
|
||||
const { getByText } = await this.renderComponent()
|
||||
|
||||
await getByText('Go to Album').click()
|
||||
|
@ -91,7 +90,7 @@ new class extends UnitTestCase {
|
|||
})
|
||||
|
||||
it('goes to artist', async () => {
|
||||
const mock = this.mock(router, 'go')
|
||||
const mock = this.mock(this.router, 'go')
|
||||
const { getByText } = await this.renderComponent()
|
||||
|
||||
await getByText('Go to Artist').click()
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
<li data-testid="play" @click="play">Play All</li>
|
||||
<li data-testid="shuffle" @click="shuffle">Shuffle All</li>
|
||||
<li class="separator"></li>
|
||||
<li data-testid="view-album" @click="viewAlbumDetails" v-if="isStandardAlbum">Go to Album</li>
|
||||
<li data-testid="view-artist" @click="viewArtistDetails" v-if="isStandardArtist">Go to Artist</li>
|
||||
<li v-if="isStandardAlbum" data-testid="view-album" @click="viewAlbumDetails">Go to Album</li>
|
||||
<li v-if="isStandardArtist" data-testid="view-artist" @click="viewArtistDetails">Go to Artist</li>
|
||||
<template v-if="isStandardAlbum && allowDownload">
|
||||
<li class="separator"></li>
|
||||
<li data-testid="download" @click="download">Download</li>
|
||||
|
@ -19,29 +19,34 @@ import { computed, ref, toRef } from 'vue'
|
|||
import { albumStore, artistStore, commonStore, songStore } from '@/stores'
|
||||
import { downloadService, playbackService } from '@/services'
|
||||
import { useContextMenu } from '@/composables'
|
||||
import router from '@/router'
|
||||
import { eventBus } from '@/utils'
|
||||
import { eventBus, requireInjection } from '@/utils'
|
||||
import { RouterKey } from '@/symbols'
|
||||
|
||||
const { context, base, ContextMenuBase, open, trigger } = useContextMenu()
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const album = ref<Album>()
|
||||
const allowDownload = toRef(commonStore.state, 'allow_download')
|
||||
|
||||
const isStandardAlbum = computed(() => !albumStore.isUnknown(album.value))
|
||||
const isStandardAlbum = computed(() => !albumStore.isUnknown(album.value!))
|
||||
|
||||
const isStandardArtist = computed(() => {
|
||||
return !artistStore.isUnknown(album.value.artist_id) && !artistStore.isVarious(album.value.artist_id)
|
||||
return !artistStore.isUnknown(album.value!.artist_id) && !artistStore.isVarious(album.value!.artist_id)
|
||||
})
|
||||
|
||||
const play = () => trigger(async () => playbackService.queueAndPlay(await songStore.fetchForAlbum(album.value)))
|
||||
const play = () => trigger(async () => {
|
||||
await playbackService.queueAndPlay(await songStore.fetchForAlbum(album.value!))
|
||||
router.go('queue')
|
||||
})
|
||||
|
||||
const shuffle = () => {
|
||||
trigger(async () => playbackService.queueAndPlay(await songStore.fetchForAlbum(album.value), true))
|
||||
}
|
||||
const shuffle = () => trigger(async () => {
|
||||
await playbackService.queueAndPlay(await songStore.fetchForAlbum(album.value!), true)
|
||||
router.go('queue')
|
||||
})
|
||||
|
||||
const viewAlbumDetails = () => trigger(() => router.go(`album/${album.value.id}`))
|
||||
const viewArtistDetails = () => trigger(() => router.go(`artist/${album.value.artist_id}`))
|
||||
const download = () => trigger(() => downloadService.fromAlbum(album.value))
|
||||
const viewAlbumDetails = () => trigger(() => router.go(`album/${album.value!.id}`))
|
||||
const viewArtistDetails = () => trigger(() => router.go(`artist/${album.value!.artist_id}`))
|
||||
const download = () => trigger(() => downloadService.fromAlbum(album.value!))
|
||||
|
||||
eventBus.on('ALBUM_CONTEXT_MENU_REQUESTED', async (e: MouseEvent, _album: Album) => {
|
||||
album.value = _album
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { expect, it } from 'vitest'
|
||||
import factory from '@/__tests__/factory'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { mediaInfoService } from '@/services/mediaInfoService'
|
||||
import { commonStore, songStore } from '@/stores'
|
||||
import { fireEvent } from '@testing-library/vue'
|
||||
import { playbackService } from '@/services'
|
||||
import { playbackService, mediaInfoService } from '@/services'
|
||||
import AlbumInfoComponent from './AlbumInfo.vue'
|
||||
|
||||
let album: Album
|
||||
|
|
|
@ -36,10 +36,13 @@ import { faCirclePlay } from '@fortawesome/free-solid-svg-icons'
|
|||
import { computed, defineAsyncComponent, ref, toRefs, watch } from 'vue'
|
||||
import { useThirdPartyServices } from '@/composables'
|
||||
import { songStore } from '@/stores'
|
||||
import { playbackService } from '@/services'
|
||||
import { mediaInfoService } from '@/services/mediaInfoService'
|
||||
import { playbackService, mediaInfoService } from '@/services'
|
||||
import { RouterKey } from '@/symbols'
|
||||
|
||||
import AlbumThumbnail from '@/components/ui/AlbumArtistThumbnail.vue'
|
||||
import { requireInjection } from '@/utils'
|
||||
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const TrackList = defineAsyncComponent(() => import('@/components/album/AlbumTrackList.vue'))
|
||||
|
||||
|
@ -60,7 +63,10 @@ watch(album, async () => {
|
|||
const showSummary = computed(() => mode.value !== 'full' && !showingFullWiki.value)
|
||||
const showFull = computed(() => !showSummary.value)
|
||||
|
||||
const play = async () => playbackService.queueAndPlay(await songStore.fetchForAlbum(album.value))
|
||||
const play = async () => {
|
||||
await playbackService.queueAndPlay(await songStore.fetchForAlbum(album.value))
|
||||
router.go('queue')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
<footer>
|
||||
<div class="info">
|
||||
<a class="name" :href="`#!/artist/${artist.id}`" data-testid="name">{{ artist.name }}</a>
|
||||
<a :href="`#!/artist/${artist.id}`" class="name" data-testid="name">{{ artist.name }}</a>
|
||||
</div>
|
||||
<p class="meta">
|
||||
<span class="left">
|
||||
|
@ -29,9 +29,9 @@
|
|||
<a
|
||||
:title="`Shuffle all songs by ${artist.name}`"
|
||||
class="shuffle-artist"
|
||||
data-testid="shuffle-artist"
|
||||
href
|
||||
role="button"
|
||||
data-testid="shuffle-artist"
|
||||
@click.prevent="shuffle"
|
||||
>
|
||||
<icon :icon="faRandom"/>
|
||||
|
@ -40,9 +40,9 @@
|
|||
v-if="allowDownload"
|
||||
:title="`Download all songs by ${artist.name}`"
|
||||
class="download-artist"
|
||||
data-testid="download-artist"
|
||||
href
|
||||
role="button"
|
||||
data-testid="download-artist"
|
||||
@click.prevent="download"
|
||||
>
|
||||
<icon :icon="faDownload"/>
|
||||
|
@ -56,13 +56,16 @@
|
|||
<script lang="ts" setup>
|
||||
import { faDownload, faRandom } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed, toRef, toRefs } from 'vue'
|
||||
import { eventBus, pluralize } from '@/utils'
|
||||
import { eventBus, pluralize, requireInjection } from '@/utils'
|
||||
import { artistStore, commonStore, songStore } from '@/stores'
|
||||
import { downloadService, playbackService } from '@/services'
|
||||
import { useDraggable } from '@/composables'
|
||||
import { RouterKey } from '@/symbols'
|
||||
|
||||
import ArtistThumbnail from '@/components/ui/AlbumArtistThumbnail.vue'
|
||||
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const { startDragging } = useDraggable('artist')
|
||||
|
||||
const props = withDefaults(defineProps<{ artist: Artist, layout?: ArtistAlbumCardLayout }>(), { layout: 'full' })
|
||||
|
@ -74,6 +77,7 @@ const showing = computed(() => artistStore.isStandard(artist.value))
|
|||
|
||||
const shuffle = async () => {
|
||||
await playbackService.queueAndPlay(await songStore.fetchForArtist(artist.value), true /* shuffled */)
|
||||
router.go('queue')
|
||||
}
|
||||
|
||||
const download = () => downloadService.fromArtist(artist.value)
|
||||
|
|
|
@ -4,7 +4,6 @@ import factory from '@/__tests__/factory'
|
|||
import { eventBus } from '@/utils'
|
||||
import { downloadService, playbackService } from '@/services'
|
||||
import { commonStore, songStore } from '@/stores'
|
||||
import router from '@/router'
|
||||
import ArtistContextMenu from './ArtistContextMenu.vue'
|
||||
|
||||
let artist: Artist
|
||||
|
@ -74,7 +73,7 @@ new class extends UnitTestCase {
|
|||
})
|
||||
|
||||
it('goes to artist', async () => {
|
||||
const mock = this.mock(router, 'go')
|
||||
const mock = this.mock(this.router, 'go')
|
||||
const { getByText } = await this.renderComponent()
|
||||
|
||||
await getByText('Go to Artist').click()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<ContextMenuBase extra-class="artist-menu" ref="base" data-testid="artist-context-menu">
|
||||
<ContextMenuBase ref="base" data-testid="artist-context-menu" extra-class="artist-menu">
|
||||
<template v-if="artist">
|
||||
<li data-testid="play" @click="play">Play All</li>
|
||||
<li data-testid="shuffle" @click="shuffle">Shuffle All</li>
|
||||
|
@ -20,27 +20,32 @@ import { computed, ref, toRef } from 'vue'
|
|||
import { artistStore, commonStore, songStore } from '@/stores'
|
||||
import { downloadService, playbackService } from '@/services'
|
||||
import { useContextMenu } from '@/composables'
|
||||
import router from '@/router'
|
||||
import { eventBus } from '@/utils'
|
||||
import { RouterKey } from '@/symbols'
|
||||
import { eventBus, requireInjection } from '@/utils'
|
||||
|
||||
const { context, base, ContextMenuBase, open, trigger } = useContextMenu()
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const artist = ref<Artist>()
|
||||
const allowDownload = toRef(commonStore.state, 'allow_download')
|
||||
|
||||
const isStandardArtist = computed(() =>
|
||||
!artistStore.isUnknown(artist.value)
|
||||
&& !artistStore.isVarious(artist.value)
|
||||
!artistStore.isUnknown(artist.value!)
|
||||
&& !artistStore.isVarious(artist.value!)
|
||||
)
|
||||
|
||||
const play = () => trigger(async () => playbackService.queueAndPlay(await songStore.fetchForArtist(artist.value)))
|
||||
const play = () => trigger(async () => {
|
||||
await playbackService.queueAndPlay(await songStore.fetchForArtist(artist.value!))
|
||||
router.go('queue')
|
||||
})
|
||||
|
||||
const shuffle = () => {
|
||||
trigger(async () => playbackService.queueAndPlay(await songStore.fetchForArtist(artist.value), true))
|
||||
}
|
||||
const shuffle = () => trigger(async () => {
|
||||
await playbackService.queueAndPlay(await songStore.fetchForArtist(artist.value!), true)
|
||||
router.go('queue')
|
||||
})
|
||||
|
||||
const viewArtistDetails = () => trigger(() => router.go(`artist/${artist.value.id}`))
|
||||
const download = () => trigger(() => downloadService.fromArtist(artist.value))
|
||||
const viewArtistDetails = () => trigger(() => router.go(`artist/${artist.value!.id}`))
|
||||
const download = () => trigger(() => downloadService.fromArtist(artist.value!))
|
||||
|
||||
eventBus.on('ARTIST_CONTEXT_MENU_REQUESTED', async (e: MouseEvent, _artist: Artist) => {
|
||||
artist.value = _artist
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { expect, it } from 'vitest'
|
||||
import factory from '@/__tests__/factory'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { mediaInfoService } from '@/services/mediaInfoService'
|
||||
import { commonStore, songStore } from '@/stores'
|
||||
import { fireEvent } from '@testing-library/vue'
|
||||
import { playbackService } from '@/services'
|
||||
import { mediaInfoService, playbackService } from '@/services'
|
||||
import ArtistInfoComponent from './ArtistInfo.vue'
|
||||
|
||||
let artist: Artist
|
||||
|
|
|
@ -32,13 +32,16 @@
|
|||
<script lang="ts" setup>
|
||||
import { faCirclePlay } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed, ref, toRefs, watch } from 'vue'
|
||||
import { playbackService } from '@/services'
|
||||
import { playbackService, mediaInfoService } from '@/services'
|
||||
import { useThirdPartyServices } from '@/composables'
|
||||
import { songStore } from '@/stores'
|
||||
import { mediaInfoService } from '@/services/mediaInfoService'
|
||||
import { RouterKey } from '@/symbols'
|
||||
import { requireInjection } from '@/utils'
|
||||
|
||||
import ArtistThumbnail from '@/components/ui/AlbumArtistThumbnail.vue'
|
||||
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const props = withDefaults(defineProps<{ artist: Artist, mode?: MediaInfoDisplayMode }>(), { mode: 'aside' })
|
||||
const { artist, mode } = toRefs(props)
|
||||
|
||||
|
@ -56,7 +59,10 @@ watch(artist, async () => {
|
|||
const showSummary = computed(() => mode.value !== 'full' && !showingFullBio.value)
|
||||
const showFull = computed(() => !showSummary.value)
|
||||
|
||||
const play = async () => playbackService.queueAndPlay(await songStore.fetchForArtist(artist.value))
|
||||
const play = async () => {
|
||||
await playbackService.queueAndPlay(await songStore.fetchForArtist(artist.value))
|
||||
router.go('queue')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -53,8 +53,9 @@
|
|||
import isMobile from 'ismobilejs'
|
||||
import { faBolt, faListOl, faSliders } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ref, toRef, toRefs } from 'vue'
|
||||
import { eventBus, isAudioContextSupported as useEqualizer } from '@/utils'
|
||||
import { eventBus, isAudioContextSupported as useEqualizer, requireInjection } from '@/utils'
|
||||
import { preferenceStore } from '@/stores'
|
||||
import { RouterKey } from '@/symbols'
|
||||
|
||||
import Equalizer from '@/components/ui/Equalizer.vue'
|
||||
import Volume from '@/components/ui/Volume.vue'
|
||||
|
@ -73,7 +74,9 @@ const toggleEqualizer = () => (showEqualizer.value = !showEqualizer.value)
|
|||
const closeEqualizer = () => (showEqualizer.value = false)
|
||||
const toggleVisualizer = () => isMobile.any || eventBus.emit('TOGGLE_VISUALIZER')
|
||||
|
||||
eventBus.on('ACTIVATE_SCREEN', (screen: ScreenName) => (viewingQueue.value = screen === 'Queue'))
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
router.onRouteChanged(route => (viewingQueue.value = route.screen === 'Queue'))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -17,17 +17,13 @@ import ExtraControls from '@/components/layout/app-footer/FooterExtraControls.vu
|
|||
import MiddlePane from '@/components/layout/app-footer/FooterMiddlePane.vue'
|
||||
import PlayerControls from '@/components/layout/app-footer/FooterPlayerControls.vue'
|
||||
|
||||
const song = ref<Song | null>(null)
|
||||
const viewingQueue = ref(false)
|
||||
const song = ref<Song>()
|
||||
|
||||
const requestContextMenu = (event: MouseEvent) => {
|
||||
song.value?.id && eventBus.emit('SONG_CONTEXT_MENU_REQUESTED', event, song.value)
|
||||
}
|
||||
|
||||
eventBus.on({
|
||||
SONG_STARTED: (newSong: Song) => (song.value = newSong),
|
||||
ACTIVATE_SCREEN: (screen: ScreenName) => (viewingQueue.value = screen === 'Queue')
|
||||
})
|
||||
eventBus.on('SONG_STARTED', (newSong: Song) => (song.value = newSong))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -19,9 +19,9 @@
|
|||
<UploadScreen v-show="screen === 'Upload'"/>
|
||||
<SearchExcerptsScreen v-show="screen === 'Search.Excerpt'"/>
|
||||
|
||||
<SearchSongResultsScreen v-if="screen === 'Search.Songs'" :q="screenProps"/>
|
||||
<AlbumScreen v-if="screen === 'Album'" :album="screenProps"/>
|
||||
<ArtistScreen v-if="screen === 'Artist'" :artist="screenProps"/>
|
||||
<SearchSongResultsScreen v-if="screen === 'Search.Songs'"/>
|
||||
<AlbumScreen v-if="screen === 'Album'"/>
|
||||
<ArtistScreen v-if="screen === 'Artist'"/>
|
||||
<SettingsScreen v-if="screen === 'Settings'"/>
|
||||
<ProfileScreen v-if="screen === 'Profile'"/>
|
||||
<UserListScreen v-if="screen === 'Users'"/>
|
||||
|
@ -32,9 +32,10 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onMounted, ref, toRef } from 'vue'
|
||||
import { eventBus } from '@/utils'
|
||||
import { eventBus, requireInjection } from '@/utils'
|
||||
import { preferenceStore } from '@/stores'
|
||||
import { useThirdPartyServices } from '@/composables'
|
||||
import { RouterKey } from '@/symbols'
|
||||
|
||||
import HomeScreen from '@/components/screens/HomeScreen.vue'
|
||||
import QueueScreen from '@/components/screens/QueueScreen.vue'
|
||||
|
@ -46,7 +47,6 @@ import FavoritesScreen from '@/components/screens/FavoritesScreen.vue'
|
|||
import RecentlyPlayedScreen from '@/components/screens/RecentlyPlayedScreen.vue'
|
||||
import UploadScreen from '@/components/screens/UploadScreen.vue'
|
||||
import SearchExcerptsScreen from '@/components/screens/search/SearchExcerptsScreen.vue'
|
||||
import router from '@/router'
|
||||
|
||||
const UserListScreen = defineAsyncComponent(() => import('@/components/screens/UserListScreen.vue'))
|
||||
const AlbumArtOverlay = defineAsyncComponent(() => import('@/components/ui/AlbumArtOverlay.vue'))
|
||||
|
@ -60,24 +60,21 @@ const NotFoundScreen = defineAsyncComponent(() => import('@/components/screens/N
|
|||
const Visualizer = defineAsyncComponent(() => import('@/components/ui/Visualizer.vue'))
|
||||
|
||||
const { useYouTube } = useThirdPartyServices()
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const showAlbumArtOverlay = toRef(preferenceStore.state, 'showAlbumArtOverlay')
|
||||
const showingVisualizer = ref(false)
|
||||
const screenProps = ref<any>(null)
|
||||
const screen = ref<ScreenName>('Home')
|
||||
const currentSong = ref<Song | null>(null)
|
||||
|
||||
eventBus.on({
|
||||
ACTIVATE_SCREEN (screenName: ScreenName, data: any) {
|
||||
screenProps.value = data
|
||||
screen.value = screenName
|
||||
},
|
||||
router.onRouteChanged(route => (screen.value = route.screen))
|
||||
|
||||
eventBus.on({
|
||||
TOGGLE_VISUALIZER: () => (showingVisualizer.value = !showingVisualizer.value),
|
||||
SONG_STARTED: (song: Song) => (currentSong.value = song)
|
||||
})
|
||||
|
||||
onMounted(() => router.resolveRoute())
|
||||
onMounted(() => router.resolve())
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -93,6 +90,7 @@ onMounted(() => router.resolveRoute())
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
|
||||
.main-scroll-wrap {
|
||||
&:not(.song-list-wrap) {
|
||||
|
|
|
@ -94,12 +94,12 @@ import { ref } from 'vue'
|
|||
import { eventBus, requireInjection } from '@/utils'
|
||||
import { queueStore } from '@/stores'
|
||||
import { useAuthorization, useDroppable, useThirdPartyServices } from '@/composables'
|
||||
import { ActiveScreenKey } from '@/symbols'
|
||||
import { RouterKey } from '@/symbols'
|
||||
|
||||
import PlaylistList from '@/components/playlist/PlaylistSidebarList.vue'
|
||||
|
||||
const showing = ref(!isMobile.phone)
|
||||
const activeScreen = requireInjection(ActiveScreenKey, ref('Home'))
|
||||
const activeScreen = ref<ScreenName>()
|
||||
const droppableToQueue = ref(false)
|
||||
|
||||
const { acceptsDrop, resolveDroppedSongs } = useDroppable(['songs', 'album', 'artist', 'playlist'])
|
||||
|
@ -128,12 +128,18 @@ const onQueueDrop = async (event: DragEvent) => {
|
|||
return false
|
||||
}
|
||||
|
||||
eventBus.on({
|
||||
/**
|
||||
* On mobile, hide the sidebar whenever a screen is activated.
|
||||
*/
|
||||
ACTIVATE_SCREEN: () => isMobile.phone && (showing.value = false),
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
router.onRouteChanged(route => {
|
||||
// On mobile, hide the sidebar whenever a screen is activated.
|
||||
if (isMobile.phone) {
|
||||
showing.value = false
|
||||
}
|
||||
|
||||
activeScreen.value = route.screen
|
||||
})
|
||||
|
||||
eventBus.on({
|
||||
/**
|
||||
* Listen to toggle sidebar event to show or hide the sidebar.
|
||||
* This should only be triggered on a mobile device.
|
||||
|
|
|
@ -31,11 +31,12 @@
|
|||
import { ref } from 'vue'
|
||||
import { playlistStore } from '@/stores'
|
||||
import { logger, requireInjection } from '@/utils'
|
||||
import { DialogBoxKey, MessageToasterKey } from '@/symbols'
|
||||
import { DialogBoxKey, MessageToasterKey, RouterKey } from '@/symbols'
|
||||
|
||||
import SoundBars from '@/components/ui/SoundBars.vue'
|
||||
import Btn from '@/components/ui/Btn.vue'
|
||||
|
||||
const router = requireInjection(RouterKey)
|
||||
const toaster = requireInjection(MessageToasterKey)
|
||||
const dialog = requireInjection(DialogBoxKey)
|
||||
|
||||
|
@ -50,9 +51,10 @@ const submit = async () => {
|
|||
loading.value = true
|
||||
|
||||
try {
|
||||
const folder = await playlistStore.store(name.value)
|
||||
const playlist = await playlistStore.store(name.value)
|
||||
close()
|
||||
toaster.value.success(`Playlist "${folder.name}" created.`)
|
||||
toaster.value.success(`Playlist "${playlist.name}" created.`)
|
||||
router.go(`playlist/${playlist.id}`)
|
||||
} catch (error) {
|
||||
dialog.value.error('Something went wrong. Please try again.')
|
||||
logger.error(error)
|
||||
|
|
|
@ -5,7 +5,6 @@ import factory from '@/__tests__/factory'
|
|||
import { fireEvent, waitFor } from '@testing-library/vue'
|
||||
import { playlistStore, songStore } from '@/stores'
|
||||
import { playbackService } from '@/services'
|
||||
import router from '@/router'
|
||||
import PlaylistFolderContextMenu from './PlaylistFolderContextMenu.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
|
@ -42,7 +41,7 @@ new class extends UnitTestCase {
|
|||
const songs = factory<Song>('song', 3)
|
||||
const fetchMock = this.mock(songStore, 'fetchForPlaylistFolder').mockResolvedValue(songs)
|
||||
const queueMock = this.mock(playbackService, 'queueAndPlay')
|
||||
const goMock = this.mock(router, 'go')
|
||||
const goMock = this.mock(this.router, 'go')
|
||||
const { getByText } = await this.renderComponent(folder)
|
||||
|
||||
await fireEvent.click(getByText('Play All'))
|
||||
|
@ -59,7 +58,7 @@ new class extends UnitTestCase {
|
|||
const songs = factory<Song>('song', 3)
|
||||
const fetchMock = this.mock(songStore, 'fetchForPlaylistFolder').mockResolvedValue(songs)
|
||||
const queueMock = this.mock(playbackService, 'queueAndPlay')
|
||||
const goMock = this.mock(router, 'go')
|
||||
const goMock = this.mock(this.router, 'go')
|
||||
const { getByText } = await this.renderComponent(folder)
|
||||
|
||||
await fireEvent.click(getByText('Shuffle All'))
|
||||
|
|
|
@ -18,12 +18,12 @@ import { useContextMenu } from '@/composables'
|
|||
import { eventBus, requireInjection } from '@/utils'
|
||||
import { playlistStore, songStore } from '@/stores'
|
||||
import { playbackService } from '@/services'
|
||||
import { DialogBoxKey } from '@/symbols'
|
||||
import router from '@/router'
|
||||
import { DialogBoxKey, RouterKey } from '@/symbols'
|
||||
|
||||
const { context, base, ContextMenuBase, open, trigger } = useContextMenu()
|
||||
|
||||
const dialog = requireInjection(DialogBoxKey)
|
||||
const router = requireInjection(RouterKey)
|
||||
const folder = ref<PlaylistFolder>()
|
||||
|
||||
const playlistsInFolder = computed(() => folder.value ? playlistStore.byFolder(folder.value) : [])
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<li
|
||||
ref="el"
|
||||
class="playlist"
|
||||
:class="{ droppable }"
|
||||
class="playlist"
|
||||
data-testid="playlist-sidebar-item"
|
||||
draggable="true"
|
||||
@contextmenu="onContextMenu"
|
||||
|
@ -32,13 +32,14 @@ import { faBoltLightning, faClockRotateLeft, faFile, faHeart, faMusic } from '@f
|
|||
import { computed, ref, toRefs } from 'vue'
|
||||
import { eventBus, pluralize, requireInjection } from '@/utils'
|
||||
import { favoriteStore, playlistStore } from '@/stores'
|
||||
import { MessageToasterKey } from '@/symbols'
|
||||
import { MessageToasterKey, RouterKey } from '@/symbols'
|
||||
import { useDraggable, useDroppable } from '@/composables'
|
||||
|
||||
const { startDragging } = useDraggable('playlist')
|
||||
const { acceptsDrop, resolveDroppedSongs } = useDroppable(['songs', 'album', 'artist'])
|
||||
|
||||
const toaster = requireInjection(MessageToasterKey)
|
||||
const router = requireInjection(RouterKey)
|
||||
const droppable = ref(false)
|
||||
|
||||
const props = defineProps<{ list: PlaylistLike }>()
|
||||
|
@ -107,8 +108,8 @@ const onDrop = async (event: DragEvent) => {
|
|||
return false
|
||||
}
|
||||
|
||||
eventBus.on('ACTIVATE_SCREEN', (screen: ScreenName, _list: PlaylistLike): void => {
|
||||
switch (screen) {
|
||||
router.onRouteChanged(route => {
|
||||
switch (route.screen) {
|
||||
case 'Favorites':
|
||||
active.value = isFavoriteList(list.value)
|
||||
break
|
||||
|
@ -118,7 +119,7 @@ eventBus.on('ACTIVATE_SCREEN', (screen: ScreenName, _list: PlaylistLike): void =
|
|||
break
|
||||
|
||||
case 'Playlist':
|
||||
active.value = list.value === _list
|
||||
active.value = (list.value as Playlist).id === parseInt(route.params!.id)
|
||||
break
|
||||
|
||||
default:
|
||||
|
|
|
@ -38,11 +38,10 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { playlistStore } from '@/stores'
|
||||
import { requireInjection } from '@/utils'
|
||||
import { DialogBoxKey, MessageToasterKey } from '@/symbols'
|
||||
import router from '@/router'
|
||||
import { logger, requireInjection } from '@/utils'
|
||||
import { DialogBoxKey, MessageToasterKey, RouterKey } from '@/symbols'
|
||||
import { useSmartPlaylistForm } from '@/components/playlist/smart-playlist/useSmartPlaylistForm'
|
||||
|
||||
const {
|
||||
|
@ -58,6 +57,7 @@ const {
|
|||
|
||||
const toaster = requireInjection(MessageToasterKey)
|
||||
const dialog = requireInjection(DialogBoxKey)
|
||||
const router = requireInjection(RouterKey)
|
||||
const name = ref('')
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
@ -74,13 +74,17 @@ const maybeClose = async () => {
|
|||
|
||||
const submit = async () => {
|
||||
loading.value = true
|
||||
const playlist = await playlistStore.store(name.value, [], collectedRuleGroups.value)
|
||||
loading.value = false
|
||||
close()
|
||||
|
||||
toaster.value.success(`Playlist "${playlist.name}" created.`)
|
||||
|
||||
await nextTick()
|
||||
router.go(`playlist/${playlist.id}`)
|
||||
try {
|
||||
const playlist = await playlistStore.store(name.value, [], collectedRuleGroups.value)
|
||||
close()
|
||||
toaster.value.success(`Playlist "${playlist.name}" created.`)
|
||||
router.go(`playlist/${playlist.id}`)
|
||||
} catch (error) {
|
||||
dialog.value.error('Something went wrong. Please try again.')
|
||||
logger.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import { ref } from 'vue'
|
||||
import { expect, it } from 'vitest'
|
||||
import factory from '@/__tests__/factory'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { albumStore, preferenceStore } from '@/stores'
|
||||
import { fireEvent, waitFor } from '@testing-library/vue'
|
||||
import { ActiveScreenKey } from '@/symbols'
|
||||
import AlbumListScreen from './AlbumListScreen.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
|
@ -12,32 +10,29 @@ new class extends UnitTestCase {
|
|||
super.beforeEach(() => this.mock(albumStore, 'paginate'))
|
||||
}
|
||||
|
||||
private renderComponent () {
|
||||
private async renderComponent () {
|
||||
albumStore.state.albums = factory<Album>('album', 9)
|
||||
return this.render(AlbumListScreen, {
|
||||
global: {
|
||||
provide: {
|
||||
[<symbol>ActiveScreenKey]: ref('Albums')
|
||||
}
|
||||
}
|
||||
})
|
||||
const rendered = this.render(AlbumListScreen)
|
||||
await this.router.activateRoute({ path: 'albums', screen: 'Albums' })
|
||||
return rendered
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders', () => {
|
||||
expect(this.renderComponent().getAllByTestId('album-card')).toHaveLength(9)
|
||||
it('renders', async () => {
|
||||
const { getAllByTestId } = await this.renderComponent()
|
||||
expect(getAllByTestId('album-card')).toHaveLength(9)
|
||||
})
|
||||
|
||||
it.each<[ArtistAlbumViewMode]>([['list'], ['thumbnails']])('sets layout from preferences', async (mode) => {
|
||||
preferenceStore.albumsViewMode = mode
|
||||
|
||||
const { getByTestId } = this.renderComponent()
|
||||
const { getByTestId } = await this.renderComponent()
|
||||
|
||||
await waitFor(() => expect(getByTestId('album-list').classList.contains(`as-${mode}`)).toBe(true))
|
||||
})
|
||||
|
||||
it('switches layout', async () => {
|
||||
const { getByTestId, getByTitle } = this.renderComponent()
|
||||
const { getByTestId, getByTitle } = await this.renderComponent()
|
||||
|
||||
await fireEvent.click(getByTitle('View as list'))
|
||||
await waitFor(() => expect(getByTestId('album-list').classList.contains(`as-list`)).toBe(true))
|
||||
|
|
|
@ -4,7 +4,6 @@ import factory from '@/__tests__/factory'
|
|||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { albumStore, commonStore, songStore } from '@/stores'
|
||||
import { downloadService } from '@/services'
|
||||
import router from '@/router'
|
||||
import { eventBus } from '@/utils'
|
||||
import CloseModalBtn from '@/components/ui/BtnCloseModal.vue'
|
||||
import AlbumScreen from './AlbumScreen.vue'
|
||||
|
@ -29,10 +28,12 @@ new class extends UnitTestCase {
|
|||
const songs = factory<Song>('song', 13)
|
||||
const fetchSongsMock = this.mock(songStore, 'fetchForAlbum').mockResolvedValue(songs)
|
||||
|
||||
await this.router.activateRoute({
|
||||
path: 'albums/42',
|
||||
screen: 'Album'
|
||||
}, { id: '42' })
|
||||
|
||||
const rendered = this.render(AlbumScreen, {
|
||||
props: {
|
||||
album: 42
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
CloseModalBtn,
|
||||
|
@ -74,7 +75,7 @@ new class extends UnitTestCase {
|
|||
})
|
||||
|
||||
it('goes back to list if album is deleted', async () => {
|
||||
const goMock = this.mock(router, 'go')
|
||||
const goMock = this.mock(this.router, 'go')
|
||||
const byIdMock = this.mock(albumStore, 'byId', null)
|
||||
await this.renderComponent()
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
<template v-slot:meta>
|
||||
<a v-if="isNormalArtist" :href="`#!/artist/${album.artist_id}`" class="artist">{{ album.artist_name }}</a>
|
||||
<span class="nope" v-else>{{ album.artist_name }}</span>
|
||||
<span v-else class="nope">{{ album.artist_name }}</span>
|
||||
<span>{{ pluralize(songs, 'song') }}</span>
|
||||
<span>{{ duration }}</span>
|
||||
<a v-if="useLastfm" class="info" href title="View album information" @click.prevent="showInfo">Info</a>
|
||||
|
@ -50,13 +50,12 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, onMounted, ref, toRef, toRefs } from 'vue'
|
||||
import { computed, defineAsyncComponent, onMounted, ref, toRef } from 'vue'
|
||||
import { eventBus, logger, pluralize, requireInjection } from '@/utils'
|
||||
import { albumStore, artistStore, commonStore, songStore } from '@/stores'
|
||||
import { downloadService } from '@/services'
|
||||
import { useSongList } from '@/composables'
|
||||
import router from '@/router'
|
||||
import { DialogBoxKey } from '@/symbols'
|
||||
import { DialogBoxKey, RouterKey } from '@/symbols'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import AlbumThumbnail from '@/components/ui/AlbumArtistThumbnail.vue'
|
||||
|
@ -67,9 +66,7 @@ const AlbumInfo = defineAsyncComponent(() => import('@/components/album/AlbumInf
|
|||
const CloseModalBtn = defineAsyncComponent(() => import('@/components/ui/BtnCloseModal.vue'))
|
||||
|
||||
const dialog = requireInjection(DialogBoxKey)
|
||||
|
||||
const props = defineProps<{ album: number }>()
|
||||
const { album: id } = toRefs(props)
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const album = ref<Album>()
|
||||
const songs = ref<Song[]>([])
|
||||
|
@ -100,16 +97,17 @@ const isNormalArtist = computed(() => {
|
|||
return !artistStore.isVarious(album.value.artist_id) && !artistStore.isUnknown(album.value.artist_id)
|
||||
})
|
||||
|
||||
const download = () => downloadService.fromAlbum(album.value)
|
||||
const download = () => downloadService.fromAlbum(album.value!)
|
||||
const showInfo = () => (showingInfo.value = true)
|
||||
|
||||
onMounted(async () => {
|
||||
const id = parseInt(router.$currentRoute.value?.params!.id)
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
[album.value, songs.value] = await Promise.all([
|
||||
albumStore.resolve(id.value),
|
||||
songStore.fetchForAlbum(id.value)
|
||||
albumStore.resolve(id),
|
||||
songStore.fetchForAlbum(id)
|
||||
])
|
||||
|
||||
sort('track')
|
||||
|
@ -121,10 +119,8 @@ onMounted(async () => {
|
|||
}
|
||||
})
|
||||
|
||||
eventBus.on('SONGS_UPDATED', () => {
|
||||
// if the current album has been deleted, go back to the list
|
||||
albumStore.byId(id.value) || router.go('albums')
|
||||
})
|
||||
// if the current album has been deleted, go back to the list
|
||||
eventBus.on('SONGS_UPDATED', () => albumStore.byId(album.value!.id) || router.go('albums'))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
import { ref } from 'vue'
|
||||
import { expect, it } from 'vitest'
|
||||
import factory from '@/__tests__/factory'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { commonStore, queueStore, songStore } from '@/stores'
|
||||
import { fireEvent, waitFor } from '@testing-library/vue'
|
||||
import { playbackService } from '@/services'
|
||||
import router from '@/router'
|
||||
import { ActiveScreenKey } from '@/symbols'
|
||||
import AllSongsScreen from './AllSongsScreen.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
|
@ -16,13 +13,15 @@ new class extends UnitTestCase {
|
|||
songStore.state.songs = factory<Song>('song', 20)
|
||||
const fetchMock = this.mock(songStore, 'paginate').mockResolvedValue(2)
|
||||
|
||||
this.router.$currentRoute.value = {
|
||||
screen: 'Songs',
|
||||
path: '/songs'
|
||||
}
|
||||
|
||||
const rendered = this.render(AllSongsScreen, {
|
||||
global: {
|
||||
stubs: {
|
||||
SongList: this.stub('song-list')
|
||||
},
|
||||
provide: {
|
||||
[<symbol>ActiveScreenKey]: ref('Songs')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -40,7 +39,7 @@ new class extends UnitTestCase {
|
|||
it('shuffles', async () => {
|
||||
const queueMock = this.mock(queueStore, 'fetchRandom')
|
||||
const playMock = this.mock(playbackService, 'playFirstInQueue')
|
||||
const goMock = this.mock(router, 'go')
|
||||
const goMock = this.mock(this.router, 'go')
|
||||
const { getByTitle } = await this.renderComponent()
|
||||
|
||||
await fireEvent.click(getByTitle('Shuffle all songs'))
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<ThumbnailStack :thumbnails="thumbnails"/>
|
||||
</template>
|
||||
|
||||
<template v-slot:meta v-if="totalSongCount">
|
||||
<template v-if="totalSongCount" v-slot:meta>
|
||||
<span>{{ pluralize(totalSongCount, 'song') }}</span>
|
||||
<span>{{ totalDuration }}</span>
|
||||
</template>
|
||||
|
@ -36,11 +36,11 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, toRef } from 'vue'
|
||||
import { pluralize, secondsToHis } from '@/utils'
|
||||
import { pluralize, requireInjection, secondsToHis } from '@/utils'
|
||||
import { commonStore, queueStore, songStore } from '@/stores'
|
||||
import { playbackService } from '@/services'
|
||||
import { useScreen, useSongList } from '@/composables'
|
||||
import router from '@/router'
|
||||
import { RouterKey } from '@/symbols'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import SongListSkeleton from '@/components/ui/skeletons/SongListSkeleton.vue'
|
||||
|
@ -65,6 +65,8 @@ const {
|
|||
onScrollBreakpoint
|
||||
} = useSongList(toRef(songStore.state, 'songs'), 'all-songs')
|
||||
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
let initialized = false
|
||||
const loading = ref(false)
|
||||
let sortField: SongListSortField = 'title' // @todo get from query string
|
||||
|
@ -99,7 +101,7 @@ const playAll = async (shuffle: boolean) => {
|
|||
}
|
||||
|
||||
await playbackService.playFirstInQueue()
|
||||
await router.go('queue')
|
||||
router.go('queue')
|
||||
}
|
||||
|
||||
useScreen('Songs').onScreenActivated(async () => {
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import { ref } from 'vue'
|
||||
import { expect, it } from 'vitest'
|
||||
import factory from '@/__tests__/factory'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { artistStore, preferenceStore } from '@/stores'
|
||||
import { fireEvent, waitFor } from '@testing-library/vue'
|
||||
import { ActiveScreenKey } from '@/symbols'
|
||||
import ArtistListScreen from './ArtistListScreen.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
|
@ -12,32 +10,29 @@ new class extends UnitTestCase {
|
|||
super.beforeEach(() => this.mock(artistStore, 'paginate'))
|
||||
}
|
||||
|
||||
private renderComponent () {
|
||||
private async renderComponent () {
|
||||
artistStore.state.artists = factory<Artist>('artist', 9)
|
||||
return this.render(ArtistListScreen, {
|
||||
global: {
|
||||
provide: {
|
||||
[<symbol>ActiveScreenKey]: ref('Artists')
|
||||
}
|
||||
}
|
||||
})
|
||||
const rendered = this.render(ArtistListScreen)
|
||||
await this.router.activateRoute({ path: 'artists', screen: 'Artists' })
|
||||
return rendered
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders', () => {
|
||||
expect(this.renderComponent().getAllByTestId('artist-card')).toHaveLength(9)
|
||||
it('renders', async () => {
|
||||
const { getAllByTestId } = await this.renderComponent()
|
||||
expect(getAllByTestId('artist-card')).toHaveLength(9)
|
||||
})
|
||||
|
||||
it.each<[ArtistAlbumViewMode]>([['list'], ['thumbnails']])('sets layout:%s from preferences', async (mode) => {
|
||||
preferenceStore.artistsViewMode = mode
|
||||
|
||||
const { getByTestId } = this.renderComponent()
|
||||
const { getByTestId } = await this.renderComponent()
|
||||
|
||||
await waitFor(() => expect(getByTestId('artist-list').classList.contains(`as-${mode}`)).toBe(true))
|
||||
})
|
||||
|
||||
it('switches layout', async () => {
|
||||
const { getByTestId, getByTitle } = this.renderComponent()
|
||||
const { getByTestId, getByTitle } = await this.renderComponent()
|
||||
|
||||
await fireEvent.click(getByTitle('View as list'))
|
||||
await waitFor(() => expect(getByTestId('artist-list').classList.contains(`as-list`)).toBe(true))
|
||||
|
|
|
@ -4,7 +4,6 @@ import factory from '@/__tests__/factory'
|
|||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { artistStore, commonStore, songStore } from '@/stores'
|
||||
import { downloadService } from '@/services'
|
||||
import router from '@/router'
|
||||
import { eventBus } from '@/utils'
|
||||
import CloseModalBtn from '@/components/ui/BtnCloseModal.vue'
|
||||
import ArtistScreen from './ArtistScreen.vue'
|
||||
|
@ -28,10 +27,12 @@ new class extends UnitTestCase {
|
|||
const songs = factory<Song>('song', 13)
|
||||
const fetchSongsMock = this.mock(songStore, 'fetchForArtist').mockResolvedValue(songs)
|
||||
|
||||
await this.router.activateRoute({
|
||||
path: 'artists/42',
|
||||
screen: 'Artist'
|
||||
}, { id: '42' })
|
||||
|
||||
const rendered = this.render(ArtistScreen, {
|
||||
props: {
|
||||
artist: 42
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
CloseModalBtn,
|
||||
|
@ -73,7 +74,7 @@ new class extends UnitTestCase {
|
|||
})
|
||||
|
||||
it('goes back to list if artist is deleted', async () => {
|
||||
const goMock = this.mock(router, 'go')
|
||||
const goMock = this.mock(this.router, 'go')
|
||||
const byIdMock = this.mock(artistStore, 'byId', null)
|
||||
await this.renderComponent()
|
||||
|
||||
|
|
|
@ -50,23 +50,23 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, onMounted, ref, toRef, toRefs } from 'vue'
|
||||
import { defineAsyncComponent, onMounted, ref, toRef } from 'vue'
|
||||
import { eventBus, logger, pluralize, requireInjection } from '@/utils'
|
||||
import { artistStore, commonStore, songStore } from '@/stores'
|
||||
import { downloadService } from '@/services'
|
||||
import { useSongList, useThirdPartyServices } from '@/composables'
|
||||
import router from '@/router'
|
||||
import { DialogBoxKey } from '@/symbols'
|
||||
import { DialogBoxKey, RouterKey } from '@/symbols'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import ArtistThumbnail from '@/components/ui/AlbumArtistThumbnail.vue'
|
||||
import ScreenHeaderSkeleton from '@/components/ui/skeletons/ScreenHeaderSkeleton.vue'
|
||||
import SongListSkeleton from '@/components/ui/skeletons/SongListSkeleton.vue'
|
||||
|
||||
const dialog = requireInjection(DialogBoxKey)
|
||||
const ArtistInfo = defineAsyncComponent(() => import('@/components/artist/ArtistInfo.vue'))
|
||||
const CloseModalBtn = defineAsyncComponent(() => import('@/components/ui/BtnCloseModal.vue'))
|
||||
|
||||
const props = defineProps<{ artist: number }>()
|
||||
const { artist: id } = toRefs(props)
|
||||
const dialog = requireInjection(DialogBoxKey)
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const artist = ref<Artist>()
|
||||
const songs = ref<Song[]>([])
|
||||
|
@ -89,22 +89,20 @@ const {
|
|||
onScrollBreakpoint
|
||||
} = useSongList(songs, 'artist', { columns: ['track', 'title', 'album', 'length'] })
|
||||
|
||||
const ArtistInfo = defineAsyncComponent(() => import('@/components/artist/ArtistInfo.vue'))
|
||||
const CloseModalBtn = defineAsyncComponent(() => import('@/components/ui/BtnCloseModal.vue'))
|
||||
|
||||
const { useLastfm } = useThirdPartyServices()
|
||||
const allowDownload = toRef(commonStore.state, 'allow_download')
|
||||
|
||||
const download = () => downloadService.fromArtist(artist.value)
|
||||
const download = () => downloadService.fromArtist(artist.value!)
|
||||
const showInfo = () => (showingInfo.value = true)
|
||||
|
||||
onMounted(async () => {
|
||||
const id = parseInt(router.$currentRoute.value!.params!.id)
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
[artist.value, songs.value] = await Promise.all([
|
||||
artistStore.resolve(id.value),
|
||||
songStore.fetchForArtist(id.value)
|
||||
artistStore.resolve(id),
|
||||
songStore.fetchForArtist(id)
|
||||
])
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
|
@ -114,10 +112,8 @@ onMounted(async () => {
|
|||
}
|
||||
})
|
||||
|
||||
eventBus.on('SONGS_UPDATED', () => {
|
||||
// if the current artist has been deleted, go back to the list
|
||||
artistStore.byId(id.value) || router.go('artists')
|
||||
})
|
||||
// if the current artist has been deleted, go back to the list
|
||||
eventBus.on('SONGS_UPDATED', () => artistStore.byId(artist.value!.id) || router.go('artists'))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -1,22 +1,16 @@
|
|||
import { ref } from 'vue'
|
||||
import { waitFor } from '@testing-library/vue'
|
||||
import { expect, it } from 'vitest'
|
||||
import factory from '@/__tests__/factory'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { favoriteStore } from '@/stores'
|
||||
import { ActiveScreenKey } from '@/symbols'
|
||||
import FavoritesScreen from './FavoritesScreen.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private async renderComponent () {
|
||||
const fetchMock = this.mock(favoriteStore, 'fetch')
|
||||
const rendered = this.render(FavoritesScreen, {
|
||||
global: {
|
||||
provide: {
|
||||
[<symbol>ActiveScreenKey]: ref('Favorites')
|
||||
}
|
||||
}
|
||||
})
|
||||
const rendered = this.render(FavoritesScreen)
|
||||
|
||||
await this.router.activateRoute({ path: 'favorites', screen: 'Favorites' })
|
||||
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalled())
|
||||
|
||||
|
|
|
@ -1,33 +1,28 @@
|
|||
import { ref } from 'vue'
|
||||
import { expect, it } from 'vitest'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { commonStore, overviewStore } from '@/stores'
|
||||
import { ActiveScreenKey } from '@/symbols'
|
||||
import { EventName } from '@/config'
|
||||
import { eventBus } from '@/utils'
|
||||
import HomeScreen from './HomeScreen.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private renderComponent () {
|
||||
return this.render(HomeScreen, {
|
||||
global: {
|
||||
provide: {
|
||||
[<symbol>ActiveScreenKey]: ref('Home')
|
||||
}
|
||||
}
|
||||
})
|
||||
private async renderComponent () {
|
||||
const rendered = this.render(HomeScreen)
|
||||
await this.router.activateRoute({ path: 'home', screen: 'Home' })
|
||||
return rendered
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('renders an empty state if no songs found', () => {
|
||||
it('renders an empty state if no songs found', async () => {
|
||||
commonStore.state.song_length = 0
|
||||
this.renderComponent().getByTestId('screen-empty-state')
|
||||
const { getByTestId } = await this.render(HomeScreen)
|
||||
getByTestId('screen-empty-state')
|
||||
})
|
||||
|
||||
it('renders overview components if applicable', () => {
|
||||
it('renders overview components if applicable', async () => {
|
||||
commonStore.state.song_length = 100
|
||||
|
||||
const { getByTestId, queryByTestId } = this.renderComponent()
|
||||
const { getByTestId, queryByTestId } = await this.renderComponent()
|
||||
|
||||
;[
|
||||
'most-played-songs',
|
||||
|
@ -42,9 +37,9 @@ new class extends UnitTestCase {
|
|||
})
|
||||
|
||||
it.each<[EventName]>([['SONGS_UPDATED'], ['SONGS_DELETED']])
|
||||
('refreshes the overviews on %s event', (eventName) => {
|
||||
('refreshes the overviews on %s event', async (eventName) => {
|
||||
const refreshMock = this.mock(overviewStore, 'refresh')
|
||||
this.renderComponent()
|
||||
await this.renderComponent()
|
||||
|
||||
eventBus.emit(eventName)
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import factory from '@/__tests__/factory'
|
|||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { eventBus } from '@/utils'
|
||||
import { fireEvent, getByTestId, waitFor } from '@testing-library/vue'
|
||||
import { songStore } from '@/stores'
|
||||
import { playlistStore, songStore } from '@/stores'
|
||||
import { downloadService } from '@/services'
|
||||
import PlaylistScreen from './PlaylistScreen.vue'
|
||||
|
||||
|
@ -12,10 +12,16 @@ let playlist: Playlist
|
|||
new class extends UnitTestCase {
|
||||
private async renderComponent (songs: Song[]) {
|
||||
playlist = playlist || factory<Playlist>('playlist')
|
||||
playlistStore.init([playlist])
|
||||
|
||||
const fetchMock = this.mock(songStore, 'fetchForPlaylist').mockResolvedValue(songs)
|
||||
|
||||
const rendered = this.render(PlaylistScreen)
|
||||
eventBus.emit('ACTIVATE_SCREEN', 'Playlist', playlist)
|
||||
|
||||
await this.router.activateRoute({
|
||||
path: `playlists/${playlist.id}`,
|
||||
screen: 'Playlist'
|
||||
}, { id: playlist.id.toString() })
|
||||
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(playlist))
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<section id="playlistWrapper" v-if="playlist">
|
||||
<section v-if="playlist" id="playlistWrapper">
|
||||
<ScreenHeader :layout="songs.length === 0 ? 'collapsed' : headerLayout">
|
||||
{{ playlist.name }}
|
||||
<ControlsToggle v-if="songs.length" v-model="showingControls"/>
|
||||
|
@ -8,7 +8,7 @@
|
|||
<ThumbnailStack :thumbnails="thumbnails"/>
|
||||
</template>
|
||||
|
||||
<template v-slot:meta v-if="songs.length">
|
||||
<template v-if="songs.length" v-slot:meta>
|
||||
<span>{{ pluralize(songs, 'song') }}</span>
|
||||
<span>{{ duration }}</span>
|
||||
<a
|
||||
|
@ -37,10 +37,10 @@
|
|||
<SongList
|
||||
v-if="!loading && songs.length"
|
||||
ref="songList"
|
||||
@sort="sort"
|
||||
@press:delete="removeSelected"
|
||||
@press:enter="onPressEnter"
|
||||
@scroll-breakpoint="onScrollBreakpoint"
|
||||
@sort="sort"
|
||||
/>
|
||||
|
||||
<ScreenEmptyState v-if="!songs.length && !loading">
|
||||
|
@ -70,13 +70,14 @@ import { eventBus, pluralize, requireInjection } from '@/utils'
|
|||
import { commonStore, playlistStore, songStore } from '@/stores'
|
||||
import { downloadService } from '@/services'
|
||||
import { useSongList } from '@/composables'
|
||||
import { MessageToasterKey } from '@/symbols'
|
||||
import { MessageToasterKey, RouterKey } from '@/symbols'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue'
|
||||
import SongListSkeleton from '@/components/ui/skeletons/SongListSkeleton.vue'
|
||||
|
||||
const toaster = requireInjection(MessageToasterKey)
|
||||
const router = requireInjection(RouterKey)
|
||||
const playlist = ref<Playlist>()
|
||||
const loading = ref(false)
|
||||
|
||||
|
@ -123,15 +124,24 @@ const fetchSongs = async () => {
|
|||
sort()
|
||||
}
|
||||
|
||||
eventBus.on({
|
||||
ACTIVATE_SCREEN: async (screen: ScreenName, p: any) => {
|
||||
if (screen === 'Playlist') {
|
||||
playlist.value = p as Playlist
|
||||
await fetchSongs()
|
||||
}
|
||||
},
|
||||
router.onRouteChanged(async (route) => {
|
||||
if (route.screen !== 'Playlist') return
|
||||
const id = parseInt(route.params!.id)
|
||||
|
||||
SMART_PLAYLIST_UPDATED: async (updated: Playlist) => updated === playlist.value && await fetchSongs()
|
||||
if (id === playlist.value?.id) return
|
||||
|
||||
const _playlist = playlistStore.byId(id)
|
||||
|
||||
if (!_playlist) {
|
||||
await router.triggerNotFound()
|
||||
return
|
||||
}
|
||||
)
|
||||
|
||||
playlist.value = _playlist
|
||||
await fetchSongs()
|
||||
})
|
||||
|
||||
eventBus.on({
|
||||
SMART_PLAYLIST_UPDATED: async (updated: Playlist) => updated === playlist.value && await fetchSongs()
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<ThumbnailStack :thumbnails="thumbnails"/>
|
||||
</template>
|
||||
|
||||
<template v-slot:meta v-if="songs.length">
|
||||
<template v-if="songs.length" v-slot:meta>
|
||||
<span>{{ pluralize(songs, 'song') }}</span>
|
||||
<span>{{ duration }}</span>
|
||||
</template>
|
||||
|
@ -16,10 +16,10 @@
|
|||
<template v-slot:controls>
|
||||
<SongListControls
|
||||
v-if="songs.length && (!isPhone || showingControls)"
|
||||
:config="controlConfig"
|
||||
@clearQueue="clearQueue"
|
||||
@playAll="playAll"
|
||||
@playSelected="playSelected"
|
||||
@clearQueue="clearQueue"
|
||||
:config="controlConfig"
|
||||
/>
|
||||
</template>
|
||||
</ScreenHeader>
|
||||
|
@ -28,9 +28,9 @@
|
|||
<SongList
|
||||
v-if="songs.length"
|
||||
ref="songList"
|
||||
@reorder="onReorder"
|
||||
@press:delete="removeSelected"
|
||||
@press:enter="onPressEnter"
|
||||
@reorder="onReorder"
|
||||
@scroll-breakpoint="onScrollBreakpoint"
|
||||
/>
|
||||
|
||||
|
@ -40,9 +40,9 @@
|
|||
</template>
|
||||
|
||||
No songs queued.
|
||||
<span class="d-block secondary" v-if="libraryNotEmpty">
|
||||
<span v-if="libraryNotEmpty" class="d-block secondary">
|
||||
How about
|
||||
<a data-testid="shuffle-library" class="start" @click.prevent="shuffleSome">playing some random songs</a>?
|
||||
<a class="start" data-testid="shuffle-library" @click.prevent="shuffleSome">playing some random songs</a>?
|
||||
</span>
|
||||
</ScreenEmptyState>
|
||||
</section>
|
||||
|
@ -55,13 +55,15 @@ import { eventBus, logger, pluralize, requireInjection } from '@/utils'
|
|||
import { commonStore, queueStore, songStore } from '@/stores'
|
||||
import { playbackService } from '@/services'
|
||||
import { useSongList } from '@/composables'
|
||||
import { DialogBoxKey } from '@/symbols'
|
||||
import { DialogBoxKey, RouterKey } from '@/symbols'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue'
|
||||
import SongListSkeleton from '@/components/ui/skeletons/SongListSkeleton.vue'
|
||||
|
||||
const dialog = requireInjection(DialogBoxKey)
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const controlConfig: Partial<SongListControlsConfig> = { clearQueue: true }
|
||||
|
||||
const {
|
||||
|
@ -84,7 +86,10 @@ const {
|
|||
const loading = ref(false)
|
||||
const libraryNotEmpty = computed(() => commonStore.state.song_count > 0)
|
||||
|
||||
const playAll = (shuffle = true) => playbackService.queueAndPlay(songs.value, shuffle)
|
||||
const playAll = async (shuffle = true) => {
|
||||
await playbackService.queueAndPlay(songs.value, shuffle)
|
||||
router.go('queue')
|
||||
}
|
||||
|
||||
const shuffleSome = async () => {
|
||||
try {
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import { ref } from 'vue'
|
||||
import { expect, it } from 'vitest'
|
||||
import factory from '@/__tests__/factory'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { recentlyPlayedStore } from '@/stores'
|
||||
import { waitFor } from '@testing-library/vue'
|
||||
import { ActiveScreenKey } from '@/symbols'
|
||||
import RecentlyPlayedScreen from './RecentlyPlayedScreen.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
|
@ -16,13 +14,12 @@ new class extends UnitTestCase {
|
|||
global: {
|
||||
stubs: {
|
||||
SongList: this.stub('song-list')
|
||||
},
|
||||
provide: {
|
||||
[<symbol>ActiveScreenKey]: ref('RecentlyPlayed')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await this.router.activateRoute({ path: 'recently-played', screen: 'RecentlyPlayed' })
|
||||
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalled())
|
||||
|
||||
return rendered
|
||||
|
|
|
@ -3,7 +3,6 @@ import UnitTestCase from '@/__tests__/UnitTestCase'
|
|||
import SettingsScreen from './SettingsScreen.vue'
|
||||
import { settingStore } from '@/stores'
|
||||
import { fireEvent, waitFor } from '@testing-library/vue'
|
||||
import router from '@/router'
|
||||
import { DialogBoxStub } from '@/__tests__/stubs'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
|
@ -12,7 +11,7 @@ new class extends UnitTestCase {
|
|||
|
||||
it('submits the settings form', async () => {
|
||||
const updateMock = this.mock(settingStore, 'update')
|
||||
const goMock = this.mock(router, 'go')
|
||||
const goMock = this.mock(this.router, 'go')
|
||||
|
||||
settingStore.state.media_path = ''
|
||||
const { getByLabelText, getByText } = this.render(SettingsScreen)
|
||||
|
@ -28,7 +27,7 @@ new class extends UnitTestCase {
|
|||
|
||||
it('confirms upon media path change', async () => {
|
||||
const updateMock = this.mock(settingStore, 'update')
|
||||
const goMock = this.mock(router, 'go')
|
||||
const goMock = this.mock(this.router, 'go')
|
||||
const confirmMock = this.mock(DialogBoxStub.value, 'confirm')
|
||||
|
||||
settingStore.state.media_path = '/old'
|
||||
|
|
|
@ -32,14 +32,15 @@
|
|||
import { computed, ref } from 'vue'
|
||||
import { settingStore } from '@/stores'
|
||||
import { forceReloadWindow, hideOverlay, parseValidationError, requireInjection, showOverlay } from '@/utils'
|
||||
import router from '@/router'
|
||||
import { DialogBoxKey, MessageToasterKey } from '@/symbols'
|
||||
import { DialogBoxKey, MessageToasterKey, RouterKey } from '@/symbols'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import Btn from '@/components/ui/Btn.vue'
|
||||
|
||||
const router = requireInjection(RouterKey)
|
||||
const toaster = requireInjection(MessageToasterKey)
|
||||
const dialog = requireInjection(DialogBoxKey)
|
||||
|
||||
const mediaPath = ref(settingStore.state.media_path)
|
||||
const originalMediaPath = mediaPath.value
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ import { expect, it } from 'vitest'
|
|||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { fireEvent } from '@testing-library/vue'
|
||||
import router from '@/router'
|
||||
import { overviewStore } from '@/stores'
|
||||
import RecentlyPlayedSongs from './RecentlyPlayedSongs.vue'
|
||||
|
||||
|
@ -14,7 +13,7 @@ new class extends UnitTestCase {
|
|||
})
|
||||
|
||||
it('goes to dedicated screen', async () => {
|
||||
const mock = this.mock(router, 'go')
|
||||
const mock = this.mock(this.router, 'go')
|
||||
const { getByTestId } = this.render(RecentlyPlayedSongs)
|
||||
|
||||
await fireEvent.click(getByTestId('home-view-all-recently-played-btn'))
|
||||
|
|
|
@ -32,13 +32,16 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { toRef, toRefs } from 'vue'
|
||||
import router from '@/router'
|
||||
import { overviewStore } from '@/stores'
|
||||
import { RouterKey } from '@/symbols'
|
||||
import { requireInjection } from '@/utils'
|
||||
|
||||
import Btn from '@/components/ui/Btn.vue'
|
||||
import SongCard from '@/components/song/SongCard.vue'
|
||||
import SongCardSkeleton from '@/components/ui/skeletons/SongCardSkeleton.vue'
|
||||
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const props = withDefaults(defineProps<{ loading?: boolean }>(), { loading: false })
|
||||
const { loading } = toRefs(props)
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
</ScreenHeader>
|
||||
|
||||
<div ref="wrapper" class="main-scroll-wrap">
|
||||
<div class="results" v-if="q">
|
||||
<div v-if="q" class="results">
|
||||
<section class="songs" data-testid="song-excerpts">
|
||||
<h1>
|
||||
Songs
|
||||
|
@ -86,9 +86,9 @@
|
|||
import { faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||
import { intersectionBy } from 'lodash'
|
||||
import { ref, toRef } from 'vue'
|
||||
import { eventBus } from '@/utils'
|
||||
import { eventBus, requireInjection } from '@/utils'
|
||||
import { searchStore } from '@/stores'
|
||||
import router from '@/router'
|
||||
import { RouterKey } from '@/symbols'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue'
|
||||
|
@ -99,11 +99,13 @@ import SongCard from '@/components/song/SongCard.vue'
|
|||
import SongCardSkeleton from '@/components/ui/skeletons/SongCardSkeleton.vue'
|
||||
import ArtistAlbumCardSkeleton from '@/components/ui/skeletons/ArtistAlbumCardSkeleton.vue'
|
||||
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const excerpt = toRef(searchStore.state, 'excerpt')
|
||||
const q = ref('')
|
||||
const searching = ref(false)
|
||||
|
||||
const goToSongResults = () => router.go(`search/songs/${q.value}`)
|
||||
const goToSongResults = () => router.go(`search/songs/?q=${q.value}`)
|
||||
|
||||
const doSearch = async () => {
|
||||
searching.value = true
|
||||
|
|
|
@ -8,11 +8,9 @@ new class extends UnitTestCase {
|
|||
it('searches for prop query on created', () => {
|
||||
const resetResultMock = this.mock(searchStore, 'resetSongResultState')
|
||||
const searchMock = this.mock(searchStore, 'songSearch')
|
||||
this.render(SearchSongResultsScreen, {
|
||||
props: {
|
||||
q: 'search me'
|
||||
}
|
||||
})
|
||||
|
||||
this.router.activateRoute({ path: 'search-songs', screen: 'Search.Songs' }, { q: 'search me' })
|
||||
this.render(SearchSongResultsScreen)
|
||||
|
||||
expect(resetResultMock).toHaveBeenCalled()
|
||||
expect(searchMock).toHaveBeenCalledWith('search me')
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<ThumbnailStack :thumbnails="thumbnails"/>
|
||||
</template>
|
||||
|
||||
<template v-slot:meta v-if="songs.length">
|
||||
<template v-if="songs.length" v-slot:meta>
|
||||
<span>{{ pluralize(songs, 'song') }}</span>
|
||||
<span>{{ duration }}</span>
|
||||
</template>
|
||||
|
@ -28,16 +28,17 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, toRef, toRefs } from 'vue'
|
||||
import { computed, onMounted, ref, toRef } from 'vue'
|
||||
import { searchStore } from '@/stores'
|
||||
import { useSongList } from '@/composables'
|
||||
import { pluralize } from '@/utils'
|
||||
import { pluralize, requireInjection } from '@/utils'
|
||||
import { RouterKey } from '@/symbols'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import SongListSkeleton from '@/components/ui/skeletons/SongListSkeleton.vue'
|
||||
|
||||
const props = defineProps<{ q: string }>()
|
||||
const { q } = toRefs(props)
|
||||
const router = requireInjection(RouterKey)
|
||||
const q = ref('')
|
||||
|
||||
const {
|
||||
SongList,
|
||||
|
@ -64,6 +65,9 @@ const loading = ref(false)
|
|||
searchStore.resetSongResultState()
|
||||
|
||||
onMounted(async () => {
|
||||
q.value = router.$currentRoute.value?.params?.q || ''
|
||||
if (!q.value) return
|
||||
|
||||
loading.value = true
|
||||
await searchStore.songSearch(q.value)
|
||||
loading.value = false
|
||||
|
|
|
@ -79,12 +79,13 @@ import { computed, nextTick, ref, toRef, toRefs, watch } from 'vue'
|
|||
import { pluralize, requireInjection } from '@/utils'
|
||||
import { playlistStore, queueStore } from '@/stores'
|
||||
import { useSongMenuMethods } from '@/composables'
|
||||
import router from '@/router'
|
||||
import { MessageToasterKey } from '@/symbols'
|
||||
import { MessageToasterKey, RouterKey } from '@/symbols'
|
||||
|
||||
import Btn from '@/components/ui/Btn.vue'
|
||||
|
||||
const toaster = requireInjection(MessageToasterKey)
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const props = defineProps<{ songs: Song[], showing: Boolean, config: AddToMenuConfig }>()
|
||||
const { songs, showing, config } = toRefs(props)
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ import factory from '@/__tests__/factory'
|
|||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { arrayify, eventBus } from '@/utils'
|
||||
import { fireEvent, waitFor } from '@testing-library/vue'
|
||||
import router from '@/router'
|
||||
import { downloadService, playbackService } from '@/services'
|
||||
import { favoriteStore, playlistStore, queueStore, songStore } from '@/stores'
|
||||
import { DialogBoxStub, MessageToasterStub } from '@/__tests__/stubs'
|
||||
|
@ -63,7 +62,7 @@ new class extends UnitTestCase {
|
|||
})
|
||||
|
||||
it('goes to album details screen', async () => {
|
||||
const goMock = this.mock(router, 'go')
|
||||
const goMock = this.mock(this.router, 'go')
|
||||
const { getByText } = await this.renderComponent(factory<Song>('song'))
|
||||
|
||||
await fireEvent.click(getByText('Go to Album'))
|
||||
|
@ -72,7 +71,7 @@ new class extends UnitTestCase {
|
|||
})
|
||||
|
||||
it('goes to artist details screen', async () => {
|
||||
const goMock = this.mock(router, 'go')
|
||||
const goMock = this.mock(this.router, 'go')
|
||||
const { getByText } = await this.renderComponent(factory<Song>('song'))
|
||||
|
||||
await fireEvent.click(getByText('Go to Artist'))
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<li v-else @click="queueSongsToBottom">Queue</li>
|
||||
<li class="separator"/>
|
||||
<li @click="addSongsToFavorite">Favorites</li>
|
||||
<li class="separator" v-if="normalPlaylists.length"/>
|
||||
<li v-if="normalPlaylists.length" class="separator"/>
|
||||
<li v-for="p in normalPlaylists" :key="p.id" @click="addSongsToExistingPlaylist(p)">{{ p.name }}</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
@ -38,14 +38,15 @@ import { computed, ref, toRef } from 'vue'
|
|||
import { arrayify, copyText, eventBus, pluralize, requireInjection } from '@/utils'
|
||||
import { commonStore, playlistStore, queueStore, songStore, userStore } from '@/stores'
|
||||
import { downloadService, playbackService } from '@/services'
|
||||
import router from '@/router'
|
||||
import { useAuthorization, useContextMenu, useSongMenuMethods } from '@/composables'
|
||||
import { DialogBoxKey, MessageToasterKey } from '@/symbols'
|
||||
import { DialogBoxKey, MessageToasterKey, RouterKey } from '@/symbols'
|
||||
|
||||
const { context, base, ContextMenuBase, open, close, trigger } = useContextMenu()
|
||||
|
||||
const dialogBox = requireInjection(DialogBoxKey)
|
||||
const toaster = requireInjection(MessageToasterKey)
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const songs = ref<Song[]>([])
|
||||
|
||||
const {
|
||||
|
|
|
@ -28,11 +28,12 @@ import { albumStore, artistStore, queueStore, songStore, userStore } from '@/sto
|
|||
import { playbackService } from '@/services'
|
||||
import { defaultCover, fileReader, logger, requireInjection } from '@/utils'
|
||||
import { useAuthorization } from '@/composables'
|
||||
import { MessageToasterKey } from '@/symbols'
|
||||
import { MessageToasterKey, RouterKey } from '@/symbols'
|
||||
|
||||
const VALID_IMAGE_TYPES = ['image/jpeg', 'image/gif', 'image/png', 'image/webp']
|
||||
|
||||
const toaster = requireInjection(MessageToasterKey)
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const props = defineProps<{ entity: Album | Artist }>()
|
||||
const { entity } = toRefs(props)
|
||||
|
@ -72,6 +73,7 @@ const playOrQueue = async (event: KeyboardEvent) => {
|
|||
}
|
||||
|
||||
await playbackService.queueAndPlay(songs)
|
||||
router.go('queue')
|
||||
}
|
||||
|
||||
const onDragEnter = () => (droppable.value = allowsUpload.value)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div id="equalizer" data-testid="equalizer" ref="root">
|
||||
<div class="presets">
|
||||
<label class="select-wrapper">
|
||||
<select v-model="selectedPresetId">
|
||||
<select v-model="selectedPresetId" title="Select equalizer">
|
||||
<option disabled value="-1">Preset</option>
|
||||
<option v-for="preset in presets" :value="preset.id" :key="preset.id" v-once>{{ preset.name }}</option>
|
||||
</select>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { expect, it } from 'vitest'
|
||||
import router from '@/router'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { fireEvent } from '@testing-library/vue'
|
||||
import { eventBus } from '@/utils'
|
||||
|
@ -16,7 +15,7 @@ new class extends UnitTestCase {
|
|||
})
|
||||
|
||||
it('goes to search screen when search box is focused', async () => {
|
||||
const mock = this.mock(router, 'go')
|
||||
const mock = this.mock(this.router, 'go')
|
||||
const { getByRole } = this.render(SearchForm)
|
||||
|
||||
await fireEvent.focus(getByRole('searchbox'))
|
||||
|
|
|
@ -18,8 +18,10 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { debounce } from 'lodash'
|
||||
import { eventBus } from '@/utils'
|
||||
import router from '@/router'
|
||||
import { eventBus, requireInjection } from '@/utils'
|
||||
import { RouterKey } from '@/symbols'
|
||||
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const input = ref<HTMLInputElement>()
|
||||
const q = ref('')
|
||||
|
|
|
@ -11,13 +11,20 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, toRefs } from 'vue'
|
||||
import { youTubeService } from '@/services'
|
||||
import { RouterKey } from '@/symbols'
|
||||
import { requireInjection } from '@/utils'
|
||||
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const props = defineProps<{ video: YouTubeVideo }>()
|
||||
const { video } = toRefs(props)
|
||||
|
||||
const url = computed(() => `https://youtu.be/${video.value.id.videoId}`)
|
||||
|
||||
const play = () => youTubeService.play(video.value)
|
||||
const play = () => {
|
||||
youTubeService.play(video.value)
|
||||
router.go('youtube')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { expect, it } from 'vitest'
|
||||
import { UploadFile, UploadStatus } from '@/config'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { fireEvent } from '@testing-library/vue'
|
||||
import { uploadService } from '@/services'
|
||||
import { UploadFile, uploadService, UploadStatus } from '@/services'
|
||||
import Btn from '@/components/ui/Btn.vue'
|
||||
import UploadItem from './UploadItem.vue'
|
||||
|
||||
|
|
|
@ -19,8 +19,7 @@
|
|||
import slugify from 'slugify'
|
||||
import { faRotateBack, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed, defineAsyncComponent, toRefs } from 'vue'
|
||||
import { UploadFile } from '@/config'
|
||||
import { uploadService } from '@/services'
|
||||
import { UploadFile, uploadService } from '@/services'
|
||||
|
||||
const Btn = defineAsyncComponent(() => import('@/components/ui/Btn.vue'))
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ import { expect, it } from 'vitest'
|
|||
import factory from '@/__tests__/factory'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { fireEvent } from '@testing-library/vue'
|
||||
import router from '@/router'
|
||||
import { eventBus } from '@/utils'
|
||||
import UserCard from './UserCard.vue'
|
||||
|
||||
|
@ -35,7 +34,7 @@ new class extends UnitTestCase {
|
|||
})
|
||||
|
||||
it('redirects to Profile screen if edit current user', async () => {
|
||||
const mock = this.mock(router, 'go')
|
||||
const mock = this.mock(this.router, 'go')
|
||||
const user = factory<User>('user')
|
||||
const { getByText } = this.actingAs(user).renderComponent(user)
|
||||
|
||||
|
|
|
@ -17,10 +17,10 @@
|
|||
<p class="email text-secondary">{{ user.email }}</p>
|
||||
|
||||
<footer>
|
||||
<Btn class="btn-edit" data-testid="edit-user-btn" small orange @click="edit">
|
||||
<Btn class="btn-edit" data-testid="edit-user-btn" orange small @click="edit">
|
||||
{{ isCurrentUser ? 'Your Profile' : 'Edit' }}
|
||||
</Btn>
|
||||
<Btn v-if="!isCurrentUser" class="btn-delete" data-testid="delete-user-btn" small red @click="confirmDelete">
|
||||
<Btn v-if="!isCurrentUser" class="btn-delete" data-testid="delete-user-btn" red small @click="confirmDelete">
|
||||
Delete
|
||||
</Btn>
|
||||
</footer>
|
||||
|
@ -34,13 +34,13 @@ import { computed, toRefs } from 'vue'
|
|||
import { userStore } from '@/stores'
|
||||
import { eventBus, requireInjection } from '@/utils'
|
||||
import { useAuthorization } from '@/composables'
|
||||
import { DialogBoxKey, MessageToasterKey } from '@/symbols'
|
||||
import router from '@/router'
|
||||
import { DialogBoxKey, MessageToasterKey, RouterKey } from '@/symbols'
|
||||
|
||||
import Btn from '@/components/ui/Btn.vue'
|
||||
|
||||
const toaster = requireInjection(MessageToasterKey)
|
||||
const dialog = requireInjection(DialogBoxKey)
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const props = defineProps<{ user: User }>()
|
||||
const { user } = toRefs(props)
|
||||
|
|
|
@ -7,14 +7,14 @@
|
|||
* Global event listeners (basically, those without a Vue instance access) go here.
|
||||
*/
|
||||
import isMobile from 'ismobilejs'
|
||||
import router from '@/router'
|
||||
import { authService } from '@/services'
|
||||
import { playlistFolderStore, playlistStore, preferenceStore, userStore } from '@/stores'
|
||||
import { eventBus, forceReloadWindow, requireInjection } from '@/utils'
|
||||
import { DialogBoxKey, MessageToasterKey } from '@/symbols'
|
||||
import { DialogBoxKey, MessageToasterKey, RouterKey } from '@/symbols'
|
||||
|
||||
const toaster = requireInjection(MessageToasterKey)
|
||||
const dialog = requireInjection(DialogBoxKey)
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
eventBus.on({
|
||||
PLAYLIST_DELETE: async (playlist: Playlist) => {
|
||||
|
@ -42,11 +42,13 @@ eventBus.on({
|
|||
forceReloadWindow()
|
||||
},
|
||||
|
||||
KOEL_READY: () => router.resolveRoute(),
|
||||
KOEL_READY: () => router.resolve()
|
||||
})
|
||||
|
||||
/**
|
||||
* Hide the panel away if a main view is triggered on mobile.
|
||||
*/
|
||||
ACTIVATE_SCREEN: () => isMobile.phone && (preferenceStore.showExtraPanel = false)
|
||||
router.onRouteChanged(() => {
|
||||
// Hide the extra panel away if a main view is triggered on mobile.
|
||||
if (isMobile.phone) {
|
||||
preferenceStore.showExtraPanel = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
import { ref, watch } from 'vue'
|
||||
import { RouterKey } from '@/symbols'
|
||||
import { requireInjection } from '@/utils'
|
||||
import { ActiveScreenKey } from '@/symbols'
|
||||
|
||||
export const useScreen = (currentScreen: ScreenName) => {
|
||||
const activeScreen = requireInjection(ActiveScreenKey, ref('Home'))
|
||||
|
||||
const onScreenActivated = (cb: Closure) => watch(activeScreen, screen => screen === currentScreen && cb(), {
|
||||
immediate: true
|
||||
})
|
||||
export const useScreen = (screen: ScreenName) => {
|
||||
const router = requireInjection(RouterKey)
|
||||
const onScreenActivated = (cb: Closure) => router.onRouteChanged(route => route.screen === screen && cb())
|
||||
|
||||
return {
|
||||
activeScreen,
|
||||
onScreenActivated
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import isMobile from 'ismobilejs'
|
|||
import { computed, reactive, Ref, ref } from 'vue'
|
||||
import { playbackService } from '@/services'
|
||||
import { queueStore, songStore } from '@/stores'
|
||||
import router from '@/router'
|
||||
import { eventBus, provideReadonly, requireInjection } from '@/utils'
|
||||
|
||||
import {
|
||||
SelectedSongsKey,
|
||||
|
@ -11,16 +11,18 @@ import {
|
|||
SongListSortFieldKey,
|
||||
SongListSortOrderKey,
|
||||
SongListTypeKey,
|
||||
SongsKey
|
||||
SongsKey,
|
||||
RouterKey
|
||||
} from '@/symbols'
|
||||
|
||||
import ControlsToggle from '@/components/ui/ScreenControlsToggle.vue'
|
||||
import SongList from '@/components/song/SongList.vue'
|
||||
import SongListControls from '@/components/song/SongListControls.vue'
|
||||
import ThumbnailStack from '@/components/ui/ThumbnailStack.vue'
|
||||
import { eventBus, provideReadonly } from '@/utils'
|
||||
|
||||
export const useSongList = (songs: Ref<Song[]>, type: SongListType, config: Partial<SongListConfig> = {}) => {
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const songList = ref<InstanceType<typeof SongList>>()
|
||||
|
||||
const isPhone = isMobile.phone
|
||||
|
@ -61,7 +63,7 @@ export const useSongList = (songs: Ref<Song[]>, type: SongListType, config: Part
|
|||
await playbackService.play(selectedSongs.value[0])
|
||||
}
|
||||
|
||||
router.go('/queue')
|
||||
router.go('queue')
|
||||
}
|
||||
|
||||
const sortField = ref<SongListSortField | null>(((): SongListSortField | null => {
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import isMobile from 'ismobilejs'
|
||||
import { computed, ref, toRef } from 'vue'
|
||||
import { useAuthorization } from '@/composables/useAuthorization'
|
||||
import { computed, toRef } from 'vue'
|
||||
import { useAuthorization } from '@/composables'
|
||||
import { settingStore } from '@/stores'
|
||||
import { acceptedMediaTypes, UploadFile } from '@/config'
|
||||
import { uploadService } from '@/services'
|
||||
import { acceptedMediaTypes } from '@/config'
|
||||
import { UploadFile, uploadService } from '@/services'
|
||||
import { getAllFileEntries, pluralize, requireInjection } from '@/utils'
|
||||
import { ActiveScreenKey, MessageToasterKey } from '@/symbols'
|
||||
import router from '@/router'
|
||||
import { MessageToasterKey, RouterKey } from '@/symbols'
|
||||
|
||||
export const useUpload = () => {
|
||||
const { isAdmin } = useAuthorization()
|
||||
|
||||
const activeScreen = requireInjection(ActiveScreenKey, ref('Home'))
|
||||
const toaster = requireInjection(MessageToasterKey)
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
const mediaPath = toRef(settingStore.state, 'media_path')
|
||||
|
||||
const mediaPathSetUp = computed(() => Boolean(mediaPath.value))
|
||||
|
@ -47,7 +47,7 @@ export const useUpload = () => {
|
|||
|
||||
if (queuedFiles.length) {
|
||||
toaster.value.success(`Queued ${pluralize(queuedFiles, 'file')} for upload`)
|
||||
activeScreen.value === 'Upload' || router.go('upload')
|
||||
router.$currentRoute.value.screen === 'Upload' || router.go('upload')
|
||||
} else {
|
||||
toaster.value.warning('No files applicable for upload')
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
export type EventName =
|
||||
'KOEL_READY'
|
||||
| 'ACTIVATE_SCREEN'
|
||||
| 'LOG_OUT'
|
||||
| 'TOGGLE_SIDEBAR'
|
||||
| 'SHOW_OVERLAY'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export * from './events'
|
||||
export * from './upload.types'
|
||||
export * from './acceptedMediaTypes'
|
||||
export * from './genres'
|
||||
export * from './routes'
|
||||
|
|
93
resources/assets/js/config/routes.ts
Normal file
93
resources/assets/js/config/routes.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
import { eventBus } from '@/utils'
|
||||
import { Route } from '@/router'
|
||||
import { userStore } from '@/stores'
|
||||
|
||||
const queueRoute: Route = {
|
||||
path: '/queue',
|
||||
screen: 'Queue'
|
||||
}
|
||||
|
||||
export const routes: Route[] = [
|
||||
queueRoute,
|
||||
{
|
||||
path: '/home',
|
||||
screen: 'Home'
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
screen: '404'
|
||||
},
|
||||
{
|
||||
path: '/songs',
|
||||
screen: 'Songs'
|
||||
},
|
||||
{
|
||||
path: '/albums',
|
||||
screen: 'Albums'
|
||||
},
|
||||
{
|
||||
path: '/artists',
|
||||
screen: 'Artists'
|
||||
},
|
||||
{
|
||||
path: '/favorites',
|
||||
screen: 'Favorites'
|
||||
},
|
||||
{
|
||||
path: '/recently-played',
|
||||
screen: 'RecentlyPlayed'
|
||||
},
|
||||
{
|
||||
path: '/search',
|
||||
screen: 'Search.Excerpt'
|
||||
},
|
||||
{
|
||||
path: '/search/songs',
|
||||
screen: 'Search.Songs'
|
||||
},
|
||||
{
|
||||
path: '/upload',
|
||||
screen: 'Upload',
|
||||
onBeforeEnter: () => userStore.current.is_admin
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
screen: 'Settings',
|
||||
onBeforeEnter: () => userStore.current.is_admin
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
screen: 'Users',
|
||||
onBeforeEnter: () => userStore.current.is_admin
|
||||
},
|
||||
{
|
||||
path: '/youtube',
|
||||
screen: 'YouTube'
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
screen: 'Profile'
|
||||
},
|
||||
{
|
||||
path: 'visualizer',
|
||||
screen: 'Visualizer'
|
||||
},
|
||||
{
|
||||
path: '/album/(?<id>\\d+)',
|
||||
screen: 'Album'
|
||||
},
|
||||
{
|
||||
path: '/artist/(?<id>\\d+)',
|
||||
screen: 'Artist'
|
||||
},
|
||||
{
|
||||
path: '/playlist/(?<id>\\d+)',
|
||||
screen: 'Playlist'
|
||||
},
|
||||
{
|
||||
path: '/song/(?<id>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})',
|
||||
screen: 'Queue',
|
||||
redirect: () => queueRoute,
|
||||
onBeforeEnter: params => eventBus.emit('SONG_QUEUED_FROM_ROUTE', params.id)
|
||||
}
|
||||
]
|
|
@ -1,15 +0,0 @@
|
|||
export type UploadStatus =
|
||||
| 'Ready'
|
||||
| 'Uploading'
|
||||
| 'Uploaded'
|
||||
| 'Canceled'
|
||||
| 'Errored'
|
||||
|
||||
export interface UploadFile {
|
||||
id: string
|
||||
file: File
|
||||
status: UploadStatus
|
||||
name: string
|
||||
progress: number
|
||||
message?: string
|
||||
}
|
|
@ -1,68 +1,94 @@
|
|||
import { eventBus, loadMainView, use } from '@/utils'
|
||||
import { playlistStore, userStore } from '@/stores'
|
||||
import { ref, Ref, watch } from 'vue'
|
||||
|
||||
class Router {
|
||||
routes: Record<string, Closure>
|
||||
paths: string[]
|
||||
type RouteParams = Record<string, string>
|
||||
type BeforeEnterHook = (params: RouteParams) => boolean | void
|
||||
type EnterHook = (params: RouteParams) => any
|
||||
type RedirectHook = (params: RouteParams) => Route
|
||||
|
||||
constructor () {
|
||||
this.routes = {
|
||||
'/home': () => loadMainView('Home'),
|
||||
'/queue': () => loadMainView('Queue'),
|
||||
'/songs': () => loadMainView('Songs'),
|
||||
'/albums': () => loadMainView('Albums'),
|
||||
'/artists': () => loadMainView('Artists'),
|
||||
'/favorites': () => loadMainView('Favorites'),
|
||||
'/recently-played': () => loadMainView('RecentlyPlayed'),
|
||||
'/search': () => loadMainView('Search.Excerpt'),
|
||||
'/search/songs/(.+)': (q: string) => loadMainView('Search.Songs', q),
|
||||
'/upload': () => userStore.current.is_admin && loadMainView('Upload'),
|
||||
'/settings': () => userStore.current.is_admin && loadMainView('Settings'),
|
||||
'/users': () => userStore.current.is_admin && loadMainView('Users'),
|
||||
'/youtube': () => loadMainView('YouTube'),
|
||||
'/visualizer': () => loadMainView('Visualizer'),
|
||||
'/profile': () => loadMainView('Profile'),
|
||||
'/album/(\\d+)': (id: string) => loadMainView('Album', parseInt(id)),
|
||||
'/artist/(\\d+)': (id: string) => loadMainView('Artist', parseInt(id)),
|
||||
'/playlist/(\\d+)': (id: string) => use(playlistStore.byId(~~id), playlist => loadMainView('Playlist', playlist)),
|
||||
'/song/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})': (id: string) => {
|
||||
eventBus.emit('SONG_QUEUED_FROM_ROUTE', id)
|
||||
loadMainView('Queue')
|
||||
export type Route = {
|
||||
path: string
|
||||
screen: ScreenName
|
||||
params?: RouteParams
|
||||
redirect?: RedirectHook
|
||||
onBeforeEnter?: BeforeEnterHook
|
||||
onEnter?: EnterHook
|
||||
}
|
||||
|
||||
type RouteChangedHandler = (newRoute: Route, oldRoute: Route | undefined) => any
|
||||
|
||||
export default class Router {
|
||||
public $currentRoute: Ref<Route>
|
||||
|
||||
private readonly routes: Route[]
|
||||
private readonly homeRoute: Route
|
||||
private readonly notFoundRoute: Route
|
||||
private routeChangedHandlers: RouteChangedHandler[] = []
|
||||
|
||||
constructor (routes: Route[]) {
|
||||
this.routes = routes
|
||||
this.homeRoute = routes.find(route => route.screen === 'Home')!
|
||||
this.notFoundRoute = routes.find(route => route.screen === '404')!
|
||||
this.$currentRoute = ref<Route>(this.homeRoute)
|
||||
|
||||
watch(
|
||||
this.$currentRoute,
|
||||
(newValue, oldValue) => this.routeChangedHandlers.forEach(async handler => await handler(newValue, oldValue)),
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
this.paths = Object.keys(this.routes)
|
||||
|
||||
addEventListener('popstate', () => this.resolveRoute(), true)
|
||||
addEventListener('popstate', () => this.resolve(), true)
|
||||
}
|
||||
|
||||
public resolveRoute () {
|
||||
if (!location.hash) {
|
||||
return this.go('home')
|
||||
public async resolve () {
|
||||
if (!location.hash || location.hash === '#!/') {
|
||||
return this.activateRoute(this.homeRoute)
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.paths.length; i++) {
|
||||
const matches = location.hash.match(new RegExp(`^#!${this.paths[i]}$`))
|
||||
for (let i = 0; i < this.routes.length; i++) {
|
||||
const route = this.routes[i]
|
||||
const matches = location.hash.match(new RegExp(`^#!${route.path}/?(?:\\?(.*))?$`))
|
||||
|
||||
if (matches) {
|
||||
const [, ...params] = matches
|
||||
this.routes[this.paths[i]](...params)
|
||||
return
|
||||
const searchParams = new URLSearchParams(new URL(location.href.replace('#!/', '')).search)
|
||||
const routeParams = Object.assign(Object.fromEntries(searchParams.entries()), matches.groups || {})
|
||||
|
||||
if (route.onBeforeEnter && route.onBeforeEnter(routeParams) === false) {
|
||||
return this.triggerNotFound()
|
||||
}
|
||||
|
||||
return this.activateRoute(route, routeParams)
|
||||
}
|
||||
}
|
||||
|
||||
loadMainView('404')
|
||||
await this.triggerNotFound()
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a (relative, hash-bang'ed) path.
|
||||
*/
|
||||
public go (path: string | number) {
|
||||
if (typeof path === 'number') {
|
||||
history.go(path)
|
||||
return
|
||||
public async triggerNotFound () {
|
||||
await this.activateRoute(this.notFoundRoute)
|
||||
}
|
||||
|
||||
public onRouteChanged (handler: RouteChangedHandler) {
|
||||
this.routeChangedHandlers.push(handler)
|
||||
}
|
||||
|
||||
public async activateRoute (route: Route, params: RouteParams = {}) {
|
||||
this.$currentRoute.value = route
|
||||
this.$currentRoute.value.params = params
|
||||
|
||||
if (this.$currentRoute.value.redirect) {
|
||||
const to = this.$currentRoute.value.redirect(params)
|
||||
return await this.activateRoute(to, to.params)
|
||||
}
|
||||
|
||||
if (this.$currentRoute.value.onEnter) {
|
||||
await this.$currentRoute.value.onEnter(params)
|
||||
}
|
||||
}
|
||||
|
||||
public go (path: string) {
|
||||
if (!path.startsWith('/')) {
|
||||
path = `/${path}`
|
||||
}
|
||||
|
@ -75,5 +101,3 @@ class Router {
|
|||
location.assign(`${location.origin}${location.pathname}${path}`)
|
||||
}
|
||||
}
|
||||
|
||||
export default new Router()
|
||||
|
|
|
@ -7,5 +7,6 @@ export * from './socketService'
|
|||
export * from './audioService'
|
||||
export * from './uploadService'
|
||||
export * from './authService'
|
||||
export * from './mediaInfoService'
|
||||
export * from './cache'
|
||||
export * from './socketListener'
|
||||
|
|
|
@ -3,7 +3,6 @@ import plyr from 'plyr'
|
|||
import lodash from 'lodash'
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import { eventBus, noop } from '@/utils'
|
||||
import router from '@/router'
|
||||
import factory from '@/__tests__/factory'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { socketService } from '@/services'
|
||||
|
@ -332,7 +331,6 @@ new class extends UnitTestCase {
|
|||
it('queues and plays songs without shuffling', async () => {
|
||||
const songs = factory<Song>('song', 5)
|
||||
const replaceQueueMock = this.mock(queueStore, 'replaceQueueWith')
|
||||
const goMock = this.mock(router, 'go')
|
||||
const playMock = this.mock(playbackService, 'play')
|
||||
const firstSongInQueue = songs[0]
|
||||
const shuffleMock = this.mock(lodash, 'shuffle')
|
||||
|
@ -343,7 +341,6 @@ new class extends UnitTestCase {
|
|||
|
||||
expect(shuffleMock).not.toHaveBeenCalled()
|
||||
expect(replaceQueueMock).toHaveBeenCalledWith(songs)
|
||||
expect(goMock).toHaveBeenCalledWith('queue')
|
||||
expect(playMock).toHaveBeenCalledWith(firstSongInQueue)
|
||||
})
|
||||
|
||||
|
@ -351,7 +348,6 @@ new class extends UnitTestCase {
|
|||
const songs = factory<Song>('song', 5)
|
||||
const shuffledSongs = factory<Song>('song', 5)
|
||||
const replaceQueueMock = this.mock(queueStore, 'replaceQueueWith')
|
||||
const goMock = this.mock(router, 'go')
|
||||
const playMock = this.mock(playbackService, 'play')
|
||||
const firstSongInQueue = songs[0]
|
||||
this.setReadOnlyProperty(queueStore, 'first', firstSongInQueue)
|
||||
|
@ -362,7 +358,6 @@ new class extends UnitTestCase {
|
|||
|
||||
expect(shuffleMock).toHaveBeenCalledWith(songs)
|
||||
expect(replaceQueueMock).toHaveBeenCalledWith(shuffledSongs)
|
||||
expect(goMock).toHaveBeenCalledWith('queue')
|
||||
expect(playMock).toHaveBeenCalledWith(firstSongInQueue)
|
||||
})
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import isMobile from 'ismobilejs'
|
||||
import plyr from 'plyr'
|
||||
import { shuffle, throttle } from 'lodash'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import {
|
||||
commonStore,
|
||||
|
@ -14,7 +13,6 @@ import {
|
|||
|
||||
import { arrayify, eventBus, isAudioContextSupported, logger } from '@/utils'
|
||||
import { audioService, socketService } from '@/services'
|
||||
import router from '@/router'
|
||||
|
||||
/**
|
||||
* The number of seconds before the current song ends to start preload the next one.
|
||||
|
@ -58,59 +56,6 @@ class PlaybackService {
|
|||
this.setMediaSessionActionHandlers()
|
||||
}
|
||||
|
||||
private setMediaSessionActionHandlers () {
|
||||
if (!navigator.mediaSession) {
|
||||
return
|
||||
}
|
||||
|
||||
navigator.mediaSession.setActionHandler('play', () => this.resume())
|
||||
navigator.mediaSession.setActionHandler('pause', () => this.pause())
|
||||
navigator.mediaSession.setActionHandler('previoustrack', () => this.playPrev())
|
||||
navigator.mediaSession.setActionHandler('nexttrack', () => this.playNext())
|
||||
}
|
||||
|
||||
private listenToMediaEvents (mediaElement: HTMLMediaElement) {
|
||||
mediaElement.addEventListener('error', () => this.playNext(), true)
|
||||
|
||||
mediaElement.addEventListener('ended', () => {
|
||||
if (commonStore.state.use_last_fm && userStore.current.preferences!.lastfm_session_key) {
|
||||
songStore.scrobble(queueStore.current!)
|
||||
}
|
||||
|
||||
preferences.repeatMode === 'REPEAT_ONE' ? this.restart() : this.playNext()
|
||||
})
|
||||
|
||||
let timeUpdateHandler = () => {
|
||||
const currentSong = queueStore.current
|
||||
|
||||
if (!currentSong) return
|
||||
|
||||
if (!currentSong.play_count_registered && !this.isTranscoding) {
|
||||
// if we've passed 25% of the song, it's safe to say the song has been "played".
|
||||
// Refer to https://github.com/koel/koel/issues/1087
|
||||
if (!mediaElement.duration || mediaElement.currentTime * 4 >= mediaElement.duration) {
|
||||
this.registerPlay(currentSong)
|
||||
}
|
||||
}
|
||||
|
||||
const nextSong = queueStore.next
|
||||
|
||||
if (!nextSong || nextSong.preloaded || this.isTranscoding) {
|
||||
return
|
||||
}
|
||||
|
||||
if (mediaElement.duration && mediaElement.currentTime + PRELOAD_BUFFER > mediaElement.duration) {
|
||||
this.preload(nextSong)
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
timeUpdateHandler = throttle(timeUpdateHandler, 1000)
|
||||
}
|
||||
|
||||
mediaElement.addEventListener('timeupdate', timeUpdateHandler)
|
||||
}
|
||||
|
||||
public registerPlay (song: Song) {
|
||||
recentlyPlayedStore.add(song)
|
||||
songStore.registerPlay(song)
|
||||
|
@ -383,16 +328,65 @@ class PlaybackService {
|
|||
|
||||
await this.stop()
|
||||
queueStore.replaceQueueWith(songs)
|
||||
|
||||
// Wait for the DOM to complete updating and play the first song in the queue.
|
||||
await nextTick()
|
||||
router.go('queue')
|
||||
await this.play(queueStore.first)
|
||||
}
|
||||
|
||||
public async playFirstInQueue () {
|
||||
queueStore.all.length && await this.play(queueStore.first)
|
||||
}
|
||||
|
||||
private setMediaSessionActionHandlers () {
|
||||
if (!navigator.mediaSession) {
|
||||
return
|
||||
}
|
||||
|
||||
navigator.mediaSession.setActionHandler('play', () => this.resume())
|
||||
navigator.mediaSession.setActionHandler('pause', () => this.pause())
|
||||
navigator.mediaSession.setActionHandler('previoustrack', () => this.playPrev())
|
||||
navigator.mediaSession.setActionHandler('nexttrack', () => this.playNext())
|
||||
}
|
||||
|
||||
private listenToMediaEvents (mediaElement: HTMLMediaElement) {
|
||||
mediaElement.addEventListener('error', () => this.playNext(), true)
|
||||
|
||||
mediaElement.addEventListener('ended', () => {
|
||||
if (commonStore.state.use_last_fm && userStore.current.preferences!.lastfm_session_key) {
|
||||
songStore.scrobble(queueStore.current!)
|
||||
}
|
||||
|
||||
preferences.repeatMode === 'REPEAT_ONE' ? this.restart() : this.playNext()
|
||||
})
|
||||
|
||||
let timeUpdateHandler = () => {
|
||||
const currentSong = queueStore.current
|
||||
|
||||
if (!currentSong) return
|
||||
|
||||
if (!currentSong.play_count_registered && !this.isTranscoding) {
|
||||
// if we've passed 25% of the song, it's safe to say the song has been "played".
|
||||
// Refer to https://github.com/koel/koel/issues/1087
|
||||
if (!mediaElement.duration || mediaElement.currentTime * 4 >= mediaElement.duration) {
|
||||
this.registerPlay(currentSong)
|
||||
}
|
||||
}
|
||||
|
||||
const nextSong = queueStore.next
|
||||
|
||||
if (!nextSong || nextSong.preloaded || this.isTranscoding) {
|
||||
return
|
||||
}
|
||||
|
||||
if (mediaElement.duration && mediaElement.currentTime + PRELOAD_BUFFER > mediaElement.duration) {
|
||||
this.preload(nextSong)
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
timeUpdateHandler = throttle(timeUpdateHandler, 1000)
|
||||
}
|
||||
|
||||
mediaElement.addEventListener('timeupdate', timeUpdateHandler)
|
||||
}
|
||||
}
|
||||
|
||||
export const playbackService = new PlaybackService()
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { without } from 'lodash'
|
||||
import { reactive } from 'vue'
|
||||
import { UploadFile } from '@/config'
|
||||
import { http } from '@/services'
|
||||
import { albumStore, overviewStore, songStore } from '@/stores'
|
||||
import { logger } from '@/utils'
|
||||
|
@ -10,6 +9,22 @@ interface UploadResult {
|
|||
album: Album
|
||||
}
|
||||
|
||||
export type UploadStatus =
|
||||
| 'Ready'
|
||||
| 'Uploading'
|
||||
| 'Uploaded'
|
||||
| 'Canceled'
|
||||
| 'Errored'
|
||||
|
||||
export interface UploadFile {
|
||||
id: string
|
||||
file: File
|
||||
status: UploadStatus
|
||||
name: string
|
||||
progress: number
|
||||
message?: string
|
||||
}
|
||||
|
||||
export const uploadService = {
|
||||
state: reactive({
|
||||
files: [] as UploadFile[]
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { eventBus } from '@/utils'
|
||||
import router from '@/router'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { expect, it } from 'vitest'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
|
@ -18,12 +17,10 @@ new class extends UnitTestCase {
|
|||
})
|
||||
|
||||
const emitMock = this.mock(eventBus, 'emit')
|
||||
const goMock = this.mock(router, 'go')
|
||||
|
||||
youTubeService.play(video)
|
||||
|
||||
expect(emitMock).toHaveBeenCalledWith('PLAY_YOUTUBE_VIDEO', { id: 'foo', title: 'Bar' })
|
||||
expect(goMock).toHaveBeenCalledWith('youtube')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { cache, http } from '@/services'
|
||||
import { eventBus } from '@/utils'
|
||||
import router from '@/router'
|
||||
|
||||
interface YouTubeSearchResult {
|
||||
nextPageToken: string
|
||||
|
@ -22,7 +21,5 @@ export const youTubeService = {
|
|||
id: video.id.videoId,
|
||||
title: video.snippet.title
|
||||
})
|
||||
|
||||
router.go('youtube')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,8 +168,9 @@ export const songStore = {
|
|||
return await this.cacheable(['artist.songs', id], http.get<Song[]>(`artists/${id}/songs`))
|
||||
},
|
||||
|
||||
async fetchForPlaylist (playlist: Playlist) {
|
||||
return await this.cacheable(['playlist.songs', playlist.id], http.get<Song[]>(`playlists/${playlist.id}/songs`))
|
||||
async fetchForPlaylist (playlist: Playlist | number) {
|
||||
const id = typeof playlist === 'number' ? playlist : playlist.id
|
||||
return await this.cacheable(['playlist.songs', id], http.get<Song[]>(`playlists/${id}/songs`))
|
||||
},
|
||||
|
||||
async fetchForPlaylistFolder (folder: PlaylistFolder) {
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
import { DeepReadonly, InjectionKey, Ref } from 'vue'
|
||||
import DialogBox from '@/components/ui/DialogBox.vue'
|
||||
import MessageToaster from '@/components/ui/MessageToaster.vue'
|
||||
import Router from '@/router'
|
||||
|
||||
export interface ReadonlyInjectionKey<T> extends InjectionKey<[Readonly<T> | DeepReadonly<T>, Closure]> {
|
||||
}
|
||||
|
||||
export const RouterKey: InjectionKey<Router> = Symbol('Router')
|
||||
|
||||
export const DialogBoxKey: InjectionKey<Ref<InstanceType<typeof DialogBox>>> = Symbol('DialogBox')
|
||||
export const MessageToasterKey: InjectionKey<Ref<InstanceType<typeof MessageToaster>>> = Symbol('MessageToaster')
|
||||
|
||||
export const SongListTypeKey: ReadonlyInjectionKey<SongListType> = Symbol('SongListType')
|
||||
export const SongsKey: ReadonlyInjectionKey<Ref<Song[]>> = Symbol('Songs')
|
||||
export const SongsKey: ReadonlyInjectionKey<Ref<Song[]>> | InjectionKey<Ref<Song[]>> = Symbol('Songs')
|
||||
export const SelectedSongsKey: ReadonlyInjectionKey<Ref<Song[]>> = Symbol('SelectedSongs')
|
||||
export const SongListConfigKey: ReadonlyInjectionKey<Partial<SongListConfig>> = Symbol('SongListConfig')
|
||||
export const SongListSortFieldKey: ReadonlyInjectionKey<Ref<SongListSortField>> = Symbol('SongListSortField')
|
||||
|
@ -20,5 +23,3 @@ export const EditSongFormInitialTabKey: ReadonlyInjectionKey<Ref<EditSongFormTab
|
|||
export const PlaylistKey: ReadonlyInjectionKey<Ref<Playlist>> = Symbol('Playlist')
|
||||
export const PlaylistFolderKey: ReadonlyInjectionKey<Ref<PlaylistFolder>> = Symbol('PlaylistFolder')
|
||||
export const UserKey: ReadonlyInjectionKey<Ref<User>> = Symbol('User')
|
||||
|
||||
export const ActiveScreenKey: InjectionKey<Ref<ScreenName>> = Symbol('ActiveScreen')
|
||||
|
|
4
resources/assets/js/types.d.ts
vendored
4
resources/assets/js/types.d.ts
vendored
|
@ -31,10 +31,6 @@ interface Plyr {
|
|||
setVolume (volume: number): void
|
||||
}
|
||||
|
||||
declare module 'plyr' {
|
||||
function setup (el: string | HTMLMediaElement | HTMLMediaElement[], options: Record<string, any>): Plyr[]
|
||||
}
|
||||
|
||||
declare module 'ismobilejs' {
|
||||
let apple: { device: boolean }
|
||||
let any: boolean
|
||||
|
|
|
@ -4,14 +4,6 @@ import defaultCover from '@/../img/covers/default.svg'
|
|||
|
||||
export { defaultCover }
|
||||
|
||||
/**
|
||||
* Load (display) a main panel (view).
|
||||
*
|
||||
* @param view
|
||||
* @param {...*} args Extra data to attach to the view.
|
||||
*/
|
||||
export const loadMainView = (view: ScreenName, ...args: any[]) => eventBus.emit('ACTIVATE_SCREEN', view, ...args)
|
||||
|
||||
/**
|
||||
* Force reloading window regardless of "Confirm before reload" setting.
|
||||
* This is handy for certain cases, for example Last.fm connect/disconnect.
|
||||
|
|
|
@ -20,7 +20,7 @@ export const eventBus = {
|
|||
this.all.has(name) ? this.all.get(name).push(callback) : this.all.set(name, [callback])
|
||||
},
|
||||
|
||||
emit (name: EventName, ...args: any) {
|
||||
emit (name: EventName, ...args: any[]) {
|
||||
if (this.all.has(name)) {
|
||||
this.all.get(name).forEach((cb: Closure) => cb(...args))
|
||||
} else {
|
||||
|
|
|
@ -161,7 +161,7 @@ export default (container: HTMLElement) => {
|
|||
Sketch.create({
|
||||
container,
|
||||
particles: [],
|
||||
setup () {
|
||||
init () {
|
||||
// generate some particles
|
||||
for (let i = 0; i < NUM_PARTICLES; i++) {
|
||||
const particle = new Particle(random(this.width), random(this.height))
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
<meta name="description" content="{{ config('app.tagline') }}">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
|
||||
<meta name="theme-color" content="#282828">
|
||||
|
|
|
@ -29,9 +29,6 @@ export default defineConfig({
|
|||
}
|
||||
}
|
||||
},
|
||||
define: {
|
||||
KOEL_ENV: '""'
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
setupFiles: path.resolve(__dirname, './resources/assets/js/__tests__/setup.ts')
|
||||
|
|
314
yarn.lock
314
yarn.lock
|
@ -953,6 +953,11 @@
|
|||
debug "^3.1.0"
|
||||
lodash.once "^4.1.1"
|
||||
|
||||
"@esbuild/linux-loong64@0.14.54":
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028"
|
||||
integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==
|
||||
|
||||
"@eslint/eslintrc@^1.2.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.2.tgz#4989b9e8c0216747ee7cca314ae73791bb281aae"
|
||||
|
@ -1154,9 +1159,9 @@
|
|||
"@types/chai" "*"
|
||||
|
||||
"@types/chai@*", "@types/chai@^4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.1.tgz#e2c6e73e0bdeb2521d00756d099218e9f5d90a04"
|
||||
integrity sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ==
|
||||
version "4.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.3.tgz#3c90752792660c4b562ad73b3fbd68bf3bc7ae07"
|
||||
integrity sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==
|
||||
|
||||
"@types/color-name@^1.1.1":
|
||||
version "1.1.1"
|
||||
|
@ -1367,9 +1372,9 @@
|
|||
eslint-visitor-keys "^3.0.0"
|
||||
|
||||
"@vitejs/plugin-vue@^2.3.1":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-2.3.1.tgz#5f286b8d3515381c6d5c8fa8eee5e6335f727e14"
|
||||
integrity sha512-YNzBt8+jt6bSwpt7LP890U1UcTOIZZxfpE5WOJ638PNxSEKOqAi0+FSKS0nVeukfdZ0Ai/H7AFd6k3hayfGZqQ==
|
||||
version "2.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-2.3.4.tgz#966a6279060eb2d9d1a02ea1a331af071afdcf9e"
|
||||
integrity sha512-IfFNbtkbIm36O9KB8QodlwwYvTEsJb4Lll4c2IwB3VHc2gie2mSPtSzL0eYay7X2jd/2WX02FjSGTWR6OPr/zg==
|
||||
|
||||
"@vue/compiler-core@3.2.32":
|
||||
version "3.2.32"
|
||||
|
@ -1461,11 +1466,16 @@
|
|||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.32.tgz#1ca0c3b8c03a5e24129156e171df736b2c1d645f"
|
||||
integrity sha512-bjcixPErUsAnTQRQX4Z5IQnICYjIfNCyCl8p29v1M6kfVzvwOICPw+dz48nNuWlTOOx2RHhzHdazJibE8GSnsw==
|
||||
|
||||
"@vue/test-utils@^2.0.0-rc.18", "@vue/test-utils@^2.0.0-rc.21":
|
||||
"@vue/test-utils@^2.0.0-rc.18":
|
||||
version "2.0.0-rc.21"
|
||||
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.0.0-rc.21.tgz#9ebb0029bafa94ee55e90a6b4eab6d1e7b7a3152"
|
||||
integrity sha512-wIJR4e/jISBKVKfiod3DV32BlDsoD744WVCuCaGtaSKvhvTL9gI5vl2AYSy00V51YaM8dCOFi3zcpCON8G1WqA==
|
||||
|
||||
"@vue/test-utils@^2.0.0-rc.21":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.1.0.tgz#c2f646aa2d6ac779f79a83f18c5b82fc40952bfd"
|
||||
integrity sha512-U4AxAD/tKJ3ajxYew1gkfEotpr96DE/gLXpbl+nPbsNRqGBfQZZA7YhwGoQNDPgon56v+IGZDrYq7pe3GDl9aw==
|
||||
|
||||
"@webassemblyjs/ast@1.11.1":
|
||||
version "1.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
|
||||
|
@ -2130,7 +2140,7 @@ chalk@^4.0.0, chalk@^4.1.0:
|
|||
check-error@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
|
||||
integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=
|
||||
integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==
|
||||
|
||||
check-more-types@2.24.0, check-more-types@^2.24.0:
|
||||
version "2.24.0"
|
||||
|
@ -2870,131 +2880,132 @@ es6-symbol@^3.1.1, es6-symbol@~3.1.1:
|
|||
d "1"
|
||||
es5-ext "~0.10.14"
|
||||
|
||||
esbuild-android-64@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.38.tgz#5b94a1306df31d55055f64a62ff6b763a47b7f64"
|
||||
integrity sha512-aRFxR3scRKkbmNuGAK+Gee3+yFxkTJO/cx83Dkyzo4CnQl/2zVSurtG6+G86EQIZ+w+VYngVyK7P3HyTBKu3nw==
|
||||
esbuild-android-64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be"
|
||||
integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==
|
||||
|
||||
esbuild-android-arm64@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.38.tgz#78acc80773d16007de5219ccce544c036abd50b8"
|
||||
integrity sha512-L2NgQRWuHFI89IIZIlpAcINy9FvBk6xFVZ7xGdOwIm8VyhX1vNCEqUJO3DPSSy945Gzdg98cxtNt8Grv1CsyhA==
|
||||
esbuild-android-arm64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771"
|
||||
integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==
|
||||
|
||||
esbuild-darwin-64@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.38.tgz#e02b1291f629ebdc2aa46fabfacc9aa28ff6aa46"
|
||||
integrity sha512-5JJvgXkX87Pd1Og0u/NJuO7TSqAikAcQQ74gyJ87bqWRVeouky84ICoV4sN6VV53aTW+NE87qLdGY4QA2S7KNA==
|
||||
esbuild-darwin-64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25"
|
||||
integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==
|
||||
|
||||
esbuild-darwin-arm64@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.38.tgz#01eb6650ec010b18c990e443a6abcca1d71290a9"
|
||||
integrity sha512-eqF+OejMI3mC5Dlo9Kdq/Ilbki9sQBw3QlHW3wjLmsLh+quNfHmGMp3Ly1eWm981iGBMdbtSS9+LRvR2T8B3eQ==
|
||||
esbuild-darwin-arm64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz#3f7cdb78888ee05e488d250a2bdaab1fa671bf73"
|
||||
integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==
|
||||
|
||||
esbuild-freebsd-64@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.38.tgz#790b8786729d4aac7be17648f9ea8e0e16475b5e"
|
||||
integrity sha512-epnPbhZUt93xV5cgeY36ZxPXDsQeO55DppzsIgWM8vgiG/Rz+qYDLmh5ts3e+Ln1wA9dQ+nZmVHw+RjaW3I5Ig==
|
||||
esbuild-freebsd-64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d"
|
||||
integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==
|
||||
|
||||
esbuild-freebsd-arm64@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.38.tgz#b66340ab28c09c1098e6d9d8ff656db47d7211e6"
|
||||
integrity sha512-/9icXUYJWherhk+y5fjPI5yNUdFPtXHQlwP7/K/zg8t8lQdHVj20SqU9/udQmeUo5pDFHMYzcEFfJqgOVeKNNQ==
|
||||
esbuild-freebsd-arm64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48"
|
||||
integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==
|
||||
|
||||
esbuild-linux-32@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.38.tgz#7927f950986fd39f0ff319e92839455912b67f70"
|
||||
integrity sha512-QfgfeNHRFvr2XeHFzP8kOZVnal3QvST3A0cgq32ZrHjSMFTdgXhMhmWdKzRXP/PKcfv3e2OW9tT9PpcjNvaq6g==
|
||||
esbuild-linux-32@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5"
|
||||
integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==
|
||||
|
||||
esbuild-linux-64@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.38.tgz#4893d07b229d9cfe34a2b3ce586399e73c3ac519"
|
||||
integrity sha512-uuZHNmqcs+Bj1qiW9k/HZU3FtIHmYiuxZ/6Aa+/KHb/pFKr7R3aVqvxlAudYI9Fw3St0VCPfv7QBpUITSmBR1Q==
|
||||
esbuild-linux-64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz#de5fdba1c95666cf72369f52b40b03be71226652"
|
||||
integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==
|
||||
|
||||
esbuild-linux-arm64@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.38.tgz#8442402e37d0b8ae946ac616784d9c1a2041056a"
|
||||
integrity sha512-HlMGZTEsBrXrivr64eZ/EO0NQM8H8DuSENRok9d+Jtvq8hOLzrxfsAT9U94K3KOGk2XgCmkaI2KD8hX7F97lvA==
|
||||
esbuild-linux-arm64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b"
|
||||
integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==
|
||||
|
||||
esbuild-linux-arm@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.38.tgz#d5dbf32d38b7f79be0ec6b5fb2f9251fd9066986"
|
||||
integrity sha512-FiFvQe8J3VKTDXG01JbvoVRXQ0x6UZwyrU4IaLBZeq39Bsbatd94Fuc3F1RGqPF5RbIWW7RvkVQjn79ejzysnA==
|
||||
esbuild-linux-arm@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59"
|
||||
integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==
|
||||
|
||||
esbuild-linux-mips64le@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.38.tgz#95081e42f698bbe35d8ccee0e3a237594b337eb5"
|
||||
integrity sha512-qd1dLf2v7QBiI5wwfil9j0HG/5YMFBAmMVmdeokbNAMbcg49p25t6IlJFXAeLzogv1AvgaXRXvgFNhScYEUXGQ==
|
||||
esbuild-linux-mips64le@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34"
|
||||
integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==
|
||||
|
||||
esbuild-linux-ppc64le@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.38.tgz#dceb0a1b186f5df679618882a7990bd422089b47"
|
||||
integrity sha512-mnbEm7o69gTl60jSuK+nn+pRsRHGtDPfzhrqEUXyCl7CTOCLtWN2bhK8bgsdp6J/2NyS/wHBjs1x8aBWwP2X9Q==
|
||||
esbuild-linux-ppc64le@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e"
|
||||
integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==
|
||||
|
||||
esbuild-linux-riscv64@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.38.tgz#61fb8edb75f475f9208c4a93ab2bfab63821afd2"
|
||||
integrity sha512-+p6YKYbuV72uikChRk14FSyNJZ4WfYkffj6Af0/Tw63/6TJX6TnIKE+6D3xtEc7DeDth1fjUOEqm+ApKFXbbVQ==
|
||||
esbuild-linux-riscv64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8"
|
||||
integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==
|
||||
|
||||
esbuild-linux-s390x@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.38.tgz#34c7126a4937406bf6a5e69100185fd702d12fe0"
|
||||
integrity sha512-0zUsiDkGJiMHxBQ7JDU8jbaanUY975CdOW1YDrurjrM0vWHfjv9tLQsW9GSyEb/heSK1L5gaweRjzfUVBFoybQ==
|
||||
esbuild-linux-s390x@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6"
|
||||
integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==
|
||||
|
||||
esbuild-netbsd-64@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.38.tgz#322ea9937d9e529183ee281c7996b93eb38a5d95"
|
||||
integrity sha512-cljBAApVwkpnJZfnRVThpRBGzCi+a+V9Ofb1fVkKhtrPLDYlHLrSYGtmnoTVWDQdU516qYI8+wOgcGZ4XIZh0Q==
|
||||
esbuild-netbsd-64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81"
|
||||
integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==
|
||||
|
||||
esbuild-openbsd-64@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.38.tgz#1ca29bb7a2bf09592dcc26afdb45108f08a2cdbd"
|
||||
integrity sha512-CDswYr2PWPGEPpLDUO50mL3WO/07EMjnZDNKpmaxUPsrW+kVM3LoAqr/CE8UbzugpEiflYqJsGPLirThRB18IQ==
|
||||
esbuild-openbsd-64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b"
|
||||
integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==
|
||||
|
||||
esbuild-sunos-64@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.38.tgz#c9446f7d8ebf45093e7bb0e7045506a88540019b"
|
||||
integrity sha512-2mfIoYW58gKcC3bck0j7lD3RZkqYA7MmujFYmSn9l6TiIcAMpuEvqksO+ntBgbLep/eyjpgdplF7b+4T9VJGOA==
|
||||
esbuild-sunos-64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da"
|
||||
integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==
|
||||
|
||||
esbuild-windows-32@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.38.tgz#f8e9b4602fd0ccbd48e5c8d117ec0ba4040f2ad1"
|
||||
integrity sha512-L2BmEeFZATAvU+FJzJiRLFUP+d9RHN+QXpgaOrs2klshoAm1AE6Us4X6fS9k33Uy5SzScn2TpcgecbqJza1Hjw==
|
||||
esbuild-windows-32@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31"
|
||||
integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==
|
||||
|
||||
esbuild-windows-64@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.38.tgz#280f58e69f78535f470905ce3e43db1746518107"
|
||||
integrity sha512-Khy4wVmebnzue8aeSXLC+6clo/hRYeNIm0DyikoEqX+3w3rcvrhzpoix0S+MF9vzh6JFskkIGD7Zx47ODJNyCw==
|
||||
esbuild-windows-64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4"
|
||||
integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==
|
||||
|
||||
esbuild-windows-arm64@0.14.38:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.38.tgz#d97e9ac0f95a4c236d9173fa9f86c983d6a53f54"
|
||||
integrity sha512-k3FGCNmHBkqdJXuJszdWciAH77PukEyDsdIryEHn9cKLQFxzhT39dSumeTuggaQcXY57UlmLGIkklWZo2qzHpw==
|
||||
esbuild-windows-arm64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982"
|
||||
integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==
|
||||
|
||||
esbuild@^0.14.27:
|
||||
version "0.14.38"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.38.tgz#99526b778cd9f35532955e26e1709a16cca2fb30"
|
||||
integrity sha512-12fzJ0fsm7gVZX1YQ1InkOE5f9Tl7cgf6JPYXRJtPIoE0zkWAbHdPHVPPaLi9tYAcEBqheGzqLn/3RdTOyBfcA==
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2"
|
||||
integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==
|
||||
optionalDependencies:
|
||||
esbuild-android-64 "0.14.38"
|
||||
esbuild-android-arm64 "0.14.38"
|
||||
esbuild-darwin-64 "0.14.38"
|
||||
esbuild-darwin-arm64 "0.14.38"
|
||||
esbuild-freebsd-64 "0.14.38"
|
||||
esbuild-freebsd-arm64 "0.14.38"
|
||||
esbuild-linux-32 "0.14.38"
|
||||
esbuild-linux-64 "0.14.38"
|
||||
esbuild-linux-arm "0.14.38"
|
||||
esbuild-linux-arm64 "0.14.38"
|
||||
esbuild-linux-mips64le "0.14.38"
|
||||
esbuild-linux-ppc64le "0.14.38"
|
||||
esbuild-linux-riscv64 "0.14.38"
|
||||
esbuild-linux-s390x "0.14.38"
|
||||
esbuild-netbsd-64 "0.14.38"
|
||||
esbuild-openbsd-64 "0.14.38"
|
||||
esbuild-sunos-64 "0.14.38"
|
||||
esbuild-windows-32 "0.14.38"
|
||||
esbuild-windows-64 "0.14.38"
|
||||
esbuild-windows-arm64 "0.14.38"
|
||||
"@esbuild/linux-loong64" "0.14.54"
|
||||
esbuild-android-64 "0.14.54"
|
||||
esbuild-android-arm64 "0.14.54"
|
||||
esbuild-darwin-64 "0.14.54"
|
||||
esbuild-darwin-arm64 "0.14.54"
|
||||
esbuild-freebsd-64 "0.14.54"
|
||||
esbuild-freebsd-arm64 "0.14.54"
|
||||
esbuild-linux-32 "0.14.54"
|
||||
esbuild-linux-64 "0.14.54"
|
||||
esbuild-linux-arm "0.14.54"
|
||||
esbuild-linux-arm64 "0.14.54"
|
||||
esbuild-linux-mips64le "0.14.54"
|
||||
esbuild-linux-ppc64le "0.14.54"
|
||||
esbuild-linux-riscv64 "0.14.54"
|
||||
esbuild-linux-s390x "0.14.54"
|
||||
esbuild-netbsd-64 "0.14.54"
|
||||
esbuild-openbsd-64 "0.14.54"
|
||||
esbuild-sunos-64 "0.14.54"
|
||||
esbuild-windows-32 "0.14.54"
|
||||
esbuild-windows-64 "0.14.54"
|
||||
esbuild-windows-arm64 "0.14.54"
|
||||
|
||||
escalade@^3.1.1:
|
||||
version "3.1.1"
|
||||
|
@ -3566,7 +3577,7 @@ gensync@^1.0.0-beta.2:
|
|||
get-func-name@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
|
||||
integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=
|
||||
integrity sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==
|
||||
|
||||
get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
|
||||
version "1.1.1"
|
||||
|
@ -3983,10 +3994,10 @@ is-ci@^3.0.0:
|
|||
dependencies:
|
||||
ci-info "^3.2.0"
|
||||
|
||||
is-core-module@^2.8.1:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211"
|
||||
integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==
|
||||
is-core-module@^2.8.1, is-core-module@^2.9.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed"
|
||||
integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==
|
||||
dependencies:
|
||||
has "^1.0.3"
|
||||
|
||||
|
@ -4463,9 +4474,9 @@ loader-utils@^2.0.0:
|
|||
json5 "^2.1.2"
|
||||
|
||||
local-pkg@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.4.1.tgz#e7b0d7aa0b9c498a1110a5ac5b00ba66ef38cfff"
|
||||
integrity sha512-lL87ytIGP2FU5PWwNDo0w3WhIo2gopIAxPg9RxDYF7m4rr5ahuZxP22xnJHIvaLTe4Z9P6uKKY2UHiwyB4pcrw==
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.4.2.tgz#13107310b77e74a0e513147a131a2ba288176c2f"
|
||||
integrity sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==
|
||||
|
||||
local-storage@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
@ -5319,9 +5330,9 @@ postcss@^8.1.10, postcss@^8.4.12:
|
|||
source-map-js "^1.0.2"
|
||||
|
||||
postcss@^8.4.13:
|
||||
version "8.4.14"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf"
|
||||
integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
|
||||
version "8.4.17"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.17.tgz#f87863ec7cd353f81f7ab2dec5d67d861bbb1be5"
|
||||
integrity sha512-UNxNOLQydcOFi41yHNMcKRZ39NeXlr8AxGuZJsdub8vIb12fHzcq37DTU/QtbI6WLxNg2gF9Z+8qtRwTj1UI1Q==
|
||||
dependencies:
|
||||
nanoid "^3.3.4"
|
||||
picocolors "^1.0.0"
|
||||
|
@ -5564,7 +5575,7 @@ resolve-url@^0.2.1:
|
|||
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
|
||||
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
|
||||
|
||||
resolve@^1.10.1, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.0:
|
||||
resolve@^1.10.1, resolve@^1.14.2, resolve@^1.20.0:
|
||||
version "1.22.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198"
|
||||
integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==
|
||||
|
@ -5573,6 +5584,15 @@ resolve@^1.10.1, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.0:
|
|||
path-parse "^1.0.7"
|
||||
supports-preserve-symlinks-flag "^1.0.0"
|
||||
|
||||
resolve@^1.22.0:
|
||||
version "1.22.1"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
|
||||
integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
|
||||
dependencies:
|
||||
is-core-module "^2.9.0"
|
||||
path-parse "^1.0.7"
|
||||
supports-preserve-symlinks-flag "^1.0.0"
|
||||
|
||||
restore-cursor@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
|
||||
|
@ -5611,10 +5631,10 @@ rimraf@^3.0.0, rimraf@^3.0.2:
|
|||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rollup@^2.59.0:
|
||||
version "2.71.1"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.71.1.tgz#82b259af7733dfd1224a8171013aaaad02971a22"
|
||||
integrity sha512-lMZk3XfUBGjrrZQpvPSoXcZSfKcJ2Bgn+Z0L1MoW2V8Wh7BVM+LOBJTPo16yul2MwL59cXedzW1ruq3rCjSRgw==
|
||||
"rollup@>=2.59.0 <2.78.0":
|
||||
version "2.77.3"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.77.3.tgz#8f00418d3a2740036e15deb653bed1a90ee0cc12"
|
||||
integrity sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
|
@ -6120,15 +6140,15 @@ through@2, through@^2.3.8, through@~2.3, through@~2.3.1:
|
|||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
|
||||
|
||||
tinypool@^0.1.2:
|
||||
tinypool@^0.1.3:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.1.3.tgz#b5570b364a1775fd403de5e7660b325308fee26b"
|
||||
integrity sha512-2IfcQh7CP46XGWGGbdyO4pjcKqsmVqFAPcXfPxcPXmOWt9cYkTP9HcDmGgsfijYoAEc4z9qcpM/BaBz46Y9/CQ==
|
||||
|
||||
tinyspy@^0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-0.3.2.tgz#2f95cb14c38089ca690385f339781cd35faae566"
|
||||
integrity sha512-2+40EP4D3sFYy42UkgkFFB+kiX2Tg3URG/lVvAZFfLxgGpnWl5qQJuBw1gaLttq8UOS+2p3C0WrhJnQigLTT2Q==
|
||||
version "0.3.3"
|
||||
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-0.3.3.tgz#8b57f8aec7fe1bf583a3a49cb9ab30c742f69237"
|
||||
integrity sha512-gRiUR8fuhUf0W9lzojPf1N1euJYA30ISebSfgca8z76FOvXtVXqd5ojEIaKLWbDQhAaC3ibxZIjqbyi4ybjcTw==
|
||||
|
||||
tmp@~0.2.1:
|
||||
version "0.2.1"
|
||||
|
@ -6389,42 +6409,30 @@ verror@1.10.0:
|
|||
core-util-is "1.0.2"
|
||||
extsprintf "^1.2.0"
|
||||
|
||||
vite@^2.9.13:
|
||||
version "2.9.13"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.13.tgz#859cb5d4c316c0d8c6ec9866045c0f7858ca6abc"
|
||||
integrity sha512-AsOBAaT0AD7Mhe8DuK+/kE4aWYFMx/i0ZNi98hJclxb4e0OhQcZYUrvLjIaQ8e59Ui7txcvKMiJC1yftqpQoDw==
|
||||
vite@^2.9.13, vite@^2.9.7:
|
||||
version "2.9.15"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.15.tgz#2858dd5b2be26aa394a283e62324281892546f0b"
|
||||
integrity sha512-fzMt2jK4vQ3yK56te3Kqpkaeq9DkcZfBbzHwYpobasvgYmP2SoAr6Aic05CsB4CzCZbsDv4sujX3pkEGhLabVQ==
|
||||
dependencies:
|
||||
esbuild "^0.14.27"
|
||||
postcss "^8.4.13"
|
||||
resolve "^1.22.0"
|
||||
rollup "^2.59.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
vite@^2.9.5:
|
||||
version "2.9.6"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.6.tgz#29f1b33193b0de9e155d67ba0dd097501c3c3281"
|
||||
integrity sha512-3IffdrByHW95Yjv0a13TQOQfJs7L5dVlSPuTt432XLbRMriWbThqJN2k/IS6kXn5WY4xBLhK9XoaWay1B8VzUw==
|
||||
dependencies:
|
||||
esbuild "^0.14.27"
|
||||
postcss "^8.4.12"
|
||||
resolve "^1.22.0"
|
||||
rollup "^2.59.0"
|
||||
rollup ">=2.59.0 <2.78.0"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
vitest@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.10.0.tgz#ab8930194f2a8943c533cca735cba2bca705d5bc"
|
||||
integrity sha512-8UXemUg9CA4QYppDTsDV76nH0e1p6C8lV9q+o9i0qMSK9AQ7vA2sjoxtkDP0M+pwNmc3ZGYetBXgSJx0M1D/gg==
|
||||
version "0.10.5"
|
||||
resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.10.5.tgz#f2cd782a8f72889d4324a809101ada9e9f424a67"
|
||||
integrity sha512-4qXdNbHwAd9YcsztJoVMWUQGcMATVlY9Xd95I3KQ2JJwDLTL97f/jgfGRotqptvNxdlmme5TBY0Gv+l6+JSYvA==
|
||||
dependencies:
|
||||
"@types/chai" "^4.3.1"
|
||||
"@types/chai-subset" "^1.3.3"
|
||||
chai "^4.3.6"
|
||||
local-pkg "^0.4.1"
|
||||
tinypool "^0.1.2"
|
||||
tinypool "^0.1.3"
|
||||
tinyspy "^0.3.2"
|
||||
vite "^2.9.5"
|
||||
vite "^2.9.7"
|
||||
|
||||
vue-eslint-parser@^8.0.1:
|
||||
version "8.3.0"
|
||||
|
|
Loading…
Reference in a new issue