mirror of
https://github.com/owntone/owntone-server.git
synced 2025-07-16 12:21:59 -04:00
[web] Improve the initialisation of stores
This commit is contained in:
parent
6b5f4ff4d7
commit
f91189d93b
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
993
web-src/package-lock.json
generated
993
web-src/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -123,7 +123,7 @@
|
||||
class="help"
|
||||
scope="global"
|
||||
>
|
||||
<br />
|
||||
<slot><br /></slot>
|
||||
</i18n-t>
|
||||
</template>
|
||||
</control-setting-text-field>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
})
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user