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 { io } from 'socket.io-client'
|
||||||
|
import { Storage } from '@capacitor/storage'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import EventEmitter from 'events'
|
import EventEmitter from 'events'
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@ class Server extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
getServerUrl(url) {
|
getServerUrl(url) {
|
||||||
|
if (!url) return null
|
||||||
var urlObject = new URL(url)
|
var urlObject = new URL(url)
|
||||||
return `${urlObject.protocol}//${urlObject.hostname}:${urlObject.port}`
|
return `${urlObject.protocol}//${urlObject.hostname}:${urlObject.port}`
|
||||||
}
|
}
|
||||||
|
@ -35,23 +37,34 @@ class Server extends EventEmitter {
|
||||||
this.store.commit('user/setUser', user)
|
this.store.commit('user/setUser', user)
|
||||||
if (user) {
|
if (user) {
|
||||||
this.store.commit('user/setSettings', user.settings)
|
this.store.commit('user/setSettings', user.settings)
|
||||||
localStorage.setItem('userToken', user.token)
|
Storage.set({ key: 'token', value: user.token })
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('userToken')
|
Storage.remove({ key: 'token' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setServerUrl(url) {
|
setServerUrl(url) {
|
||||||
this.url = url
|
this.url = url
|
||||||
localStorage.setItem('serverUrl', url)
|
|
||||||
this.store.commit('setServerUrl', url)
|
this.store.commit('setServerUrl', url)
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
Storage.set({ key: 'serverUrl', value: url })
|
||||||
|
} else {
|
||||||
|
Storage.remove({ key: 'serverUrl' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(url, token) {
|
async connect(url, token) {
|
||||||
|
if (!url) {
|
||||||
|
console.error('Invalid url to connect')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
var serverUrl = this.getServerUrl(url)
|
var serverUrl = this.getServerUrl(url)
|
||||||
var res = await this.ping(serverUrl)
|
var res = await this.ping(serverUrl)
|
||||||
|
|
||||||
if (!res || !res.success) {
|
if (!res || !res.success) {
|
||||||
this.url = null
|
this.setServerUrl(null)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
var authRes = await this.authorize(serverUrl, token)
|
var authRes = await this.authorize(serverUrl, token)
|
||||||
|
@ -60,7 +73,7 @@ class Server extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setServerUrl(serverUrl)
|
this.setServerUrl(serverUrl)
|
||||||
console.warn('Connect setting auth user', authRes)
|
|
||||||
this.setUser(authRes.user)
|
this.setUser(authRes.user)
|
||||||
this.connectSocket()
|
this.connectSocket()
|
||||||
|
|
||||||
|
@ -103,6 +116,9 @@ class Server extends EventEmitter {
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
this.setUser(null)
|
this.setUser(null)
|
||||||
|
if (this.socket) {
|
||||||
|
this.socket.disconnect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authorize(serverUrl, token) {
|
authorize(serverUrl, token) {
|
||||||
|
@ -138,9 +154,13 @@ class Server extends EventEmitter {
|
||||||
|
|
||||||
this.connected = true
|
this.connected = true
|
||||||
this.emit('connected', true)
|
this.emit('connected', true)
|
||||||
|
this.store.commit('setSocketConnected', true)
|
||||||
})
|
})
|
||||||
this.socket.on('disconnect', () => {
|
this.socket.on('disconnect', () => {
|
||||||
console.log('[Server] Socket Disconnected')
|
console.log('[Server] Socket Disconnected')
|
||||||
|
this.connected = false
|
||||||
|
this.emit('connected', false)
|
||||||
|
this.store.commit('setSocketConnected', false)
|
||||||
})
|
})
|
||||||
this.socket.on('init', (data) => {
|
this.socket.on('init', (data) => {
|
||||||
console.log('[Server] Initial socket data received', data)
|
console.log('[Server] Initial socket data received', data)
|
||||||
|
|
|
@ -10,8 +10,8 @@ android {
|
||||||
applicationId "com.audiobookshelf.app"
|
applicationId "com.audiobookshelf.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 8
|
versionCode 9
|
||||||
versionName "0.4.0-beta"
|
versionName "0.8.0-beta"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|
|
@ -9,8 +9,12 @@ android {
|
||||||
|
|
||||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation project(':capacitor-community-sqlite')
|
||||||
implementation project(':capacitor-dialog')
|
implementation project(':capacitor-dialog')
|
||||||
|
implementation project(':capacitor-network')
|
||||||
|
implementation project(':capacitor-storage')
|
||||||
implementation project(':robingenz-capacitor-app-update')
|
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",
|
"pkg": "@capacitor/dialog",
|
||||||
"classpath": "com.capacitorjs.plugins.dialog.DialogPlugin"
|
"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",
|
"pkg": "@robingenz/capacitor-app-update",
|
||||||
"classpath": "dev.robingenz.capacitor.appupdate.AppUpdatePlugin"
|
"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 playbackSpeed:Float = 1f
|
||||||
var duration:Long = 0
|
var duration:Long = 0
|
||||||
|
|
||||||
|
var isLocal:Boolean = false
|
||||||
|
var contentUrl:String = ""
|
||||||
|
|
||||||
var hasPlayerLoaded:Boolean = false
|
var hasPlayerLoaded:Boolean = false
|
||||||
|
|
||||||
val playlistUri:Uri
|
var playlistUri:Uri = Uri.EMPTY
|
||||||
val coverUri:Uri
|
var coverUri:Uri = Uri.EMPTY
|
||||||
|
var contentUri:Uri = Uri.EMPTY // For Local only
|
||||||
|
|
||||||
constructor(jsondata:JSObject) {
|
constructor(jsondata:JSObject) {
|
||||||
id = jsondata.getString("id", "audiobook").toString()
|
id = jsondata.getString("id", "audiobook").toString()
|
||||||
|
@ -34,7 +38,22 @@ class Audiobook {
|
||||||
playbackSpeed = jsondata.getDouble("playbackSpeed")!!.toFloat()
|
playbackSpeed = jsondata.getDouble("playbackSpeed")!!.toFloat()
|
||||||
duration = jsondata.getString("duration", "0")!!.toLong()
|
duration = jsondata.getString("duration", "0")!!.toLong()
|
||||||
|
|
||||||
|
// Local data
|
||||||
|
isLocal = jsondata.getBoolean("isLocal", false) == true
|
||||||
|
contentUrl = jsondata.getString("contentUrl", "").toString()
|
||||||
|
|
||||||
|
if (playlistUrl != "") {
|
||||||
playlistUri = Uri.parse(playlistUrl)
|
playlistUri = Uri.parse(playlistUrl)
|
||||||
|
}
|
||||||
|
if (cover != "") {
|
||||||
coverUri = Uri.parse(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
|
package com.audiobookshelf.app
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.app.DownloadManager
|
||||||
import android.content.Context
|
import android.content.*
|
||||||
import android.content.Intent
|
|
||||||
import android.content.ServiceConnection
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.example.myapp.MyNativeAudio
|
import android.widget.Toast
|
||||||
import com.getcapacitor.BridgeActivity
|
import com.getcapacitor.BridgeActivity
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : BridgeActivity() {
|
class MainActivity : BridgeActivity() {
|
||||||
private val tag = "MainActivity"
|
private val tag = "MainActivity"
|
||||||
|
|
||||||
|
@ -18,11 +19,45 @@ class MainActivity : BridgeActivity() {
|
||||||
private lateinit var mConnection : ServiceConnection
|
private lateinit var mConnection : ServiceConnection
|
||||||
|
|
||||||
lateinit var pluginCallback : () -> Unit
|
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?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Log.d(tag, "onCreate")
|
Log.d(tag, "onCreate")
|
||||||
registerPlugin(MyNativeAudio::class.java)
|
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?) {
|
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -62,4 +97,8 @@ class MainActivity : BridgeActivity() {
|
||||||
val stopIntent = Intent(this, PlayerNotificationService::class.java)
|
val stopIntent = Intent(this, PlayerNotificationService::class.java)
|
||||||
stopService(stopIntent)
|
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.content.Intent
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.ContextCompat
|
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.JSObject
|
||||||
import com.getcapacitor.Plugin
|
import com.getcapacitor.Plugin
|
||||||
import com.getcapacitor.PluginCall
|
import com.getcapacitor.PluginCall
|
||||||
|
@ -56,12 +53,20 @@ class MyNativeAudio : Plugin() {
|
||||||
} else {
|
} else {
|
||||||
Log.w(tag, "Service already started --")
|
Log.w(tag, "Service already started --")
|
||||||
}
|
}
|
||||||
|
var jsobj = JSObject()
|
||||||
|
|
||||||
var audiobook:Audiobook = Audiobook(call.data)
|
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() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.initPlayer(audiobook)
|
playerNotificationService.initPlayer(audiobook)
|
||||||
call.resolve()
|
jsobj.put("success", true)
|
||||||
|
call.resolve(jsobj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,6 +114,7 @@ class MyNativeAudio : Plugin() {
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun seekBackward(call: PluginCall) {
|
fun seekBackward(call: PluginCall) {
|
||||||
var amount:Long = call.getString("amount", "0")!!.toLong()
|
var amount:Long = call.getString("amount", "0")!!.toLong()
|
||||||
|
@ -117,6 +123,7 @@ class MyNativeAudio : Plugin() {
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun setPlaybackSpeed(call: PluginCall) {
|
fun setPlaybackSpeed(call: PluginCall) {
|
||||||
var playbackSpeed:Float = call.getFloat("speed", 1.0f)!!
|
var playbackSpeed:Float = call.getFloat("speed", 1.0f)!!
|
||||||
|
@ -126,6 +133,7 @@ class MyNativeAudio : Plugin() {
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun terminateStream(call: PluginCall) {
|
fun terminateStream(call: PluginCall) {
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import android.net.Uri
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.provider.MediaStore
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.support.v4.media.session.MediaControllerCompat
|
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.audio.AudioAttributes
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
|
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.source.hls.HlsMediaSource
|
||||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
||||||
import com.google.android.exoplayer2.upstream.*
|
import com.google.android.exoplayer2.upstream.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
|
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
|
||||||
|
@ -217,13 +223,13 @@ class PlayerNotificationService : Service() {
|
||||||
mediaSessionConnector = MediaSessionConnector(mediaSession)
|
mediaSessionConnector = MediaSessionConnector(mediaSession)
|
||||||
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
|
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
|
||||||
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
||||||
return MediaDescriptionCompat.Builder()
|
var builder = MediaDescriptionCompat.Builder()
|
||||||
.setMediaId(currentAudiobook!!.id)
|
.setMediaId(currentAudiobook!!.id)
|
||||||
.setTitle(currentAudiobook!!.title)
|
.setTitle(currentAudiobook!!.title)
|
||||||
.setSubtitle(currentAudiobook!!.author)
|
.setSubtitle(currentAudiobook!!.author)
|
||||||
.setMediaUri(currentAudiobook!!.playlistUri)
|
.setMediaUri(currentAudiobook!!.playlistUri)
|
||||||
.setIconUri(currentAudiobook!!.coverUri)
|
.setIconUri(currentAudiobook!!.coverUri)
|
||||||
.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mediaSessionConnector.setQueueNavigator(queueNavigator)
|
mediaSessionConnector.setQueueNavigator(queueNavigator)
|
||||||
|
@ -280,6 +286,9 @@ class PlayerNotificationService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private fun setPlayerListeners() {
|
private fun setPlayerListeners() {
|
||||||
mPlayer.addListener(object : Player.Listener {
|
mPlayer.addListener(object : Player.Listener {
|
||||||
override fun onPlayerError(error: PlaybackException) {
|
override fun onPlayerError(error: PlaybackException) {
|
||||||
|
@ -351,28 +360,42 @@ class PlayerNotificationService : Service() {
|
||||||
Log.d(tag, "Init Player audiobook already playing")
|
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_TITLE, currentAudiobook!!.title)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, currentAudiobook!!.title)
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, currentAudiobook!!.title)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, currentAudiobook!!.author)
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, currentAudiobook!!.author)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, currentAudiobook!!.author)
|
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, currentAudiobook!!.author)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, currentAudiobook!!.author)
|
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, currentAudiobook!!.author)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, currentAudiobook!!.series)
|
.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)
|
.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)
|
mediaSession.setMetadata(metadata)
|
||||||
|
|
||||||
var mediaMetadata = MediaMetadata.Builder().build()
|
var mediaMetadata = MediaMetadata.Builder().build()
|
||||||
var mediaItem = MediaItem.Builder().setUri(currentAudiobook!!.playlistUri).setMediaMetadata(mediaMetadata).build()
|
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
var dataSourceFactory = DefaultHttpDataSource.Factory()
|
||||||
dataSourceFactory.setUserAgent(channelId)
|
dataSourceFactory.setUserAgent(channelId)
|
||||||
dataSourceFactory.setDefaultRequestProperties(hashMapOf("Authorization" to "Bearer ${currentAudiobook!!.token}"))
|
dataSourceFactory.setDefaultRequestProperties(hashMapOf("Authorization" to "Bearer ${currentAudiobook!!.token}"))
|
||||||
|
|
||||||
var mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
|
mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
mPlayer.setMediaSource(mediaSource, true)
|
mPlayer.setMediaSource(mediaSource, true)
|
||||||
mPlayer.prepare()
|
mPlayer.prepare()
|
||||||
|
|
|
@ -2,8 +2,20 @@
|
||||||
include ':capacitor-android'
|
include ':capacitor-android'
|
||||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
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'
|
include ':capacitor-dialog'
|
||||||
project(':capacitor-dialog').projectDir = new File('../node_modules/@capacitor/dialog/android')
|
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'
|
include ':robingenz-capacitor-app-update'
|
||||||
project(':robingenz-capacitor-app-update').projectDir = new File('../node_modules/@robingenz/capacitor-app-update/android')
|
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-md {
|
||||||
box-shadow: 2px 8px 6px #111111aa;
|
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.isResetting = false
|
||||||
this.initObject = { ...audiobookStreamData }
|
this.initObject = { ...audiobookStreamData }
|
||||||
this.currentPlaybackRate = this.initObject.playbackSpeed
|
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() {
|
setFromObj() {
|
||||||
if (!this.initObject) {
|
if (!this.initObject) {
|
||||||
|
@ -210,7 +220,17 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.isResetting = false
|
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() {
|
play() {
|
||||||
MyNativeAudio.playPlayer()
|
MyNativeAudio.playPlayer()
|
||||||
|
|
|
@ -13,14 +13,18 @@
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<!-- <ui-menu :label="username" :items="menuItems" @action="menuAction" class="ml-5" /> -->
|
<!-- <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="flex items-center">
|
||||||
<span class="block truncate">{{ username }}</span>
|
<span class="block truncate">{{ username }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
<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 class="material-icons text-gray-100">person</span>
|
||||||
</span>
|
</span>
|
||||||
</nuxt-link>
|
</nuxt-link> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -89,4 +93,47 @@ export default {
|
||||||
#appbar {
|
#appbar {
|
||||||
box-shadow: 0px 5px 5px #11111155;
|
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>
|
</style>
|
|
@ -4,7 +4,7 @@
|
||||||
<div :key="index" class="border-b border-opacity-10 w-full bookshelfRow py-4 flex justify-around relative">
|
<div :key="index" class="border-b border-opacity-10 w-full bookshelfRow py-4 flex justify-around relative">
|
||||||
<template v-for="audiobook in shelf">
|
<template v-for="audiobook in shelf">
|
||||||
<!-- <div :key="audiobook.id" class="relative px-4"> -->
|
<!-- <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}`">
|
<!-- <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" />
|
<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> -->
|
</nuxt-link> -->
|
||||||
|
@ -44,6 +44,9 @@ export default {
|
||||||
},
|
},
|
||||||
userAudiobooks() {
|
userAudiobooks() {
|
||||||
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
||||||
|
},
|
||||||
|
localUserAudiobooks() {
|
||||||
|
return this.$store.state.user.localUserAudiobooks || {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -52,11 +55,6 @@ export default {
|
||||||
filterBy: 'all'
|
filterBy: 'all'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
playAudiobook(audiobook) {
|
|
||||||
console.log('Play Audiobook', audiobook)
|
|
||||||
this.$store.commit('setStreamAudiobook', audiobook)
|
|
||||||
this.$server.socket.emit('open_stream', audiobook.id)
|
|
||||||
},
|
|
||||||
calcShelves() {
|
calcShelves() {
|
||||||
var booksPerShelf = Math.floor(this.pageWidth / (this.cardWidth + 32))
|
var booksPerShelf = Math.floor(this.pageWidth / (this.cardWidth + 32))
|
||||||
var groupedBooks = []
|
var groupedBooks = []
|
||||||
|
@ -87,6 +85,16 @@ export default {
|
||||||
if (this.currFilterOrderKey !== this.filterOrderKey) {
|
if (this.currFilterOrderKey !== this.filterOrderKey) {
|
||||||
this.calcShelves()
|
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() {
|
mounted() {
|
||||||
|
@ -94,7 +102,13 @@ export default {
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
|
this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
|
||||||
|
|
||||||
window.addEventListener('resize', this.resize)
|
window.addEventListener('resize', this.resize)
|
||||||
|
|
||||||
|
this.$server.on('connected', this.socketConnected)
|
||||||
|
if (this.$server.connected) {
|
||||||
this.$store.dispatch('audiobooks/load')
|
this.$store.dispatch('audiobooks/load')
|
||||||
|
} else {
|
||||||
|
console.log('Bookshelf - Server not connected using downloaded')
|
||||||
|
}
|
||||||
this.init()
|
this.init()
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full p-4 pointer-events-none fixed bottom-0 left-0 right-0 z-20">
|
<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 class="pl-16 pr-2 flex items-center pb-2">
|
||||||
<div>
|
<div>
|
||||||
<p class="px-2">{{ title }}</p>
|
<p class="px-2">{{ title }}</p>
|
||||||
<p class="px-2 text-xs text-gray-400">by {{ author }}</p>
|
<p class="px-2 text-xs text-gray-400">by {{ author }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<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>
|
<span class="material-icons text-2xl">format_list_bulleted</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="material-icons" @click="cancelStream">close</span>
|
<span class="material-icons" @click="cancelStream">close</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute left-2 -top-10">
|
<div class="absolute left-2 -top-10">
|
||||||
<cards-book-cover :audiobook="streamAudiobook" :width="64" />
|
<cards-book-cover :audiobook="audiobook" :width="64" />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-speed.sync="playbackSpeed" @change="changePlaybackSpeed" />
|
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-speed.sync="playbackSpeed" @change="changePlaybackSpeed" />
|
||||||
<modals-chapters-modal v-model="showChapterModal" :chapters="chapters" @select="selectChapter" />
|
<modals-chapters-modal v-model="showChapterModal" :chapters="chapters" @select="selectChapter" />
|
||||||
|
@ -30,24 +30,62 @@ export default {
|
||||||
return {
|
return {
|
||||||
audioPlayerReady: false,
|
audioPlayerReady: false,
|
||||||
stream: null,
|
stream: null,
|
||||||
lastServerUpdateSentSeconds: 0,
|
download: null,
|
||||||
|
lastProgressTimeUpdate: 0,
|
||||||
showPlaybackSpeedModal: false,
|
showPlaybackSpeedModal: false,
|
||||||
playbackSpeed: 1,
|
playbackSpeed: 1,
|
||||||
showChapterModal: false
|
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: {
|
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() {
|
streamAudiobook() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamAudiobook
|
||||||
},
|
},
|
||||||
streamAudiobookId() {
|
|
||||||
return this.streamAudiobook ? this.streamAudiobook.id : null
|
|
||||||
},
|
|
||||||
currStreamAudiobookId() {
|
|
||||||
return this.stream ? this.stream.audiobook.id : null
|
|
||||||
},
|
|
||||||
book() {
|
book() {
|
||||||
return this.streamAudiobook ? this.streamAudiobook.book || {} : {}
|
return this.audiobook ? this.audiobook.book || {} : {}
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.book ? this.book.title : ''
|
return this.book ? this.book.title : ''
|
||||||
|
@ -62,7 +100,7 @@ export default {
|
||||||
return this.book ? this.book.series : ''
|
return this.book ? this.book.series : ''
|
||||||
},
|
},
|
||||||
chapters() {
|
chapters() {
|
||||||
return this.streamAudiobook ? this.streamAudiobook.chapters || [] : []
|
return this.audiobook ? this.audiobook.chapters || [] : []
|
||||||
},
|
},
|
||||||
volumeNumber() {
|
volumeNumber() {
|
||||||
return this.book ? this.book.volumeNumber : ''
|
return this.book ? this.book.volumeNumber : ''
|
||||||
|
@ -73,7 +111,7 @@ export default {
|
||||||
return `${this.series} #${this.volumeNumber}`
|
return `${this.series} #${this.volumeNumber}`
|
||||||
},
|
},
|
||||||
duration() {
|
duration() {
|
||||||
return this.streamAudiobook ? this.streamAudiobook.duration || 0 : 0
|
return this.audiobook ? this.audiobook.duration || 0 : 0
|
||||||
},
|
},
|
||||||
coverForNative() {
|
coverForNative() {
|
||||||
if (!this.cover) {
|
if (!this.cover) {
|
||||||
|
@ -100,6 +138,14 @@ export default {
|
||||||
this.showChapterModal = false
|
this.showChapterModal = false
|
||||||
},
|
},
|
||||||
async cancelStream() {
|
async cancelStream() {
|
||||||
|
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({
|
const { value } = await Dialog.confirm({
|
||||||
title: 'Confirm',
|
title: 'Confirm',
|
||||||
message: 'Cancel this stream?'
|
message: 'Cancel this stream?'
|
||||||
|
@ -111,16 +157,42 @@ export default {
|
||||||
this.$refs.audioPlayerMini.terminateStream()
|
this.$refs.audioPlayerMini.terminateStream()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
updateTime(currentTime) {
|
updateTime(currentTime) {
|
||||||
var diff = currentTime - this.lastServerUpdateSentSeconds
|
var diff = currentTime - this.lastProgressTimeUpdate
|
||||||
|
|
||||||
if (diff > 4 || diff < 0) {
|
if (diff > 4 || diff < 0) {
|
||||||
this.lastServerUpdateSentSeconds = currentTime
|
this.lastProgressTimeUpdate = currentTime
|
||||||
|
if (this.stream) {
|
||||||
var updatePayload = {
|
var updatePayload = {
|
||||||
currentTime,
|
currentTime,
|
||||||
streamId: this.stream.id
|
streamId: this.stream.id
|
||||||
}
|
}
|
||||||
this.$server.socket.emit('stream_update', updatePayload)
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
closeStream() {},
|
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) {
|
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) {
|
if (!this.$refs.audioPlayerMini) {
|
||||||
console.error('No Audio Player Mini')
|
console.error('No Audio Player Mini')
|
||||||
return
|
return
|
||||||
|
@ -160,6 +291,9 @@ export default {
|
||||||
|
|
||||||
this.stream = stream
|
this.stream = stream
|
||||||
|
|
||||||
|
// Update local remove current
|
||||||
|
this.$localStore.setCurrent(null)
|
||||||
|
|
||||||
var playlistUrl = stream.clientPlaylistUri
|
var playlistUrl = stream.clientPlaylistUri
|
||||||
var currentTime = stream.clientCurrentTime || 0
|
var currentTime = stream.clientCurrentTime || 0
|
||||||
var playOnLoad = this.$store.state.playOnLoad
|
var playOnLoad = this.$store.state.playOnLoad
|
||||||
|
@ -177,13 +311,17 @@ export default {
|
||||||
playlistUrl: this.$server.url + playlistUrl,
|
playlistUrl: this.$server.url + playlistUrl,
|
||||||
token: this.$store.getters['user/getToken']
|
token: this.$store.getters['user/getToken']
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$refs.audioPlayerMini.set(audiobookStreamData)
|
this.$refs.audioPlayerMini.set(audiobookStreamData)
|
||||||
},
|
},
|
||||||
audioPlayerMounted() {
|
audioPlayerMounted() {
|
||||||
console.log('Audio Player Mounted', this.$server.stream)
|
console.log('Audio Player Mounted', this.$server.stream)
|
||||||
this.audioPlayerReady = true
|
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)
|
this.streamOpen(this.$server.stream)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -206,6 +344,26 @@ export default {
|
||||||
this.playbackSpeed = settings.playbackRate
|
this.playbackSpeed = settings.playbackRate
|
||||||
this.$refs.audioPlayerMini.updatePlaybackRate()
|
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() {
|
mounted() {
|
||||||
|
@ -213,15 +371,19 @@ export default {
|
||||||
|
|
||||||
this.setListeners()
|
this.setListeners()
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
|
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
|
||||||
|
this.$store.commit('setStreamListener', this.streamUpdated)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
if (this.$server.socket) {
|
||||||
this.$server.socket.off('stream_open', this.streamOpen)
|
this.$server.socket.off('stream_open', this.streamOpen)
|
||||||
this.$server.socket.off('stream_closed', this.streamClosed)
|
this.$server.socket.off('stream_closed', this.streamClosed)
|
||||||
this.$server.socket.off('stream_progress', this.streamProgress)
|
this.$server.socket.off('stream_progress', this.streamProgress)
|
||||||
this.$server.socket.off('stream_ready', this.streamReady)
|
this.$server.socket.off('stream_ready', this.streamReady)
|
||||||
this.$server.socket.off('stream_reset', this.streamReset)
|
this.$server.socket.off('stream_reset', this.streamReset)
|
||||||
|
}
|
||||||
|
|
||||||
this.$store.commit('user/removeSettingsListener', 'streamContainer')
|
this.$store.commit('user/removeSettingsListener', 'streamContainer')
|
||||||
|
this.$store.commit('removeStreamListener')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -11,7 +11,11 @@
|
||||||
<div class="rounded-sm h-full overflow-hidden relative box-shadow-book">
|
<div class="rounded-sm h-full overflow-hidden relative box-shadow-book">
|
||||||
<nuxt-link :to="`/audiobook/${audiobookId}`" class="cursor-pointer">
|
<nuxt-link :to="`/audiobook/${audiobookId}`" class="cursor-pointer">
|
||||||
<div class="w-full relative" :style="{ height: height + 'px' }">
|
<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>
|
<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,
|
type: Object,
|
||||||
default: () => null
|
default: () => null
|
||||||
},
|
},
|
||||||
|
localUserProgress: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
},
|
||||||
width: {
|
width: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 140
|
default: 140
|
||||||
|
@ -81,8 +89,13 @@ export default {
|
||||||
orderBy() {
|
orderBy() {
|
||||||
return this.$store.getters['user/getUserSetting']('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() {
|
userProgressPercent() {
|
||||||
return this.userProgress ? this.userProgress.progress || 0 : 0
|
return this.mostRecentUserProgress ? this.mostRecentUserProgress.progress || 0 : 0
|
||||||
},
|
},
|
||||||
showError() {
|
showError() {
|
||||||
return this.hasMissingParts || this.hasInvalidParts
|
return this.hasMissingParts || this.hasInvalidParts
|
||||||
|
@ -93,6 +106,12 @@ export default {
|
||||||
hasInvalidParts() {
|
hasInvalidParts() {
|
||||||
return this.audiobook.hasInvalidParts
|
return this.audiobook.hasInvalidParts
|
||||||
},
|
},
|
||||||
|
downloadCover() {
|
||||||
|
return this.download ? this.download.cover : null
|
||||||
|
},
|
||||||
|
download() {
|
||||||
|
return this.$store.getters['downloads/getDownloadIfReady'](this.audiobookId)
|
||||||
|
},
|
||||||
errorText() {
|
errorText() {
|
||||||
var txt = ''
|
var txt = ''
|
||||||
if (this.hasMissingParts) {
|
if (this.hasMissingParts) {
|
||||||
|
@ -105,12 +124,7 @@ export default {
|
||||||
return txt || 'Unknown Error'
|
return txt || 'Unknown Error'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {},
|
||||||
play() {
|
|
||||||
this.$store.commit('setStreamAudiobook', this.audiobook)
|
|
||||||
this.$root.socket.emit('open_stream', this.audiobookId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -32,6 +32,7 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
|
downloadCover: String,
|
||||||
authorOverride: String,
|
authorOverride: String,
|
||||||
width: {
|
width: {
|
||||||
type: Number,
|
type: Number,
|
||||||
|
@ -78,7 +79,13 @@ export default {
|
||||||
serverUrl() {
|
serverUrl() {
|
||||||
return this.$store.state.serverUrl
|
return this.$store.state.serverUrl
|
||||||
},
|
},
|
||||||
|
networkConnected() {
|
||||||
|
return this.$store.state.networkConnected
|
||||||
|
},
|
||||||
fullCoverUrl() {
|
fullCoverUrl() {
|
||||||
|
if (this.downloadCover) return this.downloadCover
|
||||||
|
else if (!this.networkConnected) return this.placeholderUrl
|
||||||
|
|
||||||
if (this.cover.startsWith('http')) return this.cover
|
if (this.cover.startsWith('http')) return this.cover
|
||||||
var _clean = this.cover.replace(/\\/g, '/')
|
var _clean = this.cover.replace(/\\/g, '/')
|
||||||
if (_clean.startsWith('/local')) {
|
if (_clean.startsWith('/local')) {
|
||||||
|
@ -91,6 +98,7 @@ export default {
|
||||||
return this.book.cover || this.placeholderUrl
|
return this.book.cover || this.placeholderUrl
|
||||||
},
|
},
|
||||||
hasCover() {
|
hasCover() {
|
||||||
|
if (!this.networkConnected && !this.downloadCover) return false
|
||||||
return !!this.book.cover
|
return !!this.book.cover
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
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>
|
<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 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">
|
<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>
|
<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>
|
<template>
|
||||||
<div class="w-full min-h-screen h-full bg-bg text-white">
|
<div class="w-full min-h-screen h-full bg-bg text-white">
|
||||||
<app-appbar />
|
<app-appbar />
|
||||||
<div id="content" class="overflow-hidden" :class="streaming ? 'streaming' : ''">
|
<div id="content" class="overflow-hidden" :class="playerIsOpen ? 'playerOpen' : ''">
|
||||||
<Nuxt />
|
<Nuxt />
|
||||||
</div>
|
</div>
|
||||||
<app-stream-container ref="streamContainer" />
|
<app-stream-container ref="streamContainer" />
|
||||||
|
<modals-downloads-modal ref="downloadsModal" @selectDownload="selectDownload" @deleteDownload="deleteDownload" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Path from 'path'
|
||||||
|
import { Capacitor } from '@capacitor/core'
|
||||||
|
import { Network } from '@capacitor/network'
|
||||||
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
||||||
|
import AudioDownloader from '@/plugins/audio-downloader'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
middleware: 'authenticated',
|
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
streaming() {
|
playerIsOpen() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.getters['playerIsOpen']
|
||||||
},
|
},
|
||||||
routeName() {
|
routeName() {
|
||||||
return this.$route.name
|
return this.$route.name
|
||||||
|
@ -30,10 +34,49 @@ export default {
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
this.$router.push('/')
|
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) {
|
initialStream(stream) {
|
||||||
|
@ -49,16 +92,16 @@ export default {
|
||||||
await AppUpdate.openAppStore()
|
await AppUpdate.openAppStore()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showUpdateToast(availableVersion, immediateUpdateAllowed) {
|
// showUpdateToast(availableVersion, immediateUpdateAllowed) {
|
||||||
var toastText = immediateUpdateAllowed ? `Click here to update` : `Click here to open app store`
|
// var toastText = immediateUpdateAllowed ? `Click here to update` : `Click here to open app store`
|
||||||
this.$toast.info(`Update is available for v${availableVersion}! ${toastText}`, {
|
// this.$toast.info(`Update is available for v${availableVersion}! ${toastText}`, {
|
||||||
draggable: false,
|
// draggable: false,
|
||||||
hideProgressBar: false,
|
// hideProgressBar: false,
|
||||||
timeout: 10000,
|
// timeout: 10000,
|
||||||
closeButton: false,
|
// closeButton: false,
|
||||||
onClick: this.clickUpdateToast()
|
// onClick: this.clickUpdateToast()
|
||||||
})
|
// })
|
||||||
},
|
// },
|
||||||
async checkForUpdate() {
|
async checkForUpdate() {
|
||||||
const result = await AppUpdate.getAppUpdateInfo()
|
const result = await AppUpdate.getAppUpdateInfo()
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
@ -66,13 +109,165 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$store.commit('setAppUpdateInfo', result)
|
this.$store.commit('setAppUpdateInfo', result)
|
||||||
|
if (result.updateAvailability === 2) {
|
||||||
// if (result.updateAvailability === 2) {
|
setTimeout(() => {
|
||||||
// setTimeout(() => {
|
this.$toast.info(`Update is available!`, {
|
||||||
|
draggable: false,
|
||||||
|
hideProgressBar: false,
|
||||||
|
timeout: 4000,
|
||||||
|
closeButton: false,
|
||||||
|
onClick: this.clickUpdateToast()
|
||||||
|
})
|
||||||
// this.showUpdateToast(result.availableVersion, !!result.immediateUpdateAllowed)
|
// this.showUpdateToast(result.availableVersion, !!result.immediateUpdateAllowed)
|
||||||
// }, 5000)
|
}, 5000)
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onDownloadProgress(data) {
|
||||||
|
// var downloadId = data.downloadId
|
||||||
|
var progress = data.progress
|
||||||
|
var filename = data.filename
|
||||||
|
var audiobookId = filename ? Path.basename(filename, Path.extname(filename)) : ''
|
||||||
|
|
||||||
|
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) {
|
// parseSemver(ver) {
|
||||||
// if (!ver) return null
|
// 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-]+)*))?)$/)
|
// 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)
|
// 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() {
|
mounted() {
|
||||||
if (!this.$server) return console.error('No Server')
|
if (!this.$server) return console.error('No Server')
|
||||||
|
@ -137,16 +342,9 @@ export default {
|
||||||
this.$server.on('connected', this.connected)
|
this.$server.on('connected', this.connected)
|
||||||
this.$server.on('initialStream', this.initialStream)
|
this.$server.on('initialStream', this.initialStream)
|
||||||
|
|
||||||
if (!this.$server.connected) {
|
this.setupNetworkListener()
|
||||||
this.$router.push('/connect')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.checkForUpdate()
|
this.checkForUpdate()
|
||||||
|
this.initMediaStore()
|
||||||
// var checkForUpdateFlag = localStorage.getItem('checkForUpdate')
|
|
||||||
// if (!checkForUpdateFlag || checkForUpdateFlag !== '1') {
|
|
||||||
// this.checkForUpdate()
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -155,7 +353,8 @@ export default {
|
||||||
#content {
|
#content {
|
||||||
height: calc(100vh - 64px);
|
height: calc(100vh - 64px);
|
||||||
}
|
}
|
||||||
#content.streaming {
|
#content.playerOpen {
|
||||||
height: calc(100vh - 204px);
|
/* height: calc(100vh - 204px); */
|
||||||
|
height: calc(100vh - 240px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -26,8 +26,7 @@ export default {
|
||||||
],
|
],
|
||||||
link: [
|
link: [
|
||||||
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
|
{ 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/css2?family=Ubuntu+Mono&family=Open+Sans:wght@400;600' },
|
||||||
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/icon?family=Material+Icons' }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -37,9 +36,11 @@ export default {
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
'@/plugins/server.js',
|
'@/plugins/server.js',
|
||||||
|
'@/plugins/store.js',
|
||||||
'@/plugins/init.client.js',
|
'@/plugins/init.client.js',
|
||||||
'@/plugins/axios.js',
|
'@/plugins/axios.js',
|
||||||
'@/plugins/my-native-audio.js',
|
'@/plugins/my-native-audio.js',
|
||||||
|
'@/plugins/audio-downloader.js',
|
||||||
'@/plugins/toast.js'
|
'@/plugins/toast.js'
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
479
package-lock.json
generated
479
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "v0.2.1-beta",
|
"version": "v0.4.0-beta",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1000,6 +1000,11 @@
|
||||||
"to-fast-properties": "^2.0.0"
|
"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": {
|
"@capacitor/android": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-3.2.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-3.2.2.tgz",
|
||||||
"integrity": "sha512-Eq17Y+UDHFmYGaZcObvxHAcHw0fF9TCBAg1f5f6qdV8ab3cKKEUB9xMvoCSZAueBfxFARrD18TsZJKoxh2YsLA=="
|
"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": {
|
"@csstools/convert-colors": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz",
|
||||||
"integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw=="
|
"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": {
|
"@ionic/cli-framework-output": {
|
||||||
"version": "2.2.2",
|
"version": "2.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/@robingenz/capacitor-app-update/-/capacitor-app-update-1.0.0.tgz",
|
||||||
"integrity": "sha512-pK8Yi7VgG/O/R4kJ3JtLpdeQgJzRIDPGM61bJhofTqu/+i26h8GhQdq4MB2OLJWk06Ht8MYDIIW/E0nxatrrnA=="
|
"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": {
|
"@types/component-emitter": {
|
||||||
"version": "1.2.10",
|
"version": "1.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
|
"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": {
|
"boxen": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/boxen/-/boxen-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/boxen/-/boxen-5.0.1.tgz",
|
||||||
|
@ -3875,6 +3950,40 @@
|
||||||
"schema-utils": "^2.0.0"
|
"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": {
|
"call-bind": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001249.tgz",
|
||||||
"integrity": "sha512-vcX4U8lwVXPdqzPWi6cAJ3FnQaqXbBqy/GZseKNQzRj37J7qZdGcBtxq/QLFNLLlfsoXLUdHw8Iwenri86Tagw=="
|
"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": {
|
"chalk": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
"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": {
|
"co": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||||
|
@ -4296,6 +4422,16 @@
|
||||||
"typedarray": "^0.0.6"
|
"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": {
|
"connect": {
|
||||||
"version": "3.7.0",
|
"version": "3.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
|
"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": {
|
"deep-equal": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
|
"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": {
|
"define-properties": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz",
|
||||||
"integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50="
|
"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": {
|
"detective": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
||||||
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
|
"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": {
|
"duplexify": {
|
||||||
"version": "3.7.1",
|
"version": "3.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
|
"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": {
|
"electron-to-chromium": {
|
||||||
"version": "1.3.796",
|
"version": "1.3.796",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.796.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.796.tgz",
|
||||||
|
@ -5445,6 +5622,12 @@
|
||||||
"is-symbol": "^1.0.2"
|
"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": {
|
"escalade": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
"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": {
|
"fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
@ -6070,11 +6279,58 @@
|
||||||
"is-glob": "^4.0.1"
|
"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": {
|
"globals": {
|
||||||
"version": "11.12.0",
|
"version": "11.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
|
"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": {
|
"globby": {
|
||||||
"version": "11.0.4",
|
"version": "11.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz",
|
||||||
|
@ -6088,6 +6344,34 @@
|
||||||
"slash": "^3.0.0"
|
"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": {
|
"graceful-fs": {
|
||||||
"version": "4.2.6",
|
"version": "4.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
|
||||||
|
@ -6511,6 +6795,11 @@
|
||||||
"http-errors": "~1.7.2"
|
"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": {
|
"http-errors": {
|
||||||
"version": "1.7.3",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
|
||||||
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw=="
|
"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": {
|
"import-cwd": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||||
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="
|
"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": {
|
"json-parse-better-errors": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
|
"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": {
|
"json5": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
|
||||||
|
@ -7142,6 +7447,14 @@
|
||||||
"tsscmp": "1.0.6"
|
"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": {
|
"kind-of": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||||
|
@ -7297,6 +7610,14 @@
|
||||||
"launch-editor": "^2.2.1"
|
"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": {
|
"lilconfig": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.3.tgz",
|
"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": {
|
"locate-path": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
|
@ -7409,6 +7738,11 @@
|
||||||
"tslib": "^2.0.3"
|
"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": {
|
"lru-cache": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
|
@ -7446,6 +7780,23 @@
|
||||||
"object-visit": "^1.0.0"
|
"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": {
|
"md5.js": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz",
|
||||||
"integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ=="
|
"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": {
|
"minimalistic-assert": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz",
|
||||||
"integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg=="
|
"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": {
|
"npm-run-path": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||||
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
|
"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": {
|
"p-defer": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
"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": {
|
"promise-inflight": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
|
||||||
|
@ -11029,6 +11413,12 @@
|
||||||
"signal-exit": "^3.0.2"
|
"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": {
|
"protocols": {
|
||||||
"version": "1.4.8",
|
"version": "1.4.8",
|
||||||
"resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
|
||||||
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo="
|
"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": {
|
"restore-cursor": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
|
||||||
|
@ -11601,6 +11999,28 @@
|
||||||
"inherits": "^2.0.1"
|
"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": {
|
"run-async": {
|
||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
"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": {
|
"send": {
|
||||||
"version": "0.17.1",
|
"version": "0.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
|
"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": {
|
"serialize-javascript": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
|
"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": {
|
"supports-color": {
|
||||||
"version": "5.5.0",
|
"version": "5.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
"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": {
|
"to-regex": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
|
||||||
"integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY="
|
"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": {
|
"type-fest": {
|
||||||
"version": "0.21.3",
|
"version": "0.21.3",
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
|
"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": {
|
"use": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "v0.4.0-beta",
|
"version": "v0.8.0-beta",
|
||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt --hostname localhost --port 1337",
|
"dev": "nuxt --hostname localhost --port 1337",
|
||||||
|
@ -10,14 +10,18 @@
|
||||||
"icons-android": "cordova-res android --skip-config --copy"
|
"icons-android": "cordova-res android --skip-config --copy"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@capacitor-community/sqlite": "^3.2.0",
|
||||||
"@capacitor/android": "^3.2.2",
|
"@capacitor/android": "^3.2.2",
|
||||||
"@capacitor/cli": "^3.1.2",
|
"@capacitor/cli": "^3.1.2",
|
||||||
"@capacitor/core": "^3.2.2",
|
"@capacitor/core": "^3.2.2",
|
||||||
"@capacitor/dialog": "^1.0.3",
|
"@capacitor/dialog": "^1.0.3",
|
||||||
"@capacitor/ios": "^3.2.2",
|
"@capacitor/ios": "^3.2.2",
|
||||||
|
"@capacitor/network": "^1.0.3",
|
||||||
|
"@capacitor/storage": "^1.1.0",
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
"@robingenz/capacitor-app-update": "^1.0.0",
|
"@robingenz/capacitor-app-update": "^1.0.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
|
"capacitor-data-storage-sqlite": "^3.2.0",
|
||||||
"core-js": "^3.15.1",
|
"core-js": "^3.15.1",
|
||||||
"hls.js": "^1.0.9",
|
"hls.js": "^1.0.9",
|
||||||
"nuxt": "^2.15.7",
|
"nuxt": "^2.15.7",
|
||||||
|
|
|
@ -22,7 +22,9 @@
|
||||||
|
|
||||||
<p class="font-mono pt-1 pb-4">{{ $config.version }}</p>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -67,8 +69,15 @@ export default {
|
||||||
this.$server.logout()
|
this.$server.logout()
|
||||||
this.$router.push('/connect')
|
this.$router.push('/connect')
|
||||||
},
|
},
|
||||||
|
openAppStore() {
|
||||||
|
AppUpdate.openAppStore()
|
||||||
|
},
|
||||||
async clickUpdate() {
|
async clickUpdate() {
|
||||||
await AppUpdate.openAppStore()
|
if (this.immediateUpdateAllowed) {
|
||||||
|
AppUpdate.performImmediateUpdate()
|
||||||
|
} else {
|
||||||
|
AppUpdate.openAppStore()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|
|
@ -3,17 +3,19 @@
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="w-32">
|
<div class="w-32">
|
||||||
<div class="relative">
|
<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 class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm" :style="{ width: 128 * progressPercent + 'px' }"></div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <cards-book-cover :audiobook="audiobook" :width="128" /> -->
|
<div class="flex my-4">
|
||||||
|
<p class="text-sm">{{ numTracks }} Tracks</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-3">
|
<div class="flex-grow px-3">
|
||||||
<h1 class="text-lg">{{ title }}</h1>
|
<h1 class="text-lg">{{ title }}</h1>
|
||||||
<h3 v-if="series" class="font-book text-gray-300 text-lg leading-7">{{ seriesText }}</h3>
|
<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-sm text-gray-400">by {{ author }}</p>
|
||||||
<p class="text-gray-300 text-sm my-1">
|
<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>
|
</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' : ''">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ui-btn color="success" :disabled="streaming" class="flex items-center justify-center w-full mt-4" :padding-x="4" @click="playClick">
|
<div v-if="isConnected || isDownloadPlayable" class="flex mt-4">
|
||||||
<span v-show="!streaming" class="material-icons">play_arrow</span>
|
<ui-btn color="success" :disabled="isPlaying" class="flex items-center justify-center w-full mr-2" :padding-x="4" @click="playClick">
|
||||||
<span class="px-1">{{ streaming ? 'Streaming' : 'Play' }}</span>
|
<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>
|
||||||
<div class="flex my-4"></div>
|
<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>
|
</div>
|
||||||
<div class="w-full py-4">
|
<div class="w-full py-4">
|
||||||
|
@ -38,15 +44,25 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Path from 'path'
|
||||||
import { Dialog } from '@capacitor/dialog'
|
import { Dialog } from '@capacitor/dialog'
|
||||||
|
import { Capacitor } from '@capacitor/core'
|
||||||
|
import AudioDownloader from '@/plugins/audio-downloader'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ params, redirect, app }) {
|
async asyncData({ store, params, redirect, app }) {
|
||||||
var audiobookId = params.id
|
var audiobookId = params.id
|
||||||
var audiobook = await app.$axios.$get(`/api/audiobook/${audiobookId}`).catch((error) => {
|
var audiobook = null
|
||||||
|
|
||||||
|
if (app.$server.connected) {
|
||||||
|
audiobook = await app.$axios.$get(`/api/audiobook/${audiobookId}`).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
audiobook = store.getters['audiobooks/getAudiobook'](audiobookId)
|
||||||
|
}
|
||||||
|
|
||||||
if (!audiobook) {
|
if (!audiobook) {
|
||||||
console.error('No audiobook...', params.id)
|
console.error('No audiobook...', params.id)
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
|
@ -61,6 +77,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isConnected() {
|
||||||
|
return this.$store.state.socketConnected
|
||||||
|
},
|
||||||
audiobookId() {
|
audiobookId() {
|
||||||
return this.audiobook.id
|
return this.audiobook.id
|
||||||
},
|
},
|
||||||
|
@ -87,14 +106,11 @@ export default {
|
||||||
if (!this.volumeNumber) return this.series
|
if (!this.volumeNumber) return this.series
|
||||||
return `${this.series} #${this.volumeNumber}`
|
return `${this.series} #${this.volumeNumber}`
|
||||||
},
|
},
|
||||||
durationPretty() {
|
|
||||||
return this.audiobook.durationPretty
|
|
||||||
},
|
|
||||||
duration() {
|
duration() {
|
||||||
return this.audiobook.duration
|
return this.audiobook.duration
|
||||||
},
|
},
|
||||||
sizePretty() {
|
size() {
|
||||||
return this.audiobook.sizePretty
|
return this.audiobook.size
|
||||||
},
|
},
|
||||||
userAudiobooks() {
|
userAudiobooks() {
|
||||||
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
||||||
|
@ -102,27 +118,65 @@ export default {
|
||||||
userAudiobook() {
|
userAudiobook() {
|
||||||
return this.userAudiobooks[this.audiobookId] || null
|
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() {
|
userCurrentTime() {
|
||||||
return this.userAudiobook ? this.userAudiobook.currentTime : 0
|
return this.mostRecentUserAudiobook ? this.mostRecentUserAudiobook.currentTime : 0
|
||||||
},
|
},
|
||||||
userTimeRemaining() {
|
userTimeRemaining() {
|
||||||
return this.duration - this.userCurrentTime
|
return this.duration - this.userCurrentTime
|
||||||
},
|
},
|
||||||
progressPercent() {
|
progressPercent() {
|
||||||
return this.userAudiobook ? this.userAudiobook.progress : 0
|
return this.mostRecentUserAudiobook ? this.mostRecentUserAudiobook.progress : 0
|
||||||
},
|
|
||||||
streamAudiobook() {
|
|
||||||
return this.$store.state.streamAudiobook
|
|
||||||
},
|
},
|
||||||
isStreaming() {
|
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: {
|
methods: {
|
||||||
playClick() {
|
playClick() {
|
||||||
this.$store.commit('setPlayOnLoad', true)
|
this.$store.commit('setPlayOnLoad', true)
|
||||||
|
if (!this.isDownloadPlayable) {
|
||||||
|
// Stream
|
||||||
|
console.log('[PLAYCLICK] Set Playing STREAM ' + this.title)
|
||||||
this.$store.commit('setStreamAudiobook', this.audiobook)
|
this.$store.commit('setStreamAudiobook', this.audiobook)
|
||||||
this.$server.socket.emit('open_stream', this.audiobook.id)
|
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() {
|
async clearProgressClick() {
|
||||||
const { value } = await Dialog.confirm({
|
const { value } = await Dialog.confirm({
|
||||||
|
@ -132,18 +186,27 @@ export default {
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
this.resettingProgress = true
|
this.resettingProgress = true
|
||||||
this.$axios
|
if (this.$server.connected) {
|
||||||
|
await this.$axios
|
||||||
.$delete(`/api/user/audiobook/${this.audiobookId}`)
|
.$delete(`/api/user/audiobook/${this.audiobookId}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('Progress reset complete')
|
console.log('Progress reset complete')
|
||||||
this.$toast.success(`Your progress was reset`)
|
this.$toast.success(`Your progress was reset`)
|
||||||
this.resettingProgress = false
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Progress reset failed', error)
|
console.error('Progress reset failed', error)
|
||||||
this.resettingProgress = false
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
this.$localStore.updateUserAudiobookProgress({
|
||||||
|
audiobookId: this.audiobookId,
|
||||||
|
currentTime: 0,
|
||||||
|
totalDuration: this.duration,
|
||||||
|
progress: 0,
|
||||||
|
lastUpdate: Date.now(),
|
||||||
|
isRead: false
|
||||||
|
})
|
||||||
|
this.resettingProgress = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
audiobookUpdated() {
|
audiobookUpdated() {
|
||||||
console.log('Audiobook Updated - Fetch full audiobook')
|
console.log('Audiobook Updated - Fetch full audiobook')
|
||||||
|
@ -155,12 +218,160 @@ export default {
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', 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() {
|
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 })
|
this.$store.commit('audiobooks/addListener', { id: 'audiobook', audiobookId: this.audiobookId, meth: this.audiobookUpdated })
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
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')
|
this.$store.commit('audiobooks/removeListener', 'audiobook')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full">
|
<div class="w-full h-full">
|
||||||
<div class="relative flex items-center justify-center min-h-screen sm:pt-0">
|
<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">
|
<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" />
|
<img src="/Logo.png" class="h-20 w-20 mb-2" />
|
||||||
<h1 class="text-2xl font-book">AudioBookshelf</h1>
|
<h1 class="text-2xl font-book">AudioBookshelf</h1>
|
||||||
</div>
|
</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 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">
|
<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">
|
<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>
|
<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>
|
<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-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" type="submit" :padding-x="3" class="h-10">Submit</ui-btn>
|
<ui-btn :disabled="processing || !networkConnected" type="submit" :padding-x="3" class="h-10">{{ networkConnected ? 'Submit' : 'No Internet' }}</ui-btn>
|
||||||
</form>
|
</form>
|
||||||
<template v-if="showAuth">
|
<template v-if="showAuth">
|
||||||
<div class="flex items-center">
|
<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="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-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>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -77,8 +80,16 @@ export default {
|
||||||
loggedIn: false
|
loggedIn: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
networkConnected() {
|
||||||
|
return this.$store.state.networkConnected
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async submit() {
|
async submit() {
|
||||||
|
if (!this.networkConnected) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!this.serverUrl.startsWith('http')) {
|
if (!this.serverUrl.startsWith('http')) {
|
||||||
this.serverUrl = 'http://' + this.serverUrl
|
this.serverUrl = 'http://' + this.serverUrl
|
||||||
}
|
}
|
||||||
|
@ -94,6 +105,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submitAuth() {
|
async submitAuth() {
|
||||||
|
if (!this.networkConnected) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!this.username) {
|
if (!this.username) {
|
||||||
this.error = 'Invalid username'
|
this.error = 'Invalid username'
|
||||||
return
|
return
|
||||||
|
@ -137,8 +151,11 @@ export default {
|
||||||
}
|
}
|
||||||
this.$server.on('connected', this.socketConnected)
|
this.$server.on('connected', this.socketConnected)
|
||||||
|
|
||||||
var localServerUrl = localStorage.getItem('serverUrl')
|
var localServerUrl = await this.$localStore.getServerUrl()
|
||||||
var localUserToken = localStorage.getItem('userToken')
|
var localUserToken = await this.$localStore.getToken()
|
||||||
|
|
||||||
|
if (!this.networkConnected) return
|
||||||
|
|
||||||
if (localServerUrl) {
|
if (localServerUrl) {
|
||||||
this.serverUrl = localServerUrl
|
this.serverUrl = localServerUrl
|
||||||
if (localUserToken) {
|
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'
|
import Server from '../Server'
|
||||||
|
|
||||||
Vue.prototype.$server = null
|
export default function ({ store }, inject) {
|
||||||
export default function ({ store }) {
|
inject('server', new Server(store))
|
||||||
Vue.prototype.$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 = {
|
export const getters = {
|
||||||
|
getAudiobook: state => id => {
|
||||||
|
return state.audiobooks.find(ab => ab.id === id)
|
||||||
|
},
|
||||||
getFiltered: (state, getters, rootState) => () => {
|
getFiltered: (state, getters, rootState) => () => {
|
||||||
var filtered = state.audiobooks
|
var filtered = state.audiobooks
|
||||||
var settings = rootState.user.settings || {}
|
var settings = rootState.user.settings || {}
|
||||||
|
@ -51,7 +54,7 @@ export const getters = {
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
load({ commit }) {
|
load({ commit }) {
|
||||||
this.$axios
|
return this.$axios
|
||||||
.$get(`/api/audiobooks`)
|
.$get(`/api/audiobooks`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log('Audiobooks request data', data)
|
console.log('Audiobooks request data', data)
|
||||||
|
@ -59,12 +62,17 @@ export const actions = {
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
commit('set', [])
|
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
|
reset(state) {
|
||||||
|
state.audiobooks = []
|
||||||
|
state.genres = [...STANDARD_GENRES]
|
||||||
|
state.tags = []
|
||||||
|
state.series = []
|
||||||
|
},
|
||||||
set(state, audiobooks) {
|
set(state, audiobooks) {
|
||||||
// GENRES
|
// GENRES
|
||||||
var genres = [...state.genres]
|
var genres = [...state.genres]
|
||||||
|
@ -92,7 +100,15 @@ export const mutations = {
|
||||||
state.series = series
|
state.series = series
|
||||||
state.series.sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
|
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) => {
|
state.listeners.forEach((listener) => {
|
||||||
listener.meth()
|
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 = () => ({
|
export const state = () => ({
|
||||||
streamAudiobook: null,
|
streamAudiobook: null,
|
||||||
|
playingDownload: null,
|
||||||
playOnLoad: false,
|
playOnLoad: false,
|
||||||
serverUrl: null,
|
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 actions = {}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
|
@ -23,12 +40,37 @@ export const mutations = {
|
||||||
state.playOnLoad = val
|
state.playOnLoad = val
|
||||||
},
|
},
|
||||||
setStreamAudiobook(state, audiobook) {
|
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) {
|
setServerUrl(state, url) {
|
||||||
state.serverUrl = url
|
state.serverUrl = url
|
||||||
},
|
},
|
||||||
setUser(state, user) {
|
setSocketConnected(state, val) {
|
||||||
state.user = user
|
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 = () => ({
|
export const state = () => ({
|
||||||
user: null,
|
user: null,
|
||||||
|
localUserAudiobooks: {},
|
||||||
settings: {
|
settings: {
|
||||||
orderBy: 'book.title',
|
orderBy: 'book.title',
|
||||||
orderDesc: false,
|
orderDesc: false,
|
||||||
|
@ -28,7 +31,9 @@ export const getters = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
updateUserSettings({ commit }, payload) {
|
async updateUserSettings({ commit }, payload) {
|
||||||
|
|
||||||
|
if (Vue.prototype.$server.connected) {
|
||||||
var updatePayload = {
|
var updatePayload = {
|
||||||
...payload
|
...payload
|
||||||
}
|
}
|
||||||
|
@ -44,17 +49,24 @@ export const actions = {
|
||||||
console.error('Failed to update settings', error)
|
console.error('Failed to update settings', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
console.log('Update settings without server')
|
||||||
|
commit('setSettings', payload)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
|
setLocalUserAudiobooks(state, userAudiobooks) {
|
||||||
|
state.localUserAudiobooks = userAudiobooks
|
||||||
|
},
|
||||||
setUser(state, user) {
|
setUser(state, user) {
|
||||||
state.user = user
|
state.user = user
|
||||||
if (user) {
|
if (user) {
|
||||||
if (user.token) localStorage.setItem('token', user.token)
|
if (user.token) this.$localStore.setToken(user.token)
|
||||||
console.log('setUser', user.username)
|
console.log('setUser', user.username)
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('token')
|
this.$localStore.setToken(null)
|
||||||
console.warn('setUser cleared')
|
console.warn('setUser cleared')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -69,6 +81,9 @@ export const mutations = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
|
console.log('Update settings in local storage')
|
||||||
|
this.$localStore.setUserSettings({ ...state.settings })
|
||||||
|
|
||||||
state.settingsListeners.forEach((listener) => {
|
state.settingsListeners.forEach((listener) => {
|
||||||
listener.meth(state.settings)
|
listener.meth(state.settings)
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue