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:
advplyr 2021-11-19 20:00:34 -06:00
parent b6dd37b7f6
commit 3b6e7e1ce2
16 changed files with 908 additions and 1619 deletions

View file

@ -13,8 +13,8 @@ android {
applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 40
versionName "0.9.21-beta"
versionCode 41
versionName "0.9.22-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

View file

@ -115,6 +115,7 @@ class AudiobookManager {
Log.d(tag, "keyvalue ${it.getString("key")} | ${it.getString("value")}")
var dlobj = JSObject(it.getString("value"))
if (dlobj.has("audiobook")) {
var abobj = dlobj.getJSObject("audiobook")!!
abobj.put("isDownloaded", true)
abobj.put("contentUrl", dlobj.getString("contentUrl", "").toString())
@ -128,6 +129,7 @@ class AudiobookManager {
audiobooks.add(audiobook)
}
}
}
fun openStream(audiobook:Audiobook, streamListener:OnStreamData) {
var url = "$serverUrl/api/audiobook/${audiobook.id}/stream"

View file

@ -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>

View file

@ -98,6 +98,8 @@ export default {
currentTime: 0,
isResetting: false,
initObject: null,
streamId: null,
audiobookId: null,
stateName: 'idle',
playInterval: null,
trackWidth: 0,
@ -111,10 +113,13 @@ export default {
seekLoading: false,
onPlayingUpdateListener: null,
onMetadataListener: null,
noSyncUpdateTime: false,
// noSyncUpdateTime: false,
touchStartY: 0,
touchStartTime: 0,
touchEndY: 0
touchEndY: 0,
listenTimeInterval: null,
listeningTimeSinceLastUpdate: 0,
totalListeningTimeInSession: 0
}
},
computed: {
@ -153,6 +158,62 @@ export default {
}
},
methods: {
sendStreamSync(timeListened = 0) {
var syncData = {
timeListened,
currentTime: this.currentTime,
streamId: this.streamId,
audiobookId: this.audiobookId,
totalDuration: this.totalDuration
}
this.$emit('sync', syncData)
},
sendAddListeningTime() {
var listeningTimeToAdd = Math.floor(this.listeningTimeSinceLastUpdate)
this.listeningTimeSinceLastUpdate = Math.max(0, this.listeningTimeSinceLastUpdate - listeningTimeToAdd)
this.sendStreamSync(listeningTimeToAdd)
},
cancelListenTimeInterval() {
this.sendAddListeningTime()
clearInterval(this.listenTimeInterval)
this.listenTimeInterval = null
},
startListenTimeInterval() {
clearInterval(this.listenTimeInterval)
var lastTime = this.currentTime
var lastTick = Date.now()
var noProgressCount = 0
this.listenTimeInterval = setInterval(() => {
var timeSinceLastTick = Date.now() - lastTick
lastTick = Date.now()
var expectedAudioTime = lastTime + timeSinceLastTick / 1000
var currentTime = this.currentTime
var differenceFromExpected = expectedAudioTime - currentTime
if (currentTime === lastTime) {
noProgressCount++
if (noProgressCount > 3) {
console.error('Audio current time has not increased - cancel interval and pause player')
this.pause()
}
} else if (Math.abs(differenceFromExpected) > 0.1) {
noProgressCount = 0
console.warn('Invalid time between interval - resync last', differenceFromExpected)
lastTime = currentTime
} else {
noProgressCount = 0
var exactPlayTimeDifference = currentTime - lastTime
// console.log('Difference from expected', differenceFromExpected, 'Exact play time diff', exactPlayTimeDifference)
lastTime = currentTime
this.listeningTimeSinceLastUpdate += exactPlayTimeDifference
this.totalListeningTimeInSession += exactPlayTimeDifference
// console.log('Time since last update:', this.listeningTimeSinceLastUpdate, 'Session listening time:', this.totalListeningTimeInSession)
if (this.listeningTimeSinceLastUpdate > 5) {
this.sendAddListeningTime()
}
}
}, 1000)
},
clickContainer() {
this.showFullscreen = true
},
@ -200,9 +261,9 @@ export default {
if (this.loading) return
MyNativeAudio.seekForward({ amount: '10000' })
},
sendStreamUpdate() {
this.$emit('updateTime', this.currentTime)
},
// sendStreamUpdate() {
// this.$emit('updateTime', this.currentTime)
// },
setStreamReady() {
this.readyTrackWidth = this.trackWidth
this.$refs.readyTrack.style.width = this.trackWidth + 'px'
@ -251,8 +312,8 @@ export default {
}
this.updateTimestamp()
if (this.noSyncUpdateTime) this.noSyncUpdateTime = false
else this.sendStreamUpdate()
// if (this.noSyncUpdateTime) this.noSyncUpdateTime = false
// else this.sendStreamUpdate()
var perc = this.currentTime / this.totalDuration
var ptWidth = Math.round(perc * this.trackWidth)
@ -283,7 +344,6 @@ export default {
this.$refs.playedTrack.classList.add('bg-yellow-300')
}
},
updateVolume(volume) {},
clickTrack(e) {
if (this.loading) return
var offsetX = e.offsetX
@ -318,6 +378,8 @@ export default {
},
async set(audiobookStreamData, stream, fromAppDestroy) {
this.isResetting = false
this.streamId = stream ? stream.id : null
this.audiobookId = audiobookStreamData.audiobookId
this.initObject = { ...audiobookStreamData }
var init = true
@ -397,6 +459,8 @@ export default {
this.stopPlayInterval()
},
startPlayInterval() {
this.startListenTimeInterval()
clearInterval(this.playInterval)
this.playInterval = setInterval(async () => {
var data = await MyNativeAudio.getCurrentTime()
@ -405,6 +469,7 @@ export default {
}, 1000)
},
stopPlayInterval() {
this.cancelListenTimeInterval()
clearInterval(this.playInterval)
},
resetStream(startTime) {
@ -438,7 +503,7 @@ export default {
this.setFromObj()
}
if (this.stateName === 'ready_no_sync' || this.stateName === 'buffering_no_sync') this.noSyncUpdateTime = true
// if (this.stateName === 'ready_no_sync' || this.stateName === 'buffering_no_sync') this.noSyncUpdateTime = true
this.timeupdate()
},

View file

@ -11,7 +11,7 @@
:sleep-timer-end-of-chapter-time="sleepTimerEndOfChapterTime"
:sleep-timeout-current-time="sleepTimeoutCurrentTime"
@close="cancelStream"
@updateTime="updateTime"
@sync="sync"
@selectPlaybackSpeed="showPlaybackSpeedModal = true"
@selectChapter="clickChapterBtn"
@showSleepTimer="showSleepTimer"
@ -260,6 +260,35 @@ export default {
}
}
},
sync(syncData) {
var diff = syncData.currentTime - this.lastServerUpdateSentSeconds
if (Math.abs(diff) < 1 && !syncData.timeListened) {
// No need to sync
return
}
if (this.stream) {
this.$server.socket.emit('stream_sync', syncData)
} else {
var progressUpdate = {
audiobookId: syncData.audiobookId,
currentTime: syncData.currentTime,
totalDuration: syncData.totalDuration,
progress: Number((syncData.currentTime / syncData.totalDuration).toFixed(3)),
lastUpdate: Date.now(),
isRead: false
}
if (this.$server.connected) {
this.$server.socket.emit('progress_update', progressUpdate)
} else {
this.$store.dispatch('user/updateUserAudiobookData', progressUpdate)
// this.$localStore.updateUserAudiobookData(progressUpdate).then(() => {
// console.log('Updated user audiobook progress', currentTime)
// })
}
}
},
updateTime(currentTime) {
this.currentTime = currentTime
@ -366,7 +395,8 @@ export default {
series: this.seriesTxt,
token: this.userToken,
contentUrl: this.playingDownload.contentUrl,
isLocal: true
isLocal: true,
audiobookId: this.download.id
}
this.$refs.audioPlayer.set(audiobookStreamData, null, false)
@ -406,7 +436,8 @@ export default {
duration: String(Math.floor(this.duration * 1000)),
series: this.seriesTxt,
playlistUrl: this.$server.url + playlistUrl,
token: this.$store.getters['user/getToken']
token: this.$store.getters['user/getToken'],
audiobookId: this.audiobookId
}
this.$refs.audioPlayer.set(audiobookStreamData, stream, !this.stream)

View file

@ -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>

View file

@ -47,7 +47,9 @@ export default {
methods: {
async connected(isConnected) {
if (isConnected) {
this.syncUserProgress()
// this.syncUserProgress()
console.log('[Default] Connected socket sync user ab data')
this.$store.dispatch('user/syncUserAudiobookData')
// Load libraries
this.$store.dispatch('libraries/load')
@ -109,9 +111,11 @@ export default {
currentUserAudiobookUpdate({ id, data }) {
if (data) {
console.log(`Current User Audiobook Updated ${id} ${JSON.stringify(data)}`)
this.$localStore.updateUserAudiobookData(data)
// this.$localStore.updateUserAudiobookData(data)
this.$sqlStore.setUserAudiobookData(data)
} else {
this.$localStore.removeAudiobookProgress(id)
// this.$localStore.removeAudiobookProgress(id)
this.$sqlStore.removeUserAudiobookData(id)
}
},
initialStream(stream) {
@ -445,6 +449,10 @@ export default {
if (!this.$server) return console.error('No Server')
// console.log(`Default Mounted set SOCKET listeners ${this.$server.connected}`)
if (!this.$server.connected) {
console.log('Syncing on default mount')
this.$store.dispatch('user/syncUserAudiobookData')
}
this.$server.on('connected', this.connected)
this.$server.on('connectionFailed', this.socketConnectionFailed)
this.$server.on('initialStream', this.initialStream)
@ -459,28 +467,6 @@ export default {
this.checkForUpdate()
this.initMediaStore()
}
if (!this.$server.connected) {
}
// Old bad attempt at AA
// MyNativeAudio.addListener('onPrepareMedia', (data) => {
// var audiobookId = data.audiobookId
// var playWhenReady = data.playWhenReady
// var audiobook = this.$store.getters['audiobooks/getAudiobook'](audiobookId)
// var download = this.$store.getters['downloads/getDownloadIfReady'](audiobookId)
// this.$store.commit('setPlayOnLoad', playWhenReady)
// if (!download) {
// // Stream
// this.$store.commit('setStreamAudiobook', audiobook)
// this.$server.socket.emit('open_stream', audiobook.id)
// } else {
// // Local
// this.$store.commit('setPlayingDownload', download)
// }
// })
},
beforeDestroy() {
if (!this.$server) {

View file

@ -14,7 +14,7 @@ export default {
},
head: {
title: 'AudioBookshelf',
title: 'Audiobookshelf',
htmlAttrs: {
lang: 'en'
},
@ -36,7 +36,8 @@ export default {
plugins: [
'@/plugins/server.js',
'@/plugins/store.js',
'@/plugins/sqlStore.js',
'@/plugins/localStore.js',
'@/plugins/init.client.js',
'@/plugins/axios.js',
'@/plugins/my-native-audio.js',

View file

@ -1,6 +1,6 @@
{
"name": "audiobookshelf-app",
"version": "v0.9.21-beta",
"version": "v0.9.22-beta",
"author": "advplyr",
"scripts": {
"dev": "nuxt --hostname localhost --port 1337",

View file

@ -35,7 +35,7 @@
<span class="material-icons">auto_stories</span>
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
</ui-btn>
<ui-btn v-if="isConnected && showPlay" color="primary" :disabled="isPlaying" class="flex items-center justify-center" :padding-x="2" @click="downloadClick">
<ui-btn v-if="isConnected && showPlay" color="primary" class="flex items-center justify-center" :padding-x="2" @click="downloadClick">
<span class="material-icons" :class="downloadObj ? 'animate-pulse' : ''">{{ downloadObj ? (isDownloading || isDownloadPreparing ? 'downloading' : 'download_done') : 'download' }}</span>
</ui-btn>
</div>
@ -218,6 +218,15 @@ export default {
if (value) {
this.resettingProgress = true
this.$store.dispatch('user/updateUserAudiobookData', {
audiobookId: this.audiobookId,
currentTime: 0,
totalDuration: this.duration,
progress: 0,
lastUpdate: Date.now(),
isRead: false
})
if (this.$server.connected) {
await this.$axios
.$patch(`/api/user/audiobook/${this.audiobookId}/reset-progress`)
@ -229,14 +238,7 @@ export default {
console.error('Progress reset failed', error)
})
}
this.$localStore.updateUserAudiobookData({
audiobookId: this.audiobookId,
currentTime: 0,
totalDuration: this.duration,
progress: 0,
lastUpdate: Date.now(),
isRead: false
})
this.resettingProgress = false
}
},
@ -252,6 +254,7 @@ export default {
})
},
downloadClick() {
console.log('downloadClick ' + this.$server.connected + ' | ' + !!this.downloadObj)
if (!this.$server.connected) return
if (this.downloadObj) {
@ -260,24 +263,47 @@ export default {
this.prepareDownload()
}
},
async changeDownloadFolderClick() {
if (!this.hasStoragePermission) {
console.log('Requesting Storage Permission')
await StorageManager.requestStoragePermission()
} else {
var folderObj = await StorageManager.selectFolder()
if (folderObj.error) {
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
}
var permissionsGood = await StorageManager.checkFolderPermissions({ folderUrl: folderObj.uri })
console.log('Storage Permission check folder ' + permissionsGood)
if (!permissionsGood) {
this.$toast.error('Folder permissions failed')
return
} else {
this.$toast.success('Folder permission success')
}
await this.$localStore.setDownloadFolder(folderObj)
}
},
async prepareDownload() {
var audiobook = this.audiobook
if (!audiobook) {
return
}
if (!this.hasStoragePermission) {
this.$store.commit('downloads/setShowModal', true)
return
}
// Download Path
var dlFolder = this.$localStore.downloadFolder
if (!dlFolder) {
console.log('Prepare download: ' + this.hasStoragePermission + ' | ' + dlFolder)
if (!this.hasStoragePermission || !dlFolder) {
console.log('No download folder, request from user')
// User to select download folder from download modal to ensure permissions
this.$store.commit('downloads/setShowModal', true)
// this.$store.commit('downloads/setShowModal', true)
this.changeDownloadFolderClick()
return
} else {
console.log('Has Download folder: ' + JSON.stringify(dlFolder))
}
var downloadObject = {

View file

@ -29,7 +29,7 @@ export default {
.map((b) => ({ ...b }))
.filter((b) => b.userAbData && !b.userAbData.isRead && b.userAbData.progress > 0)
.sort((a, b) => {
return a.userAbData.lastUpdate - b.userAbData.lastUpdate
return b.userAbData.lastUpdate - a.userAbData.lastUpdate
})
return books
},
@ -38,14 +38,14 @@ export default {
.map((b) => {
return { ...b }
})
.sort((a, b) => a.addedAt - b.addedAt)
.sort((a, b) => b.addedAt - a.addedAt)
return books.slice(0, 10)
},
booksRead() {
var books = this.booksWithUserAbData
.filter((b) => b.userAbData && b.userAbData.isRead)
.sort((a, b) => {
return a.userAbData.lastUpdate - b.userAbData.lastUpdate
return b.userAbData.lastUpdate - a.userAbData.lastUpdate
})
return books.slice(0, 10)
},

230
plugins/localStore.js Normal file
View 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
View 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))
}

View file

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

View file

@ -36,6 +36,9 @@ export const mutations = {
state.showModal = val
},
setDownload(state, download) {
if (!download || !download.id) {
return
}
var index = state.downloads.findIndex(d => d.id === download.id)
if (index >= 0) {
state.downloads.splice(index, 1, download)
@ -44,6 +47,9 @@ export const mutations = {
}
},
addUpdateDownload(state, download) {
if (!download || !download.id) {
return
}
var index = state.downloads.findIndex(d => d.id === download.id)
if (index >= 0) {
state.downloads.splice(index, 1, download)

View file

@ -1,5 +1,6 @@
export const state = () => ({
user: null,
userAudiobookData: [],
localUserAudiobooks: {},
settings: {
mobileOrderBy: 'recent',
@ -29,11 +30,12 @@ export const getters = {
return state.localUserAudiobooks ? state.localUserAudiobooks[audiobookId] || null : null
},
getMostRecentUserAudiobookData: (state, getters) => (audiobookId) => {
var userAb = getters.getUserAudiobook(audiobookId)
var localUserAb = getters.getLocalUserAudiobook(audiobookId)
if (!localUserAb) return userAb
if (!userAb) return localUserAb
return localUserAb.lastUpdate > userAb.lastUpdate ? localUserAb : userAb
return state.userAudiobookData.find(uabd => uabd.audiobookId === audiobookId)
// var userAb = getters.getUserAudiobook(audiobookId)
// var localUserAb = getters.getLocalUserAudiobook(audiobookId)
// if (!localUserAb) return userAb
// if (!userAb) return localUserAb
// return localUserAb.lastUpdate > userAb.lastUpdate ? localUserAb : userAb
},
getUserSetting: (state) => (key) => {
return state.settings ? state.settings[key] || null : null
@ -85,15 +87,55 @@ export const actions = {
console.error('Failed to get collections', error)
return []
})
},
async syncUserAudiobookData({ state, commit }) {
if (!state.user) {
console.error('Sync user audiobook data invalid no user')
return
}
var localUserAudiobookData = await this.$sqlStore.getAllUserAudiobookData() || []
this.$axios.$post(`/api/syncUserAudiobookData`, { data: localUserAudiobookData }).then(async (abData) => {
console.log('Synced user audiobook data', abData)
await this.$sqlStore.setAllUserAudiobookData(abData)
}).catch((error) => {
console.error('Failed to sync user ab data', error)
})
},
async updateUserAudiobookData({ state, commit }, uabdUpdate) {
var userAbData = state.userAudiobookData.find(uab => uab.audiobookId === uabdUpdate.audiobookId)
if (!userAbData) {
uabdUpdate.startedAt = Date.now()
this.$sqlStore.setUserAudiobookData(uabdUpdate)
} else {
var mergedUabData = { ...userAbData }
for (const key in uabdUpdate) {
mergedUabData[key] = uabdUpdate[key]
}
this.$sqlStore.setUserAudiobookData(mergedUabData)
}
}
}
export const mutations = {
setUserAudiobookData(state, abdata) {
var index = state.userAudiobookData.findIndex(uab => uab.audiobookId === abdata.audiobookId)
if (index >= 0) {
state.userAudiobookData.splice(index, 1, abdata)
} else {
state.userAudiobookData.push(abdata)
}
},
removeUserAudiobookData(state, audiobookId) {
state.userAudiobookData = state.userAudiobookData.filter(uab => uab.audiobookId !== audiobookId)
},
setAllUserAudiobookData(state, allAbData) {
state.userAudiobookData = allAbData
},
setLocalUserAudiobooks(state, userAudiobooks) {
state.localUserAudiobooks = userAudiobooks
state.userAudiobooksListeners.forEach((listener) => {
listener.meth()
})
// state.localUserAudiobooks = userAudiobooks
// state.userAudiobooksListeners.forEach((listener) => {
// listener.meth()
// })
},
setUserAudiobooks(state, userAudiobooks) {
if (!state.user) return