[web] Switch to camel case

This commit is contained in:
Alain Nussbaumer 2025-03-13 23:29:06 +01:00
parent 6c09457e5d
commit 7e8672917e
64 changed files with 663 additions and 747 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -518,9 +518,9 @@
} }
}, },
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.5.0", "version": "4.5.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.0.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz",
"integrity": "sha512-RoV8Xs9eNwiDvhv7M+xcL4PWyRyIXRY/FLp3buU4h1EYfdF7unWUy3dOjPqb3C7rMUewIcqwW850PgS8h1o1yg==", "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1960,12 +1960,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.8.2", "version": "1.8.3",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",

View File

@ -5,8 +5,8 @@
<component :is="Component" /> <component :is="Component" />
</router-view> </router-view>
<modal-dialog-remote-pairing <modal-dialog-remote-pairing
:show="pairing_active" :show="pairingActive"
@close="pairing_active = false" @close="pairingActive = false"
/> />
<modal-dialog-update <modal-dialog-update
:show="showUpdateDialog" :show="showUpdateDialog"
@ -67,9 +67,9 @@ export default {
}, },
data() { data() {
return { return {
pairing_active: false, pairingActive: false,
reconnect_attempts: 0, reconnect_attempts: 0,
token_timer_id: 0 timerId: 0
} }
}, },
computed: { computed: {
@ -100,10 +100,10 @@ export default {
}, },
watch: { watch: {
showBurgerMenu() { showBurgerMenu() {
this.update_is_clipped() this.updateClipping()
}, },
showPlayerMenu() { showPlayerMenu() {
this.update_is_clipped() this.updateClipping()
} }
}, },
created() { created() {
@ -145,7 +145,7 @@ export default {
}) })
}, },
openWebsocket() { openWebsocket() {
const socket = this.create_websocket() const socket = this.createWebsocket()
const vm = this const vm = this
socket.onopen = () => { socket.onopen = () => {
vm.reconnect_attempts = 0 vm.reconnect_attempts = 0
@ -165,14 +165,14 @@ export default {
] ]
}) })
) )
vm.update_outputs() vm.updateOutputs()
vm.update_player_status() vm.updatePlayerStatus()
vm.update_library_stats() vm.updateLibraryStats()
vm.update_settings() vm.updateSettings()
vm.update_queue() vm.updateQueue()
vm.update_spotify() vm.updateSpotify()
vm.update_lastfm() vm.updateLastfm()
vm.update_pairing() vm.updatePairing()
} }
/* /*
@ -181,23 +181,23 @@ export default {
* There are two relevant events - focus and visibilitychange, so we * There are two relevant events - focus and visibilitychange, so we
* throttle the updates to avoid multiple redundant updates. * throttle the updates to avoid multiple redundant updates.
*/ */
let update_throttled = false let updateThrottled = false
const update_info = () => { const updateInfo = () => {
if (update_throttled) { if (updateThrottled) {
return return
} }
vm.update_outputs() vm.updateOutputs()
vm.update_player_status() vm.updatePlayerStatus()
vm.update_library_stats() vm.updateLibraryStats()
vm.update_settings() vm.updateSettings()
vm.update_queue() vm.updateQueue()
vm.update_spotify() vm.updateSpotify()
vm.update_lastfm() vm.updateLastfm()
vm.update_pairing() vm.updatePairing()
update_throttled = true updateThrottled = true
setTimeout(() => { setTimeout(() => {
update_throttled = false updateThrottled = false
}, 500) }, 500)
} }
@ -205,10 +205,10 @@ export default {
* These events are fired when the window becomes active in different * These events are fired when the window becomes active in different
* ways. When this happens, we should update 'now playing' info, etc. * ways. When this happens, we should update 'now playing' info, etc.
*/ */
window.addEventListener('focus', update_info) window.addEventListener('focus', updateInfo)
document.addEventListener('visibilitychange', () => { document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') { if (document.visibilityState === 'visible') {
update_info() updateInfo()
} }
}) })
@ -218,33 +218,33 @@ export default {
data.notify.includes('update') || data.notify.includes('update') ||
data.notify.includes('database') data.notify.includes('database')
) { ) {
vm.update_library_stats() vm.updateLibraryStats()
} }
if ( if (
data.notify.includes('player') || data.notify.includes('player') ||
data.notify.includes('options') || data.notify.includes('options') ||
data.notify.includes('volume') data.notify.includes('volume')
) { ) {
vm.update_player_status() vm.updatePlayerStatus()
} }
if (data.notify.includes('outputs') || data.notify.includes('volume')) { if (data.notify.includes('outputs') || data.notify.includes('volume')) {
vm.update_outputs() vm.updateOutputs()
} }
if (data.notify.includes('queue')) { if (data.notify.includes('queue')) {
vm.update_queue() vm.updateQueue()
} }
if (data.notify.includes('spotify')) { if (data.notify.includes('spotify')) {
vm.update_spotify() vm.updateSpotify()
} }
if (data.notify.includes('lastfm')) { if (data.notify.includes('lastfm')) {
vm.update_lastfm() vm.updateLastfm()
} }
if (data.notify.includes('pairing')) { if (data.notify.includes('pairing')) {
vm.update_pairing() vm.updatePairing()
} }
} }
}, },
create_websocket() { createWebsocket() {
const protocol = window.location.protocol.replace('http', 'ws') const protocol = window.location.protocol.replace('http', 'ws')
const hostname = const hostname =
(import.meta.env.DEV && (import.meta.env.DEV &&
@ -258,19 +258,19 @@ export default {
reconnectInterval: 1000 reconnectInterval: 1000
}) })
}, },
update_is_clipped() { updateClipping() {
if (this.showBurgerMenu || this.showPlayerMenu) { if (this.showBurgerMenu || this.showPlayerMenu) {
document.querySelector('html').classList.add('is-clipped') document.querySelector('html').classList.add('is-clipped')
} else { } else {
document.querySelector('html').classList.remove('is-clipped') document.querySelector('html').classList.remove('is-clipped')
} }
}, },
update_lastfm() { updateLastfm() {
webapi.lastfm().then(({ data }) => { webapi.lastfm().then(({ data }) => {
this.servicesStore.lastfm = data this.servicesStore.lastfm = data
}) })
}, },
update_library_stats() { updateLibraryStats() {
webapi.library_stats().then(({ data }) => { webapi.library_stats().then(({ data }) => {
this.libraryStore.$state = data this.libraryStore.$state = data
}) })
@ -278,7 +278,7 @@ export default {
this.libraryStore.rss = data this.libraryStore.rss = data
}) })
}, },
update_lyrics() { updateLyrics() {
const track = this.queueStore.current const track = this.queueStore.current
if (track?.track_id) { if (track?.track_id) {
webapi.library_track(track.track_id).then(({ data }) => { webapi.library_track(track.track_id).then(({ data }) => {
@ -288,44 +288,44 @@ export default {
this.lyricsStore.$reset() this.lyricsStore.$reset()
} }
}, },
update_outputs() { updateOutputs() {
webapi.outputs().then(({ data }) => { webapi.outputs().then(({ data }) => {
this.outputsStore.outputs = data.outputs this.outputsStore.outputs = data.outputs
}) })
}, },
update_pairing() { updatePairing() {
webapi.pairing().then(({ data }) => { webapi.pairing().then(({ data }) => {
this.remotesStore.$state = data this.remotesStore.$state = data
this.pairing_active = data.active this.pairingActive = data.active
}) })
}, },
update_player_status() { updatePlayerStatus() {
webapi.player_status().then(({ data }) => { webapi.player_status().then(({ data }) => {
this.playerStore.$state = data this.playerStore.$state = data
this.update_lyrics() this.updateLyrics()
}) })
}, },
update_queue() { updateQueue() {
webapi.queue().then(({ data }) => { webapi.queue().then(({ data }) => {
this.queueStore.$state = data this.queueStore.$state = data
this.update_lyrics() this.updateLyrics()
}) })
}, },
update_settings() { updateSettings() {
webapi.settings().then(({ data }) => { webapi.settings().then(({ data }) => {
this.settingsStore.$state = data this.settingsStore.$state = data
}) })
}, },
update_spotify() { updateSpotify() {
webapi.spotify().then(({ data }) => { webapi.spotify().then(({ data }) => {
this.servicesStore.spotify = data this.servicesStore.spotify = data
if (this.token_timer_id > 0) { if (this.timerId > 0) {
window.clearTimeout(this.token_timer_id) window.clearTimeout(this.timerId)
this.token_timer_id = 0 this.timerId = 0
} }
if (data.webapi_token_expires_in > 0 && data.webapi_token) { if (data.webapi_token_expires_in > 0 && data.webapi_token) {
this.token_timer_id = window.setTimeout( this.timerId = window.setTimeout(
this.update_spotify, this.updateSpotify,
1000 * data.webapi_token_expires_in 1000 * data.webapi_token_expires_in
) )
} }

View File

@ -1,75 +1,48 @@
<template> <template>
<template v-for="item in items" :key="item.itemId"> <list-item
<div v-if="!item.isItem" class="py-5"> v-for="item in items"
<span :key="item.itemId"
:id="`index_${item.index}`" :is-item="item.isItem"
class="tag is-small has-text-weight-bold" :image="url(item)"
v-text="item.index" :index="item.index"
/> :lines="[
</div> item.item.name,
<div item.item.artist,
v-else $filters.toDate(item.item.date_released)
class="media is-align-items-center is-clickable mb-0" ]"
@click="open(item.item)" @open="open(item.item)"
> @open-details="openDetails(item.item)"
<control-image />
v-if="settingsStore.show_cover_artwork_in_album_lists" <modal-dialog-album
:url="item.item.artwork_url" :item="selectedItem"
:artist="item.item.artist" :media_kind="media_kind"
:album="item.item.name" :show="showDetailsModal"
class="media-left is-small" @close="showDetailsModal = false"
/> @remove-podcast="openRemovePodcastDialog()"
<div class="media-content"> @play-count-changed="onPlayCountChange()"
<div class="is-size-6 has-text-weight-bold" v-text="item.item.name" /> />
<div <modal-dialog
class="is-size-7 has-text-grey has-text-weight-bold" :actions="actions"
v-text="item.item.artist" :show="showRemovePodcastModal"
/> :title="$t('page.podcast.remove-podcast')"
<div @cancel="showRemovePodcastModal = false"
v-if="item.item.date_released && item.item.media_kind === 'music'" @remove="removePodcast"
class="is-size-7 has-text-grey" >
v-text="$filters.toDate(item.item.date_released)" <template #content>
/> <i18n-t keypath="list.albums.info" tag="p" scope="global">
</div> <template #separator>
<div class="media-right"> <br />
<a @click.prevent.stop="openDialog(item.item)"> </template>
<mdicon class="icon has-text-grey" name="dots-vertical" size="16" /> <template #name>
</a> <b v-text="playlistToRemove.name" />
</div> </template>
</div> </i18n-t>
</template> </template>
<teleport to="#app"> </modal-dialog>
<modal-dialog-album
:item="selectedItem"
:media_kind="media_kind"
:show="showDetailsModal"
@close="showDetailsModal = false"
@remove-podcast="openRemovePodcastDialog()"
@play-count-changed="onPlayCountChange()"
/>
<modal-dialog
:actions="actions"
:show="showRemovePodcastModal"
:title="$t('page.podcast.remove-podcast')"
@cancel="showRemovePodcastModal = false"
@remove="removePodcast"
>
<template #content>
<i18n-t keypath="list.albums.info" tag="p" scope="global">
<template #separator>
<br />
</template>
<template #name>
<b v-text="rss_playlist_to_remove.name" />
</template>
</i18n-t>
</template>
</modal-dialog>
</teleport>
</template> </template>
<script> <script>
import ControlImage from '@/components/ControlImage.vue' import ListItem from '@/components/ListItem.vue'
import ModalDialog from '@/components/ModalDialog.vue' import ModalDialog from '@/components/ModalDialog.vue'
import ModalDialogAlbum from '@/components/ModalDialogAlbum.vue' import ModalDialogAlbum from '@/components/ModalDialogAlbum.vue'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
@ -77,7 +50,7 @@ import webapi from '@/webapi'
export default { export default {
name: 'ListAlbums', name: 'ListAlbums',
components: { ControlImage, ModalDialog, ModalDialogAlbum }, components: { ListItem, ModalDialog, ModalDialogAlbum },
props: { props: {
items: { required: true, type: Object }, items: { required: true, type: Object },
media_kind: { default: '', type: String } media_kind: { default: '', type: String }
@ -88,7 +61,7 @@ export default {
}, },
data() { data() {
return { return {
rss_playlist_to_remove: {}, playlistToRemove: {},
selectedItem: {}, selectedItem: {},
showDetailsModal: false, showDetailsModal: false,
showRemovePodcastModal: false showRemovePodcastModal: false
@ -119,7 +92,7 @@ export default {
this.$router.push({ name: 'music-album', params: { id: item.id } }) this.$router.push({ name: 'music-album', params: { id: item.id } })
} }
}, },
openDialog(item) { openDetails(item) {
this.selectedItem = item this.selectedItem = item
this.showDetailsModal = true this.showDetailsModal = true
}, },
@ -128,7 +101,7 @@ export default {
.library_album_tracks(this.selectedItem.id, { limit: 1 }) .library_album_tracks(this.selectedItem.id, { limit: 1 })
.then(({ data: album }) => { .then(({ data: album }) => {
webapi.library_track_playlists(album.items[0].id).then(({ data }) => { webapi.library_track_playlists(album.items[0].id).then(({ data }) => {
;[this.rss_playlist_to_remove] = data.items.filter( ;[this.playlistToRemove] = data.items.filter(
(playlist) => playlist.type === 'rss' (playlist) => playlist.type === 'rss'
) )
this.showRemovePodcastModal = true this.showRemovePodcastModal = true
@ -141,11 +114,15 @@ export default {
}, },
removePodcast() { removePodcast() {
this.showRemovePodcastModal = false this.showRemovePodcastModal = false
webapi webapi.library_playlist_delete(this.playlistToRemove.id).then(() => {
.library_playlist_delete(this.rss_playlist_to_remove.id) this.$emit('podcast-deleted')
.then(() => { })
this.$emit('podcast-deleted') },
}) url(item) {
if (this.settingsStore.show_cover_artwork_in_album_lists) {
return item.item.artwork_url
}
return null
} }
} }
} }

View File

@ -1,51 +1,33 @@
<template> <template>
<template v-for="item in items" :key="item.id"> <list-item
<div v-for="item in items"
class="media is-align-items-center is-clickable mb-0" :key="item.id"
@click="open(item)" :is-item="item.isItem"
> :image="url(item)"
<control-image :index="item.index"
v-if="settingsStore.show_cover_artwork_in_album_lists" :lines="[
:url="item.images?.[0]?.url ?? ''" item.name,
:artist="item.artist" item.artists[0]?.name,
:album="item.name" $filters.toDate(item.release_date)
class="media-left is-small" ]"
/> @open="open(item)"
<div class="media-content"> @open-details="openDetails(item)"
<div class="is-size-6 has-text-weight-bold" v-text="item.name" /> />
<div <modal-dialog-album-spotify
class="is-size-7 has-text-weight-bold has-text-grey" :item="selectedItem"
v-text="item.artists[0]?.name" :show="showDetailsModal"
/> @close="showDetailsModal = false"
<div />
class="is-size-7 has-text-grey"
v-text="$filters.toDate(item.release_date)"
/>
</div>
<div class="media-right">
<a @click.prevent.stop="openDialog(item)">
<mdicon class="icon has-text-grey" name="dots-vertical" size="16" />
</a>
</div>
</div>
</template>
<teleport to="#app">
<modal-dialog-album-spotify
:item="selectedItem"
:show="showDetailsModal"
@close="showDetailsModal = false"
/>
</teleport>
</template> </template>
<script> <script>
import ControlImage from '@/components/ControlImage.vue' import ListItem from '@/components/ListItem.vue'
import ModalDialogAlbumSpotify from '@/components/ModalDialogAlbumSpotify.vue' import ModalDialogAlbumSpotify from '@/components/ModalDialogAlbumSpotify.vue'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
export default { export default {
name: 'ListAlbumsSpotify', name: 'ListAlbumsSpotify',
components: { ControlImage, ModalDialogAlbumSpotify }, components: { ListItem, ModalDialogAlbumSpotify },
props: { items: { required: true, type: Object } }, props: { items: { required: true, type: Object } },
setup() { setup() {
return { settingsStore: useSettingsStore() } return { settingsStore: useSettingsStore() }
@ -60,9 +42,15 @@ export default {
params: { id: item.id } params: { id: item.id }
}) })
}, },
openDialog(item) { openDetails(item) {
this.selectedItem = item this.selectedItem = item
this.showDetailsModal = true this.showDetailsModal = true
},
url(item) {
if (this.settingsStore.show_cover_artwork_in_album_lists) {
return item.images?.[0]?.url ?? ''
}
return null
} }
} }
} }

View File

@ -1,48 +1,31 @@
<template> <template>
<template v-for="item in items" :key="item.itemId"> <list-item
<div v-if="!item.isItem" class="py-5"> v-for="item in items"
<span :key="item.itemId"
:id="`index_${item.index}`" :is-item="item.isItem"
class="tag is-small has-text-weight-bold" :index="item.index"
v-text="item.index" :lines="[item.item.name]"
/> @open="open(item.item)"
</div> @open-details="openDetails(item.item)"
<div />
v-else <modal-dialog-artist
class="media is-align-items-center is-clickable mb-0" :item="selectedItem"
@click="open(item.item)" :show="showDetailsModal"
> @close="showDetailsModal = false"
<div class="media-content"> />
<p class="is-size-6 has-text-weight-bold" v-text="item.item.name" />
</div>
<div class="media-right">
<a @click.prevent.stop="openDialog(item.item)">
<mdicon class="icon has-text-grey" name="dots-vertical" size="16" />
</a>
</div>
</div>
</template>
<teleport to="#app">
<modal-dialog-artist
:item="selectedItem"
:show="showDetailsModal"
@close="showDetailsModal = false"
/>
</teleport>
</template> </template>
<script> <script>
import ListItem from '@/components/ListItem.vue'
import ModalDialogArtist from '@/components/ModalDialogArtist.vue' import ModalDialogArtist from '@/components/ModalDialogArtist.vue'
export default { export default {
name: 'ListArtists', name: 'ListArtists',
components: { ModalDialogArtist }, components: { ListItem, ModalDialogArtist },
props: { items: { required: true, type: Object } }, props: { items: { required: true, type: Object } },
data() { data() {
return { selectedItem: {}, showDetailsModal: false } return { selectedItem: {}, showDetailsModal: false }
}, },
methods: { methods: {
open(item) { open(item) {
this.selectedItem = item this.selectedItem = item
@ -50,7 +33,7 @@ export default {
item.media_kind === 'audiobook' ? 'audiobooks-artist' : 'music-artist' item.media_kind === 'audiobook' ? 'audiobooks-artist' : 'music-artist'
this.$router.push({ name: route, params: { id: item.id } }) this.$router.push({ name: route, params: { id: item.id } })
}, },
openDialog(item) { openDetails(item) {
this.selectedItem = item this.selectedItem = item
this.showDetailsModal = true this.showDetailsModal = true
} }

View File

@ -1,31 +1,27 @@
<template> <template>
<template v-for="item in items" :key="item.id"> <list-item
<div class="media is-align-items-center is-clickable mb-0"> v-for="item in items"
<div class="media-content" @click="open(item)"> :key="item.id"
<p class="is-size-6 has-text-weight-bold" v-text="item.name" /> :is-item="true"
</div> :index="item.index"
<div class="media-right"> :lines="[item.name]"
<a @click.prevent.stop="openDialog(item)"> @open="open(item)"
<mdicon class="icon has-text-grey" name="dots-vertical" size="16" /> @open-details="openDetails(item)"
</a> />
</div> <modal-dialog-artist-spotify
</div> :item="selectedItem"
</template> :show="showDetailsModal"
<teleport to="#app"> @close="showDetailsModal = false"
<modal-dialog-artist-spotify />
:item="selectedItem"
:show="showDetailsModal"
@close="showDetailsModal = false"
/>
</teleport>
</template> </template>
<script> <script>
import ListItem from '@/components/ListItem.vue'
import ModalDialogArtistSpotify from '@/components/ModalDialogArtistSpotify.vue' import ModalDialogArtistSpotify from '@/components/ModalDialogArtistSpotify.vue'
export default { export default {
name: 'ListArtistsSpotify', name: 'ListArtistsSpotify',
components: { ModalDialogArtistSpotify }, components: { ListItem, ModalDialogArtistSpotify },
props: { items: { required: true, type: Object } }, props: { items: { required: true, type: Object } },
data() { data() {
@ -38,7 +34,7 @@ export default {
params: { id: item.id } params: { id: item.id }
}) })
}, },
openDialog(item) { openDetails(item) {
this.selectedItem = item this.selectedItem = item
this.showDetailsModal = true this.showDetailsModal = true
} }

View File

@ -1,50 +1,31 @@
<template> <template>
<template v-for="item in items" :key="item.itemId"> <list-item
<div v-if="!item.isItem" class="py-5"> v-for="item in items"
<div class="media-content"> :key="item.itemId"
<span :is-item="item.isItem"
:id="`index_${item.index}`" :index="item.index"
class="tag is-small has-text-weight-bold" :lines="[item.item.name]"
v-text="item.index" @open="open(item.item)"
/> @open-details="openDetails(item.item)"
</div> />
</div> <modal-dialog-composer
<div :item="selectedItem"
v-else :show="showDetailsModal"
class="media is-align-items-center is-clickable mb-0" @close="showDetailsModal = false"
@click="open(item.item)" />
>
<div class="media-content">
<p class="is-size-6 has-text-weight-bold" v-text="item.item.name" />
</div>
<div class="media-right">
<a @click.prevent.stop="openDialog(item.item)">
<mdicon class="icon has-text-grey" name="dots-vertical" size="16" />
</a>
</div>
</div>
</template>
<teleport to="#app">
<modal-dialog-composer
:item="selectedItem"
:show="showDetailsModal"
@close="showDetailsModal = false"
/>
</teleport>
</template> </template>
<script> <script>
import ListItem from '@/components/ListItem.vue'
import ModalDialogComposer from '@/components/ModalDialogComposer.vue' import ModalDialogComposer from '@/components/ModalDialogComposer.vue'
export default { export default {
name: 'ListComposers', name: 'ListComposers',
components: { ModalDialogComposer }, components: { ListItem, ModalDialogComposer },
props: { items: { required: true, type: Object } }, props: { items: { required: true, type: Object } },
data() { data() {
return { selectedItem: {}, showDetailsModal: false } return { selectedItem: {}, showDetailsModal: false }
}, },
methods: { methods: {
open(item) { open(item) {
this.selectedItem = item this.selectedItem = item
@ -53,8 +34,7 @@ export default {
params: { name: item.name } params: { name: item.name }
}) })
}, },
openDetails(item) {
openDialog(item) {
this.selectedItem = item this.selectedItem = item
this.showDetailsModal = true this.showDetailsModal = true
} }

View File

@ -27,22 +27,20 @@
> >
<mdicon class="media-left icon" name="folder" /> <mdicon class="media-left icon" name="folder" />
<div class="media-content"> <div class="media-content">
<p class="is-size-6 has-text-weight-bold" v-text="item.name" /> <div class="is-size-6 has-text-weight-bold" v-text="item.name" />
</div> </div>
<div class="media-right"> <div class="media-right">
<a @click.prevent.stop="openDialog(item)"> <a @click.prevent.stop="openDetails(item)">
<mdicon class="icon has-text-grey" name="dots-vertical" size="16" /> <mdicon class="icon has-text-grey" name="dots-vertical" size="16" />
</a> </a>
</div> </div>
</div> </div>
</template> </template>
<teleport to="#app"> <modal-dialog-directory
<modal-dialog-directory :item="selectedItem"
:item="selectedItem" :show="showDetailsModal"
:show="showDetailsModal" @close="showDetailsModal = false"
@close="showDetailsModal = false" />
/>
</teleport>
</template> </template>
<script> <script>
@ -80,7 +78,7 @@ export default {
} }
this.$router.push(route) this.$router.push(route)
}, },
openDialog(item) { openDetails(item) {
this.selectedItem = item.path this.selectedItem = item.path
this.showDetailsModal = true this.showDetailsModal = true
}, },

View File

@ -1,45 +1,28 @@
<template> <template>
<template v-for="item in items" :key="item.itemId"> <list-item
<div v-if="!item.isItem" class="py-5"> v-for="item in items"
<div class="media-content"> :key="item.itemId"
<span :is-item="item.isItem"
:id="`index_${item.index}`" :index="item.index"
class="tag is-small has-text-weight-bold" :lines="[item.item.name]"
v-text="item.index" @open="open(item.item)"
/> @open-details="openDetails(item.item)"
</div> />
</div> <modal-dialog-genre
<div :item="selectedItem"
v-else :media_kind="media_kind"
class="media is-align-items-center is-clickable mb-0" :show="showDetailsModal"
@click="open(item.item)" @close="showDetailsModal = false"
> />
<div class="media-content">
<p class="is-size-6 has-text-weight-bold" v-text="item.item.name" />
</div>
<div class="media-right">
<a @click.prevent.stop="openDialog(item.item)">
<mdicon class="icon has-text-grey" name="dots-vertical" size="16" />
</a>
</div>
</div>
</template>
<teleport to="#app">
<modal-dialog-genre
:item="selectedItem"
:media_kind="media_kind"
:show="showDetailsModal"
@close="showDetailsModal = false"
/>
</teleport>
</template> </template>
<script> <script>
import ListItem from '@/components/ListItem.vue'
import ModalDialogGenre from '@/components/ModalDialogGenre.vue' import ModalDialogGenre from '@/components/ModalDialogGenre.vue'
export default { export default {
name: 'ListGenres', name: 'ListGenres',
components: { ModalDialogGenre }, components: { ListItem, ModalDialogGenre },
props: { props: {
items: { required: true, type: Object }, items: { required: true, type: Object },
media_kind: { required: true, type: String } media_kind: { required: true, type: String }
@ -55,7 +38,7 @@ export default {
query: { media_kind: this.media_kind } query: { media_kind: this.media_kind }
}) })
}, },
openDialog(item) { openDetails(item) {
this.selectedItem = item this.selectedItem = item
this.showDetailsModal = true this.showDetailsModal = true
} }

View File

@ -0,0 +1,80 @@
<template>
<div v-if="!isItem" class="py-5">
<div class="media-content">
<span
:id="`index_${index}`"
class="tag is-small has-text-weight-bold"
v-text="index"
/>
</div>
</div>
<div
v-else
class="media is-align-items-center is-clickable mb-0"
:class="{ 'with-progress': progress }"
@click="open"
>
<mdicon v-if="icon" class="media-left icon" :name="icon" />
<control-image v-if="image" :url="image" class="media-left is-small" />
<div class="media-content">
<div
v-for="(line, position) in lines"
:key="position"
:class="{
'is-size-6': position === 0,
'is-size-7': position !== 0,
'has-text-weight-bold': position !== 2,
'has-text-grey': position !== 0
}"
v-text="line"
/>
<progress
v-if="progress"
class="progress is-dark"
max="1"
:value="progress"
/>
</div>
<div class="media-right">
<a @click.prevent.stop="openDetails">
<mdicon class="icon has-text-grey" name="dots-vertical" size="16" />
</a>
</div>
</div>
</template>
<script>
import ControlImage from '@/components/ControlImage.vue'
export default {
name: 'ListItem',
components: { ControlImage },
props: {
icon: { default: null, type: String },
image: { default: null, type: String },
index: { default: null, type: [String, Number] },
isItem: { default: true, type: Boolean },
lines: { default: null, type: Array },
progress: { default: null, type: Number }
},
emits: ['open', 'openDetails'],
methods: {
open() {
this.$emit('open')
},
openDetails() {
this.$emit('openDetails')
}
}
}
</script>
<style scoped>
.progress {
height: 0.25rem;
}
.media.with-progress {
margin-top: 0.375rem;
}
</style>

View File

@ -52,7 +52,7 @@ import webapi from '@/webapi'
export default { export default {
name: 'ListItemQueueItem', name: 'ListItemQueueItem',
props: { props: {
current_position: { required: true, type: Number }, currentPosition: { required: true, type: Number },
editing: Boolean, editing: Boolean,
item: { required: true, type: Object }, item: { required: true, type: Object },
position: { required: true, type: Number }, position: { required: true, type: Number },
@ -66,7 +66,7 @@ export default {
return this.item.id === this.playerStore.item_id return this.item.id === this.playerStore.item_id
}, },
isNext() { isNext() {
return this.current_position < 0 || this.position >= this.current_position return this.currentPosition < 0 || this.position >= this.currentPosition
} }
}, },
methods: { methods: {

View File

@ -1,41 +1,32 @@
<template> <template>
<template v-for="item in items" :key="item.itemId"> <list-item
<div v-for="item in items"
class="media is-align-items-center is-clickable mb-0" :key="item.itemId"
@click="open(item.item)" :icon="icon(item.item)"
> :is-item="item.isItem"
<mdicon class="media-left icon" :name="icon(item.item)" /> :index="item.index"
<div class="media-content"> :lines="[item.item.name]"
<p class="is-size-6 has-text-weight-bold" v-text="item.item.name" /> @open="open(item.item)"
</div> @open-details="openDetails(item.item)"
<div class="media-right"> />
<a @click.prevent.stop="openDialog(item.item)"> <modal-dialog-playlist
<mdicon class="icon has-text-grey" name="dots-vertical" size="16" /> :item="selectedItem"
</a> :show="showDetailsModal"
</div> @close="showDetailsModal = false"
</div> />
</template>
<teleport to="#app">
<modal-dialog-playlist
:item="selectedItem"
:show="showDetailsModal"
@close="showDetailsModal = false"
/>
</teleport>
</template> </template>
<script> <script>
import ListItem from '@/components/ListItem.vue'
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist.vue' import ModalDialogPlaylist from '@/components/ModalDialogPlaylist.vue'
export default { export default {
name: 'ListPlaylists', name: 'ListPlaylists',
components: { ModalDialogPlaylist }, components: { ListItem, ModalDialogPlaylist },
props: { items: { required: true, type: Object } }, props: { items: { required: true, type: Object } },
data() { data() {
return { selectedItem: {}, showDetailsModal: false } return { selectedItem: {}, showDetailsModal: false }
}, },
methods: { methods: {
icon(item) { icon(item) {
if (item.type === 'folder') { if (item.type === 'folder') {
@ -52,7 +43,7 @@ export default {
this.$router.push({ name: 'playlist', params: { id: item.id } }) this.$router.push({ name: 'playlist', params: { id: item.id } })
} }
}, },
openDialog(item) { openDetails(item) {
this.selectedItem = item this.selectedItem = item
this.showDetailsModal = true this.showDetailsModal = true
} }

View File

@ -1,40 +1,27 @@
<template> <template>
<template v-for="item in items" :key="item.id"> <list-item
<div v-for="item in items"
class="media is-align-items-center is-clickable mb-0" :key="item.id"
@click="open(item)" :is-item="item.isItem"
> :index="item.index"
<div class="media-content"> :lines="[item.name, item.owner.display_name]"
<div class="is-size-6 has-text-weight-bold" v-text="item.name" /> @open="open(item)"
<div @open-details="openDetails(item)"
class="is-size-7 has-text-weight-bold has-text-grey" />
v-text="item.owner.display_name" <modal-dialog-playlist-spotify
/> :item="selectedItem"
</div> :show="showDetailsModal"
<div class="media-right"> @close="showDetailsModal = false"
<a @click.prevent.stop="openDialog(item)"> />
<mdicon class="icon has-text-grey" name="dots-vertical" size="16" />
</a>
</div>
</div>
</template>
<teleport to="#app">
<modal-dialog-playlist-spotify
:item="selectedItem"
:show="showDetailsModal"
@close="showDetailsModal = false"
/>
</teleport>
</template> </template>
<script> <script>
import ListItem from '@/components/ListItem.vue'
import ModalDialogPlaylistSpotify from '@/components/ModalDialogPlaylistSpotify.vue' import ModalDialogPlaylistSpotify from '@/components/ModalDialogPlaylistSpotify.vue'
export default { export default {
name: 'ListPlaylistsSpotify', name: 'ListPlaylistsSpotify',
components: { components: { ListItem, ModalDialogPlaylistSpotify },
ModalDialogPlaylistSpotify
},
props: { items: { required: true, type: Object } }, props: { items: { required: true, type: Object } },
data() { data() {
@ -45,7 +32,7 @@ export default {
open(item) { open(item) {
this.$router.push({ name: 'playlist-spotify', params: { id: item.id } }) this.$router.push({ name: 'playlist-spotify', params: { id: item.id } })
}, },
openDialog(item) { openDetails(item) {
this.selectedItem = item this.selectedItem = item
this.showDetailsModal = true this.showDetailsModal = true
} }

View File

@ -1,73 +1,36 @@
<template> <template>
<template v-for="item in items" :key="item.itemId"> <list-item
<div v-if="!item.isItem" class="py-5"> v-for="item in items"
<span :key="item.itemId"
:id="`index_${item.index}`" :icon="icon"
class="tag is-small has-text-weight-bold" :is-item="item.isItem"
v-text="item.index" :index="item.index"
/> :lines="[item.item.title, item.item.artist, item.item.album]"
</div> :progress="progress(item.item)"
<div @open="open(item.item)"
v-else @open-details="openDetails(item.item)"
class="media is-align-items-center is-clickable mb-0" />
:class="{ 'with-progress': showProgress }" <modal-dialog-track
@click="play(item.item)" :item="selectedItem"
> :show="showDetailsModal"
<mdicon @close="showDetailsModal = false"
v-if="showIcon" @play-count-changed="$emit('play-count-changed')"
class="media-left icon" />
name="file-music-outline"
/>
<div class="media-content">
<div
class="is-size-6 has-text-weight-bold"
:class="{
'has-text-grey':
item.item.media_kind === 'podcast' && item.item.play_count > 0
}"
v-text="item.item.title"
/>
<div
class="is-size-7 has-text-weight-bold has-text-grey"
v-text="item.item.artist"
/>
<div class="is-size-7 has-text-grey" v-text="item.item.album" />
<progress
v-if="showProgress && item.item.seek_ms > 0"
class="progress is-dark"
:max="item.item.length_ms"
:value="item.item.seek_ms"
/>
</div>
<div class="media-right">
<a @click.prevent.stop="openDialog(item.item)">
<mdicon class="icon has-text-grey" name="dots-vertical" size="16" />
</a>
</div>
</div>
</template>
<teleport to="#app">
<modal-dialog-track
:item="selectedItem"
:show="showDetailsModal"
@close="showDetailsModal = false"
@play-count-changed="$emit('play-count-changed')"
/>
</teleport>
</template> </template>
<script> <script>
import ListItem from '@/components/ListItem.vue'
import ModalDialogTrack from '@/components/ModalDialogTrack.vue' import ModalDialogTrack from '@/components/ModalDialogTrack.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {
name: 'ListTracks', name: 'ListTracks',
components: { ModalDialogTrack }, components: { ListItem, ModalDialogTrack },
props: { props: {
expression: { default: '', type: String }, expression: { default: '', type: String },
items: { required: true, type: Object }, items: { default: null, type: Object },
showIcon: Boolean, icon: { default: null, type: String },
showProgress: Boolean, showProgress: { default: false, type: Boolean },
uris: { default: '', type: String } uris: { default: '', type: String }
}, },
emits: ['play-count-changed'], emits: ['play-count-changed'],
@ -75,11 +38,7 @@ export default {
return { selectedItem: {}, showDetailsModal: false } return { selectedItem: {}, showDetailsModal: false }
}, },
methods: { methods: {
openDialog(item) { open(item) {
this.selectedItem = item
this.showDetailsModal = true
},
play(item) {
if (this.uris) { if (this.uris) {
webapi.player_play_uri(this.uris, false, this.items.items.indexOf(item)) webapi.player_play_uri(this.uris, false, this.items.items.indexOf(item))
} else if (this.expression) { } else if (this.expression) {
@ -91,17 +50,20 @@ export default {
} else { } else {
webapi.player_play_uri(item.uri, false) webapi.player_play_uri(item.uri, false)
} }
},
openDetails(item) {
this.selectedItem = item
this.showDetailsModal = true
},
// 'has-text-grey': item.item.media_kind === 'podcast' && item.item.play_count > 0
progress(item) {
if (item.item) {
if (this.showProgress && item.item.seek_ms > 0) {
return item.item.seek_ms / item.item.length_ms
}
}
return null
} }
} }
} }
</script> </script>
<style scoped>
.progress {
height: 0.25rem;
}
.media.with-progress {
margin-top: 0.375rem;
}
</style>

View File

@ -36,19 +36,17 @@
</div> </div>
</div> </div>
<div class="media-right"> <div class="media-right">
<a @click.prevent.stop="openDialog(item)"> <a @click.prevent.stop="openDetails(item)">
<mdicon class="icon has-text-grey" name="dots-vertical" size="16" /> <mdicon class="icon has-text-grey" name="dots-vertical" size="16" />
</a> </a>
</div> </div>
</div> </div>
</template> </template>
<teleport to="#app"> <modal-dialog-track-spotify
<modal-dialog-track-spotify :item="selectedItem"
:item="selectedItem" :show="showDetailsModal"
:show="showDetailsModal" @close="showDetailsModal = false"
@close="showDetailsModal = false" />
/>
</teleport>
</template> </template>
<script> <script>
@ -66,7 +64,7 @@ export default {
return { selectedItem: {}, showDetailsModal: false } return { selectedItem: {}, showDetailsModal: false }
}, },
methods: { methods: {
openDialog(item) { openDetails(item) {
this.selectedItem = item this.selectedItem = item
this.showDetailsModal = true this.showDetailsModal = true
}, },

View File

@ -4,12 +4,12 @@
class="lyrics" class="lyrics"
@touchstart="autoScrolling = false" @touchstart="autoScrolling = false"
@touchend="autoScrolling = true" @touchend="autoScrolling = true"
@scroll.passive="start_scrolling" @scroll.passive="startScrolling"
@wheel.passive="start_scrolling" @wheel.passive="startScrolling"
> >
<template v-for="(verse, index) in lyrics" :key="index"> <template v-for="(verse, index) in lyrics" :key="index">
<div <div
v-if="index === verse_index" v-if="index === verseIndex"
:class="{ 'is-highlighted': playerStore.isPlaying }" :class="{ 'is-highlighted': playerStore.isPlaying }"
> >
<span <span
@ -85,7 +85,7 @@ export default {
} }
return parsed return parsed
}, },
verse_index() { verseIndex() {
if (this.lyrics.length && this.lyrics[0].time) { if (this.lyrics.length && this.lyrics[0].time) {
const currentTime = this.playerStore.item_progress_ms / 1000, const currentTime = this.playerStore.item_progress_ms / 1000,
la = this.lyrics, la = this.lyrics,
@ -96,7 +96,7 @@ export default {
la[this.lastIndex].time > currentTime la[this.lastIndex].time > currentTime
// Reset the cache when the track has changed or has been seeked // Reset the cache when the track has changed or has been seeked
if (trackChanged || trackSeeked) { if (trackChanged || trackSeeked) {
this.reset_scrolling() this.resetScrolling()
} }
// Check the next two items and the last one before searching // Check the next two items and the last one before searching
if ( if (
@ -134,20 +134,20 @@ export default {
} }
return index return index
} }
this.reset_scrolling() this.resetScrolling()
return -1 return -1
} }
}, },
watch: { watch: {
verse_index() { verseIndex() {
if (this.autoScrolling) { if (this.autoScrolling) {
this.scroll_to_verse() this.scrollToVerse()
} }
this.lastIndex = this.verse_index this.lastIndex = this.verseIndex
} }
}, },
methods: { methods: {
reset_scrolling() { resetScrolling() {
// Scroll to the start of the lyrics in all cases // Scroll to the start of the lyrics in all cases
if (this.playerStore.item_id !== this.lastItemId && this.$refs.lyrics) { if (this.playerStore.item_id !== this.lastItemId && this.$refs.lyrics) {
this.$refs.lyrics.scrollTo(0, 0) this.$refs.lyrics.scrollTo(0, 0)
@ -155,13 +155,13 @@ export default {
this.lastItemId = this.playerStore.item_id this.lastItemId = this.playerStore.item_id
this.lastIndex = -1 this.lastIndex = -1
}, },
scroll_to_verse() { scrollToVerse() {
const pane = this.$refs.lyrics const pane = this.$refs.lyrics
if (this.verse_index === -1) { if (this.verseIndex === -1) {
pane.scrollTo(0, 0) pane.scrollTo(0, 0)
return return
} }
const currentVerse = pane.children[this.verse_index] const currentVerse = pane.children[this.verseIndex]
pane.scrollBy({ pane.scrollBy({
behavior: 'smooth', behavior: 'smooth',
left: 0, left: 0,
@ -172,7 +172,7 @@ export default {
pane.scrollTop pane.scrollTop
}) })
}, },
start_scrolling(event) { startScrolling(event) {
// Consider only user events // Consider only user events
if (event.screenX ?? event.screenY) { if (event.screenX ?? event.screenY) {
this.autoScrolling = false this.autoScrolling = false

View File

@ -11,14 +11,14 @@
<p class="control has-icons-left"> <p class="control has-icons-left">
<input <input
ref="playlist_name_field" ref="playlist_name_field"
v-model="playlist_name" v-model="playlistName"
class="input" class="input"
type="text" type="text"
pattern=".+" pattern=".+"
required required
:placeholder="$t('dialog.playlist.save.playlist-name')" :placeholder="$t('dialog.playlist.save.playlist-name')"
:disabled="loading" :disabled="loading"
@input="check_name" @input="check"
/> />
<mdicon class="icon is-left" name="playlist-music" size="16" /> <mdicon class="icon is-left" name="playlist-music" size="16" />
</p> </p>
@ -41,7 +41,7 @@ export default {
return { return {
disabled: true, disabled: true,
loading: false, loading: false,
playlist_name: '' playlistName: ''
} }
}, },
computed: { computed: {
@ -75,17 +75,17 @@ export default {
cancel() { cancel() {
this.$emit('close') this.$emit('close')
}, },
check_name(event) { check(event) {
const { validity } = event.target const { validity } = event.target
this.disabled = validity.patternMismatch || validity.valueMissing this.disabled = validity.patternMismatch || validity.valueMissing
}, },
save() { save() {
this.loading = true this.loading = true
webapi webapi
.queue_save_playlist(this.playlist_name) .queue_save_playlist(this.playlistName)
.then(() => { .then(() => {
this.$emit('close') this.$emit('close')
this.playlist_name = '' this.playlistName = ''
}) })
.catch(() => { .catch(() => {
this.loading = false this.loading = false

View File

@ -23,7 +23,7 @@ export default {
}, },
data() { data() {
return { return {
spotify_track: {} spotifyTrack: {}
} }
}, },
computed: { computed: {
@ -89,10 +89,10 @@ export default {
spotifyApi spotifyApi
.getTrack(this.item.path.slice(this.item.path.lastIndexOf(':') + 1)) .getTrack(this.item.path.slice(this.item.path.lastIndexOf(':') + 1))
.then((response) => { .then((response) => {
this.spotify_track = response this.spotifyTrack = response
}) })
} else { } else {
this.spotify_track = {} this.spotifyTrack = {}
} }
} }
}, },
@ -102,7 +102,7 @@ export default {
if (this.item.data_kind === 'spotify') { if (this.item.data_kind === 'spotify') {
this.$router.push({ this.$router.push({
name: 'music-spotify-album', name: 'music-spotify-album',
params: { id: this.spotify_track.album.id } params: { id: this.spotifyTrack.album.id }
}) })
} else if (this.item.media_kind === 'podcast') { } else if (this.item.media_kind === 'podcast') {
this.$router.push({ this.$router.push({
@ -126,7 +126,7 @@ export default {
if (this.item.data_kind === 'spotify') { if (this.item.data_kind === 'spotify') {
this.$router.push({ this.$router.push({
name: 'music-spotify-artist', name: 'music-spotify-artist',
params: { id: this.spotify_track.artists[0].id } params: { id: this.spotifyTrack.artists[0].id }
}) })
} else if ( } else if (
this.item.media_kind === 'music' || this.item.media_kind === 'music' ||

View File

@ -31,7 +31,7 @@
</div> </div>
</div> </div>
</div> </div>
<control-switch v-model="rescan_metadata"> <control-switch v-model="rescanMetadata">
<template #label> <template #label>
<span v-text="$t('dialog.update.rescan-metadata')" /> <span v-text="$t('dialog.update.rescan-metadata')" />
</template> </template>
@ -64,7 +64,7 @@ export default {
}, },
data() { data() {
return { return {
rescan_metadata: false rescanMetadata: false
} }
}, },
computed: { computed: {
@ -87,7 +87,7 @@ export default {
}, },
methods: { methods: {
analyse() { analyse() {
if (this.rescan_metadata) { if (this.rescanMetadata) {
webapi.library_rescan(this.libraryStore.update_dialog_scan_kind) webapi.library_rescan(this.libraryStore.update_dialog_scan_kind)
} else { } else {
webapi.library_update(this.libraryStore.update_dialog_scan_kind) webapi.library_update(this.libraryStore.update_dialog_scan_kind)

View File

@ -107,6 +107,12 @@ export default {
show: true, show: true,
sub: true sub: true
}, },
{
key: 'navigation.composers',
name: 'music-composers',
show: true,
sub: true
},
{ {
key: 'navigation.spotify', key: 'navigation.spotify',
name: 'music-spotify', name: 'music-spotify',

View File

@ -1,14 +1,17 @@
<template> <template>
<section v-if="notifications.length > 0" class="notifications"> <section v-if="!notificationsStore.isEmpty" class="notifications">
<div class="columns is-centered"> <div class="columns is-centered">
<div class="column is-half"> <div class="column is-half">
<div <div
v-for="notification in notifications" v-for="notification in notificationsStore.list"
:key="notification.id" :key="notification.id"
class="notification" class="notification"
:class="notification.type ? `is-${notification.type}` : ''" :class="notification.type ? `is-${notification.type}` : ''"
> >
<button class="delete" @click="remove(notification)" /> <button
class="delete"
@click="notificationsStore.remove(notification)"
/>
<div class="text" v-text="notification.text" /> <div class="text" v-text="notification.text" />
</div> </div>
</div> </div>
@ -21,21 +24,8 @@ import { useNotificationsStore } from '@/stores/notifications'
export default { export default {
name: 'NotificationList', name: 'NotificationList',
setup() { setup() {
return { notificationsStore: useNotificationsStore() } return { notificationsStore: useNotificationsStore() }
},
computed: {
notifications() {
return this.notificationsStore.list
}
},
methods: {
remove(notification) {
this.notificationsStore.remove(notification)
}
} }
} }
</script> </script>

View File

@ -20,7 +20,8 @@
"save": "Speichern", "save": "Speichern",
"send": "Senden", "send": "Senden",
"show-more": "Zeige mehr", "show-more": "Zeige mehr",
"shuffle": "Zufallswiedergabe" "shuffle": "Zufallswiedergabe",
"update": "Neu einlesen"
}, },
"count": { "count": {
"albums": "{count} Album|{count} Album|{count} Alben", "albums": "{count} Album|{count} Album|{count} Alben",
@ -116,6 +117,7 @@
"albums": "Alben", "albums": "Alben",
"artists": "Künstler", "artists": "Künstler",
"audiobooks": "Hörbücher", "audiobooks": "Hörbücher",
"composers": "Komponisten",
"now-playing": " - {album}", "now-playing": " - {album}",
"stream-error": "HTTP-stream-Fehler: Stream kann nicht geladen werden oder wurde wg. Netzwerkfehler gestopt", "stream-error": "HTTP-stream-Fehler: Stream kann nicht geladen werden oder wurde wg. Netzwerkfehler gestopt",
"stream": "HTTP-stream", "stream": "HTTP-stream",
@ -216,8 +218,7 @@
}, },
"podcasts": { "podcasts": {
"new-episodes": "Neue Episoden", "new-episodes": "Neue Episoden",
"title": "Podcasts", "title": "Podcasts"
"update": "Neu einlesen"
}, },
"queue": { "queue": {
"title": "Warteschlange" "title": "Warteschlange"

View File

@ -20,7 +20,8 @@
"save": "Save", "save": "Save",
"send": "Send", "send": "Send",
"show-more": "Show more", "show-more": "Show more",
"shuffle": "Shuffle" "shuffle": "Shuffle",
"update": "Update"
}, },
"count": { "count": {
"albums": "{count} album|{count} album|{count} albums", "albums": "{count} album|{count} album|{count} albums",
@ -116,6 +117,7 @@
"albums": "Albums", "albums": "Albums",
"artists": "Artists", "artists": "Artists",
"audiobooks": "Audiobooks", "audiobooks": "Audiobooks",
"composers": "Composers",
"now-playing": " - {album}", "now-playing": " - {album}",
"stream-error": "HTTP stream error: failed to load stream or stopped loading due to network problem", "stream-error": "HTTP stream error: failed to load stream or stopped loading due to network problem",
"stream": "HTTP stream", "stream": "HTTP stream",
@ -216,8 +218,7 @@
}, },
"podcasts": { "podcasts": {
"new-episodes": "New Episodes", "new-episodes": "New Episodes",
"title": "Podcasts", "title": "Podcasts"
"update": "Update"
}, },
"queue": { "queue": {
"title": "Queue" "title": "Queue"

View File

@ -20,7 +20,8 @@
"save": "Enregistrer", "save": "Enregistrer",
"send": "Envoyer", "send": "Envoyer",
"show-more": "Afficher plus", "show-more": "Afficher plus",
"shuffle": "Lecture aléatoire" "shuffle": "Lecture aléatoire",
"update": "Actualiser"
}, },
"count": { "count": {
"albums": "{count} album|{count} album|{count} albums", "albums": "{count} album|{count} album|{count} albums",
@ -116,6 +117,7 @@
"albums": "Albums", "albums": "Albums",
"artists": "Artistes", "artists": "Artistes",
"audiobooks": "Livres audio", "audiobooks": "Livres audio",
"composers": "Compositeurs",
"now-playing": " - {album}", "now-playing": " - {album}",
"stream-error": "Erreur du flux HTTP : échec du chargement du flux ou arrêt du chargement en raison dun problème réseau", "stream-error": "Erreur du flux HTTP : échec du chargement du flux ou arrêt du chargement en raison dun problème réseau",
"stream": "Flux HTTP", "stream": "Flux HTTP",
@ -216,8 +218,7 @@
}, },
"podcasts": { "podcasts": {
"new-episodes": "Nouveaux épisodes", "new-episodes": "Nouveaux épisodes",
"title": "Podcasts", "title": "Podcasts"
"update": "Actualiser"
}, },
"queue": { "queue": {
"title": "File dattente" "title": "File dattente"

View File

@ -20,7 +20,8 @@
"save": "保存", "save": "保存",
"send": "发送", "send": "发送",
"show-more": "显示更多", "show-more": "显示更多",
"shuffle": "随机播放" "shuffle": "随机播放",
"update": "更新"
}, },
"count": { "count": {
"albums": "{count} 张专辑|{count} 张专辑", "albums": "{count} 张专辑|{count} 张专辑",
@ -116,6 +117,7 @@
"albums": "专辑", "albums": "专辑",
"artists": "艺人", "artists": "艺人",
"audiobooks": "有声读物", "audiobooks": "有声读物",
"composers": "作曲家",
"now-playing": " - {album}", "now-playing": " - {album}",
"stream-error": "HTTP流错误流载入失败或者由于网络原因无法载入", "stream-error": "HTTP流错误流载入失败或者由于网络原因无法载入",
"stream": "HTTP流", "stream": "HTTP流",
@ -216,8 +218,7 @@
}, },
"podcasts": { "podcasts": {
"new-episodes": "最新单集", "new-episodes": "最新单集",
"title": "播客", "title": "播客"
"update": "更新"
}, },
"queue": { "queue": {
"title": "清单" "title": "清单"

View File

@ -20,7 +20,8 @@
"save": "儲存", "save": "儲存",
"send": "發送", "send": "發送",
"show-more": "顯示更多", "show-more": "顯示更多",
"shuffle": "隨機播放" "shuffle": "隨機播放",
"update": "更新"
}, },
"count": { "count": {
"albums": "{count} 張專輯|{count} 張專輯", "albums": "{count} 張專輯|{count} 張專輯",
@ -116,6 +117,7 @@
"albums": "專輯", "albums": "專輯",
"artists": "藝人", "artists": "藝人",
"audiobooks": "有聲書", "audiobooks": "有聲書",
"composers": "作曲家",
"now-playing": " - {album}", "now-playing": " - {album}",
"stream-error": "HTTP串流錯誤串流載入失敗或者由於網絡原因無法載入", "stream-error": "HTTP串流錯誤串流載入失敗或者由於網絡原因無法載入",
"stream": "HTTP串流", "stream": "HTTP串流",
@ -216,8 +218,7 @@
}, },
"podcasts": { "podcasts": {
"new-episodes": "最新單集", "new-episodes": "最新單集",
"title": "Podcast", "title": "Podcast"
"update": "更新"
}, },
"queue": { "queue": {
"title": "清單" "title": "清單"

View File

@ -34,8 +34,6 @@
/> />
</div> </div>
</div> </div>
</template>
<template #footer>
<div <div
class="is-size-7 mt-6" class="is-size-7 mt-6"
v-text=" v-text="

View File

@ -15,7 +15,7 @@
:button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }" :button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }"
/> />
<control-button <control-button
:button="{ handler: showDetails, icon: 'dots-horizontal' }" :button="{ handler: openDetails, icon: 'dots-horizontal' }"
/> />
</div> </div>
</template> </template>
@ -25,7 +25,7 @@
:artist="album.artist" :artist="album.artist"
:album="album.name" :album="album.name"
class="is-clickable is-medium" class="is-clickable is-medium"
@click="showDetails" @click="openDetails"
/> />
</template> </template>
<template #content> <template #content>
@ -98,11 +98,11 @@ export default {
params: { id: this.album.artist_id } params: { id: this.album.artist_id }
}) })
}, },
openDetails() {
this.showDetailsModal = true
},
play() { play() {
webapi.player_play_uri(this.album.uri, true) webapi.player_play_uri(this.album.uri, true)
},
showDetails() {
this.showDetailsModal = true
} }
} }
} }

View File

@ -15,7 +15,7 @@
:button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }" :button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }"
/> />
<control-button <control-button
:button="{ handler: showDetails, icon: 'dots-horizontal' }" :button="{ handler: openDetails, icon: 'dots-horizontal' }"
/> />
</div> </div>
</template> </template>
@ -25,7 +25,7 @@
:artist="album.artists[0].name" :artist="album.artists[0].name"
:album="album.name" :album="album.name"
class="is-clickable is-medium" class="is-clickable is-medium"
@click="showDetails" @click="openDetails"
/> />
</template> </template>
<template #content> <template #content>
@ -102,12 +102,12 @@ export default {
params: { id: this.album.artists[0].id } params: { id: this.album.artists[0].id }
}) })
}, },
openDetails() {
this.showDetailsModal = true
},
play() { play() {
this.showDetailsModal = false this.showDetailsModal = false
webapi.player_play_uri(this.album.uri, true) webapi.player_play_uri(this.album.uri, true)
},
showDetails() {
this.showDetailsModal = true
} }
} }
} }

View File

@ -36,7 +36,7 @@
v-text="$t('options.sort.title')" v-text="$t('options.sort.title')"
/> />
<control-dropdown <control-dropdown
v-model:value="uiStore.albums_sort" v-model:value="uiStore.albumsSort"
:options="groupings" :options="groupings"
/> />
</div> </div>
@ -101,7 +101,7 @@ export default {
computed: { computed: {
albums() { albums() {
const { options } = this.groupings.find( const { options } = this.groupings.find(
(grouping) => grouping.id === this.uiStore.albums_sort (grouping) => grouping.id === this.uiStore.albumsSort
) )
options.filters = [ options.filters = [
(album) => !this.uiStore.hideSingles || album.track_count > 2, (album) => !this.uiStore.hideSingles || album.track_count > 2,

View File

@ -26,7 +26,7 @@
v-text="$t('page.artist.sort.title')" v-text="$t('page.artist.sort.title')"
/> />
<control-dropdown <control-dropdown
v-model:value="uiStore.artist_albums_sort" v-model:value="uiStore.artistAlbumsSort"
:options="groupings" :options="groupings"
/> />
</div> </div>
@ -37,7 +37,7 @@
</template> </template>
<template #heading-right> <template #heading-right>
<control-button <control-button
:button="{ handler: showDetails, icon: 'dots-horizontal' }" :button="{ handler: openDetails, icon: 'dots-horizontal' }"
/> />
<control-button <control-button
:button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }" :button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }"
@ -110,7 +110,7 @@ export default {
computed: { computed: {
albums() { albums() {
const { options } = this.groupings.find( const { options } = this.groupings.find(
(grouping) => grouping.id === this.uiStore.artist_albums_sort (grouping) => grouping.id === this.uiStore.artistAlbumsSort
) )
options.filters = [ options.filters = [
(album) => !this.uiStore.hideSpotify || album.data_kind !== 'spotify' (album) => !this.uiStore.hideSpotify || album.data_kind !== 'spotify'
@ -153,6 +153,9 @@ export default {
} }
}, },
methods: { methods: {
openDetails() {
this.showDetailsModal = true
},
openTracks() { openTracks() {
this.$router.push({ this.$router.push({
name: 'music-artist-tracks', name: 'music-artist-tracks',
@ -164,9 +167,6 @@ export default {
this.albums.items.map((item) => item.uri).join(), this.albums.items.map((item) => item.uri).join(),
true true
) )
},
showDetails() {
this.showDetailsModal = true
} }
} }
} }

View File

@ -6,7 +6,7 @@
</template> </template>
<template #heading-right> <template #heading-right>
<control-button <control-button
:button="{ handler: showDetails, icon: 'dots-horizontal' }" :button="{ handler: openDetails, icon: 'dots-horizontal' }"
/> />
<control-button <control-button
:button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }" :button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }"
@ -106,7 +106,7 @@ export default {
heading() { heading() {
return { return {
subtitle: [{ count: this.total, key: 'count.albums' }], subtitle: [{ count: this.total, key: 'count.albums' }],
title: this.$t('artist.name') title: this.artist.name
} }
} }
}, },
@ -130,12 +130,12 @@ export default {
loaded(data.items.length, PAGE_SIZE) loaded(data.items.length, PAGE_SIZE)
}) })
}, },
openDetails() {
this.showDetailsModal = true
},
play() { play() {
this.showDetailsModal = false this.showDetailsModal = false
webapi.player_play_uri(this.artist.uri, true) webapi.player_play_uri(this.artist.uri, true)
},
showDetails() {
this.showDetailsModal = true
} }
} }
} }

View File

@ -27,7 +27,7 @@
v-text="$t('options.sort.title')" v-text="$t('options.sort.title')"
/> />
<control-dropdown <control-dropdown
v-model:value="uiStore.artist_tracks_sort" v-model:value="uiStore.artistTracksSort"
:options="groupings" :options="groupings"
/> />
</div> </div>
@ -38,14 +38,14 @@
</template> </template>
<template #heading-right> <template #heading-right>
<control-button <control-button
:button="{ handler: showDetails, icon: 'dots-horizontal' }" :button="{ handler: openDetails, icon: 'dots-horizontal' }"
/> />
<control-button <control-button
:button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }" :button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }"
/> />
</template> </template>
<template #content> <template #content>
<list-tracks :items="tracks" :uris="track_uris" /> <list-tracks :items="tracks" :uris="trackUris" />
<modal-dialog-artist <modal-dialog-artist
:item="artist" :item="artist"
:show="showDetailsModal" :show="showDetailsModal"
@ -111,7 +111,7 @@ export default {
} }
}, },
computed: { computed: {
album_count() { albumCount() {
return new Set( return new Set(
[...this.tracks] [...this.tracks]
.filter((track) => track.isItem) .filter((track) => track.isItem)
@ -139,7 +139,7 @@ export default {
return { return {
subtitle: [ subtitle: [
{ {
count: this.album_count, count: this.albumCount,
handler: this.openArtist, handler: this.openArtist,
key: 'count.albums' key: 'count.albums'
}, },
@ -148,12 +148,12 @@ export default {
title: this.artist.name title: this.artist.name
} }
}, },
track_uris() { trackUris() {
return this.trackList.items.map((item) => item.uri).join() return this.trackList.items.map((item) => item.uri).join()
}, },
tracks() { tracks() {
const { options } = this.groupings.find( const { options } = this.groupings.find(
(grouping) => grouping.id === this.uiStore.artist_tracks_sort (grouping) => grouping.id === this.uiStore.artistTracksSort
) )
options.filters = [ options.filters = [
(track) => !this.uiStore.hideSpotify || track.data_kind !== 'spotify' (track) => !this.uiStore.hideSpotify || track.data_kind !== 'spotify'
@ -169,14 +169,14 @@ export default {
params: { id: this.artist.id } params: { id: this.artist.id }
}) })
}, },
openDetails() {
this.showDetailsModal = true
},
play() { play() {
webapi.player_play_uri( webapi.player_play_uri(
this.trackList.items.map((item) => item.uri).join(), this.trackList.items.map((item) => item.uri).join(),
true true
) )
},
showDetails() {
this.showDetailsModal = true
} }
} }
} }

View File

@ -35,7 +35,7 @@
v-text="$t('options.sort.title')" v-text="$t('options.sort.title')"
/> />
<control-dropdown <control-dropdown
v-model:value="uiStore.artists_sort" v-model:value="uiStore.artistsSort"
:options="groupings" :options="groupings"
/> />
</div> </div>
@ -100,7 +100,7 @@ export default {
computed: { computed: {
artists() { artists() {
const { options } = this.groupings.find( const { options } = this.groupings.find(
(grouping) => grouping.id === this.uiStore.artists_sort (grouping) => grouping.id === this.uiStore.artistsSort
) )
options.filters = [ options.filters = [
(artist) => (artist) =>

View File

@ -19,7 +19,7 @@
}" }"
/> />
<control-button <control-button
:button="{ handler: showDetails, icon: 'dots-horizontal' }" :button="{ handler: openDetails, icon: 'dots-horizontal' }"
/> />
</div> </div>
</template> </template>
@ -29,7 +29,7 @@
:artist="album.artist" :artist="album.artist"
:album="album.name" :album="album.name"
class="is-clickable is-medium" class="is-clickable is-medium"
@click="showDetails" @click="openDetails"
/> />
</template> </template>
<template #content> <template #content>
@ -96,11 +96,11 @@ export default {
params: { id: this.album.artist_id } params: { id: this.album.artist_id }
}) })
}, },
openDetails() {
this.showDetailsModal = true
},
play() { play() {
webapi.player_play_uri(this.album.uri, false) webapi.player_play_uri(this.album.uri, false)
},
showDetails() {
this.showDetailsModal = true
} }
} }
} }

View File

@ -6,14 +6,10 @@
</template> </template>
<template #heading-right> <template #heading-right>
<control-button <control-button
:button="{ handler: showDetails, icon: 'dots-horizontal' }" :button="{ handler: openDetails, icon: 'dots-horizontal' }"
/> />
<control-button <control-button
:button="{ :button="{ handler: play, icon: 'play', key: 'actions.play' }"
handler: play,
icon: 'play',
key: 'page.audiobooks.artist.play'
}"
/> />
</template> </template>
<template #content> <template #content>
@ -85,14 +81,14 @@ export default {
} }
}, },
methods: { methods: {
openDetails() {
this.showDetailsModal = true
},
play() { play() {
webapi.player_play_uri( webapi.player_play_uri(
this.albums.items.map((item) => item.uri).join(), this.albums.items.map((item) => item.uri).join(),
false false
) )
},
showDetails() {
this.showDetailsModal = true
} }
} }
} }

View File

@ -6,7 +6,7 @@
</template> </template>
<template #heading-right> <template #heading-right>
<control-button <control-button
:button="{ handler: showDetails, icon: 'dots-horizontal' }" :button="{ handler: openDetails, icon: 'dots-horizontal' }"
/> />
<control-button <control-button
:button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }" :button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }"
@ -89,6 +89,9 @@ export default {
} }
}, },
methods: { methods: {
openDetails() {
this.showDetailsModal = true
},
openTracks() { openTracks() {
this.$router.push({ this.$router.push({
name: 'music-composer-tracks', name: 'music-composer-tracks',
@ -97,9 +100,6 @@ export default {
}, },
play() { play() {
webapi.player_play_expression(this.expression, true) webapi.player_play_expression(this.expression, true)
},
showDetails() {
this.showDetailsModal = true
} }
} }
} }

View File

@ -10,7 +10,7 @@
v-text="$t('options.sort.title')" v-text="$t('options.sort.title')"
/> />
<control-dropdown <control-dropdown
v-model:value="uiStore.composer_tracks_sort" v-model:value="uiStore.composerTracksSort"
:options="groupings" :options="groupings"
/> />
</div> </div>
@ -21,7 +21,7 @@
</template> </template>
<template #heading-right> <template #heading-right>
<control-button <control-button
:button="{ handler: showDetails, icon: 'dots-horizontal' }" :button="{ handler: openDetails, icon: 'dots-horizontal' }"
/> />
<control-button <control-button
:button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }" :button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }"
@ -129,9 +129,9 @@ export default {
}, },
tracks() { tracks() {
const { options } = this.groupings.find( const { options } = this.groupings.find(
(grouping) => grouping.id === this.uiStore.composer_tracks_sort (grouping) => grouping.id === this.uiStore.composerTracksSort
) )
return this.tracks_list.group(options) return this.trackList.group(options)
} }
}, },
methods: { methods: {
@ -141,11 +141,11 @@ export default {
params: { name: this.composer.name } params: { name: this.composer.name }
}) })
}, },
openDetails() {
this.showDetailsModal = true
},
play() { play() {
webapi.player_play_expression(this.expression, true) webapi.player_play_expression(this.expression, true)
},
showDetails() {
this.showDetailsModal = true
} }
} }
} }

View File

@ -25,7 +25,7 @@ import TabsMusic from '@/components/TabsMusic.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
const dataObject = { const dataObject = {
load(to) { load() {
return webapi.library_composers('music') return webapi.library_composers('music')
}, },
set(vm, response) { set(vm, response) {
@ -45,7 +45,7 @@ export default {
TabsMusic TabsMusic
}, },
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => { dataObject.load().then((response) => {
next((vm) => dataObject.set(vm, response)) next((vm) => dataObject.set(vm, response))
}) })
}, },

View File

@ -6,7 +6,7 @@
</template> </template>
<template #heading-right> <template #heading-right>
<control-button <control-button
:button="{ handler: showDetails, icon: 'dots-horizontal' }" :button="{ handler: openDetails, icon: 'dots-horizontal' }"
/> />
<control-button <control-button
:button="{ handler: play, icon: 'play', key: 'actions.play' }" :button="{ handler: play, icon: 'play', key: 'actions.play' }"
@ -18,7 +18,7 @@
<list-tracks <list-tracks
:expression="expression" :expression="expression"
:items="tracks" :items="tracks"
:show-icon="true" icon="file-music-outline"
/> />
<modal-dialog-directory <modal-dialog-directory
:item="current" :item="current"
@ -120,12 +120,12 @@ export default {
} }
}, },
methods: { methods: {
openDetails() {
this.showDetailsModal = true
},
play() { play() {
webapi.player_play_expression(this.expression, false) webapi.player_play_expression(this.expression, false)
}, },
showDetails() {
this.showDetailsModal = true
},
transform(path) { transform(path) {
return { name: path.slice(path.lastIndexOf('/') + 1), path } return { name: path.slice(path.lastIndexOf('/') + 1), path }
} }

View File

@ -9,7 +9,7 @@
</template> </template>
<template #heading-right> <template #heading-right>
<control-button <control-button
:button="{ handler: showDetails, icon: 'dots-horizontal' }" :button="{ handler: openDetails, icon: 'dots-horizontal' }"
/> />
<control-button <control-button
:button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }" :button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }"
@ -95,6 +95,9 @@ export default {
} }
}, },
methods: { methods: {
openDetails() {
this.showDetailsModal = true
},
openTracks() { openTracks() {
this.showDetailsModal = false this.showDetailsModal = false
this.$router.push({ this.$router.push({
@ -108,9 +111,6 @@ export default {
`genre is "${this.genre.name}" and media_kind is ${this.media_kind}`, `genre is "${this.genre.name}" and media_kind is ${this.media_kind}`,
true true
) )
},
showDetails() {
this.showDetailsModal = true
} }
} }
} }

View File

@ -10,7 +10,7 @@
v-text="$t('options.sort.title')" v-text="$t('options.sort.title')"
/> />
<control-dropdown <control-dropdown
v-model:value="uiStore.genre_tracks_sort" v-model:value="uiStore.genreTracksSort"
:options="groupings" :options="groupings"
/> />
</div> </div>
@ -21,7 +21,7 @@
</template> </template>
<template #heading-right> <template #heading-right>
<control-button <control-button
:button="{ handler: showDetails, icon: 'dots-horizontal' }" :button="{ handler: openDetails, icon: 'dots-horizontal' }"
/> />
<control-button <control-button
:button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }" :button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }"
@ -131,7 +131,7 @@ export default {
}, },
tracks() { tracks() {
const { options } = this.groupings.find( const { options } = this.groupings.find(
(grouping) => grouping.id === this.uiStore.genre_tracks_sort (grouping) => grouping.id === this.uiStore.genreTracksSort
) )
return this.trackList.group(options) return this.trackList.group(options)
} }
@ -148,7 +148,7 @@ export default {
play() { play() {
webapi.player_play_expression(this.expression, true) webapi.player_play_expression(this.expression, true)
}, },
showDetails() { openDetails() {
this.showDetailsModal = true this.showDetailsModal = true
} }
} }

View File

@ -25,7 +25,7 @@ import TabsMusic from '@/components/TabsMusic.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
const dataObject = { const dataObject = {
load(to) { load() {
return webapi.library_genres('music') return webapi.library_genres('music')
}, },
set(vm, response) { set(vm, response) {
@ -45,7 +45,7 @@ export default {
TabsMusic TabsMusic
}, },
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => { dataObject.load().then((response) => {
next((vm) => dataObject.set(vm, response)) next((vm) => dataObject.set(vm, response))
}) })
}, },

View File

@ -50,7 +50,7 @@ import TabsMusic from '@/components/TabsMusic.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
const dataObject = { const dataObject = {
load(to) { load() {
return Promise.all([ return Promise.all([
webapi.search({ webapi.search({
expression: expression:
@ -82,14 +82,14 @@ export default {
TabsMusic TabsMusic
}, },
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => { dataObject.load().then((response) => {
next((vm) => dataObject.set(vm, response)) next((vm) => dataObject.set(vm, response))
}) })
}, },
data() { data() {
return { return {
albums: [], albums: [],
tracks: { items: [] } tracks: null
} }
} }
} }

View File

@ -24,7 +24,7 @@ import { useSettingsStore } from '@/stores/settings'
import webapi from '@/webapi' import webapi from '@/webapi'
const dataObject = { const dataObject = {
load(to) { load() {
const limit = useSettingsStore().recently_added_limit const limit = useSettingsStore().recently_added_limit
return webapi.search({ return webapi.search({
expression: expression:
@ -45,7 +45,7 @@ export default {
name: 'PageMusicRecentlyAdded', name: 'PageMusicRecentlyAdded',
components: { ContentWithHeading, HeadingTitle, ListAlbums, TabsMusic }, components: { ContentWithHeading, HeadingTitle, ListAlbums, TabsMusic },
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => { dataObject.load().then((response) => {
next((vm) => dataObject.set(vm, response)) next((vm) => dataObject.set(vm, response))
}) })
}, },

View File

@ -23,7 +23,7 @@ import TabsMusic from '@/components/TabsMusic.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
const dataObject = { const dataObject = {
load(to) { load() {
return webapi.search({ return webapi.search({
expression: expression:
'time_played after 8 weeks ago and media_kind is music order by time_played desc', 'time_played after 8 weeks ago and media_kind is music order by time_played desc',
@ -40,7 +40,7 @@ export default {
name: 'PageMusicRecentlyPlayed', name: 'PageMusicRecentlyPlayed',
components: { ContentWithHeading, HeadingTitle, ListTracks, TabsMusic }, components: { ContentWithHeading, HeadingTitle, ListTracks, TabsMusic },
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => { dataObject.load().then((response) => {
next((vm) => dataObject.set(vm, response)) next((vm) => dataObject.set(vm, response))
}) })
}, },

View File

@ -50,7 +50,7 @@ import TabsMusic from '@/components/TabsMusic.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
const dataObject = { const dataObject = {
load(to) { load() {
return webapi.spotify().then(({ data }) => { return webapi.spotify().then(({ data }) => {
const spotifyApi = new SpotifyWebApi() const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(data.webapi_token) spotifyApi.setAccessToken(data.webapi_token)
@ -82,7 +82,7 @@ export default {
TabsMusic TabsMusic
}, },
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => { dataObject.load().then((response) => {
next((vm) => dataObject.set(vm, response)) next((vm) => dataObject.set(vm, response))
}) })
}, },

View File

@ -21,7 +21,7 @@ import TabsMusic from '@/components/TabsMusic.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
const dataObject = { const dataObject = {
load(to) { load() {
return webapi.spotify().then(({ data }) => { return webapi.spotify().then(({ data }) => {
const spotifyApi = new SpotifyWebApi() const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(data.webapi_token) spotifyApi.setAccessToken(data.webapi_token)
@ -45,7 +45,7 @@ export default {
TabsMusic TabsMusic
}, },
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => { dataObject.load().then((response) => {
next((vm) => dataObject.set(vm, response)) next((vm) => dataObject.set(vm, response))
}) })
}, },

View File

@ -9,21 +9,21 @@
:album="track.album" :album="track.album"
class="is-clickable is-big" class="is-clickable is-big"
:class="{ 'is-masked': lyricsStore.active }" :class="{ 'is-masked': lyricsStore.active }"
@click="openDialog(track)" @click="openDetails(track)"
/> />
<lyrics-pane v-if="lyricsStore.active" /> <lyrics-pane v-if="lyricsStore.active" />
<control-slider <control-slider
v-model:value="track_progress" v-model:value="trackProgress"
class="mt-5" class="mt-5"
:disabled="is_live" :disabled="isLive"
:max="track_progress_max" :max="trackProgressMax"
@change="seek" @change="seek"
@mousedown="start_dragging" @mousedown="startDragging"
@mouseup="end_dragging" @mouseup="endDragging"
/> />
<div class="is-flex is-justify-content-space-between"> <div class="is-flex is-justify-content-space-between">
<p class="subtitle is-7" v-text="track_elapsed_time" /> <p class="subtitle is-7" v-text="trackElapsedTime" />
<p class="subtitle is-7" v-text="track_total_time" /> <p class="subtitle is-7" v-text="trackTotalTime" />
</div> </div>
<p class="title is-5" v-text="track.title" /> <p class="title is-5" v-text="track.title" />
<p class="title is-6" v-text="track.artist" /> <p class="title is-6" v-text="track.artist" />
@ -83,8 +83,8 @@ export default {
data() { data() {
return { return {
INTERVAL, INTERVAL,
interval_id: 0, intervalId: 0,
is_dragged: false, isDragged: false,
selectedItem: {}, selectedItem: {},
showDetailsModal: false showDetailsModal: false
} }
@ -109,16 +109,16 @@ export default {
} }
return null return null
}, },
is_live() { isLive() {
return this.track.length_ms === 0 return this.track.length_ms === 0
}, },
track() { track() {
return this.queueStore.current return this.queueStore.current
}, },
track_elapsed_time() { trackElapsedTime() {
return this.$filters.toTimecode(this.track_progress * INTERVAL) return this.$filters.toTimecode(this.trackProgress * INTERVAL)
}, },
track_progress: { trackProgress: {
get() { get() {
return Math.floor(this.playerStore.item_progress_ms / INTERVAL) return Math.floor(this.playerStore.item_progress_ms / INTERVAL)
}, },
@ -126,23 +126,23 @@ export default {
this.playerStore.item_progress_ms = value * INTERVAL this.playerStore.item_progress_ms = value * INTERVAL
} }
}, },
track_progress_max() { trackProgressMax() {
return this.is_live ? 1 : Math.floor(this.track.length_ms / INTERVAL) return this.isLive ? 1 : Math.floor(this.track.length_ms / INTERVAL)
}, },
track_total_time() { trackTotalTime() {
return this.is_live return this.isLive
? this.$t('page.now-playing.live') ? this.$t('page.now-playing.live')
: this.$filters.toTimecode(this.track.length_ms) : this.$filters.toTimecode(this.track.length_ms)
} }
}, },
watch: { watch: {
'playerStore.state'(newState) { 'playerStore.state'(newState) {
if (this.interval_id > 0) { if (this.intervalId > 0) {
window.clearTimeout(this.interval_id) window.clearTimeout(this.intervalId)
this.interval_id = 0 this.intervalId = 0
} }
if (newState === 'play') { if (newState === 'play') {
this.interval_id = window.setInterval(this.tick, INTERVAL) this.intervalId = window.setInterval(this.tick, INTERVAL)
} }
} }
}, },
@ -150,35 +150,35 @@ export default {
webapi.player_status().then(({ data }) => { webapi.player_status().then(({ data }) => {
this.playerStore.$state = data this.playerStore.$state = data
if (this.playerStore.state === 'play') { if (this.playerStore.state === 'play') {
this.interval_id = window.setInterval(this.tick, INTERVAL) this.intervalId = window.setInterval(this.tick, INTERVAL)
} }
}) })
}, },
unmounted() { unmounted() {
if (this.interval_id > 0) { if (this.intervalId > 0) {
window.clearTimeout(this.interval_id) window.clearTimeout(this.intervalId)
this.interval_id = 0 this.intervalId = 0
} }
}, },
methods: { methods: {
end_dragging() { endDragging() {
this.is_dragged = false this.isDragged = false
}, },
openDialog(item) { openDetails(item) {
this.selectedItem = item this.selectedItem = item
this.showDetailsModal = true this.showDetailsModal = true
}, },
seek() { seek() {
if (!this.is_live) { if (!this.isLive) {
webapi.player_seek_to_pos(this.track_progress * INTERVAL) webapi.player_seek_to_pos(this.trackProgress * INTERVAL)
} }
}, },
start_dragging() { startDragging() {
this.is_dragged = true this.isDragged = true
}, },
tick() { tick() {
if (!this.is_dragged) { if (!this.isDragged) {
this.track_progress += 1 this.trackProgress += 1
} }
} }
} }

View File

@ -28,7 +28,7 @@ const dataObject = {
}, },
set(vm, response) { set(vm, response) {
vm.playlist = response[0].data vm.playlist = response[0].data
vm.playlists_list = new GroupedList(response[1].data) vm.playlistList = new GroupedList(response[1].data)
} }
} }
@ -54,7 +54,7 @@ export default {
data() { data() {
return { return {
playlist: {}, playlist: {},
playlists_list: new GroupedList() playlistList: new GroupedList()
} }
}, },
computed: { computed: {
@ -68,7 +68,7 @@ export default {
} }
}, },
playlists() { playlists() {
return this.playlists_list.group({ return this.playlistList.group({
filters: [ filters: [
(playlist) => (playlist) =>
playlist.folder || playlist.folder ||

View File

@ -6,7 +6,7 @@
</template> </template>
<template #heading-right> <template #heading-right>
<control-button <control-button
:button="{ handler: showDetails, icon: 'dots-horizontal' }" :button="{ handler: openDetails, icon: 'dots-horizontal' }"
/> />
<control-button <control-button
:button="{ :button="{
@ -91,7 +91,7 @@ export default {
play() { play() {
webapi.player_play_uri(this.uris, true) webapi.player_play_uri(this.uris, true)
}, },
showDetails() { openDetails() {
this.showDetailsModal = true this.showDetailsModal = true
} }
} }

View File

@ -6,7 +6,7 @@
</template> </template>
<template #heading-right> <template #heading-right>
<control-button <control-button
:button="{ handler: showDetails, icon: 'dots-horizontal' }" :button="{ handler: openDetails, icon: 'dots-horizontal' }"
/> />
<control-button <control-button
:button="{ :button="{
@ -153,7 +153,7 @@ export default {
this.showDetailsModal = false this.showDetailsModal = false
webapi.player_play_uri(this.playlist.uri, true) webapi.player_play_uri(this.playlist.uri, true)
}, },
showDetails() { openDetails() {
this.showDetailsModal = true this.showDetailsModal = true
} }
} }

View File

@ -15,7 +15,7 @@
:button="{ handler: play, icon: 'play', key: 'actions.play' }" :button="{ handler: play, icon: 'play', key: 'actions.play' }"
/> />
<control-button <control-button
:button="{ handler: showDetails, icon: 'dots-horizontal' }" :button="{ handler: openDetails, icon: 'dots-horizontal' }"
/> />
</div> </div>
</template> </template>
@ -25,7 +25,7 @@
:artist="album.artist" :artist="album.artist"
:album="album.name" :album="album.name"
class="is-clickable is-medium" class="is-clickable is-medium"
@click="showDetails" @click="openDetails"
/> />
</template> </template>
<template #content> <template #content>
@ -55,7 +55,7 @@
<br /> <br />
</template> </template>
<template #name> <template #name>
<b v-text="rss_playlist_to_remove.name" /> <b v-text="playlistToRemove.name" />
</template> </template>
</i18n-t> </i18n-t>
</template> </template>
@ -106,7 +106,7 @@ export default {
data() { data() {
return { return {
album: {}, album: {},
rss_playlist_to_remove: {}, playlistToRemove: {},
showDetailsModal: false, showDetailsModal: false,
showRemovePodcastModal: false, showRemovePodcastModal: false,
tracks: new GroupedList() tracks: new GroupedList()
@ -133,7 +133,7 @@ export default {
webapi webapi
.library_track_playlists(this.tracks.items[0].id) .library_track_playlists(this.tracks.items[0].id)
.then(({ data }) => { .then(({ data }) => {
;[this.rss_playlist_to_remove] = data.items.filter( ;[this.playlistToRemove] = data.items.filter(
(pl) => pl.type === 'rss' (pl) => pl.type === 'rss'
) )
this.showRemovePodcastModal = true this.showRemovePodcastModal = true
@ -150,13 +150,11 @@ export default {
}, },
removePodcast() { removePodcast() {
this.showRemovePodcastModal = false this.showRemovePodcastModal = false
webapi webapi.library_playlist_delete(this.playlistToRemove.id).then(() => {
.library_playlist_delete(this.rss_playlist_to_remove.id) this.$router.replace({ name: 'podcasts' })
.then(() => { })
this.$router.replace({ name: 'podcasts' })
})
}, },
showDetails() { openDetails() {
this.showDetailsModal = true this.showDetailsModal = true
} }
} }

View File

@ -53,12 +53,12 @@
<list-item-queue-item <list-item-queue-item
:item="element" :item="element"
:position="index" :position="index"
:current_position="current_position" :current-position="currentPosition"
:hide-read-items="uiStore.hideReadItems" :hide-read-items="uiStore.hideReadItems"
:editing="editing" :editing="editing"
> >
<template #actions> <template #actions>
<a v-if="!editing" @click.prevent.stop="openDialog(element)"> <a v-if="!editing" @click.prevent.stop="openDetails(element)">
<mdicon <mdicon
class="icon has-text-grey" class="icon has-text-grey"
name="dots-vertical" name="dots-vertical"
@ -139,7 +139,7 @@ export default {
} }
}, },
computed: { computed: {
current_position() { currentPosition() {
return this.queueStore.current?.position ?? -1 return this.queueStore.current?.position ?? -1
}, },
heading() { heading() {
@ -166,7 +166,7 @@ export default {
}, },
moveItem(event) { moveItem(event) {
const oldPosition = const oldPosition =
event.oldIndex + (this.uiStore.hideReadItems && this.current_position) event.oldIndex + (this.uiStore.hideReadItems && this.currentPosition)
const item = this.items[oldPosition] const item = this.items[oldPosition]
const newPosition = item.position + (event.newIndex - event.oldIndex) const newPosition = item.position + (event.newIndex - event.oldIndex)
if (newPosition !== oldPosition) { if (newPosition !== oldPosition) {
@ -176,7 +176,7 @@ export default {
openAddStreamDialog() { openAddStreamDialog() {
this.showAddStreamDialog = true this.showAddStreamDialog = true
}, },
openDialog(item) { openDetails(item) {
this.selectedItem = item this.selectedItem = item
this.showDetailsModal = true this.showDetailsModal = true
}, },

View File

@ -33,7 +33,7 @@
/> />
<div class="content"> <div class="content">
<control-setting-switch <control-setting-switch
v-if="spotify.spotify_logged_in" v-if="servicesStore.spotify_logged_in"
category="artwork" category="artwork"
name="use_artwork_source_spotify" name="use_artwork_source_spotify"
> >
@ -95,11 +95,6 @@ export default {
}, },
setup() { setup() {
return { servicesStore: useServicesStore() } return { servicesStore: useServicesStore() }
},
computed: {
spotify() {
return this.servicesStore.spotify
}
} }
} }
</script> </script>

View File

@ -47,8 +47,8 @@
<div class="control"> <div class="control">
<a <a
class="button is-danger" class="button is-danger"
@click="logout_spotify" @click="logoutSpotify"
v-text="$t('page.settings.services.logout')" v-text="$t('actions.logout')"
/> />
</div> </div>
</div> </div>
@ -75,11 +75,11 @@
<a <a
class="button is-danger" class="button is-danger"
@click="logoutLastfm" @click="logoutLastfm"
v-text="$t('page.settings.services.logout')" v-text="$t('actions.logout')"
/> />
</div> </div>
<div v-if="!lastfm.scrobbling_enabled"> <div v-if="!lastfm.scrobbling_enabled">
<form @submit.prevent="login_lastfm"> <form @submit.prevent="loginLastfm">
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control"> <div class="control">
<input <input
@ -106,7 +106,7 @@
<button <button
class="button" class="button"
type="submit" type="submit"
v-text="$t('page.settings.services.login')" v-text="$t('actions.login')"
/> />
</div> </div>
</div> </div>
@ -172,7 +172,7 @@ export default {
} }
}, },
methods: { methods: {
login_lastfm() { loginLastfm() {
webapi.lastfm_login(this.lastfm_login).then((response) => { webapi.lastfm_login(this.lastfm_login).then((response) => {
this.lastfm_login.user = '' this.lastfm_login.user = ''
this.lastfm_login.password = '' this.lastfm_login.password = ''
@ -190,7 +190,7 @@ export default {
logoutLastfm() { logoutLastfm() {
webapi.lastfm_logout() webapi.lastfm_logout()
}, },
logout_spotify() { logoutSpotify() {
webapi.spotify_logout() webapi.spotify_logout()
} }
} }

View File

@ -9,7 +9,7 @@
</template> </template>
<template #content> <template #content>
<div v-if="pairing.active"> <div v-if="pairing.active">
<form @submit.prevent="kickoff_pairing"> <form @submit.prevent="kickoffPairing">
<label class="label has-text-weight-normal content"> <label class="label has-text-weight-normal content">
<span v-text="$t('page.settings.devices.pairing-request')" /> <span v-text="$t('page.settings.devices.pairing-request')" />
<b v-text="pairing.remote" /> <b v-text="pairing.remote" />
@ -17,7 +17,7 @@
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control"> <div class="control">
<input <input
v-model="pairing_req.pin" v-model="pairingRequest.pin"
class="input" class="input"
inputmode="numeric" inputmode="numeric"
pattern="[\d]{4}" pattern="[\d]{4}"
@ -62,12 +62,12 @@
<form <form
v-if="output.needs_auth_key" v-if="output.needs_auth_key"
class="mb-5" class="mb-5"
@submit.prevent="kickoff_verification(output.id)" @submit.prevent="kickoffVerification(output.id)"
> >
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control"> <div class="control">
<input <input
v-model="verification_req.pin" v-model="verificationRequest.pin"
class="input" class="input"
inputmode="numeric" inputmode="numeric"
pattern="[\d]{4}" pattern="[\d]{4}"
@ -106,8 +106,8 @@ export default {
}, },
data() { data() {
return { return {
pairing_req: { pin: '' }, pairingRequest: { pin: '' },
verification_req: { pin: '' } verificationRequest: { pin: '' }
} }
}, },
computed: { computed: {
@ -119,11 +119,11 @@ export default {
} }
}, },
methods: { methods: {
kickoff_pairing() { kickoffPairing() {
webapi.pairing_kickoff(this.pairing_req) webapi.pairing_kickoff(this.pairingRequest)
}, },
kickoff_verification(identifier) { kickoffVerification(identifier) {
webapi.output_update(identifier, this.verification_req) webapi.output_update(identifier, this.verificationRequest)
}, },
toggleOutput(identifier) { toggleOutput(identifier) {
webapi.output_toggle(identifier) webapi.output_toggle(identifier)

View File

@ -8,7 +8,10 @@
/> />
</template> </template>
<template #content> <template #content>
<control-dropdown v-model:value="locale" :options="locales" /> <control-dropdown
v-model:value="locale"
:options="settingsStore.locales"
/>
</template> </template>
</content-with-heading> </content-with-heading>
<content-with-heading> <content-with-heading>
@ -198,14 +201,6 @@ export default {
set(locale) { set(locale) {
this.$i18n.locale = locale this.$i18n.locale = locale
} }
},
locales: {
get() {
return this.$i18n.availableLocales.map((item) => ({
id: item,
name: this.$t(`language.${item}`)
}))
}
} }
} }
} }

View File

@ -33,5 +33,8 @@ export const useNotificationsStore = defineStore('NotificationsStore', {
} }
} }
}, },
getters: {
isEmpty: (state) => state.list.length <= 0
},
state: () => ({ list: [], nextId: 1 }) state: () => ({ list: [], nextId: 1 })
}) })

View File

@ -1,4 +1,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import i18n from '@/i18n'
const { t, availableLocales } = i18n.global
export const useSettingsStore = defineStore('SettingsStore', { export const useSettingsStore = defineStore('SettingsStore', {
actions: { actions: {
@ -25,6 +28,12 @@ export const useSettingsStore = defineStore('SettingsStore', {
} }
}, },
getters: { getters: {
locales(state) {
return availableLocales.map((item) => ({
id: item,
name: t(`language.${item}`)
}))
},
recently_added_limit: (state) => recently_added_limit: (state) =>
state.setting('webinterface', 'recently_added_limit')?.value ?? 100, state.setting('webinterface', 'recently_added_limit')?.value ?? 100,
show_composer_for_genre: (state) => show_composer_for_genre: (state) =>

View File

@ -19,12 +19,12 @@ export const useUIStore = defineStore('UIStore', {
} }
}, },
state: () => ({ state: () => ({
albums_sort: 1, albumsSort: 1,
artist_albums_sort: 1, artistAlbumsSort: 1,
artist_tracks_sort: 1, artistTracksSort: 1,
artists_sort: 1, artistsSort: 1,
composer_tracks_sort: 1, composerTracksSort: 1,
genre_tracks_sort: 1, genreTracksSort: 1,
hideReadItems: false, hideReadItems: false,
hideSingles: false, hideSingles: false,
hideSpotify: false, hideSpotify: false,

View File

@ -34,11 +34,12 @@
</div> </div>
</nav> </nav>
<slot name="content" /> <slot name="content" />
<nav v-if="$slots.footer" class="level mt-4"> <div
<div class="level-item"> v-if="$slots.footer"
<slot name="footer" /> class="is-flex is-justify-content-center mt-4"
</div> >
</nav> <slot name="footer" />
</div>
</div> </div>
</div> </div>
</div> </div>