mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-07 04:24:50 +02:00
Add: Search page, Add: Bookshelf list view, Fix: Audiobook progress sync, Fix: Download audiobook button, Change: User audiobook data to use SQL table
This commit is contained in:
parent
b6dd37b7f6
commit
3b6e7e1ce2
16 changed files with 908 additions and 1619 deletions
|
@ -13,8 +13,8 @@ android {
|
|||
applicationId "com.audiobookshelf.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 40
|
||||
versionName "0.9.21-beta"
|
||||
versionCode 41
|
||||
versionName "0.9.22-beta"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
|
|
@ -115,6 +115,7 @@ class AudiobookManager {
|
|||
Log.d(tag, "keyvalue ${it.getString("key")} | ${it.getString("value")}")
|
||||
|
||||
var dlobj = JSObject(it.getString("value"))
|
||||
if (dlobj.has("audiobook")) {
|
||||
var abobj = dlobj.getJSObject("audiobook")!!
|
||||
abobj.put("isDownloaded", true)
|
||||
abobj.put("contentUrl", dlobj.getString("contentUrl", "").toString())
|
||||
|
@ -128,6 +129,7 @@ class AudiobookManager {
|
|||
audiobooks.add(audiobook)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun openStream(audiobook:Audiobook, streamListener:OnStreamData) {
|
||||
var url = "$serverUrl/api/audiobook/${audiobook.id}/stream"
|
||||
|
|
|
@ -1,377 +0,0 @@
|
|||
<template>
|
||||
<div ref="wrapper" class="w-full pt-2">
|
||||
<div class="relative">
|
||||
<div class="flex mt-2 mb-4">
|
||||
<div class="flex-grow" />
|
||||
<template v-if="!loading">
|
||||
<div class="cursor-pointer flex items-center justify-center text-gray-300 mr-8" @mousedown.prevent @mouseup.prevent @click.stop="restart">
|
||||
<span class="material-icons text-3xl">first_page</span>
|
||||
</div>
|
||||
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="backward10">
|
||||
<span class="material-icons text-3xl">replay_10</span>
|
||||
</div>
|
||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
|
||||
<span class="material-icons">{{ seekLoading ? 'autorenew' : isPaused ? 'play_arrow' : 'pause' }}</span>
|
||||
</div>
|
||||
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="forward10">
|
||||
<span class="material-icons text-3xl">forward_10</span>
|
||||
</div>
|
||||
<div class="cursor-pointer flex items-center justify-center text-gray-300 ml-7 w-10 text-center" @mousedown.prevent @mouseup.prevent @click="$emit('selectPlaybackSpeed')">
|
||||
<span class="font-mono text-lg">{{ playbackRate }}x</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
|
||||
<span class="material-icons">autorenew</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex-grow" />
|
||||
</div>
|
||||
|
||||
<div class="absolute top-2 right-3 text-white text-opacity-75" @click="showSleepTimerModal">
|
||||
<svg v-if="!sleepTimerRunning" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
<div v-else class="h-5 w-5 flex items-center justify-around">
|
||||
<p v-if="sleepTimerEndOfChapterTime" class="text-sm font-mono text-warning">EOC</p>
|
||||
<p v-else class="text-sm font-mono text-warning">{{ Math.ceil(sleepTimeoutCurrentTime / 1000 / 60) }}m</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Track -->
|
||||
<div ref="track" class="w-full h-2 bg-gray-700 relative cursor-pointer transform duration-100 hover:scale-y-125" :class="loading ? 'animate-pulse' : ''" @click.stop="clickTrack">
|
||||
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
||||
<div ref="bufferTrack" class="h-full bg-gray-400 absolute top-0 left-0 pointer-events-none" />
|
||||
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
||||
<div ref="trackCursor" class="h-full w-0.5 bg-gray-50 absolute top-0 left-0 opacity-0 pointer-events-none" />
|
||||
</div>
|
||||
<div class="flex items-center py-1 px-0.5">
|
||||
<div>
|
||||
<p ref="currentTimestamp" class="font-mono text-sm">00:00:00</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div>
|
||||
<p class="font-mono text-sm">{{ totalDurationPretty }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <audio ref="audio" @progress="progress" @pause="paused" @playing="playing" @timeupdate="timeupdate" @loadeddata="audioLoadedData" /> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MyNativeAudio from '@/plugins/my-native-audio'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
loading: Boolean,
|
||||
sleepTimerRunning: Boolean,
|
||||
sleepTimeoutCurrentTime: Number,
|
||||
sleepTimerEndOfChapterTime: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
totalDuration: 0,
|
||||
currentPlaybackRate: 1,
|
||||
currentTime: 0,
|
||||
isResetting: false,
|
||||
initObject: null,
|
||||
stateName: 'idle',
|
||||
playInterval: null,
|
||||
trackWidth: 0,
|
||||
isPaused: true,
|
||||
src: null,
|
||||
volume: 0.5,
|
||||
readyTrackWidth: 0,
|
||||
bufferTrackWidth: 0,
|
||||
playedTrackWidth: 0,
|
||||
seekedTime: 0,
|
||||
seekLoading: false,
|
||||
onPlayingUpdateListener: null,
|
||||
onMetadataListener: null,
|
||||
noSyncUpdateTime: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
totalDurationPretty() {
|
||||
return this.$secondsToTimestamp(this.totalDuration)
|
||||
},
|
||||
playbackRate() {
|
||||
return this.$store.getters['user/getUserSetting']('playbackRate')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showSleepTimerModal() {
|
||||
this.$emit('showSleepTimer')
|
||||
},
|
||||
updatePlaybackRate() {
|
||||
this.currentPlaybackRate = this.playbackRate
|
||||
MyNativeAudio.setPlaybackSpeed({ speed: this.playbackRate })
|
||||
},
|
||||
restart() {
|
||||
this.seek(0)
|
||||
},
|
||||
backward10() {
|
||||
MyNativeAudio.seekBackward({ amount: '10000' })
|
||||
},
|
||||
forward10() {
|
||||
MyNativeAudio.seekForward({ amount: '10000' })
|
||||
},
|
||||
sendStreamUpdate() {
|
||||
this.$emit('updateTime', this.currentTime)
|
||||
},
|
||||
setStreamReady() {
|
||||
this.readyTrackWidth = this.trackWidth
|
||||
this.$refs.readyTrack.style.width = this.trackWidth + 'px'
|
||||
},
|
||||
setChunksReady(chunks, numSegments) {
|
||||
var largestSeg = 0
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
var chunk = chunks[i]
|
||||
if (typeof chunk === 'string') {
|
||||
var chunkRange = chunk.split('-').map((c) => Number(c))
|
||||
if (chunkRange.length < 2) continue
|
||||
if (chunkRange[1] > largestSeg) largestSeg = chunkRange[1]
|
||||
} else if (chunk > largestSeg) {
|
||||
largestSeg = chunk
|
||||
}
|
||||
}
|
||||
var percentageReady = largestSeg / numSegments
|
||||
var widthReady = Math.round(this.trackWidth * percentageReady)
|
||||
if (this.readyTrackWidth === widthReady) {
|
||||
return
|
||||
}
|
||||
this.readyTrackWidth = widthReady
|
||||
this.$refs.readyTrack.style.width = widthReady + 'px'
|
||||
},
|
||||
updateTimestamp() {
|
||||
var ts = this.$refs.currentTimestamp
|
||||
if (!ts) {
|
||||
console.error('No timestamp el')
|
||||
return
|
||||
}
|
||||
var currTimeClean = this.$secondsToTimestamp(this.currentTime)
|
||||
ts.innerText = currTimeClean
|
||||
},
|
||||
timeupdate() {
|
||||
if (!this.$refs.playedTrack) {
|
||||
console.error('Invalid no played track ref')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.seekLoading) {
|
||||
this.seekLoading = false
|
||||
if (this.$refs.playedTrack) {
|
||||
this.$refs.playedTrack.classList.remove('bg-yellow-300')
|
||||
this.$refs.playedTrack.classList.add('bg-gray-200')
|
||||
}
|
||||
}
|
||||
|
||||
this.updateTimestamp()
|
||||
if (this.noSyncUpdateTime) this.noSyncUpdateTime = false
|
||||
else this.sendStreamUpdate()
|
||||
|
||||
var perc = this.currentTime / this.totalDuration
|
||||
var ptWidth = Math.round(perc * this.trackWidth)
|
||||
if (this.playedTrackWidth === ptWidth) {
|
||||
return
|
||||
}
|
||||
this.$refs.playedTrack.style.width = ptWidth + 'px'
|
||||
this.playedTrackWidth = ptWidth
|
||||
},
|
||||
seek(time) {
|
||||
if (this.seekLoading) {
|
||||
console.error('Already seek loading', this.seekedTime)
|
||||
return
|
||||
}
|
||||
|
||||
this.seekedTime = time
|
||||
this.seekLoading = true
|
||||
MyNativeAudio.seekPlayer({ timeMs: String(Math.floor(time * 1000)) })
|
||||
|
||||
if (this.$refs.playedTrack) {
|
||||
var perc = time / this.totalDuration
|
||||
var ptWidth = Math.round(perc * this.trackWidth)
|
||||
this.$refs.playedTrack.style.width = ptWidth + 'px'
|
||||
this.playedTrackWidth = ptWidth
|
||||
|
||||
this.$refs.playedTrack.classList.remove('bg-gray-200')
|
||||
this.$refs.playedTrack.classList.add('bg-yellow-300')
|
||||
}
|
||||
},
|
||||
updateVolume(volume) {},
|
||||
clickTrack(e) {
|
||||
var offsetX = e.offsetX
|
||||
var perc = offsetX / this.trackWidth
|
||||
var time = perc * this.totalDuration
|
||||
if (isNaN(time) || time === null) {
|
||||
console.error('Invalid time', perc, time)
|
||||
return
|
||||
}
|
||||
this.seek(time)
|
||||
},
|
||||
playPauseClick() {
|
||||
if (this.isPaused) {
|
||||
console.log('playPause PLAY')
|
||||
this.play()
|
||||
} else {
|
||||
console.log('playPause PAUSE')
|
||||
this.pause()
|
||||
}
|
||||
},
|
||||
calcSeekBackTime(lastUpdate) {
|
||||
var time = Date.now() - lastUpdate
|
||||
var seekback = 0
|
||||
if (time < 3000) seekback = 0
|
||||
else if (time < 60000) seekback = time / 6
|
||||
else if (time < 300000) seekback = 15000
|
||||
else if (time < 1800000) seekback = 20000
|
||||
else if (time < 3600000) seekback = 25000
|
||||
else seekback = 29500
|
||||
return seekback
|
||||
},
|
||||
async set(audiobookStreamData, stream, fromAppDestroy) {
|
||||
this.isResetting = false
|
||||
this.initObject = { ...audiobookStreamData }
|
||||
|
||||
var init = true
|
||||
if (!!stream) {
|
||||
//console.log(JSON.stringify(stream))
|
||||
var data = await MyNativeAudio.getStreamSyncData()
|
||||
console.log('getStreamSyncData', JSON.stringify(data))
|
||||
console.log('lastUpdate', stream.lastUpdate || 0)
|
||||
//Same audiobook
|
||||
if (data.id == stream.id && (data.isPlaying || data.lastPauseTime >= (stream.lastUpdate || 0))) {
|
||||
console.log('Same audiobook')
|
||||
this.isPaused = !data.isPlaying
|
||||
this.currentTime = Number((data.currentTime / 1000).toFixed(2))
|
||||
this.totalDuration = Number((data.duration / 1000).toFixed(2))
|
||||
this.timeupdate()
|
||||
if (data.isPlaying) {
|
||||
console.log('playing - continue')
|
||||
if (fromAppDestroy) this.startPlayInterval()
|
||||
} else console.log('paused and newer')
|
||||
if (!fromAppDestroy) return
|
||||
init = false
|
||||
this.initObject.startTime = String(Math.floor(this.currentTime * 1000))
|
||||
}
|
||||
//new audiobook stream or sync from other client
|
||||
else if (stream.clientCurrentTime > 0) {
|
||||
console.log('new audiobook stream or sync from other client')
|
||||
if (!!stream.lastUpdate) {
|
||||
var backTime = this.calcSeekBackTime(stream.lastUpdate)
|
||||
var currentTime = Math.floor(stream.clientCurrentTime * 1000)
|
||||
if (backTime >= currentTime) backTime = currentTime - 500
|
||||
console.log('SeekBackTime', backTime)
|
||||
this.initObject.startTime = String(Math.floor(currentTime - backTime))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.currentPlaybackRate = this.initObject.playbackSpeed
|
||||
if (init)
|
||||
MyNativeAudio.initPlayer(this.initObject).then((res) => {
|
||||
if (res && res.success) {
|
||||
console.log('Success init audio player')
|
||||
} else {
|
||||
console.error('Failed to init audio player')
|
||||
}
|
||||
})
|
||||
|
||||
if (audiobookStreamData.isLocal) {
|
||||
this.setStreamReady()
|
||||
}
|
||||
},
|
||||
setFromObj() {
|
||||
if (!this.initObject) {
|
||||
console.error('Cannot set from obj')
|
||||
return
|
||||
}
|
||||
this.isResetting = false
|
||||
MyNativeAudio.initPlayer(this.initObject).then((res) => {
|
||||
if (res && res.success) {
|
||||
console.log('Success init audio player')
|
||||
} else {
|
||||
console.error('Failed to init audio player')
|
||||
}
|
||||
})
|
||||
|
||||
if (audiobookStreamData.isLocal) {
|
||||
this.setStreamReady()
|
||||
}
|
||||
},
|
||||
play() {
|
||||
MyNativeAudio.playPlayer()
|
||||
this.startPlayInterval()
|
||||
},
|
||||
pause() {
|
||||
MyNativeAudio.pausePlayer()
|
||||
this.stopPlayInterval()
|
||||
},
|
||||
startPlayInterval() {
|
||||
clearInterval(this.playInterval)
|
||||
this.playInterval = setInterval(async () => {
|
||||
var data = await MyNativeAudio.getCurrentTime()
|
||||
this.currentTime = Number((data.value / 1000).toFixed(2))
|
||||
this.timeupdate()
|
||||
}, 1000)
|
||||
},
|
||||
stopPlayInterval() {
|
||||
clearInterval(this.playInterval)
|
||||
},
|
||||
resetStream(startTime) {
|
||||
var _time = String(Math.floor(startTime * 1000))
|
||||
if (!this.initObject) {
|
||||
console.error('Terminate stream when no init object is set...')
|
||||
return
|
||||
}
|
||||
this.isResetting = true
|
||||
this.initObject.currentTime = _time
|
||||
this.terminateStream()
|
||||
},
|
||||
terminateStream() {
|
||||
MyNativeAudio.terminateStream()
|
||||
},
|
||||
onPlayingUpdate(data) {
|
||||
this.isPaused = !data.value
|
||||
if (!this.isPaused) {
|
||||
this.startPlayInterval()
|
||||
} else {
|
||||
this.stopPlayInterval()
|
||||
}
|
||||
},
|
||||
onMetadata(data) {
|
||||
console.log('Native Audio On Metadata', JSON.stringify(data))
|
||||
this.totalDuration = Number((data.duration / 1000).toFixed(2))
|
||||
this.currentTime = Number((data.currentTime / 1000).toFixed(2))
|
||||
this.stateName = data.stateName
|
||||
|
||||
if (this.stateName === 'ended' && this.isResetting) {
|
||||
this.setFromObj()
|
||||
}
|
||||
|
||||
if (this.stateName === 'ready_no_sync' || this.stateName === 'buffering_no_sync') this.noSyncUpdateTime = true
|
||||
|
||||
this.timeupdate()
|
||||
},
|
||||
init() {
|
||||
this.onPlayingUpdateListener = MyNativeAudio.addListener('onPlayingUpdate', this.onPlayingUpdate)
|
||||
this.onMetadataListener = MyNativeAudio.addListener('onMetadata', this.onMetadata)
|
||||
|
||||
if (this.$refs.track) {
|
||||
this.trackWidth = this.$refs.track.clientWidth
|
||||
} else {
|
||||
console.error('Track not loaded', this.$refs)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(this.init)
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.onPlayingUpdateListener) this.onPlayingUpdateListener.remove()
|
||||
if (this.onMetadataListener) this.onMetadataListener.remove()
|
||||
clearInterval(this.playInterval)
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -98,6 +98,8 @@ export default {
|
|||
currentTime: 0,
|
||||
isResetting: false,
|
||||
initObject: null,
|
||||
streamId: null,
|
||||
audiobookId: null,
|
||||
stateName: 'idle',
|
||||
playInterval: null,
|
||||
trackWidth: 0,
|
||||
|
@ -111,10 +113,13 @@ export default {
|
|||
seekLoading: false,
|
||||
onPlayingUpdateListener: null,
|
||||
onMetadataListener: null,
|
||||
noSyncUpdateTime: false,
|
||||
// noSyncUpdateTime: false,
|
||||
touchStartY: 0,
|
||||
touchStartTime: 0,
|
||||
touchEndY: 0
|
||||
touchEndY: 0,
|
||||
listenTimeInterval: null,
|
||||
listeningTimeSinceLastUpdate: 0,
|
||||
totalListeningTimeInSession: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -153,6 +158,62 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
sendStreamSync(timeListened = 0) {
|
||||
var syncData = {
|
||||
timeListened,
|
||||
currentTime: this.currentTime,
|
||||
streamId: this.streamId,
|
||||
audiobookId: this.audiobookId,
|
||||
totalDuration: this.totalDuration
|
||||
}
|
||||
this.$emit('sync', syncData)
|
||||
},
|
||||
sendAddListeningTime() {
|
||||
var listeningTimeToAdd = Math.floor(this.listeningTimeSinceLastUpdate)
|
||||
this.listeningTimeSinceLastUpdate = Math.max(0, this.listeningTimeSinceLastUpdate - listeningTimeToAdd)
|
||||
this.sendStreamSync(listeningTimeToAdd)
|
||||
},
|
||||
cancelListenTimeInterval() {
|
||||
this.sendAddListeningTime()
|
||||
clearInterval(this.listenTimeInterval)
|
||||
this.listenTimeInterval = null
|
||||
},
|
||||
startListenTimeInterval() {
|
||||
clearInterval(this.listenTimeInterval)
|
||||
var lastTime = this.currentTime
|
||||
var lastTick = Date.now()
|
||||
var noProgressCount = 0
|
||||
this.listenTimeInterval = setInterval(() => {
|
||||
var timeSinceLastTick = Date.now() - lastTick
|
||||
lastTick = Date.now()
|
||||
|
||||
var expectedAudioTime = lastTime + timeSinceLastTick / 1000
|
||||
var currentTime = this.currentTime
|
||||
var differenceFromExpected = expectedAudioTime - currentTime
|
||||
if (currentTime === lastTime) {
|
||||
noProgressCount++
|
||||
if (noProgressCount > 3) {
|
||||
console.error('Audio current time has not increased - cancel interval and pause player')
|
||||
this.pause()
|
||||
}
|
||||
} else if (Math.abs(differenceFromExpected) > 0.1) {
|
||||
noProgressCount = 0
|
||||
console.warn('Invalid time between interval - resync last', differenceFromExpected)
|
||||
lastTime = currentTime
|
||||
} else {
|
||||
noProgressCount = 0
|
||||
var exactPlayTimeDifference = currentTime - lastTime
|
||||
// console.log('Difference from expected', differenceFromExpected, 'Exact play time diff', exactPlayTimeDifference)
|
||||
lastTime = currentTime
|
||||
this.listeningTimeSinceLastUpdate += exactPlayTimeDifference
|
||||
this.totalListeningTimeInSession += exactPlayTimeDifference
|
||||
// console.log('Time since last update:', this.listeningTimeSinceLastUpdate, 'Session listening time:', this.totalListeningTimeInSession)
|
||||
if (this.listeningTimeSinceLastUpdate > 5) {
|
||||
this.sendAddListeningTime()
|
||||
}
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
clickContainer() {
|
||||
this.showFullscreen = true
|
||||
},
|
||||
|
@ -200,9 +261,9 @@ export default {
|
|||
if (this.loading) return
|
||||
MyNativeAudio.seekForward({ amount: '10000' })
|
||||
},
|
||||
sendStreamUpdate() {
|
||||
this.$emit('updateTime', this.currentTime)
|
||||
},
|
||||
// sendStreamUpdate() {
|
||||
// this.$emit('updateTime', this.currentTime)
|
||||
// },
|
||||
setStreamReady() {
|
||||
this.readyTrackWidth = this.trackWidth
|
||||
this.$refs.readyTrack.style.width = this.trackWidth + 'px'
|
||||
|
@ -251,8 +312,8 @@ export default {
|
|||
}
|
||||
|
||||
this.updateTimestamp()
|
||||
if (this.noSyncUpdateTime) this.noSyncUpdateTime = false
|
||||
else this.sendStreamUpdate()
|
||||
// if (this.noSyncUpdateTime) this.noSyncUpdateTime = false
|
||||
// else this.sendStreamUpdate()
|
||||
|
||||
var perc = this.currentTime / this.totalDuration
|
||||
var ptWidth = Math.round(perc * this.trackWidth)
|
||||
|
@ -283,7 +344,6 @@ export default {
|
|||
this.$refs.playedTrack.classList.add('bg-yellow-300')
|
||||
}
|
||||
},
|
||||
updateVolume(volume) {},
|
||||
clickTrack(e) {
|
||||
if (this.loading) return
|
||||
var offsetX = e.offsetX
|
||||
|
@ -318,6 +378,8 @@ export default {
|
|||
},
|
||||
async set(audiobookStreamData, stream, fromAppDestroy) {
|
||||
this.isResetting = false
|
||||
this.streamId = stream ? stream.id : null
|
||||
this.audiobookId = audiobookStreamData.audiobookId
|
||||
this.initObject = { ...audiobookStreamData }
|
||||
|
||||
var init = true
|
||||
|
@ -397,6 +459,8 @@ export default {
|
|||
this.stopPlayInterval()
|
||||
},
|
||||
startPlayInterval() {
|
||||
this.startListenTimeInterval()
|
||||
|
||||
clearInterval(this.playInterval)
|
||||
this.playInterval = setInterval(async () => {
|
||||
var data = await MyNativeAudio.getCurrentTime()
|
||||
|
@ -405,6 +469,7 @@ export default {
|
|||
}, 1000)
|
||||
},
|
||||
stopPlayInterval() {
|
||||
this.cancelListenTimeInterval()
|
||||
clearInterval(this.playInterval)
|
||||
},
|
||||
resetStream(startTime) {
|
||||
|
@ -438,7 +503,7 @@ export default {
|
|||
this.setFromObj()
|
||||
}
|
||||
|
||||
if (this.stateName === 'ready_no_sync' || this.stateName === 'buffering_no_sync') this.noSyncUpdateTime = true
|
||||
// if (this.stateName === 'ready_no_sync' || this.stateName === 'buffering_no_sync') this.noSyncUpdateTime = true
|
||||
|
||||
this.timeupdate()
|
||||
},
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
:sleep-timer-end-of-chapter-time="sleepTimerEndOfChapterTime"
|
||||
:sleep-timeout-current-time="sleepTimeoutCurrentTime"
|
||||
@close="cancelStream"
|
||||
@updateTime="updateTime"
|
||||
@sync="sync"
|
||||
@selectPlaybackSpeed="showPlaybackSpeedModal = true"
|
||||
@selectChapter="clickChapterBtn"
|
||||
@showSleepTimer="showSleepTimer"
|
||||
|
@ -260,6 +260,35 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
sync(syncData) {
|
||||
var diff = syncData.currentTime - this.lastServerUpdateSentSeconds
|
||||
if (Math.abs(diff) < 1 && !syncData.timeListened) {
|
||||
// No need to sync
|
||||
return
|
||||
}
|
||||
|
||||
if (this.stream) {
|
||||
this.$server.socket.emit('stream_sync', syncData)
|
||||
} else {
|
||||
var progressUpdate = {
|
||||
audiobookId: syncData.audiobookId,
|
||||
currentTime: syncData.currentTime,
|
||||
totalDuration: syncData.totalDuration,
|
||||
progress: Number((syncData.currentTime / syncData.totalDuration).toFixed(3)),
|
||||
lastUpdate: Date.now(),
|
||||
isRead: false
|
||||
}
|
||||
|
||||
if (this.$server.connected) {
|
||||
this.$server.socket.emit('progress_update', progressUpdate)
|
||||
} else {
|
||||
this.$store.dispatch('user/updateUserAudiobookData', progressUpdate)
|
||||
// this.$localStore.updateUserAudiobookData(progressUpdate).then(() => {
|
||||
// console.log('Updated user audiobook progress', currentTime)
|
||||
// })
|
||||
}
|
||||
}
|
||||
},
|
||||
updateTime(currentTime) {
|
||||
this.currentTime = currentTime
|
||||
|
||||
|
@ -366,7 +395,8 @@ export default {
|
|||
series: this.seriesTxt,
|
||||
token: this.userToken,
|
||||
contentUrl: this.playingDownload.contentUrl,
|
||||
isLocal: true
|
||||
isLocal: true,
|
||||
audiobookId: this.download.id
|
||||
}
|
||||
|
||||
this.$refs.audioPlayer.set(audiobookStreamData, null, false)
|
||||
|
@ -406,7 +436,8 @@ export default {
|
|||
duration: String(Math.floor(this.duration * 1000)),
|
||||
series: this.seriesTxt,
|
||||
playlistUrl: this.$server.url + playlistUrl,
|
||||
token: this.$store.getters['user/getToken']
|
||||
token: this.$store.getters['user/getToken'],
|
||||
audiobookId: this.audiobookId
|
||||
}
|
||||
this.$refs.audioPlayer.set(audiobookStreamData, stream, !this.stream)
|
||||
|
||||
|
|
|
@ -1,473 +0,0 @@
|
|||
<template>
|
||||
<div class="w-full p-4 pointer-events-none fixed bottom-0 left-0 right-0 z-20">
|
||||
<div v-if="audiobook" id="streamContainer" class="w-full bg-primary absolute bottom-0 left-0 right-0 z-50 p-2 pointer-events-auto" @click.stop @mousedown.stop @mouseup.stop>
|
||||
<div class="pl-16 pr-2 flex items-center pb-2">
|
||||
<div>
|
||||
<p class="px-2">{{ title }}</p>
|
||||
<p class="px-2 text-xs text-gray-400">by {{ author }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div v-if="chapters.length" class="cursor-pointer flex items-center justify-center mr-6 w-6 text-center" :class="chapters.length ? 'text-gray-300' : 'text-gray-400'" @mousedown.prevent @mouseup.prevent @click="clickChapterBtn">
|
||||
<span class="material-icons text-2xl">format_list_bulleted</span>
|
||||
</div>
|
||||
<span class="material-icons" @click="cancelStream">close</span>
|
||||
</div>
|
||||
<div class="absolute left-2 -top-10 bookCoverWrapper">
|
||||
<cards-book-cover :audiobook="audiobook" :download-cover="downloadedCover" :width="64" />
|
||||
</div>
|
||||
<audio-player-mini ref="audioPlayerMini" :loading="isLoading" :sleep-timer-running="isSleepTimerRunning" :sleep-timer-end-of-chapter-time="sleepTimerEndOfChapterTime" :sleep-timeout-current-time="sleepTimeoutCurrentTime" @updateTime="updateTime" @selectPlaybackSpeed="showPlaybackSpeedModal = true" @showSleepTimer="showSleepTimer" @hook:mounted="audioPlayerMounted" />
|
||||
</div>
|
||||
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-speed.sync="playbackSpeed" @change="changePlaybackSpeed" />
|
||||
|
||||
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" />
|
||||
|
||||
<modals-sleep-timer-modal v-model="showSleepTimerModal" :current-time="sleepTimeoutCurrentTime" :sleep-timer-running="isSleepTimerRunning" :current-end-of-chapter-time="currentEndOfChapterTime" :end-of-chapter-time-set="sleepTimerEndOfChapterTime" @change="selectSleepTimeout" @cancel="cancelSleepTimer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Dialog } from '@capacitor/dialog'
|
||||
import MyNativeAudio from '@/plugins/my-native-audio'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
audioPlayerReady: false,
|
||||
stream: null,
|
||||
download: null,
|
||||
lastProgressTimeUpdate: 0,
|
||||
showPlaybackSpeedModal: false,
|
||||
showSleepTimerModal: false,
|
||||
playbackSpeed: 1,
|
||||
showChapterModal: false,
|
||||
currentTime: 0,
|
||||
sleepTimeoutCurrentTime: 0,
|
||||
isSleepTimerRunning: false,
|
||||
sleepTimerEndOfChapterTime: false,
|
||||
onSleepTimerEndedListener: null,
|
||||
sleepInterval: null,
|
||||
currentEndOfChapterTime: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
socketConnected(newVal) {
|
||||
if (newVal) {
|
||||
console.log('Socket Connected set listeners')
|
||||
this.setListeners()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
currentChapter() {
|
||||
if (!this.audiobook || !this.chapters.length) return null
|
||||
return this.chapters.find((ch) => ch.start <= this.currentTime && ch.end > this.currentTime)
|
||||
},
|
||||
socketConnected() {
|
||||
return this.$store.state.socketConnected
|
||||
},
|
||||
isLoading() {
|
||||
if (this.playingDownload) return false
|
||||
if (!this.streamAudiobook) return false
|
||||
return !this.stream || this.streamAudiobook.id !== this.stream.audiobook.id
|
||||
},
|
||||
playingDownload() {
|
||||
return this.$store.state.playingDownload
|
||||
},
|
||||
audiobook() {
|
||||
if (this.playingDownload) return this.playingDownload.audiobook
|
||||
return this.streamAudiobook
|
||||
},
|
||||
audiobookId() {
|
||||
return this.audiobook ? this.audiobook.id : null
|
||||
},
|
||||
streamAudiobook() {
|
||||
return this.$store.state.streamAudiobook
|
||||
},
|
||||
book() {
|
||||
return this.audiobook ? this.audiobook.book || {} : {}
|
||||
},
|
||||
title() {
|
||||
return this.book ? this.book.title : ''
|
||||
},
|
||||
author() {
|
||||
return this.book ? this.book.author : ''
|
||||
},
|
||||
cover() {
|
||||
return this.book ? this.book.cover : ''
|
||||
},
|
||||
downloadedCover() {
|
||||
return this.download ? this.download.cover : null
|
||||
},
|
||||
series() {
|
||||
return this.book ? this.book.series : ''
|
||||
},
|
||||
chapters() {
|
||||
return this.audiobook ? this.audiobook.chapters || [] : []
|
||||
},
|
||||
volumeNumber() {
|
||||
return this.book ? this.book.volumeNumber : ''
|
||||
},
|
||||
seriesTxt() {
|
||||
if (!this.series) return ''
|
||||
if (!this.volumeNumber) return this.series
|
||||
return `${this.series} #${this.volumeNumber}`
|
||||
},
|
||||
duration() {
|
||||
return this.audiobook ? this.audiobook.duration || 0 : 0
|
||||
},
|
||||
coverForNative() {
|
||||
if (!this.cover) {
|
||||
return `${this.$store.state.serverUrl}/Logo.png`
|
||||
}
|
||||
if (this.cover.startsWith('http')) return this.cover
|
||||
var coverSrc = this.$store.getters['audiobooks/getBookCoverSrc'](this.audiobook)
|
||||
return coverSrc
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSleepTimerEnded({ value: currentPosition }) {
|
||||
this.isSleepTimerRunning = false
|
||||
if (this.sleepInterval) clearInterval(this.sleepInterval)
|
||||
|
||||
if (currentPosition) {
|
||||
console.log('Sleep Timer Ended Current Position: ' + currentPosition)
|
||||
var currentTime = Math.floor(currentPosition / 1000)
|
||||
this.updateTime(currentTime)
|
||||
}
|
||||
},
|
||||
showSleepTimer() {
|
||||
if (this.currentChapter) {
|
||||
this.currentEndOfChapterTime = Math.floor(this.currentChapter.end)
|
||||
} else {
|
||||
this.currentEndOfChapterTime = 0
|
||||
}
|
||||
this.showSleepTimerModal = true
|
||||
},
|
||||
async getSleepTimerTime() {
|
||||
var res = await MyNativeAudio.getSleepTimerTime()
|
||||
if (res && res.value) {
|
||||
var time = Number(res.value)
|
||||
return time - Date.now()
|
||||
}
|
||||
return 0
|
||||
},
|
||||
async selectSleepTimeout({ time, isChapterTime }) {
|
||||
console.log('Setting sleep timer', time, isChapterTime)
|
||||
var res = await MyNativeAudio.setSleepTimer({ time: String(time), isChapterTime })
|
||||
if (!res.success) {
|
||||
return this.$toast.error('Sleep timer did not set, invalid time')
|
||||
}
|
||||
if (isChapterTime) {
|
||||
this.sleepTimerEndOfChapterTime = time
|
||||
this.isSleepTimerRunning = true
|
||||
} else {
|
||||
this.sleepTimerEndOfChapterTime = 0
|
||||
this.setSleepTimeoutTimer(time)
|
||||
}
|
||||
},
|
||||
async cancelSleepTimer() {
|
||||
console.log('Canceling sleep timer')
|
||||
await MyNativeAudio.cancelSleepTimer()
|
||||
this.isSleepTimerRunning = false
|
||||
this.sleepTimerEndOfChapterTime = 0
|
||||
if (this.sleepInterval) clearInterval(this.sleepInterval)
|
||||
},
|
||||
async syncSleepTimer() {
|
||||
var time = await this.getSleepTimerTime()
|
||||
this.setSleepTimeoutTimer(time)
|
||||
},
|
||||
setSleepTimeoutTimer(startTime) {
|
||||
if (this.sleepInterval) clearInterval(this.sleepInterval)
|
||||
|
||||
this.sleepTimeoutCurrentTime = startTime
|
||||
this.isSleepTimerRunning = true
|
||||
var elapsed = 0
|
||||
this.sleepInterval = setInterval(() => {
|
||||
this.sleepTimeoutCurrentTime = Math.max(0, this.sleepTimeoutCurrentTime - 1000)
|
||||
|
||||
if (this.sleepTimeoutCurrentTime <= 0) {
|
||||
clearInterval(this.sleepInterval)
|
||||
return
|
||||
}
|
||||
|
||||
// Sync with the actual time from android Timer
|
||||
elapsed++
|
||||
if (elapsed > 5) {
|
||||
clearInterval(this.sleepInterval)
|
||||
this.syncSleepTimer()
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
clickChapterBtn() {
|
||||
if (!this.chapters.length) return
|
||||
this.showChapterModal = true
|
||||
},
|
||||
selectChapter(chapter) {
|
||||
if (this.$refs.audioPlayerMini) {
|
||||
this.$refs.audioPlayerMini.seek(chapter.start)
|
||||
}
|
||||
this.showChapterModal = false
|
||||
},
|
||||
async cancelStream() {
|
||||
this.currentTime = 0
|
||||
|
||||
if (this.download) {
|
||||
if (this.$refs.audioPlayerMini) {
|
||||
this.$refs.audioPlayerMini.terminateStream()
|
||||
}
|
||||
this.download = null
|
||||
this.$store.commit('setPlayingDownload', null)
|
||||
|
||||
this.$localStore.setCurrent(null)
|
||||
} else {
|
||||
const { value } = await Dialog.confirm({
|
||||
title: 'Confirm',
|
||||
message: 'Cancel this stream?'
|
||||
})
|
||||
if (value) {
|
||||
this.$server.socket.emit('close_stream')
|
||||
this.$store.commit('setStreamAudiobook', null)
|
||||
this.$server.stream = null
|
||||
if (this.$refs.audioPlayerMini) {
|
||||
this.$refs.audioPlayerMini.terminateStream()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
updateTime(currentTime) {
|
||||
this.currentTime = currentTime
|
||||
|
||||
var diff = currentTime - this.lastProgressTimeUpdate
|
||||
|
||||
if (diff > 4 || diff < 0) {
|
||||
this.lastProgressTimeUpdate = currentTime
|
||||
if (this.stream) {
|
||||
var updatePayload = {
|
||||
currentTime,
|
||||
streamId: this.stream.id
|
||||
}
|
||||
this.$server.socket.emit('stream_update', updatePayload)
|
||||
} else if (this.download) {
|
||||
var progressUpdate = {
|
||||
audiobookId: this.download.id,
|
||||
currentTime: currentTime,
|
||||
totalDuration: this.download.audiobook.duration,
|
||||
progress: Number((currentTime / this.download.audiobook.duration).toFixed(3)),
|
||||
lastUpdate: Date.now(),
|
||||
isRead: false
|
||||
}
|
||||
|
||||
if (this.$server.connected) {
|
||||
this.$server.socket.emit('progress_update', progressUpdate)
|
||||
}
|
||||
this.$localStore.updateUserAudiobookData(progressUpdate).then(() => {
|
||||
console.log('Updated user audiobook progress', currentTime)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
closeStream() {},
|
||||
streamClosed(audiobookId) {
|
||||
console.log('Stream Closed')
|
||||
if (this.stream.audiobook.id === audiobookId || audiobookId === 'n/a') {
|
||||
this.$store.commit('setStreamAudiobook', null)
|
||||
}
|
||||
},
|
||||
streamProgress(data) {
|
||||
if (!data.numSegments) return
|
||||
var chunks = data.chunks
|
||||
if (this.$refs.audioPlayerMini) {
|
||||
this.$refs.audioPlayerMini.setChunksReady(chunks, data.numSegments)
|
||||
}
|
||||
},
|
||||
streamReady() {
|
||||
console.log('[StreamContainer] Stream Ready')
|
||||
if (this.$refs.audioPlayerMini) {
|
||||
this.$refs.audioPlayerMini.setStreamReady()
|
||||
}
|
||||
},
|
||||
streamReset({ streamId, startTime }) {
|
||||
if (this.$refs.audioPlayerMini) {
|
||||
if (this.stream && this.stream.id === streamId) {
|
||||
this.$refs.audioPlayerMini.resetStream(startTime)
|
||||
}
|
||||
}
|
||||
},
|
||||
async getDownloadStartTime() {
|
||||
var userAudiobook = await this.$localStore.getMostRecentUserAudiobook(this.audiobookId)
|
||||
if (!userAudiobook) {
|
||||
console.log('[StreamContainer] getDownloadStartTime no user audiobook record found')
|
||||
return 0
|
||||
}
|
||||
return userAudiobook.currentTime
|
||||
},
|
||||
async playDownload() {
|
||||
if (this.stream) {
|
||||
if (this.$refs.audioPlayerMini) {
|
||||
this.$refs.audioPlayerMini.terminateStream()
|
||||
}
|
||||
this.stream = null
|
||||
}
|
||||
|
||||
this.lastProgressTimeUpdate = 0
|
||||
console.log('[StreamContainer] Playing local', this.playingDownload)
|
||||
if (!this.$refs.audioPlayerMini) {
|
||||
console.error('No Audio Player Mini')
|
||||
return
|
||||
}
|
||||
|
||||
var playOnLoad = this.$store.state.playOnLoad
|
||||
if (playOnLoad) this.$store.commit('setPlayOnLoad', false)
|
||||
|
||||
var currentTime = await this.getDownloadStartTime()
|
||||
if (isNaN(currentTime) || currentTime === null) currentTime = 0
|
||||
this.currentTime = currentTime
|
||||
|
||||
// Update local current time
|
||||
this.$localStore.setCurrent({
|
||||
audiobookId: this.download.id,
|
||||
lastUpdate: Date.now()
|
||||
})
|
||||
|
||||
var audiobookStreamData = {
|
||||
title: this.title,
|
||||
author: this.author,
|
||||
playWhenReady: !!playOnLoad,
|
||||
startTime: String(Math.floor(currentTime * 1000)),
|
||||
playbackSpeed: this.playbackSpeed || 1,
|
||||
cover: this.download.coverUrl || null,
|
||||
duration: String(Math.floor(this.duration * 1000)),
|
||||
series: this.seriesTxt,
|
||||
token: this.userToken,
|
||||
contentUrl: this.playingDownload.contentUrl,
|
||||
isLocal: true
|
||||
}
|
||||
|
||||
this.$refs.audioPlayerMini.set(audiobookStreamData, null, false)
|
||||
},
|
||||
streamOpen(stream) {
|
||||
if (this.download) {
|
||||
if (this.$refs.audioPlayerMini) {
|
||||
this.$refs.audioPlayerMini.terminateStream()
|
||||
}
|
||||
}
|
||||
|
||||
this.lastProgressTimeUpdate = 0
|
||||
console.log('[StreamContainer] Stream Open: ' + this.title)
|
||||
|
||||
if (!this.$refs.audioPlayerMini) {
|
||||
console.error('No Audio Player Mini')
|
||||
return
|
||||
}
|
||||
|
||||
// Update local remove current
|
||||
this.$localStore.setCurrent(null)
|
||||
|
||||
var playlistUrl = stream.clientPlaylistUri
|
||||
var currentTime = stream.clientCurrentTime || 0
|
||||
this.currentTime = currentTime
|
||||
var playOnLoad = this.$store.state.playOnLoad
|
||||
if (playOnLoad) this.$store.commit('setPlayOnLoad', false)
|
||||
|
||||
var audiobookStreamData = {
|
||||
id: stream.id,
|
||||
title: this.title,
|
||||
author: this.author,
|
||||
playWhenReady: !!playOnLoad,
|
||||
startTime: String(Math.floor(currentTime * 1000)),
|
||||
playbackSpeed: this.playbackSpeed || 1,
|
||||
cover: this.coverForNative,
|
||||
duration: String(Math.floor(this.duration * 1000)),
|
||||
series: this.seriesTxt,
|
||||
playlistUrl: this.$server.url + playlistUrl,
|
||||
token: this.$store.getters['user/getToken']
|
||||
}
|
||||
this.$refs.audioPlayerMini.set(audiobookStreamData, stream, !this.stream)
|
||||
|
||||
this.stream = stream
|
||||
},
|
||||
audioPlayerMounted() {
|
||||
console.log('Audio Player Mounted', this.$server.stream)
|
||||
this.audioPlayerReady = true
|
||||
|
||||
if (this.playingDownload) {
|
||||
console.log('[StreamContainer] Play download on audio mount')
|
||||
if (!this.download) {
|
||||
this.download = { ...this.playingDownload }
|
||||
}
|
||||
this.playDownload()
|
||||
} else if (this.$server.stream) {
|
||||
console.log('[StreamContainer] Open stream on audio mount')
|
||||
this.streamOpen(this.$server.stream)
|
||||
}
|
||||
},
|
||||
changePlaybackSpeed(speed) {
|
||||
this.$store.dispatch('user/updateUserSettings', { playbackRate: speed })
|
||||
},
|
||||
settingsUpdated(settings) {
|
||||
if (this.$refs.audioPlayerMini && this.$refs.audioPlayerMini.currentPlaybackRate !== settings.playbackRate) {
|
||||
this.playbackSpeed = settings.playbackRate
|
||||
this.$refs.audioPlayerMini.updatePlaybackRate()
|
||||
}
|
||||
},
|
||||
streamUpdated(type, data) {
|
||||
if (type === 'download') {
|
||||
if (data) {
|
||||
this.download = { ...data }
|
||||
if (this.audioPlayerReady) {
|
||||
this.playDownload()
|
||||
}
|
||||
} else if (this.download) {
|
||||
this.cancelStream()
|
||||
}
|
||||
}
|
||||
},
|
||||
setListeners() {
|
||||
if (!this.$server.socket) {
|
||||
console.error('Invalid server socket not set')
|
||||
return
|
||||
}
|
||||
this.$server.socket.on('stream_open', this.streamOpen)
|
||||
this.$server.socket.on('stream_closed', this.streamClosed)
|
||||
this.$server.socket.on('stream_progress', this.streamProgress)
|
||||
this.$server.socket.on('stream_ready', this.streamReady)
|
||||
this.$server.socket.on('stream_reset', this.streamReset)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.onSleepTimerEndedListener = MyNativeAudio.addListener('onSleepTimerEnded', this.onSleepTimerEnded)
|
||||
|
||||
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
|
||||
|
||||
this.setListeners()
|
||||
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
|
||||
this.$store.commit('setStreamListener', this.streamUpdated)
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove()
|
||||
|
||||
if (this.$server.socket) {
|
||||
this.$server.socket.off('stream_open', this.streamOpen)
|
||||
this.$server.socket.off('stream_closed', this.streamClosed)
|
||||
this.$server.socket.off('stream_progress', this.streamProgress)
|
||||
this.$server.socket.off('stream_ready', this.streamReady)
|
||||
this.$server.socket.off('stream_reset', this.streamReset)
|
||||
}
|
||||
|
||||
this.$store.commit('user/removeSettingsListener', 'streamContainer')
|
||||
this.$store.commit('removeStreamListener')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.bookCoverWrapper {
|
||||
box-shadow: 3px -2px 5px #00000066;
|
||||
}
|
||||
#streamContainer {
|
||||
box-shadow: 0px -8px 8px #11111177;
|
||||
}
|
||||
</style>
|
|
@ -47,7 +47,9 @@ export default {
|
|||
methods: {
|
||||
async connected(isConnected) {
|
||||
if (isConnected) {
|
||||
this.syncUserProgress()
|
||||
// this.syncUserProgress()
|
||||
console.log('[Default] Connected socket sync user ab data')
|
||||
this.$store.dispatch('user/syncUserAudiobookData')
|
||||
|
||||
// Load libraries
|
||||
this.$store.dispatch('libraries/load')
|
||||
|
@ -109,9 +111,11 @@ export default {
|
|||
currentUserAudiobookUpdate({ id, data }) {
|
||||
if (data) {
|
||||
console.log(`Current User Audiobook Updated ${id} ${JSON.stringify(data)}`)
|
||||
this.$localStore.updateUserAudiobookData(data)
|
||||
// this.$localStore.updateUserAudiobookData(data)
|
||||
this.$sqlStore.setUserAudiobookData(data)
|
||||
} else {
|
||||
this.$localStore.removeAudiobookProgress(id)
|
||||
// this.$localStore.removeAudiobookProgress(id)
|
||||
this.$sqlStore.removeUserAudiobookData(id)
|
||||
}
|
||||
},
|
||||
initialStream(stream) {
|
||||
|
@ -445,6 +449,10 @@ export default {
|
|||
if (!this.$server) return console.error('No Server')
|
||||
// console.log(`Default Mounted set SOCKET listeners ${this.$server.connected}`)
|
||||
|
||||
if (!this.$server.connected) {
|
||||
console.log('Syncing on default mount')
|
||||
this.$store.dispatch('user/syncUserAudiobookData')
|
||||
}
|
||||
this.$server.on('connected', this.connected)
|
||||
this.$server.on('connectionFailed', this.socketConnectionFailed)
|
||||
this.$server.on('initialStream', this.initialStream)
|
||||
|
@ -459,28 +467,6 @@ export default {
|
|||
this.checkForUpdate()
|
||||
this.initMediaStore()
|
||||
}
|
||||
|
||||
if (!this.$server.connected) {
|
||||
}
|
||||
|
||||
// Old bad attempt at AA
|
||||
// MyNativeAudio.addListener('onPrepareMedia', (data) => {
|
||||
// var audiobookId = data.audiobookId
|
||||
// var playWhenReady = data.playWhenReady
|
||||
|
||||
// var audiobook = this.$store.getters['audiobooks/getAudiobook'](audiobookId)
|
||||
|
||||
// var download = this.$store.getters['downloads/getDownloadIfReady'](audiobookId)
|
||||
// this.$store.commit('setPlayOnLoad', playWhenReady)
|
||||
// if (!download) {
|
||||
// // Stream
|
||||
// this.$store.commit('setStreamAudiobook', audiobook)
|
||||
// this.$server.socket.emit('open_stream', audiobook.id)
|
||||
// } else {
|
||||
// // Local
|
||||
// this.$store.commit('setPlayingDownload', download)
|
||||
// }
|
||||
// })
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (!this.$server) {
|
||||
|
|
|
@ -14,7 +14,7 @@ export default {
|
|||
},
|
||||
|
||||
head: {
|
||||
title: 'AudioBookshelf',
|
||||
title: 'Audiobookshelf',
|
||||
htmlAttrs: {
|
||||
lang: 'en'
|
||||
},
|
||||
|
@ -36,7 +36,8 @@ export default {
|
|||
|
||||
plugins: [
|
||||
'@/plugins/server.js',
|
||||
'@/plugins/store.js',
|
||||
'@/plugins/sqlStore.js',
|
||||
'@/plugins/localStore.js',
|
||||
'@/plugins/init.client.js',
|
||||
'@/plugins/axios.js',
|
||||
'@/plugins/my-native-audio.js',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "audiobookshelf-app",
|
||||
"version": "v0.9.21-beta",
|
||||
"version": "v0.9.22-beta",
|
||||
"author": "advplyr",
|
||||
"scripts": {
|
||||
"dev": "nuxt --hostname localhost --port 1337",
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
<span class="material-icons">auto_stories</span>
|
||||
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
||||
</ui-btn>
|
||||
<ui-btn v-if="isConnected && showPlay" color="primary" :disabled="isPlaying" class="flex items-center justify-center" :padding-x="2" @click="downloadClick">
|
||||
<ui-btn v-if="isConnected && showPlay" color="primary" class="flex items-center justify-center" :padding-x="2" @click="downloadClick">
|
||||
<span class="material-icons" :class="downloadObj ? 'animate-pulse' : ''">{{ downloadObj ? (isDownloading || isDownloadPreparing ? 'downloading' : 'download_done') : 'download' }}</span>
|
||||
</ui-btn>
|
||||
</div>
|
||||
|
@ -218,6 +218,15 @@ export default {
|
|||
|
||||
if (value) {
|
||||
this.resettingProgress = true
|
||||
this.$store.dispatch('user/updateUserAudiobookData', {
|
||||
audiobookId: this.audiobookId,
|
||||
currentTime: 0,
|
||||
totalDuration: this.duration,
|
||||
progress: 0,
|
||||
lastUpdate: Date.now(),
|
||||
isRead: false
|
||||
})
|
||||
|
||||
if (this.$server.connected) {
|
||||
await this.$axios
|
||||
.$patch(`/api/user/audiobook/${this.audiobookId}/reset-progress`)
|
||||
|
@ -229,14 +238,7 @@ export default {
|
|||
console.error('Progress reset failed', error)
|
||||
})
|
||||
}
|
||||
this.$localStore.updateUserAudiobookData({
|
||||
audiobookId: this.audiobookId,
|
||||
currentTime: 0,
|
||||
totalDuration: this.duration,
|
||||
progress: 0,
|
||||
lastUpdate: Date.now(),
|
||||
isRead: false
|
||||
})
|
||||
|
||||
this.resettingProgress = false
|
||||
}
|
||||
},
|
||||
|
@ -252,6 +254,7 @@ export default {
|
|||
})
|
||||
},
|
||||
downloadClick() {
|
||||
console.log('downloadClick ' + this.$server.connected + ' | ' + !!this.downloadObj)
|
||||
if (!this.$server.connected) return
|
||||
|
||||
if (this.downloadObj) {
|
||||
|
@ -260,24 +263,47 @@ export default {
|
|||
this.prepareDownload()
|
||||
}
|
||||
},
|
||||
async changeDownloadFolderClick() {
|
||||
if (!this.hasStoragePermission) {
|
||||
console.log('Requesting Storage Permission')
|
||||
await StorageManager.requestStoragePermission()
|
||||
} else {
|
||||
var folderObj = await StorageManager.selectFolder()
|
||||
if (folderObj.error) {
|
||||
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
|
||||
}
|
||||
|
||||
var permissionsGood = await StorageManager.checkFolderPermissions({ folderUrl: folderObj.uri })
|
||||
console.log('Storage Permission check folder ' + permissionsGood)
|
||||
|
||||
if (!permissionsGood) {
|
||||
this.$toast.error('Folder permissions failed')
|
||||
return
|
||||
} else {
|
||||
this.$toast.success('Folder permission success')
|
||||
}
|
||||
|
||||
await this.$localStore.setDownloadFolder(folderObj)
|
||||
}
|
||||
},
|
||||
async prepareDownload() {
|
||||
var audiobook = this.audiobook
|
||||
if (!audiobook) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.hasStoragePermission) {
|
||||
this.$store.commit('downloads/setShowModal', true)
|
||||
return
|
||||
}
|
||||
|
||||
// Download Path
|
||||
var dlFolder = this.$localStore.downloadFolder
|
||||
if (!dlFolder) {
|
||||
console.log('Prepare download: ' + this.hasStoragePermission + ' | ' + dlFolder)
|
||||
|
||||
if (!this.hasStoragePermission || !dlFolder) {
|
||||
console.log('No download folder, request from user')
|
||||
// User to select download folder from download modal to ensure permissions
|
||||
this.$store.commit('downloads/setShowModal', true)
|
||||
// this.$store.commit('downloads/setShowModal', true)
|
||||
this.changeDownloadFolderClick()
|
||||
return
|
||||
} else {
|
||||
console.log('Has Download folder: ' + JSON.stringify(dlFolder))
|
||||
}
|
||||
|
||||
var downloadObject = {
|
||||
|
|
|
@ -29,7 +29,7 @@ export default {
|
|||
.map((b) => ({ ...b }))
|
||||
.filter((b) => b.userAbData && !b.userAbData.isRead && b.userAbData.progress > 0)
|
||||
.sort((a, b) => {
|
||||
return a.userAbData.lastUpdate - b.userAbData.lastUpdate
|
||||
return b.userAbData.lastUpdate - a.userAbData.lastUpdate
|
||||
})
|
||||
return books
|
||||
},
|
||||
|
@ -38,14 +38,14 @@ export default {
|
|||
.map((b) => {
|
||||
return { ...b }
|
||||
})
|
||||
.sort((a, b) => a.addedAt - b.addedAt)
|
||||
.sort((a, b) => b.addedAt - a.addedAt)
|
||||
return books.slice(0, 10)
|
||||
},
|
||||
booksRead() {
|
||||
var books = this.booksWithUserAbData
|
||||
.filter((b) => b.userAbData && b.userAbData.isRead)
|
||||
.sort((a, b) => {
|
||||
return a.userAbData.lastUpdate - b.userAbData.lastUpdate
|
||||
return b.userAbData.lastUpdate - a.userAbData.lastUpdate
|
||||
})
|
||||
return books.slice(0, 10)
|
||||
},
|
||||
|
|
230
plugins/localStore.js
Normal file
230
plugins/localStore.js
Normal file
|
@ -0,0 +1,230 @@
|
|||
import { Storage } from '@capacitor/storage'
|
||||
|
||||
class LocalStorage {
|
||||
constructor(vuexStore) {
|
||||
this.vuexStore = vuexStore
|
||||
|
||||
this.userAudiobooksLoaded = false
|
||||
this.downloadFolder = null
|
||||
this.userAudiobooks = {}
|
||||
}
|
||||
|
||||
async getMostRecentUserAudiobook(audiobookId) {
|
||||
if (!this.userAudiobooksLoaded) {
|
||||
await this.loadUserAudiobooks()
|
||||
}
|
||||
var local = this.getUserAudiobook(audiobookId)
|
||||
var server = this.vuexStore.getters['user/getUserAudiobook'](audiobookId)
|
||||
|
||||
if (local && server) {
|
||||
if (local.lastUpdate > server.lastUpdate) {
|
||||
console.log('[LocalStorage] Most recent user audiobook is from LOCAL')
|
||||
return local
|
||||
}
|
||||
console.log('[LocalStorage] Most recent user audiobook is from SERVER')
|
||||
return server
|
||||
} else if (local) {
|
||||
console.log('[LocalStorage] Most recent user audiobook is from LOCAL')
|
||||
return local
|
||||
} else if (server) {
|
||||
console.log('[LocalStorage] Most recent user audiobook is from SERVER')
|
||||
return server
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async loadUserAudiobooks() {
|
||||
try {
|
||||
var val = (await Storage.get({ key: 'userAudiobooks' }) || {}).value || null
|
||||
this.userAudiobooks = val ? JSON.parse(val) : {}
|
||||
this.userAudiobooksLoaded = true
|
||||
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to load user audiobooks', error)
|
||||
}
|
||||
}
|
||||
|
||||
async saveUserAudiobooks() {
|
||||
try {
|
||||
await Storage.set({ key: 'userAudiobooks', value: JSON.stringify(this.userAudiobooks) })
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to set user audiobooks', error)
|
||||
}
|
||||
}
|
||||
|
||||
async setAllAudiobookProgress(progresses) {
|
||||
this.userAudiobooks = progresses
|
||||
await this.saveUserAudiobooks()
|
||||
|
||||
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
||||
}
|
||||
|
||||
async updateUserAudiobookData(progressPayload) {
|
||||
this.userAudiobooks[progressPayload.audiobookId] = {
|
||||
...progressPayload
|
||||
}
|
||||
await this.saveUserAudiobooks()
|
||||
|
||||
this.vuexStore.commit('user/setUserAudiobooks', { ...this.userAudiobooks })
|
||||
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
||||
}
|
||||
|
||||
async removeAudiobookProgress(audiobookId) {
|
||||
if (!this.userAudiobooks[audiobookId]) return
|
||||
delete this.userAudiobooks[audiobookId]
|
||||
await this.saveUserAudiobooks()
|
||||
|
||||
this.vuexStore.commit('user/setUserAudiobooks', { ...this.userAudiobooks })
|
||||
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
||||
}
|
||||
|
||||
getUserAudiobook(audiobookId) {
|
||||
return this.userAudiobooks[audiobookId] || null
|
||||
}
|
||||
|
||||
async setToken(token) {
|
||||
try {
|
||||
if (token) {
|
||||
await Storage.set({ key: 'token', value: token })
|
||||
} else {
|
||||
await Storage.remove({ key: 'token' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to set token', error)
|
||||
}
|
||||
}
|
||||
|
||||
async getToken() {
|
||||
try {
|
||||
return (await Storage.get({ key: 'token' }) || {}).value || null
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get token', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async setCurrentLibrary(library) {
|
||||
try {
|
||||
if (library) {
|
||||
await Storage.set({ key: 'library', value: JSON.stringify(library) })
|
||||
} else {
|
||||
await Storage.remove({ key: 'library' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to set library', error)
|
||||
}
|
||||
}
|
||||
|
||||
async getCurrentLibrary() {
|
||||
try {
|
||||
var _value = (await Storage.get({ key: 'library' }) || {}).value || null
|
||||
if (!_value) return null
|
||||
return JSON.parse(_value)
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get current library', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async setDownloadFolder(folderObj) {
|
||||
try {
|
||||
if (folderObj) {
|
||||
await Storage.set({ key: 'downloadFolder', value: JSON.stringify(folderObj) })
|
||||
this.downloadFolder = folderObj
|
||||
this.vuexStore.commit('setDownloadFolder', { ...this.downloadFolder })
|
||||
} else {
|
||||
await Storage.remove({ key: 'downloadFolder' })
|
||||
this.downloadFolder = null
|
||||
this.vuexStore.commit('setDownloadFolder', null)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to set download folder', error)
|
||||
}
|
||||
}
|
||||
|
||||
async getDownloadFolder() {
|
||||
try {
|
||||
var _value = (await Storage.get({ key: 'downloadFolder' }) || {}).value || null
|
||||
if (!_value) return null
|
||||
this.downloadFolder = JSON.parse(_value)
|
||||
this.vuexStore.commit('setDownloadFolder', { ...this.downloadFolder })
|
||||
return this.downloadFolder
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get download folder', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async getServerUrl() {
|
||||
try {
|
||||
return (await Storage.get({ key: 'serverUrl' }) || {}).value || null
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get serverUrl', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async setUserSettings(settings) {
|
||||
try {
|
||||
await Storage.set({ key: 'userSettings', value: JSON.stringify(settings) })
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to update user settings', error)
|
||||
}
|
||||
}
|
||||
|
||||
async getUserSettings() {
|
||||
try {
|
||||
var settingsObj = await Storage.get({ key: 'userSettings' }) || {}
|
||||
return settingsObj.value ? JSON.parse(settingsObj.value) : null
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get user settings', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async setCurrent(current) {
|
||||
try {
|
||||
if (current) {
|
||||
await Storage.set({ key: 'current', value: JSON.stringify(current) })
|
||||
} else {
|
||||
await Storage.remove({ key: 'current' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to set current', error)
|
||||
}
|
||||
}
|
||||
|
||||
async getCurrent() {
|
||||
try {
|
||||
var currentObj = await Storage.get({ key: 'current' }) || {}
|
||||
return currentObj.value ? JSON.parse(currentObj.value) : null
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get current', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async setBookshelfView(view) {
|
||||
try {
|
||||
await Storage.set({ key: 'bookshelfView', value: view })
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to set bookshelf view', error)
|
||||
}
|
||||
}
|
||||
|
||||
async getBookshelfView() {
|
||||
try {
|
||||
var view = await Storage.get({ key: 'bookshelfView' }) || {}
|
||||
return view.value || null
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get bookshelf view', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default ({ app, store }, inject) => {
|
||||
inject('localStore', new LocalStorage(store))
|
||||
}
|
439
plugins/sqlStore.js
Normal file
439
plugins/sqlStore.js
Normal file
|
@ -0,0 +1,439 @@
|
|||
import { Capacitor } from '@capacitor/core';
|
||||
import { CapacitorDataStorageSqlite } from 'capacitor-data-storage-sqlite';
|
||||
|
||||
class StoreService {
|
||||
store
|
||||
platform
|
||||
isOpen = false
|
||||
|
||||
constructor(vuexStore) {
|
||||
this.vuexStore = vuexStore
|
||||
this.currentTable = null
|
||||
this.init()
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin Initialization
|
||||
*/
|
||||
init() {
|
||||
this.platform = Capacitor.getPlatform()
|
||||
this.store = CapacitorDataStorageSqlite
|
||||
console.log('in init ', this.platform)
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a Store
|
||||
* @param _dbName string optional
|
||||
* @param _table string optional
|
||||
* @param _encrypted boolean optional
|
||||
* @param _mode string optional
|
||||
*/
|
||||
async openStore(_dbName, _table, _encrypted, _mode) {
|
||||
if (this.store != null) {
|
||||
const database = _dbName ? _dbName : "storage"
|
||||
const table = _table ? _table : "storage_table"
|
||||
const encrypted = _encrypted ? _encrypted : false
|
||||
const mode = _mode ? _mode : "no-encryption"
|
||||
|
||||
this.isOpen = false
|
||||
try {
|
||||
await this.store.openStore({ database, table, encrypted, mode })
|
||||
// return Promise.resolve()
|
||||
this.currentTable = table
|
||||
this.isOpen = true
|
||||
return true
|
||||
} catch (err) {
|
||||
// return Promise.reject(err)
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// return Promise.reject(new Error("openStore: Store not opened"))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a store
|
||||
* @param dbName
|
||||
* @returns
|
||||
*/
|
||||
async closeStore(dbName) {
|
||||
if (this.store != null) {
|
||||
try {
|
||||
await this.store.closeStore({ database: dbName })
|
||||
return Promise.resolve()
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("close: Store not opened"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a store is opened
|
||||
* @param dbName
|
||||
* @returns
|
||||
*/
|
||||
async isStoreOpen(dbName) {
|
||||
if (this.store != null) {
|
||||
try {
|
||||
const ret = await this.store.isStoreOpen({ database: dbName })
|
||||
return Promise.resolve(ret)
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("isStoreOpen: Store not opened"))
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Check if a store already exists
|
||||
* @param dbName
|
||||
* @returns
|
||||
*/
|
||||
async isStoreExists(dbName) {
|
||||
if (this.store != null) {
|
||||
try {
|
||||
const ret = await this.store.isStoreExists({ database: dbName })
|
||||
return Promise.resolve(ret)
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("isStoreExists: Store not opened"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create/Set a Table
|
||||
* @param table string
|
||||
*/
|
||||
async setTable(table) {
|
||||
if (this.store != null) {
|
||||
try {
|
||||
await this.store.setTable({ table })
|
||||
this.currentTable = table
|
||||
return Promise.resolve()
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("setTable: Store not opened"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of Key
|
||||
* @param key string
|
||||
* @param value string
|
||||
*/
|
||||
async setItem(key, value) {
|
||||
if (this.store != null) {
|
||||
if (key.length > 0) {
|
||||
try {
|
||||
await this.store.set({ key, value });
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("setItem: Must give a key"));
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("setItem: Store not opened"));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the Value for a given Key
|
||||
* @param key string
|
||||
*/
|
||||
async getItem(key) {
|
||||
if (this.store != null) {
|
||||
if (key.length > 0) {
|
||||
try {
|
||||
const { value } = await this.store.get({ key });
|
||||
console.log("in getItem value ", value)
|
||||
return Promise.resolve(value);
|
||||
} catch (err) {
|
||||
console.error(`in getItem key: ${key} err: ${JSON.stringify(err)}`)
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("getItem: Must give a key"));
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("getItem: Store not opened"));
|
||||
}
|
||||
|
||||
}
|
||||
async isKey(key) {
|
||||
if (this.store != null) {
|
||||
if (key.length > 0) {
|
||||
try {
|
||||
const { result } = await this.store.iskey({ key });
|
||||
return Promise.resolve(result);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("isKey: Must give a key"));
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("isKey: Store not opened"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async getAllKeysValues() {
|
||||
if (this.store != null) {
|
||||
try {
|
||||
const { keysvalues } = await this.store.keysvalues();
|
||||
return Promise.resolve(keysvalues);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("getAllKeysValues: Store not opened"));
|
||||
}
|
||||
}
|
||||
async removeItem(key) {
|
||||
if (this.store != null) {
|
||||
if (key.length > 0) {
|
||||
try {
|
||||
await this.store.remove({ key });
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("removeItem: Must give a key"));
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("removeItem: Store not opened"));
|
||||
}
|
||||
}
|
||||
async clear() {
|
||||
if (this.store != null) {
|
||||
try {
|
||||
await this.store.clear()
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('[SqlStore] Failed to clear table', err.message)
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
console.error('[SqlStore] Clear: Store not opened')
|
||||
return false
|
||||
}
|
||||
}
|
||||
async deleteStore(_dbName) {
|
||||
const database = _dbName ? _dbName : "storage"
|
||||
|
||||
if (this.store != null) {
|
||||
try {
|
||||
await this.store.deleteStore({ database })
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
return Promise.reject(err.message)
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("deleteStore: Store not opened"));
|
||||
}
|
||||
}
|
||||
async isTable(table) {
|
||||
if (this.store != null) {
|
||||
if (table.length > 0) {
|
||||
try {
|
||||
const { result } = await this.store.isTable({ table });
|
||||
return Promise.resolve(result);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("isTable: Must give a table"));
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("isTable: Store not opened"));
|
||||
}
|
||||
}
|
||||
async getAllTables() {
|
||||
if (this.store != null) {
|
||||
try {
|
||||
const { tables } = await this.store.tables();
|
||||
return Promise.resolve(tables);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("getAllTables: Store not opened"));
|
||||
}
|
||||
}
|
||||
|
||||
async ensureTable(tablename) {
|
||||
if (!this.isOpen) {
|
||||
var success = await this.openStore('storage', tablename)
|
||||
if (!success) {
|
||||
console.error('Store failed to open')
|
||||
return false
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.setTable(tablename)
|
||||
console.log('[SqlStore] Set Table ' + this.currentTable)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to set table', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async setDownload(download) {
|
||||
if (!download) return false
|
||||
if (!(await this.ensureTable('downloads'))) {
|
||||
return false
|
||||
}
|
||||
if (!download.id) {
|
||||
console.error(`[SqlStore] set download invalid download ${download ? JSON.stringify(download) : 'null'}`)
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
await this.setItem(download.id, JSON.stringify(download))
|
||||
console.log(`[STORE] Set Download ${download.id}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to set download in store', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async removeDownload(id) {
|
||||
if (!id) return false
|
||||
if (!(await this.ensureTable('downloads'))) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
await this.removeItem(id)
|
||||
console.log(`[STORE] Removed download ${id}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to remove download in store', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async getAllDownloads() {
|
||||
if (!(await this.ensureTable('downloads'))) {
|
||||
return false
|
||||
}
|
||||
|
||||
var keysvalues = await this.getAllKeysValues()
|
||||
var downloads = []
|
||||
|
||||
for (let i = 0; i < keysvalues.length; i++) {
|
||||
try {
|
||||
var download = JSON.parse(keysvalues[i].value)
|
||||
if (!download.id) {
|
||||
console.error('[SqlStore] Removing invalid download')
|
||||
await this.removeItem(keysvalues[i].key)
|
||||
} else {
|
||||
downloads.push(download)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse download', error)
|
||||
await this.removeItem(keysvalues[i].key)
|
||||
}
|
||||
}
|
||||
|
||||
return downloads
|
||||
}
|
||||
|
||||
async setUserAudiobookData(userAudiobookData) {
|
||||
if (!(await this.ensureTable('userAudiobookData'))) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
await this.setItem(userAudiobookData.audiobookId, JSON.stringify(userAudiobookData))
|
||||
this.vuexStore.commit('user/setUserAudiobookData', userAudiobookData)
|
||||
|
||||
console.log(`[STORE] Set UserAudiobookData ${userAudiobookData.audiobookId}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to set UserAudiobookData in store', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async removeUserAudiobookData(audiobookId) {
|
||||
if (!(await this.ensureTable('userAudiobookData'))) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
await this.removeItem(audiobookId)
|
||||
this.vuexStore.commit('user/removeUserAudiobookData', audiobookId)
|
||||
|
||||
console.log(`[STORE] Removed userAudiobookData ${id}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to remove userAudiobookData in store', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async getAllUserAudiobookData() {
|
||||
if (!(await this.ensureTable('userAudiobookData'))) {
|
||||
return false
|
||||
}
|
||||
|
||||
var keysvalues = await this.getAllKeysValues()
|
||||
var data = []
|
||||
|
||||
for (let i = 0; i < keysvalues.length; i++) {
|
||||
try {
|
||||
var abdata = JSON.parse(keysvalues[i].value)
|
||||
if (!abdata.audiobookId) {
|
||||
console.error('[SqlStore] Removing invalid user audiobook data')
|
||||
await this.removeItem(keysvalues[i].key)
|
||||
} else {
|
||||
data.push(abdata)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse userAudiobookData', error)
|
||||
await this.removeItem(keysvalues[i].key)
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
async setAllUserAudiobookData(userAbData) {
|
||||
if (!(await this.ensureTable('userAudiobookData'))) {
|
||||
return false
|
||||
}
|
||||
|
||||
console.log('[SqlStore] Setting all user audiobook data ' + userAbData.length)
|
||||
|
||||
var success = await this.clear()
|
||||
if (!success) {
|
||||
console.error('[SqlStore] Did not clear old user ab data, overwriting')
|
||||
}
|
||||
|
||||
for (let i = 0; i < userAbData.length; i++) {
|
||||
try {
|
||||
var abdata = userAbData[i]
|
||||
await this.setItem(abdata.audiobookId, JSON.stringify(abdata))
|
||||
} catch (error) {
|
||||
console.error('[SqlStore] Failed to set userAudiobookData', error)
|
||||
}
|
||||
}
|
||||
|
||||
this.vuexStore.commit('user/setAllUserAudiobookData', userAbData)
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ app, store }, inject) => {
|
||||
inject('sqlStore', new StoreService(store))
|
||||
}
|
689
plugins/store.js
689
plugins/store.js
|
@ -1,689 +0,0 @@
|
|||
import { Capacitor } from '@capacitor/core';
|
||||
import { CapacitorDataStorageSqlite } from 'capacitor-data-storage-sqlite';
|
||||
import { Storage } from '@capacitor/storage'
|
||||
|
||||
class StoreService {
|
||||
store
|
||||
isService = false
|
||||
platform
|
||||
isOpen = false
|
||||
|
||||
constructor() {
|
||||
this.init()
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin Initialization
|
||||
*/
|
||||
init() {
|
||||
this.platform = Capacitor.getPlatform()
|
||||
this.store = CapacitorDataStorageSqlite
|
||||
this.isService = true
|
||||
console.log('in init ', this.platform, this.isService)
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a Store
|
||||
* @param _dbName string optional
|
||||
* @param _table string optional
|
||||
* @param _encrypted boolean optional
|
||||
* @param _mode string optional
|
||||
*/
|
||||
async openStore(_dbName, _table, _encrypted, _mode) {
|
||||
if (this.isService && this.store != null) {
|
||||
const database = _dbName ? _dbName : "storage"
|
||||
const table = _table ? _table : "storage_table"
|
||||
const encrypted = _encrypted ? _encrypted : false
|
||||
const mode = _mode ? _mode : "no-encryption"
|
||||
|
||||
this.isOpen = false
|
||||
try {
|
||||
await this.store.openStore({ database, table, encrypted, mode })
|
||||
// return Promise.resolve()
|
||||
this.isOpen = true
|
||||
return true
|
||||
} catch (err) {
|
||||
// return Promise.reject(err)
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// return Promise.reject(new Error("openStore: Store not opened"))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a store
|
||||
* @param dbName
|
||||
* @returns
|
||||
*/
|
||||
async closeStore(dbName) {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
await this.store.closeStore({ database: dbName })
|
||||
return Promise.resolve()
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("close: Store not opened"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a store is opened
|
||||
* @param dbName
|
||||
* @returns
|
||||
*/
|
||||
async isStoreOpen(dbName) {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
const ret = await this.store.isStoreOpen({ database: dbName })
|
||||
return Promise.resolve(ret)
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("isStoreOpen: Store not opened"))
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Check if a store already exists
|
||||
* @param dbName
|
||||
* @returns
|
||||
*/
|
||||
async isStoreExists(dbName) {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
const ret = await this.store.isStoreExists({ database: dbName })
|
||||
return Promise.resolve(ret)
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("isStoreExists: Store not opened"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create/Set a Table
|
||||
* @param table string
|
||||
*/
|
||||
async setTable(table) {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
await this.store.setTable({ table })
|
||||
return Promise.resolve()
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("setTable: Store not opened"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of Key
|
||||
* @param key string
|
||||
* @param value string
|
||||
*/
|
||||
async setItem(key, value) {
|
||||
if (this.isService && this.store != null) {
|
||||
if (key.length > 0) {
|
||||
try {
|
||||
await this.store.set({ key, value });
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("setItem: Must give a key"));
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("setItem: Store not opened"));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the Value for a given Key
|
||||
* @param key string
|
||||
*/
|
||||
async getItem(key) {
|
||||
if (this.isService && this.store != null) {
|
||||
if (key.length > 0) {
|
||||
try {
|
||||
const { value } = await this.store.get({ key });
|
||||
console.log("in getItem value ", value)
|
||||
return Promise.resolve(value);
|
||||
} catch (err) {
|
||||
console.error(`in getItem key: ${key} err: ${JSON.stringify(err)}`)
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("getItem: Must give a key"));
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("getItem: Store not opened"));
|
||||
}
|
||||
|
||||
}
|
||||
async isKey(key) {
|
||||
if (this.isService && this.store != null) {
|
||||
if (key.length > 0) {
|
||||
try {
|
||||
const { result } = await this.store.iskey({ key });
|
||||
return Promise.resolve(result);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("isKey: Must give a key"));
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("isKey: Store not opened"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async getAllKeys() {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
const { keys } = await this.store.keys();
|
||||
return Promise.resolve(keys);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("getAllKeys: Store not opened"));
|
||||
}
|
||||
}
|
||||
async getAllValues() {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
const { values } = await this.store.values();
|
||||
return Promise.resolve(values);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("getAllValues: Store not opened"));
|
||||
}
|
||||
}
|
||||
async getFilterValues(filter) {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
const { values } = await this.store.filtervalues({ filter });
|
||||
return Promise.resolve(values);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("getFilterValues: Store not opened"));
|
||||
}
|
||||
}
|
||||
async getAllKeysValues() {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
const { keysvalues } = await this.store.keysvalues();
|
||||
return Promise.resolve(keysvalues);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("getAllKeysValues: Store not opened"));
|
||||
}
|
||||
}
|
||||
|
||||
async removeItem(key) {
|
||||
if (this.isService && this.store != null) {
|
||||
if (key.length > 0) {
|
||||
try {
|
||||
await this.store.remove({ key });
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("removeItem: Must give a key"));
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("removeItem: Store not opened"));
|
||||
}
|
||||
}
|
||||
|
||||
async clear() {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
await this.store.clear();
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
return Promise.reject(err.message);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("clear: Store not opened"));
|
||||
}
|
||||
}
|
||||
|
||||
async deleteStore(_dbName) {
|
||||
const database = _dbName ? _dbName : "storage"
|
||||
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
await this.store.deleteStore({ database })
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
return Promise.reject(err.message)
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("deleteStore: Store not opened"));
|
||||
}
|
||||
}
|
||||
async isTable(table) {
|
||||
if (this.isService && this.store != null) {
|
||||
if (table.length > 0) {
|
||||
try {
|
||||
const { result } = await this.store.isTable({ table });
|
||||
return Promise.resolve(result);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("isTable: Must give a table"));
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("isTable: Store not opened"));
|
||||
}
|
||||
}
|
||||
async getAllTables() {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
const { tables } = await this.store.tables();
|
||||
return Promise.resolve(tables);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("getAllTables: Store not opened"));
|
||||
}
|
||||
}
|
||||
async deleteTable(table) {
|
||||
if (this.isService && this.store != null) {
|
||||
if (table.length > 0) {
|
||||
try {
|
||||
await this.store.deleteTable({ table });
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("deleteTable: Must give a table"));
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("deleteTable: Store not opened"));
|
||||
}
|
||||
}
|
||||
|
||||
async setServerConfig(config) {
|
||||
if (!this.isOpen) {
|
||||
var success = await this.openStore('storage', 'serverConfig')
|
||||
if (!success) {
|
||||
console.error('Store failed to open')
|
||||
return false
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.setTable('serverConfig')
|
||||
} catch (error) {
|
||||
console.error('Failed to set table', error)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.setItem('config', JSON.stringify(config))
|
||||
console.log(`[STORE] Set Server Config`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to set server config in store', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async getServerConfig() {
|
||||
if (!this.isOpen) {
|
||||
var success = await this.openStore('storage', 'serverConfig')
|
||||
if (!success) {
|
||||
console.error('Store failed to open')
|
||||
return false
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.setTable('serverConfig')
|
||||
} catch (error) {
|
||||
console.error('Failed to set table', error)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
var configVal = await this.getItem('config')
|
||||
if (!configVal) {
|
||||
console.log(`[STORE] server config not available`)
|
||||
return null
|
||||
}
|
||||
var config = JSON.parse(configVal)
|
||||
console.log(`[STORE] Got Server Config`, JSON.stringify(config))
|
||||
return config
|
||||
} catch (error) {
|
||||
console.error('Failed to set server config in store', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async setDownload(download) {
|
||||
if (!this.isOpen) {
|
||||
var success = await this.openStore('storage', 'downloads')
|
||||
if (!success) {
|
||||
console.error('Store failed to open')
|
||||
return false
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.setTable('downloads')
|
||||
} catch (error) {
|
||||
console.error('Failed to set table', error)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.setItem(download.id, JSON.stringify(download))
|
||||
console.log(`[STORE] Set Download ${download.id}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to set download in store', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async removeDownload(id) {
|
||||
if (!this.isOpen) {
|
||||
var success = await this.openStore('storage', 'downloads')
|
||||
if (!success) {
|
||||
console.error('Store failed to open')
|
||||
return false
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.setTable('downloads')
|
||||
} catch (error) {
|
||||
console.error('Failed to set table', error)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.removeItem(id)
|
||||
console.log(`[STORE] Removed download ${id}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to remove download in store', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async getAllDownloads() {
|
||||
if (!this.isOpen) {
|
||||
var success = await this.openStore('storage', 'downloads')
|
||||
if (!success) {
|
||||
console.error('Store failed to open')
|
||||
return []
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.setTable('downloads')
|
||||
} catch (error) {
|
||||
console.error('Failed to set table', error)
|
||||
return
|
||||
}
|
||||
|
||||
var keysvalues = await this.getAllKeysValues()
|
||||
var downloads = []
|
||||
|
||||
for (let i = 0; i < keysvalues.length; i++) {
|
||||
try {
|
||||
var download = JSON.parse(keysvalues[i].value)
|
||||
downloads.push(download)
|
||||
} catch (error) {
|
||||
console.error('Failed to parse download', error)
|
||||
await this.removeItem(keysvalues[i].key)
|
||||
}
|
||||
}
|
||||
|
||||
return downloads
|
||||
}
|
||||
}
|
||||
|
||||
class LocalStorage {
|
||||
constructor(vuexStore) {
|
||||
this.vuexStore = vuexStore
|
||||
|
||||
this.userAudiobooksLoaded = false
|
||||
this.downloadFolder = null
|
||||
this.userAudiobooks = {}
|
||||
}
|
||||
|
||||
async getMostRecentUserAudiobook(audiobookId) {
|
||||
if (!this.userAudiobooksLoaded) {
|
||||
await this.loadUserAudiobooks()
|
||||
}
|
||||
var local = this.getUserAudiobook(audiobookId)
|
||||
var server = this.vuexStore.getters['user/getUserAudiobook'](audiobookId)
|
||||
|
||||
if (local && server) {
|
||||
if (local.lastUpdate > server.lastUpdate) {
|
||||
console.log('[LocalStorage] Most recent user audiobook is from LOCAL')
|
||||
return local
|
||||
}
|
||||
console.log('[LocalStorage] Most recent user audiobook is from SERVER')
|
||||
return server
|
||||
} else if (local) {
|
||||
console.log('[LocalStorage] Most recent user audiobook is from LOCAL')
|
||||
return local
|
||||
} else if (server) {
|
||||
console.log('[LocalStorage] Most recent user audiobook is from SERVER')
|
||||
return server
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async loadUserAudiobooks() {
|
||||
try {
|
||||
var val = (await Storage.get({ key: 'userAudiobooks' }) || {}).value || null
|
||||
this.userAudiobooks = val ? JSON.parse(val) : {}
|
||||
this.userAudiobooksLoaded = true
|
||||
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to load user audiobooks', error)
|
||||
}
|
||||
}
|
||||
|
||||
async saveUserAudiobooks() {
|
||||
try {
|
||||
await Storage.set({ key: 'userAudiobooks', value: JSON.stringify(this.userAudiobooks) })
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to set user audiobooks', error)
|
||||
}
|
||||
}
|
||||
|
||||
async setAllAudiobookProgress(progresses) {
|
||||
this.userAudiobooks = progresses
|
||||
await this.saveUserAudiobooks()
|
||||
|
||||
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
||||
}
|
||||
|
||||
async updateUserAudiobookData(progressPayload) {
|
||||
this.userAudiobooks[progressPayload.audiobookId] = {
|
||||
...progressPayload
|
||||
}
|
||||
await this.saveUserAudiobooks()
|
||||
|
||||
this.vuexStore.commit('user/setUserAudiobooks', { ...this.userAudiobooks })
|
||||
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
||||
}
|
||||
|
||||
async removeAudiobookProgress(audiobookId) {
|
||||
if (!this.userAudiobooks[audiobookId]) return
|
||||
delete this.userAudiobooks[audiobookId]
|
||||
await this.saveUserAudiobooks()
|
||||
|
||||
this.vuexStore.commit('user/setUserAudiobooks', { ...this.userAudiobooks })
|
||||
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
||||
}
|
||||
|
||||
getUserAudiobook(audiobookId) {
|
||||
return this.userAudiobooks[audiobookId] || null
|
||||
}
|
||||
|
||||
async setToken(token) {
|
||||
try {
|
||||
if (token) {
|
||||
await Storage.set({ key: 'token', value: token })
|
||||
} else {
|
||||
await Storage.remove({ key: 'token' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to set token', error)
|
||||
}
|
||||
}
|
||||
|
||||
async getToken() {
|
||||
try {
|
||||
return (await Storage.get({ key: 'token' }) || {}).value || null
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get token', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async setCurrentLibrary(library) {
|
||||
try {
|
||||
if (library) {
|
||||
await Storage.set({ key: 'library', value: JSON.stringify(library) })
|
||||
} else {
|
||||
await Storage.remove({ key: 'library' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to set library', error)
|
||||
}
|
||||
}
|
||||
|
||||
async getCurrentLibrary() {
|
||||
try {
|
||||
var _value = (await Storage.get({ key: 'library' }) || {}).value || null
|
||||
if (!_value) return null
|
||||
return JSON.parse(_value)
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get current library', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async setDownloadFolder(folderObj) {
|
||||
try {
|
||||
if (folderObj) {
|
||||
await Storage.set({ key: 'downloadFolder', value: JSON.stringify(folderObj) })
|
||||
this.downloadFolder = folderObj
|
||||
this.vuexStore.commit('setDownloadFolder', { ...this.downloadFolder })
|
||||
} else {
|
||||
await Storage.remove({ key: 'downloadFolder' })
|
||||
this.downloadFolder = null
|
||||
this.vuexStore.commit('setDownloadFolder', null)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to set download folder', error)
|
||||
}
|
||||
}
|
||||
|
||||
async getDownloadFolder() {
|
||||
try {
|
||||
var _value = (await Storage.get({ key: 'downloadFolder' }) || {}).value || null
|
||||
if (!_value) return null
|
||||
this.downloadFolder = JSON.parse(_value)
|
||||
this.vuexStore.commit('setDownloadFolder', { ...this.downloadFolder })
|
||||
return this.downloadFolder
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get download folder', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async getServerUrl() {
|
||||
try {
|
||||
return (await Storage.get({ key: 'serverUrl' }) || {}).value || null
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get serverUrl', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async setUserSettings(settings) {
|
||||
try {
|
||||
await Storage.set({ key: 'userSettings', value: JSON.stringify(settings) })
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to update user settings', error)
|
||||
}
|
||||
}
|
||||
|
||||
async getUserSettings() {
|
||||
try {
|
||||
var settingsObj = await Storage.get({ key: 'userSettings' }) || {}
|
||||
return settingsObj.value ? JSON.parse(settingsObj.value) : null
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get user settings', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async setCurrent(current) {
|
||||
try {
|
||||
if (current) {
|
||||
await Storage.set({ key: 'current', value: JSON.stringify(current) })
|
||||
} else {
|
||||
await Storage.remove({ key: 'current' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to set current', error)
|
||||
}
|
||||
}
|
||||
|
||||
async getCurrent() {
|
||||
try {
|
||||
var currentObj = await Storage.get({ key: 'current' }) || {}
|
||||
return currentObj.value ? JSON.parse(currentObj.value) : null
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get current', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async setBookshelfView(view) {
|
||||
try {
|
||||
await Storage.set({ key: 'bookshelfView', value: view })
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to set bookshelf view', error)
|
||||
}
|
||||
}
|
||||
|
||||
async getBookshelfView() {
|
||||
try {
|
||||
var view = await Storage.get({ key: 'bookshelfView' }) || {}
|
||||
return view.value || null
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get bookshelf view', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ app, store }, inject) => {
|
||||
inject('sqlStore', new StoreService())
|
||||
inject('localStore', new LocalStorage(store))
|
||||
}
|
|
@ -36,6 +36,9 @@ export const mutations = {
|
|||
state.showModal = val
|
||||
},
|
||||
setDownload(state, download) {
|
||||
if (!download || !download.id) {
|
||||
return
|
||||
}
|
||||
var index = state.downloads.findIndex(d => d.id === download.id)
|
||||
if (index >= 0) {
|
||||
state.downloads.splice(index, 1, download)
|
||||
|
@ -44,6 +47,9 @@ export const mutations = {
|
|||
}
|
||||
},
|
||||
addUpdateDownload(state, download) {
|
||||
if (!download || !download.id) {
|
||||
return
|
||||
}
|
||||
var index = state.downloads.findIndex(d => d.id === download.id)
|
||||
if (index >= 0) {
|
||||
state.downloads.splice(index, 1, download)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export const state = () => ({
|
||||
user: null,
|
||||
userAudiobookData: [],
|
||||
localUserAudiobooks: {},
|
||||
settings: {
|
||||
mobileOrderBy: 'recent',
|
||||
|
@ -29,11 +30,12 @@ export const getters = {
|
|||
return state.localUserAudiobooks ? state.localUserAudiobooks[audiobookId] || null : null
|
||||
},
|
||||
getMostRecentUserAudiobookData: (state, getters) => (audiobookId) => {
|
||||
var userAb = getters.getUserAudiobook(audiobookId)
|
||||
var localUserAb = getters.getLocalUserAudiobook(audiobookId)
|
||||
if (!localUserAb) return userAb
|
||||
if (!userAb) return localUserAb
|
||||
return localUserAb.lastUpdate > userAb.lastUpdate ? localUserAb : userAb
|
||||
return state.userAudiobookData.find(uabd => uabd.audiobookId === audiobookId)
|
||||
// var userAb = getters.getUserAudiobook(audiobookId)
|
||||
// var localUserAb = getters.getLocalUserAudiobook(audiobookId)
|
||||
// if (!localUserAb) return userAb
|
||||
// if (!userAb) return localUserAb
|
||||
// return localUserAb.lastUpdate > userAb.lastUpdate ? localUserAb : userAb
|
||||
},
|
||||
getUserSetting: (state) => (key) => {
|
||||
return state.settings ? state.settings[key] || null : null
|
||||
|
@ -85,15 +87,55 @@ export const actions = {
|
|||
console.error('Failed to get collections', error)
|
||||
return []
|
||||
})
|
||||
},
|
||||
async syncUserAudiobookData({ state, commit }) {
|
||||
if (!state.user) {
|
||||
console.error('Sync user audiobook data invalid no user')
|
||||
return
|
||||
}
|
||||
var localUserAudiobookData = await this.$sqlStore.getAllUserAudiobookData() || []
|
||||
this.$axios.$post(`/api/syncUserAudiobookData`, { data: localUserAudiobookData }).then(async (abData) => {
|
||||
console.log('Synced user audiobook data', abData)
|
||||
await this.$sqlStore.setAllUserAudiobookData(abData)
|
||||
}).catch((error) => {
|
||||
console.error('Failed to sync user ab data', error)
|
||||
})
|
||||
},
|
||||
async updateUserAudiobookData({ state, commit }, uabdUpdate) {
|
||||
var userAbData = state.userAudiobookData.find(uab => uab.audiobookId === uabdUpdate.audiobookId)
|
||||
if (!userAbData) {
|
||||
uabdUpdate.startedAt = Date.now()
|
||||
this.$sqlStore.setUserAudiobookData(uabdUpdate)
|
||||
} else {
|
||||
var mergedUabData = { ...userAbData }
|
||||
for (const key in uabdUpdate) {
|
||||
mergedUabData[key] = uabdUpdate[key]
|
||||
}
|
||||
this.$sqlStore.setUserAudiobookData(mergedUabData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
setUserAudiobookData(state, abdata) {
|
||||
var index = state.userAudiobookData.findIndex(uab => uab.audiobookId === abdata.audiobookId)
|
||||
if (index >= 0) {
|
||||
state.userAudiobookData.splice(index, 1, abdata)
|
||||
} else {
|
||||
state.userAudiobookData.push(abdata)
|
||||
}
|
||||
},
|
||||
removeUserAudiobookData(state, audiobookId) {
|
||||
state.userAudiobookData = state.userAudiobookData.filter(uab => uab.audiobookId !== audiobookId)
|
||||
},
|
||||
setAllUserAudiobookData(state, allAbData) {
|
||||
state.userAudiobookData = allAbData
|
||||
},
|
||||
setLocalUserAudiobooks(state, userAudiobooks) {
|
||||
state.localUserAudiobooks = userAudiobooks
|
||||
state.userAudiobooksListeners.forEach((listener) => {
|
||||
listener.meth()
|
||||
})
|
||||
// state.localUserAudiobooks = userAudiobooks
|
||||
// state.userAudiobooksListeners.forEach((listener) => {
|
||||
// listener.meth()
|
||||
// })
|
||||
},
|
||||
setUserAudiobooks(state, userAudiobooks) {
|
||||
if (!state.user) return
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue