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