Add new connection page to support multiple server connection configs

This commit is contained in:
advplyr 2022-04-03 14:24:17 -05:00
parent 7a091dd428
commit f57f0e4e0d
30 changed files with 789 additions and 1284 deletions

View file

@ -146,7 +146,7 @@ class AudioDownloader : Plugin() {
Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioFile.metadata.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName ${destinationFilename}") Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioFile.metadata.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName ${destinationFilename}")
var destinationFile = File("$itemFolderPath/$destinationFilename") var destinationFile = File("$itemFolderPath/$destinationFilename")
var destinationUri = Uri.fromFile(destinationFile) var destinationUri = Uri.fromFile(destinationFile)
var downloadUri = Uri.parse("${apiHandler.serverUrl}${serverPath}?token=${apiHandler.token}") var downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}")
Log.d(tag, "Audio File Destination Uri $destinationUri | Download URI $downloadUri") Log.d(tag, "Audio File Destination Uri $destinationUri | Download URI $downloadUri")
var downloadItemPart = DownloadItemPart(UUID.randomUUID().toString(), destinationFilename, bookTitle, serverPath, localFolder.name var downloadItemPart = DownloadItemPart(UUID.randomUUID().toString(), destinationFilename, bookTitle, serverPath, localFolder.name
?: "", localFolder.id, downloadUri, destinationUri, null, 0) ?: "", localFolder.id, downloadUri, destinationUri, null, 0)

View file

@ -2,6 +2,7 @@ package com.audiobookshelf.app.data
import android.util.Log import android.util.Log
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.audiobookshelf.app.device.DeviceManager
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.JSObject import com.getcapacitor.JSObject
import com.getcapacitor.Plugin import com.getcapacitor.Plugin
@ -19,19 +20,19 @@ import java.io.File
class DbManager : Plugin() { class DbManager : Plugin() {
val tag = "DbManager" val tag = "DbManager"
fun loadDeviceData(): DeviceData { fun getDeviceData(): DeviceData {
return Paper.book("device").read("data") ?: DeviceData(mutableListOf(), null) return Paper.book("device").read("data") ?: DeviceData(mutableListOf(), null)
} }
fun saveDeviceData(deviceData:DeviceData) { fun saveDeviceData(deviceData:DeviceData) {
Paper.book("device").write("data", deviceData) Paper.book("device").write("data", deviceData)
} }
fun loadLocalMediaItems():MutableList<LocalMediaItem> { fun getLocalMediaItems():MutableList<LocalMediaItem> {
var localMediaItems:MutableList<LocalMediaItem> = mutableListOf() var localMediaItems:MutableList<LocalMediaItem> = mutableListOf()
Paper.book("localMediaItems").allKeys.forEach { Paper.book("localMediaItems").allKeys.forEach {
var localMediaItem:LocalMediaItem? = Paper.book("localMediaItems").read(it) var localMediaItem:LocalMediaItem? = Paper.book("localMediaItems").read(it)
if (localMediaItem != null) { if (localMediaItem != null) {
// TODO: Check to make sure all file paths exist
// if (localMediaItem.coverContentUrl != null) { // if (localMediaItem.coverContentUrl != null) {
// var file = DocumentFile.fromSingleUri(ctx) // var file = DocumentFile.fromSingleUri(ctx)
// if (!file.exists()) { // if (!file.exists()) {
@ -45,15 +46,11 @@ class DbManager : Plugin() {
// } // }
} }
} }
// localMediaItems = localMediaItems.filter {
//
// file.exists()
// }
return localMediaItems return localMediaItems
} }
fun getLocalMediaItemsInFolder(folderId:String):List<LocalMediaItem> { fun getLocalMediaItemsInFolder(folderId:String):List<LocalMediaItem> {
var localMediaItems = loadLocalMediaItems() var localMediaItems = getLocalMediaItems()
return localMediaItems.filter { return localMediaItems.filter {
it.folderId == folderId it.folderId == folderId
} }
@ -111,35 +108,15 @@ class DbManager : Plugin() {
return json return json
} }
//
// Database calls from webview
//
@PluginMethod @PluginMethod
fun saveFromWebview(call: PluginCall) { fun getDeviceData_WV(call:PluginCall) {
var db = call.getString("db", "").toString()
var key = call.getString("key", "").toString()
var value = call.getObject("value")
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
if (db == "" || key == "" || value == null) { var deviceData = getDeviceData()
Log.d(tag, "saveFromWebview Invalid key/value") call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(deviceData)))
} else {
var json = value as JSONObject
saveObject(db, key, json)
} }
call.resolve()
}
}
@PluginMethod
fun loadFromWebview(call:PluginCall) {
var db = call.getString("db", "").toString()
var key = call.getString("key", "").toString()
if (db == "" || key == "") {
Log.d(tag, "loadFromWebview Invalid Key")
call.resolve()
return
}
var json = loadObject(db, key)
var jsobj = JSObject.fromJSONObject(json)
call.resolve(jsobj)
} }
@PluginMethod @PluginMethod
@ -175,4 +152,108 @@ class DbManager : Plugin() {
call.resolve(jsobj) call.resolve(jsobj)
} }
} }
@PluginMethod
fun setCurrentServerConnectionConfig_WV(call:PluginCall) {
var serverConnectionConfigId = call.getString("id", "").toString()
var serverConnectionConfig = DeviceManager.deviceData.serverConnectionConfigs.find { it.id == serverConnectionConfigId }
var username = call.getString("username", "").toString()
var token = call.getString("token", "").toString()
GlobalScope.launch(Dispatchers.IO) {
if (serverConnectionConfig == null) { // New Server Connection
var serverAddress = call.getString("address", "").toString()
// Create new server connection config
var sscId = DeviceManager.getBase64Id("$serverAddress@$username")
var sscIndex = DeviceManager.deviceData.serverConnectionConfigs.size
serverConnectionConfig = ServerConnectionConfig(sscId, sscIndex, "$serverAddress ($username)", serverAddress, username, token)
// Add and save
DeviceManager.deviceData.serverConnectionConfigs.add(serverConnectionConfig!!)
DeviceManager.deviceData.lastServerConnectionConfigId = serverConnectionConfig?.id
saveDeviceData(DeviceManager.deviceData)
} else {
var shouldSave = false
if (serverConnectionConfig?.username != username || serverConnectionConfig?.token != token) {
serverConnectionConfig?.username = username
serverConnectionConfig?.name = "${serverConnectionConfig?.address} (${serverConnectionConfig?.username})"
serverConnectionConfig?.token = token
shouldSave = true
}
// Set last connection config
if (DeviceManager.deviceData.lastServerConnectionConfigId != serverConnectionConfigId) {
DeviceManager.deviceData.lastServerConnectionConfigId = serverConnectionConfigId
shouldSave = true
}
if (shouldSave) saveDeviceData(DeviceManager.deviceData)
}
DeviceManager.serverConnectionConfig = serverConnectionConfig
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(DeviceManager.serverConnectionConfig)))
}
}
@PluginMethod
fun removeServerConnectionConfig_WV(call:PluginCall) {
GlobalScope.launch(Dispatchers.IO) {
var serverConnectionConfigId = call.getString("serverConnectionConfigId", "").toString()
DeviceManager.deviceData.serverConnectionConfigs = DeviceManager.deviceData.serverConnectionConfigs.filter { it.id != serverConnectionConfigId } as MutableList<ServerConnectionConfig>
if (DeviceManager.deviceData.lastServerConnectionConfigId == serverConnectionConfigId) {
DeviceManager.deviceData.lastServerConnectionConfigId = null
}
saveDeviceData(DeviceManager.deviceData)
if (DeviceManager.serverConnectionConfig?.id == serverConnectionConfigId) {
DeviceManager.serverConnectionConfig = null
}
call.resolve()
}
}
@PluginMethod
fun logout_WV(call:PluginCall) {
GlobalScope.launch(Dispatchers.IO) {
DeviceManager.serverConnectionConfig = null
DeviceManager.deviceData.lastServerConnectionConfigId = null
saveDeviceData(DeviceManager.deviceData)
call.resolve()
}
}
//
// Generic Webview calls to db
//
@PluginMethod
fun saveFromWebview(call: PluginCall) {
var db = call.getString("db", "").toString()
var key = call.getString("key", "").toString()
var value = call.getObject("value")
GlobalScope.launch(Dispatchers.IO) {
if (db == "" || key == "" || value == null) {
Log.d(tag, "saveFromWebview Invalid key/value")
} else {
var json = value as JSONObject
saveObject(db, key, json)
}
call.resolve()
}
}
@PluginMethod
fun loadFromWebview(call:PluginCall) {
var db = call.getString("db", "").toString()
var key = call.getString("key", "").toString()
if (db == "" || key == "") {
Log.d(tag, "loadFromWebview Invalid Key")
call.resolve()
return
}
var json = loadObject(db, key)
var jsobj = JSObject.fromJSONObject(json)
call.resolve(jsobj)
}
} }

View file

@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import java.util.* import java.util.*
data class ServerConfig( data class ServerConnectionConfig(
var id:String, var id:String,
var index:Int, var index:Int,
var name:String, var name:String,
@ -14,8 +14,8 @@ data class ServerConfig(
) )
data class DeviceData( data class DeviceData(
var serverConfigs:MutableList<ServerConfig>, var serverConnectionConfigs:MutableList<ServerConnectionConfig>,
var lastServerConfigId:String? var lastServerConnectionConfigId:String?
) )
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)

View file

@ -1,19 +1,18 @@
package com.audiobookshelf.app.device package com.audiobookshelf.app.device
import android.util.Log import android.util.Log
import com.anggrayudi.storage.file.id
import com.audiobookshelf.app.data.DbManager import com.audiobookshelf.app.data.DbManager
import com.audiobookshelf.app.data.DeviceData import com.audiobookshelf.app.data.DeviceData
import com.audiobookshelf.app.data.ServerConfig import com.audiobookshelf.app.data.ServerConnectionConfig
object DeviceManager { object DeviceManager {
val tag = "DeviceManager" val tag = "DeviceManager"
val dbManager:DbManager = DbManager() val dbManager:DbManager = DbManager()
var deviceData:DeviceData = dbManager.loadDeviceData() var deviceData:DeviceData = dbManager.getDeviceData()
var currentServerConfig: ServerConfig? = null var serverConnectionConfig: ServerConnectionConfig? = null
val serverAddress get() = currentServerConfig?.address ?: "" val serverAddress get() = serverConnectionConfig?.address ?: ""
val token get() = currentServerConfig?.token ?: "" val token get() = serverConnectionConfig?.token ?: ""
init { init {
Log.d(tag, "Device Manager Singleton invoked") Log.d(tag, "Device Manager Singleton invoked")

View file

@ -1,13 +1,12 @@
package com.audiobookshelf.app.server package com.audiobookshelf.app.server
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.util.Log import android.util.Log
import com.audiobookshelf.app.data.Library import com.audiobookshelf.app.data.Library
import com.audiobookshelf.app.data.LibraryItem import com.audiobookshelf.app.data.LibraryItem
import com.audiobookshelf.app.data.MediaTypeMetadata
import com.audiobookshelf.app.data.PlaybackSession import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.device.DeviceManager
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.getcapacitor.JSArray import com.getcapacitor.JSArray
@ -21,26 +20,15 @@ class ApiHandler {
val tag = "ApiHandler" val tag = "ApiHandler"
private var client = OkHttpClient() private var client = OkHttpClient()
var ctx: Context var ctx: Context
var serverUrl = ""
var token = ""
var storageSharedPreferences: SharedPreferences? = null var storageSharedPreferences: SharedPreferences? = null
constructor(_ctx: Context) { constructor(_ctx: Context) {
ctx = _ctx ctx = _ctx
init()
}
fun init() {
storageSharedPreferences = ctx.getSharedPreferences("CapacitorStorage", Activity.MODE_PRIVATE)
serverUrl = storageSharedPreferences?.getString("serverUrl", "").toString()
Log.d(tag, "SHARED PREF SERVERURL $serverUrl")
token = storageSharedPreferences?.getString("token", "").toString()
Log.d(tag, "SHARED PREF TOKEN $token")
} }
fun getRequest(endpoint:String, cb: (JSObject) -> Unit) { fun getRequest(endpoint:String, cb: (JSObject) -> Unit) {
val request = Request.Builder() val request = Request.Builder()
.url("$serverUrl$endpoint").addHeader("Authorization", "Bearer $token") .url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}")
.build() .build()
makeRequest(request, cb) makeRequest(request, cb)
} }
@ -49,7 +37,7 @@ class ApiHandler {
val mediaType = "application/json; charset=utf-8".toMediaType() val mediaType = "application/json; charset=utf-8".toMediaType()
val requestBody = payload.toString().toRequestBody(mediaType) val requestBody = payload.toString().toRequestBody(mediaType)
val request = Request.Builder().post(requestBody) val request = Request.Builder().post(requestBody)
.url("$serverUrl$endpoint").addHeader("Authorization", "Bearer $token") .url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}")
.build() .build()
makeRequest(request, cb) makeRequest(request, cb)
} }
@ -125,8 +113,8 @@ class ApiHandler {
else payload.put("forceTranscode", true) else payload.put("forceTranscode", true)
postRequest("/api/items/$libraryItemId/play", payload) { postRequest("/api/items/$libraryItemId/play", payload) {
it.put("serverUrl", serverUrl) it.put("serverUrl", DeviceManager.serverAddress)
it.put("token", token) it.put("token", DeviceManager.token)
val playbackSession = mapper.readValue<PlaybackSession>(it.toString()) val playbackSession = mapper.readValue<PlaybackSession>(it.toString())
cb(playbackSession) cb(playbackSession)
} }

View file

@ -151,15 +151,15 @@ export default {
} }
}, },
setListeners() { setListeners() {
if (!this.$server.socket) { // if (!this.$server.socket) {
console.error('Invalid server socket not set') // console.error('Invalid server socket not set')
return // return
} // }
this.$server.socket.on('stream_open', this.streamOpen) // this.$server.socket.on('stream_open', this.streamOpen)
this.$server.socket.on('stream_closed', this.streamClosed) // this.$server.socket.on('stream_closed', this.streamClosed)
this.$server.socket.on('stream_progress', this.streamProgress) // this.$server.socket.on('stream_progress', this.streamProgress)
this.$server.socket.on('stream_ready', this.streamReady) // this.$server.socket.on('stream_ready', this.streamReady)
this.$server.socket.on('stream_reset', this.streamReset) // this.$server.socket.on('stream_reset', this.streamReset)
}, },
closeStreamOnly() { closeStreamOnly() {
// If user logs out or disconnects from server and not playing local // If user logs out or disconnects from server and not playing local
@ -203,13 +203,13 @@ export default {
if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove() if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove()
if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove() if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove()
if (this.$server.socket) { // if (this.$server.socket) {
this.$server.socket.off('stream_open', this.streamOpen) // this.$server.socket.off('stream_open', this.streamOpen)
this.$server.socket.off('stream_closed', this.streamClosed) // this.$server.socket.off('stream_closed', this.streamClosed)
this.$server.socket.off('stream_progress', this.streamProgress) // this.$server.socket.off('stream_progress', this.streamProgress)
this.$server.socket.off('stream_ready', this.streamReady) // this.$server.socket.off('stream_ready', this.streamReady)
this.$server.socket.off('stream_reset', this.streamReset) // this.$server.socket.off('stream_reset', this.streamReset)
} // }
this.$eventBus.$off('play-item', this.playLibraryItem) this.$eventBus.$off('play-item', this.playLibraryItem)
this.$eventBus.$off('play-local-item', this.playLocalItem) this.$eventBus.$off('play-local-item', this.playLocalItem)
this.$eventBus.$off('close-stream', this.closeStreamOnly) this.$eventBus.$off('close-stream', this.closeStreamOnly)

View file

@ -3,7 +3,7 @@
<div class="absolute top-0 left-0 w-full h-full bg-black transition-opacity duration-200" :class="show ? 'bg-opacity-60 pointer-events-auto' : 'bg-opacity-0'" @click="clickBackground" /> <div class="absolute top-0 left-0 w-full h-full bg-black transition-opacity duration-200" :class="show ? 'bg-opacity-60 pointer-events-auto' : 'bg-opacity-0'" @click="clickBackground" />
<div class="absolute top-0 right-0 w-64 h-full bg-primary transform transition-transform py-6 pointer-events-auto" :class="show ? '' : 'translate-x-64'" @click.stop> <div class="absolute top-0 right-0 w-64 h-full bg-primary transform transition-transform py-6 pointer-events-auto" :class="show ? '' : 'translate-x-64'" @click.stop>
<div class="px-6 mb-4"> <div class="px-6 mb-4">
<p v-if="socketConnected" class="text-base"> <p v-if="user" class="text-base">
Welcome, Welcome,
<strong>{{ username }}</strong> <strong>{{ username }}</strong>
</p> </p>
@ -16,16 +16,21 @@
</nuxt-link> </nuxt-link>
</template> </template>
</div> </div>
<div class="absolute bottom-0 left-0 w-full flex items-center py-6 px-6 text-gray-300"> <div class="absolute bottom-0 left-0 w-full py-6 px-6 text-gray-300">
<div v-if="serverConnectionConfig" class="mb-4 flex justify-center">
<p class="text-xs">{{ serverConnectionConfig.address }}</p>
</div>
<div class="flex items-center">
<p class="text-xs">{{ $config.version }}</p> <p class="text-xs">{{ $config.version }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
<div v-if="socketConnected" class="flex items-center" @click="logout"> <div v-if="user" class="flex items-center" @click="logout">
<p class="text-xs pr-2">Logout</p> <p class="text-xs pr-2">Logout</p>
<span class="material-icons text-sm">logout</span> <span class="material-icons text-sm">logout</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>
<script> <script>
@ -62,6 +67,9 @@ export default {
user() { user() {
return this.$store.state.user.user return this.$store.state.user.user
}, },
serverConnectionConfig() {
return this.$store.state.user.serverConnectionConfig
},
username() { username() {
return this.user ? this.user.username : '' return this.user ? this.user.username : ''
}, },
@ -112,7 +120,9 @@ export default {
await this.$axios.$post('/logout').catch((error) => { await this.$axios.$post('/logout').catch((error) => {
console.error(error) console.error(error)
}) })
this.$server.logout() this.$socket.logout()
await this.$db.logout()
this.$store.commit('user/logout')
this.$router.push('/connect') this.$router.push('/connect')
}, },
touchstart(e) { touchstart(e) {

View file

@ -448,15 +448,15 @@ export default {
this.$eventBus.$on('downloads-loaded', this.downloadsLoaded) this.$eventBus.$on('downloads-loaded', this.downloadsLoaded)
this.$store.commit('user/addSettingsListener', { id: 'lazy-bookshelf', meth: this.settingsUpdated }) this.$store.commit('user/addSettingsListener', { id: 'lazy-bookshelf', meth: this.settingsUpdated })
if (this.$server.socket) { // if (this.$server.socket) {
this.$server.socket.on('item_updated', this.libraryItemUpdated) // this.$server.socket.on('item_updated', this.libraryItemUpdated)
this.$server.socket.on('item_added', this.libraryItemAdded) // this.$server.socket.on('item_added', this.libraryItemAdded)
this.$server.socket.on('item_removed', this.libraryItemRemoved) // this.$server.socket.on('item_removed', this.libraryItemRemoved)
this.$server.socket.on('items_updated', this.libraryItemsUpdated) // this.$server.socket.on('items_updated', this.libraryItemsUpdated)
this.$server.socket.on('items_added', this.libraryItemsAdded) // this.$server.socket.on('items_added', this.libraryItemsAdded)
} else { // } else {
console.error('Bookshelf - Socket not initialized') // console.error('Bookshelf - Socket not initialized')
} // }
}, },
removeListeners() { removeListeners() {
var bookshelf = document.getElementById('bookshelf-wrapper') var bookshelf = document.getElementById('bookshelf-wrapper')
@ -468,28 +468,28 @@ export default {
this.$eventBus.$off('downloads-loaded', this.downloadsLoaded) this.$eventBus.$off('downloads-loaded', this.downloadsLoaded)
this.$store.commit('user/removeSettingsListener', 'lazy-bookshelf') this.$store.commit('user/removeSettingsListener', 'lazy-bookshelf')
if (this.$server.socket) { // if (this.$server.socket) {
this.$server.socket.off('item_updated', this.libraryItemUpdated) // this.$server.socket.off('item_updated', this.libraryItemUpdated)
this.$server.socket.off('item_added', this.libraryItemAdded) // this.$server.socket.off('item_added', this.libraryItemAdded)
this.$server.socket.off('item_removed', this.libraryItemRemoved) // this.$server.socket.off('item_removed', this.libraryItemRemoved)
this.$server.socket.off('items_updated', this.libraryItemsUpdated) // this.$server.socket.off('items_updated', this.libraryItemsUpdated)
this.$server.socket.off('items_added', this.libraryItemsAdded) // this.$server.socket.off('items_added', this.libraryItemsAdded)
} else { // } else {
console.error('Bookshelf - Socket not initialized') // console.error('Bookshelf - Socket not initialized')
} // }
} }
}, },
mounted() { mounted() {
if (this.$server.initialized) { // if (this.$server.initialized) {
this.init() // this.init()
} else { // } else {
this.initDownloads() // this.initDownloads()
} // }
this.$server.on('initialized', this.socketInit) this.$socket.on('initialized', this.socketInit)
this.initListeners() this.initListeners()
}, },
beforeDestroy() { beforeDestroy() {
this.$server.off('initialized', this.socketInit) this.$socket.off('initialized', this.socketInit)
this.removeListeners() this.removeListeners()
} }
} }

View file

@ -0,0 +1,312 @@
<template>
<div class="w-full max-w-md mx-auto px-4 sm:px-6 lg:px-8 z-10">
<div v-show="!loggedIn" class="mt-8 bg-primary overflow-hidden shadow rounded-lg p-6 w-full">
<template v-if="!showForm">
<div v-for="config in serverConnectionConfigs" :key="config.id" class="flex items-center py-4 my-1 border-b border-white border-opacity-10 relative" @click="connectToServer(config)">
<span class="material-icons-outlined text-xl text-gray-300">dns</span>
<p class="pl-3 pr-6 text-base text-gray-200">{{ config.name }}</p>
<div class="absolute top-0 right-0 h-full px-4 flex items-center" @click.stop="editServerConfig(config)">
<span class="material-icons text-lg text-gray-300">more_vert</span>
</div>
</div>
<div class="my-1 py-4 w-full">
<ui-btn class="w-full" @click="newServerConfigClick">Add New Server</ui-btn>
</div>
</template>
<template v-else>
<form v-show="!showAuth" @submit.prevent="submit" novalidate class="w-full">
<h2 class="text-lg leading-7 mb-2">Server address</h2>
<ui-text-input v-model="serverConfig.address" :disabled="processing || !networkConnected || serverConfig.id" placeholder="http://55.55.55.55:13378" type="url" class="w-full sm:w-72 h-10" />
<div class="flex justify-end">
<ui-btn :disabled="processing || !networkConnected" type="submit" :padding-x="3" class="h-10 mt-4">{{ networkConnected ? 'Submit' : 'No Internet' }}</ui-btn>
</div>
</form>
<template v-if="showAuth">
<div v-if="serverConfig.id" class="flex items-center mb-4" @click="showServerList">
<span class="material-icons text-gray-300">arrow_back</span>
</div>
<div class="flex items-center">
<p class="text-gray-300">{{ serverConfig.address }}</p>
<div class="flex-grow" />
<span v-if="!serverConfig.id" class="material-icons" style="font-size: 1.1rem" @click="editServerAddress">edit</span>
</div>
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
<form @submit.prevent="submitAuth" class="pt-3">
<ui-text-input v-model="serverConfig.username" :disabled="processing" placeholder="username" class="w-full mb-2 text-lg" />
<ui-text-input v-model="password" type="password" :disabled="processing" placeholder="password" class="w-full mb-2 text-lg" />
<div class="flex items-center pt-2">
<ui-icon-btn v-if="serverConfig.id" small bg-color="error" icon="delete" @click="removeServerConfigClick" />
<div class="flex-grow" />
<ui-btn :disabled="processing || !networkConnected" type="submit" class="mt-1 h-10">{{ networkConnected ? 'Submit' : 'No Internet' }}</ui-btn>
</div>
</form>
</template>
<div v-show="error" class="w-full rounded-lg bg-red-600 bg-opacity-10 border border-error border-opacity-50 py-3 px-2 flex items-center mt-4">
<span class="material-icons mr-2 text-error" style="font-size: 1.1rem">warning</span>
<p class="text-error">{{ error }}</p>
</div>
</template>
</div>
<div :class="processing ? 'opacity-100' : 'opacity-0 pointer-events-none'" class="fixed w-full h-full top-0 left-0 bg-black bg-opacity-75 flex items-center justify-center z-30 transition-opacity duration-500">
<div>
<div class="absolute top-0 left-0 w-full p-6 flex items-center flex-col justify-center z-0 short:hidden">
<img src="/Logo.png" class="h-20 w-20 mb-2" />
</div>
<svg class="animate-spin w-16 h-16" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
</div>
</div>
</div>
</template>
<script>
import { Dialog } from '@capacitor/dialog'
export default {
props: {},
data() {
return {
deviceData: null,
// serverConnectionConfigs: [
// {
// id: 'test1',
// name: 'http://192.168.0.1:3333 (root)',
// address: 'http://192.168.0.1:3333',
// username: 'root',
// token: 'asdf'
// },
// {
// id: 'test2',
// name: 'https://someserver.com (user)',
// address: 'https://someserver.com',
// username: 'user',
// token: 'asdf'
// }
// ],
loggedIn: false,
showAuth: false,
processing: false,
serverConfig: {
address: null,
username: null
},
password: null,
error: null,
showForm: false
}
},
computed: {
networkConnected() {
return this.$store.state.networkConnected
},
serverConnectionConfigs() {
return this.deviceData ? this.deviceData.serverConnectionConfigs || [] : []
},
lastServerConnectionConfigId() {
return this.deviceData ? this.deviceData.lastServerConnectionConfigId : null
},
lastServerConnectionConfig() {
if (!this.lastServerConnectionConfigId || !this.serverConnectionConfigs.length) return null
return this.serverConnectionConfigs.find((s) => s.id == this.lastServerConnectionConfigId)
}
},
methods: {
showServerList() {
this.showForm = false
this.showAuth = false
this.error = null
this.serverConfig = {
address: null,
username: null
}
},
async connectToServer(config) {
this.processing = true
this.serverConfig = {
...config
}
this.showForm = true
var success = await this.pingServerAddress(config.address)
if (!success) {
return
}
this.error = null
this.processing = false
var payload = await this.authenticateToken()
if (payload) {
this.setUserAndConnection(payload.user, payload.userDefaultLibraryId)
} else {
this.showAuth = true
}
},
async removeServerConfigClick() {
if (!this.serverConfig.id) return
const { value } = await Dialog.confirm({
title: 'Confirm',
message: `Remove this server config?`
})
if (value) {
this.processing = true
await this.$db.removeServerConnectionConfig(this.serverConfig.id)
this.deviceData.serverConnectionConfigs = this.deviceData.serverConnectionConfigs.filter((scc) => scc.id != this.serverConfig.id)
this.serverConfig = {
address: null,
username: null
}
this.password = null
this.processing = false
this.showAuth = false
this.showForm = !this.serverConnectionConfigs.length
}
},
editServerConfig(serverConfig) {
this.serverConfig = {
...serverConfig
}
this.showForm = true
this.showAuth = true
console.log('Edit server config', serverConfig)
},
newServerConfigClick() {
this.showForm = true
this.showAuth = false
},
editServerAddress() {
this.error = null
this.showAuth = false
},
validateServerUrl(url) {
try {
var urlObject = new URL(url)
var address = `${urlObject.protocol}//${urlObject.hostname}`
if (urlObject.port) address += ':' + urlObject.port
return address
} catch (error) {
console.error('Invalid URL', error)
return null
}
},
pingServerAddress(address) {
return this.$axios
.$get(`${address}/ping`, { timeout: 1000 })
.then((data) => data.success)
.catch((error) => {
console.error('Server check failed', error)
this.error = 'Failed to ping server'
return false
})
},
requestServerLogin() {
return this.$axios
.$post(`${this.serverConfig.address}/login`, { username: this.serverConfig.username, password: this.password })
.then((data) => {
if (!data.user) {
console.error(data.error)
this.error = data.error || 'Unknown Error'
return false
}
return data
})
.catch((error) => {
console.error('Server auth failed', error)
var errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
this.error = errorMsg
return false
})
},
async submit() {
if (!this.networkConnected) return
if (!this.serverConfig.address) return
if (!this.serverConfig.address.startsWith('http')) {
this.serverConfig.address = 'http://' + this.serverConfig.address
}
var validServerAddress = this.validateServerUrl(this.serverConfig.address)
if (!validServerAddress) {
this.error = 'Invalid server address'
return
}
this.serverConfig.address = validServerAddress
this.processing = true
this.error = null
var success = await this.pingServerAddress(this.serverConfig.address)
this.processing = false
if (success) this.showAuth = true
},
async submitAuth() {
if (!this.networkConnected) return
if (!this.serverConfig.username) {
this.error = 'Invalid username'
return
}
this.error = null
this.processing = true
var payload = await this.requestServerLogin()
this.processing = false
if (payload) {
this.setUserAndConnection(payload.user, payload.userDefaultLibraryId)
}
},
async setUserAndConnection(user, userDefaultLibraryId) {
if (user) {
console.log('Successfully logged in', JSON.stringify(user))
if (userDefaultLibraryId) {
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
}
this.serverConfig.token = user.token
var serverConnectionConfig = await this.$db.setServerConnectionConfig(this.serverConfig)
this.$store.commit('user/setUser', user)
this.$store.commit('user/setServerConnectionConfig', serverConnectionConfig)
this.$socket.connect(this.serverConfig.address, this.serverConfig.token)
this.$router.replace('/bookshelf')
}
},
async authenticateToken() {
if (!this.networkConnected) return
if (!this.serverConfig.token) {
this.error = 'No token'
return
}
this.error = null
this.processing = true
var authRes = await this.$axios.$post(`${this.serverConfig.address}/api/authorize`, null, { headers: { Authorization: `Bearer ${this.serverConfig.token}` } }).catch((error) => {
console.error('[Server] Server auth failed', error)
var errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
this.error = errorMsg
return false
})
this.processing = false
return authRes
},
async init() {
this.deviceData = await this.$db.getDeviceData()
if (this.lastServerConnectionConfig) {
this.connectToServer(this.lastServerConnectionConfig)
} else {
this.showForm = !this.serverConnectionConfigs.length
}
}
},
mounted() {
this.init()
}
}
</script>

View file

@ -1,254 +0,0 @@
<template>
<modals-modal v-model="show" width="100%" height="100%">
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
<p class="absolute top-6 left-2 text-2xl">Downloads</p>
<div class="absolute top-16 left-0 right-0 w-full px-2 py-1" :class="hasStoragePermission ? '' : 'text-error'">
<div class="flex items-center">
<span class="material-icons" @click="changeDownloadFolderClick">{{ hasStoragePermission ? 'folder' : 'error' }}</span>
<p v-if="hasStoragePermission" class="text-sm px-4" @click="changeDownloadFolderClick">{{ downloadFolderSimplePath || 'No Download Folder Selected' }}</p>
<p v-else class="text-sm px-4" @click="changeDownloadFolderClick">No Storage Permissions. Click here</p>
</div>
<!-- <p v-if="hasStoragePermission" class="text-xs text-gray-400 break-all max-w-full">{{ downloadFolderUri }}</p> -->
</div>
<div v-if="totalSize" class="absolute bottom-0 left-0 right-0 w-full py-3 text-center">
<p class="text-sm text-center text-gray-300">Total: {{ $bytesPretty(totalSize) }}</p>
</div>
<div v-if="downloadFolder && hasStoragePermission" class="w-full relative mt-10" @click.stop>
<div class="w-full h-10 relative">
<div class="absolute top-px left-0 z-10 w-full h-full flex">
<div class="flex-grow h-full bg-primary rounded-t-md mr-px" @click="showingDownloads = true">
<div class="flex items-center justify-center rounded-t-md border-t border-l border-r border-white border-opacity-20 h-full" :class="showingDownloads ? 'text-gray-100' : 'border-b bg-black bg-opacity-20 text-gray-400'">
<p>Downloads</p>
</div>
</div>
<div class="flex-grow h-full bg-primary rounded-t-md ml-px" @click="showingDownloads = false">
<div class="flex items-center justify-center h-full rounded-t-md border-t border-l border-r border-white border-opacity-20" :class="!showingDownloads ? 'text-gray-100' : 'border-b bg-black bg-opacity-20 text-gray-400'">
<p>Files</p>
</div>
</div>
</div>
</div>
<div class="list-content-body relative w-full overflow-x-hidden overflow-y-auto bg-primary rounded-b-lg border border-white border-opacity-20" style="max-height: 70vh; height: 70vh">
<template v-if="showingDownloads">
<div v-if="!totalDownloads" class="flex items-center justify-center h-40">
<p>No Downloads</p>
</div>
<ul v-else class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="download in downloadsDownloading">
<li :key="download.id" class="text-gray-400 select-none relative px-4 py-5 border-b border-white border-opacity-10 bg-black bg-opacity-10">
<div class="flex items-center justify-center">
<div class="w-3/4">
<span class="text-xs">({{ downloadingProgress[download.id] || 0 }}%) {{ download.isPreparing ? 'Preparing' : 'Downloading' }}...</span>
<p class="font-normal truncate text-sm">{{ download.audiobook.book.title }}</p>
</div>
<div class="flex-grow" />
<div class="shadow-sm text-white flex items-center justify-center rounded-full animate-spin">
<span class="material-icons">refresh</span>
</div>
</div>
</li>
</template>
<template v-for="download in downloadsReady">
<li :key="download.id" class="text-gray-50 select-none relative pr-4 pl-2 py-5 border-b border-white border-opacity-10" @click="jumpToAudiobook(download)">
<modals-downloads-download-item :download="download" @play="playDownload" @delete="clickDeleteDownload" />
</li>
</template>
</ul>
</template>
<template v-else>
<div class="w-full h-full">
<div class="w-full flex justify-around py-4 px-2">
<ui-btn small @click="searchFolder">Re-Scan</ui-btn>
<ui-btn small @click="changeDownloadFolderClick">Change Folder</ui-btn>
<ui-btn small color="error" @click="resetFolder">Reset</ui-btn>
</div>
<p v-if="isScanning" class="text-center my-8">Scanning Folder..</p>
<p v-else-if="!mediaScanResults" class="text-center my-8">No Files Found</p>
<template v-else>
<template v-for="mediaFolder in mediaScanResults.folders">
<div :key="mediaFolder.uri" class="w-full px-2 py-2">
<div class="flex items-center">
<span class="material-icons text-base text-white text-opacity-50">folder</span>
<p class="ml-1 py-0.5">{{ mediaFolder.name }}</p>
</div>
<div v-for="mediaFile in mediaFolder.files" :key="mediaFile.uri" class="ml-3 flex items-center">
<span class="material-icons text-base text-white text-opacity-50">{{ mediaFile.isAudio ? 'music_note' : 'image' }}</span>
<p class="ml-1 py-0.5">{{ mediaFile.name }}</p>
</div>
</div>
</template>
<template v-for="mediaFile in mediaScanResults.files">
<div :key="mediaFile.uri" class="w-full px-2 py-2">
<div class="flex items-center">
<span class="material-icons text-base text-white text-opacity-50">{{ mediaFile.isAudio ? 'music_note' : 'image' }}</span>
<p class="ml-1 py-0.5">{{ mediaFile.name }}</p>
</div>
</div>
</template>
</template>
</div>
</template>
</div>
</div>
<div v-else class="list-content-body relative w-full overflow-x-hidden overflow-y-auto bg-primary rounded-b-lg border border-white border-opacity-20 py-8 px-4" @click.stop>
<ui-btn class="w-full" color="info" @click="changeDownloadFolderClick">Select Folder</ui-btn>
</div>
</div>
</modals-modal>
</template>
<script>
import { Dialog } from '@capacitor/dialog'
import AudioDownloader from '@/plugins/audio-downloader'
import StorageManager from '@/plugins/storage-manager'
export default {
data() {
return {
downloadingProgress: {},
totalSize: 0,
showingDownloads: true,
isScanning: false
}
},
watch: {
async show(newValue) {
if (newValue) {
await this.$localStore.getDownloadFolder()
this.setTotalSize()
}
}
},
computed: {
show: {
get() {
return this.$store.state.downloads.showModal
},
set(val) {
this.$store.commit('downloads/setShowModal', val)
}
},
hasStoragePermission() {
return this.$store.state.hasStoragePermission
},
downloadFolder() {
return this.$store.state.downloadFolder
},
downloadFolderSimplePath() {
return this.downloadFolder ? this.downloadFolder.simplePath : null
},
downloadFolderUri() {
return this.downloadFolder ? this.downloadFolder.uri : null
},
totalDownloads() {
return this.downloadsReady.length + this.downloadsDownloading.length
},
downloadsDownloading() {
return this.downloads.filter((d) => d.isDownloading || d.isPreparing)
},
downloadsReady() {
return this.downloads.filter((d) => !d.isDownloading && !d.isPreparing)
},
downloads() {
return this.$store.state.downloads.downloads
},
mediaScanResults() {
return this.$store.state.downloads.mediaScanResults
}
},
methods: {
setTotalSize() {
var totalSize = 0
this.downloadsReady.forEach((dl) => {
totalSize += dl.size && !isNaN(dl.size) ? Number(dl.size) : 0
})
this.totalSize = totalSize
},
async changeDownloadFolderClick() {
if (!this.hasStoragePermission) {
console.log('Requesting Storage Permission')
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)
this.searchFolder()
}
},
async searchFolder() {
this.isScanning = true
var response = await StorageManager.searchFolder({ folderUrl: this.downloadFolderUri })
var searchResults = response
searchResults.folders = JSON.parse(searchResults.folders)
searchResults.files = JSON.parse(searchResults.files)
if (searchResults.folders.length) {
console.log('Search results folders length', searchResults.folders.length)
searchResults.folders = searchResults.folders.map((sr) => {
if (sr.files) {
sr.files = JSON.parse(sr.files)
}
return sr
})
this.$store.commit('downloads/setMediaScanResults', searchResults)
} else {
this.$toast.warning('No audio or image files found')
}
this.isScanning = false
},
async resetFolder() {
await this.$localStore.setDownloadFolder(null)
this.$store.commit('downloads/setMediaScanResults', {})
this.$toast.info('Unlinked Folder')
},
updateDownloadProgress({ audiobookId, progress }) {
this.$set(this.downloadingProgress, audiobookId, progress)
},
jumpToAudiobook(download) {
this.show = false
this.$router.push(`/audiobook/${download.id}`)
},
async clickDeleteDownload(download) {
const { value } = await Dialog.confirm({
title: 'Confirm',
message: 'Delete this download?'
})
if (value) {
this.$emit('deleteDownload', download)
}
},
playDownload(download) {
this.$store.commit('setPlayOnLoad', true)
this.$store.commit('setPlayingDownload', download)
this.show = false
}
},
mounted() {}
}
</script>
<style>
.list-content-body {
max-height: calc(75% - 40px);
}
</style>

View file

@ -55,7 +55,6 @@ export default {
this.show = false this.show = false
await this.$store.dispatch('libraries/fetch', lib.id) await this.$store.dispatch('libraries/fetch', lib.id)
this.$eventBus.$emit('library-changed', lib.id) this.$eventBus.$emit('library-changed', lib.id)
this.$localStore.setCurrentLibrary(lib)
} }
}, },
mounted() {} mounted() {}

View file

@ -53,7 +53,7 @@ export default {
this.$emit('select', folder) this.$emit('select', folder)
}, },
async init() { async init() {
var localFolders = (await this.$db.loadFolders()) || [] var localFolders = (await this.$db.getLocalFolders()) || []
this.localFolders = localFolders.filter((lf) => lf.mediaType == this.mediaType) this.localFolders = localFolders.filter((lf) => lf.mediaType == this.mediaType)
} }
}, },

View file

@ -40,6 +40,9 @@ export default {
networkConnected() { networkConnected() {
return this.$store.state.networkConnected return this.$store.state.networkConnected
}, },
user() {
return this.$store.state.user.user
},
currentLibraryId() { currentLibraryId() {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
}, },
@ -48,24 +51,6 @@ export default {
} }
}, },
methods: { methods: {
async connected(isConnected) {
if (isConnected) {
console.log('[Default] Connected socket sync user ab data')
// this.$store.dispatch('user/syncUserAudiobookData')
this.initSocketListeners()
// Load libraries
await this.$store.dispatch('libraries/load')
this.$eventBus.$emit('library-changed')
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
} else {
this.removeSocketListeners()
}
},
socketConnectionFailed(err) {
this.$toast.error('Socket connection error: ' + err.message)
},
currentUserAudiobookUpdate({ id, data }) { currentUserAudiobookUpdate({ id, data }) {
if (data) { if (data) {
console.log(`Current User Audiobook Updated ${id} ${JSON.stringify(data)}`) console.log(`Current User Audiobook Updated ${id} ${JSON.stringify(data)}`)
@ -218,41 +203,41 @@ export default {
return {} return {}
} }
}, },
async syncDownloads(downloads, downloadFolder) { // async syncDownloads(downloads, downloadFolder) {
console.log('Syncing downloads ' + downloads.length) // console.log('Syncing downloads ' + downloads.length)
var mediaScanResults = await this.searchFolder(downloadFolder) // var mediaScanResults = await this.searchFolder(downloadFolder)
this.$store.commit('downloads/setMediaScanResults', mediaScanResults) // this.$store.commit('downloads/setMediaScanResults', mediaScanResults)
// Filter out media folders without any audio files // // Filter out media folders without any audio files
var mediaFolders = mediaScanResults.folders.filter((sr) => { // var mediaFolders = mediaScanResults.folders.filter((sr) => {
if (!sr.files) return false // if (!sr.files) return false
var audioFiles = sr.files.filter((mf) => !!mf.isAudio) // var audioFiles = sr.files.filter((mf) => !!mf.isAudio)
return audioFiles.length // return audioFiles.length
}) // })
downloads.forEach((download) => { // downloads.forEach((download) => {
var mediaFolder = mediaFolders.find((mf) => mf.name === download.folderName) // var mediaFolder = mediaFolders.find((mf) => mf.name === download.folderName)
if (mediaFolder) { // if (mediaFolder) {
console.log('Found download ' + download.folderName) // console.log('Found download ' + download.folderName)
if (download.isMissing) { // if (download.isMissing) {
download.isMissing = false // download.isMissing = false
this.$store.commit('downloads/addUpdateDownload', download) // this.$store.commit('downloads/addUpdateDownload', download)
} // }
} else { // } else {
console.error('Download not found ' + download.folderName) // console.error('Download not found ' + download.folderName)
if (!download.isMissing) { // if (!download.isMissing) {
download.isMissing = true // download.isMissing = true
this.$store.commit('downloads/addUpdateDownload', download) // this.$store.commit('downloads/addUpdateDownload', download)
} // }
} // }
}) // })
// Match media scanned folders with books from server // // Match media scanned folders with books from server
if (this.isSocketConnected) { // if (this.isSocketConnected) {
await this.$store.dispatch('downloads/linkOrphanDownloads') // await this.$store.dispatch('downloads/linkOrphanDownloads')
} // }
}, // },
onItemDownloadUpdate(data) { onItemDownloadUpdate(data) {
console.log('ON ITEM DOWNLOAD UPDATE', JSON.stringify(data)) console.log('ON ITEM DOWNLOAD UPDATE', JSON.stringify(data))
}, },
@ -274,22 +259,18 @@ export default {
// this.onDownloadProgress(data) // this.onDownloadProgress(data)
// }) // })
var downloads = await this.$store.dispatch('downloads/loadFromStorage') // var downloads = await this.$store.dispatch('downloads/loadFromStorage')
var downloadFolder = await this.$localStore.getDownloadFolder() // var downloadFolder = await this.$localStore.getDownloadFolder()
// this.$eventBus.$emit('downloads-loaded')
if (downloadFolder) { // var checkPermission = await StorageManager.checkStoragePermission()
// await this.syncDownloads(downloads, downloadFolder) // console.log('Storage Permission is' + checkPermission.value)
} // if (!checkPermission.value) {
this.$eventBus.$emit('downloads-loaded') // console.log('Will require permissions')
// } else {
var checkPermission = await StorageManager.checkStoragePermission() // console.log('Has Storage Permission')
console.log('Storage Permission is' + checkPermission.value) // this.$store.commit('setHasStoragePermission', true)
if (!checkPermission.value) { // }
console.log('Will require permissions')
} else {
console.log('Has Storage Permission')
this.$store.commit('setHasStoragePermission', true)
}
}, },
async loadSavedSettings() { async loadSavedSettings() {
var userSavedServerSettings = await this.$localStore.getServerSettings() var userSavedServerSettings = await this.$localStore.getServerSettings()
@ -305,42 +286,44 @@ export default {
console.log('Loading offline user audiobook data') console.log('Loading offline user audiobook data')
await this.$store.dispatch('user/loadOfflineUserAudiobookData') await this.$store.dispatch('user/loadOfflineUserAudiobookData')
}, },
showErrorToast(message) {
this.$toast.error(message)
},
showSuccessToast(message) {
this.$toast.success(message)
},
async attemptConnection() { async attemptConnection() {
if (!this.$server) return
if (!this.networkConnected) { if (!this.networkConnected) {
console.warn('No network connection') console.warn('No network connection')
return return
} }
var localServerUrl = await this.$localStore.getServerUrl() var deviceData = await this.$db.getDeviceData()
var localUserToken = await this.$localStore.getToken() var serverConfig = null
if (localServerUrl) { if (deviceData && deviceData.lastServerConnectionConfigId && deviceData.serverConnectionConfigs.length) {
// Server and Token are stored serverConfig = deviceData.serverConnectionConfigs.find((scc) => scc.id == deviceData.lastServerConnectionConfigId)
if (localUserToken) {
var isSocketAlreadyEstablished = this.$server.socket
var success = await this.$server.connect(localServerUrl, localUserToken)
if (!success && !this.$server.url) {
// Bad URL
} else if (!success) {
// Failed to connect
} else if (isSocketAlreadyEstablished) {
// No need to wait for connect event
} }
if (!serverConfig) {
// No last server config set
return
} }
var authRes = await this.$axios.$post(`${serverConfig.address}/api/authorize`, null, { headers: { Authorization: `Bearer ${serverConfig.token}` } }).catch((error) => {
console.error('[Server] Server auth failed', error)
var errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
this.error = errorMsg
return false
})
if (!authRes) return
const { user, userDefaultLibraryId } = authRes
if (userDefaultLibraryId) {
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
} }
var serverConnectionConfig = await this.$db.setServerConnectionConfig(serverConfig)
this.$store.commit('user/setUser', user)
this.$store.commit('user/setServerConnectionConfig', serverConnectionConfig)
this.$socket.connect(serverConnectionConfig.address, serverConnectionConfig.token)
console.log('Successful connection on last saved connection config', JSON.stringify(serverConnectionConfig))
await this.initLibraries()
}, },
// audiobookAdded(audiobook) {
// this.$store.commit('libraries/updateFilterDataWithAudiobook', audiobook)
// },
// audiobookUpdated(audiobook) {
// this.$store.commit('libraries/updateFilterDataWithAudiobook', audiobook)
// },
itemRemoved(libraryItem) { itemRemoved(libraryItem) {
if (this.$route.name.startsWith('item')) { if (this.$route.name.startsWith('item')) {
if (this.$route.params.id === libraryItem.id) { if (this.$route.params.id === libraryItem.id) {
@ -348,60 +331,45 @@ export default {
} }
} }
}, },
// audiobooksAdded(audiobooks) {
// audiobooks.forEach((ab) => {
// this.audiobookAdded(ab)
// })
// },
// audiobooksUpdated(audiobooks) {
// audiobooks.forEach((ab) => {
// this.audiobookUpdated(ab)
// })
// },
userLoggedOut() { userLoggedOut() {
// Only cancels stream if streamining not playing downloaded // Only cancels stream if streamining not playing downloaded
this.$eventBus.$emit('close-stream') this.$eventBus.$emit('close-stream')
}, },
initSocketListeners() { socketConnectionUpdate(isConnected) {
if (this.$server.socket) { console.log('Socket connection update', isConnected)
// this.$server.socket.on('audiobook_updated', this.audiobookUpdated)
// this.$server.socket.on('audiobook_added', this.audiobookAdded)
this.$server.socket.on('item_removed', this.itemRemoved)
// this.$server.socket.on('audiobooks_updated', this.audiobooksUpdated)
// this.$server.socket.on('audiobooks_added', this.audiobooksAdded)
}
}, },
removeSocketListeners() { socketConnectionFailed(err) {
if (this.$server.socket) { this.$toast.error('Socket connection error: ' + err.message)
// this.$server.socket.off('audiobook_updated', this.audiobookUpdated) },
// this.$server.socket.off('audiobook_added', this.audiobookAdded) socketInit(data) {},
this.$server.socket.off('item_removed', this.itemRemoved) async initLibraries() {
// this.$server.socket.off('audiobooks_updated', this.audiobooksUpdated) await this.$store.dispatch('libraries/load')
// this.$server.socket.off('audiobooks_added', this.audiobooksAdded) this.$eventBus.$emit('library-changed')
} this.$store.dispatch('libraries/fetch', this.currentLibraryId)
} }
}, },
async mounted() { async mounted() {
if (!this.$server) return console.error('No Server') // this.$server.on('logout', this.userLoggedOut)
// console.log(`Default Mounted set SOCKET listeners ${this.$server.connected}`) // this.$server.on('connected', this.connected)
// this.$server.on('connectionFailed', this.socketConnectionFailed)
// this.$server.on('initialStream', this.initialStream)
// this.$server.on('currentUserAudiobookUpdate', this.currentUserAudiobookUpdate)
// this.$server.on('show_error_toast', this.showErrorToast)
// this.$server.on('show_success_toast', this.showSuccessToast)
if (this.$server.connected) { this.$socket.on('connection-update', this.socketConnectionUpdate)
console.log('Syncing on default mount') this.$socket.on('initialized', this.socketInit)
this.connected(true)
}
this.$server.on('logout', this.userLoggedOut)
this.$server.on('connected', this.connected)
this.$server.on('connectionFailed', this.socketConnectionFailed)
this.$server.on('initialStream', this.initialStream)
this.$server.on('currentUserAudiobookUpdate', this.currentUserAudiobookUpdate)
this.$server.on('show_error_toast', this.showErrorToast)
this.$server.on('show_success_toast', this.showSuccessToast)
if (this.$store.state.isFirstLoad) { if (this.$store.state.isFirstLoad) {
this.$store.commit('setIsFirstLoad', false) this.$store.commit('setIsFirstLoad', false)
await this.$store.dispatch('setupNetworkListener') await this.$store.dispatch('setupNetworkListener')
this.attemptConnection()
if (this.$store.state.user.serverConnectionConfig) {
await this.initLibraries()
} else {
await this.attemptConnection()
}
this.checkForUpdate() this.checkForUpdate()
this.loadSavedSettings() this.loadSavedSettings()
this.initMediaStore() this.initMediaStore()
@ -412,13 +380,9 @@ export default {
console.error('No Server beforeDestroy') console.error('No Server beforeDestroy')
return return
} }
this.removeSocketListeners()
this.$server.off('logout', this.userLoggedOut) this.$socket.off('connection-update', this.socketConnectionUpdate)
this.$server.off('connected', this.connected) this.$socket.off('initialized', this.socketInit)
this.$server.off('connectionFailed', this.socketConnectionFailed)
this.$server.off('initialStream', this.initialStream)
this.$server.off('show_error_toast', this.showErrorToast)
this.$server.off('show_success_toast', this.showSuccessToast)
} }
} }
</script> </script>

View file

@ -1,442 +0,0 @@
<template>
<div class="w-full h-full px-3 py-4 overflow-y-auto">
<div class="flex">
<div class="w-32">
<div class="relative">
<covers-book-cover :audiobook="audiobook" :download-cover="downloadedCover" :width="128" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm z-10" :style="{ width: 128 * progressPercent + 'px' }"></div>
</div>
<div class="flex my-4">
<p v-if="numTracks" class="text-sm">{{ numTracks }} Tracks</p>
</div>
</div>
<div class="flex-grow px-3">
<h1 class="text-lg">{{ title }}</h1>
<h3 v-if="series" class="font-book text-gray-300 text-lg leading-7">{{ seriesText }}</h3>
<p class="text-sm text-gray-400">by {{ author }}</p>
<p v-if="numTracks" class="text-gray-300 text-sm my-1">
{{ $elapsedPretty(duration) }}
<span class="px-4">{{ $bytesPretty(size) }}</span>
</p>
<div v-if="progressPercent > 0" class="px-4 py-2 bg-primary text-sm font-semibold rounded-md text-gray-200 mt-4 relative" :class="resettingProgress ? 'opacity-25' : ''">
<p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
<p v-if="progressPercent < 1" class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
<span class="material-icons text-sm">close</span>
</div>
</div>
<div v-if="(isConnected && (showPlay || showRead)) || isDownloadPlayable" class="flex mt-4 -mr-2">
<ui-btn v-if="showPlay" color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick">
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
<span class="px-1 text-sm">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : isDownloadPlayable ? 'Play local' : 'Play stream' }}</span>
</ui-btn>
<ui-btn v-if="showRead && isConnected" color="info" class="flex items-center justify-center mr-2" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
<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 && !isIos" 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>
</div>
</div>
<div class="w-full py-4">
<p>{{ description }}</p>
</div>
</div>
</template>
<script>
import Path from 'path'
import { Dialog } from '@capacitor/dialog'
import AudioDownloader from '@/plugins/audio-downloader'
import StorageManager from '@/plugins/storage-manager'
export default {
async asyncData({ store, params, redirect, app }) {
var audiobookId = params.id
var audiobook = null
if (app.$server.connected) {
audiobook = await app.$axios.$get(`/api/books/${audiobookId}`).catch((error) => {
console.error('Failed', error)
return false
})
} else {
var download = store.getters['downloads/getDownload'](audiobookId)
if (download) {
audiobook = download.audiobook
}
}
if (!audiobook) {
console.error('No audiobook...', params.id)
return redirect('/')
}
return {
audiobook
}
},
data() {
return {
resettingProgress: false
}
},
computed: {
isIos() {
return this.$platform === 'ios'
},
isConnected() {
return this.$store.state.socketConnected
},
bookCoverAspectRatio() {
return this.$store.getters['getBookCoverAspectRatio']
},
audiobookId() {
return this.audiobook.id
},
book() {
return this.audiobook.book || {}
},
title() {
return this.book.title
},
author() {
return this.book.author || 'Unknown'
},
description() {
return this.book.description || ''
},
series() {
return this.book.series || null
},
volumeNumber() {
return this.book.volumeNumber || null
},
seriesText() {
if (!this.series) return ''
if (!this.volumeNumber) return this.series
return `${this.series} #${this.volumeNumber}`
},
duration() {
return this.audiobook.duration
},
size() {
return this.audiobook.size
},
userAudiobook() {
return this.$store.getters['user/getUserAudiobook'](this.audiobookId)
},
userToken() {
return this.$store.getters['user/getToken']
},
userCurrentTime() {
return this.userAudiobook ? this.userAudiobook.currentTime : 0
},
userTimeRemaining() {
return Math.max(0, this.duration - this.userCurrentTime)
},
progressPercent() {
return this.userAudiobook ? this.userAudiobook.progress : 0
},
isStreaming() {
return this.$store.getters['isAudiobookStreaming'](this.audiobookId)
},
isPlaying() {
return this.$store.getters['isAudiobookPlaying'](this.audiobookId)
},
numTracks() {
if (this.audiobook.tracks) return this.audiobook.tracks.length
return this.audiobook.numTracks || 0
},
isMissing() {
return this.audiobook.isMissing
},
isIncomplete() {
return this.audiobook.isIncomplete
},
isDownloading() {
return this.downloadObj ? this.downloadObj.isDownloading : false
},
showPlay() {
return !this.isMissing && !this.isIncomplete && this.numTracks
},
showRead() {
return this.hasEbook && this.ebookFormat !== '.pdf'
},
hasEbook() {
return this.audiobook.numEbooks
},
ebookFormat() {
if (!this.audiobook || !this.audiobook.ebooks || !this.audiobook.ebooks.length) return null
return this.audiobook.ebooks[0].ext.substr(1)
},
isDownloadPreparing() {
return this.downloadObj ? this.downloadObj.isPreparing : false
},
isDownloadPlayable() {
return this.downloadObj && !this.isDownloading && !this.isDownloadPreparing
},
downloadedCover() {
return this.downloadObj ? this.downloadObj.cover : null
},
downloadObj() {
return this.$store.getters['downloads/getDownload'](this.audiobookId)
},
hasStoragePermission() {
return this.$store.state.hasStoragePermission
}
},
methods: {
readBook() {
this.$store.commit('openReader', this.audiobook)
},
playClick() {
this.$store.commit('setPlayOnLoad', true)
if (!this.isDownloadPlayable) {
// Stream
console.log('[PLAYCLICK] Set Playing STREAM ' + this.title)
this.$store.commit('setStreamAudiobook', this.audiobook)
this.$server.socket.emit('open_stream', this.audiobook.id)
} else {
// Local
console.log('[PLAYCLICK] Set Playing Local Download ' + this.title)
this.$store.commit('setPlayingDownload', this.downloadObj)
}
},
async clearProgressClick() {
const { value } = await Dialog.confirm({
title: 'Confirm',
message: 'Are you sure you want to reset your progress?'
})
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/me/audiobook/${this.audiobookId}/reset-progress`)
.then(() => {
console.log('Progress reset complete')
this.$toast.success(`Your progress was reset`)
})
.catch((error) => {
console.error('Progress reset failed', error)
})
}
this.resettingProgress = false
}
},
audiobookUpdated(audiobook) {
if (audiobook.id === this.audiobookId) {
console.log('Audiobook Updated - Fetch full audiobook')
this.$axios
.$get(`/api/books/${this.audiobookId}`)
.then((audiobook) => {
this.audiobook = audiobook
})
.catch((error) => {
console.error('Failed', error)
})
}
},
downloadClick() {
console.log('downloadClick ' + this.$server.connected + ' | ' + !!this.downloadObj)
if (!this.$server.connected) return
if (this.downloadObj) {
console.log('Already downloaded', this.downloadObj)
} else {
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
}
// Download Path
var dlFolder = this.$localStore.downloadFolder
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.changeDownloadFolderClick()
return
} else {
console.log('Has Download folder: ' + JSON.stringify(dlFolder))
}
var downloadObject = {
id: this.audiobookId,
downloadFolderUrl: dlFolder.uri,
audiobook: {
...audiobook
},
isPreparing: true,
isDownloading: false,
toastId: this.$toast(`Preparing download for "${this.title}"`, { timeout: false })
}
if (audiobook.tracks.length === 1) {
// Single track should not need preparation
console.log('Single track, start download no prep needed')
var track = audiobook.tracks[0]
var fileext = track.ext
console.log('Download Single Track Path: ' + track.path)
var relTrackPath = track.path.replace('\\', '/').replace(this.audiobook.path.replace('\\', '/'), '')
var url = `${this.$store.state.serverUrl}/s/book/${this.audiobookId}${relTrackPath}?token=${this.userToken}`
this.startDownload(url, fileext, downloadObject)
} else {
// Multi-track merge
this.$store.commit('downloads/addUpdateDownload', downloadObject)
var prepareDownloadPayload = {
audiobookId: this.audiobookId,
audioFileType: 'same',
type: 'singleAudio'
}
this.$server.socket.emit('download', prepareDownloadPayload)
}
},
getCoverUrlForDownload() {
if (!this.book || !this.book.cover) return null
var cover = this.book.cover
if (cover.startsWith('http')) return cover
var coverSrc = this.$store.getters['audiobooks/getBookCoverSrc'](this.audiobook)
return coverSrc
// var _clean = cover.replace(/\\/g, '/')
// if (_clean.startsWith('/local')) {
// var _cover = process.env.NODE_ENV !== 'production' && process.env.PROD !== '1' ? _clean.replace('/local', '') : _clean
// return `${this.$store.state.serverUrl}${_cover}?token=${this.userToken}&ts=${Date.now()}`
// } else if (_clean.startsWith('/metadata')) {
// return `${this.$store.state.serverUrl}${_clean}?token=${this.userToken}&ts=${Date.now()}`
// }
// return _clean
},
async startDownload(url, fileext, download) {
this.$toast.update(download.toastId, { content: `Downloading "${download.audiobook.book.title}"...` })
var coverDownloadUrl = this.getCoverUrlForDownload()
var coverFilename = null
if (coverDownloadUrl) {
var coverNoQueryString = coverDownloadUrl.split('?')[0]
var coverExt = Path.extname(coverNoQueryString) || '.jpg'
coverFilename = `cover-${download.id}${coverExt}`
}
download.isDownloading = true
download.isPreparing = false
download.filename = `${download.audiobook.book.title}${fileext}`
this.$store.commit('downloads/addUpdateDownload', download)
console.log('Starting Download URL', url)
var downloadRequestPayload = {
audiobookId: download.id,
filename: download.filename,
coverFilename,
coverDownloadUrl,
downloadUrl: url,
title: download.audiobook.book.title,
downloadFolderUrl: download.downloadFolderUrl
}
var downloadRes = await AudioDownloader.download(downloadRequestPayload)
if (downloadRes.error) {
var errorMsg = downloadRes.error || 'Unknown error'
console.error('Download error', errorMsg)
this.$toast.update(download.toastId, { content: `Error: ${errorMsg}`, options: { timeout: 5000, type: 'error' } })
this.$store.commit('downloads/removeDownload', download)
}
},
downloadReady(prepareDownload) {
var download = this.$store.getters['downloads/getDownload'](prepareDownload.audiobookId)
if (download) {
var fileext = prepareDownload.ext
var url = `${this.$store.state.serverUrl}/downloads/${prepareDownload.id}/${prepareDownload.filename}?token=${this.userToken}`
this.startDownload(url, fileext, download)
} else {
console.error('Prepare download killed but download not found', prepareDownload)
}
},
downloadKilled(prepareDownload) {
var download = this.$store.getters['downloads/getDownload'](prepareDownload.audiobookId)
if (download) {
this.$toast.update(download.toastId, { content: `Prepare download killed for "${download.audiobook.book.title}"`, options: { timeout: 5000, type: 'error' } })
this.$store.commit('downloads/removeDownload', download)
} else {
console.error('Prepare download killed but download not found', prepareDownload)
}
},
downloadFailed(prepareDownload) {
var download = this.$store.getters['downloads/getDownload'](prepareDownload.audiobookId)
if (download) {
this.$toast.update(download.toastId, { content: `Prepare download failed for "${download.audiobook.book.title}"`, options: { timeout: 5000, type: 'error' } })
this.$store.commit('downloads/removeDownload', download)
} else {
console.error('Prepare download failed but download not found', prepareDownload)
}
}
},
mounted() {
if (!this.$server.socket) {
console.warn('Audiobook Page mounted: Server socket not set')
} else {
this.$server.socket.on('download_ready', this.downloadReady)
this.$server.socket.on('download_killed', this.downloadKilled)
this.$server.socket.on('download_failed', this.downloadFailed)
this.$server.socket.on('audiobook_updated', this.audiobookUpdated)
}
},
beforeDestroy() {
if (!this.$server.socket) {
console.warn('Audiobook Page beforeDestroy: Server socket not set')
} else {
this.$server.socket.off('download_ready', this.downloadReady)
this.$server.socket.off('download_killed', this.downloadKilled)
this.$server.socket.off('download_failed', this.downloadFailed)
this.$server.socket.off('audiobook_updated', this.audiobookUpdated)
}
}
}
</script>

View file

@ -8,20 +8,20 @@
<div> <div>
<p class="mb-4 text-center text-xl"> <p class="mb-4 text-center text-xl">
Bookshelf empty Bookshelf empty
<span v-show="isSocketConnected"> <span v-show="user">
for library for library
<strong>{{ currentLibraryName }}</strong> <strong>{{ currentLibraryName }}</strong>
</span> </span>
</p> </p>
<div class="w-full" v-if="!isSocketConnected"> <div class="w-full" v-if="!user">
<div class="flex justify-center items-center mb-3"> <div class="flex justify-center items-center mb-3">
<span class="material-icons text-error text-lg">cloud_off</span> <span class="material-icons text-error text-lg">cloud_off</span>
<p class="pl-2 text-error text-sm">Audiobookshelf server not connected.</p> <p class="pl-2 text-error text-sm">Audiobookshelf server not connected.</p>
</div> </div>
<p class="px-4 text-center text-error absolute bottom-12 left-0 right-0 mx-auto"><strong>Important!</strong> This app requires that you are running <u>your own server</u> and does not provide any content.</p> <!-- <p class="px-4 text-center text-error absolute bottom-12 left-0 right-0 mx-auto"><strong>Important!</strong> This app requires that you are running <u>your own server</u> and does not provide any content.</p> -->
</div> </div>
<div class="flex justify-center"> <div class="flex justify-center">
<ui-btn v-if="!isSocketConnected" small @click="$router.push('/connect')" class="w-32">Connect</ui-btn> <ui-btn v-if="!user" small @click="$router.push('/connect')" class="w-32">Connect</ui-btn>
</div> </div>
</div> </div>
</div> </div>
@ -46,6 +46,9 @@ export default {
return ab return ab
}) })
}, },
user() {
return this.$store.state.user.user
},
isSocketConnected() { isSocketConnected() {
return this.$store.state.socketConnected return this.$store.state.socketConnected
}, },
@ -133,16 +136,16 @@ export default {
this.shelves = categories this.shelves = categories
console.log('Shelves', this.shelves) console.log('Shelves', this.shelves)
}, },
async socketInit(isConnected) { // async socketInit(isConnected) {
if (isConnected && this.currentLibraryId) { // if (isConnected && this.currentLibraryId) {
console.log('Connected - Load from server') // console.log('Connected - Load from server')
await this.fetchCategories() // await this.fetchCategories()
} else { // } else {
console.log('Disconnected - Reset to local storage') // console.log('Disconnected - Reset to local storage')
this.shelves = this.downloadOnlyShelves // this.shelves = this.downloadOnlyShelves
} // }
this.loading = false // this.loading = false
}, // },
async libraryChanged(libid) { async libraryChanged(libid) {
if (this.isSocketConnected && this.currentLibraryId) { if (this.isSocketConnected && this.currentLibraryId) {
await this.fetchCategories() await this.fetchCategories()
@ -211,43 +214,24 @@ export default {
}) })
}, },
initListeners() { initListeners() {
this.$server.on('initialized', this.socketInit) // this.$server.on('initialized', this.socketInit)
this.$eventBus.$on('library-changed', this.libraryChanged) this.$eventBus.$on('library-changed', this.libraryChanged)
this.$eventBus.$on('downloads-loaded', this.downloadsLoaded) this.$eventBus.$on('downloads-loaded', this.downloadsLoaded)
if (this.$server.socket) {
this.$server.socket.on('audiobook_updated', this.audiobookUpdated)
this.$server.socket.on('audiobook_added', this.audiobookAdded)
this.$server.socket.on('audiobook_removed', this.audiobookRemoved)
this.$server.socket.on('audiobooks_updated', this.audiobooksUpdated)
this.$server.socket.on('audiobooks_added', this.audiobooksAdded)
} else {
console.error('Error socket not initialized')
}
}, },
removeListeners() { removeListeners() {
this.$server.off('initialized', this.socketInit) // this.$server.off('initialized', this.socketInit)
this.$eventBus.$off('library-changed', this.libraryChanged) this.$eventBus.$off('library-changed', this.libraryChanged)
this.$eventBus.$off('downloads-loaded', this.downloadsLoaded) this.$eventBus.$off('downloads-loaded', this.downloadsLoaded)
if (this.$server.socket) {
this.$server.socket.off('audiobook_updated', this.audiobookUpdated)
this.$server.socket.off('audiobook_added', this.audiobookAdded)
this.$server.socket.off('audiobook_removed', this.audiobookRemoved)
this.$server.socket.off('audiobooks_updated', this.audiobooksUpdated)
this.$server.socket.off('audiobooks_added', this.audiobooksAdded)
} else {
console.error('Error socket not initialized')
}
} }
}, },
mounted() { mounted() {
this.initListeners() this.initListeners()
if (this.$server.initialized && this.currentLibraryId) {
this.fetchCategories() this.fetchCategories()
} else { // if (this.$server.initialized && this.currentLibraryId) {
this.shelves = this.downloadOnlyShelves // this.fetchCategories()
} // } else {
// this.shelves = this.downloadOnlyShelves
// }
}, },
beforeDestroy() { beforeDestroy() {
this.removeListeners() this.removeListeners()

View file

@ -10,53 +10,11 @@
</div> </div>
<p class="hidden absolute short:block top-1.5 left-12 p-2 font-book text-xl">audiobookshelf</p> <p class="hidden absolute short:block top-1.5 left-12 p-2 font-book text-xl">audiobookshelf</p>
<p class="absolute bottom-16 left-0 right-0 px-2 text-center text-error"><strong>Important!</strong> This app requires that you are running <u>your own server</u> and does not provide any content.</p> <!-- <p class="absolute bottom-16 left-0 right-0 px-2 text-center text-error"><strong>Important!</strong> This app requires that you are running <u>your own server</u> and does not provide any content.</p> -->
<div class="w-full max-w-md mx-auto px-4 sm:px-6 lg:px-8 z-10"> <connection-server-connect-form :server-connection-configs="serverConnectionConfigs" :last-server-connection-config="lastServerConnectionConfig" />
<div v-show="loggedIn" class="mt-8 bg-primary overflow-hidden shadow rounded-lg p-6 text-center">
<p class="text-success text-xl mb-2">Login Success!</p>
<p>Connecting socket..</p>
</div> </div>
<div v-show="!loggedIn" class="mt-8 bg-primary overflow-hidden shadow rounded-lg p-6 w-full">
<h2 class="text-lg leading-7 mb-4">Server address</h2>
<form v-show="!showAuth" @submit.prevent="submit" novalidate class="w-full">
<ui-text-input v-model="serverUrl" :disabled="processing || !networkConnected" placeholder="http://55.55.55.55:13378" type="url" class="w-full sm:w-72 h-10" />
<div class="flex justify-end">
<ui-btn :disabled="processing || !networkConnected" type="submit" :padding-x="3" class="h-10 mt-4">{{ networkConnected ? 'Submit' : 'No Internet' }}</ui-btn>
</div>
</form>
<template v-if="showAuth">
<div class="flex items-center">
<p class="">{{ serverUrl }}</p>
<div class="flex-grow" />
<span class="material-icons" style="font-size: 1.1rem" @click="editServerUrl">edit</span>
</div>
<div class="w-full h-px bg-gray-200 my-2" />
<form @submit.prevent="submitAuth" class="pt-3">
<ui-text-input v-model="username" :disabled="processing" placeholder="username" class="w-full my-1 text-lg" />
<ui-text-input v-model="password" type="password" :disabled="processing" placeholder="password" class="w-full my-1 text-lg" />
<ui-btn :disabled="processing || !networkConnected" type="submit" class="mt-1 h-10">{{ networkConnected ? 'Submit' : 'No Internet' }}</ui-btn>
</form>
</template>
<div v-show="error" class="w-full rounded-lg bg-red-600 bg-opacity-10 border border-error border-opacity-50 py-3 px-2 flex items-center mt-4">
<span class="material-icons mr-2 text-error" style="font-size: 1.1rem">warning</span>
<p class="text-error">{{ error }}</p>
</div>
</div>
</div>
</div>
<div :class="processing ? 'opacity-100' : 'opacity-0 pointer-events-none'" class="fixed w-full h-full top-0 left-0 bg-black bg-opacity-75 flex items-center justify-center z-30 transition-opacity duration-500">
<div>
<div class="absolute top-0 left-0 w-full p-6 flex items-center flex-col justify-center z-0 short:hidden">
<img src="/Logo.png" class="h-20 w-20 mb-2" />
</div>
<svg class="animate-spin w-16 h-16" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
</div>
</div>
<div class="flex items-center justify-center pt-4 fixed bottom-4 left-0 right-0"> <div class="flex items-center justify-center pt-4 fixed bottom-4 left-0 right-0">
<a href="https://github.com/advplyr/audiobookshelf-app" target="_blank" class="text-sm pr-2">Follow the project on Github</a> <a href="https://github.com/advplyr/audiobookshelf-app" target="_blank" class="text-sm pr-2">Follow the project on Github</a>
<a href="https://github.com/advplyr/audiobookshelf-app" target="_blank" <a href="https://github.com/advplyr/audiobookshelf-app" target="_blank"
@ -74,15 +32,7 @@
export default { export default {
layout: 'blank', layout: 'blank',
data() { data() {
return { return {}
serverUrl: null,
processing: false,
showAuth: false,
username: null,
password: null,
error: null,
loggedIn: false
}
}, },
computed: { computed: {
networkConnected() { networkConnected() {
@ -90,110 +40,12 @@ export default {
} }
}, },
methods: { methods: {
async submit() {
if (!this.networkConnected) {
return
}
if (!this.serverUrl.startsWith('http')) {
this.serverUrl = 'http://' + this.serverUrl
}
this.processing = true
this.error = null
var response = await this.$server.check(this.serverUrl)
this.processing = false
if (!response || response.error) {
console.error('Server invalid')
this.error = response ? response.error : 'Invalid Server'
} else {
this.showAuth = true
}
},
async submitAuth() {
if (!this.networkConnected) {
return
}
if (!this.username) {
this.error = 'Invalid username'
return
}
this.error = null
this.processing = true
var response = await this.$server.login(this.serverUrl, this.username, this.password)
this.processing = false
if (response.error) {
console.error('Login failed')
this.error = response.error
} else {
console.log('Login Success!')
this.loggedIn = true
}
},
editServerUrl() {
this.error = null
this.showAuth = false
},
redirect() {
if (this.$route.query && this.$route.query.redirect) {
this.$router.replace(this.$route.query.redirect)
} else {
this.$router.replace('/bookshelf')
}
},
socketConnected() {
console.log('Socket connected')
this.redirect()
},
async init() { async init() {
await this.$store.dispatch('setupNetworkListener') await this.$store.dispatch('setupNetworkListener')
if (!this.$server) {
console.error('Invalid server not initialized')
return
}
if (this.$server.connected) {
console.warn('Server already connected')
return this.redirect()
}
this.$server.on('connected', this.socketConnected)
var localServerUrl = await this.$localStore.getServerUrl()
var localUserToken = await this.$localStore.getToken()
if (!this.networkConnected) return
if (localServerUrl) {
this.serverUrl = localServerUrl
if (localUserToken) {
this.processing = true
var response = await this.$server.connect(localServerUrl, localUserToken)
if (!response || response.error) {
var errorMsg = response ? response.error : 'Unknown Error'
this.processing = false
this.error = errorMsg
if (!this.$server.url) {
this.serverUrl = null
this.showAuth = false
}
return
}
console.log('Server connect success')
this.showAuth = true
} else {
this.submit()
}
}
} }
}, },
mounted() { mounted() {
this.init() this.init()
},
beforeDestroy() {
if (!this.$server) {
console.error('Connected beforeDestroy: No Server')
return
}
this.$server.off('connected', this.socketConnected)
} }
} }
</script> </script>

View file

@ -277,7 +277,7 @@ export default {
} }
}, },
async init() { async init() {
this.localFolders = (await this.$db.loadFolders()) || [] this.localFolders = (await this.$db.getLocalFolders()) || []
AudioDownloader.addListener('onDownloadProgress', this.onDownloadProgress) AudioDownloader.addListener('onDownloadProgress', this.onDownloadProgress)
} }
}, },

View file

@ -61,7 +61,7 @@ export default {
var libraryItemId = params.id var libraryItemId = params.id
var libraryItem = null var libraryItem = null
if (app.$server.connected) { if (store.state.user.serverConnectionConfig) {
libraryItem = await app.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => { libraryItem = await app.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => {
console.error('Failed', error) console.error('Failed', error)
return false return false
@ -218,10 +218,10 @@ export default {
// } // }
}, },
async clearProgressClick() { async clearProgressClick() {
if (!this.$server.connected) { // if (!this.$server.connected) {
this.$toast.info('Clear downloaded book progress not yet implemented') // this.$toast.info('Clear downloaded book progress not yet implemented')
return // return
} // }
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: 'Confirm',
@ -264,9 +264,6 @@ export default {
this.download() this.download()
}, },
async download(selectedLocalFolder = null) { async download(selectedLocalFolder = null) {
console.log('downloadClick ' + this.$server.connected + ' | ' + !!this.downloadObj)
if (!this.$server.connected) return
if (!this.numTracks || this.downloadObj) { if (!this.numTracks || this.downloadObj) {
return return
} }
@ -274,7 +271,7 @@ export default {
// Get the local folder to download to // Get the local folder to download to
var localFolder = selectedLocalFolder var localFolder = selectedLocalFolder
if (!localFolder) { if (!localFolder) {
var localFolders = (await this.$db.loadFolders()) || [] var localFolders = (await this.$db.getLocalFolders()) || []
console.log('Local folders loaded', localFolders.length) console.log('Local folders loaded', localFolders.length)
var foldersWithMediaType = localFolders.filter((lf) => { var foldersWithMediaType = localFolders.filter((lf) => {
console.log('Checking local folder', lf.mediaType) console.log('Checking local folder', lf.mediaType)
@ -468,24 +465,24 @@ export default {
} }
}, },
mounted() { mounted() {
if (!this.$server.socket) { // if (!this.$server.socket) {
console.warn('Library Item Page mounted: Server socket not set') // console.warn('Library Item Page mounted: Server socket not set')
} else { // } else {
this.$server.socket.on('download_ready', this.downloadReady) // this.$server.socket.on('download_ready', this.downloadReady)
this.$server.socket.on('download_killed', this.downloadKilled) // this.$server.socket.on('download_killed', this.downloadKilled)
this.$server.socket.on('download_failed', this.downloadFailed) // this.$server.socket.on('download_failed', this.downloadFailed)
this.$server.socket.on('item_updated', this.itemUpdated) // this.$server.socket.on('item_updated', this.itemUpdated)
} // }
}, },
beforeDestroy() { beforeDestroy() {
if (!this.$server.socket) { // if (!this.$server.socket) {
console.warn('Library Item Page beforeDestroy: Server socket not set') // console.warn('Library Item Page beforeDestroy: Server socket not set')
} else { // } else {
this.$server.socket.off('download_ready', this.downloadReady) // this.$server.socket.off('download_ready', this.downloadReady)
this.$server.socket.off('download_killed', this.downloadKilled) // this.$server.socket.off('download_killed', this.downloadKilled)
this.$server.socket.off('download_failed', this.downloadFailed) // this.$server.socket.off('download_failed', this.downloadFailed)
this.$server.socket.off('item_updated', this.itemUpdated) // this.$server.socket.off('item_updated', this.itemUpdated)
} // }
} }
} }
</script> </script>

View file

@ -82,7 +82,7 @@ export default {
this.$router.push(`/localMedia/folders/${folderObj.id}?scan=1`) this.$router.push(`/localMedia/folders/${folderObj.id}?scan=1`)
}, },
async init() { async init() {
this.localFolders = (await this.$db.loadFolders()) || [] this.localFolders = (await this.$db.getLocalFolders()) || []
} }
}, },
mounted() { mounted() {

View file

@ -13,7 +13,7 @@
<p v-if="bookResults.length" class="font-semibold text-sm mb-1">Books</p> <p v-if="bookResults.length" class="font-semibold text-sm mb-1">Books</p>
<template v-for="bookResult in bookResults"> <template v-for="bookResult in bookResults">
<div :key="bookResult.audiobook.id" class="w-full h-16 py-1"> <div :key="bookResult.audiobook.id" class="w-full h-16 py-1">
<nuxt-link :to="`/audiobook/${bookResult.audiobook.id}`"> <nuxt-link :to="`/item/${bookResult.audiobook.id}`">
<cards-book-search-card :audiobook="bookResult.audiobook" :search="lastSearch" :match-key="bookResult.matchKey" :match-text="bookResult.matchText" /> <cards-book-search-card :audiobook="bookResult.audiobook" :search="lastSearch" :match-key="bookResult.matchKey" :match-text="bookResult.matchText" />
</nuxt-link> </nuxt-link>
</div> </div>

View file

@ -11,7 +11,7 @@ export default function ({ $axios, store }) {
console.warn('[Axios] No Bearer Token for request') console.warn('[Axios] No Bearer Token for request')
} }
var serverUrl = store.state.serverUrl var serverUrl = store.getters['user/getServerAddress']
if (serverUrl) { if (serverUrl) {
config.url = `${serverUrl}${config.url}` config.url = `${serverUrl}${config.url}`
} }

View file

@ -1,11 +1,13 @@
import { registerPlugin } from '@capacitor/core'; import { registerPlugin, Capacitor } from '@capacitor/core';
const DbManager = registerPlugin('DbManager'); const isWeb = Capacitor.getPlatform() == 'web'
const DbManager = registerPlugin('DbManager')
class DbService { class DbService {
constructor() { } constructor() { }
save(db, key, value) { save(db, key, value) {
if (isWeb) return
return DbManager.saveFromWebview({ db, key, value }).then(() => { return DbManager.saveFromWebview({ db, key, value }).then(() => {
console.log('Saved data', db, key, JSON.stringify(value)) console.log('Saved data', db, key, JSON.stringify(value))
}).catch((error) => { }).catch((error) => {
@ -14,6 +16,7 @@ class DbService {
} }
load(db, key) { load(db, key) {
if (isWeb) return null
return DbManager.loadFromWebview({ db, key }).then((data) => { return DbManager.loadFromWebview({ db, key }).then((data) => {
console.log('Loaded data', db, key, JSON.stringify(data)) console.log('Loaded data', db, key, JSON.stringify(data))
return data return data
@ -23,7 +26,37 @@ class DbService {
}) })
} }
loadFolders() { getDeviceData() {
if (isWeb) return {}
return DbManager.getDeviceData_WV().then((data) => {
console.log('Loaded device data', JSON.stringify(data))
return data
})
}
setServerConnectionConfig(serverConnectionConfig) {
if (isWeb) return null
return DbManager.setCurrentServerConnectionConfig_WV(serverConnectionConfig).then((data) => {
console.log('Set server connection config', JSON.stringify(data))
return data
})
}
removeServerConnectionConfig(serverConnectionConfigId) {
if (isWeb) return null
return DbManager.removeServerConnectionConfig_WV({ serverConnectionConfigId }).then((data) => {
console.log('Removed server connection config', serverConnectionConfigId)
return true
})
}
logout() {
if (isWeb) return null
return DbManager.logout_WV()
}
getLocalFolders() {
if (isWeb) return []
return DbManager.getLocalFolders_WV().then((data) => { return DbManager.getLocalFolders_WV().then((data) => {
console.log('Loaded local folders', JSON.stringify(data)) console.log('Loaded local folders', JSON.stringify(data))
if (data.folders && typeof data.folders == 'string') { if (data.folders && typeof data.folders == 'string') {
@ -37,6 +70,7 @@ class DbService {
} }
getLocalFolder(folderId) { getLocalFolder(folderId) {
if (isWeb) return null
return DbManager.getLocalFolder_WV({ folderId }).then((data) => { return DbManager.getLocalFolder_WV({ folderId }).then((data) => {
console.log('Got local folder', JSON.stringify(data)) console.log('Got local folder', JSON.stringify(data))
return data return data
@ -44,6 +78,7 @@ class DbService {
} }
getLocalMediaItemsInFolder(folderId) { getLocalMediaItemsInFolder(folderId) {
if (isWeb) return []
return DbManager.getLocalMediaItemsInFolder_WV({ folderId }).then((data) => { return DbManager.getLocalMediaItemsInFolder_WV({ folderId }).then((data) => {
console.log('Loaded local media items in folder', JSON.stringify(data)) console.log('Loaded local media items in folder', JSON.stringify(data))
if (data.localMediaItems && typeof data.localMediaItems == 'string') { if (data.localMediaItems && typeof data.localMediaItems == 'string') {

View file

@ -4,6 +4,8 @@ import { Dialog } from '@capacitor/dialog'
import { StatusBar, Style } from '@capacitor/status-bar'; import { StatusBar, Style } from '@capacitor/status-bar';
import { formatDistance, format } from 'date-fns' import { formatDistance, format } from 'date-fns'
Vue.prototype.$eventBus = new Vue()
const setStatusBarStyleDark = async () => { const setStatusBarStyleDark = async () => {
await StatusBar.setStyle({ style: Style.Dark }) await StatusBar.setStyle({ style: Style.Dark })
} }
@ -22,9 +24,7 @@ App.addListener('backButton', async ({ canGoBack }) => {
} else { } else {
window.history.back() window.history.back()
} }
}); })
Vue.prototype.$eventBus = new Vue()
Vue.prototype.$isDev = process.env.NODE_ENV !== 'production' Vue.prototype.$isDev = process.env.NODE_ENV !== 'production'

View file

@ -8,50 +8,6 @@ class LocalStorage {
this.downloadFolder = null this.downloadFolder = 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) { async setDownloadFolder(folderObj) {
try { try {
if (folderObj) { if (folderObj) {
@ -82,15 +38,6 @@ class LocalStorage {
} }
} }
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) { async setUserSettings(settings) {
try { try {
await Storage.set({ key: 'userSettings', value: JSON.stringify(settings) }) await Storage.set({ key: 'userSettings', value: JSON.stringify(settings) })

View file

@ -1,5 +1,73 @@
import Server from '../Server' import Vue from 'vue'
import { io } from 'socket.io-client'
import EventEmitter from 'events'
export default function ({ store, $axios }, inject) { class ServerSocket extends EventEmitter {
inject('server', new Server(store, $axios)) constructor(store) {
super()
this.$store = store
this.socket = null
this.connected = false
this.serverAddress = null
this.token = null
}
connect(serverAddress, token) {
this.serverAddress = serverAddress
this.token = token
console.log('[SOCKET] Connect Socket', this.serverAddress)
const socketOptions = {
transports: ['websocket'],
upgrade: false,
// reconnectionAttempts: 3
}
this.socket = io(this.serverAddress, socketOptions)
this.setSocketListeners()
}
logout() {
if (this.socket) this.socket.disconnect()
}
setSocketListeners() {
if (!this.socket) return
this.socket.on('connect', this.onConnect.bind(this))
this.socket.on('disconnect', this.onDisconnect.bind(this))
this.socket.on('init', this.onInit.bind(this))
}
onConnect() {
console.log('[SOCKET] Socket Connected ' + this.socket.id)
this.connected = true
this.$store.commit('setSocketConnected', true)
this.emit('connection-update', true)
}
onDisconnect(reason) {
console.log('[SOCKET] Socket Disconnected: ' + reason)
this.connected = false
this.$store.commit('setSocketConnected', false)
this.emit('connection-update', false)
this.socket.removeAllListeners()
if (this.socket.io && this.socket.io.removeAllListeners) {
this.socket.io.removeAllListeners()
}
}
onInit(data) {
console.log('[SOCKET] Initial socket data received', data)
if (data.serverSettings) {
this.$store.commit('setServerSettings', data.serverSettings)
}
this.emit('initialized', true)
}
}
export default ({ app, store }, inject) => {
console.log('Check event bus', this, Vue.prototype.$eventBus)
inject('socket', new ServerSocket(store))
} }

View file

@ -1,35 +0,0 @@
export const state = () => ({
})
export const getters = {
getBookCoverSrc: (state, getters, rootState, rootGetters) => (bookItem, placeholder = '/book_placeholder.jpg') => {
if (!bookItem) return placeholder
var book = bookItem.book
if (!book || !book.cover || book.cover === placeholder) return placeholder
// Absolute URL covers (should no longer be used)
if (book.cover.startsWith('http:') || book.cover.startsWith('https:')) return book.cover
var userToken = rootGetters['user/getToken']
var bookLastUpdate = book.lastUpdate || Date.now()
if (!bookItem.id) {
console.error('No book item id', bookItem)
}
if (process.env.NODE_ENV !== 'production') { // Testing
// return `http://localhost:3333/api/books/${bookItem.id}/cover?token=${userToken}&ts=${bookLastUpdate}`
}
var url = new URL(`/api/books/${bookItem.id}/cover`, rootState.serverUrl)
return `${url}?token=${userToken}&ts=${bookLastUpdate}`
}
}
export const actions = {
}
export const mutations = {
}

View file

@ -18,7 +18,7 @@ export const getters = {
// return `http://localhost:3333/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}` // return `http://localhost:3333/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}`
} }
var url = new URL(`/api/items/${libraryItem.id}/cover`, rootState.serverUrl) var url = new URL(`/api/items/${libraryItem.id}/cover`, rootGetters['user/getServerAddress'])
return `${url}?token=${userToken}&ts=${lastUpdate}` return `${url}?token=${userToken}&ts=${lastUpdate}`
} }
} }

View file

@ -47,11 +47,13 @@ export const getters = {
} }
export const actions = { export const actions = {
// Listen for network connection
async setupNetworkListener({ state, commit }) { async setupNetworkListener({ state, commit }) {
if (state.isNetworkListenerInit) return if (state.isNetworkListenerInit) return
commit('setNetworkListenerInit', true) commit('setNetworkListenerInit', true)
var status = await Network.getStatus() var status = await Network.getStatus()
console.log('Network status', status)
commit('setNetworkStatus', status) commit('setNetworkStatus', status)
Network.addListener('networkStatusChange', (status) => { Network.addListener('networkStatusChange', (status) => {

View file

@ -27,12 +27,6 @@ export const actions = {
return false return false
} }
// var library = state.libraries.find(lib => lib.id === libraryId)
// if (library) {
// commit('setCurrentLibrary', libraryId)
// return library
// }
return this.$axios return this.$axios
.$get(`/api/libraries/${libraryId}?include=filterdata`) .$get(`/api/libraries/${libraryId}?include=filterdata`)
.then((data) => { .then((data) => {

View file

@ -1,5 +1,6 @@
export const state = () => ({ export const state = () => ({
user: null, user: null,
serverConnectionConfig: null,
userAudiobookData: [], userAudiobookData: [],
settings: { settings: {
mobileOrderBy: 'addedAt', mobileOrderBy: 'addedAt',
@ -20,6 +21,9 @@ export const getters = {
getToken: (state) => { getToken: (state) => {
return state.user ? state.user.token : null return state.user ? state.user.token : null
}, },
getServerAddress: (state) => {
return state.serverConnectionConfig ? state.serverConnectionConfig.address : null
},
getUserLibraryItemProgress: (state) => (libraryItemId) => { getUserLibraryItemProgress: (state) => (libraryItemId) => {
if (!state.user.libraryItemProgress) return null if (!state.user.libraryItemProgress) return null
return state.user.libraryItemProgress.find(li => li.id == libraryItemId) return state.user.libraryItemProgress.find(li => li.id == libraryItemId)
@ -102,6 +106,16 @@ export const actions = {
} }
export const mutations = { export const mutations = {
logout(state) {
state.user = null
state.serverConnectionConfig = null
},
setUser(state, user) {
state.user = user
},
setServerConnectionConfig(state, serverConnectionConfig) {
state.serverConnectionConfig = serverConnectionConfig
},
setUserAudiobookData(state, abdata) { setUserAudiobookData(state, abdata) {
var index = state.userAudiobookData.findIndex(uab => uab.audiobookId === abdata.audiobookId) var index = state.userAudiobookData.findIndex(uab => uab.audiobookId === abdata.audiobookId)
if (index >= 0) { if (index >= 0) {
@ -116,16 +130,6 @@ export const mutations = {
setAllUserAudiobookData(state, allAbData) { setAllUserAudiobookData(state, allAbData) {
state.userAudiobookData = allAbData state.userAudiobookData = allAbData
}, },
setUser(state, user) {
state.user = user
if (user) {
if (user.token) this.$localStore.setToken(user.token)
console.log('setUser', user.username)
} else {
this.$localStore.setToken(null)
console.warn('setUser cleared')
}
},
setSettings(state, settings) { setSettings(state, settings) {
if (!settings) return if (!settings) return