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

View File

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

View File

@ -1,75 +1,48 @@
<template>
<template v-for="item in items" :key="item.itemId">
<div v-if="!item.isItem" class="py-5">
<span
:id="`index_${item.index}`"
class="tag is-small has-text-weight-bold"
v-text="item.index"
/>
</div>
<div
v-else
class="media is-align-items-center is-clickable mb-0"
@click="open(item.item)"
>
<control-image
v-if="settingsStore.show_cover_artwork_in_album_lists"
:url="item.item.artwork_url"
:artist="item.item.artist"
:album="item.item.name"
class="media-left is-small"
/>
<div class="media-content">
<div class="is-size-6 has-text-weight-bold" v-text="item.item.name" />
<div
class="is-size-7 has-text-grey has-text-weight-bold"
v-text="item.item.artist"
/>
<div
v-if="item.item.date_released && item.item.media_kind === 'music'"
class="is-size-7 has-text-grey"
v-text="$filters.toDate(item.item.date_released)"
/>
</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-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>
<list-item
v-for="item in items"
:key="item.itemId"
:is-item="item.isItem"
:image="url(item)"
:index="item.index"
:lines="[
item.item.name,
item.item.artist,
$filters.toDate(item.item.date_released)
]"
@open="open(item.item)"
@open-details="openDetails(item.item)"
/>
<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="playlistToRemove.name" />
</template>
</i18n-t>
</template>
</modal-dialog>
</template>
<script>
import ControlImage from '@/components/ControlImage.vue'
import ListItem from '@/components/ListItem.vue'
import ModalDialog from '@/components/ModalDialog.vue'
import ModalDialogAlbum from '@/components/ModalDialogAlbum.vue'
import { useSettingsStore } from '@/stores/settings'
@ -77,7 +50,7 @@ import webapi from '@/webapi'
export default {
name: 'ListAlbums',
components: { ControlImage, ModalDialog, ModalDialogAlbum },
components: { ListItem, ModalDialog, ModalDialogAlbum },
props: {
items: { required: true, type: Object },
media_kind: { default: '', type: String }
@ -88,7 +61,7 @@ export default {
},
data() {
return {
rss_playlist_to_remove: {},
playlistToRemove: {},
selectedItem: {},
showDetailsModal: false,
showRemovePodcastModal: false
@ -119,7 +92,7 @@ export default {
this.$router.push({ name: 'music-album', params: { id: item.id } })
}
},
openDialog(item) {
openDetails(item) {
this.selectedItem = item
this.showDetailsModal = true
},
@ -128,7 +101,7 @@ export default {
.library_album_tracks(this.selectedItem.id, { limit: 1 })
.then(({ data: album }) => {
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'
)
this.showRemovePodcastModal = true
@ -141,11 +114,15 @@ export default {
},
removePodcast() {
this.showRemovePodcastModal = false
webapi
.library_playlist_delete(this.rss_playlist_to_remove.id)
.then(() => {
this.$emit('podcast-deleted')
})
webapi.library_playlist_delete(this.playlistToRemove.id).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 v-for="item in items" :key="item.id">
<div
class="media is-align-items-center is-clickable mb-0"
@click="open(item)"
>
<control-image
v-if="settingsStore.show_cover_artwork_in_album_lists"
:url="item.images?.[0]?.url ?? ''"
:artist="item.artist"
:album="item.name"
class="media-left is-small"
/>
<div class="media-content">
<div class="is-size-6 has-text-weight-bold" v-text="item.name" />
<div
class="is-size-7 has-text-weight-bold has-text-grey"
v-text="item.artists[0]?.name"
/>
<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>
<list-item
v-for="item in items"
:key="item.id"
:is-item="item.isItem"
:image="url(item)"
:index="item.index"
:lines="[
item.name,
item.artists[0]?.name,
$filters.toDate(item.release_date)
]"
@open="open(item)"
@open-details="openDetails(item)"
/>
<modal-dialog-album-spotify
:item="selectedItem"
:show="showDetailsModal"
@close="showDetailsModal = false"
/>
</template>
<script>
import ControlImage from '@/components/ControlImage.vue'
import ListItem from '@/components/ListItem.vue'
import ModalDialogAlbumSpotify from '@/components/ModalDialogAlbumSpotify.vue'
import { useSettingsStore } from '@/stores/settings'
export default {
name: 'ListAlbumsSpotify',
components: { ControlImage, ModalDialogAlbumSpotify },
components: { ListItem, ModalDialogAlbumSpotify },
props: { items: { required: true, type: Object } },
setup() {
return { settingsStore: useSettingsStore() }
@ -60,9 +42,15 @@ export default {
params: { id: item.id }
})
},
openDialog(item) {
openDetails(item) {
this.selectedItem = item
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 v-for="item in items" :key="item.itemId">
<div v-if="!item.isItem" class="py-5">
<span
:id="`index_${item.index}`"
class="tag is-small has-text-weight-bold"
v-text="item.index"
/>
</div>
<div
v-else
class="media is-align-items-center is-clickable mb-0"
@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-artist
:item="selectedItem"
:show="showDetailsModal"
@close="showDetailsModal = false"
/>
</teleport>
<list-item
v-for="item in items"
:key="item.itemId"
:is-item="item.isItem"
:index="item.index"
:lines="[item.item.name]"
@open="open(item.item)"
@open-details="openDetails(item.item)"
/>
<modal-dialog-artist
:item="selectedItem"
:show="showDetailsModal"
@close="showDetailsModal = false"
/>
</template>
<script>
import ListItem from '@/components/ListItem.vue'
import ModalDialogArtist from '@/components/ModalDialogArtist.vue'
export default {
name: 'ListArtists',
components: { ModalDialogArtist },
components: { ListItem, ModalDialogArtist },
props: { items: { required: true, type: Object } },
data() {
return { selectedItem: {}, showDetailsModal: false }
},
methods: {
open(item) {
this.selectedItem = item
@ -50,7 +33,7 @@ export default {
item.media_kind === 'audiobook' ? 'audiobooks-artist' : 'music-artist'
this.$router.push({ name: route, params: { id: item.id } })
},
openDialog(item) {
openDetails(item) {
this.selectedItem = item
this.showDetailsModal = true
}

View File

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

View File

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

View File

@ -27,22 +27,20 @@
>
<mdicon class="media-left icon" name="folder" />
<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 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" />
</a>
</div>
</div>
</template>
<teleport to="#app">
<modal-dialog-directory
:item="selectedItem"
:show="showDetailsModal"
@close="showDetailsModal = false"
/>
</teleport>
<modal-dialog-directory
:item="selectedItem"
:show="showDetailsModal"
@close="showDetailsModal = false"
/>
</template>
<script>
@ -80,7 +78,7 @@ export default {
}
this.$router.push(route)
},
openDialog(item) {
openDetails(item) {
this.selectedItem = item.path
this.showDetailsModal = true
},

View File

@ -1,45 +1,28 @@
<template>
<template v-for="item in items" :key="item.itemId">
<div v-if="!item.isItem" class="py-5">
<div class="media-content">
<span
:id="`index_${item.index}`"
class="tag is-small has-text-weight-bold"
v-text="item.index"
/>
</div>
</div>
<div
v-else
class="media is-align-items-center is-clickable mb-0"
@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-genre
:item="selectedItem"
:media_kind="media_kind"
:show="showDetailsModal"
@close="showDetailsModal = false"
/>
</teleport>
<list-item
v-for="item in items"
:key="item.itemId"
:is-item="item.isItem"
:index="item.index"
:lines="[item.item.name]"
@open="open(item.item)"
@open-details="openDetails(item.item)"
/>
<modal-dialog-genre
:item="selectedItem"
:media_kind="media_kind"
:show="showDetailsModal"
@close="showDetailsModal = false"
/>
</template>
<script>
import ListItem from '@/components/ListItem.vue'
import ModalDialogGenre from '@/components/ModalDialogGenre.vue'
export default {
name: 'ListGenres',
components: { ModalDialogGenre },
components: { ListItem, ModalDialogGenre },
props: {
items: { required: true, type: Object },
media_kind: { required: true, type: String }
@ -55,7 +38,7 @@ export default {
query: { media_kind: this.media_kind }
})
},
openDialog(item) {
openDetails(item) {
this.selectedItem = item
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 {
name: 'ListItemQueueItem',
props: {
current_position: { required: true, type: Number },
currentPosition: { required: true, type: Number },
editing: Boolean,
item: { required: true, type: Object },
position: { required: true, type: Number },
@ -66,7 +66,7 @@ export default {
return this.item.id === this.playerStore.item_id
},
isNext() {
return this.current_position < 0 || this.position >= this.current_position
return this.currentPosition < 0 || this.position >= this.currentPosition
}
},
methods: {

View File

@ -1,41 +1,32 @@
<template>
<template v-for="item in items" :key="item.itemId">
<div
class="media is-align-items-center is-clickable mb-0"
@click="open(item.item)"
>
<mdicon class="media-left icon" :name="icon(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-playlist
:item="selectedItem"
:show="showDetailsModal"
@close="showDetailsModal = false"
/>
</teleport>
<list-item
v-for="item in items"
:key="item.itemId"
:icon="icon(item.item)"
:is-item="item.isItem"
:index="item.index"
:lines="[item.item.name]"
@open="open(item.item)"
@open-details="openDetails(item.item)"
/>
<modal-dialog-playlist
:item="selectedItem"
:show="showDetailsModal"
@close="showDetailsModal = false"
/>
</template>
<script>
import ListItem from '@/components/ListItem.vue'
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist.vue'
export default {
name: 'ListPlaylists',
components: { ModalDialogPlaylist },
components: { ListItem, ModalDialogPlaylist },
props: { items: { required: true, type: Object } },
data() {
return { selectedItem: {}, showDetailsModal: false }
},
methods: {
icon(item) {
if (item.type === 'folder') {
@ -52,7 +43,7 @@ export default {
this.$router.push({ name: 'playlist', params: { id: item.id } })
}
},
openDialog(item) {
openDetails(item) {
this.selectedItem = item
this.showDetailsModal = true
}

View File

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

View File

@ -1,73 +1,36 @@
<template>
<template v-for="item in items" :key="item.itemId">
<div v-if="!item.isItem" class="py-5">
<span
:id="`index_${item.index}`"
class="tag is-small has-text-weight-bold"
v-text="item.index"
/>
</div>
<div
v-else
class="media is-align-items-center is-clickable mb-0"
:class="{ 'with-progress': showProgress }"
@click="play(item.item)"
>
<mdicon
v-if="showIcon"
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>
<list-item
v-for="item in items"
:key="item.itemId"
:icon="icon"
:is-item="item.isItem"
:index="item.index"
:lines="[item.item.title, item.item.artist, item.item.album]"
:progress="progress(item.item)"
@open="open(item.item)"
@open-details="openDetails(item.item)"
/>
<modal-dialog-track
:item="selectedItem"
:show="showDetailsModal"
@close="showDetailsModal = false"
@play-count-changed="$emit('play-count-changed')"
/>
</template>
<script>
import ListItem from '@/components/ListItem.vue'
import ModalDialogTrack from '@/components/ModalDialogTrack.vue'
import webapi from '@/webapi'
export default {
name: 'ListTracks',
components: { ModalDialogTrack },
components: { ListItem, ModalDialogTrack },
props: {
expression: { default: '', type: String },
items: { required: true, type: Object },
showIcon: Boolean,
showProgress: Boolean,
items: { default: null, type: Object },
icon: { default: null, type: String },
showProgress: { default: false, type: Boolean },
uris: { default: '', type: String }
},
emits: ['play-count-changed'],
@ -75,11 +38,7 @@ export default {
return { selectedItem: {}, showDetailsModal: false }
},
methods: {
openDialog(item) {
this.selectedItem = item
this.showDetailsModal = true
},
play(item) {
open(item) {
if (this.uris) {
webapi.player_play_uri(this.uris, false, this.items.items.indexOf(item))
} else if (this.expression) {
@ -91,17 +50,20 @@ export default {
} else {
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>
<style scoped>
.progress {
height: 0.25rem;
}
.media.with-progress {
margin-top: 0.375rem;
}
</style>

View File

@ -36,19 +36,17 @@
</div>
</div>
<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" />
</a>
</div>
</div>
</template>
<teleport to="#app">
<modal-dialog-track-spotify
:item="selectedItem"
:show="showDetailsModal"
@close="showDetailsModal = false"
/>
</teleport>
<modal-dialog-track-spotify
:item="selectedItem"
:show="showDetailsModal"
@close="showDetailsModal = false"
/>
</template>
<script>
@ -66,7 +64,7 @@ export default {
return { selectedItem: {}, showDetailsModal: false }
},
methods: {
openDialog(item) {
openDetails(item) {
this.selectedItem = item
this.showDetailsModal = true
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,8 @@
"save": "Enregistrer",
"send": "Envoyer",
"show-more": "Afficher plus",
"shuffle": "Lecture aléatoire"
"shuffle": "Lecture aléatoire",
"update": "Actualiser"
},
"count": {
"albums": "{count} album|{count} album|{count} albums",
@ -116,6 +117,7 @@
"albums": "Albums",
"artists": "Artistes",
"audiobooks": "Livres audio",
"composers": "Compositeurs",
"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": "Flux HTTP",
@ -216,8 +218,7 @@
},
"podcasts": {
"new-episodes": "Nouveaux épisodes",
"title": "Podcasts",
"update": "Actualiser"
"title": "Podcasts"
},
"queue": {
"title": "File dattente"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@
</template>
<template #heading-right>
<control-button
:button="{ handler: showDetails, icon: 'dots-horizontal' }"
:button="{ handler: openDetails, icon: 'dots-horizontal' }"
/>
<control-button
:button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }"
@ -106,7 +106,7 @@ export default {
heading() {
return {
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)
})
},
openDetails() {
this.showDetailsModal = true
},
play() {
this.showDetailsModal = false
webapi.player_play_uri(this.artist.uri, true)
},
showDetails() {
this.showDetailsModal = true
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@
v-text="$t('options.sort.title')"
/>
<control-dropdown
v-model:value="uiStore.composer_tracks_sort"
v-model:value="uiStore.composerTracksSort"
:options="groupings"
/>
</div>
@ -21,7 +21,7 @@
</template>
<template #heading-right>
<control-button
:button="{ handler: showDetails, icon: 'dots-horizontal' }"
:button="{ handler: openDetails, icon: 'dots-horizontal' }"
/>
<control-button
:button="{ handler: play, icon: 'shuffle', key: 'actions.shuffle' }"
@ -129,9 +129,9 @@ export default {
},
tracks() {
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: {
@ -141,11 +141,11 @@ export default {
params: { name: this.composer.name }
})
},
openDetails() {
this.showDetailsModal = true
},
play() {
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'
const dataObject = {
load(to) {
load() {
return webapi.library_composers('music')
},
set(vm, response) {
@ -45,7 +45,7 @@ export default {
TabsMusic
},
beforeRouteEnter(to, from, next) {
dataObject.load(to).then((response) => {
dataObject.load().then((response) => {
next((vm) => dataObject.set(vm, response))
})
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,10 @@
/>
</template>
<template #content>
<control-dropdown v-model:value="locale" :options="locales" />
<control-dropdown
v-model:value="locale"
:options="settingsStore.locales"
/>
</template>
</content-with-heading>
<content-with-heading>
@ -198,14 +201,6 @@ export default {
set(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 })
})

View File

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

View File

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

View File

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