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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
</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>
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<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 -->
|
||||
<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" />
|
||||
|
|
|
@ -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">
|
||||
<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>
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
</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"
|
||||
|
@ -60,8 +60,8 @@
|
|||
|
||||
<div
|
||||
v-show="activeTab === 'YouTube'"
|
||||
data-testid="extra-panel-youtube"
|
||||
id="extraPanelYouTube"
|
||||
data-testid="extra-panel-youtube"
|
||||
aria-labelledby="extraTabYouTube"
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
|
@ -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)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<nav id="sidebar" :class="{ showing: mobileShowing }" class="side side-nav" v-koel-clickaway="closeIfMobile">
|
||||
<nav id="sidebar" v-koel-clickaway="closeIfMobile" :class="{ showing: mobileShowing }" class="side side-nav">
|
||||
<SearchForm />
|
||||
<section class="music">
|
||||
<h1>Your Music</h1>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div id="mainWrapper">
|
||||
<Sidebar/>
|
||||
<SideBar />
|
||||
<MainContent />
|
||||
<ExtraPanel />
|
||||
<ModalWrapper />
|
||||
|
@ -10,7 +10,7 @@
|
|||
<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">
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,10 +2,10 @@
|
|||
<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 />
|
||||
|
|
|
@ -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,7 +25,7 @@
|
|||
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">
|
||||
|
|
|
@ -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,7 +31,7 @@
|
|||
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">
|
||||
|
@ -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
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</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,14 +10,14 @@
|
|||
</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>
|
||||
<Btn class="btn-add-rule" green small uppercase @click.prevent="addRule">
|
||||
<icon :icon="faPlus" />
|
||||
Rule
|
||||
</Btn>
|
||||
|
@ -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 }>()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
<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> = {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<section id="albumsWrapper">
|
||||
<ScreenHeader layout="collapsed">
|
||||
Albums
|
||||
<template v-slot:controls>
|
||||
<template #controls>
|
||||
<ViewModeSwitch v-model="viewMode" />
|
||||
</template>
|
||||
</ScreenHeader>
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
<template>
|
||||
<section id="albumWrapper">
|
||||
<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" />
|
||||
|
||||
<template v-slot:thumbnail>
|
||||
<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,15 +40,15 @@
|
|||
<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>
|
||||
|
||||
|
@ -67,8 +66,8 @@
|
|||
<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>
|
||||
|
@ -80,7 +79,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'Info'" class="info-pane" v-if="useLastfm && album">
|
||||
<div v-show="activeTab === 'Info'" v-if="useLastfm && album" class="info-pane">
|
||||
<AlbumInfo :album="album" mode="full" />
|
||||
</div>
|
||||
</ScreenTabs>
|
||||
|
@ -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>
|
||||
|
|
|
@ -4,20 +4,20 @@
|
|||
All Songs
|
||||
<ControlsToggle v-model="showingControls" />
|
||||
|
||||
<template v-slot:thumbnail>
|
||||
<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>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<section id="artistsWrapper">
|
||||
<ScreenHeader layout="collapsed">
|
||||
Artists
|
||||
<template v-slot:controls>
|
||||
<template #controls>
|
||||
<ViewModeSwitch v-model="viewMode" />
|
||||
</template>
|
||||
</ScreenHeader>
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
{{ artist.name }}
|
||||
<ControlsToggle v-model="showingControls" />
|
||||
|
||||
<template v-slot:thumbnail>
|
||||
<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,15 +39,15 @@
|
|||
<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>
|
||||
|
||||
|
@ -76,7 +75,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'Info'" class="info-pane" v-if="useLastfm && artist">
|
||||
<div v-show="activeTab === 'Info'" v-if="useLastfm && artist" class="info-pane">
|
||||
<ArtistInfo :artist="artist" mode="full" />
|
||||
</div>
|
||||
</ScreenTabs>
|
||||
|
|
|
@ -4,18 +4,17 @@
|
|||
Songs You Love
|
||||
<ControlsToggle v-model="showingControls" />
|
||||
|
||||
<template v-slot:thumbnail>
|
||||
<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,11 +23,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>
|
||||
|
@ -44,7 +43,7 @@
|
|||
/>
|
||||
|
||||
<ScreenEmptyState v-else>
|
||||
<template v-slot:icon>
|
||||
<template #icon>
|
||||
<icon :icon="faHeartBroken" />
|
||||
</template>
|
||||
No favorites yet.
|
||||
|
|
|
@ -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,7 +15,7 @@
|
|||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="genres" v-else>
|
||||
<ul v-else class="genres">
|
||||
<li v-for="i in 20" :key="i">
|
||||
<GenreItemSkeleton />
|
||||
</li>
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
<template>
|
||||
<section id="genreWrapper">
|
||||
<ScreenHeader :layout="headerLayout" v-if="genre">
|
||||
Genre: <span class="text-thin">{{ decodeURIComponent(name) }}</span>
|
||||
<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>
|
||||
<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 />
|
||||
|
@ -30,7 +30,7 @@
|
|||
/>
|
||||
|
||||
<ScreenEmptyState v-if="!songs.length && !loading">
|
||||
<template v-slot:icon>
|
||||
<template #icon>
|
||||
<icon :icon="faTags" />
|
||||
</template>
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<div class="main-scroll-wrap" @scroll="scrolling">
|
||||
<ScreenEmptyState v-if="libraryEmpty">
|
||||
<template v-slot:icon>
|
||||
<template #icon>
|
||||
<icon :icon="faVolumeOff" />
|
||||
</template>
|
||||
No songs found.
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<div class="main-scroll-wrap">
|
||||
<ScreenEmptyState>
|
||||
<template v-slot:icon>
|
||||
<template #icon>
|
||||
<icon :icon="faKiwiBird" :mask="faMap" transform="shrink-12" />
|
||||
</template>
|
||||
|
||||
|
|
|
@ -4,16 +4,15 @@
|
|||
{{ playlist.name }}
|
||||
<ControlsToggle v-if="songs.length" v-model="showingControls" />
|
||||
|
||||
<template v-slot:thumbnail>
|
||||
<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,13 +21,13 @@
|
|||
</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>
|
||||
|
@ -45,7 +44,7 @@
|
|||
/>
|
||||
|
||||
<ScreenEmptyState v-if="!songs.length && !loading">
|
||||
<template v-slot:icon>
|
||||
<template #icon>
|
||||
<icon :icon="faFile" />
|
||||
</template>
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -4,22 +4,22 @@
|
|||
Current Queue
|
||||
<ControlsToggle v-model="showingControls" />
|
||||
|
||||
<template v-slot:thumbnail>
|
||||
<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>
|
||||
|
@ -35,7 +35,7 @@
|
|||
/>
|
||||
|
||||
<ScreenEmptyState v-else>
|
||||
<template v-slot:icon>
|
||||
<template #icon>
|
||||
<icon :icon="faCoffee" />
|
||||
</template>
|
||||
|
||||
|
|
|
@ -4,20 +4,20 @@
|
|||
Recently Played
|
||||
<ControlsToggle v-model="showingControls" />
|
||||
|
||||
<template v-slot:thumbnail>
|
||||
<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>
|
||||
|
@ -27,7 +27,7 @@
|
|||
<SongList v-if="songs.length" ref="songList" @press:enter="onPressEnter" @scroll-breakpoint="onScrollBreakpoint" />
|
||||
|
||||
<ScreenEmptyState v-else>
|
||||
<template v-slot:icon>
|
||||
<template #icon>
|
||||
<icon :icon="faClock" />
|
||||
</template>
|
||||
No songs recently played.
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
<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" />
|
||||
Retry All
|
||||
|
@ -27,12 +27,12 @@
|
|||
@drop.prevent="onDrop"
|
||||
@dragover.prevent
|
||||
>
|
||||
<div class="upload-files" v-if="files.length">
|
||||
<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>
|
||||
<template #icon>
|
||||
<icon :icon="faUpload" />
|
||||
</template>
|
||||
|
||||
|
@ -41,14 +41,14 @@
|
|||
<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>
|
||||
<template #icon>
|
||||
<icon :icon="faWarning" />
|
||||
</template>
|
||||
No media path set.
|
||||
|
@ -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) {
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
Users
|
||||
<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" />
|
||||
Add
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<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" />
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<div id="player">
|
||||
<ScreenEmptyState data-testid="youtube-placeholder">
|
||||
<template v-slot:icon>
|
||||
<template #icon>
|
||||
<icon :icon="faYoutube" />
|
||||
</template>
|
||||
YouTube videos will be played here.
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
</div>
|
||||
|
||||
<ScreenEmptyState v-else>
|
||||
<template v-slot:icon>
|
||||
<template #icon>
|
||||
<icon :icon="faSearch" />
|
||||
</template>
|
||||
Find songs, artists, and albums,
|
||||
|
|
|
@ -4,20 +4,20 @@
|
|||
Songs for <span class="text-thin">{{ decodedQ }}</span>
|
||||
<ControlsToggle v-model="showingControls" />
|
||||
|
||||
<template v-slot:thumbnail>
|
||||
<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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -4,7 +4,12 @@
|
|||
<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" />
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<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" />
|
||||
</a>
|
||||
|
|
|
@ -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"
|
||||
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
<span class="btn-group">
|
||||
<slot></slot>
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -31,8 +31,8 @@
|
|||
</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"
|
||||
|
@ -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) => {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<template v-if="song">
|
||||
<div v-show="song.lyrics">
|
||||
<pre ref="lyricsContainer">{{ lyrics }}</pre>
|
||||
<Magnifier @in="zoomLevel++" @out="zoomLevel--" class="magnifier"/>
|
||||
<Magnifier class="magnifier" @in="zoomLevel++" @out="zoomLevel--" />
|
||||
</div>
|
||||
<p v-if="song.id && !song.lyrics" class="none text-secondary">
|
||||
<template v-if="isAdmin">
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<div
|
||||
class="message"
|
||||
:class="message.type"
|
||||
@click="dismiss"
|
||||
title="Click to dismiss"
|
||||
@click="dismiss"
|
||||
>
|
||||
<aside>
|
||||
<icon :icon="typeIcon" class="icon" />
|
||||
|
@ -34,7 +34,7 @@ const typeIcon = computed(() => {
|
|||
return faCircleCheck
|
||||
case 'warning':
|
||||
return faTriangleExclamation
|
||||
case 'danger':
|
||||
default:
|
||||
return faCircleExclamation
|
||||
}
|
||||
})
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { faSlash, faWifi } from '@fortawesome/free-solid-svg-icons'</script>
|
||||
import { faSlash, faWifi } from '@fortawesome/free-solid-svg-icons'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.offline {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<dialog ref="el" :class="state.type" @cancel.prevent="onCancel" data-testid="overlay">
|
||||
<dialog ref="el" :class="state.type" data-testid="overlay" @cancel.prevent="onCancel">
|
||||
<div class="wrapper">
|
||||
<SoundBars v-if="state.type === 'loading'" />
|
||||
<icon v-if="state.type === 'error'" :icon="faCircleExclamation" />
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
href="/#/profile"
|
||||
title="Profile and preferences"
|
||||
>
|
||||
<img :alt="`Avatar of ${currentUser.name}`" :src="currentUser.avatar"/>
|
||||
<img :alt="`Avatar of ${currentUser.name}`" :src="currentUser.avatar">
|
||||
</a>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<label v-if="isMobile.phone" class="text-highlight">
|
||||
<input type="checkbox" v-model="value"/>
|
||||
<input v-model="value" type="checkbox">
|
||||
<icon :icon="value ? faCaretUp : faCaretDown" class="toggle" />
|
||||
<span>Toggle the song list controls</span>
|
||||
</label>
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
<template>
|
||||
<header class="screen-header" :class="[ layout, disabled ? 'disabled' : '' ]">
|
||||
<aside class="thumbnail-wrapper">
|
||||
<slot name="thumbnail"></slot>
|
||||
<slot name="thumbnail" />
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
<div class="heading-wrapper">
|
||||
<h1 class="name">
|
||||
<slot></slot>
|
||||
<slot />
|
||||
</h1>
|
||||
<span class="meta text-secondary">
|
||||
<slot name="meta"></slot>
|
||||
<slot name="meta" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<slot name="controls"></slot>
|
||||
<slot name="controls" />
|
||||
</main>
|
||||
</header>
|
||||
</template>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div :style="{ height: `${totalHeight}px` }">
|
||||
<div :style="{ transform: `translateY(${offsetY}px)`}">
|
||||
<template v-for="item in renderedItems">
|
||||
<slot :item="item"></slot>
|
||||
<slot :item="item" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -51,12 +51,12 @@ const level = computed(() => {
|
|||
|
||||
const mute = () => volumeManager.mute()
|
||||
const unmute = () => volumeManager.unmute()
|
||||
const setVolume = (e: InputEvent) => volumeManager.set(parseFloat((e.target as HTMLInputElement).value))
|
||||
const setVolume = (e: Event) => volumeManager.set(parseFloat((e.target as HTMLInputElement).value))
|
||||
|
||||
/**
|
||||
* Broadcast the volume changed event to remote controller.
|
||||
*/
|
||||
const broadcastVolume = (e: InputEvent) => {
|
||||
const broadcastVolume = (e: Event) => {
|
||||
socketService.broadcast('SOCKET_VOLUME_CHANGED', parseFloat((e.target as HTMLInputElement).value))
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<Btn v-if="!loading" class="more" @click.prevent="loadMore">Load More</Btn>
|
||||
</template>
|
||||
|
||||
<p class="nope" v-if="loading">Loading…</p>
|
||||
<p v-if="loading" class="nope">Loading…</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`renders for album 1`] = `<span class="cover" style="background-image: url(undefined/resources/assets/img/covers/default.svg);" data-testid="album-artist-thumbnail" data-v-e37470a2=""><img alt="IV" src="https://test/album.jpg" loading="lazy" data-v-e37470a2=""><a class="control control-play" href="" role="button" data-v-e37470a2=""><span class="hidden" data-v-e37470a2="">Play all songs in the album IV</span><span class="icon" data-v-e37470a2=""></span></a></span>`;
|
||||
exports[`renders for album 1`] = `<span class="cover" style="background-image: url(undefined/resources/assets/img/covers/default.svg);" data-testid="album-artist-thumbnail" data-v-e37470a2=""><img alt="IV" src="https://test/album.jpg" loading="lazy" data-v-e37470a2=""><a class="control control-play" role="button" data-v-e37470a2=""><span class="hidden" data-v-e37470a2="">Play all songs in the album IV</span><span class="icon" data-v-e37470a2=""></span></a></span>`;
|
||||
|
||||
exports[`renders for artist 1`] = `<span class="cover" style="background-image: url(undefined/resources/assets/img/covers/default.svg);" data-testid="album-artist-thumbnail" data-v-e37470a2=""><img alt="Led Zeppelin" src="https://test/blimp.jpg" loading="lazy" data-v-e37470a2=""><a class="control control-play" href="" role="button" data-v-e37470a2=""><span class="hidden" data-v-e37470a2="">Play all songs by Led Zeppelin</span><span class="icon" data-v-e37470a2=""></span></a></span>`;
|
||||
exports[`renders for artist 1`] = `<span class="cover" style="background-image: url(undefined/resources/assets/img/covers/default.svg);" data-testid="album-artist-thumbnail" data-v-e37470a2=""><img alt="Led Zeppelin" src="https://test/blimp.jpg" loading="lazy" data-v-e37470a2=""><a class="control control-play" role="button" data-v-e37470a2=""><span class="hidden" data-v-e37470a2="">Play all songs by Led Zeppelin</span><span class="icon" data-v-e37470a2=""></span></a></span>`;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<article class="skeleton pulse" :style="{ width: `${width}px` }">
|
||||
<span class="name"></span>
|
||||
<span class="name" />
|
||||
<span class="count pulse" />
|
||||
</article>
|
||||
</template>
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
<div class="form-row">
|
||||
<label>
|
||||
<CheckBox name="is_admin" v-model="newUser.is_admin"/>
|
||||
<CheckBox v-model="newUser.is_admin" name="is_admin" />
|
||||
User is an admin
|
||||
<TooltipIcon title="Admins can perform administrative tasks like managing users and uploading songs." />
|
||||
</label>
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
<template>
|
||||
<span class="profile" id="userBadge" v-if="currentUser">
|
||||
<span v-if="currentUser" id="userBadge" class="profile">
|
||||
<a class="view-profile" href="/#/profile" title="View/edit user profile">
|
||||
<img :alt="`Avatar of ${currentUser.name}`" :src="currentUser.avatar" class="avatar"/>
|
||||
<img :alt="`Avatar of ${currentUser.name}`" :src="currentUser.avatar" class="avatar">
|
||||
<span class="name">{{ currentUser.name }}</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="logout control"
|
||||
href
|
||||
role="button"
|
||||
title="Log out"
|
||||
@click.prevent="logout"
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`renders 1`] = `<span class="profile" id="userBadge"><a class="view-profile" href="/#/profile" title="View/edit user profile"><img alt="Avatar of John Doe" src="https://gravatar.com/foo" class="avatar"><span class="name">John Doe</span></a><a class="logout control" href="" role="button" title="Log out"><br data-testid="icon" icon="[object Object]"></a></span>`;
|
||||
exports[`renders 1`] = `<span id="userBadge" class="profile"><a class="view-profile" href="/#/profile" title="View/edit user profile"><img alt="Avatar of John Doe" src="https://gravatar.com/foo" class="avatar"><span class="name">John Doe</span></a><a class="logout control" role="button" title="Log out"><br data-testid="icon" icon="[object Object]"></a></span>`;
|
||||
|
|
|
@ -1,18 +1,10 @@
|
|||
import { reactive, ref } from 'vue'
|
||||
import ContextMenuBase from '@/components/ui/ContextMenuBase.vue'
|
||||
|
||||
export type ContextMenuContext = Record<string, any>
|
||||
|
||||
export const useContextMenu = () => {
|
||||
const base = ref<InstanceType<typeof ContextMenuBase>>()
|
||||
|
||||
const context = reactive<ContextMenuContext>({})
|
||||
|
||||
const open = async (top: number, left: number, ctx: ContextMenuContext = {}) => {
|
||||
Object.assign(context, ctx)
|
||||
await base.value?.open(top, left, ctx)
|
||||
}
|
||||
|
||||
const open = async (top: number, left: number) => await base.value?.open(top, left)
|
||||
const close = () => base.value?.close()
|
||||
|
||||
const trigger = (func: Closure) => {
|
||||
|
@ -23,7 +15,6 @@ export const useContextMenu = () => {
|
|||
return {
|
||||
ContextMenuBase,
|
||||
base,
|
||||
context,
|
||||
open,
|
||||
close,
|
||||
trigger
|
||||
|
|
|
@ -9,10 +9,18 @@ export type Config = {
|
|||
}
|
||||
|
||||
export const useFloatingUi = (
|
||||
reference: HTMLElement | Ref<HTMLElement>,
|
||||
floating: HTMLElement | Ref<HTMLElement>,
|
||||
reference: HTMLElement | Ref<HTMLElement | undefined>,
|
||||
floating: HTMLElement | Ref<HTMLElement | undefined>,
|
||||
config: Partial<Config> = {}
|
||||
) => {
|
||||
const extractRef = <T extends HTMLElement | Ref<HTMLElement | undefined>>(ref: T): HTMLElement => {
|
||||
if (isRef(ref) && !ref.value) {
|
||||
throw new TypeError('Reference element is not defined')
|
||||
}
|
||||
|
||||
return isRef(ref) ? ref.value! : ref
|
||||
}
|
||||
|
||||
const mergedConfig: Config = Object.assign({
|
||||
placement: 'bottom',
|
||||
useArrow: true,
|
||||
|
@ -25,10 +33,10 @@ export const useFloatingUi = (
|
|||
let _trigger: Closure
|
||||
|
||||
const setup = () => {
|
||||
reference = isRef(reference) ? reference.value : reference
|
||||
floating = isRef(floating) ? floating.value : floating
|
||||
const referenceElement = extractRef(reference)
|
||||
const floatingElement = extractRef(floating)
|
||||
|
||||
floating.style.display = 'none'
|
||||
floatingElement.style.display = 'none'
|
||||
|
||||
const middleware = [
|
||||
flip(),
|
||||
|
@ -40,7 +48,7 @@ export const useFloatingUi = (
|
|||
if (mergedConfig.useArrow) {
|
||||
arrow = document.createElement('div')
|
||||
arrow.className = 'arrow'
|
||||
floating.appendChild(arrow)
|
||||
floatingElement.appendChild(arrow)
|
||||
|
||||
middleware.push(arrowMiddleware({
|
||||
element: arrow,
|
||||
|
@ -48,26 +56,26 @@ export const useFloatingUi = (
|
|||
}))
|
||||
}
|
||||
|
||||
const update = async () => await updateFloatingUi(reference, floating, {
|
||||
const update = async () => await updateFloatingUi(referenceElement, floatingElement, {
|
||||
placement: mergedConfig.placement,
|
||||
middleware
|
||||
}, arrow)
|
||||
|
||||
_cleanUp = autoUpdate(reference, floating, update)
|
||||
_cleanUp = autoUpdate(referenceElement, floatingElement, update)
|
||||
|
||||
_show = async () => {
|
||||
floating.style.display = 'block'
|
||||
floatingElement.style.display = 'block'
|
||||
await update()
|
||||
}
|
||||
|
||||
_hide = () => (floating.style.display = 'none')
|
||||
_trigger = () => floating.style.display === 'none' ? _show() : _hide()
|
||||
_hide = () => (floatingElement.style.display = 'none')
|
||||
_trigger = () => floatingElement.style.display === 'none' ? _show() : _hide()
|
||||
|
||||
if (mergedConfig.autoTrigger) {
|
||||
reference.addEventListener('mouseenter', _show)
|
||||
reference.addEventListener('focus', _show)
|
||||
reference.addEventListener('mouseleave', _hide)
|
||||
reference.addEventListener('blur', _hide)
|
||||
referenceElement.addEventListener('mouseenter', _show)
|
||||
referenceElement.addEventListener('focus', _show)
|
||||
referenceElement.addEventListener('mouseleave', _hide)
|
||||
referenceElement.addEventListener('blur', _hide)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,9 @@ import ToTopButton from '@/components/ui/BtnScrollToTop.vue'
|
|||
export const useInfiniteScroll = (loadMore: Closure) => {
|
||||
const scroller = ref<HTMLElement>()
|
||||
|
||||
const scrolling = ({ target }: { target: HTMLElement }) => {
|
||||
const scrolling = (event: UIEvent) => {
|
||||
const target = event.target as HTMLElement
|
||||
|
||||
// Here we check if the user has scrolled to the end of the wrapper (or 32px to the end).
|
||||
// If that's true, load more items.
|
||||
if (target.scrollTop + target.clientHeight >= target.scrollHeight - 32) {
|
||||
|
|
|
@ -12,7 +12,7 @@ export const useSmartPlaylistForm = (initialRuleGroups: SmartPlaylistRuleGroup[]
|
|||
const addGroup = () => collectedRuleGroups.value.push(playlistStore.createEmptySmartPlaylistRuleGroup())
|
||||
|
||||
const onGroupChanged = (data: SmartPlaylistRuleGroup) => {
|
||||
const changedGroup = Object.assign(collectedRuleGroups.value.find(g => g.id === data.id), data)
|
||||
const changedGroup = Object.assign(collectedRuleGroups.value.find(g => g.id === data.id)!, data)
|
||||
|
||||
// Remove empty group
|
||||
if (changedGroup.rules.length === 0) {
|
||||
|
|
|
@ -41,7 +41,7 @@ export const useSongList = (songs: Ref<Song[]>, config: Partial<SongListConfig>
|
|||
return take(Array.from(new Set(sampleCovers)), 4)
|
||||
})
|
||||
|
||||
const getSongsToPlay = (): Song[] => songList.value.getAllSongsWithSort()
|
||||
const getSongsToPlay = (): Song[] => songList.value!.getAllSongsWithSort()
|
||||
|
||||
const playAll = (shuffle: boolean) => {
|
||||
playbackService.queueAndPlay(getSongsToPlay(), shuffle)
|
||||
|
|
|
@ -9,7 +9,7 @@ import { useAuthorization, useMessageToaster, useRouter } from '@/composables'
|
|||
export const useUpload = () => {
|
||||
const { isAdmin } = useAuthorization()
|
||||
const { toastSuccess, toastWarning } = useMessageToaster()
|
||||
const { go, isCurrentRoute } = useRouter()
|
||||
const { go, isCurrentScreen } = useRouter()
|
||||
|
||||
const mediaPath = toRef(settingStore.state, 'media_path')
|
||||
|
||||
|
@ -45,7 +45,7 @@ export const useUpload = () => {
|
|||
|
||||
if (queuedFiles.length) {
|
||||
toastSuccess(`Queued ${pluralize(queuedFiles, 'file')} for upload`)
|
||||
isCurrentRoute('Upload') || go('upload')
|
||||
isCurrentScreen('Upload') || go('upload')
|
||||
} else {
|
||||
toastWarning('No files applicable for upload')
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ export interface Events {
|
|||
|
||||
MODAL_SHOW_ADD_USER_FORM: () => void
|
||||
MODAL_SHOW_EDIT_USER_FORM: (user: User) => void
|
||||
MODAL_SHOW_EDIT_SONG_FORM: (songs: Song | Song[], initialTab: EditSongFormTabName) => void
|
||||
MODAL_SHOW_EDIT_SONG_FORM: (songs: Song | Song[], initialTab?: EditSongFormTabName) => void
|
||||
MODAL_SHOW_CREATE_PLAYLIST_FORM: (folder: PlaylistFolder | null) => void
|
||||
MODAL_SHOW_EDIT_PLAYLIST_FORM: (playlist: Playlist) => void
|
||||
MODAL_SHOW_CREATE_SMART_PLAYLIST_FORM: (folder: PlaylistFolder | null) => void
|
||||
|
@ -41,10 +41,9 @@ export interface Events {
|
|||
SOCKET_PLAY_PREV: () => void
|
||||
SOCKET_PLAYBACK_STOPPED: () => void
|
||||
SOCKET_GET_STATUS: () => void
|
||||
SOCKET_STATUS: () => void
|
||||
SOCKET_STATUS: (data: { song?: Song, volume: number }) => void
|
||||
SOCKET_GET_CURRENT_SONG: () => void
|
||||
SOCKET_SONG: (song: Song) => void
|
||||
SOCKET_SET_VOLUME: (volume: number) => void
|
||||
SOCKET_VOLUME_CHANGED: (volume: number) => void
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import { Directive } from 'vue'
|
||||
|
||||
/**
|
||||
* A simple directive to set focus into an input field when it's shown.
|
||||
*/
|
||||
export const focus: Directive = {
|
||||
mounted: (el: HTMLElement) => el.focus()
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="none text-secondary" v-else>No song is playing.</p>
|
||||
<p v-else class="none text-secondary">No song is playing.</p>
|
||||
<footer>
|
||||
<a class="favorite" :class="song?.liked ? 'yep' : ''" @click.prevent="toggleFavorite">
|
||||
<icon :icon="song?.liked ? faHeart : faEmptyHeart" />
|
||||
|
@ -32,8 +32,8 @@
|
|||
<span class="volume">
|
||||
<span
|
||||
v-show="showingVolumeSlider"
|
||||
ref="volumeSlider"
|
||||
id="volumeSlider"
|
||||
ref="volumeSlider"
|
||||
v-koel-clickaway="closeVolumeSlider"
|
||||
/>
|
||||
<span class="icon" @click.stop="toggleVolumeSlider">
|
||||
|
@ -45,7 +45,7 @@
|
|||
<div v-else class="loader">
|
||||
<div v-if="!maxRetriesReached">
|
||||
<p>Searching for Koel…</p>
|
||||
<div class="signal"></div>
|
||||
<div class="signal" />
|
||||
</div>
|
||||
<p v-else>
|
||||
No active Koel instance found.
|
||||
|
@ -55,7 +55,7 @@
|
|||
</main>
|
||||
</template>
|
||||
|
||||
<div class="login-wrapper" v-else>
|
||||
<div v-else class="login-wrapper">
|
||||
<LoginForm @loggedin="onUserLoggedIn" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -116,7 +116,7 @@ watch(connected, async () => {
|
|||
throw new Error('Failed to initialize noUISlider on element #volumeSlider')
|
||||
}
|
||||
|
||||
volumeSlider.value.noUiSlider.on('change', (values: number[], handle: number) => {
|
||||
volumeSlider.value.noUiSlider.on('change', (values: string[], handle: number) => {
|
||||
const volume = values[handle]
|
||||
muted.value = !volume
|
||||
socketService.broadcast('SOCKET_SET_VOLUME', volume)
|
||||
|
|
|
@ -18,7 +18,7 @@ export const playlistStore = {
|
|||
}),
|
||||
|
||||
init (playlists: Playlist[]) {
|
||||
this.sort(reactive(playlists)).forEach((playlist: Playlist) => {
|
||||
this.sort(reactive(playlists)).forEach(playlist => {
|
||||
if (!playlist.is_smart) {
|
||||
this.state.playlists.push(playlist)
|
||||
} else {
|
||||
|
@ -38,7 +38,8 @@ export const playlistStore = {
|
|||
setupSmartPlaylist: (playlist: Playlist) => {
|
||||
playlist.rules.forEach(group => {
|
||||
group.rules.forEach(rule => {
|
||||
const model = models.find(model => model.name === rule.model as unknown as string)
|
||||
const serializedRule = rule as unknown as SerializedSmartPlaylistRule
|
||||
const model = models.find(model => model.name === serializedRule.model)
|
||||
|
||||
if (!model) {
|
||||
logger.error(`Invalid model ${rule.model} found in smart playlist ${playlist.name} (ID ${playlist.id})`)
|
||||
|
|
|
@ -5,7 +5,7 @@ import factory from 'factoria'
|
|||
import { http } from '@/services'
|
||||
import { queueStore, songStore } from '.'
|
||||
|
||||
let songs
|
||||
let songs: Song[]
|
||||
|
||||
new class extends UnitTestCase {
|
||||
protected beforeEach () {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue