mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-21 11:14:38 +02:00
Fix: download single track audiobooks #15, Change: check write permission when selecting folder #13, Add: Show folders and files list in user selected folder, Fix: Seek back only if audiobook was played #20
This commit is contained in:
parent
de4340487b
commit
ebf628315c
17 changed files with 727 additions and 502 deletions
|
@ -13,8 +13,8 @@ android {
|
|||
applicationId "com.audiobookshelf.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 27
|
||||
versionName "0.9.11-beta"
|
||||
versionCode 28
|
||||
versionName "0.9.12-beta"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
|
|
@ -2,28 +2,19 @@ package com.audiobookshelf.app
|
|||
|
||||
import android.app.DownloadManager
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.anggrayudi.storage.SimpleStorage
|
||||
import com.anggrayudi.storage.SimpleStorageHelper
|
||||
import com.anggrayudi.storage.callback.FileCallback
|
||||
import com.anggrayudi.storage.callback.FolderPickerCallback
|
||||
import com.anggrayudi.storage.callback.StorageAccessCallback
|
||||
import com.anggrayudi.storage.file.*
|
||||
import com.anggrayudi.storage.media.FileDescription
|
||||
|
||||
import com.getcapacitor.JSObject
|
||||
import com.getcapacitor.Plugin
|
||||
import com.getcapacitor.PluginCall
|
||||
import com.getcapacitor.PluginMethod
|
||||
import com.getcapacitor.annotation.CapacitorPlugin
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
|
||||
|
||||
|
@ -34,219 +25,117 @@ class AudioDownloader : Plugin() {
|
|||
lateinit var mainActivity:MainActivity
|
||||
lateinit var downloadManager:DownloadManager
|
||||
|
||||
data class AudiobookItem(val uri: Uri, val name: String, val size: Long, val coverUrl: String) {
|
||||
fun toJSObject() : JSObject {
|
||||
var obj = JSObject()
|
||||
obj.put("uri", this.uri)
|
||||
obj.put("name", this.name)
|
||||
obj.put("size", this.size)
|
||||
obj.put("coverUrl", this.coverUrl)
|
||||
return obj
|
||||
}
|
||||
}
|
||||
// data class AudiobookItem(val uri: Uri, val name: String, val size: Long, val coverUrl: String) {
|
||||
// fun toJSObject() : JSObject {
|
||||
// var obj = JSObject()
|
||||
// obj.put("uri", this.uri)
|
||||
// obj.put("name", this.name)
|
||||
// obj.put("size", this.size)
|
||||
// obj.put("coverUrl", this.coverUrl)
|
||||
// return obj
|
||||
// }
|
||||
// }
|
||||
|
||||
override fun load() {
|
||||
mainActivity = (activity as MainActivity)
|
||||
downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
// storage = SimpleStorage(mainActivity)
|
||||
|
||||
var recieverEvent : (evt: String, id: Long) -> Unit = { evt: String, id: Long ->
|
||||
if (evt == "complete") {}
|
||||
var recieverEvent: (evt: String, id: Long) -> Unit = { evt: String, id: Long ->
|
||||
if (evt == "complete") {
|
||||
}
|
||||
if (evt == "clicked") {
|
||||
Log.d(tag, "Clicked $id back in the audiodownloader")
|
||||
}
|
||||
}
|
||||
mainActivity.registerBroadcastReceiver(recieverEvent)
|
||||
|
||||
|
||||
setupSimpleStorage()
|
||||
|
||||
Log.d(tag, "Build SDK ${Build.VERSION.SDK_INT}")
|
||||
// Android 9 OR Below Request Permissions
|
||||
// if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
|
||||
// Log.d(tag, "Requires Permission")
|
||||
//// storage.requestStorageAccess(9)
|
||||
// var jsobj = JSObject()
|
||||
// jsobj.put("value", "required")
|
||||
// notifyListeners("permission", jsobj)
|
||||
// } else {
|
||||
// Log.d(tag, "Does not request permission")
|
||||
}
|
||||
|
||||
|
||||
// @PluginMethod
|
||||
// fun load(call: PluginCall) {
|
||||
// var audiobookUrls = call.data.getJSONArray("audiobookUrls")
|
||||
// var len = audiobookUrls?.length()
|
||||
// if (len == null) {
|
||||
// len = 0
|
||||
// }
|
||||
// Log.d(tag, "CALLED LOAD $len")
|
||||
// var audiobookItems:MutableList<AudiobookItem> = mutableListOf()
|
||||
//
|
||||
// (0 until len).forEach {
|
||||
// var jsobj = audiobookUrls.get(it) as JSONObject
|
||||
// var audiobookUrl = jsobj.get("contentUrl").toString()
|
||||
// var coverUrl = jsobj.get("coverUrl").toString()
|
||||
// var storageId = ""
|
||||
// if(jsobj.has("storageId")) jsobj.get("storageId").toString()
|
||||
//
|
||||
// var basePath = ""
|
||||
// if(jsobj.has("basePath")) jsobj.get("basePath").toString()
|
||||
//
|
||||
// var coverBasePath = ""
|
||||
// if(jsobj.has("coverBasePath")) jsobj.get("coverBasePath").toString()
|
||||
//
|
||||
// Log.d(tag, "LOOKUP $storageId $basePath $audiobookUrl")
|
||||
//
|
||||
// var audiobookFile: DocumentFile? = null
|
||||
// var coverFile: DocumentFile? = null
|
||||
//
|
||||
// // Android 9 OR Below use storage id and base path
|
||||
// if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
|
||||
// audiobookFile = DocumentFileCompat.fromSimplePath(context, storageId, basePath)
|
||||
// if (coverUrl != null && coverUrl != "") {
|
||||
// coverFile = DocumentFileCompat.fromSimplePath(context, storageId, coverBasePath)
|
||||
// }
|
||||
// } else {
|
||||
// // Android 10 and up manually deleting will still load the file causing crash
|
||||
// var exists = checkUriExists(Uri.parse(audiobookUrl))
|
||||
// if (exists) {
|
||||
// Log.d(tag, "Audiobook exists")
|
||||
// audiobookFile = DocumentFileCompat.fromUri(context, Uri.parse(audiobookUrl))
|
||||
// } else {
|
||||
// Log.e(tag, "Audiobook does not exist")
|
||||
// }
|
||||
//
|
||||
// var coverExists = checkUriExists(Uri.parse(coverUrl))
|
||||
// if (coverExists) {
|
||||
// Log.d(tag, "Cover Exists")
|
||||
// coverFile = DocumentFileCompat.fromUri(context, Uri.parse(coverUrl))
|
||||
// } else if (coverUrl != null && coverUrl != "") {
|
||||
// Log.e(tag, "Cover does not exist")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (audiobookFile == null) {
|
||||
// Log.e(tag, "Audiobook was not found $audiobookUrl")
|
||||
// } else {
|
||||
// Log.d(tag, "Audiobook File Found StorageId:${audiobookFile.getStorageId(context)} | AbsolutePath:${audiobookFile.getAbsolutePath(context)} | BasePath:${audiobookFile.getBasePath(context)}")
|
||||
//
|
||||
// var _name = audiobookFile.name
|
||||
// if (_name == null) _name = ""
|
||||
//
|
||||
// var size = audiobookFile.length()
|
||||
//
|
||||
// if (audiobookFile.uri.toString() !== audiobookUrl) {
|
||||
// Log.d(tag, "Audiobook URI ${audiobookFile.uri} is different from $audiobookUrl => using the latter")
|
||||
// }
|
||||
//
|
||||
// // Use existing URI's - bug happening where new uri is different from initial
|
||||
// var abItem = AudiobookItem(Uri.parse(audiobookUrl), _name, size, coverUrl)
|
||||
//
|
||||
// Log.d(tag, "Setting AB ITEM ${abItem.name} | ${abItem.size} | ${abItem.uri} | ${abItem.coverUrl}")
|
||||
//
|
||||
// audiobookItems.add(abItem)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Log.d(tag, "Load Finished ${audiobookItems.size} found")
|
||||
//
|
||||
// var audiobookObjs:List<JSObject> = audiobookItems.map{ it.toJSObject() }
|
||||
// var mediaItemNoticePayload = JSObject()
|
||||
// mediaItemNoticePayload.put("items", audiobookObjs)
|
||||
// notifyListeners("onMediaLoaded", mediaItemNoticePayload)
|
||||
// }
|
||||
}
|
||||
|
||||
private fun setupSimpleStorage() {
|
||||
mainActivity.storageHelper.onFolderSelected = { requestCode, folder ->
|
||||
Log.d(tag, "FOLDER SELECTED $requestCode ${folder.name} ${folder.uri}")
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("value", "granted")
|
||||
jsobj.put("uri", folder.uri)
|
||||
jsobj.put("absolutePath", folder.getAbsolutePath(context))
|
||||
jsobj.put("storageId", folder.getStorageId(context))
|
||||
jsobj.put("storageType", folder.getStorageType(context))
|
||||
jsobj.put("simplePath", folder.getSimplePath(context))
|
||||
jsobj.put("basePath", folder.getBasePath(context))
|
||||
notifyListeners("permission", jsobj)
|
||||
}
|
||||
|
||||
mainActivity.storage.storageAccessCallback = object : StorageAccessCallback {
|
||||
override fun onRootPathNotSelected(
|
||||
requestCode: Int,
|
||||
rootPath: String,
|
||||
uri: Uri,
|
||||
selectedStorageType: StorageType,
|
||||
expectedStorageType: StorageType
|
||||
) {
|
||||
Log.d(tag, "STORAGE ACCESS CALLBACK")
|
||||
}
|
||||
|
||||
override fun onCanceledByUser(requestCode: Int) {
|
||||
Log.d(tag, "STORAGE ACCESS CALLBACK")
|
||||
}
|
||||
|
||||
override fun onExpectedStorageNotSelected(requestCode: Int, selectedFolder: DocumentFile, selectedStorageType: StorageType, expectedBasePath: String, expectedStorageType: StorageType) {
|
||||
Log.d(tag, "STORAGE ACCESS CALLBACK")
|
||||
}
|
||||
|
||||
override fun onStoragePermissionDenied(requestCode: Int) {
|
||||
Log.d(tag, "STORAGE ACCESS CALLBACK")
|
||||
}
|
||||
|
||||
override fun onRootPathPermissionGranted(requestCode: Int, root: DocumentFile) {
|
||||
Log.d(tag, "STORAGE ACCESS CALLBACK")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
@PluginMethod
|
||||
fun requestStoragePermission(call: PluginCall) {
|
||||
Log.d(tag, "Request Storage Permissions")
|
||||
mainActivity.storageHelper.requestStorageAccess()
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun checkStoragePermission(call: PluginCall) {
|
||||
var res = false
|
||||
|
||||
if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
|
||||
res = SimpleStorage.hasStoragePermission(context)
|
||||
Log.d(tag, "Check Storage Access $res")
|
||||
} else {
|
||||
Log.d(tag, "Has permission on Android 10 or up")
|
||||
res = true
|
||||
}
|
||||
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("value", res)
|
||||
call.resolve(jsobj)
|
||||
}
|
||||
|
||||
fun checkUriExists(uri: Uri?): Boolean {
|
||||
if (uri == null) return false
|
||||
val resolver = context.contentResolver
|
||||
//1. Check Uri
|
||||
var cursor: Cursor? = null
|
||||
val isUriExist: Boolean = try {
|
||||
cursor = resolver.query(uri, null, null, null, null)
|
||||
//cursor null: content Uri was invalid or some other error occurred
|
||||
//cursor.moveToFirst() false: Uri was ok but no entry found.
|
||||
(cursor != null && cursor.moveToFirst())
|
||||
} catch (t: Throwable) {
|
||||
false
|
||||
} finally {
|
||||
try {
|
||||
cursor?.close()
|
||||
} catch (t: Throwable) {
|
||||
}
|
||||
false
|
||||
}
|
||||
return isUriExist
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun load(call: PluginCall) {
|
||||
var audiobookUrls = call.data.getJSONArray("audiobookUrls")
|
||||
var len = audiobookUrls?.length()
|
||||
if (len == null) {
|
||||
len = 0
|
||||
}
|
||||
Log.d(tag, "CALLED LOAD $len")
|
||||
var audiobookItems:MutableList<AudiobookItem> = mutableListOf()
|
||||
|
||||
(0 until len).forEach {
|
||||
var jsobj = audiobookUrls.get(it) as JSONObject
|
||||
var audiobookUrl = jsobj.get("contentUrl").toString()
|
||||
var coverUrl = jsobj.get("coverUrl").toString()
|
||||
var storageId = ""
|
||||
if(jsobj.has("storageId")) jsobj.get("storageId").toString()
|
||||
|
||||
var basePath = ""
|
||||
if(jsobj.has("basePath")) jsobj.get("basePath").toString()
|
||||
|
||||
var coverBasePath = ""
|
||||
if(jsobj.has("coverBasePath")) jsobj.get("coverBasePath").toString()
|
||||
|
||||
Log.d(tag, "LOOKUP $storageId $basePath $audiobookUrl")
|
||||
|
||||
var audiobookFile: DocumentFile? = null
|
||||
var coverFile: DocumentFile? = null
|
||||
|
||||
// Android 9 OR Below use storage id and base path
|
||||
if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
|
||||
audiobookFile = DocumentFileCompat.fromSimplePath(context, storageId, basePath)
|
||||
if (coverUrl != null && coverUrl != "") {
|
||||
coverFile = DocumentFileCompat.fromSimplePath(context, storageId, coverBasePath)
|
||||
}
|
||||
} else {
|
||||
// Android 10 and up manually deleting will still load the file causing crash
|
||||
var exists = checkUriExists(Uri.parse(audiobookUrl))
|
||||
if (exists) {
|
||||
Log.d(tag, "Audiobook exists")
|
||||
audiobookFile = DocumentFileCompat.fromUri(context, Uri.parse(audiobookUrl))
|
||||
} else {
|
||||
Log.e(tag, "Audiobook does not exist")
|
||||
}
|
||||
|
||||
var coverExists = checkUriExists(Uri.parse(coverUrl))
|
||||
if (coverExists) {
|
||||
Log.d(tag, "Cover Exists")
|
||||
coverFile = DocumentFileCompat.fromUri(context, Uri.parse(coverUrl))
|
||||
} else if (coverUrl != null && coverUrl != "") {
|
||||
Log.e(tag, "Cover does not exist")
|
||||
}
|
||||
}
|
||||
|
||||
if (audiobookFile == null) {
|
||||
Log.e(tag, "Audiobook was not found $audiobookUrl")
|
||||
} else {
|
||||
Log.d(tag, "Audiobook File Found StorageId:${audiobookFile.getStorageId(context)} | AbsolutePath:${audiobookFile.getAbsolutePath(context)} | BasePath:${audiobookFile.getBasePath(context)}")
|
||||
|
||||
var _name = audiobookFile.name
|
||||
if (_name == null) _name = ""
|
||||
|
||||
var size = audiobookFile.length()
|
||||
|
||||
if (audiobookFile.uri.toString() !== audiobookUrl) {
|
||||
Log.d(tag, "Audiobook URI ${audiobookFile.uri} is different from $audiobookUrl => using the latter")
|
||||
}
|
||||
|
||||
// Use existing URI's - bug happening where new uri is different from initial
|
||||
var abItem = AudiobookItem(Uri.parse(audiobookUrl), _name, size, coverUrl)
|
||||
|
||||
Log.d(tag, "Setting AB ITEM ${abItem.name} | ${abItem.size} | ${abItem.uri} | ${abItem.coverUrl}")
|
||||
|
||||
audiobookItems.add(abItem)
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(tag, "Load Finished ${audiobookItems.size} found")
|
||||
|
||||
var audiobookObjs:List<JSObject> = audiobookItems.map{ it.toJSObject() }
|
||||
var mediaItemNoticePayload = JSObject()
|
||||
mediaItemNoticePayload.put("items", audiobookObjs)
|
||||
notifyListeners("onMediaLoaded", mediaItemNoticePayload)
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun download(call: PluginCall) {
|
||||
|
@ -399,74 +288,6 @@ class AudioDownloader : Plugin() {
|
|||
call.resolve(ret)
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun selectFolder(call: PluginCall) {
|
||||
mainActivity.storage.folderPickerCallback = object : FolderPickerCallback {
|
||||
override fun onFolderSelected(requestCode: Int, folder: DocumentFile) {
|
||||
Log.d(tag, "ONF OLDER SELECRTED ${folder.uri} ${folder.name}")
|
||||
|
||||
var absolutePath = folder.getAbsolutePath(activity)
|
||||
var storageId = folder.getStorageId(activity)
|
||||
var storageType = folder.getStorageType(activity)
|
||||
var simplePath = folder.getSimplePath(activity)
|
||||
var basePath = folder.getBasePath(activity)
|
||||
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("uri", folder.uri)
|
||||
jsobj.put("absolutePath", absolutePath)
|
||||
jsobj.put("storageId", storageId)
|
||||
jsobj.put("storageType", storageType)
|
||||
jsobj.put("simplePath", simplePath)
|
||||
jsobj.put("basePath", basePath)
|
||||
call.resolve(jsobj)
|
||||
}
|
||||
|
||||
override fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType) {
|
||||
Log.e(tag, "STORAGE ACCESS DENIED")
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("error", "Access Denied")
|
||||
call.resolve(jsobj)
|
||||
}
|
||||
|
||||
override fun onStoragePermissionDenied(requestCode: Int) {
|
||||
Log.d(tag, "STORAGE PERMISSION DENIED $requestCode")
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("error", "Permission Denied")
|
||||
call.resolve(jsobj)
|
||||
}
|
||||
}
|
||||
mainActivity.storage.openFolderPicker(6)
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun delete(call: PluginCall) {
|
||||
var url = call.data.getString("url", "").toString()
|
||||
var coverUrl = call.data.getString("coverUrl", "").toString()
|
||||
var folderUrl = call.data.getString("folderUrl", "").toString()
|
||||
|
||||
if (folderUrl != "") {
|
||||
Log.d(tag, "CALLED DELETE FIOLDER: $folderUrl")
|
||||
var folder = DocumentFileCompat.fromUri(context, Uri.parse(folderUrl))
|
||||
var success = folder?.deleteRecursively(context)
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("success", success)
|
||||
call.resolve()
|
||||
} else {
|
||||
// Older audiobooks did not store a folder url, use cover and audiobook url
|
||||
var abExists = checkUriExists(Uri.parse(url))
|
||||
if (abExists) {
|
||||
var abfile = DocumentFileCompat.fromUri(context, Uri.parse(url))
|
||||
abfile?.delete()
|
||||
}
|
||||
|
||||
var coverExists = checkUriExists(Uri.parse(coverUrl))
|
||||
if (coverExists) {
|
||||
var coverfile = DocumentFileCompat.fromUri(context, Uri.parse(coverUrl))
|
||||
coverfile?.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class DownloadProgressUpdater(private val manager: DownloadManager, private val downloadId: Long, private var receiver: (Long, Long) -> Unit, private var doneReceiver: (Long, Boolean) -> Unit) : Thread() {
|
||||
private val query: DownloadManager.Query = DownloadManager.Query()
|
||||
private var totalBytes: Int = 0
|
||||
|
|
|
@ -43,6 +43,7 @@ class MainActivity : BridgeActivity() {
|
|||
Log.d(tag, "onCreate")
|
||||
registerPlugin(MyNativeAudio::class.java)
|
||||
registerPlugin(AudioDownloader::class.java)
|
||||
registerPlugin(StorageManager::class.java)
|
||||
|
||||
var filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE).apply {
|
||||
addAction(DownloadManager.ACTION_NOTIFICATION_CLICKED)
|
||||
|
|
|
@ -263,7 +263,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
|
||||
override fun getSupportedPrepareActions(): Long {
|
||||
Log.d(tag, "GET SUPORTED ACITONS")
|
||||
return PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or
|
||||
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
|
||||
PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH or
|
||||
|
|
|
@ -0,0 +1,257 @@
|
|||
package com.audiobookshelf.app
|
||||
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
|
||||
import com.anggrayudi.storage.SimpleStorage
|
||||
import com.anggrayudi.storage.callback.FolderPickerCallback
|
||||
import com.anggrayudi.storage.callback.StorageAccessCallback
|
||||
import com.anggrayudi.storage.file.*
|
||||
import com.getcapacitor.JSObject
|
||||
import com.getcapacitor.Plugin
|
||||
import com.getcapacitor.PluginCall
|
||||
import com.getcapacitor.PluginMethod
|
||||
import com.getcapacitor.annotation.CapacitorPlugin
|
||||
|
||||
@CapacitorPlugin(name = "StorageManager")
|
||||
class StorageManager : Plugin() {
|
||||
private val TAG = "StorageManager"
|
||||
|
||||
lateinit var mainActivity:MainActivity
|
||||
|
||||
data class MediaFile(val uri: Uri, val name: String, val simplePath: String, val size: Long, val type: String, val isAudio: Boolean) {
|
||||
fun toJSObject() : JSObject {
|
||||
var obj = JSObject()
|
||||
obj.put("uri", this.uri)
|
||||
obj.put("name", this.name)
|
||||
obj.put("simplePath", this.simplePath)
|
||||
obj.put("size", this.size)
|
||||
obj.put("type", this.type)
|
||||
obj.put("isAudio", this.isAudio)
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
data class MediaFolder(val uri: Uri, val name: String, val simplePath: String, val mediaFiles:List<MediaFile>) {
|
||||
fun toJSObject() : JSObject {
|
||||
var obj = JSObject()
|
||||
obj.put("uri", this.uri)
|
||||
obj.put("name", this.name)
|
||||
obj.put("simplePath", this.simplePath)
|
||||
obj.put("files", this.mediaFiles.map { it.toJSObject() })
|
||||
return obj
|
||||
}
|
||||
}
|
||||
|
||||
override fun load() {
|
||||
mainActivity = (activity as MainActivity)
|
||||
|
||||
mainActivity.storage.storageAccessCallback = object : StorageAccessCallback {
|
||||
override fun onRootPathNotSelected(
|
||||
requestCode: Int,
|
||||
rootPath: String,
|
||||
uri: Uri,
|
||||
selectedStorageType: StorageType,
|
||||
expectedStorageType: StorageType
|
||||
) {
|
||||
Log.d(TAG, "STORAGE ACCESS CALLBACK")
|
||||
}
|
||||
|
||||
override fun onCanceledByUser(requestCode: Int) {
|
||||
Log.d(TAG, "STORAGE ACCESS CALLBACK")
|
||||
}
|
||||
|
||||
override fun onExpectedStorageNotSelected(requestCode: Int, selectedFolder: DocumentFile, selectedStorageType: StorageType, expectedBasePath: String, expectedStorageType: StorageType) {
|
||||
Log.d(TAG, "STORAGE ACCESS CALLBACK")
|
||||
}
|
||||
|
||||
override fun onStoragePermissionDenied(requestCode: Int) {
|
||||
Log.d(TAG, "STORAGE ACCESS CALLBACK")
|
||||
}
|
||||
|
||||
override fun onRootPathPermissionGranted(requestCode: Int, root: DocumentFile) {
|
||||
Log.d(TAG, "STORAGE ACCESS CALLBACK")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun selectFolder(call: PluginCall) {
|
||||
mainActivity.storage.folderPickerCallback = object : FolderPickerCallback {
|
||||
override fun onFolderSelected(requestCode: Int, folder: DocumentFile) {
|
||||
Log.d(TAG, "ON FOLDER SELECTED ${folder.uri} ${folder.name}")
|
||||
|
||||
var absolutePath = folder.getAbsolutePath(activity)
|
||||
var storageId = folder.getStorageId(activity)
|
||||
var storageType = folder.getStorageType(activity)
|
||||
var simplePath = folder.getSimplePath(activity)
|
||||
var basePath = folder.getBasePath(activity)
|
||||
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("uri", folder.uri)
|
||||
jsobj.put("absolutePath", absolutePath)
|
||||
jsobj.put("storageId", storageId)
|
||||
jsobj.put("storageType", storageType)
|
||||
jsobj.put("simplePath", simplePath)
|
||||
jsobj.put("basePath", basePath)
|
||||
call.resolve(jsobj)
|
||||
}
|
||||
|
||||
override fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType) {
|
||||
Log.e(TAG, "STORAGE ACCESS DENIED")
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("error", "Access Denied")
|
||||
call.resolve(jsobj)
|
||||
}
|
||||
|
||||
override fun onStoragePermissionDenied(requestCode: Int) {
|
||||
Log.d(TAG, "STORAGE PERMISSION DENIED $requestCode")
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("error", "Permission Denied")
|
||||
call.resolve(jsobj)
|
||||
}
|
||||
}
|
||||
mainActivity.storage.openFolderPicker(6)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
@PluginMethod
|
||||
fun requestStoragePermission(call: PluginCall) {
|
||||
Log.d(TAG, "Request Storage Permissions")
|
||||
mainActivity.storageHelper.requestStorageAccess()
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun checkStoragePermission(call: PluginCall) {
|
||||
var res = false
|
||||
|
||||
if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
|
||||
res = SimpleStorage.hasStoragePermission(context)
|
||||
Log.d(TAG, "Check Storage Access $res")
|
||||
} else {
|
||||
Log.d(TAG, "Has permission on Android 10 or up")
|
||||
res = true
|
||||
}
|
||||
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("value", res)
|
||||
call.resolve(jsobj)
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun checkFolderPermissions(call: PluginCall) {
|
||||
var folderUrl = call.data.getString("folderUrl", "").toString()
|
||||
Log.d(TAG, "Check Folder Permissions for $folderUrl")
|
||||
|
||||
var hasAccess = SimpleStorage.hasStorageAccess(context,folderUrl,true)
|
||||
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("value", hasAccess)
|
||||
call.resolve(jsobj)
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun searchFolder(call: PluginCall) {
|
||||
var folderUrl = call.data.getString("folderUrl", "").toString()
|
||||
Log.d(TAG, "Searching folder $folderUrl")
|
||||
|
||||
var df: DocumentFile = DocumentFileCompat.fromUri(context, Uri.parse(folderUrl))!!
|
||||
Log.d(TAG, "Folder as DF ${df.isDirectory} | ${df.getSimplePath(context)} | ${df.getBasePath(context)} | ${df.name}")
|
||||
|
||||
var mediaFolders = mutableListOf<MediaFolder>()
|
||||
var foldersFound = df.search(false, DocumentFileType.FOLDER)
|
||||
|
||||
foldersFound.forEach {
|
||||
Log.d(TAG, "Iterating over Folder Found ${it.name} | ${it.getSimplePath(context)} | URI: ${it.uri}")
|
||||
var folderName = it.name ?: ""
|
||||
var mediaFiles = mutableListOf<MediaFile>()
|
||||
|
||||
var filesInFolder = it.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
|
||||
filesInFolder.forEach { it2 ->
|
||||
var mimeType = it2?.mimeType ?: ""
|
||||
var filename = it2?.name ?: ""
|
||||
var isAudio = mimeType.startsWith("audio")
|
||||
Log.d(TAG, "Found $mimeType file $filename in folder $folderName")
|
||||
var imageFile = MediaFile(it2.uri, filename, it2.getSimplePath(context), it2.length(), mimeType, isAudio)
|
||||
mediaFiles.add(imageFile)
|
||||
}
|
||||
if (mediaFiles.size > 0) {
|
||||
mediaFolders.add(MediaFolder(it.uri, folderName, it.getSimplePath(context), mediaFiles))
|
||||
}
|
||||
}
|
||||
|
||||
// Files in root dir
|
||||
var rootMediaFiles = mutableListOf<MediaFile>()
|
||||
var mediaFilesFound:List<DocumentFile> = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
|
||||
mediaFilesFound.forEach {
|
||||
Log.d(TAG, "Folder Root File Found ${it.name} | ${it.getSimplePath(context)} | URI: ${it.uri} | ${it.mimeType}")
|
||||
var mimeType = it?.mimeType ?: ""
|
||||
var filename = it?.name ?: ""
|
||||
var isAudio = mimeType.startsWith("audio")
|
||||
Log.d(TAG, "Found $mimeType file $filename in root folder")
|
||||
var imageFile = MediaFile(it.uri, filename, it.getSimplePath(context), it.length(), mimeType, isAudio)
|
||||
rootMediaFiles.add(imageFile)
|
||||
}
|
||||
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("folders", mediaFolders.map{ it.toJSObject() })
|
||||
jsobj.put("files", rootMediaFiles.map{ it.toJSObject() })
|
||||
call.resolve(jsobj)
|
||||
}
|
||||
|
||||
|
||||
@PluginMethod
|
||||
fun delete(call: PluginCall) {
|
||||
var url = call.data.getString("url", "").toString()
|
||||
var coverUrl = call.data.getString("coverUrl", "").toString()
|
||||
var folderUrl = call.data.getString("folderUrl", "").toString()
|
||||
|
||||
if (folderUrl != "") {
|
||||
Log.d(TAG, "CALLED DELETE FOLDER: $folderUrl")
|
||||
var folder = DocumentFileCompat.fromUri(context, Uri.parse(folderUrl))
|
||||
var success = folder?.deleteRecursively(context)
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("success", success)
|
||||
call.resolve()
|
||||
} else {
|
||||
// Older audiobooks did not store a folder url, use cover and audiobook url
|
||||
var abExists = checkUriExists(Uri.parse(url))
|
||||
if (abExists) {
|
||||
var abfile = DocumentFileCompat.fromUri(context, Uri.parse(url))
|
||||
abfile?.delete()
|
||||
}
|
||||
|
||||
var coverExists = checkUriExists(Uri.parse(coverUrl))
|
||||
if (coverExists) {
|
||||
var coverfile = DocumentFileCompat.fromUri(context, Uri.parse(coverUrl))
|
||||
coverfile?.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun checkUriExists(uri: Uri?): Boolean {
|
||||
if (uri == null) return false
|
||||
val resolver = context.contentResolver
|
||||
var cursor: Cursor? = null
|
||||
return try {
|
||||
cursor = resolver.query(uri, null, null, null, null)
|
||||
//cursor null: content Uri was invalid or some other error occurred
|
||||
//cursor.moveToFirst() false: Uri was ok but no entry found.
|
||||
(cursor != null && cursor.moveToFirst())
|
||||
} catch (t: Throwable) {
|
||||
false
|
||||
} finally {
|
||||
try {
|
||||
cursor?.close()
|
||||
} catch (t: Throwable) {
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@
|
|||
<div class="flex-grow" />
|
||||
<!-- <ui-menu :label="username" :items="menuItems" @action="menuAction" class="ml-5" /> -->
|
||||
|
||||
<span class="material-icons cursor-pointer mx-4" @click="$store.commit('downloads/setShowModal', true)">source</span>
|
||||
<span class="material-icons cursor-pointer mx-4" :class="hasDownloadsFolder ? '' : 'text-warning'" @click="$store.commit('downloads/setShowModal', true)">source</span>
|
||||
|
||||
<widgets-connection-icon />
|
||||
|
||||
|
@ -74,6 +74,9 @@ export default {
|
|||
} else {
|
||||
return process.env.IOS_APP_URL
|
||||
}
|
||||
},
|
||||
hasDownloadsFolder() {
|
||||
return !!this.$store.state.downloadFolder
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -3,21 +3,41 @@
|
|||
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
||||
<p class="absolute top-6 left-2 text-2xl">Downloads</p>
|
||||
|
||||
<div class="absolute top-16 left-0 right-0 w-full flex items-center px-2 py-1" :class="hasStoragePermission ? '' : 'text-error'">
|
||||
<div class="absolute top-16 left-0 right-0 w-full px-2 py-1" :class="hasStoragePermission ? '' : 'text-error'">
|
||||
<div class="flex items-center">
|
||||
<span class="material-icons" @click="changeDownloadFolderClick">{{ hasStoragePermission ? 'folder' : 'error' }}</span>
|
||||
<p v-if="hasStoragePermission" class="text-sm px-4" @click="changeDownloadFolderClick">{{ downloadFolderSimplePath || 'No Download Folder Selected' }}</p>
|
||||
<p v-else class="text-sm px-4" @click="changeDownloadFolderClick">No Storage Permissions. Click here</p>
|
||||
</div>
|
||||
<!-- <p v-if="hasStoragePermission" class="text-xs text-gray-400 break-all max-w-full">{{ downloadFolderUri }}</p> -->
|
||||
</div>
|
||||
|
||||
<div v-if="totalSize" class="absolute bottom-0 left-0 right-0 w-full py-3 text-center">
|
||||
<p class="text-sm text-center text-gray-300">Total: {{ $bytesPretty(totalSize) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20 mt-10" style="max-height: 75%" @click.stop>
|
||||
<div v-if="downloadFolder && hasStoragePermission" class="w-full relative mt-10" @click.stop>
|
||||
<div class="w-full h-10 relative">
|
||||
<div class="absolute top-px left-0 z-10 w-full h-full flex">
|
||||
<div class="flex-grow h-full bg-primary rounded-t-md mr-px" @click="showingDownloads = true">
|
||||
<div class="flex items-center justify-center rounded-t-md border-t border-l border-r border-white border-opacity-20 h-full" :class="showingDownloads ? 'text-gray-100' : 'border-b bg-black bg-opacity-20 text-gray-400'">
|
||||
<p>Downloads</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow h-full bg-primary rounded-t-md ml-px" @click="showingDownloads = false">
|
||||
<div class="flex items-center justify-center h-full rounded-t-md border-t border-l border-r border-white border-opacity-20" :class="!showingDownloads ? 'text-gray-100' : 'border-b bg-black bg-opacity-20 text-gray-400'">
|
||||
<p>Files</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-content-body relative w-full overflow-x-hidden overflow-y-auto bg-primary rounded-b-lg border border-white border-opacity-20" style="max-height: 70vh; height: 70vh">
|
||||
<template v-if="showingDownloads">
|
||||
<div v-if="!totalDownloads" class="flex items-center justify-center h-40">
|
||||
<p>No Downloads</p>
|
||||
</div>
|
||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<ul v-else class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<template v-for="download in downloadsDownloading">
|
||||
<li :key="download.id" class="text-gray-400 select-none relative px-4 py-5 border-b border-white border-opacity-10 bg-black bg-opacity-10">
|
||||
<div class="flex items-center justify-center">
|
||||
|
@ -35,46 +55,48 @@
|
|||
</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 w-2/3">
|
||||
<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>
|
||||
<p class="font-normal truncate text-xs text-gray-400">{{ $bytesPretty(download.size) }}</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">
|
||||
<div class="w-3/4">
|
||||
<span class="text-xs text-gray-400">Unknown Audio File</span>
|
||||
<p class="font-normal truncate text-sm">{{ download.filename }}</p>
|
||||
</div>
|
||||
<!-- <span class="font-normal block truncate text-sm pr-2">{{ 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>
|
||||
<modals-downloads-download-item :download="download" @play="playDownload" @delete="clickDeleteDownload" />
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="w-full h-full">
|
||||
<div class="w-full flex justify-around py-4 px-2">
|
||||
<ui-btn small @click="searchFolder">Re-Scan</ui-btn>
|
||||
<ui-btn small @click="changeDownloadFolderClick">Change Folder</ui-btn>
|
||||
<ui-btn small color="error" @click="resetFolder">Reset</ui-btn>
|
||||
</div>
|
||||
<p v-if="isScanning" class="text-center my-8">Scanning Folder..</p>
|
||||
<p v-else-if="!mediaScanResults" class="text-center my-8">No Files Found</p>
|
||||
<template v-else>
|
||||
<template v-for="mediaFolder in mediaScanResults.folders">
|
||||
<div :key="mediaFolder.uri" class="w-full px-2 py-2">
|
||||
<div class="flex items-center">
|
||||
<span class="material-icons text-base text-white text-opacity-50">folder</span>
|
||||
<p class="ml-1 py-0.5">{{ mediaFolder.name }}</p>
|
||||
</div>
|
||||
<div v-for="mediaFile in mediaFolder.files" :key="mediaFile.uri" class="ml-3 flex items-center">
|
||||
<span class="material-icons text-base text-white text-opacity-50">{{ mediaFile.isAudio ? 'music_note' : 'image' }}</span>
|
||||
<p class="ml-1 py-0.5">{{ mediaFile.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-for="mediaFile in mediaScanResults.files">
|
||||
<div :key="mediaFile.uri" class="w-full px-2 py-2">
|
||||
<div class="flex items-center">
|
||||
<span class="material-icons text-base text-white text-opacity-50">{{ mediaFile.isAudio ? 'music_note' : 'image' }}</span>
|
||||
<p class="ml-1 py-0.5">{{ mediaFile.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="list-content-body relative w-full overflow-x-hidden overflow-y-auto bg-primary rounded-b-lg border border-white border-opacity-20 py-8 px-4" @click.stop>
|
||||
<ui-btn class="w-full" color="info" @click="changeDownloadFolderClick">Select Folder</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
|
@ -83,19 +105,21 @@
|
|||
<script>
|
||||
import { Dialog } from '@capacitor/dialog'
|
||||
import AudioDownloader from '@/plugins/audio-downloader'
|
||||
import StorageManager from '@/plugins/storage-manager'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
downloadFolder: null,
|
||||
downloadingProgress: {},
|
||||
totalSize: 0
|
||||
totalSize: 0,
|
||||
showingDownloads: true,
|
||||
isScanning: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async show(newValue) {
|
||||
if (newValue) {
|
||||
this.downloadFolder = await this.$localStore.getDownloadFolder()
|
||||
await this.$localStore.getDownloadFolder()
|
||||
this.setTotalSize()
|
||||
}
|
||||
}
|
||||
|
@ -112,11 +136,17 @@ export default {
|
|||
hasStoragePermission() {
|
||||
return this.$store.state.hasStoragePermission
|
||||
},
|
||||
downloadFolder() {
|
||||
return this.$store.state.downloadFolder
|
||||
},
|
||||
downloadFolderSimplePath() {
|
||||
return this.downloadFolder ? this.downloadFolder.simplePath : null
|
||||
},
|
||||
downloadFolderUri() {
|
||||
return this.downloadFolder ? this.downloadFolder.uri : null
|
||||
},
|
||||
totalDownloads() {
|
||||
return this.downloadsReady.length + this.orphanDownloads.length + this.downloadsDownloading.length
|
||||
return this.downloadsReady.length + this.downloadsDownloading.length
|
||||
},
|
||||
downloadsDownloading() {
|
||||
return this.downloads.filter((d) => d.isDownloading || d.isPreparing)
|
||||
|
@ -124,39 +154,11 @@ export default {
|
|||
downloadsReady() {
|
||||
return this.downloads.filter((d) => !d.isDownloading && !d.isPreparing)
|
||||
},
|
||||
orphanDownloads() {
|
||||
return this.$store.state.downloads.orphanDownloads
|
||||
// return [
|
||||
// {
|
||||
// id: 'asdf',
|
||||
// filename: 'Test Title 1 another long title on the downloading widget.jpg'
|
||||
// }
|
||||
// ]
|
||||
},
|
||||
downloads() {
|
||||
return this.$store.state.downloads.downloads
|
||||
// return [
|
||||
// {
|
||||
// id: 'asdf1',
|
||||
// audiobook: {
|
||||
// book: {
|
||||
// title: 'Test Title 1 another long title on the downloading widget',
|
||||
// author: 'Test Author 1'
|
||||
// }
|
||||
// },
|
||||
// isDownloading: true
|
||||
// },
|
||||
// {
|
||||
// id: 'asdf2',
|
||||
// audiobook: {
|
||||
// book: {
|
||||
// title: 'Test Title 2',
|
||||
// author: 'Test Author 2 long test author to test the overflow capabilities'
|
||||
// }
|
||||
// },
|
||||
// isReady: true
|
||||
// }
|
||||
// ]
|
||||
},
|
||||
mediaScanResults() {
|
||||
return this.$store.state.mediaScanResults
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -170,14 +172,54 @@ export default {
|
|||
async changeDownloadFolderClick() {
|
||||
if (!this.hasStoragePermission) {
|
||||
console.log('Requesting Storage Permission')
|
||||
AudioDownloader.requestStoragePermission()
|
||||
StorageManager.requestStoragePermission()
|
||||
} else {
|
||||
var folderObj = await AudioDownloader.selectFolder()
|
||||
var folderObj = await StorageManager.selectFolder()
|
||||
if (folderObj.error) {
|
||||
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
|
||||
}
|
||||
await this.$localStore.setDownloadFolder(folderObj)
|
||||
|
||||
var permissionsGood = await StorageManager.checkFolderPermissions({ folderUrl: folderObj.uri })
|
||||
console.log('Storage Permission check folder ' + permissionsGood)
|
||||
|
||||
if (!permissionsGood) {
|
||||
this.$toast.error('Folder permissions failed')
|
||||
return
|
||||
} else {
|
||||
this.$toast.success('Folder permission success')
|
||||
}
|
||||
|
||||
await this.$localStore.setDownloadFolder(folderObj)
|
||||
|
||||
this.searchFolder()
|
||||
}
|
||||
},
|
||||
async searchFolder() {
|
||||
this.isScanning = true
|
||||
var response = await StorageManager.searchFolder({ folderUrl: this.downloadFolderUri })
|
||||
var searchResults = response
|
||||
searchResults.folders = JSON.parse(searchResults.folders)
|
||||
searchResults.files = JSON.parse(searchResults.files)
|
||||
|
||||
if (searchResults.folders.length) {
|
||||
console.log('Search results folders length', searchResults.folders.length)
|
||||
|
||||
searchResults.folders = searchResults.folders.map((sr) => {
|
||||
if (sr.files) {
|
||||
sr.files = JSON.parse(sr.files)
|
||||
}
|
||||
return sr
|
||||
})
|
||||
this.$store.commit('setMediaScanResults', searchResults)
|
||||
} else {
|
||||
this.$toast.warning('No audio or image files found')
|
||||
}
|
||||
this.isScanning = false
|
||||
},
|
||||
async resetFolder() {
|
||||
await this.$localStore.setDownloadFolder(null)
|
||||
this.$store.commit('setMediaScanResults', {})
|
||||
this.$toast.info('Unlinked Folder')
|
||||
},
|
||||
updateDownloadProgress({ audiobookId, progress }) {
|
||||
this.$set(this.downloadingProgress, audiobookId, progress)
|
||||
|
@ -186,7 +228,7 @@ export default {
|
|||
this.show = false
|
||||
this.$router.push(`/audiobook/${download.id}`)
|
||||
},
|
||||
async clickDelete(download) {
|
||||
async clickDeleteDownload(download) {
|
||||
const { value } = await Dialog.confirm({
|
||||
title: 'Confirm',
|
||||
message: 'Delete this download?'
|
||||
|
@ -195,12 +237,18 @@ export default {
|
|||
this.$emit('deleteDownload', download)
|
||||
}
|
||||
},
|
||||
clickedOption(download) {
|
||||
console.log('Clicked download', download)
|
||||
this.$emit('selectDownload', download)
|
||||
playDownload(download) {
|
||||
this.$store.commit('setPlayOnLoad', true)
|
||||
this.$store.commit('setPlayingDownload', download)
|
||||
this.show = false
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.list-content-body {
|
||||
max-height: calc(75% - 40px);
|
||||
}
|
||||
</style>
|
52
components/modals/downloads/DownloadItem.vue
Normal file
52
components/modals/downloads/DownloadItem.vue
Normal file
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<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 w-2/3">
|
||||
<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>
|
||||
<p class="font-normal truncate text-xs text-gray-400">{{ $bytesPretty(download.size) }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div v-if="download.isIncomplete || download.isMissing" class="shadow-sm text-warning flex items-center justify-center rounded-full mr-4">
|
||||
<span class="material-icons">error_outline</span>
|
||||
</div>
|
||||
<button v-if="!isMissing" class="shadow-sm text-accent flex items-center justify-center rounded-full" @click.stop="playDownload">
|
||||
<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">
|
||||
<span class="material-icons" style="font-size: 1.2rem">delete</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
download: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
isIncomplete() {
|
||||
return this.download.isIncomplete
|
||||
},
|
||||
isMissing() {
|
||||
return this.download.isMissing
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
playDownload() {
|
||||
this.$emit('play', this.download)
|
||||
},
|
||||
clickDelete() {
|
||||
this.$emit('delete', this.download)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
|
@ -5,7 +5,7 @@
|
|||
<Nuxt />
|
||||
</div>
|
||||
<app-stream-container ref="streamContainer" />
|
||||
<modals-downloads-modal ref="downloadsModal" @selectDownload="selectDownload" @deleteDownload="deleteDownload" />
|
||||
<modals-downloads-modal ref="downloadsModal" @deleteDownload="deleteDownload" />
|
||||
<modals-libraries-modal />
|
||||
<readers-reader />
|
||||
</div>
|
||||
|
@ -17,6 +17,7 @@ import { Network } from '@capacitor/network'
|
|||
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
||||
import AudioDownloader from '@/plugins/audio-downloader'
|
||||
import MyNativeAudio from '@/plugins/my-native-audio'
|
||||
import StorageManager from '@/plugins/storage-manager'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
@ -234,46 +235,100 @@ export default {
|
|||
this.$localStore.setCurrent(null)
|
||||
}
|
||||
},
|
||||
async onMediaLoaded(items) {
|
||||
var jsitems = JSON.parse(items)
|
||||
jsitems = jsitems.map((item) => {
|
||||
return {
|
||||
filename: item.name,
|
||||
size: item.size,
|
||||
contentUrl: item.uri,
|
||||
coverUrl: item.coverUrl || null
|
||||
async searchFolder(downloadFolder) {
|
||||
try {
|
||||
var response = await StorageManager.searchFolder({ folderUrl: downloadFolder.uri })
|
||||
var searchResults = response
|
||||
searchResults.folders = JSON.parse(searchResults.folders)
|
||||
searchResults.files = JSON.parse(searchResults.files)
|
||||
|
||||
console.log('Search folders results length', searchResults.folders.length)
|
||||
searchResults.folders = searchResults.folders.map((sr) => {
|
||||
if (sr.files) {
|
||||
sr.files = JSON.parse(sr.files)
|
||||
}
|
||||
return sr
|
||||
})
|
||||
|
||||
var downloads = await this.$sqlStore.getAllDownloads()
|
||||
return searchResults
|
||||
} catch (error) {
|
||||
console.error('Failed', error)
|
||||
this.$toast.error('Failed to search downloads folder')
|
||||
return {}
|
||||
}
|
||||
},
|
||||
async syncDownloads(downloads, downloadFolder) {
|
||||
console.log('Syncing downloads ' + downloads.length)
|
||||
|
||||
var mediaScanResults = await this.searchFolder(downloadFolder)
|
||||
|
||||
this.$store.commit('setMediaScanResults', mediaScanResults)
|
||||
|
||||
// Filter out media folders without any audio files
|
||||
var mediaFolders = mediaScanResults.folders.filter((sr) => {
|
||||
if (!sr.files) return false
|
||||
var audioFiles = sr.files.filter((mf) => !!mf.isAudio)
|
||||
return audioFiles.length
|
||||
})
|
||||
|
||||
downloads.forEach((download) => {
|
||||
var mediaFolder = mediaFolders.find((mf) => mf.name === download.folderName)
|
||||
if (mediaFolder) {
|
||||
console.log('Found download ' + download.folderName)
|
||||
|
||||
if (download.isPreparing || download.isDownloading) {
|
||||
download.isIncomplete = true
|
||||
download.isPreparing = false
|
||||
download.isDownloading = false
|
||||
}
|
||||
|
||||
for (let i = 0; i < downloads.length; i++) {
|
||||
var download = downloads[i]
|
||||
var jsitem = jsitems.find((item) => item.contentUrl === download.contentUrl)
|
||||
if (!jsitem) {
|
||||
console.error('Removing download was not found', JSON.stringify(download))
|
||||
await this.$sqlStore.removeDownload(download.id)
|
||||
} else if (download.coverUrl && !jsitem.coverUrl) {
|
||||
console.error('Removing cover for download was not found')
|
||||
download.cover = null
|
||||
download.coverUrl = null
|
||||
download.size = jsitem.size || 0
|
||||
this.$store.commit('downloads/addUpdateDownload', download)
|
||||
this.$store.commit('audiobooks/addUpdate', download.audiobook)
|
||||
} else {
|
||||
download.size = jsitem.size || 0
|
||||
console.error('Download not found ' + download.folderName)
|
||||
download.isMissing = true
|
||||
download.isPreparing = false
|
||||
download.isDownloading = false
|
||||
this.$store.commit('downloads/addUpdateDownload', download)
|
||||
this.$store.commit('audiobooks/addUpdate', download.audiobook)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
// async onMediaLoaded(items) {
|
||||
// var jsitems = JSON.parse(items)
|
||||
// jsitems = jsitems.map((item) => {
|
||||
// return {
|
||||
// filename: item.name,
|
||||
// size: item.size,
|
||||
// contentUrl: item.uri,
|
||||
// coverUrl: item.coverUrl || null
|
||||
// }
|
||||
// })
|
||||
|
||||
this.checkLoadCurrent()
|
||||
this.$store.dispatch('audiobooks/setNativeAudiobooks')
|
||||
},
|
||||
selectDownload(download) {
|
||||
this.$store.commit('setPlayOnLoad', true)
|
||||
this.$store.commit('setPlayingDownload', download)
|
||||
},
|
||||
// var downloads = await this.$sqlStore.getAllDownloads()
|
||||
|
||||
// for (let i = 0; i < downloads.length; i++) {
|
||||
// var download = downloads[i]
|
||||
// var jsitem = jsitems.find((item) => item.contentUrl === download.contentUrl)
|
||||
// if (!jsitem) {
|
||||
// console.error('Removing download was not found', JSON.stringify(download))
|
||||
// await this.$sqlStore.removeDownload(download.id)
|
||||
// } else if (download.coverUrl && !jsitem.coverUrl) {
|
||||
// console.error('Removing cover for download was not found')
|
||||
// download.cover = null
|
||||
// download.coverUrl = null
|
||||
// download.size = jsitem.size || 0
|
||||
// this.$store.commit('downloads/addUpdateDownload', download)
|
||||
// this.$store.commit('audiobooks/addUpdate', download.audiobook)
|
||||
// } else {
|
||||
// download.size = jsitem.size || 0
|
||||
// this.$store.commit('downloads/addUpdateDownload', download)
|
||||
// this.$store.commit('audiobooks/addUpdate', download.audiobook)
|
||||
// }
|
||||
// }
|
||||
|
||||
// this.checkLoadCurrent()
|
||||
// this.$store.dispatch('audiobooks/setNativeAudiobooks')
|
||||
// },
|
||||
async deleteDownload(download) {
|
||||
console.log('Delete download', download.filename)
|
||||
|
||||
|
@ -284,83 +339,63 @@ export default {
|
|||
}
|
||||
}
|
||||
if (download.contentUrl) {
|
||||
await AudioDownloader.delete(download)
|
||||
await StorageManager.delete(download)
|
||||
}
|
||||
this.$store.commit('downloads/removeDownload', download)
|
||||
},
|
||||
async onPermissionUpdate(data) {
|
||||
var val = data.value
|
||||
console.log('Permission Update', val)
|
||||
if (val === 'required') {
|
||||
console.log('Permission Required - Making Request')
|
||||
this.$store.commit('setHasStoragePermission', false)
|
||||
} else if (val === 'canceled' || val === 'denied') {
|
||||
console.error('Permission denied by user')
|
||||
this.$store.commit('setHasStoragePermission', false)
|
||||
} else if (val === 'granted') {
|
||||
console.log('User Granted permission')
|
||||
this.$store.commit('setHasStoragePermission', true)
|
||||
|
||||
var folder = data
|
||||
delete folder.value
|
||||
console.log('Setting folder', JSON.stringify(folder))
|
||||
await this.$localStore.setDownloadFolder(folder)
|
||||
} else {
|
||||
console.warn('Other permisiso update', val)
|
||||
}
|
||||
},
|
||||
async initMediaStore() {
|
||||
// Request and setup listeners for media files on native
|
||||
AudioDownloader.addListener('permission', (data) => {
|
||||
this.onPermissionUpdate(data)
|
||||
})
|
||||
AudioDownloader.addListener('onDownloadComplete', (data) => {
|
||||
this.onDownloadComplete(data)
|
||||
})
|
||||
AudioDownloader.addListener('onDownloadFailed', (data) => {
|
||||
this.onDownloadFailed(data)
|
||||
})
|
||||
AudioDownloader.addListener('onMediaLoaded', (data) => {
|
||||
this.onMediaLoaded(data.items)
|
||||
})
|
||||
// AudioDownloader.addListener('onMediaLoaded', (data) => {
|
||||
// this.onMediaLoaded(data.items)
|
||||
// })
|
||||
AudioDownloader.addListener('onDownloadProgress', (data) => {
|
||||
this.onDownloadProgress(data)
|
||||
})
|
||||
|
||||
await this.$localStore.loadUserAudiobooks()
|
||||
await this.$localStore.getDownloadFolder()
|
||||
|
||||
var downloads = await this.$sqlStore.getAllDownloads()
|
||||
var downloadFolder = await this.$localStore.getDownloadFolder()
|
||||
|
||||
if (downloadFolder && downloads.length) {
|
||||
await this.syncDownloads(downloads, downloadFolder)
|
||||
}
|
||||
|
||||
var userSavedSettings = await this.$localStore.getUserSettings()
|
||||
if (userSavedSettings) {
|
||||
this.$store.commit('user/setSettings', userSavedSettings)
|
||||
}
|
||||
|
||||
var downloads = await this.$sqlStore.getAllDownloads()
|
||||
if (downloads.length) {
|
||||
var urls = downloads
|
||||
.map((d) => {
|
||||
return {
|
||||
contentUrl: d.contentUrl,
|
||||
coverUrl: d.coverUrl || '',
|
||||
storageId: d.storageId,
|
||||
basePath: d.basePath,
|
||||
coverBasePath: d.coverBasePath || ''
|
||||
}
|
||||
})
|
||||
.filter((d) => {
|
||||
if (!d.contentUrl) {
|
||||
console.error('Invalid Download no Content URL', JSON.stringify(d))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
// if (downloads.length) {
|
||||
// var urls = downloads
|
||||
// .map((d) => {
|
||||
// return {
|
||||
// contentUrl: d.contentUrl,
|
||||
// coverUrl: d.coverUrl || '',
|
||||
// storageId: d.storageId,
|
||||
// basePath: d.basePath,
|
||||
// coverBasePath: d.coverBasePath || ''
|
||||
// }
|
||||
// })
|
||||
// .filter((d) => {
|
||||
// if (!d.contentUrl) {
|
||||
// console.error('Invalid Download no Content URL', JSON.stringify(d))
|
||||
// return false
|
||||
// }
|
||||
// return true
|
||||
// })
|
||||
// AudioDownloader.load({
|
||||
// audiobookUrls: urls
|
||||
// })
|
||||
// }
|
||||
|
||||
AudioDownloader.load({
|
||||
audiobookUrls: urls
|
||||
})
|
||||
}
|
||||
|
||||
var checkPermission = await AudioDownloader.checkStoragePermission()
|
||||
var checkPermission = await StorageManager.checkStoragePermission()
|
||||
console.log('Storage Permission is' + checkPermission.value)
|
||||
if (!checkPermission.value) {
|
||||
console.log('Will require permissions')
|
||||
|
|
|
@ -41,6 +41,7 @@ export default {
|
|||
'@/plugins/axios.js',
|
||||
'@/plugins/my-native-audio.js',
|
||||
'@/plugins/audio-downloader.js',
|
||||
'@/plugins/storage-manager.js',
|
||||
'@/plugins/toast.js'
|
||||
],
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "audiobookshelf-app",
|
||||
"version": "v0.9.11-beta",
|
||||
"version": "v0.9.12-beta",
|
||||
"author": "advplyr",
|
||||
"scripts": {
|
||||
"dev": "nuxt --hostname localhost --port 1337",
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
import Path from 'path'
|
||||
import { Dialog } from '@capacitor/dialog'
|
||||
import AudioDownloader from '@/plugins/audio-downloader'
|
||||
import StorageManager from '@/plugins/storage-manager'
|
||||
|
||||
export default {
|
||||
async asyncData({ store, params, redirect, app }) {
|
||||
|
@ -266,21 +267,17 @@ export default {
|
|||
}
|
||||
|
||||
if (!this.hasStoragePermission) {
|
||||
await AudioDownloader.requestStoragePermission()
|
||||
this.$store.commit('downloads/setShowModal', true)
|
||||
return
|
||||
}
|
||||
|
||||
// Download Path
|
||||
var dlFolder = this.$localStore.downloadFolder
|
||||
if (!dlFolder) {
|
||||
console.log('No download folder, request from ujser')
|
||||
var folderObj = await AudioDownloader.selectFolder()
|
||||
|
||||
if (folderObj.error) {
|
||||
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
|
||||
}
|
||||
dlFolder = folderObj
|
||||
await this.$localStore.setDownloadFolder(folderObj)
|
||||
console.log('No download folder, request from user')
|
||||
// User to select download folder from download modal to ensure permissions
|
||||
this.$store.commit('downloads/setShowModal', true)
|
||||
return
|
||||
}
|
||||
|
||||
var downloadObject = {
|
||||
|
@ -299,8 +296,11 @@ export default {
|
|||
var track = audiobook.tracks[0]
|
||||
var fileext = track.ext
|
||||
|
||||
console.log('Download Single Track Path: ' + track.path)
|
||||
|
||||
var relTrackPath = track.path.replace('\\', '/').replace(this.audiobook.path.replace('\\', '/'), '')
|
||||
var url = `${this.$store.state.serverUrl}/s/book/${this.audiobookId}/${relTrackPath}?token=${this.userToken}`
|
||||
|
||||
var url = `${this.$store.state.serverUrl}/s/book/${this.audiobookId}${relTrackPath}?token=${this.userToken}`
|
||||
this.startDownload(url, fileext, downloadObject)
|
||||
} else {
|
||||
// Multi-track merge
|
||||
|
|
|
@ -32,7 +32,7 @@ Vue.prototype.$secondsToTimestamp = (seconds) => {
|
|||
_seconds -= _minutes * 60
|
||||
var _hours = Math.floor(_minutes / 60)
|
||||
_minutes -= _hours * 60
|
||||
_seconds = Math.round(_seconds)
|
||||
_seconds = Math.floor(_seconds)
|
||||
if (!_hours) {
|
||||
return `${_minutes}:${_seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
|
5
plugins/storage-manager.js
Normal file
5
plugins/storage-manager.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { registerPlugin } from '@capacitor/core';
|
||||
|
||||
const StorageManager = registerPlugin('StorageManager');
|
||||
|
||||
export default StorageManager;
|
|
@ -441,7 +441,7 @@ class LocalStorage {
|
|||
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)
|
||||
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to load user audiobooks', error)
|
||||
}
|
||||
|
@ -458,7 +458,7 @@ class LocalStorage {
|
|||
async setAllAudiobookProgress(progresses) {
|
||||
this.userAudiobooks = progresses
|
||||
await this.saveUserAudiobooks()
|
||||
this.vuexStore.commit('user/setLocalUserAudiobooks', this.userAudiobooks)
|
||||
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
||||
}
|
||||
|
||||
async updateUserAudiobookProgress(progressPayload) {
|
||||
|
@ -467,14 +467,14 @@ class LocalStorage {
|
|||
}
|
||||
console.log('[LocalStorage] Updated User Audiobook Progress ' + progressPayload.audiobookId)
|
||||
await this.saveUserAudiobooks()
|
||||
this.vuexStore.commit('user/setLocalUserAudiobooks', this.userAudiobooks)
|
||||
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
||||
}
|
||||
|
||||
async removeAudiobookProgress(audiobookId) {
|
||||
if (!this.userAudiobooks[audiobookId]) return
|
||||
delete this.userAudiobooks[audiobookId]
|
||||
await this.saveUserAudiobooks()
|
||||
this.vuexStore.commit('user/setLocalUserAudiobooks', this.userAudiobooks)
|
||||
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
||||
}
|
||||
|
||||
getUserAudiobook(audiobookId) {
|
||||
|
@ -530,10 +530,13 @@ class LocalStorage {
|
|||
if (folderObj) {
|
||||
await Storage.set({ key: 'downloadFolder', value: JSON.stringify(folderObj) })
|
||||
this.downloadFolder = folderObj
|
||||
this.vuexStore.commit('setDownloadFolder', { ...this.downloadFolder })
|
||||
} else {
|
||||
await Storage.remove({ key: 'downloadFolder' })
|
||||
this.downloadFolder = null
|
||||
this.vuexStore.commit('setDownloadFolder', null)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to set download folder', error)
|
||||
}
|
||||
|
@ -544,6 +547,7 @@ class LocalStorage {
|
|||
var _value = (await Storage.get({ key: 'downloadFolder' }) || {}).value || null
|
||||
if (!_value) return null
|
||||
this.downloadFolder = JSON.parse(_value)
|
||||
this.vuexStore.commit('setDownloadFolder', { ...this.downloadFolder })
|
||||
return this.downloadFolder
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get download folder', error)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
export const state = () => ({
|
||||
downloads: [],
|
||||
orphanDownloads: [],
|
||||
showModal: false
|
||||
})
|
||||
|
||||
|
@ -45,24 +44,16 @@ export const mutations = {
|
|||
}
|
||||
},
|
||||
addUpdateDownload(state, download) {
|
||||
var key = download.isOrphan ? 'orphanDownloads' : 'downloads'
|
||||
var index = state[key].findIndex(d => d.id === download.id)
|
||||
var index = state.downloads.findIndex(d => d.id === download.id)
|
||||
if (index >= 0) {
|
||||
state[key].splice(index, 1, download)
|
||||
state.downloads.splice(index, 1, download)
|
||||
} else {
|
||||
state[key].push(download)
|
||||
state.downloads.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') {
|
||||
state.downloads = state.downloads.filter(d => d.id !== download.id)
|
||||
this.$sqlStore.removeDownload(download.id)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,7 +13,9 @@ export const state = () => ({
|
|||
isFirstLoad: true,
|
||||
hasStoragePermission: false,
|
||||
selectedBook: null,
|
||||
showReader: false
|
||||
showReader: false,
|
||||
downloadFolder: null,
|
||||
mediaScanResults: {}
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
|
@ -89,5 +91,11 @@ export const mutations = {
|
|||
},
|
||||
setShowReader(state, val) {
|
||||
state.showReader = val
|
||||
},
|
||||
setDownloadFolder(state, val) {
|
||||
state.downloadFolder = val
|
||||
},
|
||||
setMediaScanResults(state, val) {
|
||||
state.mediaScanResults = val
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue