mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
chore: code style and some minor fixes
This commit is contained in:
parent
e3c7d51ad5
commit
4b8ae1a78e
146 changed files with 642 additions and 634 deletions
|
@ -45,6 +45,8 @@
|
|||
"vue/valid-v-on": 0,
|
||||
"vue/no-side-effects-in-computed-properties": 0,
|
||||
"vue/max-attributes-per-line": 0,
|
||||
"vue/no-v-html": 0
|
||||
"vue/no-v-html": 0,
|
||||
"vue/singleline-html-element-content-newline": 0,
|
||||
"vue/multi-word-component-names": 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
<template>
|
||||
<Overlay ref="overlay"/>
|
||||
<DialogBox ref="dialog"/>
|
||||
<MessageToaster ref="toaster"/>
|
||||
<GlobalEventListeners/>
|
||||
<OfflineNotification v-if="offline"/>
|
||||
<Overlay ref="overlay" />
|
||||
<DialogBox ref="dialog" />
|
||||
<MessageToaster ref="toaster" />
|
||||
<GlobalEventListeners />
|
||||
<OfflineNotification v-if="offline" />
|
||||
|
||||
<div v-if="authenticated" id="main" @dragend="onDragEnd" @dragover="onDragOver" @drop="onDrop">
|
||||
<Hotkeys/>
|
||||
<MainWrapper/>
|
||||
<AppFooter/>
|
||||
<SupportKoel/>
|
||||
<SongContextMenu/>
|
||||
<AlbumContextMenu/>
|
||||
<ArtistContextMenu/>
|
||||
<PlaylistContextMenu/>
|
||||
<PlaylistFolderContextMenu/>
|
||||
<CreateNewPlaylistContextMenu/>
|
||||
<DropZone v-show="showDropZone"/>
|
||||
<Hotkeys />
|
||||
<MainWrapper />
|
||||
<AppFooter />
|
||||
<SupportKoel />
|
||||
<SongContextMenu />
|
||||
<AlbumContextMenu />
|
||||
<ArtistContextMenu />
|
||||
<PlaylistContextMenu />
|
||||
<PlaylistFolderContextMenu />
|
||||
<CreateNewPlaylistContextMenu />
|
||||
<DropZone v-show="showDropZone" />
|
||||
</div>
|
||||
|
||||
<div v-else class="login-wrapper">
|
||||
<LoginForm @loggedin="onUserLoggedIn"/>
|
||||
<LoginForm @loggedin="onUserLoggedIn" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -92,7 +92,7 @@ onMounted(async () => {
|
|||
})
|
||||
|
||||
const init = async () => {
|
||||
overlay.value.show({ message: 'Just a little patience…' })
|
||||
overlay.value!.show({ message: 'Just a little patience…' })
|
||||
|
||||
try {
|
||||
await commonStore.init()
|
||||
|
@ -108,7 +108,7 @@ const init = async () => {
|
|||
})
|
||||
|
||||
await socketService.init() && socketListener.listen()
|
||||
overlay.value.hide()
|
||||
overlay.value!.hide()
|
||||
} catch (err) {
|
||||
authenticated.value = false
|
||||
throw err
|
||||
|
|
|
@ -7,6 +7,6 @@ export default (faker: Faker): AlbumInfo => ({
|
|||
summary: faker.lorem.sentence(),
|
||||
full: faker.lorem.sentences(4)
|
||||
},
|
||||
tracks: factory<AlbumTrack[]>('album-track', 8),
|
||||
tracks: factory<AlbumTrack>('album-track', 8),
|
||||
url: faker.internet.url()
|
||||
})
|
||||
|
|
|
@ -3,5 +3,5 @@ import factory from 'factoria'
|
|||
|
||||
export default (faker: Faker): SmartPlaylistRuleGroup => ({
|
||||
id: faker.datatype.number(),
|
||||
rules: factory<SmartPlaylistRule[]>('smart-playlist-rule', 3)
|
||||
rules: factory<SmartPlaylistRule>('smart-playlist-rule', 3)
|
||||
})
|
||||
|
|
|
@ -5,22 +5,22 @@ import MessageToaster from '@/components/ui/MessageToaster.vue'
|
|||
import DialogBox from '@/components/ui/DialogBox.vue'
|
||||
import Overlay from '@/components/ui/Overlay.vue'
|
||||
|
||||
export const MessageToasterStub: Ref<InstanceType<typeof MessageToaster>> = ref({
|
||||
export const MessageToasterStub = ref({
|
||||
info: noop,
|
||||
success: noop,
|
||||
warning: noop,
|
||||
error: noop
|
||||
})
|
||||
}) as unknown as Ref<InstanceType<typeof MessageToaster>>
|
||||
|
||||
export const DialogBoxStub: Ref<InstanceType<typeof DialogBox>> = ref({
|
||||
export const DialogBoxStub = ref({
|
||||
info: noop,
|
||||
success: noop,
|
||||
warning: noop,
|
||||
error: noop,
|
||||
confirm: noop
|
||||
})
|
||||
}) as unknown as Ref<InstanceType<typeof DialogBox>>
|
||||
|
||||
export const OverlayStub: Ref<InstanceType<typeof Overlay>> = ref({
|
||||
export const OverlayStub = ref({
|
||||
show: noop,
|
||||
hide: noop
|
||||
})
|
||||
}) as unknown as Ref<InstanceType<typeof Overlay>>
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
<a
|
||||
:title="`Shuffle all songs in the album ${album.name}`"
|
||||
class="shuffle-album"
|
||||
href
|
||||
role="button"
|
||||
@click.prevent="shuffle"
|
||||
>
|
||||
|
@ -28,7 +27,6 @@
|
|||
v-if="allowDownload"
|
||||
:title="`Download all songs in the album ${album.name}`"
|
||||
class="download-album"
|
||||
href
|
||||
role="button"
|
||||
@click.prevent="download"
|
||||
>
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
<template v-if="album">
|
||||
<li @click="play">Play All</li>
|
||||
<li @click="shuffle">Shuffle All</li>
|
||||
<li class="separator"></li>
|
||||
<li class="separator" />
|
||||
<li v-if="isStandardAlbum" @click="viewAlbumDetails">Go to Album</li>
|
||||
<li v-if="isStandardArtist" @click="viewArtistDetails">Go to Artist</li>
|
||||
<template v-if="isStandardAlbum && allowDownload">
|
||||
<li class="separator"></li>
|
||||
<li class="separator" />
|
||||
<li @click="download">Download</li>
|
||||
</template>
|
||||
</template>
|
||||
|
@ -22,7 +22,7 @@ import { useContextMenu, useRouter } from '@/composables'
|
|||
import { eventBus } from '@/utils'
|
||||
|
||||
const { go } = useRouter()
|
||||
const { context, base, ContextMenuBase, open, trigger } = useContextMenu()
|
||||
const { base, ContextMenuBase, open, trigger } = useContextMenu()
|
||||
|
||||
const album = ref<Album>()
|
||||
const allowDownload = toRef(commonStore.state, 'allow_download')
|
||||
|
@ -49,6 +49,6 @@ const download = () => trigger(() => downloadService.fromAlbum(album.value!))
|
|||
|
||||
eventBus.on('ALBUM_CONTEXT_MENU_REQUESTED', async (e, _album) => {
|
||||
album.value = _album
|
||||
await open(e.pageY, e.pageX, { album })
|
||||
await open(e.pageY, e.pageX)
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -3,24 +3,24 @@
|
|||
<h1 v-if="mode === 'aside'" class="name">
|
||||
<span>{{ album.name }}</span>
|
||||
<button :title="`Play all songs in ${album.name}`" class="control" type="button" @click.prevent="play">
|
||||
<icon :icon="faCirclePlay" size="xl"/>
|
||||
<icon :icon="faCirclePlay" size="xl" />
|
||||
</button>
|
||||
</h1>
|
||||
|
||||
<main>
|
||||
<AlbumThumbnail v-if="mode === 'aside'" :entity="album"/>
|
||||
<AlbumThumbnail v-if="mode === 'aside'" :entity="album" />
|
||||
|
||||
<template v-if="info">
|
||||
<div v-if="info.wiki?.summary" class="wiki">
|
||||
<div v-if="showSummary" class="summary" data-testid="summary" v-html="info.wiki.summary"/>
|
||||
<div v-if="showFull" class="full" data-testid="full" v-html="info.wiki.full"/>
|
||||
<div v-if="showSummary" class="summary" data-testid="summary" v-html="info.wiki.summary" />
|
||||
<div v-if="showFull" class="full" data-testid="full" v-html="info.wiki.full" />
|
||||
|
||||
<button v-if="showSummary" class="more" @click.prevent="showingFullWiki = true">
|
||||
Full Wiki
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<TrackList v-if="info.tracks?.length" :album="album" :tracks="info.tracks" data-testid="album-info-tracks"/>
|
||||
<TrackList v-if="info.tracks?.length" :album="album" :tracks="info.tracks" data-testid="album-info-tracks" />
|
||||
|
||||
<footer>
|
||||
Data ©
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<ul class="tracks">
|
||||
<li v-for="(track, index) in tracks" :key="index" data-testid="album-track-item">
|
||||
<TrackListItem :album="album" :track="track"/>
|
||||
<TrackListItem :album="album" :track="track" />
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
@ -22,6 +22,7 @@ const { album, tracks } = toRefs(props)
|
|||
|
||||
const songs = ref<Song[]>([])
|
||||
|
||||
// @ts-ignore
|
||||
provide(SongsKey, songs)
|
||||
|
||||
onMounted(async () => songs.value = await songStore.fetchForAlbum(album.value))
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
@click="play"
|
||||
>
|
||||
<span class="title">{{ track.title }}</span>
|
||||
<AppleMusicButton v-if="useAppleMusic && !matchedSong" :url="iTunesUrl"/>
|
||||
<AppleMusicButton v-if="useAppleMusic && !matchedSong" :url="iTunesUrl" />
|
||||
<span class="length">{{ fmtLength }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -4,7 +4,7 @@ exports[`renders 1`] = `
|
|||
<article class="item full" draggable="true" tabindex="0" title="IV by Led Zeppelin" data-v-f01bdc56=""><br data-testid="thumbnail" entity="[object Object]" data-v-f01bdc56="">
|
||||
<footer data-v-f01bdc56="">
|
||||
<div class="name" data-v-f01bdc56=""><a href="#/album/42" class="text-normal" data-testid="name">IV</a><a href="#/artist/17">Led Zeppelin</a></div>
|
||||
<p class="meta" data-v-f01bdc56=""><a title="Shuffle all songs in the album IV" class="shuffle-album" href="" role="button"> Shuffle </a><a title="Download all songs in the album IV" class="download-album" href="" role="button"> Download </a></p>
|
||||
<p class="meta" data-v-f01bdc56=""><a title="Shuffle all songs in the album IV" class="shuffle-album" role="button"> Shuffle </a><a title="Download all songs in the album IV" class="download-album" role="button"> Download </a></p>
|
||||
</footer>
|
||||
</article>
|
||||
`;
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
<a
|
||||
:title="`Shuffle all songs by ${artist.name}`"
|
||||
class="shuffle-artist"
|
||||
href
|
||||
role="button"
|
||||
@click.prevent="shuffle"
|
||||
>
|
||||
|
@ -25,7 +24,6 @@
|
|||
v-if="allowDownload"
|
||||
:title="`Download all songs by ${artist.name}`"
|
||||
class="download-artist"
|
||||
href
|
||||
role="button"
|
||||
@click.prevent="download"
|
||||
>
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
<li @click="play">Play All</li>
|
||||
<li @click="shuffle">Shuffle All</li>
|
||||
<template v-if="isStandardArtist">
|
||||
<li class="separator"></li>
|
||||
<li class="separator" />
|
||||
<li @click="viewArtistDetails">Go to Artist</li>
|
||||
</template>
|
||||
<template v-if="isStandardArtist && allowDownload">
|
||||
<li class="separator"></li>
|
||||
<li class="separator" />
|
||||
<li @click="download">Download</li>
|
||||
</template>
|
||||
</template>
|
||||
|
@ -23,7 +23,7 @@ import { useContextMenu, useRouter } from '@/composables'
|
|||
import { eventBus } from '@/utils'
|
||||
|
||||
const { go } = useRouter()
|
||||
const { context, base, ContextMenuBase, open, trigger } = useContextMenu()
|
||||
const { base, ContextMenuBase, open, trigger } = useContextMenu()
|
||||
|
||||
const artist = ref<Artist>()
|
||||
const allowDownload = toRef(commonStore.state, 'allow_download')
|
||||
|
@ -48,6 +48,6 @@ const download = () => trigger(() => downloadService.fromArtist(artist.value!))
|
|||
|
||||
eventBus.on('ARTIST_CONTEXT_MENU_REQUESTED', async (e, _artist) => {
|
||||
artist.value = _artist
|
||||
await open(e.pageY, e.pageX, { _artist })
|
||||
await open(e.pageY, e.pageX)
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -3,17 +3,17 @@
|
|||
<h1 v-if="mode === 'aside'" class="name">
|
||||
<span>{{ artist.name }}</span>
|
||||
<button :title="`Play all songs by ${artist.name}`" class="control" type="button" @click.prevent="play">
|
||||
<icon :icon="faCirclePlay" size="xl"/>
|
||||
<icon :icon="faCirclePlay" size="xl" />
|
||||
</button>
|
||||
</h1>
|
||||
|
||||
<main>
|
||||
<ArtistThumbnail v-if="mode === 'aside'" :entity="artist"/>
|
||||
<ArtistThumbnail v-if="mode === 'aside'" :entity="artist" />
|
||||
|
||||
<template v-if="info">
|
||||
<div v-if="info.bio?.summary" class="bio">
|
||||
<div v-if="showSummary" class="summary" data-testid="summary" v-html="info.bio.summary"/>
|
||||
<div v-if="showFull" class="full" data-testid="full" v-html="info.bio.full"/>
|
||||
<div v-if="showSummary" class="summary" data-testid="summary" v-html="info.bio.summary" />
|
||||
<div v-if="showFull" class="full" data-testid="full" v-html="info.bio.full" />
|
||||
|
||||
<button v-if="showSummary" class="more" @click.prevent="showingFullBio = true">
|
||||
Full Bio
|
||||
|
|
|
@ -4,7 +4,7 @@ exports[`renders 1`] = `
|
|||
<article class="item full" draggable="true" tabindex="0" title="Led Zeppelin" data-v-f01bdc56=""><br data-testid="thumbnail" entity="[object Object]" data-v-f01bdc56="">
|
||||
<footer data-v-f01bdc56="">
|
||||
<div class="name" data-v-f01bdc56=""><a href="#/artist/42" class="text-normal" data-testid="name">Led Zeppelin</a></div>
|
||||
<p class="meta" data-v-f01bdc56=""><a title="Shuffle all songs by Led Zeppelin" class="shuffle-artist" href="" role="button"> Shuffle </a><a title="Download all songs by Led Zeppelin" class="download-artist" href="" role="button"> Download </a></p>
|
||||
<p class="meta" data-v-f01bdc56=""><a title="Shuffle all songs by Led Zeppelin" class="shuffle-artist" role="button"> Shuffle </a><a title="Download all songs by Led Zeppelin" class="download-artist" role="button"> Download </a></p>
|
||||
</footer>
|
||||
</article>
|
||||
`;
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { screen } from '@testing-library/vue'
|
||||
import { expect, it } from 'vitest'
|
||||
import { expect, it, Mock } from 'vitest'
|
||||
import { userStore } from '@/stores'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import LoginFrom from './LoginForm.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private async submitForm (loginMock: SpyInstanceFn) {
|
||||
private async submitForm (loginMock: Mock) {
|
||||
const rendered = this.render(LoginFrom)
|
||||
|
||||
await this.type(screen.getByPlaceholderText('Email Address'), 'john@doe.com')
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
<template>
|
||||
<dialog ref="dialog" class="text-primary bg-primary" @cancel.prevent>
|
||||
<Component :is="modalNameToComponentMap[activeModalName]" v-if="activeModalName" @close="close"/>
|
||||
<Component :is="modalNameToComponentMap[activeModalName]" v-if="activeModalName" @close="close" />
|
||||
</dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ComponentPublicInstance, defineAsyncComponent, ref, watch } from 'vue'
|
||||
import { defineAsyncComponent, ref, watch } from 'vue'
|
||||
import { arrayify, eventBus, provideReadonly } from '@/utils'
|
||||
import { ModalContextKey } from '@/symbols'
|
||||
|
||||
const modalNameToComponentMap: Record<string, ComponentPublicInstance> = {
|
||||
const modalNameToComponentMap = {
|
||||
'create-playlist-form': defineAsyncComponent(() => import('@/components/playlist/CreatePlaylistForm.vue')),
|
||||
'edit-playlist-form': defineAsyncComponent(() => import('@/components/playlist/EditPlaylistForm.vue')),
|
||||
'create-smart-playlist-form': defineAsyncComponent(() => import('@/components/playlist/smart-playlist/CreateSmartPlaylistForm.vue')),
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
A very thin wrapper around Plyr, extracted as a standalone component for easier styling and to work better with HMR.
|
||||
-->
|
||||
<div class="plyr">
|
||||
<audio controls crossorigin="anonymous"></audio>
|
||||
<audio controls crossorigin="anonymous" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
href="/#/visualizer"
|
||||
title="Show the visualizer"
|
||||
>
|
||||
<icon :icon="faBolt"/>
|
||||
<icon :icon="faBolt" />
|
||||
</a>
|
||||
|
||||
<button
|
||||
|
@ -20,10 +20,10 @@
|
|||
type="button"
|
||||
@click.prevent="showEqualizer"
|
||||
>
|
||||
<icon :icon="faSliders"/>
|
||||
<icon :icon="faSliders" />
|
||||
</button>
|
||||
|
||||
<Volume/>
|
||||
<Volume />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -4,14 +4,14 @@ import factory from '@/__tests__/factory'
|
|||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { CurrentSongKey } from '@/symbols'
|
||||
import { playbackService } from '@/services'
|
||||
import FooterPlaybackControls from './FooterPlaybackControls.vue'
|
||||
import { screen } from '@testing-library/vue'
|
||||
import FooterPlaybackControls from './FooterPlaybackControls.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
private renderComponent (song?: Song | null) {
|
||||
if (song === undefined) {
|
||||
song = factory<Song>('song', {
|
||||
id: 42,
|
||||
id: '00000000-0000-0000-0000-000000000000',
|
||||
title: 'Fahrstuhl to Heaven',
|
||||
artist_name: 'Led Zeppelin',
|
||||
artist_id: 3,
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
<template>
|
||||
<div class="playback-controls" data-testid="footer-middle-pane">
|
||||
<div class="buttons">
|
||||
<LikeButton v-if="song" :song="song" class="like-btn"/>
|
||||
<button type="button" v-else/> <!-- a placeholder to maintain the flex layout -->
|
||||
<LikeButton v-if="song" :song="song" class="like-btn" />
|
||||
<button v-else type="button" /> <!-- a placeholder to maintain the flex layout -->
|
||||
|
||||
<button type="button" title="Play previous song" @click.prevent="playPrev">
|
||||
<icon :icon="faStepBackward"/>
|
||||
<icon :icon="faStepBackward" />
|
||||
</button>
|
||||
|
||||
<PlayButton/>
|
||||
<PlayButton />
|
||||
|
||||
<button type="button" title="Play next song" @click.prevent="playNext">
|
||||
<icon :icon="faStepForward"/>
|
||||
<icon :icon="faStepForward" />
|
||||
</button>
|
||||
|
||||
<RepeatModeSwitch class="repeat-mode-btn"/>
|
||||
<RepeatModeSwitch class="repeat-mode-btn" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { expect, it } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
import factory from '@/__tests__/factory'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import FooterSongInfo from './FooterSongInfo.vue'
|
||||
import { ref } from 'vue'
|
||||
import { CurrentSongKey } from '@/symbols'
|
||||
import FooterSongInfo from './FooterSongInfo.vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
protected test () {
|
||||
|
@ -21,7 +21,7 @@ new class extends UnitTestCase {
|
|||
expect(this.render(FooterSongInfo, {
|
||||
global: {
|
||||
provide: {
|
||||
[CurrentSongKey]: ref(song)
|
||||
[<symbol>CurrentSongKey]: ref(song)
|
||||
}
|
||||
}
|
||||
}).html()).toMatchSnapshot()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="song-info" :class="{ playing: song?.playback_state === 'Playing' }">
|
||||
<span :style="{ backgroundImage: `url('${cover}')` }" class="album-thumb"/>
|
||||
<div class="meta" v-if="song">
|
||||
<span :style="{ backgroundImage: `url('${cover}')` }" class="album-thumb" />
|
||||
<div v-if="song" class="meta">
|
||||
<h3 class="title">{{ song.title }}</h3>
|
||||
<a :href="`/#/artist/${song.artist_id}`" class="artist">{{ song.artist_name }}</a>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<footer id="mainFooter" @contextmenu.prevent="requestContextMenu">
|
||||
<AudioPlayer/>
|
||||
<AudioPlayer />
|
||||
|
||||
<div class="wrapper">
|
||||
<SongInfo/>
|
||||
<PlaybackControls/>
|
||||
<ExtraControls/>
|
||||
<SongInfo />
|
||||
<PlaybackControls />
|
||||
<ExtraControls />
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<div id="extraPanel" :class="{ 'showing-pane': activeTab }">
|
||||
<div class="controls">
|
||||
<div class="top">
|
||||
<SidebarMenuToggleButton class="burger"/>
|
||||
<ExtraPanelTabHeader v-if="song" v-model="activeTab"/>
|
||||
<SidebarMenuToggleButton class="burger" />
|
||||
<ExtraPanelTabHeader v-if="song" v-model="activeTab" />
|
||||
</div>
|
||||
|
||||
<div class="bottom">
|
||||
|
@ -13,19 +13,19 @@
|
|||
type="button"
|
||||
@click.prevent="openAboutKoelModal"
|
||||
>
|
||||
<icon :icon="faInfoCircle"/>
|
||||
<span v-if="shouldNotifyNewVersion" class="new-version-notification"/>
|
||||
<icon :icon="faInfoCircle" />
|
||||
<span v-if="shouldNotifyNewVersion" class="new-version-notification" />
|
||||
</button>
|
||||
|
||||
<button v-koel-tooltip.left title="Log out" type="button" @click.prevent="logout">
|
||||
<icon :icon="faArrowRightFromBracket"/>
|
||||
<icon :icon="faArrowRightFromBracket" />
|
||||
</button>
|
||||
|
||||
<ProfileAvatar @click="onProfileLinkClick"/>
|
||||
<ProfileAvatar @click="onProfileLinkClick" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panes" v-if="song" v-show="activeTab">
|
||||
<div v-if="song" v-show="activeTab" class="panes">
|
||||
<div
|
||||
v-show="activeTab === 'Lyrics'"
|
||||
id="extraPanelLyrics"
|
||||
|
@ -33,7 +33,7 @@
|
|||
role="tabpanel"
|
||||
tabindex="0"
|
||||
>
|
||||
<LyricsPane :song="song"/>
|
||||
<LyricsPane :song="song" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
@ -43,7 +43,7 @@
|
|||
role="tabpanel"
|
||||
tabindex="0"
|
||||
>
|
||||
<ArtistInfo v-if="artist" :artist="artist" mode="aside"/>
|
||||
<ArtistInfo v-if="artist" :artist="artist" mode="aside" />
|
||||
<span v-else>Loading…</span>
|
||||
</div>
|
||||
|
||||
|
@ -54,19 +54,19 @@
|
|||
role="tabpanel"
|
||||
tabindex="0"
|
||||
>
|
||||
<AlbumInfo v-if="album" :album="album" mode="aside"/>
|
||||
<AlbumInfo v-if="album" :album="album" mode="aside" />
|
||||
<span v-else>Loading…</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="activeTab === 'YouTube'"
|
||||
data-testid="extra-panel-youtube"
|
||||
id="extraPanelYouTube"
|
||||
data-testid="extra-panel-youtube"
|
||||
aria-labelledby="extraTabYouTube"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
>
|
||||
<YouTubeVideoList v-if="useYouTube && song" :song="song"/>
|
||||
<YouTubeVideoList v-if="useYouTube && song" :song="song" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -97,16 +97,16 @@ const { shouldNotifyNewVersion } = useNewVersionNotification()
|
|||
const song = requireInjection(CurrentSongKey, ref(null))
|
||||
const activeTab = ref<ExtraPanelTab | null>(null)
|
||||
|
||||
const artist = ref<Artist | null>(null)
|
||||
const album = ref<Album | null>(null)
|
||||
const artist = ref<Artist>()
|
||||
const album = ref<Album>()
|
||||
|
||||
watch(song, song => song && fetchSongInfo(song))
|
||||
watch(activeTab, tab => (preferenceStore.activeExtraPanelTab = tab))
|
||||
|
||||
const fetchSongInfo = async (_song: Song) => {
|
||||
song.value = _song
|
||||
artist.value = null
|
||||
album.value = null
|
||||
artist.value = undefined
|
||||
album.value = undefined
|
||||
|
||||
try {
|
||||
artist.value = await artistStore.resolve(_song.artist_id)
|
||||
|
|
|
@ -5,30 +5,30 @@
|
|||
lists), so we use v-show.
|
||||
For those that don't need to maintain their own UI state, we use v-if and enjoy some code-splitting juice.
|
||||
-->
|
||||
<VisualizerScreen v-if="screen === 'Visualizer'"/>
|
||||
<AlbumArtOverlay v-if="showAlbumArtOverlay && currentSong" :album="currentSong?.album_id"/>
|
||||
<VisualizerScreen v-if="screen === 'Visualizer'" />
|
||||
<AlbumArtOverlay v-if="showAlbumArtOverlay && currentSong" :album="currentSong?.album_id" />
|
||||
|
||||
<HomeScreen v-show="screen === 'Home'"/>
|
||||
<QueueScreen v-show="screen === 'Queue'"/>
|
||||
<AllSongsScreen v-show="screen === 'Songs'"/>
|
||||
<AlbumListScreen v-show="screen === 'Albums'"/>
|
||||
<ArtistListScreen v-show="screen === 'Artists'"/>
|
||||
<PlaylistScreen v-show="screen === 'Playlist'"/>
|
||||
<FavoritesScreen v-show="screen === 'Favorites'"/>
|
||||
<RecentlyPlayedScreen v-show="screen === 'RecentlyPlayed'"/>
|
||||
<UploadScreen v-show="screen === 'Upload'"/>
|
||||
<SearchExcerptsScreen v-show="screen === 'Search.Excerpt'"/>
|
||||
<GenreScreen v-show="screen === 'Genre'"/>
|
||||
<HomeScreen v-show="screen === 'Home'" />
|
||||
<QueueScreen v-show="screen === 'Queue'" />
|
||||
<AllSongsScreen v-show="screen === 'Songs'" />
|
||||
<AlbumListScreen v-show="screen === 'Albums'" />
|
||||
<ArtistListScreen v-show="screen === 'Artists'" />
|
||||
<PlaylistScreen v-show="screen === 'Playlist'" />
|
||||
<FavoritesScreen v-show="screen === 'Favorites'" />
|
||||
<RecentlyPlayedScreen v-show="screen === 'RecentlyPlayed'" />
|
||||
<UploadScreen v-show="screen === 'Upload'" />
|
||||
<SearchExcerptsScreen v-show="screen === 'Search.Excerpt'" />
|
||||
<GenreScreen v-show="screen === 'Genre'" />
|
||||
|
||||
<GenreListScreen v-if="screen === 'Genres'"/>
|
||||
<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'"/>
|
||||
<YoutubeScreen v-if="useYouTube" v-show="screen === 'YouTube'"/>
|
||||
<NotFoundScreen v-if="screen === '404'"/>
|
||||
<GenreListScreen v-if="screen === 'Genres'" />
|
||||
<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'" />
|
||||
<YoutubeScreen v-if="useYouTube" v-show="screen === 'YouTube'" />
|
||||
<NotFoundScreen v-if="screen === '404'" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<nav id="sidebar" :class="{ showing: mobileShowing }" class="side side-nav" v-koel-clickaway="closeIfMobile">
|
||||
<SearchForm/>
|
||||
<nav id="sidebar" v-koel-clickaway="closeIfMobile" :class="{ showing: mobileShowing }" class="side side-nav">
|
||||
<SearchForm />
|
||||
<section class="music">
|
||||
<h1>Your Music</h1>
|
||||
|
||||
<ul class="menu">
|
||||
<li>
|
||||
<a :class="['home', activeScreen === 'Home' ? 'active' : '']" href="#/home">
|
||||
<icon :icon="faHome" fixed-width/>
|
||||
<icon :icon="faHome" fixed-width />
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
|
@ -18,44 +18,44 @@
|
|||
@drop="onQueueDrop"
|
||||
>
|
||||
<a :class="['queue', activeScreen === 'Queue' ? 'active' : '']" href="#/queue">
|
||||
<icon :icon="faListOl" fixed-width/>
|
||||
<icon :icon="faListOl" fixed-width />
|
||||
Current Queue
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['songs', activeScreen === 'Songs' ? 'active' : '']" href="#/songs">
|
||||
<icon :icon="faMusic" fixed-width/>
|
||||
<icon :icon="faMusic" fixed-width />
|
||||
All Songs
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['albums', activeScreen === 'Albums' ? 'active' : '']" href="#/albums">
|
||||
<icon :icon="faCompactDisc" fixed-width/>
|
||||
<icon :icon="faCompactDisc" fixed-width />
|
||||
Albums
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['artists', activeScreen === 'Artists' ? 'active' : '']" href="#/artists">
|
||||
<icon :icon="faMicrophone" fixed-width/>
|
||||
<icon :icon="faMicrophone" fixed-width />
|
||||
Artists
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['genres', activeScreen === 'Genres' ? 'active' : '']" href="#/genres">
|
||||
<icon :icon="faTags" fixed-width/>
|
||||
<icon :icon="faTags" fixed-width />
|
||||
Genres
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="useYouTube">
|
||||
<a :class="['youtube', activeScreen === 'YouTube' ? 'active' : '']" href="#/youtube">
|
||||
<icon :icon="faYoutube" fixed-width/>
|
||||
<icon :icon="faYoutube" fixed-width />
|
||||
YouTube Video
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<PlaylistList/>
|
||||
<PlaylistList />
|
||||
|
||||
<section v-if="isAdmin" class="manage">
|
||||
<h1>Manage</h1>
|
||||
|
@ -63,19 +63,19 @@
|
|||
<ul class="menu">
|
||||
<li>
|
||||
<a :class="['settings', activeScreen === 'Settings' ? 'active' : '']" href="#/settings">
|
||||
<icon :icon="faTools" fixed-width/>
|
||||
<icon :icon="faTools" fixed-width />
|
||||
Settings
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['upload', activeScreen === 'Upload' ? 'active' : '']" href="#/upload">
|
||||
<icon :icon="faUpload" fixed-width/>
|
||||
<icon :icon="faUpload" fixed-width />
|
||||
Upload
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :class="['users', activeScreen === 'Users' ? 'active' : '']" href="#/users">
|
||||
<icon :icon="faUsers" fixed-width/>
|
||||
<icon :icon="faUsers" fixed-width />
|
||||
Users
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
<template>
|
||||
<div id="mainWrapper">
|
||||
<Sidebar/>
|
||||
<MainContent/>
|
||||
<ExtraPanel/>
|
||||
<ModalWrapper/>
|
||||
<SideBar />
|
||||
<MainContent />
|
||||
<ExtraPanel />
|
||||
<ModalWrapper />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import Sidebar from '@/components/layout/main-wrapper/Sidebar.vue'
|
||||
import SideBar from '@/components/layout/main-wrapper/SideBar.vue'
|
||||
import MainContent from '@/components/layout/main-wrapper/MainContent.vue'
|
||||
import ExtraPanel from '@/components/layout/main-wrapper/ExtraPanel.vue'
|
||||
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
<a href="https://github.com/phanan" rel="noopener" target="_blank">Phan An</a>
|
||||
and quite a few
|
||||
<a href="https://github.com/koel/core/graphs/contributors" rel="noopener" target="_blank">awesome</a> <a
|
||||
href="https://github.com/koel/koel/graphs/contributors" rel="noopener" target="_blank">contributors</a>.
|
||||
href="https://github.com/koel/koel/graphs/contributors" rel="noopener" target="_blank"
|
||||
>contributors</a>.
|
||||
</p>
|
||||
|
||||
<div v-if="credits" class="credit-wrapper" data-testid="demo-credits">
|
||||
|
@ -30,7 +31,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<SponsorList/>
|
||||
<SponsorList />
|
||||
|
||||
<p>
|
||||
Loving Koel? Please consider supporting its development via
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<a href="https://opencollective.com/koel" rel="noopener" target="_blank">OpenCollective</a>.
|
||||
</p>
|
||||
<button type="button" @click.prevent="close">Hide</button>
|
||||
<span class="sep"></span>
|
||||
<span class="sep" />
|
||||
<button type="button" @click.prevent="stopBugging">
|
||||
Don't bug me again
|
||||
</button>
|
||||
|
|
|
@ -7,7 +7,7 @@ import CreateNewPlaylistContextMenu from './CreateNewPlaylistContextMenu.vue'
|
|||
|
||||
new class extends UnitTestCase {
|
||||
private async renderComponent () {
|
||||
await this.render(CreateNewPlaylistContextMenu)
|
||||
this.render(CreateNewPlaylistContextMenu)
|
||||
eventBus.emit('CREATE_NEW_PLAYLIST_CONTEXT_MENU_REQUESTED', { pageX: 420, pageY: 42 } as MouseEvent)
|
||||
await this.tick(2)
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ new class extends UnitTestCase {
|
|||
const storeMock = this.mock(playlistFolderStore, 'store')
|
||||
.mockResolvedValue(factory<PlaylistFolder>('playlist-folder'))
|
||||
|
||||
await this.render(CreatePlaylistFolderForm)
|
||||
this.render(CreatePlaylistFolderForm)
|
||||
|
||||
await this.type(screen.getByPlaceholderText('Folder name'), 'My folder')
|
||||
await this.user.click(screen.getByRole('button', { name: 'Save' }))
|
||||
|
|
|
@ -20,8 +20,8 @@
|
|||
<label class="folder">
|
||||
Folder
|
||||
<select v-model="folderId">
|
||||
<option :value="null"></option>
|
||||
<option v-for="folder in folders" :value="folder.id">{{ folder.name }}</option>
|
||||
<option :value="null" />
|
||||
<option v-for="folder in folders" :key="folder.id" :value="folder.id">{{ folder.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -21,8 +21,8 @@
|
|||
<label class="folder">
|
||||
Folder
|
||||
<select v-model="folderId">
|
||||
<option :value="null"></option>
|
||||
<option v-for="folder in folders" :value="folder.id">{{ folder.name }}</option>
|
||||
<option :value="null" />
|
||||
<option v-for="folder in folders" :key="folder.id" :value="folder.id">{{ folder.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -7,8 +7,8 @@ import PlaylistContextMenu from './PlaylistContextMenu.vue'
|
|||
|
||||
new class extends UnitTestCase {
|
||||
private async renderComponent (playlist: Playlist) {
|
||||
await this.render(PlaylistContextMenu)
|
||||
eventBus.emit('PLAYLIST_CONTEXT_MENU_REQUESTED', { pageX: 420, pageY: 42 }, playlist)
|
||||
this.render(PlaylistContextMenu)
|
||||
eventBus.emit('PLAYLIST_CONTEXT_MENU_REQUESTED', { pageX: 420, pageY: 42 } as MouseEvent, playlist)
|
||||
await this.tick(2)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<ContextMenuBase ref="base">
|
||||
<li :data-testid="`playlist-context-menu-edit-${playlist.id}`" @click="editPlaylist">Edit</li>
|
||||
<li :data-testid="`playlist-context-menu-delete-${playlist.id}`" @click="deletePlaylist">Delete</li>
|
||||
<li @click="editPlaylist">Edit</li>
|
||||
<li @click="deletePlaylist">Delete</li>
|
||||
</ContextMenuBase>
|
||||
</template>
|
||||
|
||||
|
@ -10,14 +10,14 @@ import { ref } from 'vue'
|
|||
import { eventBus } from '@/utils'
|
||||
import { useContextMenu } from '@/composables'
|
||||
|
||||
const { context, base, ContextMenuBase, open, trigger } = useContextMenu()
|
||||
const { base, ContextMenuBase, open, trigger } = useContextMenu()
|
||||
const playlist = ref<Playlist>()
|
||||
|
||||
const editPlaylist = () => trigger(() => eventBus.emit('MODAL_SHOW_EDIT_PLAYLIST_FORM', playlist.value))
|
||||
const deletePlaylist = () => trigger(() => eventBus.emit('PLAYLIST_DELETE', playlist.value))
|
||||
const editPlaylist = () => trigger(() => eventBus.emit('MODAL_SHOW_EDIT_PLAYLIST_FORM', playlist.value!))
|
||||
const deletePlaylist = () => trigger(() => eventBus.emit('PLAYLIST_DELETE', playlist.value!))
|
||||
|
||||
eventBus.on('PLAYLIST_CONTEXT_MENU_REQUESTED', async (event, _playlist) => {
|
||||
playlist.value = _playlist
|
||||
await open(event.pageY, event.pageX, { playlist })
|
||||
await open(event.pageY, event.pageX)
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -9,8 +9,8 @@ import PlaylistFolderContextMenu from './PlaylistFolderContextMenu.vue'
|
|||
|
||||
new class extends UnitTestCase {
|
||||
private async renderComponent (folder: PlaylistFolder) {
|
||||
await this.render(PlaylistFolderContextMenu)
|
||||
eventBus.emit('PLAYLIST_FOLDER_CONTEXT_MENU_REQUESTED', { pageX: 420, pageY: 42 }, folder)
|
||||
this.render(PlaylistFolderContextMenu)
|
||||
eventBus.emit('PLAYLIST_FOLDER_CONTEXT_MENU_REQUESTED', { pageX: 420, pageY: 42 } as MouseEvent, folder)
|
||||
await this.tick(2)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
<template v-if="playable">
|
||||
<li @click="play">Play All</li>
|
||||
<li @click="shuffle">Shuffle All</li>
|
||||
<li class="separator"/>
|
||||
<li class="separator" />
|
||||
</template>
|
||||
<li @click="createPlaylist">Create Playlist</li>
|
||||
<li @click="createSmartPlaylist">Create Smart Playlist</li>
|
||||
<li class="separator"/>
|
||||
<li class="separator" />
|
||||
<li @click="rename">Rename</li>
|
||||
<li @click="destroy">Delete</li>
|
||||
</template>
|
||||
|
@ -23,7 +23,7 @@ import { playbackService } from '@/services'
|
|||
import { useContextMenu, useRouter } from '@/composables'
|
||||
|
||||
const { go } = useRouter()
|
||||
const { context, base, ContextMenuBase, open, trigger } = useContextMenu()
|
||||
const { base, ContextMenuBase, open, trigger } = useContextMenu()
|
||||
|
||||
const folder = ref<PlaylistFolder>()
|
||||
|
||||
|
@ -40,13 +40,13 @@ const shuffle = () => trigger(async () => {
|
|||
go('queue')
|
||||
})
|
||||
|
||||
const createPlaylist = () => trigger(() => eventBus.emit('MODAL_SHOW_CREATE_PLAYLIST_FORM', folder.value))
|
||||
const createSmartPlaylist = () => trigger(() => eventBus.emit('MODAL_SHOW_CREATE_SMART_PLAYLIST_FORM', folder.value))
|
||||
const rename = () => trigger(() => eventBus.emit('MODAL_SHOW_EDIT_PLAYLIST_FOLDER_FORM', folder.value))
|
||||
const destroy = () => trigger(() => eventBus.emit('PLAYLIST_FOLDER_DELETE', folder.value))
|
||||
const createPlaylist = () => trigger(() => eventBus.emit('MODAL_SHOW_CREATE_PLAYLIST_FORM', folder.value!))
|
||||
const createSmartPlaylist = () => trigger(() => eventBus.emit('MODAL_SHOW_CREATE_SMART_PLAYLIST_FORM', folder.value!))
|
||||
const rename = () => trigger(() => eventBus.emit('MODAL_SHOW_EDIT_PLAYLIST_FOLDER_FORM', folder.value!))
|
||||
const destroy = () => trigger(() => eventBus.emit('PLAYLIST_FOLDER_DELETE', folder.value!))
|
||||
|
||||
eventBus.on('PLAYLIST_FOLDER_CONTEXT_MENU_REQUESTED', async (e, _folder) => {
|
||||
folder.value = _folder
|
||||
await open(e.pageY, e.pageX, { folder })
|
||||
await open(e.pageY, e.pageX)
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -2,18 +2,18 @@
|
|||
<li
|
||||
class="playlist-folder"
|
||||
:class="{ droppable }"
|
||||
tabindex="0"
|
||||
@dragleave="onDragLeave"
|
||||
@dragover="onDragOver"
|
||||
@drop="onDrop"
|
||||
tabindex="0"
|
||||
>
|
||||
<a @click.prevent="toggle" @contextmenu.prevent="onContextMenu">
|
||||
<icon :icon="opened ? faFolderOpen : faFolder" fixed-width/>
|
||||
<icon :icon="opened ? faFolderOpen : faFolder" fixed-width />
|
||||
{{ folder.name }}
|
||||
</a>
|
||||
|
||||
<ul v-if="playlistsInFolder.length" v-show="opened">
|
||||
<PlaylistSidebarItem v-for="playlist in playlistsInFolder" :key="playlist.id" :list="playlist" class="sub-item"/>
|
||||
<PlaylistSidebarItem v-for="playlist in playlistsInFolder" :key="playlist.id" :list="playlist" class="sub-item" />
|
||||
</ul>
|
||||
|
||||
<div
|
||||
|
|
|
@ -13,10 +13,10 @@
|
|||
</h1>
|
||||
|
||||
<ul>
|
||||
<PlaylistSidebarItem :list="{ name: 'Favorites', songs: favorites }"/>
|
||||
<PlaylistSidebarItem :list="{ name: 'Recently Played', songs: [] }"/>
|
||||
<PlaylistFolderSidebarItem v-for="folder in folders" :key="folder.id" :folder="folder"/>
|
||||
<PlaylistSidebarItem v-for="playlist in orphanPlaylists" :key="playlist.id" :list="playlist"/>
|
||||
<PlaylistSidebarItem :list="{ name: 'Favorites', songs: favorites }" />
|
||||
<PlaylistSidebarItem :list="{ name: 'Recently Played', songs: [] }" />
|
||||
<PlaylistFolderSidebarItem v-for="folder in folders" :key="folder.id" :folder="folder" />
|
||||
<PlaylistSidebarItem v-for="playlist in orphanPlaylists" :key="playlist.id" :list="playlist" />
|
||||
</ul>
|
||||
</section>
|
||||
</template>
|
||||
|
|
|
@ -14,8 +14,8 @@
|
|||
<label class="folder">
|
||||
Folder
|
||||
<select v-model="folderId">
|
||||
<option :value="null"></option>
|
||||
<option v-for="folder in folders" :value="folder.id">{{ folder.name }}</option>
|
||||
<option :value="null" />
|
||||
<option v-for="folder in folders" :key="folder.id" :value="folder.id">{{ folder.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -25,11 +25,11 @@
|
|||
v-for="(group, index) in collectedRuleGroups"
|
||||
:key="group.id"
|
||||
:group="group"
|
||||
:isFirstGroup="index === 0"
|
||||
:is-first-group="index === 0"
|
||||
@input="onGroupChanged"
|
||||
/>
|
||||
<Btn class="btn-add-group" green small title="Add a new group" uppercase @click.prevent="addGroup">
|
||||
<icon :icon="faPlus"/>
|
||||
<icon :icon="faPlus" />
|
||||
Group
|
||||
</Btn>
|
||||
</div>
|
||||
|
|
|
@ -20,8 +20,8 @@
|
|||
<label class="folder">
|
||||
Folder
|
||||
<select v-model="mutablePlaylist.folder_id">
|
||||
<option :value="null"></option>
|
||||
<option v-for="folder in folders" :value="folder.id">{{ folder.name }}</option>
|
||||
<option :value="null" />
|
||||
<option v-for="folder in folders" :key="folder.id" :value="folder.id">{{ folder.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -31,11 +31,11 @@
|
|||
v-for="(group, index) in mutablePlaylist.rules"
|
||||
:key="group.id"
|
||||
:group="group"
|
||||
:isFirstGroup="index === 0"
|
||||
:is-first-group="index === 0"
|
||||
@input="onGroupChanged"
|
||||
/>
|
||||
<Btn class="btn-add-group" green small title="Add a new group" uppercase @click.prevent="addGroup">
|
||||
<icon :icon="faPlus"/>
|
||||
<icon :icon="faPlus" />
|
||||
</Btn>
|
||||
</div>
|
||||
</main>
|
||||
|
@ -63,9 +63,7 @@ const playlist = useModal().getFromContext<Playlist>('playlist')
|
|||
|
||||
const folders = toRef(playlistFolderStore.state, 'folders')
|
||||
|
||||
let mutablePlaylist: Playlist
|
||||
|
||||
watch(playlist, () => (mutablePlaylist = reactive(cloneDeep(playlist))), { immediate: true })
|
||||
const mutablePlaylist = reactive(cloneDeep(playlist))
|
||||
|
||||
const isPristine = () => isEqual(mutablePlaylist.rules, playlist.rules)
|
||||
&& mutablePlaylist.name.trim() === playlist.name
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="smart-playlist-form">
|
||||
<slot/>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div class="row" data-testid="smart-playlist-rule-row">
|
||||
<Btn class="remove-rule" red title="Remove this rule" @click.prevent="removeRule">
|
||||
<icon :icon="faTrashCan"/>
|
||||
<icon :icon="faTrashCan" />
|
||||
</Btn>
|
||||
|
||||
<select v-model="selectedModel" name="model[]">
|
||||
<option v-for="model in models" :key="model.name" :value="model">{{ model.label }}</option>
|
||||
<option v-for="m in models" :key="m.name" :value="m">{{ model.label }}</option>
|
||||
</select>
|
||||
|
||||
<select v-model="selectedOperator" name="operator[]">
|
||||
|
@ -17,9 +17,9 @@
|
|||
v-for="input in availableInputs"
|
||||
:key="input.id"
|
||||
v-model="input.value"
|
||||
:type="selectedOperator?.type || selectedModel?.type"
|
||||
:type="(selectedOperator?.type || selectedModel?.type)!"
|
||||
:value="input.value"
|
||||
@update:modelValue="onInput"
|
||||
@update:model-value="onInput"
|
||||
/>
|
||||
|
||||
<span v-if="valueSuffix" class="suffix">{{ valueSuffix }}</span>
|
||||
|
@ -39,7 +39,7 @@ const RuleInput = defineAsyncComponent(() => import('@/components/playlist/smart
|
|||
const props = defineProps<{ rule: SmartPlaylistRule }>()
|
||||
const { rule } = toRefs(props)
|
||||
|
||||
const mutatedRule = Object.assign({}, rule.value)
|
||||
const mutatedRule = Object.assign({}, rule.value) as SmartPlaylistRule
|
||||
|
||||
const selectedModel = ref<SmartPlaylistModel>()
|
||||
const selectedOperator = ref<SmartPlaylistOperator>()
|
||||
|
@ -104,8 +104,8 @@ const emit = defineEmits<{
|
|||
const onInput = () => {
|
||||
emit('input', {
|
||||
id: mutatedRule.id,
|
||||
model: selectedModel.value,
|
||||
operator: selectedOperator.value?.operator,
|
||||
model: selectedModel.value!,
|
||||
operator: selectedOperator.value?.operator!,
|
||||
value: availableInputs.value.map(input => input.value)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -10,15 +10,15 @@
|
|||
</div>
|
||||
|
||||
<Rule
|
||||
v-for="rule in mutatedGroup.rules"
|
||||
:key="rule.id"
|
||||
:rule="rule"
|
||||
@input="onRuleChanged"
|
||||
@remove="removeRule(rule)"
|
||||
v-for="rule in mutatedGroup.rules"
|
||||
/>
|
||||
|
||||
<Btn @click.prevent="addRule" class="btn-add-rule" green small uppercase>
|
||||
<icon :icon="faPlus"/>
|
||||
<Btn class="btn-add-rule" green small uppercase @click.prevent="addRule">
|
||||
<icon :icon="faPlus" />
|
||||
Rule
|
||||
</Btn>
|
||||
</div>
|
||||
|
@ -44,7 +44,7 @@ const notifyParentForUpdate = () => emit('input', mutatedGroup)
|
|||
const addRule = () => mutatedGroup.rules.push(playlistStore.createEmptySmartPlaylistRule())
|
||||
|
||||
const onRuleChanged = (data: SmartPlaylistRule) => {
|
||||
Object.assign(mutatedGroup.rules.find(r => r.id === data.id), data)
|
||||
Object.assign(mutatedGroup.rules.find(r => r.id === data.id)!, data)
|
||||
notifyParentForUpdate()
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { computed, toRefs } from 'vue'
|
||||
import inputTypes from '@/config/smart-playlist/inputTypes'
|
||||
|
||||
const props = withDefaults(defineProps<{ type?: keyof typeof inputTypes, value?: any }>(), { value: undefined })
|
||||
const props = withDefaults(defineProps<{ type: keyof typeof inputTypes, value?: any }>(), { value: undefined })
|
||||
const { type } = toRefs(props)
|
||||
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', value: any): void }>()
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
</p>
|
||||
<div class="buttons">
|
||||
<Btn class="connect" @click.prevent="connect">
|
||||
<icon :icon="faLastfm"/>
|
||||
<icon :icon="faLastfm" />
|
||||
{{ connected ? 'Reconnect' : 'Connect' }}
|
||||
</Btn>
|
||||
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="form-row" v-if="!isPhone">
|
||||
<div v-if="!isPhone" class="form-row">
|
||||
<label>
|
||||
<CheckBox name="notify" v-model="preferences.notify"/>
|
||||
<CheckBox v-model="preferences.notify" name="notify" />
|
||||
Show “Now Playing” song notification
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-row" v-if="!isPhone">
|
||||
<div v-if="!isPhone" class="form-row">
|
||||
<label>
|
||||
<CheckBox name="confirm_closing" v-model="preferences.confirmClosing"/>
|
||||
<CheckBox v-model="preferences.confirmClosing" name="confirm_closing" />
|
||||
Confirm before closing Koel
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-row" v-if="isPhone">
|
||||
<div v-if="isPhone" class="form-row">
|
||||
<label>
|
||||
<CheckBox name="transcode_on_mobile" v-model="preferences.transcodeOnMobile"/>
|
||||
<CheckBox v-model="preferences.transcodeOnMobile" name="transcode_on_mobile" />
|
||||
Convert and play media at 128kbps on mobile
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>
|
||||
<CheckBox name="show_album_art_overlay" v-model="preferences.showAlbumArtOverlay"/>
|
||||
<CheckBox v-model="preferences.showAlbumArtOverlay" name="show_album_art_overlay" />
|
||||
Show a translucent, blurred overlay of the current album’s art
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -40,14 +40,14 @@
|
|||
type="password"
|
||||
>
|
||||
<span class="password-rules help">
|
||||
Min. 10 characters. Should be a mix of characters, numbers, and symbols.
|
||||
</span>
|
||||
Min. 10 characters. Should be a mix of characters, numbers, and symbols.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<Btn class="btn-submit" type="submit">Save</Btn>
|
||||
<span v-if="isDemo" class="demo-notice">
|
||||
<span v-if="isDemo()" class="demo-notice">
|
||||
Changes will not be saved in the demo version.
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -18,6 +18,8 @@ import { slugToTitle } from '@/utils'
|
|||
const props = defineProps<{ theme: Theme }>()
|
||||
const { theme } = toRefs(props)
|
||||
|
||||
const emit = defineEmits<{ (e: 'selected', theme: Theme): void }>()
|
||||
|
||||
const name = theme.value.name ? theme.value.name : slugToTitle(theme.value.id)
|
||||
|
||||
const thumbnailStyles: Record<string, string> = {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<h1>Theme</h1>
|
||||
<ul class="themes">
|
||||
<li v-for="theme in themes" :key="theme.id" data-testid="theme-card">
|
||||
<ThemeCard :key="theme.id" :theme="theme" @selected="setTheme"/>
|
||||
<ThemeCard :key="theme.id" :theme="theme" @selected="setTheme" />
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<section id="albumsWrapper">
|
||||
<ScreenHeader layout="collapsed">
|
||||
Albums
|
||||
<template v-slot:controls>
|
||||
<ViewModeSwitch v-model="viewMode"/>
|
||||
<template #controls>
|
||||
<ViewModeSwitch v-model="viewMode" />
|
||||
</template>
|
||||
</ScreenHeader>
|
||||
|
||||
|
@ -15,11 +15,11 @@
|
|||
@scroll="scrolling"
|
||||
>
|
||||
<template v-if="showSkeletons">
|
||||
<AlbumCardSkeleton v-for="i in 10" :key="i" :layout="itemLayout"/>
|
||||
<AlbumCardSkeleton v-for="i in 10" :key="i" :layout="itemLayout" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<AlbumCard v-for="album in albums" :key="album.id" :album="album" :layout="itemLayout"/>
|
||||
<ToTopButton/>
|
||||
<AlbumCard v-for="album in albums" :key="album.id" :album="album" :layout="itemLayout" />
|
||||
<ToTopButton />
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
<template>
|
||||
<section id="albumWrapper">
|
||||
<ScreenHeaderSkeleton v-if="loading"/>
|
||||
<section v-if="album" id="albumWrapper">
|
||||
<ScreenHeaderSkeleton v-if="loading" />
|
||||
|
||||
<ScreenHeader v-if="!loading && album" :layout="songs.length === 0 ? 'collapsed' : headerLayout">
|
||||
{{ album.name }}
|
||||
<ControlsToggle v-model="showingControls"/>
|
||||
<ControlsToggle v-model="showingControls" />
|
||||
|
||||
<template v-slot:thumbnail>
|
||||
<AlbumThumbnail :entity="album"/>
|
||||
<template #thumbnail>
|
||||
<AlbumThumbnail :entity="album" />
|
||||
</template>
|
||||
|
||||
<template v-slot:meta>
|
||||
<template #meta>
|
||||
<a v-if="isNormalArtist" :href="`#/artist/${album.artist_id}`" class="artist">{{ album.artist_name }}</a>
|
||||
<span v-else class="nope">{{ album.artist_name }}</span>
|
||||
<span>{{ pluralize(songs, 'song') }}</span>
|
||||
|
@ -19,7 +19,6 @@
|
|||
<a
|
||||
v-if="allowDownload"
|
||||
class="download"
|
||||
href
|
||||
role="button"
|
||||
title="Download all songs in album"
|
||||
@click.prevent="download"
|
||||
|
@ -28,11 +27,11 @@
|
|||
</a>
|
||||
</template>
|
||||
|
||||
<template v-slot:controls>
|
||||
<template #controls>
|
||||
<SongListControls
|
||||
v-if="songs.length && (!isPhone || showingControls)"
|
||||
@playAll="playAll"
|
||||
@playSelected="playSelected"
|
||||
@play-all="playAll"
|
||||
@play-selected="playSelected"
|
||||
/>
|
||||
</template>
|
||||
</ScreenHeader>
|
||||
|
@ -41,20 +40,20 @@
|
|||
<template #header>
|
||||
<label :class="{ active: activeTab === 'Songs' }">
|
||||
Songs
|
||||
<input type="radio" name="tab" value="Songs" v-model="activeTab"/>
|
||||
<input v-model="activeTab" type="radio" name="tab" value="Songs">
|
||||
</label>
|
||||
<label :class="{ active: activeTab === 'OtherAlbums' }">
|
||||
Other Albums
|
||||
<input type="radio" name="tab" value="OtherAlbums" v-model="activeTab"/>
|
||||
<input v-model="activeTab" type="radio" name="tab" value="OtherAlbums">
|
||||
</label>
|
||||
<label :class="{ active: activeTab === 'Info' }" v-if="useLastfm">
|
||||
<label v-if="useLastfm" :class="{ active: activeTab === 'Info' }">
|
||||
Information
|
||||
<input type="radio" name="tab" value="Info" v-model="activeTab"/>
|
||||
<input v-model="activeTab" type="radio" name="tab" value="Info">
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<div v-show="activeTab === 'Songs'">
|
||||
<SongListSkeleton v-if="loading"/>
|
||||
<SongListSkeleton v-if="loading" />
|
||||
<SongList
|
||||
v-else
|
||||
ref="songList"
|
||||
|
@ -67,21 +66,21 @@
|
|||
<div v-show="activeTab === 'OtherAlbums'" class="albums-pane" data-testid="albums-pane">
|
||||
<template v-if="otherAlbums">
|
||||
<ul v-if="otherAlbums.length" class="as-list">
|
||||
<li v-for="album in otherAlbums" :key="album.id">
|
||||
<AlbumCard :album="album" layout="compact"/>
|
||||
<li v-for="a in otherAlbums" :key="a.id">
|
||||
<AlbumCard :album="a" layout="compact" />
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="text-secondary">No other albums by {{ album.artist_name }} found in the library.</p>
|
||||
</template>
|
||||
<ul v-else class="as-list">
|
||||
<li v-for="i in 12" :key="i">
|
||||
<AlbumCardSkeleton layout="compact"/>
|
||||
<AlbumCardSkeleton layout="compact" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'Info'" class="info-pane" v-if="useLastfm && album">
|
||||
<AlbumInfo :album="album" mode="full"/>
|
||||
<div v-show="activeTab === 'Info'" v-if="useLastfm && album" class="info-pane">
|
||||
<AlbumInfo :album="album" mode="full" />
|
||||
</div>
|
||||
</ScreenTabs>
|
||||
</section>
|
||||
|
@ -180,7 +179,7 @@ onMounted(async () => (albumId.value = parseInt(getRouteParam('id')!)))
|
|||
onRouteChanged(route => route.screen === 'Album' && (albumId.value = parseInt(getRouteParam('id')!)))
|
||||
|
||||
// if the current album has been deleted, go back to the list
|
||||
eventBus.on('SONGS_UPDATED', () => albumStore.byId(albumId.value) || go('albums'))
|
||||
eventBus.on('SONGS_UPDATED', () => albumStore.byId(albumId.value!) || go('albums'))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -2,27 +2,27 @@
|
|||
<section id="songsWrapper">
|
||||
<ScreenHeader :layout="headerLayout">
|
||||
All Songs
|
||||
<ControlsToggle v-model="showingControls"/>
|
||||
<ControlsToggle v-model="showingControls" />
|
||||
|
||||
<template v-slot:thumbnail>
|
||||
<ThumbnailStack :thumbnails="thumbnails"/>
|
||||
<template #thumbnail>
|
||||
<ThumbnailStack :thumbnails="thumbnails" />
|
||||
</template>
|
||||
|
||||
<template v-if="totalSongCount" v-slot:meta>
|
||||
<template v-if="totalSongCount" #meta>
|
||||
<span>{{ pluralize(totalSongCount, 'song') }}</span>
|
||||
<span>{{ totalDuration }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:controls>
|
||||
<template #controls>
|
||||
<SongListControls
|
||||
v-if="totalSongCount && (!isPhone || showingControls)"
|
||||
@playAll="playAll"
|
||||
@playSelected="playSelected"
|
||||
@play-all="playAll"
|
||||
@playselected="playSelected"
|
||||
/>
|
||||
</template>
|
||||
</ScreenHeader>
|
||||
|
||||
<SongListSkeleton v-if="showSkeletons"/>
|
||||
<SongListSkeleton v-if="showSkeletons" />
|
||||
<SongList
|
||||
v-else
|
||||
ref="songList"
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<section id="artistsWrapper">
|
||||
<ScreenHeader layout="collapsed">
|
||||
Artists
|
||||
<template v-slot:controls>
|
||||
<ViewModeSwitch v-model="viewMode"/>
|
||||
<template #controls>
|
||||
<ViewModeSwitch v-model="viewMode" />
|
||||
</template>
|
||||
</ScreenHeader>
|
||||
|
||||
|
@ -15,11 +15,11 @@
|
|||
@scroll="scrolling"
|
||||
>
|
||||
<template v-if="showSkeletons">
|
||||
<ArtistCardSkeleton v-for="i in 10" :key="i" :layout="itemLayout"/>
|
||||
<ArtistCardSkeleton v-for="i in 10" :key="i" :layout="itemLayout" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArtistCard v-for="artist in artists" :key="artist.id" :artist="artist" :layout="itemLayout"/>
|
||||
<ToTopButton/>
|
||||
<ArtistCard v-for="artist in artists" :key="artist.id" :artist="artist" :layout="itemLayout" />
|
||||
<ToTopButton />
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
<template>
|
||||
<section id="artistWrapper">
|
||||
<ScreenHeaderSkeleton v-if="loading"/>
|
||||
<ScreenHeaderSkeleton v-if="loading" />
|
||||
|
||||
<ScreenHeader v-if="!loading && artist" :layout="songs.length === 0 ? 'collapsed' : headerLayout">
|
||||
{{ artist.name }}
|
||||
<ControlsToggle v-model="showingControls"/>
|
||||
<ControlsToggle v-model="showingControls" />
|
||||
|
||||
<template v-slot:thumbnail>
|
||||
<ArtistThumbnail :entity="artist"/>
|
||||
<template #thumbnail>
|
||||
<ArtistThumbnail :entity="artist" />
|
||||
</template>
|
||||
|
||||
<template v-slot:meta>
|
||||
<template #meta>
|
||||
<span>{{ pluralize(albumCount, 'album') }}</span>
|
||||
<span>{{ pluralize(songs, 'song') }}</span>
|
||||
<span>{{ duration }}</span>
|
||||
|
@ -18,7 +18,6 @@
|
|||
<a
|
||||
v-if="allowDownload"
|
||||
class="download"
|
||||
href
|
||||
role="button"
|
||||
title="Download all songs by this artist"
|
||||
@click.prevent="download"
|
||||
|
@ -27,11 +26,11 @@
|
|||
</a>
|
||||
</template>
|
||||
|
||||
<template v-slot:controls>
|
||||
<template #controls>
|
||||
<SongListControls
|
||||
v-if="songs.length && (!isPhone || showingControls)"
|
||||
@playAll="playAll"
|
||||
@playSelected="playSelected"
|
||||
@play-all="playAll"
|
||||
@play-selected="playSelected"
|
||||
/>
|
||||
</template>
|
||||
</ScreenHeader>
|
||||
|
@ -40,20 +39,20 @@
|
|||
<template #header>
|
||||
<label :class="{ active: activeTab === 'Songs' }">
|
||||
Songs
|
||||
<input type="radio" name="tab" value="Songs" v-model="activeTab"/>
|
||||
<input v-model="activeTab" type="radio" name="tab" value="Songs">
|
||||
</label>
|
||||
<label :class="{ active: activeTab === 'Albums' }">
|
||||
Albums
|
||||
<input type="radio" name="tab" value="Albums" v-model="activeTab"/>
|
||||
<input v-model="activeTab" type="radio" name="tab" value="Albums">
|
||||
</label>
|
||||
<label :class="{ active: activeTab === 'Info' }" v-if="useLastfm">
|
||||
<label v-if="useLastfm" :class="{ active: activeTab === 'Info' }">
|
||||
Information
|
||||
<input type="radio" name="tab" value="Info" v-model="activeTab"/>
|
||||
<input v-model="activeTab" type="radio" name="tab" value="Info">
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<div v-show="activeTab === 'Songs'" class="songs-pane">
|
||||
<SongListSkeleton v-if="loading"/>
|
||||
<SongListSkeleton v-if="loading" />
|
||||
<SongList
|
||||
v-else
|
||||
ref="songList"
|
||||
|
@ -66,18 +65,18 @@
|
|||
<div v-show="activeTab === 'Albums'" class="albums-pane">
|
||||
<ul v-if="albums" class="as-list">
|
||||
<li v-for="album in albums" :key="album.id">
|
||||
<AlbumCard :album="album" layout="compact"/>
|
||||
<AlbumCard :album="album" layout="compact" />
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-else class="as-list">
|
||||
<li v-for="i in 12" :key="i">
|
||||
<AlbumCardSkeleton layout="compact"/>
|
||||
<AlbumCardSkeleton layout="compact" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'Info'" class="info-pane" v-if="useLastfm && artist">
|
||||
<ArtistInfo :artist="artist" mode="full"/>
|
||||
<div v-show="activeTab === 'Info'" v-if="useLastfm && artist" class="info-pane">
|
||||
<ArtistInfo :artist="artist" mode="full" />
|
||||
</div>
|
||||
</ScreenTabs>
|
||||
</section>
|
||||
|
|
|
@ -2,20 +2,19 @@
|
|||
<section id="favoritesWrapper">
|
||||
<ScreenHeader :layout="songs.length === 0 ? 'collapsed' : headerLayout">
|
||||
Songs You Love
|
||||
<ControlsToggle v-model="showingControls"/>
|
||||
<ControlsToggle v-model="showingControls" />
|
||||
|
||||
<template v-slot:thumbnail>
|
||||
<ThumbnailStack :thumbnails="thumbnails"/>
|
||||
<template #thumbnail>
|
||||
<ThumbnailStack :thumbnails="thumbnails" />
|
||||
</template>
|
||||
|
||||
<template v-slot:meta v-if="songs.length">
|
||||
<template v-if="songs.length" #meta>
|
||||
<span>{{ pluralize(songs, 'song') }}</span>
|
||||
<span>{{ duration }}</span>
|
||||
|
||||
<a
|
||||
v-if="allowDownload"
|
||||
class="download"
|
||||
href
|
||||
role="button"
|
||||
title="Download all songs in playlist"
|
||||
@click.prevent="download"
|
||||
|
@ -24,16 +23,16 @@
|
|||
</a>
|
||||
</template>
|
||||
|
||||
<template v-slot:controls>
|
||||
<template #controls>
|
||||
<SongListControls
|
||||
v-if="songs.length && (!isPhone || showingControls)"
|
||||
@playAll="playAll"
|
||||
@playSelected="playSelected"
|
||||
@play-all="playAll"
|
||||
@play-selected="playSelected"
|
||||
/>
|
||||
</template>
|
||||
</ScreenHeader>
|
||||
|
||||
<SongListSkeleton v-if="loading"/>
|
||||
<SongListSkeleton v-if="loading" />
|
||||
<SongList
|
||||
v-if="songs.length"
|
||||
ref="songList"
|
||||
|
@ -44,13 +43,13 @@
|
|||
/>
|
||||
|
||||
<ScreenEmptyState v-else>
|
||||
<template v-slot:icon>
|
||||
<icon :icon="faHeartBroken"/>
|
||||
<template #icon>
|
||||
<icon :icon="faHeartBroken" />
|
||||
</template>
|
||||
No favorites yet.
|
||||
<span class="secondary d-block">
|
||||
Click the
|
||||
<icon :icon="faHeart"/>
|
||||
<icon :icon="faHeart" />
|
||||
icon to mark a song as favorite.
|
||||
</span>
|
||||
</ScreenEmptyState>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<section id="genresWrapper">
|
||||
<ScreenHeader layout="compact">
|
||||
<ScreenHeader layout="collapsed">
|
||||
Genres
|
||||
</ScreenHeader>
|
||||
<div class="main-scroll-wrap">
|
||||
<ul class="genres" v-if="genres">
|
||||
<ul v-if="genres" class="genres">
|
||||
<li v-for="genre in genres" :key="genre.name" :class="`level-${getLevel(genre)}`">
|
||||
<a
|
||||
:href="`/#/genres/${encodeURIComponent(genre.name)}`"
|
||||
|
@ -15,9 +15,9 @@
|
|||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="genres" v-else>
|
||||
<ul v-else class="genres">
|
||||
<li v-for="i in 20" :key="i">
|
||||
<GenreItemSkeleton/>
|
||||
<GenreItemSkeleton />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
<template>
|
||||
<section id="genreWrapper">
|
||||
<ScreenHeader :layout="headerLayout" v-if="genre">
|
||||
Genre: <span class="text-thin">{{ decodeURIComponent(name) }}</span>
|
||||
<ControlsToggle v-if="songs.length" v-model="showingControls"/>
|
||||
<ScreenHeader v-if="genre" :layout="headerLayout">
|
||||
Genre: <span class="text-thin">{{ decodeURIComponent(name!) }}</span>
|
||||
<ControlsToggle v-if="songs.length" v-model="showingControls" />
|
||||
|
||||
<template v-slot:thumbnail>
|
||||
<ThumbnailStack :thumbnails="thumbnails"/>
|
||||
<template #thumbnail>
|
||||
<ThumbnailStack :thumbnails="thumbnails" />
|
||||
</template>
|
||||
|
||||
<template v-if="genre" v-slot:meta>
|
||||
<template v-if="genre" #meta>
|
||||
<span>{{ pluralize(genre.song_count, 'song') }}</span>
|
||||
<span>{{ duration }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:controls>
|
||||
<SongListControls v-if="!isPhone || showingControls" @playAll="playAll" @playSelected="playSelected"/>
|
||||
<template #controls>
|
||||
<SongListControls v-if="!isPhone || showingControls" @play-all="playAll" @play-selected="playSelected" />
|
||||
</template>
|
||||
</ScreenHeader>
|
||||
<ScreenHeaderSkeleton v-else/>
|
||||
<ScreenHeaderSkeleton v-else />
|
||||
|
||||
<SongListSkeleton v-if="showSkeletons"/>
|
||||
<SongListSkeleton v-if="showSkeletons" />
|
||||
<SongList
|
||||
v-else
|
||||
ref="songList"
|
||||
|
@ -30,8 +30,8 @@
|
|||
/>
|
||||
|
||||
<ScreenEmptyState v-if="!songs.length && !loading">
|
||||
<template v-slot:icon>
|
||||
<icon :icon="faTags"/>
|
||||
<template #icon>
|
||||
<icon :icon="faTags" />
|
||||
</template>
|
||||
|
||||
No songs in this genre.
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
<div class="main-scroll-wrap" @scroll="scrolling">
|
||||
<ScreenEmptyState v-if="libraryEmpty">
|
||||
<template v-slot:icon>
|
||||
<icon :icon="faVolumeOff"/>
|
||||
<template #icon>
|
||||
<icon :icon="faVolumeOff" />
|
||||
</template>
|
||||
No songs found.
|
||||
<span class="secondary d-block">
|
||||
|
@ -15,19 +15,19 @@
|
|||
|
||||
<template v-else>
|
||||
<div class="two-cols">
|
||||
<MostPlayedSongs data-testid="most-played-songs" :loading="loading"/>
|
||||
<RecentlyPlayedSongs data-testid="recently-played-songs" :loading="loading"/>
|
||||
<MostPlayedSongs data-testid="most-played-songs" :loading="loading" />
|
||||
<RecentlyPlayedSongs data-testid="recently-played-songs" :loading="loading" />
|
||||
</div>
|
||||
|
||||
<div class="two-cols">
|
||||
<RecentlyAddedAlbums data-testid="recently-added-albums" :loading="loading"/>
|
||||
<RecentlyAddedSongs data-testid="recently-added-songs" :loading="loading"/>
|
||||
<RecentlyAddedAlbums data-testid="recently-added-albums" :loading="loading" />
|
||||
<RecentlyAddedSongs data-testid="recently-added-songs" :loading="loading" />
|
||||
</div>
|
||||
|
||||
<MostPlayedArtists data-testid="most-played-artists" :loading="loading"/>
|
||||
<MostPlayedAlbums data-testid="most-played-albums" :loading="loading"/>
|
||||
<MostPlayedArtists data-testid="most-played-artists" :loading="loading" />
|
||||
<MostPlayedAlbums data-testid="most-played-albums" :loading="loading" />
|
||||
|
||||
<ToTopButton/>
|
||||
<ToTopButton />
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
<div class="main-scroll-wrap">
|
||||
<ScreenEmptyState>
|
||||
<template v-slot:icon>
|
||||
<icon :icon="faKiwiBird" :mask="faMap" transform="shrink-12"/>
|
||||
<template #icon>
|
||||
<icon :icon="faKiwiBird" :mask="faMap" transform="shrink-12" />
|
||||
</template>
|
||||
|
||||
The requested content cannot be found.
|
||||
|
|
|
@ -2,18 +2,17 @@
|
|||
<section v-if="playlist" id="playlistWrapper">
|
||||
<ScreenHeader :layout="songs.length === 0 ? 'collapsed' : headerLayout" :disabled="loading">
|
||||
{{ playlist.name }}
|
||||
<ControlsToggle v-if="songs.length" v-model="showingControls"/>
|
||||
<ControlsToggle v-if="songs.length" v-model="showingControls" />
|
||||
|
||||
<template v-slot:thumbnail>
|
||||
<ThumbnailStack :thumbnails="thumbnails"/>
|
||||
<template #thumbnail>
|
||||
<ThumbnailStack :thumbnails="thumbnails" />
|
||||
</template>
|
||||
|
||||
<template v-if="songs.length" v-slot:meta>
|
||||
<template v-if="songs.length" #meta>
|
||||
<span>{{ pluralize(songs, 'song') }}</span>
|
||||
<span>{{ duration }}</span>
|
||||
<a
|
||||
v-if="allowDownload"
|
||||
href
|
||||
role="button"
|
||||
title="Download all songs in playlist"
|
||||
@click.prevent="download"
|
||||
|
@ -22,19 +21,19 @@
|
|||
</a>
|
||||
</template>
|
||||
|
||||
<template v-slot:controls>
|
||||
<template #controls>
|
||||
<SongListControls
|
||||
v-if="!isPhone || showingControls"
|
||||
:config="controlsConfig"
|
||||
@deletePlaylist="destroy"
|
||||
@playAll="playAll"
|
||||
@playSelected="playSelected"
|
||||
@delete-playlist="destroy"
|
||||
@play-all="playAll"
|
||||
@play-selected="playSelected"
|
||||
@refresh="fetchSongs(true)"
|
||||
/>
|
||||
</template>
|
||||
</ScreenHeader>
|
||||
|
||||
<SongListSkeleton v-show="loading"/>
|
||||
<SongListSkeleton v-show="loading" />
|
||||
<SongList
|
||||
v-if="!loading && songs.length"
|
||||
ref="songList"
|
||||
|
@ -45,8 +44,8 @@
|
|||
/>
|
||||
|
||||
<ScreenEmptyState v-if="!songs.length && !loading">
|
||||
<template v-slot:icon>
|
||||
<icon :icon="faFile"/>
|
||||
<template #icon>
|
||||
<icon :icon="faFile" />
|
||||
</template>
|
||||
|
||||
<template v-if="playlist?.is_smart">
|
||||
|
@ -111,9 +110,9 @@ const { removeSongsFromPlaylist } = usePlaylistManagement()
|
|||
|
||||
const allowDownload = toRef(commonStore.state, 'allow_download')
|
||||
|
||||
const destroy = () => eventBus.emit('PLAYLIST_DELETE', playlist.value)
|
||||
const destroy = () => eventBus.emit('PLAYLIST_DELETE', playlist.value!)
|
||||
const download = () => downloadService.fromPlaylist(playlist.value!)
|
||||
const editPlaylist = () => eventBus.emit('MODAL_SHOW_EDIT_PLAYLIST_FORM', playlist.value)
|
||||
const editPlaylist = () => eventBus.emit('MODAL_SHOW_EDIT_PLAYLIST_FORM', playlist.value!)
|
||||
|
||||
const removeSelected = async () => await removeSongsFromPlaylist(playlist.value!, selectedSongs.value)
|
||||
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
<ScreenHeader>Profile & Preferences</ScreenHeader>
|
||||
|
||||
<div class="main-scroll-wrap">
|
||||
<ProfileForm/>
|
||||
<ThemeList/>
|
||||
<PreferencesForm/>
|
||||
<LastfmIntegration/>
|
||||
<ProfileForm />
|
||||
<ThemeList />
|
||||
<PreferencesForm />
|
||||
<LastfmIntegration />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
@ -16,7 +16,8 @@ import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
|||
import ProfileForm from '@/components/profile-preferences/ProfileForm.vue'
|
||||
import LastfmIntegration from '@/components/profile-preferences/LastfmIntegration.vue'
|
||||
import PreferencesForm from '@/components/profile-preferences/PreferencesForm.vue'
|
||||
import ThemeList from '@/components/profile-preferences/ThemeList.vue'</script>
|
||||
import ThemeList from '@/components/profile-preferences/ThemeList.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#profileWrapper {
|
||||
|
|
|
@ -2,29 +2,29 @@
|
|||
<section id="queueWrapper">
|
||||
<ScreenHeader :layout="songs.length === 0 ? 'collapsed' : headerLayout">
|
||||
Current Queue
|
||||
<ControlsToggle v-model="showingControls"/>
|
||||
<ControlsToggle v-model="showingControls" />
|
||||
|
||||
<template v-slot:thumbnail>
|
||||
<ThumbnailStack :thumbnails="thumbnails"/>
|
||||
<template #thumbnail>
|
||||
<ThumbnailStack :thumbnails="thumbnails" />
|
||||
</template>
|
||||
|
||||
<template v-if="songs.length" v-slot:meta>
|
||||
<template v-if="songs.length" #meta>
|
||||
<span>{{ pluralize(songs, 'song') }}</span>
|
||||
<span>{{ duration }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:controls>
|
||||
<template #controls>
|
||||
<SongListControls
|
||||
v-if="songs.length && (!isPhone || showingControls)"
|
||||
:config="controlConfig"
|
||||
@clearQueue="clearQueue"
|
||||
@playAll="playAll"
|
||||
@playSelected="playSelected"
|
||||
@clear-queue="clearQueue"
|
||||
@play-all="playAll"
|
||||
@play-selected="playSelected"
|
||||
/>
|
||||
</template>
|
||||
</ScreenHeader>
|
||||
|
||||
<SongListSkeleton v-if="loading"/>
|
||||
<SongListSkeleton v-if="loading" />
|
||||
<SongList
|
||||
v-if="songs.length"
|
||||
ref="songList"
|
||||
|
@ -35,8 +35,8 @@
|
|||
/>
|
||||
|
||||
<ScreenEmptyState v-else>
|
||||
<template v-slot:icon>
|
||||
<icon :icon="faCoffee"/>
|
||||
<template #icon>
|
||||
<icon :icon="faCoffee" />
|
||||
</template>
|
||||
|
||||
No songs queued.
|
||||
|
|
|
@ -2,33 +2,33 @@
|
|||
<section id="recentlyPlayedWrapper">
|
||||
<ScreenHeader :layout="songs.length === 0 ? 'collapsed' : headerLayout">
|
||||
Recently Played
|
||||
<ControlsToggle v-model="showingControls"/>
|
||||
<ControlsToggle v-model="showingControls" />
|
||||
|
||||
<template v-slot:thumbnail>
|
||||
<ThumbnailStack :thumbnails="thumbnails"/>
|
||||
<template #thumbnail>
|
||||
<ThumbnailStack :thumbnails="thumbnails" />
|
||||
</template>
|
||||
|
||||
<template v-slot:meta v-if="songs.length">
|
||||
<template v-if="songs.length" #meta>
|
||||
<span>{{ pluralize(songs, 'song') }}</span>
|
||||
<span>{{ duration }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:controls>
|
||||
<template #controls>
|
||||
<SongListControls
|
||||
v-if="songs.length && (!isPhone || showingControls)"
|
||||
@playAll="playAll"
|
||||
@playSelected="playSelected"
|
||||
@play-all="playAll"
|
||||
@play-selected="playSelected"
|
||||
/>
|
||||
</template>
|
||||
</ScreenHeader>
|
||||
|
||||
<SongListSkeleton v-if="loading"/>
|
||||
<SongListSkeleton v-if="loading" />
|
||||
|
||||
<SongList v-if="songs.length" ref="songList" @press:enter="onPressEnter" @scroll-breakpoint="onScrollBreakpoint"/>
|
||||
<SongList v-if="songs.length" ref="songList" @press:enter="onPressEnter" @scroll-breakpoint="onScrollBreakpoint" />
|
||||
|
||||
<ScreenEmptyState v-else>
|
||||
<template v-slot:icon>
|
||||
<icon :icon="faClock"/>
|
||||
<template #icon>
|
||||
<icon :icon="faClock" />
|
||||
</template>
|
||||
No songs recently played.
|
||||
<span class="secondary d-block">Start playing to populate this playlist.</span>
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
<ScreenHeader layout="collapsed">
|
||||
Upload Media
|
||||
|
||||
<template v-slot:controls>
|
||||
<BtnGroup uppercased v-if="hasUploadFailures">
|
||||
<template #controls>
|
||||
<BtnGroup v-if="hasUploadFailures" uppercased>
|
||||
<Btn data-testid="upload-retry-all-btn" green @click="retryAll">
|
||||
<icon :icon="faRotateRight"/>
|
||||
<icon :icon="faRotateRight" />
|
||||
Retry All
|
||||
</Btn>
|
||||
<Btn data-testid="upload-remove-all-btn" orange @click="removeFailedEntries">
|
||||
<icon :icon="faTrashCan"/>
|
||||
<icon :icon="faTrashCan" />
|
||||
Remove Failed
|
||||
</Btn>
|
||||
</BtnGroup>
|
||||
|
@ -27,13 +27,13 @@
|
|||
@drop.prevent="onDrop"
|
||||
@dragover.prevent
|
||||
>
|
||||
<div class="upload-files" v-if="files.length">
|
||||
<UploadItem v-for="file in files" :key="file.id" :file="file" data-testid="upload-item"/>
|
||||
<div v-if="files.length" class="upload-files">
|
||||
<UploadItem v-for="file in files" :key="file.id" :file="file" data-testid="upload-item" />
|
||||
</div>
|
||||
|
||||
<ScreenEmptyState v-else>
|
||||
<template v-slot:icon>
|
||||
<icon :icon="faUpload"/>
|
||||
<template #icon>
|
||||
<icon :icon="faUpload" />
|
||||
</template>
|
||||
|
||||
{{ canDropFolders ? 'Drop files or folders to upload' : 'Drop files to upload' }}
|
||||
|
@ -41,15 +41,15 @@
|
|||
<span class="secondary d-block">
|
||||
<a class="or-click d-block" role="button">
|
||||
or click here to select songs
|
||||
<input :accept="acceptAttribute" multiple name="file[]" type="file" @change="onFileInputChange"/>
|
||||
<input :accept="acceptAttribute" multiple name="file[]" type="file" @change="onFileInputChange">
|
||||
</a>
|
||||
</span>
|
||||
</ScreenEmptyState>
|
||||
</div>
|
||||
|
||||
<ScreenEmptyState v-else>
|
||||
<template v-slot:icon>
|
||||
<icon :icon="faWarning"/>
|
||||
<template #icon>
|
||||
<icon :icon="faWarning" />
|
||||
</template>
|
||||
No media path set.
|
||||
</ScreenEmptyState>
|
||||
|
@ -85,7 +85,7 @@ const hasUploadFailures = computed(() => files.value.filter((file) => file.statu
|
|||
const onDragEnter = () => (droppable.value = allowsUpload.value)
|
||||
const onDragLeave = () => (droppable.value = false)
|
||||
|
||||
const onFileInputChange = (event: InputEvent) => {
|
||||
const onFileInputChange = (event: Event) => {
|
||||
const selectedFileList = (event.target as HTMLInputElement).files
|
||||
|
||||
if (selectedFileList?.length) {
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
<section id="usersWrapper">
|
||||
<ScreenHeader layout="collapsed">
|
||||
Users
|
||||
<ControlsToggle v-model="showingControls"/>
|
||||
<ControlsToggle v-model="showingControls" />
|
||||
|
||||
<template v-slot:controls>
|
||||
<BtnGroup uppercased v-if="showingControls || !isPhone">
|
||||
<template #controls>
|
||||
<BtnGroup v-if="showingControls || !isPhone" uppercased>
|
||||
<Btn class="btn-add" green @click="showAddUserForm">
|
||||
<icon :icon="faPlus"/>
|
||||
<icon :icon="faPlus" />
|
||||
Add
|
||||
</Btn>
|
||||
</BtnGroup>
|
||||
|
@ -17,7 +17,7 @@
|
|||
<div class="main-scroll-wrap">
|
||||
<ul class="users">
|
||||
<li v-for="user in users" :key="user.id">
|
||||
<UserCard :user="user"/>
|
||||
<UserCard :user="user" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<section id="vizContainer" :class="{ fullscreen: isFullscreen }" @dblclick="toggleFullscreen">
|
||||
<div class="artifacts">
|
||||
<div class="credits" v-if="selectedVisualizer">
|
||||
<div v-if="selectedVisualizer" class="credits">
|
||||
<h3>{{ selectedVisualizer.name }}</h3>
|
||||
<p class="text-secondary" v-if="selectedVisualizer.credits">
|
||||
<p v-if="selectedVisualizer.credits" class="text-secondary">
|
||||
by {{ selectedVisualizer.credits.author }}
|
||||
<a :href="selectedVisualizer.credits.url" target="_blank">
|
||||
<icon :icon="faUpRightFromSquare"/>
|
||||
<icon :icon="faUpRightFromSquare" />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -16,7 +16,7 @@
|
|||
<option v-for="v in visualizers" :key="v.id" :value="v.id">{{ v.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div ref="el" class="viz"/>
|
||||
<div ref="el" class="viz" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
<div id="player">
|
||||
<ScreenEmptyState data-testid="youtube-placeholder">
|
||||
<template v-slot:icon>
|
||||
<icon :icon="faYoutube"/>
|
||||
<template #icon>
|
||||
<icon :icon="faYoutube" />
|
||||
</template>
|
||||
YouTube videos will be played here.
|
||||
<span class="d-block instruction">Start a video playback from the right sidebar.</span>
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
|
||||
<ol v-if="loading" class="two-cols top-album-list">
|
||||
<li v-for="i in 4" :key="i">
|
||||
<AlbumCardSkeleton layout="compact"/>
|
||||
<AlbumCardSkeleton layout="compact" />
|
||||
</li>
|
||||
</ol>
|
||||
<template v-else>
|
||||
<ol v-if="albums.length" class="two-cols top-album-list">
|
||||
<li v-for="album in albums" :key="album.id">
|
||||
<AlbumCard :album="album" layout="compact"/>
|
||||
<AlbumCard :album="album" layout="compact" />
|
||||
</li>
|
||||
</ol>
|
||||
<p v-else class="text-secondary">No albums found.</p>
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
|
||||
<ol v-if="loading" class="two-cols top-album-list">
|
||||
<li v-for="i in 4" :key="i">
|
||||
<ArtistCardSkeleton layout="compact"/>
|
||||
<ArtistCardSkeleton layout="compact" />
|
||||
</li>
|
||||
</ol>
|
||||
<template v-else>
|
||||
<ol v-if="artists.length" class="two-cols top-artist-list">
|
||||
<li v-for="artist in artists" :key="artist.id">
|
||||
<ArtistCard :artist="artist" layout="compact"/>
|
||||
<ArtistCard :artist="artist" layout="compact" />
|
||||
</li>
|
||||
</ol>
|
||||
<p v-else class="text-secondary">No artists found.</p>
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
<h1>Most Played</h1>
|
||||
<ol v-if="loading" class="top-song-list">
|
||||
<li v-for="i in 3" :key="i">
|
||||
<SongCardSkeleton/>
|
||||
<SongCardSkeleton />
|
||||
</li>
|
||||
</ol>
|
||||
<template v-else>
|
||||
<ol v-if="songs.length" class="top-song-list">
|
||||
<li v-for="song in songs" :key="song.id">
|
||||
<SongCard :song="song"/>
|
||||
<SongCard :song="song" />
|
||||
</li>
|
||||
</ol>
|
||||
<p v-else class="text-secondary">You don’t seem to have been playing.</p>
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
|
||||
<ol v-if="loading" class="recently-added-album-list">
|
||||
<li v-for="i in 2" :key="i">
|
||||
<AlbumCardSkeleton layout="compact"/>
|
||||
<AlbumCardSkeleton layout="compact" />
|
||||
</li>
|
||||
</ol>
|
||||
<template v-else>
|
||||
<ol v-if="albums.length" class="recently-added-album-list">
|
||||
<li v-for="album in albums" :key="album.id">
|
||||
<AlbumCard :album="album" layout="compact"/>
|
||||
<AlbumCard :album="album" layout="compact" />
|
||||
</li>
|
||||
</ol>
|
||||
<p v-else class="text-secondary">No albums added yet.</p>
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
<h1>New Songs</h1>
|
||||
<ol v-if="loading" class="recently-added-song-list">
|
||||
<li v-for="i in 3" :key="i">
|
||||
<SongCardSkeleton/>
|
||||
<SongCardSkeleton />
|
||||
</li>
|
||||
</ol>
|
||||
<template v-else>
|
||||
<ol v-if="songs.length" class="recently-added-song-list">
|
||||
<li v-for="song in songs" :key="song.id">
|
||||
<SongCard :song="song"/>
|
||||
<SongCard :song="song" />
|
||||
</li>
|
||||
</ol>
|
||||
<p v-else class="text-secondary">No songs added so far.</p>
|
||||
|
|
|
@ -15,13 +15,13 @@
|
|||
|
||||
<ol v-if="loading" class="recent-song-list">
|
||||
<li v-for="i in 3" :key="i">
|
||||
<SongCardSkeleton/>
|
||||
<SongCardSkeleton />
|
||||
</li>
|
||||
</ol>
|
||||
<template v-else>
|
||||
<ol v-if="songs.length" class="recent-song-list">
|
||||
<li v-for="song in songs" :key="song.id">
|
||||
<SongCard :song="song"/>
|
||||
<SongCard :song="song" />
|
||||
</li>
|
||||
</ol>
|
||||
<p v-else class="text-secondary">No songs played as of late.</p>
|
||||
|
|
|
@ -23,13 +23,13 @@
|
|||
</h1>
|
||||
<ul v-if="searching">
|
||||
<li v-for="i in 6" :key="i">
|
||||
<SongCardSkeleton/>
|
||||
<SongCardSkeleton />
|
||||
</li>
|
||||
</ul>
|
||||
<template v-else>
|
||||
<ul v-if="excerpt.songs.length">
|
||||
<li v-for="song in excerpt.songs" :key="song.id">
|
||||
<SongCard :song="song"/>
|
||||
<SongCard :song="song" />
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else>None found.</p>
|
||||
|
@ -40,13 +40,13 @@
|
|||
<h1>Artists</h1>
|
||||
<ul v-if="searching">
|
||||
<li v-for="i in 6" :key="i">
|
||||
<ArtistAlbumCardSkeleton layout="compact"/>
|
||||
<ArtistAlbumCardSkeleton layout="compact" />
|
||||
</li>
|
||||
</ul>
|
||||
<template v-else>
|
||||
<ul v-if="excerpt.artists.length">
|
||||
<li v-for="artist in excerpt.artists" :key="artist.id">
|
||||
<ArtistCard :artist="artist" layout="compact"/>
|
||||
<ArtistCard :artist="artist" layout="compact" />
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else>None found.</p>
|
||||
|
@ -57,13 +57,13 @@
|
|||
<h1>Albums</h1>
|
||||
<ul v-if="searching">
|
||||
<li v-for="i in 6" :key="i">
|
||||
<ArtistAlbumCardSkeleton layout="compact"/>
|
||||
<ArtistAlbumCardSkeleton layout="compact" />
|
||||
</li>
|
||||
</ul>
|
||||
<template v-else>
|
||||
<ul v-if="excerpt.albums.length">
|
||||
<li v-for="album in excerpt.albums" :key="album.id">
|
||||
<AlbumCard :album="album" layout="compact"/>
|
||||
<AlbumCard :album="album" layout="compact" />
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else>None found.</p>
|
||||
|
@ -72,8 +72,8 @@
|
|||
</div>
|
||||
|
||||
<ScreenEmptyState v-else>
|
||||
<template v-slot:icon>
|
||||
<icon :icon="faSearch"/>
|
||||
<template #icon>
|
||||
<icon :icon="faSearch" />
|
||||
</template>
|
||||
Find songs, artists, and albums,
|
||||
<span class="secondary d-block">all in one place.</span>
|
||||
|
|
|
@ -2,28 +2,28 @@
|
|||
<section id="songResultsWrapper">
|
||||
<ScreenHeader :layout="songs.length === 0 ? 'collapsed' : headerLayout">
|
||||
Songs for <span class="text-thin">{{ decodedQ }}</span>
|
||||
<ControlsToggle v-model="showingControls"/>
|
||||
<ControlsToggle v-model="showingControls" />
|
||||
|
||||
<template v-slot:thumbnail>
|
||||
<ThumbnailStack :thumbnails="thumbnails"/>
|
||||
<template #thumbnail>
|
||||
<ThumbnailStack :thumbnails="thumbnails" />
|
||||
</template>
|
||||
|
||||
<template v-if="songs.length" v-slot:meta>
|
||||
<template v-if="songs.length" #meta>
|
||||
<span>{{ pluralize(songs, 'song') }}</span>
|
||||
<span>{{ duration }}</span>
|
||||
</template>
|
||||
|
||||
<template v-slot:controls>
|
||||
<template #controls>
|
||||
<SongListControls
|
||||
v-if="songs.length && (!isPhone || showingControls)"
|
||||
@playAll="playAll"
|
||||
@playSelected="playSelected"
|
||||
@play-all="playAll"
|
||||
@play-selected="playSelected"
|
||||
/>
|
||||
</template>
|
||||
</ScreenHeader>
|
||||
|
||||
<SongListSkeleton v-if="loading"/>
|
||||
<SongList v-else ref="songList" @sort="sort" @press:enter="onPressEnter" @scroll-breakpoint="onScrollBreakpoint"/>
|
||||
<SongListSkeleton v-if="loading" />
|
||||
<SongList v-else ref="songList" @sort="sort" @press:enter="onPressEnter" @scroll-breakpoint="onScrollBreakpoint" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<form @submit.prevent="submit" @keydown.esc="maybeClose">
|
||||
<header>
|
||||
<span class="cover" :style="{ backgroundImage: `url(${coverUrl})` }"/>
|
||||
<span class="cover" :style="{ backgroundImage: `url(${coverUrl})` }" />
|
||||
<div class="meta">
|
||||
<h1 :class="{ mixed: !editingOnlyOneSong }">{{ displayedTitle }}</h1>
|
||||
<h2
|
||||
|
@ -140,7 +140,7 @@
|
|||
list="genres"
|
||||
>
|
||||
<datalist id="genres">
|
||||
<option v-for="genre in genres" :key="genre" :value="genre"/>
|
||||
<option v-for="genre in genres" :key="genre" :value="genre" />
|
||||
</datalist>
|
||||
</label>
|
||||
<label>
|
||||
|
@ -165,13 +165,13 @@
|
|||
tabindex="0"
|
||||
>
|
||||
<div class="form-row">
|
||||
<textarea
|
||||
v-model="formData.lyrics"
|
||||
v-koel-focus
|
||||
data-testid="lyrics-input"
|
||||
name="lyrics"
|
||||
title="Lyrics"
|
||||
/>
|
||||
<textarea
|
||||
v-model="formData.lyrics"
|
||||
v-koel-focus
|
||||
data-testid="lyrics-input"
|
||||
name="lyrics"
|
||||
title="Lyrics"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
@contextmenu.prevent="requestContextMenu"
|
||||
@dblclick.prevent="play"
|
||||
>
|
||||
<SongThumbnail :song="song"/>
|
||||
<SongThumbnail :song="song" />
|
||||
<main>
|
||||
<div class="details">
|
||||
<h3>{{ song.title }}</h3>
|
||||
|
@ -16,7 +16,7 @@
|
|||
- {{ pluralize(song.play_count, 'play') }}
|
||||
</p>
|
||||
</div>
|
||||
<LikeButton :song="song"/>
|
||||
<LikeButton :song="song" />
|
||||
</main>
|
||||
</article>
|
||||
</template>
|
||||
|
|
|
@ -18,24 +18,24 @@
|
|||
</template>
|
||||
<li v-else @click="queueSongsToBottom">Queue</li>
|
||||
<template v-if="!isFavoritesScreen">
|
||||
<li class="separator"/>
|
||||
<li class="separator" />
|
||||
<li @click="addSongsToFavorite">Favorites</li>
|
||||
</template>
|
||||
<li v-if="normalPlaylists.length" class="separator"/>
|
||||
<li v-if="normalPlaylists.length" class="separator" />
|
||||
<li v-for="p in normalPlaylists" :key="p.id" @click="addSongsToExistingPlaylist(p)">{{ p.name }}</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<template v-if="isQueueScreen">
|
||||
<li class="separator"/>
|
||||
<li class="separator" />
|
||||
<li @click="removeFromQueue">Remove from Queue</li>
|
||||
<li class="separator"/>
|
||||
<li class="separator" />
|
||||
</template>
|
||||
|
||||
<template v-if="isFavoritesScreen">
|
||||
<li class="separator"/>
|
||||
<li class="separator" />
|
||||
<li @click="removeFromFavorites">Remove from Favorites</li>
|
||||
<li class="separator"/>
|
||||
<li class="separator" />
|
||||
</template>
|
||||
|
||||
<li v-if="isAdmin" @click="openEditForm">Edit</li>
|
||||
|
@ -43,12 +43,12 @@
|
|||
<li v-if="onlyOneSongSelected" @click="copyUrl">Copy Shareable URL</li>
|
||||
|
||||
<template v-if="canBeRemovedFromPlaylist">
|
||||
<li class="separator"/>
|
||||
<li class="separator" />
|
||||
<li @click="removeFromPlaylist">Remove from Playlist</li>
|
||||
</template>
|
||||
|
||||
<template v-if="isAdmin">
|
||||
<li class="separator"/>
|
||||
<li class="separator" />
|
||||
<li @click="deleteFromFilesystem">Delete from Filesystem</li>
|
||||
</template>
|
||||
</ContextMenuBase>
|
||||
|
@ -73,7 +73,7 @@ const { toastSuccess } = useMessageToaster()
|
|||
const { showConfirmDialog } = useDialogBox()
|
||||
const { go, getRouteParam, isCurrentScreen } = useRouter()
|
||||
const { isAdmin } = useAuthorization()
|
||||
const { context, base, ContextMenuBase, open, close, trigger } = useContextMenu()
|
||||
const { base, ContextMenuBase, open, close, trigger } = useContextMenu()
|
||||
const { removeSongsFromPlaylist } = usePlaylistManagement()
|
||||
|
||||
const songs = ref<Song[]>([])
|
||||
|
@ -154,6 +154,6 @@ const deleteFromFilesystem = () => trigger(async () => {
|
|||
|
||||
eventBus.on('SONG_CONTEXT_MENU_REQUESTED', async (e, _songs) => {
|
||||
songs.value = arrayify(_songs)
|
||||
await open(e.pageY, e.pageX, { songs: songs.value })
|
||||
await open(e.pageY, e.pageX)
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<button :title="title" type="button" @click.stop="toggleLike">
|
||||
<icon v-if="song.liked" :icon="faHeart"/>
|
||||
<icon v-else :icon="faEmptyHeart"/>
|
||||
<icon v-if="song.liked" :icon="faHeart" />
|
||||
<icon v-else :icon="faEmptyHeart" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
>
|
||||
#
|
||||
<template v-if="config.sortable">
|
||||
<icon v-if="sortField === 'track' && sortOrder === 'asc'" :icon="faCaretDown" class="text-highlight"/>
|
||||
<icon v-if="sortField === 'track' && sortOrder === 'desc'" :icon="faCaretUp" class="text-highlight"/>
|
||||
<icon v-if="sortField === 'track' && sortOrder === 'asc'" :icon="faCaretDown" class="text-highlight" />
|
||||
<icon v-if="sortField === 'track' && sortOrder === 'desc'" :icon="faCaretUp" class="text-highlight" />
|
||||
</template>
|
||||
</span>
|
||||
<span
|
||||
|
@ -31,8 +31,8 @@
|
|||
>
|
||||
Title
|
||||
<template v-if="config.sortable">
|
||||
<icon v-if="sortField === 'title' && sortOrder === 'asc'" :icon="faCaretDown" class="text-highlight"/>
|
||||
<icon v-if="sortField === 'title' && sortOrder === 'desc'" :icon="faCaretUp" class="text-highlight"/>
|
||||
<icon v-if="sortField === 'title' && sortOrder === 'asc'" :icon="faCaretDown" class="text-highlight" />
|
||||
<icon v-if="sortField === 'title' && sortOrder === 'desc'" :icon="faCaretUp" class="text-highlight" />
|
||||
</template>
|
||||
</span>
|
||||
<span
|
||||
|
@ -44,8 +44,8 @@
|
|||
>
|
||||
Album
|
||||
<template v-if="config.sortable">
|
||||
<icon v-if="sortField === 'album_name' && sortOrder === 'asc'" :icon="faCaretDown" class="text-highlight"/>
|
||||
<icon v-if="sortField === 'album_name' && sortOrder === 'desc'" :icon="faCaretUp" class="text-highlight"/>
|
||||
<icon v-if="sortField === 'album_name' && sortOrder === 'asc'" :icon="faCaretDown" class="text-highlight" />
|
||||
<icon v-if="sortField === 'album_name' && sortOrder === 'desc'" :icon="faCaretUp" class="text-highlight" />
|
||||
</template>
|
||||
</span>
|
||||
<span
|
||||
|
@ -57,12 +57,12 @@
|
|||
>
|
||||
Time
|
||||
<template v-if="config.sortable">
|
||||
<icon v-if="sortField === 'length' && sortOrder === 'asc'" :icon="faCaretDown" class="text-highlight"/>
|
||||
<icon v-if="sortField === 'length' && sortOrder === 'desc'" :icon="faCaretUp" class="text-highlight"/>
|
||||
<icon v-if="sortField === 'length' && sortOrder === 'asc'" :icon="faCaretDown" class="text-highlight" />
|
||||
<icon v-if="sortField === 'length' && sortOrder === 'desc'" :icon="faCaretUp" class="text-highlight" />
|
||||
</template>
|
||||
</span>
|
||||
<span class="extra">
|
||||
<SongListSorter v-if="config.sortable" :field="sortField" :order="sortOrder" @sort="sort"/>
|
||||
<SongListSorter v-if="config.sortable" :field="sortField" :order="sortOrder" @sort="sort" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
@ -243,7 +243,7 @@ const onDragStart = (row: SongRow, event: DragEvent) => {
|
|||
|
||||
// Add "dragging" class to the wrapper so that we can disable pointer events on child elements.
|
||||
// This prevents dragleave events from firing when the user drags the mouse over the child elements.
|
||||
wrapper.value.classList.add('dragging')
|
||||
wrapper.value?.classList.add('dragging')
|
||||
|
||||
startDragging(event, selectedSongs.value)
|
||||
}
|
||||
|
@ -261,11 +261,11 @@ const onDragEnter = (event: DragEvent) => {
|
|||
|
||||
const onDrop = (item: SongRow, event: DragEvent) => {
|
||||
if (!config.reorderable || !getDroppedData(event) || !selectedSongs.value.length) {
|
||||
wrapper.value.classList.remove('dragging')
|
||||
wrapper.value?.classList.remove('dragging')
|
||||
return onDragLeave(event)
|
||||
}
|
||||
|
||||
wrapper.value.classList.remove('dragging')
|
||||
wrapper.value?.classList.remove('dragging')
|
||||
|
||||
emit('reorder', item.song)
|
||||
return onDragLeave(event)
|
||||
|
@ -276,7 +276,7 @@ const onDragLeave = (event: DragEvent) => {
|
|||
return false
|
||||
}
|
||||
|
||||
const onDragEnd = () => wrapper.value.classList.remove('dragging')
|
||||
const onDragEnd = () => wrapper.value?.classList.remove('dragging')
|
||||
|
||||
const openContextMenu = async (row: SongRow, event: MouseEvent) => {
|
||||
if (!row.selected) {
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
title="Play all songs"
|
||||
@click.prevent="playAll"
|
||||
>
|
||||
<icon :icon="faPlay" fixed-width/>
|
||||
<icon :icon="faPlay" fixed-width />
|
||||
All
|
||||
</Btn>
|
||||
|
||||
|
@ -22,7 +22,7 @@
|
|||
title="Play selected songs"
|
||||
@click.prevent="playSelected"
|
||||
>
|
||||
<icon :icon="faPlay" fixed-width/>
|
||||
<icon :icon="faPlay" fixed-width />
|
||||
Selected
|
||||
</Btn>
|
||||
</template>
|
||||
|
@ -36,7 +36,7 @@
|
|||
title="Shuffle all songs"
|
||||
@click.prevent="shuffle"
|
||||
>
|
||||
<icon :icon="faRandom" fixed-width/>
|
||||
<icon :icon="faRandom" fixed-width />
|
||||
All
|
||||
</Btn>
|
||||
|
||||
|
@ -48,7 +48,7 @@
|
|||
title="Shuffle selected songs"
|
||||
@click.prevent="shuffleSelected"
|
||||
>
|
||||
<icon :icon="faRandom" fixed-width/>
|
||||
<icon :icon="faRandom" fixed-width />
|
||||
Selected
|
||||
</Btn>
|
||||
</template>
|
||||
|
@ -63,7 +63,7 @@
|
|||
|
||||
<BtnGroup>
|
||||
<Btn v-if="config.refresh" v-koel-tooltip green title="Refresh" @click.prevent="refresh">
|
||||
<icon :icon="faRotateRight" fixed-width/>
|
||||
<icon :icon="faRotateRight" fixed-width />
|
||||
</Btn>
|
||||
|
||||
<Btn
|
||||
|
@ -74,13 +74,13 @@
|
|||
title="Delete this playlist"
|
||||
@click.prevent="deletePlaylist"
|
||||
>
|
||||
<icon :icon="faTrashCan"/>
|
||||
<icon :icon="faTrashCan" />
|
||||
</Btn>
|
||||
</BtnGroup>
|
||||
</div>
|
||||
|
||||
<div ref="addToMenu" v-koel-clickaway="closeAddToMenu" class="menu-wrapper">
|
||||
<AddToMenu :config="mergedConfig.addTo" :songs="selectedSongs" @closing="closeAddToMenu"/>
|
||||
<AddToMenu :config="mergedConfig.addTo" :songs="selectedSongs" @closing="closeAddToMenu" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -103,7 +103,7 @@ const [songs] = requireInjection<[Ref<Song[]>]>(SongsKey)
|
|||
const [selectedSongs] = requireInjection(SelectedSongsKey)
|
||||
|
||||
const el = ref<HTMLElement>()
|
||||
const addToButton = ref<InstanceType<Btn>>()
|
||||
const addToButton = ref<InstanceType<typeof Btn>>()
|
||||
const addToMenu = ref<HTMLDivElement>()
|
||||
const showingAddToMenu = ref(false)
|
||||
const altPressed = ref(false)
|
||||
|
@ -147,7 +147,7 @@ watch(showAddToButton, async showingButton => {
|
|||
await nextTick()
|
||||
|
||||
if (showingButton) {
|
||||
usedFloatingUi = useFloatingUi(addToButton.value.button, addToMenu, { autoTrigger: false })
|
||||
usedFloatingUi = useFloatingUi(addToButton.value!.button!, addToMenu, { autoTrigger: false })
|
||||
usedFloatingUi.setup()
|
||||
} else {
|
||||
usedFloatingUi?.teardown()
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
@dblclick.prevent.stop="play"
|
||||
>
|
||||
<span class="track-number">
|
||||
<SoundBars v-if="song.playback_state === 'Playing'"/>
|
||||
<SoundBars v-if="song.playback_state === 'Playing'" />
|
||||
<span v-else class="text-secondary">{{ song.track || '' }}</span>
|
||||
</span>
|
||||
<span class="thumbnail">
|
||||
<SongThumbnail :song="song"/>
|
||||
<SongThumbnail :song="song" />
|
||||
</span>
|
||||
<span class="title-artist">
|
||||
<span class="title text-primary">{{ song.title }}</span>
|
||||
|
@ -22,7 +22,7 @@
|
|||
<span class="album">{{ song.album_name }}</span>
|
||||
<span class="time">{{ fmtLength }}</span>
|
||||
<span class="extra">
|
||||
<LikeButton :song="song"/>
|
||||
<LikeButton :song="song" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
<template>
|
||||
<div>
|
||||
<button ref="button" title="Sort" @click.stop="trigger">
|
||||
<icon :icon="faSort"/>
|
||||
<icon :icon="faSort" />
|
||||
</button>
|
||||
<menu ref="menu" v-koel-clickaway="hide">
|
||||
<li v-for="item in menuItems" :class="item.field === field && 'active'" @click="sort(item.field)">
|
||||
<li
|
||||
v-for="item in menuItems"
|
||||
:key="item.label"
|
||||
:class="item.field === field && 'active'"
|
||||
@click="sort(item.field)"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
<span class="icon">
|
||||
<icon v-if="order === 'asc'" :icon="faArrowDown"/>
|
||||
<icon v-else :icon="faArrowUp"/>
|
||||
</span>
|
||||
<icon v-if="order === 'asc'" :icon="faArrowDown" />
|
||||
<icon v-else :icon="faArrowUp" />
|
||||
</span>
|
||||
</li>
|
||||
</menu>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div :style="{ backgroundImage: `url(${defaultCover})` }" class="cover">
|
||||
<img v-koel-hide-broken-icon :alt="song.album_name" :src="song.album_cover" loading="lazy"/>
|
||||
<img v-koel-hide-broken-icon :alt="song.album_name" :src="song.album_cover" loading="lazy">
|
||||
<a :title="title" class="control" role="button" @click.prevent="changeSongState">
|
||||
<icon :icon="song.playback_state === 'Playing' ? faPause : faPlay" class="text-highlight"/>
|
||||
<icon :icon="song.playback_state === 'Playing' ? faPause : faPlay" class="text-highlight" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div :style="{ backgroundImage: thumbnailUrl ? `url(${thumbnailUrl})` : 'none' }" data-testid="album-art-overlay"/>
|
||||
<div :style="{ backgroundImage: thumbnailUrl ? `url(${thumbnailUrl})` : 'none' }" data-testid="album-art-overlay" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
|
@ -5,10 +5,9 @@
|
|||
class="cover"
|
||||
data-testid="album-artist-thumbnail"
|
||||
>
|
||||
<img v-koel-hide-broken-icon :alt="entity.name" :src="image" loading="lazy"/>
|
||||
<img v-koel-hide-broken-icon :alt="entity.name" :src="image" loading="lazy">
|
||||
<a
|
||||
class="control control-play"
|
||||
href
|
||||
role="button"
|
||||
@click.prevent="playOrQueue"
|
||||
@dragenter.prevent="onDragEnter"
|
||||
|
@ -17,7 +16,7 @@
|
|||
@dragover.prevent
|
||||
>
|
||||
<span class="hidden">{{ buttonLabel }}</span>
|
||||
<span class="icon"/>
|
||||
<span class="icon" />
|
||||
</a>
|
||||
</span>
|
||||
</template>
|
||||
|
@ -57,7 +56,7 @@ const buttonLabel = computed(() => forAlbum.value
|
|||
|
||||
const { isAdmin: allowsUpload } = useAuthorization()
|
||||
|
||||
const playOrQueue = async (event: KeyboardEvent) => {
|
||||
const playOrQueue = async (event: MouseEvent) => {
|
||||
const songs = forAlbum.value
|
||||
? await songStore.fetchForAlbum(entity.value as Album)
|
||||
: await songStore.fetchForArtist(entity.value as Artist)
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
fill-rule="nonzero"
|
||||
stroke="none"
|
||||
stroke-width="1"
|
||||
></path>
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</template>
|
||||
|
@ -20,7 +20,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { toRefs } from 'vue'
|
||||
|
||||
const props = defineProps({ url: String })
|
||||
const props = defineProps<{ url: string }>()
|
||||
const { url } = toRefs(props)
|
||||
</script>
|
||||
|
||||
|
|
|
@ -8,13 +8,13 @@
|
|||
@dragstart="onDragStart"
|
||||
@contextmenu.prevent="onContextMenu"
|
||||
>
|
||||
<AlbumArtistThumbnail :entity="entity"/>
|
||||
<AlbumArtistThumbnail :entity="entity" />
|
||||
<footer>
|
||||
<div class="name">
|
||||
<slot name="name"/>
|
||||
<slot name="name" />
|
||||
</div>
|
||||
<p class="meta">
|
||||
<slot name="meta"/>
|
||||
<slot name="meta" />
|
||||
</p>
|
||||
</footer>
|
||||
</article>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div class="tabs">
|
||||
<header>
|
||||
<slot name="header"/>
|
||||
<slot name="header" />
|
||||
</header>
|
||||
<main>
|
||||
<slot/>
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<button type="button" ref="button">
|
||||
<button ref="button" type="button">
|
||||
<slot>Click me</slot>
|
||||
</button>
|
||||
</template>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<button data-testid="close-modal-btn" title="Dismiss" type="button" @click.prevent="$emit('click')">
|
||||
<icon :icon="faTimes"/>
|
||||
<icon :icon="faTimes" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<span class="btn-group">
|
||||
<slot></slot>
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<Transition name="fade">
|
||||
<button v-show="showing" ref="el" title="Scroll to top" type="button" @click="scrollToTop">
|
||||
<icon :icon="faCircleUp"/>
|
||||
<icon :icon="faCircleUp" />
|
||||
Top
|
||||
</button>
|
||||
</Transition>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<span>
|
||||
<input :checked="checked" type="checkbox" v-bind="$attrs" @input="onInput">
|
||||
<icon :icon="faCheck" v-if="checked"/>
|
||||
<icon v-if="checked" :icon="faCheck" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
@ -17,7 +17,7 @@ const checked = ref(props.modelValue)
|
|||
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
|
||||
|
||||
const onInput = (event: InputEvent) => {
|
||||
const onInput = (event: Event) => {
|
||||
checked.value = (event.target as HTMLInputElement).checked
|
||||
emit('update:modelValue', checked.value)
|
||||
}
|
||||
|
|
|
@ -78,20 +78,20 @@ const open = async (_top = 0, _left = 0) => {
|
|||
|
||||
try {
|
||||
await preventOffScreen(el.value!)
|
||||
await initSubmenus()
|
||||
initSubmenus()
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
// in a non-browser environment (e.g., unit testing), these two functions are broken due to calls to
|
||||
// getBoundingClientRect() and querySelectorAll()
|
||||
}
|
||||
|
||||
eventBus.emit('CONTEXT_MENU_OPENED', el)
|
||||
eventBus.emit('CONTEXT_MENU_OPENED', el.value!)
|
||||
}
|
||||
|
||||
const close = () => (shown.value = false)
|
||||
|
||||
// ensure there's only one context menu at any time
|
||||
eventBus.on('CONTEXT_MENU_OPENED', target => target === el || close())
|
||||
eventBus.on('CONTEXT_MENU_OPENED', target => target === el.value || close())
|
||||
|
||||
defineExpose({ open, close, shown })
|
||||
</script>
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
<option disabled value="-1">Preset</option>
|
||||
<option v-for="preset in presets" :key="preset.id" :value="preset.id">{{ preset.name }}</option>
|
||||
</select>
|
||||
<icon :icon="faCaretDown" class="arrow text-highlight" size="sm"/>
|
||||
<icon :icon="faCaretDown" class="arrow text-highlight" size="sm" />
|
||||
</label>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="bands">
|
||||
<span class="band">
|
||||
<span class="slider"/>
|
||||
<span class="slider" />
|
||||
<label>Preamp</label>
|
||||
</span>
|
||||
|
||||
|
@ -24,7 +24,7 @@
|
|||
</span>
|
||||
|
||||
<span v-for="band in bands" :key="band.label" class="band">
|
||||
<span class="slider"/>
|
||||
<span class="slider" />
|
||||
<label>{{ band.label }}</label>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
type="button"
|
||||
@click.prevent="toggleTab('Lyrics')"
|
||||
>
|
||||
<icon :icon="faFileLines" fixed-width/>
|
||||
<icon :icon="faFileLines" fixed-width />
|
||||
</button>
|
||||
<button
|
||||
id="extraTabArtist"
|
||||
|
@ -17,7 +17,7 @@
|
|||
type="button"
|
||||
@click.prevent="toggleTab('Artist')"
|
||||
>
|
||||
<icon :icon="faMicrophone" fixed-width/>
|
||||
<icon :icon="faMicrophone" fixed-width />
|
||||
</button>
|
||||
<button
|
||||
id="extraTabAlbum"
|
||||
|
@ -27,18 +27,18 @@
|
|||
type="button"
|
||||
@click.prevent="toggleTab('Album')"
|
||||
>
|
||||
<icon :icon="faCompactDisc" fixed-width/>
|
||||
<icon :icon="faCompactDisc" fixed-width />
|
||||
</button>
|
||||
<button
|
||||
v-if="useYouTube"
|
||||
v-koel-tooltip.left
|
||||
id="extraTabYouTube"
|
||||
v-koel-tooltip.left
|
||||
:class="{ active: value === 'YouTube' }"
|
||||
title="Related YouTube videos"
|
||||
type="button"
|
||||
@click.prevent="toggleTab('YouTube')"
|
||||
>
|
||||
<icon :icon="faYoutube" fixed-width/>
|
||||
<icon :icon="faYoutube" fixed-width />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
@ -48,9 +48,11 @@ import { faYoutube } from '@fortawesome/free-brands-svg-icons'
|
|||
import { computed } from 'vue'
|
||||
import { useThirdPartyServices } from '@/composables'
|
||||
|
||||
const props = defineProps<{ modelValue?: ExtraPanelTab }>()
|
||||
const props = withDefaults(defineProps<{ modelValue?: ExtraPanelTab | null }>(), {
|
||||
modelValue: null
|
||||
})
|
||||
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', value: ExtraPanelTab): void }>()
|
||||
const emit = defineEmits<{ (e: 'update:modelValue', value: ExtraPanelTab | null): void }>()
|
||||
|
||||
const { useYouTube } = useThirdPartyServices()
|
||||
|
||||
|
@ -59,7 +61,7 @@ const value = computed({
|
|||
set: value => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const toggleTab = (tab: ExtraPanelTab) => (value.value = value.value === tab ? undefined : tab)
|
||||
const toggleTab = (tab: ExtraPanelTab) => (value.value = value.value === tab ? null : tab)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -55,7 +55,11 @@ new class extends UnitTestCase {
|
|||
})
|
||||
})
|
||||
|
||||
it.each<[ScreenName, object, string]>([
|
||||
it.each<[
|
||||
ScreenName,
|
||||
typeof favoriteStore | typeof recentlyPlayedStore,
|
||||
MethodOf<typeof favoriteStore | typeof recentlyPlayedStore>
|
||||
]>([
|
||||
['Favorites', favoriteStore, 'fetch'],
|
||||
['RecentlyPlayed', recentlyPlayedStore, 'fetch']
|
||||
])('initiates playback for %s screen', async (screenName, store, fetchMethod) => {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue