chore: code style and some minor fixes

This commit is contained in:
Phan An 2022-12-02 17:17:37 +01:00
parent e3c7d51ad5
commit 4b8ae1a78e
No known key found for this signature in database
GPG key ID: A81E4477F0BB6FDC
146 changed files with 642 additions and 634 deletions

View file

@ -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
}
}

View file

@ -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

View file

@ -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()
})

View file

@ -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)
})

View file

@ -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>>

View file

@ -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"
>

View file

@ -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>

View file

@ -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))

View file

@ -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>
`;

View file

@ -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"
>

View file

@ -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>

View file

@ -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>
`;

View file

@ -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')

View file

@ -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')),

View file

@ -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>

View file

@ -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,

View file

@ -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" />

View file

@ -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()

View file

@ -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>

View file

@ -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)

View file

@ -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>

View file

@ -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'

View file

@ -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>&nbsp;<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">

View file

@ -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>

View file

@ -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)
}

View file

@ -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' }))

View file

@ -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>

View file

@ -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>

View file

@ -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)
}

View file

@ -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>

View file

@ -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)
}

View file

@ -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>

View file

@ -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 />

View file

@ -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">

View file

@ -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

View file

@ -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)
})
}

View file

@ -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()
}

View file

@ -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 }>()

View file

@ -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 albums art
</label>
</div>

View file

@ -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>

View file

@ -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> = {

View file

@ -2,7 +2,7 @@
<section id="albumsWrapper">
<ScreenHeader layout="collapsed">
Albums
<template v-slot:controls>
<template #controls>
<ViewModeSwitch v-model="viewMode" />
</template>
</ScreenHeader>

View file

@ -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>

View file

@ -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>

View file

@ -2,7 +2,7 @@
<section id="artistsWrapper">
<ScreenHeader layout="collapsed">
Artists
<template v-slot:controls>
<template #controls>
<ViewModeSwitch v-model="viewMode" />
</template>
</ScreenHeader>

View file

@ -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>

View file

@ -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.

View file

@ -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>

View file

@ -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>

View file

@ -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.

View file

@ -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>

View file

@ -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)

View file

@ -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 {

View file

@ -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>

View file

@ -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.

View file

@ -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) {

View file

@ -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

View file

@ -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" />

View file

@ -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.

View file

@ -72,7 +72,7 @@
</div>
<ScreenEmptyState v-else>
<template v-slot:icon>
<template #icon>
<icon :icon="faSearch" />
</template>
Find songs, artists, and albums,

View file

@ -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>

View file

@ -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>

View file

@ -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) {

View file

@ -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()

View file

@ -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" />

View file

@ -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>

View file

@ -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)

View file

@ -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>

View file

@ -1,5 +1,5 @@
<template>
<button type="button" ref="button">
<button ref="button" type="button">
<slot>Click me</slot>
</button>
</template>

View file

@ -1,6 +1,6 @@
<template>
<span class="btn-group">
<slot></slot>
<slot />
</span>
</template>

View file

@ -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)
}

View file

@ -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>

View file

@ -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>

View file

@ -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) => {

View file

@ -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">

View file

@ -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
}
})

View file

@ -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 {

View file

@ -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" />

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>`;

View file

@ -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>

View file

@ -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>

View file

@ -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"

View file

@ -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>`;

View file

@ -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

View file

@ -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)
}
}

View file

@ -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) {

View file

@ -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) {

View file

@ -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)

View file

@ -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')
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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)

View file

@ -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})`)

View file

@ -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