mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-06 20:14:48 +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"
|
applicationId "com.audiobookshelf.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 40
|
versionCode 41
|
||||||
versionName "0.9.21-beta"
|
versionName "0.9.22-beta"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|
|
@ -115,17 +115,19 @@ class AudiobookManager {
|
||||||
Log.d(tag, "keyvalue ${it.getString("key")} | ${it.getString("value")}")
|
Log.d(tag, "keyvalue ${it.getString("key")} | ${it.getString("value")}")
|
||||||
|
|
||||||
var dlobj = JSObject(it.getString("value"))
|
var dlobj = JSObject(it.getString("value"))
|
||||||
var abobj = dlobj.getJSObject("audiobook")!!
|
if (dlobj.has("audiobook")) {
|
||||||
abobj.put("isDownloaded", true)
|
var abobj = dlobj.getJSObject("audiobook")!!
|
||||||
abobj.put("contentUrl", dlobj.getString("contentUrl", "").toString())
|
abobj.put("isDownloaded", true)
|
||||||
abobj.put("filename", dlobj.getString("filename", "").toString())
|
abobj.put("contentUrl", dlobj.getString("contentUrl", "").toString())
|
||||||
abobj.put("folderUrl", dlobj.getString("folderUrl", "").toString())
|
abobj.put("filename", dlobj.getString("filename", "").toString())
|
||||||
abobj.put("downloadFolderUrl", dlobj.getString("downloadFolderUrl", "").toString())
|
abobj.put("folderUrl", dlobj.getString("folderUrl", "").toString())
|
||||||
abobj.put("localCoverUrl", dlobj.getString("coverUrl", "").toString())
|
abobj.put("downloadFolderUrl", dlobj.getString("downloadFolderUrl", "").toString())
|
||||||
abobj.put("localCover", dlobj.getString("cover", "").toString())
|
abobj.put("localCoverUrl", dlobj.getString("coverUrl", "").toString())
|
||||||
|
abobj.put("localCover", dlobj.getString("cover", "").toString())
|
||||||
|
|
||||||
var audiobook = Audiobook(abobj, serverUrl, token)
|
var audiobook = Audiobook(abobj, serverUrl, token)
|
||||||
audiobooks.add(audiobook)
|
audiobooks.add(audiobook)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
currentTime: 0,
|
||||||
isResetting: false,
|
isResetting: false,
|
||||||
initObject: null,
|
initObject: null,
|
||||||
|
streamId: null,
|
||||||
|
audiobookId: null,
|
||||||
stateName: 'idle',
|
stateName: 'idle',
|
||||||
playInterval: null,
|
playInterval: null,
|
||||||
trackWidth: 0,
|
trackWidth: 0,
|
||||||
|
@ -111,10 +113,13 @@ export default {
|
||||||
seekLoading: false,
|
seekLoading: false,
|
||||||
onPlayingUpdateListener: null,
|
onPlayingUpdateListener: null,
|
||||||
onMetadataListener: null,
|
onMetadataListener: null,
|
||||||
noSyncUpdateTime: false,
|
// noSyncUpdateTime: false,
|
||||||
touchStartY: 0,
|
touchStartY: 0,
|
||||||
touchStartTime: 0,
|
touchStartTime: 0,
|
||||||
touchEndY: 0
|
touchEndY: 0,
|
||||||
|
listenTimeInterval: null,
|
||||||
|
listeningTimeSinceLastUpdate: 0,
|
||||||
|
totalListeningTimeInSession: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -153,6 +158,62 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
clickContainer() {
|
||||||
this.showFullscreen = true
|
this.showFullscreen = true
|
||||||
},
|
},
|
||||||
|
@ -200,9 +261,9 @@ export default {
|
||||||
if (this.loading) return
|
if (this.loading) return
|
||||||
MyNativeAudio.seekForward({ amount: '10000' })
|
MyNativeAudio.seekForward({ amount: '10000' })
|
||||||
},
|
},
|
||||||
sendStreamUpdate() {
|
// sendStreamUpdate() {
|
||||||
this.$emit('updateTime', this.currentTime)
|
// this.$emit('updateTime', this.currentTime)
|
||||||
},
|
// },
|
||||||
setStreamReady() {
|
setStreamReady() {
|
||||||
this.readyTrackWidth = this.trackWidth
|
this.readyTrackWidth = this.trackWidth
|
||||||
this.$refs.readyTrack.style.width = this.trackWidth + 'px'
|
this.$refs.readyTrack.style.width = this.trackWidth + 'px'
|
||||||
|
@ -251,8 +312,8 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateTimestamp()
|
this.updateTimestamp()
|
||||||
if (this.noSyncUpdateTime) this.noSyncUpdateTime = false
|
// if (this.noSyncUpdateTime) this.noSyncUpdateTime = false
|
||||||
else this.sendStreamUpdate()
|
// else this.sendStreamUpdate()
|
||||||
|
|
||||||
var perc = this.currentTime / this.totalDuration
|
var perc = this.currentTime / this.totalDuration
|
||||||
var ptWidth = Math.round(perc * this.trackWidth)
|
var ptWidth = Math.round(perc * this.trackWidth)
|
||||||
|
@ -283,7 +344,6 @@ export default {
|
||||||
this.$refs.playedTrack.classList.add('bg-yellow-300')
|
this.$refs.playedTrack.classList.add('bg-yellow-300')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateVolume(volume) {},
|
|
||||||
clickTrack(e) {
|
clickTrack(e) {
|
||||||
if (this.loading) return
|
if (this.loading) return
|
||||||
var offsetX = e.offsetX
|
var offsetX = e.offsetX
|
||||||
|
@ -318,6 +378,8 @@ export default {
|
||||||
},
|
},
|
||||||
async set(audiobookStreamData, stream, fromAppDestroy) {
|
async set(audiobookStreamData, stream, fromAppDestroy) {
|
||||||
this.isResetting = false
|
this.isResetting = false
|
||||||
|
this.streamId = stream ? stream.id : null
|
||||||
|
this.audiobookId = audiobookStreamData.audiobookId
|
||||||
this.initObject = { ...audiobookStreamData }
|
this.initObject = { ...audiobookStreamData }
|
||||||
|
|
||||||
var init = true
|
var init = true
|
||||||
|
@ -397,6 +459,8 @@ export default {
|
||||||
this.stopPlayInterval()
|
this.stopPlayInterval()
|
||||||
},
|
},
|
||||||
startPlayInterval() {
|
startPlayInterval() {
|
||||||
|
this.startListenTimeInterval()
|
||||||
|
|
||||||
clearInterval(this.playInterval)
|
clearInterval(this.playInterval)
|
||||||
this.playInterval = setInterval(async () => {
|
this.playInterval = setInterval(async () => {
|
||||||
var data = await MyNativeAudio.getCurrentTime()
|
var data = await MyNativeAudio.getCurrentTime()
|
||||||
|
@ -405,6 +469,7 @@ export default {
|
||||||
}, 1000)
|
}, 1000)
|
||||||
},
|
},
|
||||||
stopPlayInterval() {
|
stopPlayInterval() {
|
||||||
|
this.cancelListenTimeInterval()
|
||||||
clearInterval(this.playInterval)
|
clearInterval(this.playInterval)
|
||||||
},
|
},
|
||||||
resetStream(startTime) {
|
resetStream(startTime) {
|
||||||
|
@ -438,7 +503,7 @@ export default {
|
||||||
this.setFromObj()
|
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()
|
this.timeupdate()
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
:sleep-timer-end-of-chapter-time="sleepTimerEndOfChapterTime"
|
:sleep-timer-end-of-chapter-time="sleepTimerEndOfChapterTime"
|
||||||
:sleep-timeout-current-time="sleepTimeoutCurrentTime"
|
:sleep-timeout-current-time="sleepTimeoutCurrentTime"
|
||||||
@close="cancelStream"
|
@close="cancelStream"
|
||||||
@updateTime="updateTime"
|
@sync="sync"
|
||||||
@selectPlaybackSpeed="showPlaybackSpeedModal = true"
|
@selectPlaybackSpeed="showPlaybackSpeedModal = true"
|
||||||
@selectChapter="clickChapterBtn"
|
@selectChapter="clickChapterBtn"
|
||||||
@showSleepTimer="showSleepTimer"
|
@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) {
|
updateTime(currentTime) {
|
||||||
this.currentTime = currentTime
|
this.currentTime = currentTime
|
||||||
|
|
||||||
|
@ -366,7 +395,8 @@ export default {
|
||||||
series: this.seriesTxt,
|
series: this.seriesTxt,
|
||||||
token: this.userToken,
|
token: this.userToken,
|
||||||
contentUrl: this.playingDownload.contentUrl,
|
contentUrl: this.playingDownload.contentUrl,
|
||||||
isLocal: true
|
isLocal: true,
|
||||||
|
audiobookId: this.download.id
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$refs.audioPlayer.set(audiobookStreamData, null, false)
|
this.$refs.audioPlayer.set(audiobookStreamData, null, false)
|
||||||
|
@ -406,7 +436,8 @@ export default {
|
||||||
duration: String(Math.floor(this.duration * 1000)),
|
duration: String(Math.floor(this.duration * 1000)),
|
||||||
series: this.seriesTxt,
|
series: this.seriesTxt,
|
||||||
playlistUrl: this.$server.url + playlistUrl,
|
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)
|
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: {
|
methods: {
|
||||||
async connected(isConnected) {
|
async connected(isConnected) {
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
this.syncUserProgress()
|
// this.syncUserProgress()
|
||||||
|
console.log('[Default] Connected socket sync user ab data')
|
||||||
|
this.$store.dispatch('user/syncUserAudiobookData')
|
||||||
|
|
||||||
// Load libraries
|
// Load libraries
|
||||||
this.$store.dispatch('libraries/load')
|
this.$store.dispatch('libraries/load')
|
||||||
|
@ -109,9 +111,11 @@ export default {
|
||||||
currentUserAudiobookUpdate({ id, data }) {
|
currentUserAudiobookUpdate({ id, data }) {
|
||||||
if (data) {
|
if (data) {
|
||||||
console.log(`Current User Audiobook Updated ${id} ${JSON.stringify(data)}`)
|
console.log(`Current User Audiobook Updated ${id} ${JSON.stringify(data)}`)
|
||||||
this.$localStore.updateUserAudiobookData(data)
|
// this.$localStore.updateUserAudiobookData(data)
|
||||||
|
this.$sqlStore.setUserAudiobookData(data)
|
||||||
} else {
|
} else {
|
||||||
this.$localStore.removeAudiobookProgress(id)
|
// this.$localStore.removeAudiobookProgress(id)
|
||||||
|
this.$sqlStore.removeUserAudiobookData(id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
initialStream(stream) {
|
initialStream(stream) {
|
||||||
|
@ -445,6 +449,10 @@ export default {
|
||||||
if (!this.$server) return console.error('No Server')
|
if (!this.$server) return console.error('No Server')
|
||||||
// console.log(`Default Mounted set SOCKET listeners ${this.$server.connected}`)
|
// 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('connected', this.connected)
|
||||||
this.$server.on('connectionFailed', this.socketConnectionFailed)
|
this.$server.on('connectionFailed', this.socketConnectionFailed)
|
||||||
this.$server.on('initialStream', this.initialStream)
|
this.$server.on('initialStream', this.initialStream)
|
||||||
|
@ -459,28 +467,6 @@ export default {
|
||||||
this.checkForUpdate()
|
this.checkForUpdate()
|
||||||
this.initMediaStore()
|
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() {
|
beforeDestroy() {
|
||||||
if (!this.$server) {
|
if (!this.$server) {
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
head: {
|
head: {
|
||||||
title: 'AudioBookshelf',
|
title: 'Audiobookshelf',
|
||||||
htmlAttrs: {
|
htmlAttrs: {
|
||||||
lang: 'en'
|
lang: 'en'
|
||||||
},
|
},
|
||||||
|
@ -36,7 +36,8 @@ export default {
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
'@/plugins/server.js',
|
'@/plugins/server.js',
|
||||||
'@/plugins/store.js',
|
'@/plugins/sqlStore.js',
|
||||||
|
'@/plugins/localStore.js',
|
||||||
'@/plugins/init.client.js',
|
'@/plugins/init.client.js',
|
||||||
'@/plugins/axios.js',
|
'@/plugins/axios.js',
|
||||||
'@/plugins/my-native-audio.js',
|
'@/plugins/my-native-audio.js',
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "v0.9.21-beta",
|
"version": "v0.9.22-beta",
|
||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt --hostname localhost --port 1337",
|
"dev": "nuxt --hostname localhost --port 1337",
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
<span class="material-icons">auto_stories</span>
|
<span class="material-icons">auto_stories</span>
|
||||||
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
||||||
</ui-btn>
|
</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>
|
<span class="material-icons" :class="downloadObj ? 'animate-pulse' : ''">{{ downloadObj ? (isDownloading || isDownloadPreparing ? 'downloading' : 'download_done') : 'download' }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
@ -218,6 +218,15 @@ export default {
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
this.resettingProgress = true
|
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) {
|
if (this.$server.connected) {
|
||||||
await this.$axios
|
await this.$axios
|
||||||
.$patch(`/api/user/audiobook/${this.audiobookId}/reset-progress`)
|
.$patch(`/api/user/audiobook/${this.audiobookId}/reset-progress`)
|
||||||
|
@ -229,14 +238,7 @@ export default {
|
||||||
console.error('Progress reset failed', error)
|
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
|
this.resettingProgress = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -252,6 +254,7 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
downloadClick() {
|
downloadClick() {
|
||||||
|
console.log('downloadClick ' + this.$server.connected + ' | ' + !!this.downloadObj)
|
||||||
if (!this.$server.connected) return
|
if (!this.$server.connected) return
|
||||||
|
|
||||||
if (this.downloadObj) {
|
if (this.downloadObj) {
|
||||||
|
@ -260,24 +263,47 @@ export default {
|
||||||
this.prepareDownload()
|
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() {
|
async prepareDownload() {
|
||||||
var audiobook = this.audiobook
|
var audiobook = this.audiobook
|
||||||
if (!audiobook) {
|
if (!audiobook) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.hasStoragePermission) {
|
|
||||||
this.$store.commit('downloads/setShowModal', true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download Path
|
// Download Path
|
||||||
var dlFolder = this.$localStore.downloadFolder
|
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')
|
console.log('No download folder, request from user')
|
||||||
// User to select download folder from download modal to ensure permissions
|
// 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
|
return
|
||||||
|
} else {
|
||||||
|
console.log('Has Download folder: ' + JSON.stringify(dlFolder))
|
||||||
}
|
}
|
||||||
|
|
||||||
var downloadObject = {
|
var downloadObject = {
|
||||||
|
|
|
@ -29,7 +29,7 @@ export default {
|
||||||
.map((b) => ({ ...b }))
|
.map((b) => ({ ...b }))
|
||||||
.filter((b) => b.userAbData && !b.userAbData.isRead && b.userAbData.progress > 0)
|
.filter((b) => b.userAbData && !b.userAbData.isRead && b.userAbData.progress > 0)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return a.userAbData.lastUpdate - b.userAbData.lastUpdate
|
return b.userAbData.lastUpdate - a.userAbData.lastUpdate
|
||||||
})
|
})
|
||||||
return books
|
return books
|
||||||
},
|
},
|
||||||
|
@ -38,14 +38,14 @@ export default {
|
||||||
.map((b) => {
|
.map((b) => {
|
||||||
return { ...b }
|
return { ...b }
|
||||||
})
|
})
|
||||||
.sort((a, b) => a.addedAt - b.addedAt)
|
.sort((a, b) => b.addedAt - a.addedAt)
|
||||||
return books.slice(0, 10)
|
return books.slice(0, 10)
|
||||||
},
|
},
|
||||||
booksRead() {
|
booksRead() {
|
||||||
var books = this.booksWithUserAbData
|
var books = this.booksWithUserAbData
|
||||||
.filter((b) => b.userAbData && b.userAbData.isRead)
|
.filter((b) => b.userAbData && b.userAbData.isRead)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return a.userAbData.lastUpdate - b.userAbData.lastUpdate
|
return b.userAbData.lastUpdate - a.userAbData.lastUpdate
|
||||||
})
|
})
|
||||||
return books.slice(0, 10)
|
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
|
state.showModal = val
|
||||||
},
|
},
|
||||||
setDownload(state, download) {
|
setDownload(state, download) {
|
||||||
|
if (!download || !download.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
var index = state.downloads.findIndex(d => d.id === download.id)
|
var index = state.downloads.findIndex(d => d.id === download.id)
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
state.downloads.splice(index, 1, download)
|
state.downloads.splice(index, 1, download)
|
||||||
|
@ -44,6 +47,9 @@ export const mutations = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addUpdateDownload(state, download) {
|
addUpdateDownload(state, download) {
|
||||||
|
if (!download || !download.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
var index = state.downloads.findIndex(d => d.id === download.id)
|
var index = state.downloads.findIndex(d => d.id === download.id)
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
state.downloads.splice(index, 1, download)
|
state.downloads.splice(index, 1, download)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
user: null,
|
user: null,
|
||||||
|
userAudiobookData: [],
|
||||||
localUserAudiobooks: {},
|
localUserAudiobooks: {},
|
||||||
settings: {
|
settings: {
|
||||||
mobileOrderBy: 'recent',
|
mobileOrderBy: 'recent',
|
||||||
|
@ -29,11 +30,12 @@ export const getters = {
|
||||||
return state.localUserAudiobooks ? state.localUserAudiobooks[audiobookId] || null : null
|
return state.localUserAudiobooks ? state.localUserAudiobooks[audiobookId] || null : null
|
||||||
},
|
},
|
||||||
getMostRecentUserAudiobookData: (state, getters) => (audiobookId) => {
|
getMostRecentUserAudiobookData: (state, getters) => (audiobookId) => {
|
||||||
var userAb = getters.getUserAudiobook(audiobookId)
|
return state.userAudiobookData.find(uabd => uabd.audiobookId === audiobookId)
|
||||||
var localUserAb = getters.getLocalUserAudiobook(audiobookId)
|
// var userAb = getters.getUserAudiobook(audiobookId)
|
||||||
if (!localUserAb) return userAb
|
// var localUserAb = getters.getLocalUserAudiobook(audiobookId)
|
||||||
if (!userAb) return localUserAb
|
// if (!localUserAb) return userAb
|
||||||
return localUserAb.lastUpdate > userAb.lastUpdate ? localUserAb : userAb
|
// if (!userAb) return localUserAb
|
||||||
|
// return localUserAb.lastUpdate > userAb.lastUpdate ? localUserAb : userAb
|
||||||
},
|
},
|
||||||
getUserSetting: (state) => (key) => {
|
getUserSetting: (state) => (key) => {
|
||||||
return state.settings ? state.settings[key] || null : null
|
return state.settings ? state.settings[key] || null : null
|
||||||
|
@ -85,15 +87,55 @@ export const actions = {
|
||||||
console.error('Failed to get collections', error)
|
console.error('Failed to get collections', error)
|
||||||
return []
|
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 = {
|
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) {
|
setLocalUserAudiobooks(state, userAudiobooks) {
|
||||||
state.localUserAudiobooks = userAudiobooks
|
// state.localUserAudiobooks = userAudiobooks
|
||||||
state.userAudiobooksListeners.forEach((listener) => {
|
// state.userAudiobooksListeners.forEach((listener) => {
|
||||||
listener.meth()
|
// listener.meth()
|
||||||
})
|
// })
|
||||||
},
|
},
|
||||||
setUserAudiobooks(state, userAudiobooks) {
|
setUserAudiobooks(state, userAudiobooks) {
|
||||||
if (!state.user) return
|
if (!state.user) return
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue