mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-21 11:14:38 +02:00
Offline support, downloading, syncing progress
This commit is contained in:
parent
e97218f2e8
commit
a412c9d359
37 changed files with 2836 additions and 201 deletions
30
Server.js
30
Server.js
|
@ -1,4 +1,5 @@
|
|||
import { io } from 'socket.io-client'
|
||||
import { Storage } from '@capacitor/storage'
|
||||
import axios from 'axios'
|
||||
import EventEmitter from 'events'
|
||||
|
||||
|
@ -26,6 +27,7 @@ class Server extends EventEmitter {
|
|||
}
|
||||
|
||||
getServerUrl(url) {
|
||||
if (!url) return null
|
||||
var urlObject = new URL(url)
|
||||
return `${urlObject.protocol}//${urlObject.hostname}:${urlObject.port}`
|
||||
}
|
||||
|
@ -35,23 +37,34 @@ class Server extends EventEmitter {
|
|||
this.store.commit('user/setUser', user)
|
||||
if (user) {
|
||||
this.store.commit('user/setSettings', user.settings)
|
||||
localStorage.setItem('userToken', user.token)
|
||||
Storage.set({ key: 'token', value: user.token })
|
||||
} else {
|
||||
localStorage.removeItem('userToken')
|
||||
Storage.remove({ key: 'token' })
|
||||
}
|
||||
}
|
||||
|
||||
setServerUrl(url) {
|
||||
this.url = url
|
||||
localStorage.setItem('serverUrl', url)
|
||||
this.store.commit('setServerUrl', url)
|
||||
|
||||
if (url) {
|
||||
Storage.set({ key: 'serverUrl', value: url })
|
||||
} else {
|
||||
Storage.remove({ key: 'serverUrl' })
|
||||
}
|
||||
}
|
||||
|
||||
async connect(url, token) {
|
||||
if (!url) {
|
||||
console.error('Invalid url to connect')
|
||||
return false
|
||||
}
|
||||
|
||||
var serverUrl = this.getServerUrl(url)
|
||||
var res = await this.ping(serverUrl)
|
||||
|
||||
if (!res || !res.success) {
|
||||
this.url = null
|
||||
this.setServerUrl(null)
|
||||
return false
|
||||
}
|
||||
var authRes = await this.authorize(serverUrl, token)
|
||||
|
@ -60,7 +73,7 @@ class Server extends EventEmitter {
|
|||
}
|
||||
|
||||
this.setServerUrl(serverUrl)
|
||||
console.warn('Connect setting auth user', authRes)
|
||||
|
||||
this.setUser(authRes.user)
|
||||
this.connectSocket()
|
||||
|
||||
|
@ -103,6 +116,9 @@ class Server extends EventEmitter {
|
|||
|
||||
logout() {
|
||||
this.setUser(null)
|
||||
if (this.socket) {
|
||||
this.socket.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
authorize(serverUrl, token) {
|
||||
|
@ -138,9 +154,13 @@ class Server extends EventEmitter {
|
|||
|
||||
this.connected = true
|
||||
this.emit('connected', true)
|
||||
this.store.commit('setSocketConnected', true)
|
||||
})
|
||||
this.socket.on('disconnect', () => {
|
||||
console.log('[Server] Socket Disconnected')
|
||||
this.connected = false
|
||||
this.emit('connected', false)
|
||||
this.store.commit('setSocketConnected', false)
|
||||
})
|
||||
this.socket.on('init', (data) => {
|
||||
console.log('[Server] Initial socket data received', data)
|
||||
|
|
|
@ -10,8 +10,8 @@ android {
|
|||
applicationId "com.audiobookshelf.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 8
|
||||
versionName "0.4.0-beta"
|
||||
versionCode 9
|
||||
versionName "0.8.0-beta"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
|
|
@ -9,8 +9,12 @@ android {
|
|||
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':capacitor-community-sqlite')
|
||||
implementation project(':capacitor-dialog')
|
||||
implementation project(':capacitor-network')
|
||||
implementation project(':capacitor-storage')
|
||||
implementation project(':robingenz-capacitor-app-update')
|
||||
implementation project(':capacitor-data-storage-sqlite')
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,26 @@
|
|||
[
|
||||
{
|
||||
"pkg": "@capacitor-community/sqlite",
|
||||
"classpath": "com.getcapacitor.community.database.sqlite.CapacitorSQLitePlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/dialog",
|
||||
"classpath": "com.capacitorjs.plugins.dialog.DialogPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/network",
|
||||
"classpath": "com.capacitorjs.plugins.network.NetworkPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/storage",
|
||||
"classpath": "com.capacitorjs.plugins.storage.StoragePlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@robingenz/capacitor-app-update",
|
||||
"classpath": "dev.robingenz.capacitor.appupdate.AppUpdatePlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "capacitor-data-storage-sqlite",
|
||||
"classpath": "com.jeep.plugin.capacitor.capacitordatastoragesqlite.CapacitorDataStorageSqlitePlugin"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -0,0 +1,320 @@
|
|||
package com.audiobookshelf.app
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.getcapacitor.JSObject
|
||||
import com.getcapacitor.Plugin
|
||||
import com.getcapacitor.PluginCall
|
||||
import com.getcapacitor.PluginMethod
|
||||
import com.getcapacitor.annotation.CapacitorPlugin
|
||||
import java.io.File
|
||||
|
||||
|
||||
@CapacitorPlugin(name = "AudioDownloader")
|
||||
class AudioDownloader : Plugin() {
|
||||
private val tag = "AudioDownloader"
|
||||
|
||||
lateinit var mainActivity:MainActivity
|
||||
lateinit var downloadManager:DownloadManager
|
||||
|
||||
data class AudiobookDownload(val url: String, val filename:String, val downloadId: Long)
|
||||
var downloads:MutableList<AudiobookDownload> = mutableListOf()
|
||||
|
||||
data class CoverItem(val name:String, val coverUrl:String)
|
||||
data class AudiobookItem(val id: Long, val uri: Uri, val name: String, val size: Int, val duration: Int, val coverUrl: String) {
|
||||
fun toJSObject() : JSObject {
|
||||
var obj = JSObject()
|
||||
obj.put("id", this.id)
|
||||
obj.put("uri", this.uri)
|
||||
obj.put("name", this.name)
|
||||
obj.put("size", this.size)
|
||||
obj.put("duration", this.duration)
|
||||
obj.put("coverUrl", this.coverUrl)
|
||||
return obj
|
||||
}
|
||||
}
|
||||
var audiobookItems:MutableList<AudiobookItem> = mutableListOf()
|
||||
|
||||
override fun load() {
|
||||
mainActivity = (activity as MainActivity)
|
||||
downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
|
||||
var recieverEvent : (evt: String, id: Long) -> Unit = { evt: String, id: Long ->
|
||||
Log.d(tag, "RECEIVE EVT $evt $id")
|
||||
if (evt == "complete") {
|
||||
var path = downloadManager.getUriForDownloadedFile(id)
|
||||
|
||||
var download = downloads.find { it.downloadId == id }
|
||||
var filename = download?.filename
|
||||
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("downloadId", id)
|
||||
jsobj.put("contentUrl", path)
|
||||
jsobj.put("filename", filename)
|
||||
notifyListeners("onDownloadComplete", jsobj)
|
||||
downloads = downloads.filter { it.downloadId != id } as MutableList<AudiobookDownload>
|
||||
}
|
||||
if (evt == "clicked") {
|
||||
Log.d(tag, "Clicked $id back in the audiodownloader")
|
||||
}
|
||||
}
|
||||
|
||||
mainActivity.registerBroadcastReceiver(recieverEvent)
|
||||
}
|
||||
|
||||
fun loadAudiobooks() {
|
||||
var covers = loadCovers()
|
||||
|
||||
val projection = arrayOf(
|
||||
MediaStore.Audio.Media._ID,
|
||||
MediaStore.Audio.Media.DISPLAY_NAME,
|
||||
MediaStore.Audio.Media.DURATION,
|
||||
MediaStore.Audio.Media.SIZE,
|
||||
MediaStore.Audio.Media.IS_AUDIOBOOK
|
||||
)
|
||||
|
||||
var _audiobookItems:MutableList<AudiobookItem> = mutableListOf()
|
||||
val selection = "${MediaStore.Audio.Media.IS_AUDIOBOOK} == ?"
|
||||
val selectionArgs = arrayOf("1")
|
||||
val sortOrder = "${MediaStore.Audio.Media.DISPLAY_NAME} ASC"
|
||||
|
||||
activity.applicationContext.contentResolver.query(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||
projection,
|
||||
selection,
|
||||
selectionArgs,
|
||||
sortOrder
|
||||
)?.use { cursor ->
|
||||
|
||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
|
||||
val nameColumn =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)
|
||||
val durationColumn =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
|
||||
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)
|
||||
val isAudiobookColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.IS_AUDIOBOOK)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idColumn)
|
||||
val name = cursor.getString(nameColumn)
|
||||
val duration = cursor.getInt(durationColumn)
|
||||
val size = cursor.getInt(sizeColumn)
|
||||
var isAudiobook = cursor.getInt(isAudiobookColumn)
|
||||
|
||||
if (isAudiobook == 1) {
|
||||
val contentUri: Uri = ContentUris.withAppendedId(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||
id
|
||||
)
|
||||
|
||||
Log.d(tag, "Got Content FRom MEdia STORE $id $contentUri, Name: $name, Dur: $duration, Size: $size")
|
||||
var audiobookId = File(name).nameWithoutExtension
|
||||
var coverItem:CoverItem? = covers.find{it.name == audiobookId}
|
||||
var coverUrl = coverItem?.coverUrl ?: ""
|
||||
|
||||
_audiobookItems.add(AudiobookItem(id, contentUri, name, duration, size, coverUrl))
|
||||
}
|
||||
}
|
||||
audiobookItems = _audiobookItems
|
||||
|
||||
var audiobookObjs:List<JSObject> = _audiobookItems.map{ it.toJSObject() }
|
||||
|
||||
var mediaItemNoticePayload = JSObject()
|
||||
mediaItemNoticePayload.put("items", audiobookObjs)
|
||||
notifyListeners("onMediaLoaded", mediaItemNoticePayload)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadCovers() : MutableList<CoverItem> {
|
||||
val projection = arrayOf(
|
||||
MediaStore.Images.Media._ID,
|
||||
MediaStore.Images.Media.DISPLAY_NAME
|
||||
)
|
||||
val sortOrder = "${MediaStore.Images.Media.DISPLAY_NAME} ASC"
|
||||
|
||||
var coverItems:MutableList<CoverItem> = mutableListOf()
|
||||
|
||||
activity.applicationContext.contentResolver.query(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
projection,
|
||||
null,
|
||||
null,
|
||||
sortOrder
|
||||
)?.use { cursor ->
|
||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
|
||||
val nameColumn =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idColumn)
|
||||
val filename = cursor.getString(nameColumn)
|
||||
val contentUri: Uri = ContentUris.withAppendedId(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
id
|
||||
)
|
||||
|
||||
var name = File(filename).nameWithoutExtension
|
||||
Log.d(tag, "Got IMAGE FRom Media STORE $id $contentUri, Name: $name")
|
||||
|
||||
var coverItem = CoverItem(name, contentUri.toString())
|
||||
coverItems.add(coverItem)
|
||||
}
|
||||
}
|
||||
return coverItems
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun load(call: PluginCall) {
|
||||
loadAudiobooks()
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun downloadCover(call: PluginCall) {
|
||||
var url = call.data.getString("downloadUrl", "unknown").toString()
|
||||
var title = call.data.getString("title", "Cover").toString()
|
||||
var filename = call.data.getString("filename", "audiobook.jpg").toString()
|
||||
|
||||
Log.d(tag, "Called download cover: $url")
|
||||
|
||||
var dlRequest = DownloadManager.Request(Uri.parse(url))
|
||||
dlRequest.setTitle("Cover Art: $title")
|
||||
dlRequest.setDescription("Cover art for audiobook")
|
||||
dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION)
|
||||
dlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_AUDIOBOOKS, filename)
|
||||
var downloadId = downloadManager.enqueue(dlRequest)
|
||||
|
||||
var progressReceiver : (prog: Long) -> Unit = { prog: Long ->
|
||||
//
|
||||
}
|
||||
|
||||
var doneReceiver : (success: Boolean) -> Unit = { success: Boolean ->
|
||||
var jsobj = JSObject()
|
||||
if (success) {
|
||||
var path = downloadManager.getUriForDownloadedFile(downloadId)
|
||||
jsobj.put("url", path)
|
||||
call.resolve(jsobj)
|
||||
} else {
|
||||
jsobj.put("failed", true)
|
||||
call.resolve(jsobj)
|
||||
}
|
||||
}
|
||||
|
||||
var progressUpdater = DownloadProgressUpdater(downloadManager, downloadId, progressReceiver, doneReceiver)
|
||||
progressUpdater.run()
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun download(call: PluginCall) {
|
||||
var url = call.data.getString("downloadUrl", "unknown").toString()
|
||||
var title = call.data.getString("title", "Audiobook").toString()
|
||||
var filename = call.data.getString("filename", "audiobook.mp3").toString()
|
||||
|
||||
Log.d(tag, "Called download: $url")
|
||||
|
||||
var dlRequest = DownloadManager.Request(Uri.parse(url))
|
||||
dlRequest.setTitle(title)
|
||||
dlRequest.setDescription("Downloading to Audiobooks directory")
|
||||
dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION)
|
||||
|
||||
dlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_AUDIOBOOKS, filename)
|
||||
|
||||
var downloadId = downloadManager.enqueue(dlRequest)
|
||||
|
||||
var download = AudiobookDownload(url, filename, downloadId)
|
||||
downloads.add(download)
|
||||
|
||||
var progressReceiver : (prog: Long) -> Unit = { prog: Long ->
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("filename", filename)
|
||||
jsobj.put("downloadId", downloadId)
|
||||
jsobj.put("progress", prog)
|
||||
notifyListeners("onDownloadProgress", jsobj)
|
||||
}
|
||||
|
||||
var doneReceiver : (success: Boolean) -> Unit = { success: Boolean ->
|
||||
Log.d(tag, "RECIEVER DONE, SUCCES? $success")
|
||||
}
|
||||
|
||||
var progressUpdater = DownloadProgressUpdater(downloadManager, downloadId, progressReceiver, doneReceiver)
|
||||
progressUpdater.run()
|
||||
|
||||
val ret = JSObject()
|
||||
ret.put("value", downloadId)
|
||||
call.resolve(ret)
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun delete(call:PluginCall) {
|
||||
var filename = call.data.getString("filename", "audiobook.mp3").toString()
|
||||
var url = call.data.getString("url", "").toString()
|
||||
var coverUrl = call.data.getString("coverUrl", "").toString()
|
||||
|
||||
Log.d(tag, "Called delete file $filename $url")
|
||||
|
||||
var contentResolver = activity.applicationContext.contentResolver
|
||||
contentResolver.delete(Uri.parse(url), null, null)
|
||||
|
||||
if (coverUrl != "") {
|
||||
contentResolver.delete(Uri.parse(coverUrl), null, null)
|
||||
}
|
||||
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
internal class DownloadProgressUpdater(private val manager: DownloadManager, private val downloadId: Long, private var receiver:(Long) -> Unit, private var doneReceiver:(Boolean) -> Unit) : Thread() {
|
||||
private val query: DownloadManager.Query = DownloadManager.Query()
|
||||
private var totalBytes: Int = 0
|
||||
private var TAG = "DownloadProgressUpdater"
|
||||
|
||||
init {
|
||||
query.setFilterById(this.downloadId)
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
Log.d(TAG, "RUN FOR ID $downloadId")
|
||||
var keepRunning = true
|
||||
while (keepRunning) {
|
||||
Thread.sleep(500)
|
||||
|
||||
manager.query(query).use {
|
||||
if (it.moveToFirst()) {
|
||||
|
||||
//get total bytes of the file
|
||||
if (totalBytes <= 0) {
|
||||
totalBytes = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
|
||||
}
|
||||
|
||||
val downloadStatus = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||
val bytesDownloadedSoFar = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
|
||||
|
||||
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL || downloadStatus == DownloadManager.STATUS_FAILED) {
|
||||
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL) {
|
||||
doneReceiver(true)
|
||||
} else {
|
||||
doneReceiver(false)
|
||||
}
|
||||
keepRunning = false
|
||||
this.interrupt()
|
||||
} else {
|
||||
//update progress
|
||||
val percentProgress = ((bytesDownloadedSoFar * 100L) / totalBytes)
|
||||
receiver(percentProgress)
|
||||
}
|
||||
|
||||
} else {
|
||||
Log.e(TAG, "NOT FOUND IN QUERY")
|
||||
keepRunning = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -16,10 +16,14 @@ class Audiobook {
|
|||
var playbackSpeed:Float = 1f
|
||||
var duration:Long = 0
|
||||
|
||||
var isLocal:Boolean = false
|
||||
var contentUrl:String = ""
|
||||
|
||||
var hasPlayerLoaded:Boolean = false
|
||||
|
||||
val playlistUri:Uri
|
||||
val coverUri:Uri
|
||||
var playlistUri:Uri = Uri.EMPTY
|
||||
var coverUri:Uri = Uri.EMPTY
|
||||
var contentUri:Uri = Uri.EMPTY // For Local only
|
||||
|
||||
constructor(jsondata:JSObject) {
|
||||
id = jsondata.getString("id", "audiobook").toString()
|
||||
|
@ -34,7 +38,22 @@ class Audiobook {
|
|||
playbackSpeed = jsondata.getDouble("playbackSpeed")!!.toFloat()
|
||||
duration = jsondata.getString("duration", "0")!!.toLong()
|
||||
|
||||
playlistUri = Uri.parse(playlistUrl)
|
||||
coverUri = Uri.parse(cover)
|
||||
// Local data
|
||||
isLocal = jsondata.getBoolean("isLocal", false) == true
|
||||
contentUrl = jsondata.getString("contentUrl", "").toString()
|
||||
|
||||
if (playlistUrl != "") {
|
||||
playlistUri = Uri.parse(playlistUrl)
|
||||
}
|
||||
if (cover != "") {
|
||||
coverUri = Uri.parse(cover)
|
||||
} else {
|
||||
coverUri = Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
||||
cover = coverUri.toString()
|
||||
}
|
||||
|
||||
if (contentUrl != "") {
|
||||
contentUri = Uri.parse(contentUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
package com.audiobookshelf.app
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.app.DownloadManager
|
||||
import android.content.*
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.example.myapp.MyNativeAudio
|
||||
import android.widget.Toast
|
||||
import com.getcapacitor.BridgeActivity
|
||||
|
||||
|
||||
class MainActivity : BridgeActivity() {
|
||||
private val tag = "MainActivity"
|
||||
|
||||
|
@ -18,11 +19,45 @@ class MainActivity : BridgeActivity() {
|
|||
private lateinit var mConnection : ServiceConnection
|
||||
|
||||
lateinit var pluginCallback : () -> Unit
|
||||
lateinit var downloaderCallback : (String, Long) -> Unit
|
||||
|
||||
val broadcastReceiver = object: BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
DownloadManager.ACTION_DOWNLOAD_COMPLETE -> {
|
||||
var thisdlid = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0L)
|
||||
|
||||
downloaderCallback("complete", thisdlid)
|
||||
|
||||
Log.d(tag, "DOWNNLAOD COMPELTE $thisdlid")
|
||||
Toast.makeText(this@MainActivity, "Download Completed $thisdlid", Toast.LENGTH_SHORT)
|
||||
}
|
||||
DownloadManager.ACTION_NOTIFICATION_CLICKED -> {
|
||||
var thisdlid = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0L)
|
||||
downloaderCallback("clicked", thisdlid)
|
||||
|
||||
Log.d(tag, "CLICKED NOTFIFICAIONT $thisdlid")
|
||||
Toast.makeText(this@MainActivity, "Download CLICKED $thisdlid", Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.d(tag, "onCreate")
|
||||
registerPlugin(MyNativeAudio::class.java)
|
||||
registerPlugin(AudioDownloader::class.java)
|
||||
|
||||
var filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE).apply {
|
||||
addAction(DownloadManager.ACTION_NOTIFICATION_CLICKED)
|
||||
}
|
||||
registerReceiver(broadcastReceiver, filter)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
// unregisterReceiver(broadcastReceiver)
|
||||
}
|
||||
|
||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -62,4 +97,8 @@ class MainActivity : BridgeActivity() {
|
|||
val stopIntent = Intent(this, PlayerNotificationService::class.java)
|
||||
stopService(stopIntent)
|
||||
}
|
||||
|
||||
fun registerBroadcastReceiver(cb: (String, Long) -> Unit) {
|
||||
downloaderCallback = cb
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
package com.example.myapp
|
||||
package com.audiobookshelf.app
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.audiobookshelf.app.Audiobook
|
||||
import com.audiobookshelf.app.MainActivity
|
||||
import com.audiobookshelf.app.PlayerNotificationService
|
||||
import com.getcapacitor.JSObject
|
||||
import com.getcapacitor.Plugin
|
||||
import com.getcapacitor.PluginCall
|
||||
|
@ -56,12 +53,20 @@ class MyNativeAudio : Plugin() {
|
|||
} else {
|
||||
Log.w(tag, "Service already started --")
|
||||
}
|
||||
|
||||
var jsobj = JSObject()
|
||||
|
||||
var audiobook:Audiobook = Audiobook(call.data)
|
||||
if (audiobook.playlistUrl == "" && audiobook.contentUrl == "") {
|
||||
Log.e(tag, "Invalid URL for init audio player")
|
||||
|
||||
jsobj.put("success", false)
|
||||
return call.resolve(jsobj)
|
||||
}
|
||||
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
playerNotificationService.initPlayer(audiobook)
|
||||
call.resolve()
|
||||
jsobj.put("success", true)
|
||||
call.resolve(jsobj)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,6 +114,7 @@ class MyNativeAudio : Plugin() {
|
|||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun seekBackward(call: PluginCall) {
|
||||
var amount:Long = call.getString("amount", "0")!!.toLong()
|
||||
|
@ -117,6 +123,7 @@ class MyNativeAudio : Plugin() {
|
|||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun setPlaybackSpeed(call: PluginCall) {
|
||||
var playbackSpeed:Float = call.getFloat("speed", 1.0f)!!
|
||||
|
@ -126,6 +133,7 @@ class MyNativeAudio : Plugin() {
|
|||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun terminateStream(call: PluginCall) {
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.net.Uri
|
|||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.provider.MediaStore
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import android.support.v4.media.session.MediaControllerCompat
|
||||
|
@ -24,10 +25,15 @@ import com.google.android.exoplayer2.*
|
|||
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
|
||||
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
|
||||
import com.google.android.exoplayer2.source.MediaSource
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaExtractor
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
||||
import com.google.android.exoplayer2.upstream.*
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
|
||||
|
||||
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
|
||||
|
@ -217,13 +223,13 @@ class PlayerNotificationService : Service() {
|
|||
mediaSessionConnector = MediaSessionConnector(mediaSession)
|
||||
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
|
||||
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
||||
return MediaDescriptionCompat.Builder()
|
||||
var builder = MediaDescriptionCompat.Builder()
|
||||
.setMediaId(currentAudiobook!!.id)
|
||||
.setTitle(currentAudiobook!!.title)
|
||||
.setSubtitle(currentAudiobook!!.author)
|
||||
.setMediaUri(currentAudiobook!!.playlistUri)
|
||||
.setIconUri(currentAudiobook!!.coverUri)
|
||||
.build()
|
||||
return builder.build()
|
||||
}
|
||||
}
|
||||
mediaSessionConnector.setQueueNavigator(queueNavigator)
|
||||
|
@ -280,6 +286,9 @@ class PlayerNotificationService : Service() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private fun setPlayerListeners() {
|
||||
mPlayer.addListener(object : Player.Listener {
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
|
@ -351,28 +360,42 @@ class PlayerNotificationService : Service() {
|
|||
Log.d(tag, "Init Player audiobook already playing")
|
||||
}
|
||||
|
||||
val metadata = MediaMetadataCompat.Builder()
|
||||
var metadataBuilder = MediaMetadataCompat.Builder()
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, currentAudiobook!!.title)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, currentAudiobook!!.title)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, currentAudiobook!!.author)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, currentAudiobook!!.author)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, currentAudiobook!!.author)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, currentAudiobook!!.series)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, currentAudiobook!!.cover)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, currentAudiobook!!.cover)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, currentAudiobook!!.id)
|
||||
.build()
|
||||
|
||||
if (currentAudiobook!!.cover != "") {
|
||||
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, currentAudiobook!!.cover)
|
||||
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, currentAudiobook!!.cover)
|
||||
}
|
||||
|
||||
var metadata = metadataBuilder.build()
|
||||
mediaSession.setMetadata(metadata)
|
||||
|
||||
var mediaMetadata = MediaMetadata.Builder().build()
|
||||
var mediaItem = MediaItem.Builder().setUri(currentAudiobook!!.playlistUri).setMediaMetadata(mediaMetadata).build()
|
||||
|
||||
var dataSourceFactory = DefaultHttpDataSource.Factory()
|
||||
dataSourceFactory.setUserAgent(channelId)
|
||||
dataSourceFactory.setDefaultRequestProperties(hashMapOf("Authorization" to "Bearer ${currentAudiobook!!.token}"))
|
||||
|
||||
var mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
|
||||
var mediaSource:MediaSource
|
||||
if (currentAudiobook!!.isLocal) {
|
||||
Log.d(tag, "Playing Local File")
|
||||
var mediaItem = MediaItem.Builder().setUri(currentAudiobook!!.contentUri).setMediaMetadata(mediaMetadata).build()
|
||||
var dataSourceFactory = DefaultDataSourceFactory(ctx, channelId)
|
||||
mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
|
||||
} else {
|
||||
Log.d(tag, "Playing HLS File")
|
||||
var mediaItem = MediaItem.Builder().setUri(currentAudiobook!!.playlistUri).setMediaMetadata(mediaMetadata).build()
|
||||
var dataSourceFactory = DefaultHttpDataSource.Factory()
|
||||
dataSourceFactory.setUserAgent(channelId)
|
||||
dataSourceFactory.setDefaultRequestProperties(hashMapOf("Authorization" to "Bearer ${currentAudiobook!!.token}"))
|
||||
|
||||
mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
|
||||
}
|
||||
|
||||
|
||||
mPlayer.setMediaSource(mediaSource, true)
|
||||
mPlayer.prepare()
|
||||
|
|
|
@ -2,8 +2,20 @@
|
|||
include ':capacitor-android'
|
||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||
|
||||
include ':capacitor-community-sqlite'
|
||||
project(':capacitor-community-sqlite').projectDir = new File('../node_modules/@capacitor-community/sqlite/android')
|
||||
|
||||
include ':capacitor-dialog'
|
||||
project(':capacitor-dialog').projectDir = new File('../node_modules/@capacitor/dialog/android')
|
||||
|
||||
include ':capacitor-network'
|
||||
project(':capacitor-network').projectDir = new File('../node_modules/@capacitor/network/android')
|
||||
|
||||
include ':capacitor-storage'
|
||||
project(':capacitor-storage').projectDir = new File('../node_modules/@capacitor/storage/android')
|
||||
|
||||
include ':robingenz-capacitor-app-update'
|
||||
project(':robingenz-capacitor-app-update').projectDir = new File('../node_modules/@robingenz/capacitor-app-update/android')
|
||||
|
||||
include ':capacitor-data-storage-sqlite'
|
||||
project(':capacitor-data-storage-sqlite').projectDir = new File('../node_modules/capacitor-data-storage-sqlite/android')
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import "./fonts.css";
|
||||
|
||||
.box-shadow-md {
|
||||
box-shadow: 2px 8px 6px #111111aa;
|
||||
}
|
||||
|
|
41
assets/fonts.css
Normal file
41
assets/fonts.css
Normal file
|
@ -0,0 +1,41 @@
|
|||
/* fallback */
|
||||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(/material-icons.woff2) format('woff2');
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
-webkit-font-feature-settings: 'liga';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Gentium Book Basic';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(/GentiumBookBasic.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Gentium Book Basic';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(/GentiumBookBasic.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
|
@ -202,7 +202,17 @@ export default {
|
|||
this.isResetting = false
|
||||
this.initObject = { ...audiobookStreamData }
|
||||
this.currentPlaybackRate = this.initObject.playbackSpeed
|
||||
MyNativeAudio.initPlayer(this.initObject)
|
||||
MyNativeAudio.initPlayer(this.initObject).then((res) => {
|
||||
if (res && res.success) {
|
||||
console.log('Success init audio player')
|
||||
} else {
|
||||
console.error('Failed to init audio player')
|
||||
}
|
||||
})
|
||||
|
||||
if (audiobookStreamData.isLocal) {
|
||||
this.setStreamReady()
|
||||
}
|
||||
},
|
||||
setFromObj() {
|
||||
if (!this.initObject) {
|
||||
|
@ -210,7 +220,17 @@ export default {
|
|||
return
|
||||
}
|
||||
this.isResetting = false
|
||||
MyNativeAudio.initPlayer(this.initObject)
|
||||
MyNativeAudio.initPlayer(this.initObject).then((res) => {
|
||||
if (res && res.success) {
|
||||
console.log('Success init audio player')
|
||||
} else {
|
||||
console.error('Failed to init audio player')
|
||||
}
|
||||
})
|
||||
|
||||
if (audiobookStreamData.isLocal) {
|
||||
this.setStreamReady()
|
||||
}
|
||||
},
|
||||
play() {
|
||||
MyNativeAudio.playPlayer()
|
||||
|
|
|
@ -13,14 +13,18 @@
|
|||
<div class="flex-grow" />
|
||||
<!-- <ui-menu :label="username" :items="menuItems" @action="menuAction" class="ml-5" /> -->
|
||||
|
||||
<nuxt-link to="/account" class="relative w-28 bg-fg border border-gray-500 rounded shadow-sm ml-5 pl-3 pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer hover:bg-bg hover:bg-opacity-40" aria-haspopup="listbox" aria-expanded="true">
|
||||
<span class="material-icons cursor-pointer mx-4" @click="$store.commit('downloads/setShowModal', true)">source</span>
|
||||
|
||||
<widgets-connection-icon />
|
||||
|
||||
<!-- <nuxt-link to="/account" class="relative w-28 bg-fg border border-gray-500 rounded shadow-sm ml-5 pl-3 pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer hover:bg-bg hover:bg-opacity-40" aria-haspopup="listbox" aria-expanded="true">
|
||||
<span class="flex items-center">
|
||||
<span class="block truncate">{{ username }}</span>
|
||||
</span>
|
||||
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<span class="material-icons text-gray-100">person</span>
|
||||
</span>
|
||||
</nuxt-link>
|
||||
</nuxt-link> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -89,4 +93,47 @@ export default {
|
|||
#appbar {
|
||||
box-shadow: 0px 5px 5px #11111155;
|
||||
}
|
||||
.loader-dots div {
|
||||
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
||||
}
|
||||
.loader-dots div:nth-child(1) {
|
||||
left: 0px;
|
||||
animation: loader-dots1 0.6s infinite;
|
||||
}
|
||||
.loader-dots div:nth-child(2) {
|
||||
left: 0px;
|
||||
animation: loader-dots2 0.6s infinite;
|
||||
}
|
||||
.loader-dots div:nth-child(3) {
|
||||
left: 10px;
|
||||
animation: loader-dots2 0.6s infinite;
|
||||
}
|
||||
.loader-dots div:nth-child(4) {
|
||||
left: 20px;
|
||||
animation: loader-dots3 0.6s infinite;
|
||||
}
|
||||
@keyframes loader-dots1 {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes loader-dots3 {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
@keyframes loader-dots2 {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(10px, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -4,7 +4,7 @@
|
|||
<div :key="index" class="border-b border-opacity-10 w-full bookshelfRow py-4 flex justify-around relative">
|
||||
<template v-for="audiobook in shelf">
|
||||
<!-- <div :key="audiobook.id" class="relative px-4"> -->
|
||||
<cards-book-card :key="audiobook.id" :audiobook="audiobook" :width="cardWidth" :user-progress="userAudiobooks[audiobook.id]" />
|
||||
<cards-book-card :key="audiobook.id" :audiobook="audiobook" :width="cardWidth" :user-progress="userAudiobooks[audiobook.id]" :local-user-progress="localUserAudiobooks[audiobook.id]" />
|
||||
<!-- <nuxt-link :to="`/audiobook/${audiobook.id}`">
|
||||
<cards-book-cover :audiobook="audiobook" :width="cardWidth" class="mx-auto -mb-px" style="box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166" />
|
||||
</nuxt-link> -->
|
||||
|
@ -44,6 +44,9 @@ export default {
|
|||
},
|
||||
userAudiobooks() {
|
||||
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
||||
},
|
||||
localUserAudiobooks() {
|
||||
return this.$store.state.user.localUserAudiobooks || {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -52,11 +55,6 @@ export default {
|
|||
filterBy: 'all'
|
||||
})
|
||||
},
|
||||
playAudiobook(audiobook) {
|
||||
console.log('Play Audiobook', audiobook)
|
||||
this.$store.commit('setStreamAudiobook', audiobook)
|
||||
this.$server.socket.emit('open_stream', audiobook.id)
|
||||
},
|
||||
calcShelves() {
|
||||
var booksPerShelf = Math.floor(this.pageWidth / (this.cardWidth + 32))
|
||||
var groupedBooks = []
|
||||
|
@ -87,6 +85,16 @@ export default {
|
|||
if (this.currFilterOrderKey !== this.filterOrderKey) {
|
||||
this.calcShelves()
|
||||
}
|
||||
},
|
||||
socketConnected(isConnected) {
|
||||
if (isConnected) {
|
||||
console.log('Connected - Load from server')
|
||||
this.$store.dispatch('audiobooks/load')
|
||||
} else {
|
||||
console.log('Disconnected - Reset to local storage')
|
||||
this.$store.commit('audiobooks/reset')
|
||||
this.$store.dispatch('downloads/loadFromStorage')
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
@ -94,7 +102,13 @@ export default {
|
|||
this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
|
||||
|
||||
window.addEventListener('resize', this.resize)
|
||||
this.$store.dispatch('audiobooks/load')
|
||||
|
||||
this.$server.on('connected', this.socketConnected)
|
||||
if (this.$server.connected) {
|
||||
this.$store.dispatch('audiobooks/load')
|
||||
} else {
|
||||
console.log('Bookshelf - Server not connected using downloaded')
|
||||
}
|
||||
this.init()
|
||||
},
|
||||
beforeDestroy() {
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
<template>
|
||||
<div class="w-full p-4 pointer-events-none fixed bottom-0 left-0 right-0 z-20">
|
||||
<div v-if="streamAudiobook" class="w-full bg-primary absolute bottom-0 left-0 right-0 z-50 p-2 pointer-events-auto" @click.stop @mousedown.stop @mouseup.stop>
|
||||
<div v-if="audiobook" class="w-full bg-primary absolute bottom-0 left-0 right-0 z-50 p-2 pointer-events-auto" @click.stop @mousedown.stop @mouseup.stop>
|
||||
<div class="pl-16 pr-2 flex items-center pb-2">
|
||||
<div>
|
||||
<p class="px-2">{{ title }}</p>
|
||||
<p class="px-2 text-xs text-gray-400">by {{ author }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div class="cursor-pointer flex items-center justify-center mr-6 w-6 text-center" :class="chapters.length ? 'text-gray-300' : 'text-gray-400'" @mousedown.prevent @mouseup.prevent @click="clickChapterBtn">
|
||||
<div v-if="chapters.length" class="cursor-pointer flex items-center justify-center mr-6 w-6 text-center" :class="chapters.length ? 'text-gray-300' : 'text-gray-400'" @mousedown.prevent @mouseup.prevent @click="clickChapterBtn">
|
||||
<span class="material-icons text-2xl">format_list_bulleted</span>
|
||||
</div>
|
||||
<span class="material-icons" @click="cancelStream">close</span>
|
||||
</div>
|
||||
<div class="absolute left-2 -top-10">
|
||||
<cards-book-cover :audiobook="streamAudiobook" :width="64" />
|
||||
<cards-book-cover :audiobook="audiobook" :width="64" />
|
||||
</div>
|
||||
<audio-player-mini ref="audioPlayerMini" :loading="!stream || currStreamAudiobookId !== streamAudiobookId" @updateTime="updateTime" @selectPlaybackSpeed="showPlaybackSpeedModal = true" @hook:mounted="audioPlayerMounted" />
|
||||
<audio-player-mini ref="audioPlayerMini" :loading="isLoading" @updateTime="updateTime" @selectPlaybackSpeed="showPlaybackSpeedModal = true" @hook:mounted="audioPlayerMounted" />
|
||||
</div>
|
||||
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-speed.sync="playbackSpeed" @change="changePlaybackSpeed" />
|
||||
<modals-chapters-modal v-model="showChapterModal" :chapters="chapters" @select="selectChapter" />
|
||||
|
@ -30,24 +30,62 @@ export default {
|
|||
return {
|
||||
audioPlayerReady: false,
|
||||
stream: null,
|
||||
lastServerUpdateSentSeconds: 0,
|
||||
download: null,
|
||||
lastProgressTimeUpdate: 0,
|
||||
showPlaybackSpeedModal: false,
|
||||
playbackSpeed: 1,
|
||||
showChapterModal: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// playingDownload: {
|
||||
// handler(newVal, oldVal) {
|
||||
// console.log('[StreamContainer] Download AUDIOBOOK Changed ' + newVal + '|' + oldVal)
|
||||
// if (newVal) {
|
||||
// if (this.audioPlayerReady) {
|
||||
// this.playDownload()
|
||||
// }
|
||||
// } else if (this.download) {
|
||||
// this.download = null
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// streamAudiobook(newVal, oldval) {
|
||||
// console.log('[StreamContainer] Stream AUDIOBOOK Changed ' + newVal + '|' + oldVal)
|
||||
// }
|
||||
},
|
||||
watch: {
|
||||
socketConnected(newVal) {
|
||||
if (newVal) {
|
||||
console.log('Socket Connected set listeners')
|
||||
this.setListeners()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
socketConnected() {
|
||||
return this.$store.state.socketConnected
|
||||
},
|
||||
isLoading() {
|
||||
if (this.playingDownload) return false
|
||||
if (!this.streamAudiobook) return false
|
||||
return !this.stream || this.streamAudiobook.id !== this.stream.audiobook.id
|
||||
},
|
||||
playingDownload() {
|
||||
return this.$store.state.playingDownload
|
||||
},
|
||||
audiobook() {
|
||||
if (this.playingDownload) return this.playingDownload.audiobook
|
||||
return this.streamAudiobook
|
||||
},
|
||||
audiobookId() {
|
||||
return this.audiobook ? this.audiobook.id : null
|
||||
},
|
||||
streamAudiobook() {
|
||||
return this.$store.state.streamAudiobook
|
||||
},
|
||||
streamAudiobookId() {
|
||||
return this.streamAudiobook ? this.streamAudiobook.id : null
|
||||
},
|
||||
currStreamAudiobookId() {
|
||||
return this.stream ? this.stream.audiobook.id : null
|
||||
},
|
||||
book() {
|
||||
return this.streamAudiobook ? this.streamAudiobook.book || {} : {}
|
||||
return this.audiobook ? this.audiobook.book || {} : {}
|
||||
},
|
||||
title() {
|
||||
return this.book ? this.book.title : ''
|
||||
|
@ -62,7 +100,7 @@ export default {
|
|||
return this.book ? this.book.series : ''
|
||||
},
|
||||
chapters() {
|
||||
return this.streamAudiobook ? this.streamAudiobook.chapters || [] : []
|
||||
return this.audiobook ? this.audiobook.chapters || [] : []
|
||||
},
|
||||
volumeNumber() {
|
||||
return this.book ? this.book.volumeNumber : ''
|
||||
|
@ -73,7 +111,7 @@ export default {
|
|||
return `${this.series} #${this.volumeNumber}`
|
||||
},
|
||||
duration() {
|
||||
return this.streamAudiobook ? this.streamAudiobook.duration || 0 : 0
|
||||
return this.audiobook ? this.audiobook.duration || 0 : 0
|
||||
},
|
||||
coverForNative() {
|
||||
if (!this.cover) {
|
||||
|
@ -100,27 +138,61 @@ export default {
|
|||
this.showChapterModal = false
|
||||
},
|
||||
async cancelStream() {
|
||||
const { value } = await Dialog.confirm({
|
||||
title: 'Confirm',
|
||||
message: 'Cancel this stream?'
|
||||
})
|
||||
if (value) {
|
||||
this.$server.socket.emit('close_stream')
|
||||
this.$store.commit('setStreamAudiobook', null)
|
||||
if (this.download) {
|
||||
if (this.$refs.audioPlayerMini) {
|
||||
this.$refs.audioPlayerMini.terminateStream()
|
||||
}
|
||||
this.$store.commit('setPlayingDownload', null)
|
||||
|
||||
this.$localStore.setCurrent(null)
|
||||
} else {
|
||||
const { value } = await Dialog.confirm({
|
||||
title: 'Confirm',
|
||||
message: 'Cancel this stream?'
|
||||
})
|
||||
if (value) {
|
||||
this.$server.socket.emit('close_stream')
|
||||
this.$store.commit('setStreamAudiobook', null)
|
||||
if (this.$refs.audioPlayerMini) {
|
||||
this.$refs.audioPlayerMini.terminateStream()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
updateTime(currentTime) {
|
||||
var diff = currentTime - this.lastServerUpdateSentSeconds
|
||||
var diff = currentTime - this.lastProgressTimeUpdate
|
||||
|
||||
if (diff > 4 || diff < 0) {
|
||||
this.lastServerUpdateSentSeconds = currentTime
|
||||
var updatePayload = {
|
||||
currentTime,
|
||||
streamId: this.stream.id
|
||||
this.lastProgressTimeUpdate = currentTime
|
||||
if (this.stream) {
|
||||
var updatePayload = {
|
||||
currentTime,
|
||||
streamId: this.stream.id
|
||||
}
|
||||
this.$server.socket.emit('stream_update', updatePayload)
|
||||
} else if (this.download) {
|
||||
var progressUpdate = {
|
||||
audiobookId: this.download.id,
|
||||
currentTime: currentTime,
|
||||
totalDuration: this.download.audiobook.duration,
|
||||
progress: Number((currentTime / this.download.audiobook.duration).toFixed(3)),
|
||||
lastUpdate: Date.now(),
|
||||
isRead: false
|
||||
}
|
||||
|
||||
if (this.$server.connected) {
|
||||
// var updateObj = {
|
||||
// audiobookId: this.download.id,
|
||||
// totalDuration: this.download.audiobook.duration,
|
||||
// clientCurrentTime: currentTime,
|
||||
// clientProgress: Number((currentTime / this.download.audiobook.duration).toFixed(3))
|
||||
// }
|
||||
this.$server.socket.emit('progress_update', progressUpdate)
|
||||
}
|
||||
this.$localStore.updateUserAudiobookProgress(progressUpdate).then(() => {
|
||||
console.log('Updated user audiobook progress', currentTime)
|
||||
})
|
||||
}
|
||||
this.$server.socket.emit('stream_update', updatePayload)
|
||||
}
|
||||
},
|
||||
closeStream() {},
|
||||
|
@ -151,8 +223,67 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
async getDownloadStartTime() {
|
||||
var userAudiobook = await this.$localStore.getMostRecentUserAudiobook(this.audiobookId)
|
||||
if (!userAudiobook) {
|
||||
console.log('[StreamContainer] getDownloadStartTime no user audiobook record found')
|
||||
return 0
|
||||
}
|
||||
return userAudiobook.currentTime
|
||||
},
|
||||
async playDownload() {
|
||||
if (this.stream) {
|
||||
if (this.$refs.audioPlayerMini) {
|
||||
this.$refs.audioPlayerMini.terminateStream()
|
||||
}
|
||||
this.stream = null
|
||||
}
|
||||
|
||||
this.lastProgressTimeUpdate = 0
|
||||
console.log('[StreamContainer] Playing local', this.playingDownload)
|
||||
if (!this.$refs.audioPlayerMini) {
|
||||
console.error('No Audio Player Mini')
|
||||
return
|
||||
}
|
||||
|
||||
var playOnLoad = this.$store.state.playOnLoad
|
||||
if (playOnLoad) this.$store.commit('setPlayOnLoad', false)
|
||||
|
||||
var currentTime = await this.getDownloadStartTime()
|
||||
if (isNaN(currentTime) || currentTime === null) currentTime = 0
|
||||
|
||||
// Update local current time
|
||||
this.$localStore.setCurrent({
|
||||
audiobookId: this.download.id,
|
||||
lastUpdate: Date.now()
|
||||
})
|
||||
|
||||
var audiobookStreamData = {
|
||||
title: this.title,
|
||||
author: this.author,
|
||||
playWhenReady: !!playOnLoad,
|
||||
startTime: String(Math.floor(currentTime * 1000)),
|
||||
playbackSpeed: this.playbackSpeed || 1,
|
||||
cover: this.download.coverUrl || null,
|
||||
duration: String(Math.floor(this.duration * 1000)),
|
||||
series: this.seriesTxt,
|
||||
token: this.$store.getters['user/getToken'],
|
||||
contentUrl: this.playingDownload.contentUrl,
|
||||
isLocal: true
|
||||
}
|
||||
|
||||
this.$refs.audioPlayerMini.set(audiobookStreamData)
|
||||
},
|
||||
streamOpen(stream) {
|
||||
console.log('[StreamContainer] Stream Open', stream)
|
||||
if (this.download) {
|
||||
if (this.$refs.audioPlayerMini) {
|
||||
this.$refs.audioPlayerMini.terminateStream()
|
||||
}
|
||||
}
|
||||
|
||||
this.lastProgressTimeUpdate = 0
|
||||
console.log('[StreamContainer] Stream Open: ' + this.title)
|
||||
|
||||
if (!this.$refs.audioPlayerMini) {
|
||||
console.error('No Audio Player Mini')
|
||||
return
|
||||
|
@ -160,6 +291,9 @@ export default {
|
|||
|
||||
this.stream = stream
|
||||
|
||||
// Update local remove current
|
||||
this.$localStore.setCurrent(null)
|
||||
|
||||
var playlistUrl = stream.clientPlaylistUri
|
||||
var currentTime = stream.clientCurrentTime || 0
|
||||
var playOnLoad = this.$store.state.playOnLoad
|
||||
|
@ -177,13 +311,17 @@ export default {
|
|||
playlistUrl: this.$server.url + playlistUrl,
|
||||
token: this.$store.getters['user/getToken']
|
||||
}
|
||||
|
||||
this.$refs.audioPlayerMini.set(audiobookStreamData)
|
||||
},
|
||||
audioPlayerMounted() {
|
||||
console.log('Audio Player Mounted', this.$server.stream)
|
||||
this.audioPlayerReady = true
|
||||
if (this.$server.stream) {
|
||||
|
||||
if (this.playingDownload) {
|
||||
console.log('[StreamContainer] Play download on audio mount')
|
||||
this.playDownload()
|
||||
} else if (this.$server.stream) {
|
||||
console.log('[StreamContainer] Open stream on audio mount')
|
||||
this.streamOpen(this.$server.stream)
|
||||
}
|
||||
},
|
||||
|
@ -206,6 +344,26 @@ export default {
|
|||
this.playbackSpeed = settings.playbackRate
|
||||
this.$refs.audioPlayerMini.updatePlaybackRate()
|
||||
}
|
||||
},
|
||||
streamUpdated(type, data) {
|
||||
if (type === 'download') {
|
||||
if (data) {
|
||||
console.log('START DOWNLOAD PLAY')
|
||||
this.download = { ...data }
|
||||
if (this.audioPlayerReady) {
|
||||
this.playDownload()
|
||||
}
|
||||
} else if (this.download) {
|
||||
console.log('STOP DOWNLOAD PLAY')
|
||||
this.cancelStream()
|
||||
}
|
||||
} else {
|
||||
if (data) {
|
||||
console.log('STARTING STREAM')
|
||||
} else {
|
||||
console.log('STOPPING STREAM')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
@ -213,15 +371,19 @@ export default {
|
|||
|
||||
this.setListeners()
|
||||
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
|
||||
this.$store.commit('setStreamListener', this.streamUpdated)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$server.socket.off('stream_open', this.streamOpen)
|
||||
this.$server.socket.off('stream_closed', this.streamClosed)
|
||||
this.$server.socket.off('stream_progress', this.streamProgress)
|
||||
this.$server.socket.off('stream_ready', this.streamReady)
|
||||
this.$server.socket.off('stream_reset', this.streamReset)
|
||||
if (this.$server.socket) {
|
||||
this.$server.socket.off('stream_open', this.streamOpen)
|
||||
this.$server.socket.off('stream_closed', this.streamClosed)
|
||||
this.$server.socket.off('stream_progress', this.streamProgress)
|
||||
this.$server.socket.off('stream_ready', this.streamReady)
|
||||
this.$server.socket.off('stream_reset', this.streamReset)
|
||||
}
|
||||
|
||||
this.$store.commit('user/removeSettingsListener', 'streamContainer')
|
||||
this.$store.commit('removeStreamListener')
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -11,7 +11,11 @@
|
|||
<div class="rounded-sm h-full overflow-hidden relative box-shadow-book">
|
||||
<nuxt-link :to="`/audiobook/${audiobookId}`" class="cursor-pointer">
|
||||
<div class="w-full relative" :style="{ height: height + 'px' }">
|
||||
<cards-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" />
|
||||
<cards-book-cover :audiobook="audiobook" :download-cover="downloadCover" :author-override="authorFormat" :width="width" />
|
||||
|
||||
<div v-if="download" class="absolute" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }">
|
||||
<span class="material-icons text-success" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">download_done</span>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
||||
|
||||
|
@ -35,6 +39,10 @@ export default {
|
|||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
localUserProgress: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 140
|
||||
|
@ -81,8 +89,13 @@ export default {
|
|||
orderBy() {
|
||||
return this.$store.getters['user/getUserSetting']('orderBy')
|
||||
},
|
||||
mostRecentUserProgress() {
|
||||
if (!this.localUserProgress) return this.userProgress
|
||||
if (!this.userProgress) return this.localUserProgress
|
||||
return this.localUserProgress.lastUpdate > this.userProgress.lastUpdate ? this.localUserProgress : this.userProgress
|
||||
},
|
||||
userProgressPercent() {
|
||||
return this.userProgress ? this.userProgress.progress || 0 : 0
|
||||
return this.mostRecentUserProgress ? this.mostRecentUserProgress.progress || 0 : 0
|
||||
},
|
||||
showError() {
|
||||
return this.hasMissingParts || this.hasInvalidParts
|
||||
|
@ -93,6 +106,12 @@ export default {
|
|||
hasInvalidParts() {
|
||||
return this.audiobook.hasInvalidParts
|
||||
},
|
||||
downloadCover() {
|
||||
return this.download ? this.download.cover : null
|
||||
},
|
||||
download() {
|
||||
return this.$store.getters['downloads/getDownloadIfReady'](this.audiobookId)
|
||||
},
|
||||
errorText() {
|
||||
var txt = ''
|
||||
if (this.hasMissingParts) {
|
||||
|
@ -105,12 +124,7 @@ export default {
|
|||
return txt || 'Unknown Error'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
play() {
|
||||
this.$store.commit('setStreamAudiobook', this.audiobook)
|
||||
this.$root.socket.emit('open_stream', this.audiobookId)
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -32,6 +32,7 @@ export default {
|
|||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
downloadCover: String,
|
||||
authorOverride: String,
|
||||
width: {
|
||||
type: Number,
|
||||
|
@ -78,7 +79,13 @@ export default {
|
|||
serverUrl() {
|
||||
return this.$store.state.serverUrl
|
||||
},
|
||||
networkConnected() {
|
||||
return this.$store.state.networkConnected
|
||||
},
|
||||
fullCoverUrl() {
|
||||
if (this.downloadCover) return this.downloadCover
|
||||
else if (!this.networkConnected) return this.placeholderUrl
|
||||
|
||||
if (this.cover.startsWith('http')) return this.cover
|
||||
var _clean = this.cover.replace(/\\/g, '/')
|
||||
if (_clean.startsWith('/local')) {
|
||||
|
@ -91,6 +98,7 @@ export default {
|
|||
return this.book.cover || this.placeholderUrl
|
||||
},
|
||||
hasCover() {
|
||||
if (!this.networkConnected && !this.downloadCover) return false
|
||||
return !!this.book.cover
|
||||
},
|
||||
sizeMultiplier() {
|
||||
|
|
147
components/modals/DownloadsModal.vue
Normal file
147
components/modals/DownloadsModal.vue
Normal file
|
@ -0,0 +1,147 @@
|
|||
<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="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
|
||||
<div v-if="!totalDownloads" class="flex items-center justify-center h-40">
|
||||
<p>No Downloads</p>
|
||||
</div>
|
||||
<ul 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>
|
||||
<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)">
|
||||
<div class="flex items-center justify-center">
|
||||
<img v-if="download.cover" :src="download.cover" class="w-10 h-16 object-contain" />
|
||||
<img v-else src="/book_placeholder.jpg" class="w-10 h-16 object-contain" />
|
||||
<div class="pl-2">
|
||||
<p class="font-normal truncate text-sm">{{ download.audiobook.book.title }}</p>
|
||||
<p class="font-normal truncate text-xs text-gray-400">{{ download.audiobook.book.author }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div v-if="download.isIncomplete" class="shadow-sm text-warning flex items-center justify-center rounded-full mr-4">
|
||||
<span class="material-icons">error_outline</span>
|
||||
</div>
|
||||
<button class="shadow-sm text-accent flex items-center justify-center rounded-full" @click.stop="clickedOption(download)">
|
||||
<span class="material-icons" style="font-size: 2rem">play_arrow</span>
|
||||
</button>
|
||||
<div class="shadow-sm text-error flex items-center justify-center rounded-ful ml-4" @click.stop="clickDelete(download)">
|
||||
<span class="material-icons" style="font-size: 1.2rem">delete</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
<template v-for="download in orphanDownloads">
|
||||
<li :key="download.id" class="text-gray-50 select-none relative cursor-pointer px-4 py-5 border-b border-white border-opacity-10">
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="font-normal block truncate text-sm">{{ download.filename }}</span>
|
||||
<div class="flex-grow" />
|
||||
<div class="shadow-sm text-warning flex items-center justify-center rounded-full">
|
||||
<span class="material-icons">error_outline</span>
|
||||
</div>
|
||||
<div class="shadow-sm text-error flex items-center justify-center rounded-ful ml-4" @click="clickDelete(download)">
|
||||
<span class="material-icons" style="font-size: 1.2rem">delete</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Dialog } from '@capacitor/dialog'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
downloadingProgress: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.$store.state.downloads.showModal
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit('downloads/setShowModal', val)
|
||||
}
|
||||
},
|
||||
totalDownloads() {
|
||||
return this.downloadsReady.length + this.orphanDownloads.length + this.downloadsDownloading.length
|
||||
},
|
||||
downloadsDownloading() {
|
||||
return this.downloads.filter((d) => d.isDownloading || d.isPreparing)
|
||||
},
|
||||
downloadsReady() {
|
||||
return this.downloads.filter((d) => !d.isDownloading && !d.isPreparing)
|
||||
},
|
||||
orphanDownloads() {
|
||||
return this.$store.state.downloads.orphanDownloads
|
||||
},
|
||||
downloads() {
|
||||
return this.$store.state.downloads.downloads
|
||||
// return [
|
||||
// {
|
||||
// id: 'asdf1',
|
||||
// title: 'Test Title 1',
|
||||
// author: 'Test Author 1',
|
||||
// isDownloading: true
|
||||
// },
|
||||
// {
|
||||
// id: 'asdf2',
|
||||
// title: 'Test Title 2',
|
||||
// author: 'Author 2',
|
||||
// isReady: true
|
||||
// },
|
||||
// {
|
||||
// id: 'asdf3',
|
||||
// name: 'asdf.mp3',
|
||||
// isReady: false,
|
||||
// isDownloading: false
|
||||
// }
|
||||
// ]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateDownloadProgress({ audiobookId, progress }) {
|
||||
this.$set(this.downloadingProgress, audiobookId, progress)
|
||||
},
|
||||
jumpToAudiobook(download) {
|
||||
this.show = false
|
||||
this.$router.push(`/audiobook/${download.id}`)
|
||||
},
|
||||
async clickDelete(download) {
|
||||
const { value } = await Dialog.confirm({
|
||||
title: 'Confirm',
|
||||
message: 'Delete this download?'
|
||||
})
|
||||
if (value) {
|
||||
this.$emit('deleteDownload', download)
|
||||
}
|
||||
},
|
||||
clickedOption(download) {
|
||||
console.log('Clicked download', download)
|
||||
this.$emit('selectDownload', download)
|
||||
this.show = false
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div ref="wrapper" class="modal modal-bg w-full h-full max-h-screen fixed top-0 left-0 bg-primary bg-opacity-75 flex items-center justify-center z-30 opacity-0">
|
||||
<div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" />
|
||||
<div class="absolute top-0 left-0 w-full h-36 bg-gradient-to-b from-black to-transparent opacity-70 pointer-events-none" />
|
||||
|
||||
<div class="absolute z-40 top-4 right-4 h-12 w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" @click="show = false">
|
||||
<span class="material-icons text-4xl">close</span>
|
||||
|
|
103
components/widgets/ConnectionIcon.vue
Normal file
103
components/widgets/ConnectionIcon.vue
Normal file
|
@ -0,0 +1,103 @@
|
|||
<template>
|
||||
<div>
|
||||
<nuxt-link v-if="isConnected" to="/account" class="p-2 bg-white bg-opacity-10 border border-white border-opacity-40 rounded-full h-11 w-11 flex items-center justify-center">
|
||||
<span class="material-icons">person</span>
|
||||
</nuxt-link>
|
||||
<div v-else-if="processing" class="relative p-2 bg-warning bg-opacity-10 border border-warning border-opacity-40 rounded-full h-11 w-11 flex items-center justify-center">
|
||||
<div class="loader-dots block relative w-10 h-2.5">
|
||||
<div class="absolute top-0 mt-0.5 w-1.5 h-1.5 rounded-full bg-warning"></div>
|
||||
<div class="absolute top-0 mt-0.5 w-1.5 h-1.5 rounded-full bg-warning"></div>
|
||||
<div class="absolute top-0 mt-0.5 w-1.5 h-1.5 rounded-full bg-warning"></div>
|
||||
<div class="absolute top-0 mt-0.5 w-1.5 h-1.5 rounded-full bg-warning"></div>
|
||||
</div>
|
||||
</div>
|
||||
<nuxt-link v-else to="/connect" class="relative p-2 bg-warning bg-opacity-10 border border-warning border-opacity-40 rounded-full h-11 w-11 flex items-center justify-center">
|
||||
<span class="material-icons">{{ networkIcon }}</span>
|
||||
<!-- <div class="absolute top-0 left-0"> -->
|
||||
<!-- <div class="absolute -top-5 -right-5 overflow-hidden">
|
||||
<svg class="w-20 h-20 animate-spin" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path clip-rule="evenodd" d="M15.165 8.53a.5.5 0 01-.404.58A7 7 0 1023 16a.5.5 0 011 0 8 8 0 11-9.416-7.874.5.5 0 01.58.404z" fill="currentColor" fill-rule="evenodd" />
|
||||
</svg>
|
||||
</div> -->
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
serverUrl: null,
|
||||
isConnected: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
networkConnected(newVal) {
|
||||
if (newVal) {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
user() {
|
||||
return this.$store.state.user.user
|
||||
},
|
||||
networkIcon() {
|
||||
if (!this.networkConnected) return 'signal_wifi_connected_no_internet_4'
|
||||
return 'cloud_off'
|
||||
},
|
||||
networkConnected() {
|
||||
return this.$store.state.networkConnected
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
socketConnected(val) {
|
||||
this.processing = false
|
||||
this.isConnected = val
|
||||
},
|
||||
async init() {
|
||||
if (this.isConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.$server) {
|
||||
console.error('Invalid server not initialized')
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.networkConnected) return
|
||||
|
||||
this.$server.on('connected', this.socketConnected)
|
||||
var localServerUrl = await this.$localStore.getServerUrl()
|
||||
var localUserToken = await this.$localStore.getToken()
|
||||
if (localServerUrl) {
|
||||
this.serverUrl = localServerUrl
|
||||
|
||||
// Server and Token are stored
|
||||
if (localUserToken) {
|
||||
this.processing = true
|
||||
var success = await this.$server.connect(localServerUrl, localUserToken)
|
||||
if (!success && !this.$server.url) {
|
||||
this.processing = false
|
||||
this.serverUrl = null
|
||||
} else if (!success) {
|
||||
this.processing = false
|
||||
}
|
||||
} else {
|
||||
// Server only is stored
|
||||
var success = await this.$server.check(this.serverUrl)
|
||||
if (!success) {
|
||||
console.error('Invalid server')
|
||||
this.$server.setServerUrl(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,24 +1,28 @@
|
|||
<template>
|
||||
<div class="w-full min-h-screen h-full bg-bg text-white">
|
||||
<app-appbar />
|
||||
<div id="content" class="overflow-hidden" :class="streaming ? 'streaming' : ''">
|
||||
<div id="content" class="overflow-hidden" :class="playerIsOpen ? 'playerOpen' : ''">
|
||||
<Nuxt />
|
||||
</div>
|
||||
<app-stream-container ref="streamContainer" />
|
||||
<modals-downloads-modal ref="downloadsModal" @selectDownload="selectDownload" @deleteDownload="deleteDownload" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Path from 'path'
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { Network } from '@capacitor/network'
|
||||
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
||||
import AudioDownloader from '@/plugins/audio-downloader'
|
||||
|
||||
export default {
|
||||
middleware: 'authenticated',
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
streaming() {
|
||||
return this.$store.state.streamAudiobook
|
||||
playerIsOpen() {
|
||||
return this.$store.getters['playerIsOpen']
|
||||
},
|
||||
routeName() {
|
||||
return this.$route.name
|
||||
|
@ -30,11 +34,50 @@ export default {
|
|||
if (isConnected) {
|
||||
this.$router.push('/')
|
||||
}
|
||||
} else {
|
||||
if (!isConnected) {
|
||||
this.$router.push('/connect')
|
||||
}
|
||||
this.syncUserProgress()
|
||||
},
|
||||
updateAudiobookProgressOnServer(audiobookProgress) {
|
||||
if (this.$server.socket) {
|
||||
this.$server.socket.emit('progress_update', audiobookProgress)
|
||||
}
|
||||
},
|
||||
syncUserProgress() {
|
||||
if (!this.$store.state.user.user) return
|
||||
|
||||
var userAudiobooks = this.$store.state.user.user.audiobooks
|
||||
var localAudiobooks = this.$store.state.user.localUserAudiobooks
|
||||
var localHasUpdates = false
|
||||
|
||||
var newestLocal = { ...localAudiobooks }
|
||||
for (const audiobookId in userAudiobooks) {
|
||||
if (localAudiobooks[audiobookId]) {
|
||||
if (localAudiobooks[audiobookId].lastUpdate > userAudiobooks[audiobookId].lastUpdate) {
|
||||
// Local progress is more recent than user progress
|
||||
this.updateAudiobookProgressOnServer(localAudiobooks[audiobookId])
|
||||
} else {
|
||||
// Server is more recent than local
|
||||
newestLocal[audiobookId] = userAudiobooks[audiobookId]
|
||||
localHasUpdates = true
|
||||
}
|
||||
} else {
|
||||
// Not on local yet - store on local
|
||||
newestLocal[audiobookId] = userAudiobooks[audiobookId]
|
||||
localHasUpdates = true
|
||||
}
|
||||
}
|
||||
|
||||
for (const audiobookId in localAudiobooks) {
|
||||
if (!userAudiobooks[audiobookId]) {
|
||||
// Local progress is not on server
|
||||
this.updateAudiobookProgressOnServer(localAudiobooks[audiobookId])
|
||||
}
|
||||
}
|
||||
|
||||
if (localHasUpdates) {
|
||||
console.log('Local audiobook progress has updates from server')
|
||||
this.$localStore.setAllAudiobookProgress(newestLocal)
|
||||
}
|
||||
},
|
||||
initialStream(stream) {
|
||||
if (this.$refs.streamContainer && this.$refs.streamContainer.audioPlayerReady) {
|
||||
|
@ -49,16 +92,16 @@ export default {
|
|||
await AppUpdate.openAppStore()
|
||||
}
|
||||
},
|
||||
showUpdateToast(availableVersion, immediateUpdateAllowed) {
|
||||
var toastText = immediateUpdateAllowed ? `Click here to update` : `Click here to open app store`
|
||||
this.$toast.info(`Update is available for v${availableVersion}! ${toastText}`, {
|
||||
draggable: false,
|
||||
hideProgressBar: false,
|
||||
timeout: 10000,
|
||||
closeButton: false,
|
||||
onClick: this.clickUpdateToast()
|
||||
})
|
||||
},
|
||||
// showUpdateToast(availableVersion, immediateUpdateAllowed) {
|
||||
// var toastText = immediateUpdateAllowed ? `Click here to update` : `Click here to open app store`
|
||||
// this.$toast.info(`Update is available for v${availableVersion}! ${toastText}`, {
|
||||
// draggable: false,
|
||||
// hideProgressBar: false,
|
||||
// timeout: 10000,
|
||||
// closeButton: false,
|
||||
// onClick: this.clickUpdateToast()
|
||||
// })
|
||||
// },
|
||||
async checkForUpdate() {
|
||||
const result = await AppUpdate.getAppUpdateInfo()
|
||||
if (!result) {
|
||||
|
@ -66,13 +109,165 @@ export default {
|
|||
return
|
||||
}
|
||||
this.$store.commit('setAppUpdateInfo', result)
|
||||
if (result.updateAvailability === 2) {
|
||||
setTimeout(() => {
|
||||
this.$toast.info(`Update is available!`, {
|
||||
draggable: false,
|
||||
hideProgressBar: false,
|
||||
timeout: 4000,
|
||||
closeButton: false,
|
||||
onClick: this.clickUpdateToast()
|
||||
})
|
||||
// this.showUpdateToast(result.availableVersion, !!result.immediateUpdateAllowed)
|
||||
}, 5000)
|
||||
}
|
||||
},
|
||||
onDownloadProgress(data) {
|
||||
// var downloadId = data.downloadId
|
||||
var progress = data.progress
|
||||
var filename = data.filename
|
||||
var audiobookId = filename ? Path.basename(filename, Path.extname(filename)) : ''
|
||||
|
||||
// if (result.updateAvailability === 2) {
|
||||
// setTimeout(() => {
|
||||
// this.showUpdateToast(result.availableVersion, !!result.immediateUpdateAllowed)
|
||||
// }, 5000)
|
||||
// }
|
||||
}
|
||||
var downloadObj = this.$store.getters['downloads/getDownload'](audiobookId)
|
||||
if (downloadObj) {
|
||||
if (this.$refs.downloadsModal) {
|
||||
this.$refs.downloadsModal.updateDownloadProgress({ audiobookId, progress })
|
||||
}
|
||||
this.$toast.update(downloadObj.toastId, { content: `${progress}% Downloading ${downloadObj.audiobook.book.title}` })
|
||||
}
|
||||
},
|
||||
onDownloadComplete(data) {
|
||||
var downloadId = data.downloadId
|
||||
var contentUrl = data.contentUrl
|
||||
var filename = data.filename
|
||||
var audiobookId = filename ? Path.basename(filename, Path.extname(filename)) : ''
|
||||
|
||||
if (audiobookId) {
|
||||
// Notify server to remove prepared download
|
||||
if (this.$server.socket) {
|
||||
this.$server.socket.emit('remove_download', downloadId)
|
||||
}
|
||||
|
||||
console.log('Download complete', filename, downloadId, contentUrl, 'AudiobookId:', audiobookId)
|
||||
var downloadObj = this.$store.getters['downloads/getDownload'](audiobookId)
|
||||
if (!downloadObj) {
|
||||
console.error('Could not find download...')
|
||||
} else {
|
||||
this.$toast.update(downloadObj.toastId, { content: `Success! ${downloadObj.audiobook.book.title} downloaded.`, options: { timeout: 5000, type: 'success' } }, true)
|
||||
|
||||
console.log('Found download, update with content url')
|
||||
delete downloadObj.isDownloading
|
||||
delete downloadObj.isPreparing
|
||||
downloadObj.contentUrl = contentUrl
|
||||
this.$store.commit('downloads/addUpdateDownload', downloadObj)
|
||||
}
|
||||
}
|
||||
},
|
||||
async checkLoadCurrent() {
|
||||
var currentObj = await this.$localStore.getCurrent()
|
||||
if (!currentObj) return
|
||||
|
||||
console.log('Has Current playing', currentObj.audiobookId)
|
||||
var download = this.$store.getters['downloads/getDownload'](currentObj.audiobookId)
|
||||
if (download) {
|
||||
this.$store.commit('setPlayingDownload', download)
|
||||
} else {
|
||||
console.warn('Download not available for previous current playing', currentObj.audiobookId)
|
||||
this.$localStore.setCurrent(null)
|
||||
}
|
||||
},
|
||||
onMediaLoaded(items) {
|
||||
var jsitems = JSON.parse(items)
|
||||
jsitems = jsitems.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
size: item.size,
|
||||
duration: item.duration,
|
||||
filename: item.name,
|
||||
audiobookId: item.name ? Path.basename(item.name, Path.extname(item.name)) : '',
|
||||
contentUrl: item.uri.replace(/\\\//g, '/'),
|
||||
coverUrl: item.coverUrl || null
|
||||
}
|
||||
})
|
||||
jsitems.forEach((item) => {
|
||||
var download = this.$store.getters['downloads/getDownload'](item.audiobookId)
|
||||
if (!download) {
|
||||
console.error(`Unknown media item found for filename ${item.filename}`)
|
||||
|
||||
var orphanDownload = {
|
||||
id: `orphan-${item.id}`,
|
||||
contentUrl: item.contentUrl,
|
||||
coverUrl: item.coverUrl,
|
||||
cover: item.coverUrl ? Capacitor.convertFileSrc(item.coverUrl) : null,
|
||||
mediaId: item.id,
|
||||
filename: item.filename,
|
||||
size: item.size,
|
||||
duration: item.duration,
|
||||
isOrphan: true
|
||||
}
|
||||
this.$store.commit('downloads/addUpdateDownload', orphanDownload)
|
||||
} else {
|
||||
console.log(`Found media item for audiobook ${download.audiobook.book.title} (${item.audiobookId})`)
|
||||
download.contentUrl = item.contentUrl
|
||||
download.coverUrl = item.coverUrl
|
||||
download.cover = item.coverUrl ? Capacitor.convertFileSrc(item.coverUrl) : null
|
||||
download.size = item.size
|
||||
download.duration = item.duration
|
||||
this.$store.commit('downloads/addUpdateDownload', download)
|
||||
|
||||
download.audiobook.isDownloaded = true
|
||||
this.$store.commit('audiobooks/addUpdate', download.audiobook)
|
||||
}
|
||||
})
|
||||
|
||||
var downloads = this.$store.state.downloads.downloads
|
||||
|
||||
downloads.forEach((download) => {
|
||||
var matchingItem = jsitems.find((item) => item.audiobookId === download.id)
|
||||
if (!matchingItem) {
|
||||
console.error(`Saved download not in media store ${download.audiobook.book.title} (${download.id})`)
|
||||
this.$store.commit('downloads/removeDownload', download)
|
||||
}
|
||||
})
|
||||
|
||||
this.checkLoadCurrent()
|
||||
},
|
||||
selectDownload(download) {
|
||||
this.$store.commit('setPlayOnLoad', true)
|
||||
this.$store.commit('setPlayingDownload', download)
|
||||
},
|
||||
async deleteDownload(download) {
|
||||
console.log('Delete download', download.filename)
|
||||
|
||||
if (this.$store.state.playingDownload && this.$store.state.playingDownload.id === download.id) {
|
||||
console.warn('Deleting download when currently playing download - terminate play')
|
||||
if (this.$refs.streamContainer) {
|
||||
this.$refs.streamContainer.cancelStream()
|
||||
}
|
||||
}
|
||||
|
||||
if (download.contentUrl) {
|
||||
await AudioDownloader.delete({ filename: download.filename, url: download.contentUrl, coverUrl: download.coverUrl })
|
||||
}
|
||||
this.$store.commit('downloads/removeDownload', download)
|
||||
},
|
||||
async initMediaStore() {
|
||||
// Load local database of downloads
|
||||
await this.$store.dispatch('downloads/loadFromStorage')
|
||||
await this.$localStore.loadUserAudiobooks()
|
||||
|
||||
// Request and setup listeners for media files on native
|
||||
AudioDownloader.addListener('onDownloadComplete', (data) => {
|
||||
this.onDownloadComplete(data)
|
||||
})
|
||||
AudioDownloader.addListener('onMediaLoaded', (data) => {
|
||||
this.onMediaLoaded(data.items)
|
||||
})
|
||||
AudioDownloader.addListener('onDownloadProgress', (data) => {
|
||||
this.onDownloadProgress(data)
|
||||
})
|
||||
AudioDownloader.load()
|
||||
},
|
||||
// parseSemver(ver) {
|
||||
// if (!ver) return null
|
||||
// var groups = ver.match(/^v((([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)$/)
|
||||
|
@ -130,6 +325,16 @@ export default {
|
|||
// this.$store.commit('setHasUpdate', true)
|
||||
// }
|
||||
// }
|
||||
async setupNetworkListener() {
|
||||
var status = await Network.getStatus()
|
||||
console.log('Network status', status.connected, status.connectionType)
|
||||
this.$store.commit('setNetworkStatus', status)
|
||||
|
||||
Network.addListener('networkStatusChange', (status) => {
|
||||
console.log('Network status changed', status.connected, status.connectionType)
|
||||
this.$store.commit('setNetworkStatus', status)
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.$server) return console.error('No Server')
|
||||
|
@ -137,16 +342,9 @@ export default {
|
|||
this.$server.on('connected', this.connected)
|
||||
this.$server.on('initialStream', this.initialStream)
|
||||
|
||||
if (!this.$server.connected) {
|
||||
this.$router.push('/connect')
|
||||
}
|
||||
|
||||
this.setupNetworkListener()
|
||||
this.checkForUpdate()
|
||||
|
||||
// var checkForUpdateFlag = localStorage.getItem('checkForUpdate')
|
||||
// if (!checkForUpdateFlag || checkForUpdateFlag !== '1') {
|
||||
// this.checkForUpdate()
|
||||
// }
|
||||
this.initMediaStore()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -155,7 +353,8 @@ export default {
|
|||
#content {
|
||||
height: calc(100vh - 64px);
|
||||
}
|
||||
#content.streaming {
|
||||
height: calc(100vh - 204px);
|
||||
#content.playerOpen {
|
||||
/* height: calc(100vh - 204px); */
|
||||
height: calc(100vh - 240px);
|
||||
}
|
||||
</style>
|
|
@ -26,8 +26,7 @@ export default {
|
|||
],
|
||||
link: [
|
||||
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
|
||||
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Fira+Mono&family=Ubuntu+Mono&family=Open+Sans:wght@400;600&family=Gentium+Book+Basic' },
|
||||
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/icon?family=Material+Icons' }
|
||||
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Ubuntu+Mono&family=Open+Sans:wght@400;600' },
|
||||
]
|
||||
},
|
||||
|
||||
|
@ -37,9 +36,11 @@ export default {
|
|||
|
||||
plugins: [
|
||||
'@/plugins/server.js',
|
||||
'@/plugins/store.js',
|
||||
'@/plugins/init.client.js',
|
||||
'@/plugins/axios.js',
|
||||
'@/plugins/my-native-audio.js',
|
||||
'@/plugins/audio-downloader.js',
|
||||
'@/plugins/toast.js'
|
||||
],
|
||||
|
||||
|
|
479
package-lock.json
generated
479
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "audiobookshelf-app",
|
||||
"version": "v0.2.1-beta",
|
||||
"version": "v0.4.0-beta",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -1000,6 +1000,11 @@
|
|||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@capacitor-community/sqlite": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-3.2.0.tgz",
|
||||
"integrity": "sha512-0tD+XKrtXS44DpeVLV0LR+UZafTBmLNHy3nQBr1lVMsefiunaU75q/BiYGhVdUIdp7x397y16s2RyTqroj5mbg=="
|
||||
},
|
||||
"@capacitor/android": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-3.2.2.tgz",
|
||||
|
@ -1061,11 +1066,62 @@
|
|||
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-3.2.2.tgz",
|
||||
"integrity": "sha512-Eq17Y+UDHFmYGaZcObvxHAcHw0fF9TCBAg1f5f6qdV8ab3cKKEUB9xMvoCSZAueBfxFARrD18TsZJKoxh2YsLA=="
|
||||
},
|
||||
"@capacitor/network": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/network/-/network-1.0.3.tgz",
|
||||
"integrity": "sha512-DgRusTC0UkTJE9IQIAMgqBnRnTaj8nFeGH7dwRldfVBZAtHBTkU8wCK/tU1oWtaY2Wam+iyVKXUAhYDO7yeD9Q=="
|
||||
},
|
||||
"@capacitor/storage": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/storage/-/storage-1.1.0.tgz",
|
||||
"integrity": "sha512-Fi5R542sHWfkzBcYeRl4zde+4zPqUpLk7S0LzrkCwn9eAy90v8yY6Pa6dXfADXvkPKDdPckmyCQ7CmM+YImsrQ=="
|
||||
},
|
||||
"@csstools/convert-colors": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz",
|
||||
"integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw=="
|
||||
},
|
||||
"@electron/get": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@electron/get/-/get-1.13.0.tgz",
|
||||
"integrity": "sha512-+SjZhRuRo+STTO1Fdhzqnv9D2ZhjxXP6egsJ9kiO8dtP68cDx7dFCwWi64dlMQV7sWcfW1OYCW4wviEBzmRsfQ==",
|
||||
"requires": {
|
||||
"debug": "^4.1.1",
|
||||
"env-paths": "^2.2.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"global-agent": "^2.0.2",
|
||||
"global-tunnel-ng": "^2.7.1",
|
||||
"got": "^9.6.0",
|
||||
"progress": "^2.0.3",
|
||||
"semver": "^6.2.0",
|
||||
"sumchecker": "^3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"fs-extra": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"universalify": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@ionic/cli-framework-output": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.2.tgz",
|
||||
|
@ -2619,6 +2675,19 @@
|
|||
"resolved": "https://registry.npmjs.org/@robingenz/capacitor-app-update/-/capacitor-app-update-1.0.0.tgz",
|
||||
"integrity": "sha512-pK8Yi7VgG/O/R4kJ3JtLpdeQgJzRIDPGM61bJhofTqu/+i26h8GhQdq4MB2OLJWk06Ht8MYDIIW/E0nxatrrnA=="
|
||||
},
|
||||
"@sindresorhus/is": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
|
||||
"integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ=="
|
||||
},
|
||||
"@szmarczak/http-timer": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
|
||||
"integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==",
|
||||
"requires": {
|
||||
"defer-to-connect": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@types/component-emitter": {
|
||||
"version": "1.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
|
||||
|
@ -3523,6 +3592,12 @@
|
|||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
|
||||
},
|
||||
"boolean": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.1.4.tgz",
|
||||
"integrity": "sha512-3hx0kwU3uzG6ReQ3pnaFQPSktpBw6RHN3/ivDKEuU8g1XSfafowyvDnadjv1xp8IZqhtSukxlwv9bF6FhX8m0w==",
|
||||
"optional": true
|
||||
},
|
||||
"boxen": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/boxen/-/boxen-5.0.1.tgz",
|
||||
|
@ -3875,6 +3950,40 @@
|
|||
"schema-utils": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"cacheable-request": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
|
||||
"integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==",
|
||||
"requires": {
|
||||
"clone-response": "^1.0.2",
|
||||
"get-stream": "^5.1.0",
|
||||
"http-cache-semantics": "^4.0.0",
|
||||
"keyv": "^3.0.0",
|
||||
"lowercase-keys": "^2.0.0",
|
||||
"normalize-url": "^4.1.0",
|
||||
"responselike": "^1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||
"requires": {
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"lowercase-keys": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="
|
||||
},
|
||||
"normalize-url": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
|
||||
"integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
|
@ -3941,6 +4050,15 @@
|
|||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001249.tgz",
|
||||
"integrity": "sha512-vcX4U8lwVXPdqzPWi6cAJ3FnQaqXbBqy/GZseKNQzRj37J7qZdGcBtxq/QLFNLLlfsoXLUdHw8Iwenri86Tagw=="
|
||||
},
|
||||
"capacitor-data-storage-sqlite": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/capacitor-data-storage-sqlite/-/capacitor-data-storage-sqlite-3.2.0.tgz",
|
||||
"integrity": "sha512-+8uyd9O+k/UleSGAKI7rNkF5AAAMeLzvzIqBle7sYjLRuhRiuCLapJ5b5GLAbzdtyUp72YoZX29XCKk3ywDAyg==",
|
||||
"requires": {
|
||||
"electron": "~13.1.4",
|
||||
"localforage": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
|
@ -4167,6 +4285,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"clone-response": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
|
||||
"integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
|
||||
"requires": {
|
||||
"mimic-response": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
|
@ -4296,6 +4422,16 @@
|
|||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"config-chain": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
|
||||
"integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ini": "^1.3.4",
|
||||
"proto-list": "~1.2.1"
|
||||
}
|
||||
},
|
||||
"connect": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz",
|
||||
|
@ -5016,6 +5152,14 @@
|
|||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
|
||||
},
|
||||
"decompress-response": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
|
||||
"integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
|
||||
"requires": {
|
||||
"mimic-response": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"deep-equal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
|
||||
|
@ -5027,6 +5171,11 @@
|
|||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
|
||||
},
|
||||
"defer-to-connect": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz",
|
||||
"integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ=="
|
||||
},
|
||||
"define-properties": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
|
||||
|
@ -5118,6 +5267,12 @@
|
|||
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz",
|
||||
"integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50="
|
||||
},
|
||||
"detect-node": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
||||
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||
"optional": true
|
||||
},
|
||||
"detective": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz",
|
||||
|
@ -5256,6 +5411,11 @@
|
|||
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
||||
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
|
||||
},
|
||||
"duplexer3": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
|
||||
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
|
||||
},
|
||||
"duplexify": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
|
||||
|
@ -5272,6 +5432,23 @@
|
|||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
|
||||
},
|
||||
"electron": {
|
||||
"version": "13.1.9",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-13.1.9.tgz",
|
||||
"integrity": "sha512-By4Zb72XNQLrPb70BXdIW3NtEHFwybP5DIQjohnCxOYONq5vojuHjNcTuWnBgMvwQ2qwykk6Tw5EwF2Pt0CWjA==",
|
||||
"requires": {
|
||||
"@electron/get": "^1.0.1",
|
||||
"@types/node": "^14.6.2",
|
||||
"extract-zip": "^1.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "14.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.15.tgz",
|
||||
"integrity": "sha512-D1sdW0EcSCmNdLKBGMYb38YsHUS6JcM7yQ6sLQ9KuZ35ck7LYCKE7kYFHOO59ayFOY3zobWVZxf4KXhYHcHYFA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.796",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.796.tgz",
|
||||
|
@ -5445,6 +5622,12 @@
|
|||
"is-symbol": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"es6-error": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
|
||||
"optional": true
|
||||
},
|
||||
"escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
|
@ -5726,6 +5909,32 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"extract-zip": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz",
|
||||
"integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==",
|
||||
"requires": {
|
||||
"concat-stream": "^1.6.2",
|
||||
"debug": "^2.6.9",
|
||||
"mkdirp": "^0.5.4",
|
||||
"yauzl": "^2.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
}
|
||||
}
|
||||
},
|
||||
"fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
|
@ -6070,11 +6279,58 @@
|
|||
"is-glob": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"global-agent": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-2.2.0.tgz",
|
||||
"integrity": "sha512-+20KpaW6DDLqhG7JDiJpD1JvNvb8ts+TNl7BPOYcURqCrXqnN1Vf+XVOrkKJAFPqfX+oEhsdzOj1hLWkBTdNJg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"boolean": "^3.0.1",
|
||||
"core-js": "^3.6.5",
|
||||
"es6-error": "^4.1.1",
|
||||
"matcher": "^3.0.0",
|
||||
"roarr": "^2.15.3",
|
||||
"semver": "^7.3.2",
|
||||
"serialize-error": "^7.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "7.3.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"global-tunnel-ng": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz",
|
||||
"integrity": "sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"encodeurl": "^1.0.2",
|
||||
"lodash": "^4.17.10",
|
||||
"npm-conf": "^1.1.3",
|
||||
"tunnel": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
|
||||
},
|
||||
"globalthis": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.2.tgz",
|
||||
"integrity": "sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"define-properties": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"globby": {
|
||||
"version": "11.0.4",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz",
|
||||
|
@ -6088,6 +6344,34 @@
|
|||
"slash": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"got": {
|
||||
"version": "9.6.0",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
|
||||
"integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==",
|
||||
"requires": {
|
||||
"@sindresorhus/is": "^0.14.0",
|
||||
"@szmarczak/http-timer": "^1.1.2",
|
||||
"cacheable-request": "^6.0.0",
|
||||
"decompress-response": "^3.3.0",
|
||||
"duplexer3": "^0.1.4",
|
||||
"get-stream": "^4.1.0",
|
||||
"lowercase-keys": "^1.0.1",
|
||||
"mimic-response": "^1.0.1",
|
||||
"p-cancelable": "^1.0.0",
|
||||
"to-readable-stream": "^1.0.0",
|
||||
"url-parse-lax": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"get-stream": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
|
||||
"integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
|
||||
"requires": {
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.6",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
|
||||
|
@ -6511,6 +6795,11 @@
|
|||
"http-errors": "~1.7.2"
|
||||
}
|
||||
},
|
||||
"http-cache-semantics": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
|
||||
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
|
||||
},
|
||||
"http-errors": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
|
||||
|
@ -6611,6 +6900,11 @@
|
|||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
|
||||
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw=="
|
||||
},
|
||||
"immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
|
||||
},
|
||||
"import-cwd": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
|
||||
|
@ -7100,6 +7394,11 @@
|
|||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="
|
||||
},
|
||||
"json-buffer": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
|
||||
"integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg="
|
||||
},
|
||||
"json-parse-better-errors": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
|
||||
|
@ -7116,6 +7415,12 @@
|
|||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
|
||||
},
|
||||
"json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
|
||||
"optional": true
|
||||
},
|
||||
"json5": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
|
||||
|
@ -7142,6 +7447,14 @@
|
|||
"tsscmp": "1.0.6"
|
||||
}
|
||||
},
|
||||
"keyv": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
|
||||
"integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==",
|
||||
"requires": {
|
||||
"json-buffer": "3.0.0"
|
||||
}
|
||||
},
|
||||
"kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
|
@ -7297,6 +7610,14 @@
|
|||
"launch-editor": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"lie": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
|
||||
"integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
|
||||
"requires": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"lilconfig": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.3.tgz",
|
||||
|
@ -7334,6 +7655,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"localforage": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
|
||||
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
|
||||
"requires": {
|
||||
"lie": "3.1.1"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
|
@ -7409,6 +7738,11 @@
|
|||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"lowercase-keys": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
|
||||
"integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA=="
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
|
@ -7446,6 +7780,23 @@
|
|||
"object-visit": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"matcher": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"escape-string-regexp": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"md5.js": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
||||
|
@ -7572,6 +7923,11 @@
|
|||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz",
|
||||
"integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ=="
|
||||
},
|
||||
"mimic-response": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="
|
||||
},
|
||||
"minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
|
@ -7949,6 +8305,24 @@
|
|||
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz",
|
||||
"integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg=="
|
||||
},
|
||||
"npm-conf": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz",
|
||||
"integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"config-chain": "^1.1.11",
|
||||
"pify": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"pify": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
|
||||
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"npm-run-path": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
|
||||
|
@ -8173,6 +8547,11 @@
|
|||
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
|
||||
},
|
||||
"p-cancelable": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
|
||||
"integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw=="
|
||||
},
|
||||
"p-defer": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
|
||||
|
@ -10998,6 +11377,11 @@
|
|||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="
|
||||
},
|
||||
"promise-inflight": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
|
||||
|
@ -11029,6 +11413,12 @@
|
|||
"signal-exit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"proto-list": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
||||
"integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=",
|
||||
"optional": true
|
||||
},
|
||||
"protocols": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz",
|
||||
|
@ -11550,6 +11940,14 @@
|
|||
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
|
||||
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo="
|
||||
},
|
||||
"responselike": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
|
||||
"integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=",
|
||||
"requires": {
|
||||
"lowercase-keys": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"restore-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
|
||||
|
@ -11601,6 +11999,28 @@
|
|||
"inherits": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"roarr": {
|
||||
"version": "2.15.4",
|
||||
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
|
||||
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"boolean": "^3.0.1",
|
||||
"detect-node": "^2.0.4",
|
||||
"globalthis": "^1.0.1",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"semver-compare": "^1.0.0",
|
||||
"sprintf-js": "^1.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"sprintf-js": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
|
||||
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"run-async": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
|
||||
|
@ -11680,6 +12100,12 @@
|
|||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
||||
},
|
||||
"semver-compare": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
||||
"integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
|
||||
"optional": true
|
||||
},
|
||||
"send": {
|
||||
"version": "0.17.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
|
||||
|
@ -11727,6 +12153,23 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"serialize-error": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
|
||||
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"type-fest": "^0.13.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"type-fest": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
||||
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"serialize-javascript": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
|
||||
|
@ -12361,6 +12804,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"sumchecker": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
||||
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
|
||||
"requires": {
|
||||
"debug": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
|
@ -12847,6 +13298,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"to-readable-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q=="
|
||||
},
|
||||
"to-regex": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
|
||||
|
@ -12902,6 +13358,12 @@
|
|||
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
|
||||
"integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY="
|
||||
},
|
||||
"tunnel": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
|
||||
"optional": true
|
||||
},
|
||||
"type-fest": {
|
||||
"version": "0.21.3",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
|
||||
|
@ -13141,6 +13603,21 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"url-parse-lax": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
|
||||
"integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=",
|
||||
"requires": {
|
||||
"prepend-http": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"prepend-http": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
|
||||
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc="
|
||||
}
|
||||
}
|
||||
},
|
||||
"use": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "audiobookshelf-app",
|
||||
"version": "v0.4.0-beta",
|
||||
"version": "v0.8.0-beta",
|
||||
"author": "advplyr",
|
||||
"scripts": {
|
||||
"dev": "nuxt --hostname localhost --port 1337",
|
||||
|
@ -10,14 +10,18 @@
|
|||
"icons-android": "cordova-res android --skip-config --copy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor-community/sqlite": "^3.2.0",
|
||||
"@capacitor/android": "^3.2.2",
|
||||
"@capacitor/cli": "^3.1.2",
|
||||
"@capacitor/core": "^3.2.2",
|
||||
"@capacitor/dialog": "^1.0.3",
|
||||
"@capacitor/ios": "^3.2.2",
|
||||
"@capacitor/network": "^1.0.3",
|
||||
"@capacitor/storage": "^1.1.0",
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
"@robingenz/capacitor-app-update": "^1.0.0",
|
||||
"axios": "^0.21.1",
|
||||
"capacitor-data-storage-sqlite": "^3.2.0",
|
||||
"core-js": "^3.15.1",
|
||||
"hls.js": "^1.0.9",
|
||||
"nuxt": "^2.15.7",
|
||||
|
|
|
@ -22,7 +22,9 @@
|
|||
|
||||
<p class="font-mono pt-1 pb-4">{{ $config.version }}</p>
|
||||
|
||||
<ui-btn v-if="isUpdateAvailable" class="w-full my-4" color="success" @click="clickUpdate"> Version {{ availableVersion }} is available! Open App Store</ui-btn>
|
||||
<ui-btn v-if="isUpdateAvailable" class="w-full my-4" color="success" @click="clickUpdate">Update is available</ui-btn>
|
||||
|
||||
<ui-btn v-if="!isUpdateAvailable || immediateUpdateAllowed" class="w-full my-4" color="primary" @click="openAppStore">Open app store</ui-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -67,8 +69,15 @@ export default {
|
|||
this.$server.logout()
|
||||
this.$router.push('/connect')
|
||||
},
|
||||
openAppStore() {
|
||||
AppUpdate.openAppStore()
|
||||
},
|
||||
async clickUpdate() {
|
||||
await AppUpdate.openAppStore()
|
||||
if (this.immediateUpdateAllowed) {
|
||||
AppUpdate.performImmediateUpdate()
|
||||
} else {
|
||||
AppUpdate.openAppStore()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
|
|
|
@ -3,17 +3,19 @@
|
|||
<div class="flex">
|
||||
<div class="w-32">
|
||||
<div class="relative">
|
||||
<cards-book-cover :audiobook="audiobook" :width="128" />
|
||||
<cards-book-cover :audiobook="audiobook" :download-cover="downlaodedCover" :width="128" />
|
||||
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm" :style="{ width: 128 * progressPercent + 'px' }"></div>
|
||||
</div>
|
||||
<!-- <cards-book-cover :audiobook="audiobook" :width="128" /> -->
|
||||
<div class="flex my-4">
|
||||
<p 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 class="text-gray-300 text-sm my-1">
|
||||
{{ durationPretty }}<span class="px-4">{{ sizePretty }}</span>
|
||||
{{ $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' : ''">
|
||||
|
@ -24,11 +26,15 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<ui-btn color="success" :disabled="streaming" class="flex items-center justify-center w-full mt-4" :padding-x="4" @click="playClick">
|
||||
<span v-show="!streaming" class="material-icons">play_arrow</span>
|
||||
<span class="px-1">{{ streaming ? 'Streaming' : 'Play' }}</span>
|
||||
</ui-btn>
|
||||
<div class="flex my-4"></div>
|
||||
<div v-if="isConnected || isDownloadPlayable" class="flex mt-4">
|
||||
<ui-btn color="success" :disabled="isPlaying" class="flex items-center justify-center w-full mr-2" :padding-x="4" @click="playClick">
|
||||
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
|
||||
<span class="px-1">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : isDownloadPlayable ? 'Play local' : 'Play stream' }}</span>
|
||||
</ui-btn>
|
||||
<ui-btn v-if="isConnected" color="primary" :disabled="isPlaying" class="flex items-center justify-center" :padding-x="2" @click="downloadClick">
|
||||
<span class="material-icons" :class="isDownloaded ? 'animate-pulse' : ''">{{ downloadObj ? (isDownloading || isDownloadPreparing ? 'downloading' : 'download_done') : 'download' }}</span>
|
||||
</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full py-4">
|
||||
|
@ -38,15 +44,25 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Path from 'path'
|
||||
import { Dialog } from '@capacitor/dialog'
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import AudioDownloader from '@/plugins/audio-downloader'
|
||||
|
||||
export default {
|
||||
async asyncData({ params, redirect, app }) {
|
||||
async asyncData({ store, params, redirect, app }) {
|
||||
var audiobookId = params.id
|
||||
var audiobook = await app.$axios.$get(`/api/audiobook/${audiobookId}`).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
return false
|
||||
})
|
||||
var audiobook = null
|
||||
|
||||
if (app.$server.connected) {
|
||||
audiobook = await app.$axios.$get(`/api/audiobook/${audiobookId}`).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
return false
|
||||
})
|
||||
} else {
|
||||
audiobook = store.getters['audiobooks/getAudiobook'](audiobookId)
|
||||
}
|
||||
|
||||
if (!audiobook) {
|
||||
console.error('No audiobook...', params.id)
|
||||
return redirect('/')
|
||||
|
@ -61,6 +77,9 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
isConnected() {
|
||||
return this.$store.state.socketConnected
|
||||
},
|
||||
audiobookId() {
|
||||
return this.audiobook.id
|
||||
},
|
||||
|
@ -87,14 +106,11 @@ export default {
|
|||
if (!this.volumeNumber) return this.series
|
||||
return `${this.series} #${this.volumeNumber}`
|
||||
},
|
||||
durationPretty() {
|
||||
return this.audiobook.durationPretty
|
||||
},
|
||||
duration() {
|
||||
return this.audiobook.duration
|
||||
},
|
||||
sizePretty() {
|
||||
return this.audiobook.sizePretty
|
||||
size() {
|
||||
return this.audiobook.size
|
||||
},
|
||||
userAudiobooks() {
|
||||
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
||||
|
@ -102,27 +118,65 @@ export default {
|
|||
userAudiobook() {
|
||||
return this.userAudiobooks[this.audiobookId] || null
|
||||
},
|
||||
localUserAudiobooks() {
|
||||
return this.$store.state.user.localUserAudiobooks || {}
|
||||
},
|
||||
localUserAudiobook() {
|
||||
return this.localUserAudiobooks[this.audiobookId] || null
|
||||
},
|
||||
mostRecentUserAudiobook() {
|
||||
if (!this.localUserAudiobook) return this.userAudiobook
|
||||
if (!this.userAudiobook) return this.localUserAudiobook
|
||||
return this.localUserAudiobook.lastUpdate > this.userAudiobook.lastUpdate ? this.localUserAudiobook : this.userAudiobook
|
||||
},
|
||||
userCurrentTime() {
|
||||
return this.userAudiobook ? this.userAudiobook.currentTime : 0
|
||||
return this.mostRecentUserAudiobook ? this.mostRecentUserAudiobook.currentTime : 0
|
||||
},
|
||||
userTimeRemaining() {
|
||||
return this.duration - this.userCurrentTime
|
||||
},
|
||||
progressPercent() {
|
||||
return this.userAudiobook ? this.userAudiobook.progress : 0
|
||||
},
|
||||
streamAudiobook() {
|
||||
return this.$store.state.streamAudiobook
|
||||
return this.mostRecentUserAudiobook ? this.mostRecentUserAudiobook.progress : 0
|
||||
},
|
||||
isStreaming() {
|
||||
return this.streamAudiobook && this.streamAudiobook.id === this.audiobookId
|
||||
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
|
||||
},
|
||||
isDownloading() {
|
||||
return this.downloadObj ? this.downloadObj.isDownloading : false
|
||||
},
|
||||
isDownloadPreparing() {
|
||||
return this.downloadObj ? this.downloadObj.isPreparing : false
|
||||
},
|
||||
isDownloadPlayable() {
|
||||
return this.downloadObj && !this.isDownloading && !this.isDownloadPreparing
|
||||
},
|
||||
downlaodedCover() {
|
||||
return this.downloadObj ? this.downloadObj.cover : null
|
||||
},
|
||||
downloadObj() {
|
||||
return this.$store.getters['downloads/getDownload'](this.audiobookId)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
playClick() {
|
||||
this.$store.commit('setPlayOnLoad', true)
|
||||
this.$store.commit('setStreamAudiobook', this.audiobook)
|
||||
this.$server.socket.emit('open_stream', this.audiobook.id)
|
||||
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({
|
||||
|
@ -132,17 +186,26 @@ export default {
|
|||
|
||||
if (value) {
|
||||
this.resettingProgress = true
|
||||
this.$axios
|
||||
.$delete(`/api/user/audiobook/${this.audiobookId}`)
|
||||
.then(() => {
|
||||
console.log('Progress reset complete')
|
||||
this.$toast.success(`Your progress was reset`)
|
||||
this.resettingProgress = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Progress reset failed', error)
|
||||
this.resettingProgress = false
|
||||
})
|
||||
if (this.$server.connected) {
|
||||
await this.$axios
|
||||
.$delete(`/api/user/audiobook/${this.audiobookId}`)
|
||||
.then(() => {
|
||||
console.log('Progress reset complete')
|
||||
this.$toast.success(`Your progress was reset`)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Progress reset failed', error)
|
||||
})
|
||||
}
|
||||
this.$localStore.updateUserAudiobookProgress({
|
||||
audiobookId: this.audiobookId,
|
||||
currentTime: 0,
|
||||
totalDuration: this.duration,
|
||||
progress: 0,
|
||||
lastUpdate: Date.now(),
|
||||
isRead: false
|
||||
})
|
||||
this.resettingProgress = false
|
||||
}
|
||||
},
|
||||
audiobookUpdated() {
|
||||
|
@ -155,12 +218,160 @@ export default {
|
|||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
})
|
||||
},
|
||||
downloadClick() {
|
||||
if (!this.$server.connected) return
|
||||
|
||||
if (this.downloadObj) {
|
||||
console.log('Already downloaded', this.downloadObj)
|
||||
} else {
|
||||
this.prepareDownload()
|
||||
}
|
||||
},
|
||||
async prepareDownload() {
|
||||
// var audiobook = await this.$axios.$get(`/api/audiobook/${this.audiobookId}`).catch((error) => {
|
||||
// console.error('Failed', error)
|
||||
// return false
|
||||
// })
|
||||
var audiobook = this.audiobook
|
||||
if (!audiobook) {
|
||||
return
|
||||
}
|
||||
|
||||
var downloadObject = {
|
||||
id: this.audiobookId,
|
||||
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
|
||||
var url = `${this.$store.state.serverUrl}/local/${track.path}`
|
||||
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 _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}`
|
||||
}
|
||||
return _clean
|
||||
},
|
||||
async downloadCover(download) {
|
||||
var coverUrl = this.getCoverUrlForDownload()
|
||||
if (!coverUrl) {
|
||||
return null
|
||||
}
|
||||
var extname = Path.extname(coverUrl) || '.jpg'
|
||||
|
||||
var downloadRequestPayload = {
|
||||
downloadUrl: coverUrl,
|
||||
filename: `${download.id}${extname}`,
|
||||
title: download.audiobook.book.title
|
||||
}
|
||||
console.log('Starting cover download', coverUrl, downloadRequestPayload.filename)
|
||||
var downloadRes = await AudioDownloader.downloadCover(downloadRequestPayload)
|
||||
if (downloadRes && downloadRes.url) {
|
||||
console.log('Cover art downloaded', downloadRes.url)
|
||||
return downloadRes.url
|
||||
} else {
|
||||
console.error('Cover art failed to download')
|
||||
return null
|
||||
}
|
||||
},
|
||||
async startDownload(url, fileext, download) {
|
||||
this.$toast.update(download.toastId, { content: `Downloading "${download.audiobook.book.title}"...` })
|
||||
|
||||
download.isDownloading = true
|
||||
download.isPreparing = false
|
||||
download.filename = `${download.id}${fileext}`
|
||||
this.$store.commit('downloads/addUpdateDownload', download)
|
||||
|
||||
console.log('Starting Download URL', url)
|
||||
var downloadRequestPayload = {
|
||||
filename: download.filename,
|
||||
downloadUrl: url,
|
||||
title: download.audiobook.book.title
|
||||
}
|
||||
var downloadRes = await AudioDownloader.download(downloadRequestPayload)
|
||||
var downloadId = downloadRes.value
|
||||
if (!downloadId) {
|
||||
console.error('Invalid download, removing')
|
||||
this.$toast.update(download.toastId, { content: `Failed download "${download.audiobook.book.title}"`, options: { timeout: 5000, type: 'error' } })
|
||||
this.$store.commit('downloads/removeDownload', download)
|
||||
} else {
|
||||
console.log('Download cover now')
|
||||
var coverUrl = await this.downloadCover(download)
|
||||
if (coverUrl) {
|
||||
console.log('Got cover url', coverUrl)
|
||||
download.coverUrl = coverUrl
|
||||
download.cover = Capacitor.convertFileSrc(coverUrl)
|
||||
this.$store.commit('downloads/addUpdateDownload', 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}`
|
||||
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() {
|
||||
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.$store.commit('audiobooks/addListener', { id: 'audiobook', audiobookId: this.audiobookId, meth: this.audiobookUpdated })
|
||||
},
|
||||
beforeDestroy() {
|
||||
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.$store.commit('audiobooks/removeListener', 'audiobook')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
<template>
|
||||
<div class="w-full h-full">
|
||||
<div class="relative flex items-center justify-center min-h-screen sm:pt-0">
|
||||
<nuxt-link to="/" class="absolute top-2 left-2 z-20">
|
||||
<span class="material-icons text-4xl">arrow_back</span>
|
||||
</nuxt-link>
|
||||
<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" />
|
||||
<h1 class="text-2xl font-book">AudioBookshelf</h1>
|
||||
</div>
|
||||
<p class="hidden absolute short:block top-0 left-0 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>
|
||||
|
||||
<div class="max-w-sm mx-auto sm:px-6 lg:px-8 z-10">
|
||||
<div v-show="loggedIn" class="mt-8 bg-primary overflow-hidden shadow rounded-lg p-6 text-center">
|
||||
|
@ -15,8 +18,8 @@
|
|||
<div v-show="!loggedIn" class="mt-8 bg-primary overflow-hidden shadow rounded-lg p-6">
|
||||
<h2 class="text-xl leading-7 mb-4">Enter an <span class="font-book font-normal">AudioBookshelf</span><br />server address:</h2>
|
||||
<form v-show="!showAuth" @submit.prevent="submit" novalidate>
|
||||
<ui-text-input v-model="serverUrl" :disabled="processing" placeholder="http://55.55.55.55:13378" type="url" class="w-60 sm:w-72 h-10" />
|
||||
<ui-btn :disabled="processing" type="submit" :padding-x="3" class="h-10">Submit</ui-btn>
|
||||
<ui-text-input v-model="serverUrl" :disabled="processing || !networkConnected" placeholder="http://55.55.55.55:13378" type="url" class="w-60 sm:w-72 h-10" />
|
||||
<ui-btn :disabled="processing || !networkConnected" type="submit" :padding-x="3" class="h-10">{{ networkConnected ? 'Submit' : 'No Internet' }}</ui-btn>
|
||||
</form>
|
||||
<template v-if="showAuth">
|
||||
<div class="flex items-center">
|
||||
|
@ -29,7 +32,7 @@
|
|||
<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" type="submit" class="mt-1 h-10">Submit</ui-btn>
|
||||
<ui-btn :disabled="processing || !networkConnected" type="submit" class="mt-1 h-10">{{ networkConnected ? 'Submit' : 'No Internet' }}</ui-btn>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
|
@ -77,8 +80,16 @@ export default {
|
|||
loggedIn: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
networkConnected() {
|
||||
return this.$store.state.networkConnected
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async submit() {
|
||||
if (!this.networkConnected) {
|
||||
return
|
||||
}
|
||||
if (!this.serverUrl.startsWith('http')) {
|
||||
this.serverUrl = 'http://' + this.serverUrl
|
||||
}
|
||||
|
@ -94,6 +105,9 @@ export default {
|
|||
}
|
||||
},
|
||||
async submitAuth() {
|
||||
if (!this.networkConnected) {
|
||||
return
|
||||
}
|
||||
if (!this.username) {
|
||||
this.error = 'Invalid username'
|
||||
return
|
||||
|
@ -137,8 +151,11 @@ export default {
|
|||
}
|
||||
this.$server.on('connected', this.socketConnected)
|
||||
|
||||
var localServerUrl = localStorage.getItem('serverUrl')
|
||||
var localUserToken = localStorage.getItem('userToken')
|
||||
var localServerUrl = await this.$localStore.getServerUrl()
|
||||
var localUserToken = await this.$localStore.getToken()
|
||||
|
||||
if (!this.networkConnected) return
|
||||
|
||||
if (localServerUrl) {
|
||||
this.serverUrl = localServerUrl
|
||||
if (localUserToken) {
|
||||
|
|
4
plugins/audio-downloader.js
Normal file
4
plugins/audio-downloader.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { registerPlugin } from '@capacitor/core';
|
||||
|
||||
const AudioDownloader = registerPlugin('AudioDownloader');
|
||||
export default AudioDownloader;
|
|
@ -1,7 +1,5 @@
|
|||
import Vue from 'vue'
|
||||
import Server from '../Server'
|
||||
|
||||
Vue.prototype.$server = null
|
||||
export default function ({ store }) {
|
||||
Vue.prototype.$server = new Server(store)
|
||||
export default function ({ store }, inject) {
|
||||
inject('server', new Server(store))
|
||||
}
|
||||
|
|
558
plugins/store.js
Normal file
558
plugins/store.js
Normal file
|
@ -0,0 +1,558 @@
|
|||
import { Capacitor } from '@capacitor/core';
|
||||
import { CapacitorDataStorageSqlite } from 'capacitor-data-storage-sqlite';
|
||||
import { Storage } from '@capacitor/storage'
|
||||
|
||||
class StoreService {
|
||||
store
|
||||
isService = false
|
||||
platform
|
||||
isOpen = false
|
||||
tableName = 'downloads'
|
||||
|
||||
constructor() {
|
||||
this.init()
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin Initialization
|
||||
*/
|
||||
init() {
|
||||
this.platform = Capacitor.getPlatform()
|
||||
this.store = CapacitorDataStorageSqlite
|
||||
this.isService = true
|
||||
console.log('in init ', this.platform, this.isService)
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a Store
|
||||
* @param _dbName string optional
|
||||
* @param _table string optional
|
||||
* @param _encrypted boolean optional
|
||||
* @param _mode string optional
|
||||
*/
|
||||
async openStore(_dbName, _table, _encrypted, _mode) {
|
||||
if (this.isService && this.store != null) {
|
||||
const database = _dbName ? _dbName : "storage"
|
||||
const table = _table ? _table : "storage_table"
|
||||
const encrypted = _encrypted ? _encrypted : false
|
||||
const mode = _mode ? _mode : "no-encryption"
|
||||
|
||||
this.isOpen = false
|
||||
try {
|
||||
await this.store.openStore({ database, table, encrypted, mode })
|
||||
// return Promise.resolve()
|
||||
this.isOpen = true
|
||||
return true
|
||||
} catch (err) {
|
||||
// return Promise.reject(err)
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// return Promise.reject(new Error("openStore: Store not opened"))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a store
|
||||
* @param dbName
|
||||
* @returns
|
||||
*/
|
||||
async closeStore(dbName) {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
await this.store.closeStore({ database: dbName })
|
||||
return Promise.resolve()
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("close: Store not opened"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a store is opened
|
||||
* @param dbName
|
||||
* @returns
|
||||
*/
|
||||
async isStoreOpen(dbName) {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
const ret = await this.store.isStoreOpen({ database: dbName })
|
||||
return Promise.resolve(ret)
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("isStoreOpen: Store not opened"))
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Check if a store already exists
|
||||
* @param dbName
|
||||
* @returns
|
||||
*/
|
||||
async isStoreExists(dbName) {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
const ret = await this.store.isStoreExists({ database: dbName })
|
||||
return Promise.resolve(ret)
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("isStoreExists: Store not opened"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create/Set a Table
|
||||
* @param table string
|
||||
*/
|
||||
async setTable(table) {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
await this.store.setTable({ table })
|
||||
return Promise.resolve()
|
||||
} catch (err) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("setTable: Store not opened"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of Key
|
||||
* @param key string
|
||||
* @param value string
|
||||
*/
|
||||
async setItem(key, value) {
|
||||
if (this.isService && this.store != null) {
|
||||
if (key.length > 0) {
|
||||
try {
|
||||
await this.store.set({ key, value });
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("setItem: Must give a key"));
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("setItem: Store not opened"));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Get the Value for a given Key
|
||||
* @param key string
|
||||
*/
|
||||
async getItem(key) {
|
||||
if (this.isService && this.store != null) {
|
||||
if (key.length > 0) {
|
||||
try {
|
||||
const { value } = await this.store.get({ key });
|
||||
console.log("in getItem value ", value)
|
||||
return Promise.resolve(value);
|
||||
} catch (err) {
|
||||
console.error(`in getItem key: ${key} err: ${JSON.stringify(err)}`)
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("getItem: Must give a key"));
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("getItem: Store not opened"));
|
||||
}
|
||||
|
||||
}
|
||||
async isKey(key) {
|
||||
if (this.isService && this.store != null) {
|
||||
if (key.length > 0) {
|
||||
try {
|
||||
const { result } = await this.store.iskey({ key });
|
||||
return Promise.resolve(result);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("isKey: Must give a key"));
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("isKey: Store not opened"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async getAllKeys() {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
const { keys } = await this.store.keys();
|
||||
return Promise.resolve(keys);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("getAllKeys: Store not opened"));
|
||||
}
|
||||
}
|
||||
async getAllValues() {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
const { values } = await this.store.values();
|
||||
return Promise.resolve(values);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("getAllValues: Store not opened"));
|
||||
}
|
||||
}
|
||||
async getFilterValues(filter) {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
const { values } = await this.store.filtervalues({ filter });
|
||||
return Promise.resolve(values);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("getFilterValues: Store not opened"));
|
||||
}
|
||||
}
|
||||
async getAllKeysValues() {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
const { keysvalues } = await this.store.keysvalues();
|
||||
return Promise.resolve(keysvalues);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("getAllKeysValues: Store not opened"));
|
||||
}
|
||||
}
|
||||
|
||||
async removeItem(key) {
|
||||
if (this.isService && this.store != null) {
|
||||
if (key.length > 0) {
|
||||
try {
|
||||
await this.store.remove({ key });
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("removeItem: Must give a key"));
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("removeItem: Store not opened"));
|
||||
}
|
||||
}
|
||||
|
||||
async clear() {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
await this.store.clear();
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
return Promise.reject(err.message);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("clear: Store not opened"));
|
||||
}
|
||||
}
|
||||
|
||||
async deleteStore(_dbName) {
|
||||
const database = _dbName ? _dbName : "storage"
|
||||
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
await this.store.deleteStore({ database })
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
return Promise.reject(err.message)
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("deleteStore: Store not opened"));
|
||||
}
|
||||
}
|
||||
async isTable(table) {
|
||||
if (this.isService && this.store != null) {
|
||||
if (table.length > 0) {
|
||||
try {
|
||||
const { result } = await this.store.isTable({ table });
|
||||
return Promise.resolve(result);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("isTable: Must give a table"));
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("isTable: Store not opened"));
|
||||
}
|
||||
}
|
||||
async getAllTables() {
|
||||
if (this.isService && this.store != null) {
|
||||
try {
|
||||
const { tables } = await this.store.tables();
|
||||
return Promise.resolve(tables);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("getAllTables: Store not opened"));
|
||||
}
|
||||
}
|
||||
async deleteTable(table) {
|
||||
if (this.isService && this.store != null) {
|
||||
if (table.length > 0) {
|
||||
try {
|
||||
await this.store.deleteTable({ table });
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("deleteTable: Must give a table"));
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error("deleteTable: Store not opened"));
|
||||
}
|
||||
}
|
||||
|
||||
async getDownload(id) {
|
||||
if (!this.isOpen) {
|
||||
var success = await this.openStore('storage', this.tableName)
|
||||
if (!success) {
|
||||
console.error('Store failed to open')
|
||||
return null
|
||||
}
|
||||
}
|
||||
try {
|
||||
var value = await this.getItem(id)
|
||||
return JSON.parse(value)
|
||||
} catch (error) {
|
||||
console.error('Failed to get download from store', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async setDownload(download) {
|
||||
if (!this.isOpen) {
|
||||
var success = await this.openStore('storage', this.tableName)
|
||||
if (!success) {
|
||||
console.error('Store failed to open')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.setItem(download.id, JSON.stringify(download))
|
||||
console.log(`[STORE] Set Download ${download.id}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to set download in store', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async removeDownload(id) {
|
||||
if (!this.isOpen) {
|
||||
var success = await this.openStore('storage', this.tableName)
|
||||
if (!success) {
|
||||
console.error('Store failed to open')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.removeItem(id)
|
||||
console.log(`[STORE] Removed download ${id}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to remove download in store', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async getAllDownloads() {
|
||||
if (!this.isOpen) {
|
||||
var success = await this.openStore('storage', this.tableName)
|
||||
if (!success) {
|
||||
console.error('Store failed to open')
|
||||
return []
|
||||
}
|
||||
}
|
||||
var keysvalues = await this.getAllKeysValues()
|
||||
var downloads = []
|
||||
|
||||
for (let i = 0; i < keysvalues.length; i++) {
|
||||
try {
|
||||
var download = JSON.parse(keysvalues[i].value)
|
||||
downloads.push(download)
|
||||
} catch (error) {
|
||||
console.error('Failed to parse download', error)
|
||||
await this.removeItem(keysvalues[i].key)
|
||||
}
|
||||
}
|
||||
|
||||
return downloads
|
||||
}
|
||||
}
|
||||
|
||||
class LocalStorage {
|
||||
constructor(vuexStore) {
|
||||
this.vuexStore = vuexStore
|
||||
|
||||
this.userAudiobooksLoaded = false
|
||||
this.userAudiobooks = {}
|
||||
}
|
||||
|
||||
async getMostRecentUserAudiobook(audiobookId) {
|
||||
if (!this.userAudiobooksLoaded) {
|
||||
await this.loadUserAudiobooks()
|
||||
}
|
||||
var local = this.getUserAudiobook(audiobookId)
|
||||
var server = this.vuexStore.getters['user/getUserAudiobook'](audiobookId)
|
||||
|
||||
if (local && server) {
|
||||
if (local.lastUpdate > server.lastUpdate) {
|
||||
console.log('[LocalStorage] Most recent user audiobook is from LOCAL')
|
||||
return local
|
||||
}
|
||||
console.log('[LocalStorage] Most recent user audiobook is from SERVER')
|
||||
return server
|
||||
} else if (local) {
|
||||
console.log('[LocalStorage] Most recent user audiobook is from LOCAL')
|
||||
return local
|
||||
} else if (server) {
|
||||
console.log('[LocalStorage] Most recent user audiobook is from SERVER')
|
||||
return server
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async loadUserAudiobooks() {
|
||||
try {
|
||||
var val = (await Storage.get({ key: 'userAudiobooks' }) || {}).value || null
|
||||
this.userAudiobooks = val ? JSON.parse(val) : {}
|
||||
this.userAudiobooksLoaded = true
|
||||
this.vuexStore.commit('user/setLocalUserAudiobooks', this.userAudiobooks)
|
||||
console.log('[LocalStorage] Loaded Local USER Audiobooks ' + JSON.stringify(this.userAudiobooks))
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to load user audiobooks', error)
|
||||
}
|
||||
}
|
||||
|
||||
async saveUserAudiobooks() {
|
||||
try {
|
||||
await Storage.set({ key: 'userAudiobooks', value: JSON.stringify(this.userAudiobooks) })
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to set user audiobooks', error)
|
||||
}
|
||||
}
|
||||
|
||||
async setAllAudiobookProgress(progresses) {
|
||||
this.userAudiobooks = progresses
|
||||
await this.saveUserAudiobooks()
|
||||
this.vuexStore.commit('user/setLocalUserAudiobooks', this.userAudiobooks)
|
||||
}
|
||||
|
||||
async updateUserAudiobookProgress(progressPayload) {
|
||||
this.userAudiobooks[progressPayload.audiobookId] = {
|
||||
...progressPayload
|
||||
}
|
||||
console.log('[LocalStorage] Updated User Audiobook Progress ' + progressPayload.audiobookId)
|
||||
await this.saveUserAudiobooks()
|
||||
this.vuexStore.commit('user/setLocalUserAudiobooks', this.userAudiobooks)
|
||||
}
|
||||
|
||||
async removeAudiobookProgress(audiobookId) {
|
||||
if (!this.userAudiobooks[audiobookId]) return
|
||||
delete this.userAudiobooks[audiobookId]
|
||||
await this.saveUserAudiobooks()
|
||||
this.vuexStore.commit('user/setLocalUserAudiobooks', this.userAudiobooks)
|
||||
}
|
||||
|
||||
getUserAudiobook(audiobookId) {
|
||||
return this.userAudiobooks[audiobookId] || null
|
||||
}
|
||||
|
||||
async setToken(token) {
|
||||
try {
|
||||
if (token) {
|
||||
await Storage.set({ key: 'token', value: token })
|
||||
} else {
|
||||
await Storage.remove({ key: 'token' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to set token', error)
|
||||
}
|
||||
}
|
||||
|
||||
async getToken() {
|
||||
try {
|
||||
return (await Storage.get({ key: 'token' }) || {}).value || null
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get token', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async getServerUrl() {
|
||||
try {
|
||||
return (await Storage.get({ key: 'serverUrl' }) || {}).value || null
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get serverUrl', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async setUserSettings(settings) {
|
||||
try {
|
||||
await Storage.set({ key: 'userSettings', value: JSON.stringify(settings) })
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to update user settings', error)
|
||||
}
|
||||
}
|
||||
|
||||
async getUserSettings() {
|
||||
try {
|
||||
var settingsObj = await Storage.get({ key: 'userSettings' }) || {}
|
||||
return settingsObj.value ? JSON.parse(settingsObj.value) : null
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get user settings', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async setCurrent(current) {
|
||||
try {
|
||||
if (current) {
|
||||
await Storage.set({ key: 'current', value: JSON.stringify(current) })
|
||||
} else {
|
||||
await Storage.remove({ key: 'current' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to set current', error)
|
||||
}
|
||||
}
|
||||
|
||||
async getCurrent() {
|
||||
try {
|
||||
var currentObj = await Storage.get({ key: 'current' }) || {}
|
||||
return currentObj.value ? JSON.parse(currentObj.value) : null
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get current', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ app, store }, inject) => {
|
||||
inject('sqlStore', new StoreService())
|
||||
inject('localStore', new LocalStorage(store))
|
||||
}
|
BIN
static/GentiumBookBasic.woff2
Normal file
BIN
static/GentiumBookBasic.woff2
Normal file
Binary file not shown.
BIN
static/material-icons.woff2
Normal file
BIN
static/material-icons.woff2
Normal file
Binary file not shown.
|
@ -12,6 +12,9 @@ export const state = () => ({
|
|||
})
|
||||
|
||||
export const getters = {
|
||||
getAudiobook: state => id => {
|
||||
return state.audiobooks.find(ab => ab.id === id)
|
||||
},
|
||||
getFiltered: (state, getters, rootState) => () => {
|
||||
var filtered = state.audiobooks
|
||||
var settings = rootState.user.settings || {}
|
||||
|
@ -51,7 +54,7 @@ export const getters = {
|
|||
|
||||
export const actions = {
|
||||
load({ commit }) {
|
||||
this.$axios
|
||||
return this.$axios
|
||||
.$get(`/api/audiobooks`)
|
||||
.then((data) => {
|
||||
console.log('Audiobooks request data', data)
|
||||
|
@ -59,12 +62,17 @@ export const actions = {
|
|||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
commit('set', [])
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
reset(state) {
|
||||
state.audiobooks = []
|
||||
state.genres = [...STANDARD_GENRES]
|
||||
state.tags = []
|
||||
state.series = []
|
||||
},
|
||||
set(state, audiobooks) {
|
||||
// GENRES
|
||||
var genres = [...state.genres]
|
||||
|
@ -92,7 +100,15 @@ export const mutations = {
|
|||
state.series = series
|
||||
state.series.sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
|
||||
|
||||
state.audiobooks = audiobooks
|
||||
audiobooks.forEach((ab) => {
|
||||
var indexOf = state.audiobooks.findIndex(_ab => _ab.id === ab.id)
|
||||
if (indexOf >= 0) {
|
||||
state.audiobooks.splice(indexOf, 1, ab)
|
||||
} else {
|
||||
state.audiobooks.push(ab)
|
||||
}
|
||||
})
|
||||
// state.audiobooks = audiobooks
|
||||
state.listeners.forEach((listener) => {
|
||||
listener.meth()
|
||||
})
|
||||
|
|
65
store/downloads.js
Normal file
65
store/downloads.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
export const state = () => ({
|
||||
downloads: [],
|
||||
orphanDownloads: [],
|
||||
showModal: false
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
getDownload: (state) => id => {
|
||||
return state.downloads.find(d => d.id === id)
|
||||
},
|
||||
getDownloadIfReady: (state) => id => {
|
||||
var download = state.downloads.find(d => d.id === id)
|
||||
return !!download && !download.isDownloading && !download.isPreparing ? download : null
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
async loadFromStorage({ commit }) {
|
||||
var downloads = await this.$sqlStore.getAllDownloads()
|
||||
|
||||
downloads.forEach(ab => {
|
||||
if (ab.isDownloading || ab.isPreparing) {
|
||||
ab.isIncomplete = true
|
||||
}
|
||||
ab.isDownloading = false
|
||||
ab.isPreparing = false
|
||||
commit('setDownload', ab)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
setShowModal(state, val) {
|
||||
state.showModal = val
|
||||
},
|
||||
setDownload(state, download) {
|
||||
var index = state.downloads.findIndex(d => d.id === download.id)
|
||||
if (index >= 0) {
|
||||
state.downloads.splice(index, 1, download)
|
||||
} else {
|
||||
state.downloads.push(download)
|
||||
}
|
||||
},
|
||||
addUpdateDownload(state, download) {
|
||||
var key = download.isOrphan ? 'orphanDownloads' : 'downloads'
|
||||
var index = state[key].findIndex(d => d.id === download.id)
|
||||
if (index >= 0) {
|
||||
state[key].splice(index, 1, download)
|
||||
} else {
|
||||
state[key].push(download)
|
||||
}
|
||||
|
||||
if (key === 'downloads') {
|
||||
this.$sqlStore.setDownload(download)
|
||||
}
|
||||
},
|
||||
removeDownload(state, download) {
|
||||
var key = download.isOrphan ? 'orphanDownloads' : 'downloads'
|
||||
state[key] = state[key].filter(d => d.id !== download.id)
|
||||
|
||||
if (key === 'downloads') {
|
||||
this.$sqlStore.removeDownload(download.id)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,29 @@
|
|||
import Vue from 'vue'
|
||||
|
||||
export const state = () => ({
|
||||
streamAudiobook: null,
|
||||
playingDownload: null,
|
||||
playOnLoad: false,
|
||||
serverUrl: null,
|
||||
user: null,
|
||||
appUpdateInfo: null
|
||||
appUpdateInfo: null,
|
||||
socketConnected: false,
|
||||
networkConnected: false,
|
||||
networkConnectionType: 'unknown',
|
||||
streamListener: null
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
playerIsOpen: (state) => {
|
||||
return state.streamAudiobook || state.playingDownload
|
||||
},
|
||||
isAudiobookStreaming: (state) => id => {
|
||||
return (state.streamAudiobook && state.streamAudiobook.id === id)
|
||||
},
|
||||
isAudiobookPlaying: (state) => id => {
|
||||
return (state.playingDownload && state.playingDownload.id === id) || (state.streamAudiobook && state.streamAudiobook.id === id)
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {}
|
||||
|
||||
export const mutations = {
|
||||
|
@ -23,12 +40,37 @@ export const mutations = {
|
|||
state.playOnLoad = val
|
||||
},
|
||||
setStreamAudiobook(state, audiobook) {
|
||||
state.streamAudiobook = audiobook
|
||||
if (audiobook) {
|
||||
state.playingDownload = null
|
||||
}
|
||||
Vue.set(state, 'streamAudiobook', audiobook)
|
||||
if (state.streamListener) {
|
||||
state.streamListener('stream', audiobook)
|
||||
}
|
||||
},
|
||||
setPlayingDownload(state, download) {
|
||||
if (download) {
|
||||
state.streamAudiobook = null
|
||||
}
|
||||
Vue.set(state, 'playingDownload', download)
|
||||
if (state.streamListener) {
|
||||
state.streamListener('download', download)
|
||||
}
|
||||
},
|
||||
setServerUrl(state, url) {
|
||||
state.serverUrl = url
|
||||
},
|
||||
setUser(state, user) {
|
||||
state.user = user
|
||||
setSocketConnected(state, val) {
|
||||
state.socketConnected = val
|
||||
},
|
||||
setNetworkStatus(state, val) {
|
||||
state.networkConnected = val.connected
|
||||
state.networkConnectionType = val.connectionType
|
||||
},
|
||||
setStreamListener(state, val) {
|
||||
state.streamListener = val
|
||||
},
|
||||
removeStreamListener(state) {
|
||||
state.streamListener = null
|
||||
}
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
import { Storage } from '@capacitor/storage'
|
||||
import Vue from 'vue'
|
||||
|
||||
export const state = () => ({
|
||||
user: null,
|
||||
localUserAudiobooks: {},
|
||||
settings: {
|
||||
orderBy: 'book.title',
|
||||
orderDesc: false,
|
||||
|
@ -28,33 +31,42 @@ export const getters = {
|
|||
}
|
||||
|
||||
export const actions = {
|
||||
updateUserSettings({ commit }, payload) {
|
||||
var updatePayload = {
|
||||
...payload
|
||||
}
|
||||
return this.$axios.$patch('/api/user/settings', updatePayload).then((result) => {
|
||||
if (result.success) {
|
||||
commit('setSettings', result.settings)
|
||||
console.log('Settings updated', result.settings)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
async updateUserSettings({ commit }, payload) {
|
||||
|
||||
if (Vue.prototype.$server.connected) {
|
||||
var updatePayload = {
|
||||
...payload
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('Failed to update settings', error)
|
||||
return false
|
||||
})
|
||||
return this.$axios.$patch('/api/user/settings', updatePayload).then((result) => {
|
||||
if (result.success) {
|
||||
commit('setSettings', result.settings)
|
||||
console.log('Settings updated', result.settings)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('Failed to update settings', error)
|
||||
return false
|
||||
})
|
||||
} else {
|
||||
console.log('Update settings without server')
|
||||
commit('setSettings', payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
setLocalUserAudiobooks(state, userAudiobooks) {
|
||||
state.localUserAudiobooks = userAudiobooks
|
||||
},
|
||||
setUser(state, user) {
|
||||
state.user = user
|
||||
if (user) {
|
||||
if (user.token) localStorage.setItem('token', user.token)
|
||||
if (user.token) this.$localStore.setToken(user.token)
|
||||
console.log('setUser', user.username)
|
||||
} else {
|
||||
localStorage.removeItem('token')
|
||||
this.$localStore.setToken(null)
|
||||
console.warn('setUser cleared')
|
||||
}
|
||||
},
|
||||
|
@ -69,6 +81,9 @@ export const mutations = {
|
|||
}
|
||||
}
|
||||
if (hasChanges) {
|
||||
console.log('Update settings in local storage')
|
||||
this.$localStore.setUserSettings({ ...state.settings })
|
||||
|
||||
state.settingsListeners.forEach((listener) => {
|
||||
listener.meth(state.settings)
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue