[web] Improve the initialisation of stores

This commit is contained in:
Alain Nussbaumer 2025-05-17 00:24:58 +02:00
parent 6b5f4ff4d7
commit f91189d93b
11 changed files with 289 additions and 1094 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,7 @@
"pinia": "^3.0.2",
"reconnectingwebsocket": "^1.0.0",
"spotify-web-api-js": "^1.5.2",
"vue": "^3.5.13",
"vue": "^3.5.14",
"vue-i18n": "^11.1.3",
"vue-router": "^4.5.1",
"vue3-click-away": "^1.2.4",
@ -32,11 +32,11 @@
"devDependencies": {
"@intlify/unplugin-vue-i18n": "^6.0.8",
"@vitejs/plugin-vue": "^5.2.4",
"eslint": "^9.26.0",
"eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-vue": "^10.1.0",
"prettier": "^3.5.3",
"sass": "^1.88.0",
"sass": "^1.89.0",
"vite": "^6.3.5"
}
}

View File

@ -29,7 +29,6 @@ import NavbarBottom from '@/components/NavbarBottom.vue'
import NavbarTop from '@/components/NavbarTop.vue'
import ReconnectingWebSocket from 'reconnectingwebsocket'
import configuration from '@/api/configuration'
import services from '@/api/services'
import { useConfigurationStore } from '@/stores/configuration'
import { useLibraryStore } from '@/stores/library'
import { useNotificationsStore } from '@/stores/notifications'
@ -66,7 +65,7 @@ export default {
},
data() {
return {
timerId: 0
updateThrottled: false
}
},
watch: {
@ -114,79 +113,33 @@ export default {
},
openWebsocket() {
const socket = this.createWebsocket()
const events = [
'database',
'lastfm',
'options',
'outputs',
'pairing',
'player',
'queue',
'settings',
'spotify',
'update',
'volume'
]
socket.onopen = () => {
socket.send(
JSON.stringify({
notify: [
'update',
'database',
'player',
'options',
'outputs',
'volume',
'queue',
'spotify',
'lastfm',
'pairing'
]
})
)
this.updateOutputs()
this.updatePlayer()
this.updateLibrary()
this.updateSettings()
this.updateQueue()
this.updateSpotify()
this.updateLastfm()
this.updateRemotes()
socket.send(JSON.stringify({ notify: events }))
this.handleEvents(events)
}
let updateThrottled = false
const updateInfo = () => {
if (updateThrottled) {
return
}
this.updateOutputs()
this.updatePlayer()
this.updateLibrary()
this.updateSettings()
this.updateQueue()
this.updateSpotify()
this.updateLastfm()
this.updateRemotes()
updateThrottled = true
setTimeout(() => {
updateThrottled = false
}, 500)
}
window.addEventListener('focus', updateInfo)
window.addEventListener('focus', () => {
this.handleEvents(events)
})
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
updateInfo()
this.handleEvents(events)
}
})
socket.onmessage = (response) => {
const data = JSON.parse(response.data)
const notify = new Set(data.notify || [])
const handlers = [
{ events: ['update', 'database'], handler: this.updateLibrary },
{
events: ['player', 'options', 'volume'],
handler: this.updatePlayer
},
{ events: ['outputs', 'volume'], handler: this.updateOutputs },
{ events: ['queue'], handler: this.updateQueue },
{ events: ['spotify'], handler: this.updateSpotify },
{ events: ['lastfm'], handler: this.updateLastfm },
{ events: ['pairing'], handler: this.updateRemotes }
]
handlers.forEach(({ handler, events }) => {
if (events.some((key) => notify.has(key))) {
handler.call(this)
}
})
this.handleEvents(JSON.parse(response.data).notify)
}
},
createWebsocket() {
@ -203,50 +156,46 @@ export default {
reconnectInterval: 1000
})
},
handleEvents(notifications = []) {
if (this.updateThrottled) {
return
}
const handlers = [
{
events: ['update', 'database'],
handler: this.libraryStore.initialise
},
{
events: ['player', 'options', 'volume'],
handler: this.playerStore.initialise
},
{
events: ['outputs', 'volume'],
handler: this.outputsStore.initialise
},
{ events: ['queue'], handler: this.queueStore.initialise },
{ events: ['settings'], handler: this.settingsStore.initialise },
{ events: ['spotify'], handler: this.servicesStore.initialiseSpotify },
{ events: ['lastfm'], handler: this.servicesStore.initialiseLastfm },
{ events: ['pairing'], handler: this.remotesStore.initialise }
]
const notificationSet = new Set(notifications)
handlers.forEach(({ handler, events }) => {
if (events.some((key) => notificationSet.has(key))) {
handler.call(this)
}
})
this.updateThrottled = true
setTimeout(() => {
this.updateThrottled = false
}, 500)
},
updateClipping() {
if (this.uiStore.showBurgerMenu || this.uiStore.showPlayerMenu) {
document.querySelector('html').classList.add('is-clipped')
} else {
document.querySelector('html').classList.remove('is-clipped')
}
},
updateLastfm() {
services.lastfm().then((data) => {
this.servicesStore.lastfm = data
})
},
updateLibrary() {
this.libraryStore.initialise()
},
updateOutputs() {
this.outputsStore.initialise()
},
updateRemotes() {
this.remotesStore.initialise()
},
updatePlayer() {
this.playerStore.initialise()
},
updateQueue() {
this.queueStore.initialise()
},
updateSettings() {
this.settingsStore.initialise()
},
updateSpotify() {
services.spotify().then((data) => {
this.servicesStore.spotify = data
if (this.timerId > 0) {
window.clearTimeout(this.timerId)
this.timerId = 0
}
if (data.webapi_token_expires_in > 0 && data.webapi_token) {
this.timerId = window.setTimeout(
this.updateSpotify,
1000 * data.webapi_token_expires_in
)
}
})
}
}
}

View File

@ -1,5 +1,5 @@
<template>
<button :class="{ 'is-dark': playerStore.lyrics }" @click="toggle">
<button :class="{ 'is-dark': playerStore.showLyrics }" @click="toggle">
<mdicon
class="icon"
:name="icon"
@ -21,14 +21,14 @@ export default {
},
computed: {
icon() {
return this.playerStore.lyrics
return this.playerStore.showLyrics
? 'script-text-play'
: 'script-text-outline'
}
},
methods: {
toggle() {
this.playerStore.lyrics = !this.playerStore.lyrics
this.playerStore.showLyrics = !this.playerStore.showLyrics
}
}
}

View File

@ -1,8 +1,12 @@
<template>
<div class="lyrics is-overlay">
<div v-for="(verse, index) in visibleLyrics" :key="index">
<div
class="lyrics is-overlay"
@wheel.prevent="onScroll"
@touchmove.prevent="onScroll"
>
<div v-for="(verse, index) in visibleVerses" :key="index">
<div v-if="verse">
<div v-if="index === 3" class="title is-5 my-5 lh-2">
<div v-if="index === MIDDLE_POSITION" class="title is-5 my-5 lh-2">
<span
v-for="(word, wordIndex) in verse.words"
:key="wordIndex"
@ -19,36 +23,40 @@
<script>
import { usePlayerStore } from '@/stores/player'
import { useQueueStore } from '@/stores/queue'
const VISIBLE_VERSES = 7
const MIDDLE_POSITION = Math.floor(VISIBLE_VERSES / 2)
export default {
name: 'PaneLyrics',
setup() {
return { playerStore: usePlayerStore(), queueStore: useQueueStore() }
const VISIBLE_VERSES = 7
const MIDDLE_POSITION = Math.floor(VISIBLE_VERSES / 2)
const SCROLL_THRESHOLD = 10
return {
MIDDLE_POSITION,
playerStore: usePlayerStore(),
SCROLL_THRESHOLD,
VISIBLE_VERSES
}
},
data() {
return {
lastIndex: -1,
time: 0,
timerId: null,
lastProgress: 0,
lastUpdateTime: 0,
lyrics: []
lyrics: { synchronised: false, verses: [] },
scrollIndex: 0,
scrollDelta: 0,
time: 0,
timerId: null
}
},
computed: {
verseIndex() {
const currentTime = this.time
const { lyrics } = this
const { verses } = this.lyrics
let start = 0
let end = lyrics.length - 1
let end = verses.length - 1
while (start <= end) {
const mid = Math.floor((start + end) / 2)
const midTime = lyrics[mid].time
const nextTime = lyrics[mid + 1]?.time
const midTime = verses[mid].time
const nextTime = verses[mid + 1]?.time
if (midTime <= currentTime && (!nextTime || nextTime > currentTime)) {
return mid
} else if (midTime < currentTime) {
@ -59,13 +67,13 @@ export default {
}
return -1
},
visibleLyrics() {
const current = this.verseIndex
const total = this.lyrics.length
return Array.from({ length: VISIBLE_VERSES }, (_, i) => {
const index = current - MIDDLE_POSITION + i
return index >= 0 && index < total ? this.lyrics[index] : null
})
visibleVerses() {
const { verses, synchronised } = this.lyrics
const index = synchronised ? this.verseIndex : this.scrollIndex
return Array.from(
{ length: this.VISIBLE_VERSES },
(_, i) => verses[index - this.MIDDLE_POSITION + i] ?? null
)
}
},
watch: {
@ -78,7 +86,6 @@ export default {
}
},
'playerStore.item_progress_ms'(progress) {
this.lastProgress = progress
this.lastUpdateTime = Date.now()
if (!this.playerStore.isPlaying) {
this.time = progress
@ -86,13 +93,10 @@ export default {
},
'playerStore.lyricsContent'() {
this.lyrics = this.parseLyrics()
},
verseIndex(index) {
this.lastIndex = index
}
},
mounted() {
this.lastProgress = this.playerStore.item_progress_ms
this.playerStore.initialise()
this.lastUpdateTime = Date.now()
this.lyrics = this.parseLyrics()
this.updateTime()
@ -101,11 +105,24 @@ export default {
this.stopTimer()
},
methods: {
onScroll(event) {
if (this.verseIndex >= 0) {
return
}
this.scrollDelta += event.deltaY
if (Math.abs(this.scrollDelta) >= this.SCROLL_THRESHOLD) {
const newIndex = this.scrollIndex + Math.sign(this.scrollDelta)
if (newIndex >= -1 && newIndex < this.lyrics.verses.length) {
this.scrollIndex = newIndex
}
this.scrollDelta = 0
}
},
isWordHighlighted(word) {
return this.time >= word.start && this.time < word.end
},
parseLyrics() {
const verses = []
const lyrics = { synchronised: false, verses: [] }
const regex =
/(?:\[(?<minutes>\d+):(?<seconds>\d+)(?:\.(?<hundredths>\d+))?\])?\s*(?<text>\S.*\S)?\s*/u
this.playerStore.lyricsContent.split('\n').forEach((line) => {
@ -117,25 +134,25 @@ export default {
const time =
(Number(minutes) * 60 + Number(`${seconds}.${hundredths ?? 0}`)) *
1000
verses.push({ text: verse, time })
lyrics.synchronised = !isNaN(time)
lyrics.verses.push({ text: verse, time })
}
}
})
verses.forEach((verse, index, lyrics) => {
const nextTime = lyrics[index + 1]?.time ?? verse.time + 3000
lyrics.verses.forEach((verse, index, verses) => {
const nextTime = verses[index + 1]?.time ?? verse.time + 3000
const totalDuration = nextTime - verse.time
const words = verse.text.match(/\S+\s*/gu) || []
const totalLength = words.reduce((sum, word) => sum + word.length, 0)
let currentTime = verse.time
verse.words = words.map((text) => {
const duration = totalDuration * (text.length / totalLength)
const start = currentTime
const end = start + duration
const end = start + totalDuration * (text.length / totalLength)
currentTime = end
return { text, start, end }
})
})
return verses
return lyrics
},
startTimer() {
if (this.timerId) {
@ -150,13 +167,15 @@ export default {
}
},
tick() {
this.time = this.lastProgress + Date.now() - this.lastUpdateTime
this.time =
this.playerStore.item_progress_ms + Date.now() - this.lastUpdateTime
},
updateTime() {
this.lastUpdateTime = Date.now()
if (this.playerStore.isPlaying) {
this.startTimer()
} else {
this.time = this.lastProgress
this.time = this.playerStore.item_progress_ms
}
}
}

View File

@ -8,10 +8,10 @@
:url="track.artwork_url"
:caption="track.album"
class="is-clickable is-big"
:class="{ 'is-masked': playerStore.lyrics }"
:class="{ 'is-masked': playerStore.showLyrics }"
@click="openDetails(track)"
/>
<pane-lyrics v-if="playerStore.lyrics" />
<pane-lyrics v-if="playerStore.showLyrics" />
</div>
<control-slider
v-model:value="trackProgress"

View File

@ -123,7 +123,7 @@
class="help"
scope="global"
>
<br />
<slot><br /></slot>
</i18n-t>
</template>
</control-setting-text-field>

View File

@ -27,9 +27,9 @@ export const usePlayerStore = defineStore('PlayerStore', {
item_id: 0,
item_length_ms: 0,
item_progress_ms: 0,
lyrics: false,
lyricsContent: '',
repeat: 'off',
showLyrics: false,
shuffle: false,
state: 'stop',
volume: 0

View File

@ -1,6 +1,27 @@
import { defineStore } from 'pinia'
import services from '@/api/services'
export const useServicesStore = defineStore('ServicesStore', {
actions: {
async initialiseLastfm() {
this.lastfm = await services.lastfm()
},
initialiseSpotify() {
services.spotify().then((data) => {
this.spotify = data
if (this.spotifyTimerId > 0) {
clearTimeout(this.spotifyTimerId)
this.spotifyTimerId = 0
}
if (data.webapi_token_expires_in > 0 && data.webapi_token) {
this.spotifyTimerId = setTimeout(
() => this.updateSpotify(),
1000 * data.webapi_token_expires_in
)
}
})
}
},
getters: {
hasMissingSpotifyScopes: (state) => state.missingSpotifyScopes.length > 0,
isAuthorizationRequired: (state) =>
@ -22,6 +43,7 @@ export const useServicesStore = defineStore('ServicesStore', {
},
state: () => ({
lastfm: {},
spotify: {}
spotify: {},
spotifyTimerId: 0
})
})