From 3b6e7e1ce2f34824e59e2bbaf82cc1815eef1659 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 19 Nov 2021 20:00:34 -0600 Subject: [PATCH] Add: Search page, Add: Bookshelf list view, Fix: Audiobook progress sync, Fix: Download audiobook button, Change: User audiobook data to use SQL table --- android/app/build.gradle | 4 +- .../audiobookshelf/app/AudiobookManager.kt | 22 +- components/AudioPlayerMini.vue | 377 ---------- components/app/AudioPlayer.vue | 83 ++- components/app/AudioPlayerContainer.vue | 37 +- components/app/StreamContainer.vue | 473 ------------ layouts/default.vue | 36 +- nuxt.config.js | 5 +- package.json | 2 +- pages/audiobook/_id/index.vue | 58 +- pages/bookshelf/index.vue | 6 +- plugins/localStore.js | 230 ++++++ plugins/sqlStore.js | 439 +++++++++++ plugins/store.js | 689 ------------------ store/downloads.js | 6 + store/user.js | 60 +- 16 files changed, 908 insertions(+), 1619 deletions(-) delete mode 100644 components/AudioPlayerMini.vue delete mode 100644 components/app/StreamContainer.vue create mode 100644 plugins/localStore.js create mode 100644 plugins/sqlStore.js delete mode 100644 plugins/store.js diff --git a/android/app/build.gradle b/android/app/build.gradle index 8ff1751b..5a0e5942 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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. diff --git a/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt b/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt index dd371cda..04a771ea 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/AudiobookManager.kt @@ -115,17 +115,19 @@ class AudiobookManager { Log.d(tag, "keyvalue ${it.getString("key")} | ${it.getString("value")}") var dlobj = JSObject(it.getString("value")) - var abobj = dlobj.getJSObject("audiobook")!! - abobj.put("isDownloaded", true) - abobj.put("contentUrl", dlobj.getString("contentUrl", "").toString()) - abobj.put("filename", dlobj.getString("filename", "").toString()) - abobj.put("folderUrl", dlobj.getString("folderUrl", "").toString()) - abobj.put("downloadFolderUrl", dlobj.getString("downloadFolderUrl", "").toString()) - abobj.put("localCoverUrl", dlobj.getString("coverUrl", "").toString()) - abobj.put("localCover", dlobj.getString("cover", "").toString()) + if (dlobj.has("audiobook")) { + var abobj = dlobj.getJSObject("audiobook")!! + abobj.put("isDownloaded", true) + abobj.put("contentUrl", dlobj.getString("contentUrl", "").toString()) + abobj.put("filename", dlobj.getString("filename", "").toString()) + abobj.put("folderUrl", dlobj.getString("folderUrl", "").toString()) + abobj.put("downloadFolderUrl", dlobj.getString("downloadFolderUrl", "").toString()) + abobj.put("localCoverUrl", dlobj.getString("coverUrl", "").toString()) + abobj.put("localCover", dlobj.getString("cover", "").toString()) - var audiobook = Audiobook(abobj, serverUrl, token) - audiobooks.add(audiobook) + var audiobook = Audiobook(abobj, serverUrl, token) + audiobooks.add(audiobook) + } } } diff --git a/components/AudioPlayerMini.vue b/components/AudioPlayerMini.vue deleted file mode 100644 index 34581fdf..00000000 --- a/components/AudioPlayerMini.vue +++ /dev/null @@ -1,377 +0,0 @@ - - - \ No newline at end of file diff --git a/components/app/AudioPlayer.vue b/components/app/AudioPlayer.vue index 72b47144..dc224656 100644 --- a/components/app/AudioPlayer.vue +++ b/components/app/AudioPlayer.vue @@ -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() }, diff --git a/components/app/AudioPlayerContainer.vue b/components/app/AudioPlayerContainer.vue index adaa545f..335483af 100644 --- a/components/app/AudioPlayerContainer.vue +++ b/components/app/AudioPlayerContainer.vue @@ -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) diff --git a/components/app/StreamContainer.vue b/components/app/StreamContainer.vue deleted file mode 100644 index cf572bb5..00000000 --- a/components/app/StreamContainer.vue +++ /dev/null @@ -1,473 +0,0 @@ - - - - - \ No newline at end of file diff --git a/layouts/default.vue b/layouts/default.vue index d4a0a99c..b924dd6d 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -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) { diff --git a/nuxt.config.js b/nuxt.config.js index 83b36cee..02c185a2 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -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', diff --git a/package.json b/package.json index 01739a01..284f3ec7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pages/audiobook/_id/index.vue b/pages/audiobook/_id/index.vue index 170868b2..442e94f6 100644 --- a/pages/audiobook/_id/index.vue +++ b/pages/audiobook/_id/index.vue @@ -35,7 +35,7 @@ auto_stories Read {{ ebookFormat }} - + {{ downloadObj ? (isDownloading || isDownloadPreparing ? 'downloading' : 'download_done') : 'download' }} @@ -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 = { diff --git a/pages/bookshelf/index.vue b/pages/bookshelf/index.vue index a5e028c8..6d7dc3f8 100644 --- a/pages/bookshelf/index.vue +++ b/pages/bookshelf/index.vue @@ -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) }, diff --git a/plugins/localStore.js b/plugins/localStore.js new file mode 100644 index 00000000..a6c31e5c --- /dev/null +++ b/plugins/localStore.js @@ -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)) +} \ No newline at end of file diff --git a/plugins/sqlStore.js b/plugins/sqlStore.js new file mode 100644 index 00000000..8fbd5125 --- /dev/null +++ b/plugins/sqlStore.js @@ -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)) +} \ No newline at end of file diff --git a/plugins/store.js b/plugins/store.js deleted file mode 100644 index 4d891ae6..00000000 --- a/plugins/store.js +++ /dev/null @@ -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)) -} \ No newline at end of file diff --git a/store/downloads.js b/store/downloads.js index ea85f244..25ea100d 100644 --- a/store/downloads.js +++ b/store/downloads.js @@ -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) diff --git a/store/user.js b/store/user.js index 98b62dc1..c4cee8db 100644 --- a/store/user.js +++ b/store/user.js @@ -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