mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-22 19:54:45 +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"
|
applicationId "com.audiobookshelf.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 27
|
versionCode 28
|
||||||
versionName "0.9.11-beta"
|
versionName "0.9.12-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.
|
||||||
|
|
|
@ -2,28 +2,19 @@ package com.audiobookshelf.app
|
||||||
|
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
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.FileCallback
|
||||||
import com.anggrayudi.storage.callback.FolderPickerCallback
|
|
||||||
import com.anggrayudi.storage.callback.StorageAccessCallback
|
|
||||||
import com.anggrayudi.storage.file.*
|
import com.anggrayudi.storage.file.*
|
||||||
import com.anggrayudi.storage.media.FileDescription
|
import com.anggrayudi.storage.media.FileDescription
|
||||||
|
|
||||||
import com.getcapacitor.JSObject
|
import com.getcapacitor.JSObject
|
||||||
import com.getcapacitor.Plugin
|
import com.getcapacitor.Plugin
|
||||||
import com.getcapacitor.PluginCall
|
import com.getcapacitor.PluginCall
|
||||||
import com.getcapacitor.PluginMethod
|
import com.getcapacitor.PluginMethod
|
||||||
import com.getcapacitor.annotation.CapacitorPlugin
|
import com.getcapacitor.annotation.CapacitorPlugin
|
||||||
import org.json.JSONObject
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,219 +25,117 @@ class AudioDownloader : Plugin() {
|
||||||
lateinit var mainActivity:MainActivity
|
lateinit var mainActivity:MainActivity
|
||||||
lateinit var downloadManager:DownloadManager
|
lateinit var downloadManager:DownloadManager
|
||||||
|
|
||||||
data class AudiobookItem(val uri: Uri, val name: String, val size: Long, val coverUrl: String) {
|
// data class AudiobookItem(val uri: Uri, val name: String, val size: Long, val coverUrl: String) {
|
||||||
fun toJSObject() : JSObject {
|
// fun toJSObject() : JSObject {
|
||||||
var obj = JSObject()
|
// var obj = JSObject()
|
||||||
obj.put("uri", this.uri)
|
// obj.put("uri", this.uri)
|
||||||
obj.put("name", this.name)
|
// obj.put("name", this.name)
|
||||||
obj.put("size", this.size)
|
// obj.put("size", this.size)
|
||||||
obj.put("coverUrl", this.coverUrl)
|
// obj.put("coverUrl", this.coverUrl)
|
||||||
return obj
|
// return obj
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
override fun load() {
|
override fun load() {
|
||||||
mainActivity = (activity as MainActivity)
|
mainActivity = (activity as MainActivity)
|
||||||
downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
// storage = SimpleStorage(mainActivity)
|
|
||||||
|
|
||||||
var recieverEvent : (evt: String, id: Long) -> Unit = { evt: String, id: Long ->
|
var recieverEvent: (evt: String, id: Long) -> Unit = { evt: String, id: Long ->
|
||||||
if (evt == "complete") {}
|
if (evt == "complete") {
|
||||||
|
}
|
||||||
if (evt == "clicked") {
|
if (evt == "clicked") {
|
||||||
Log.d(tag, "Clicked $id back in the audiodownloader")
|
Log.d(tag, "Clicked $id back in the audiodownloader")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mainActivity.registerBroadcastReceiver(recieverEvent)
|
mainActivity.registerBroadcastReceiver(recieverEvent)
|
||||||
|
|
||||||
|
|
||||||
setupSimpleStorage()
|
|
||||||
|
|
||||||
Log.d(tag, "Build SDK ${Build.VERSION.SDK_INT}")
|
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)
|
// @PluginMethod
|
||||||
// var jsobj = JSObject()
|
// fun load(call: PluginCall) {
|
||||||
// jsobj.put("value", "required")
|
// var audiobookUrls = call.data.getJSONArray("audiobookUrls")
|
||||||
// notifyListeners("permission", jsobj)
|
// var len = audiobookUrls?.length()
|
||||||
// } else {
|
// if (len == null) {
|
||||||
// Log.d(tag, "Does not request permission")
|
// 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
|
@PluginMethod
|
||||||
fun download(call: PluginCall) {
|
fun download(call: PluginCall) {
|
||||||
|
@ -399,74 +288,6 @@ class AudioDownloader : Plugin() {
|
||||||
call.resolve(ret)
|
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() {
|
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 val query: DownloadManager.Query = DownloadManager.Query()
|
||||||
private var totalBytes: Int = 0
|
private var totalBytes: Int = 0
|
||||||
|
|
|
@ -43,6 +43,7 @@ class MainActivity : BridgeActivity() {
|
||||||
Log.d(tag, "onCreate")
|
Log.d(tag, "onCreate")
|
||||||
registerPlugin(MyNativeAudio::class.java)
|
registerPlugin(MyNativeAudio::class.java)
|
||||||
registerPlugin(AudioDownloader::class.java)
|
registerPlugin(AudioDownloader::class.java)
|
||||||
|
registerPlugin(StorageManager::class.java)
|
||||||
|
|
||||||
var filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE).apply {
|
var filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE).apply {
|
||||||
addAction(DownloadManager.ACTION_NOTIFICATION_CLICKED)
|
addAction(DownloadManager.ACTION_NOTIFICATION_CLICKED)
|
||||||
|
|
|
@ -263,7 +263,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getSupportedPrepareActions(): Long {
|
override fun getSupportedPrepareActions(): Long {
|
||||||
Log.d(tag, "GET SUPORTED ACITONS")
|
|
||||||
return PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or
|
return PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or
|
||||||
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
|
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
|
||||||
PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH 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" />
|
<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" /> -->
|
||||||
|
|
||||||
<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 />
|
<widgets-connection-icon />
|
||||||
|
|
||||||
|
@ -74,6 +74,9 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
return process.env.IOS_APP_URL
|
return process.env.IOS_APP_URL
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
hasDownloadsFolder() {
|
||||||
|
return !!this.$store.state.downloadFolder
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
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">
|
<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>
|
<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>
|
<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-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>
|
<p v-else class="text-sm px-4" @click="changeDownloadFolderClick">No Storage Permissions. Click here</p>
|
||||||
</div>
|
</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">
|
<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>
|
<p class="text-sm text-center text-gray-300">Total: {{ $bytesPretty(totalSize) }}</p>
|
||||||
</div>
|
</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">
|
<div v-if="!totalDownloads" class="flex items-center justify-center h-40">
|
||||||
<p>No Downloads</p>
|
<p>No Downloads</p>
|
||||||
</div>
|
</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">
|
<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">
|
<li :key="download.id" class="text-gray-400 select-none relative px-4 py-5 border-b border-white border-opacity-10 bg-black bg-opacity-10">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
|
@ -35,46 +55,48 @@
|
||||||
</template>
|
</template>
|
||||||
<template v-for="download in downloadsReady">
|
<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)">
|
<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">
|
<modals-downloads-download-item :download="download" @play="playDownload" @delete="clickDeleteDownload" />
|
||||||
<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>
|
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
</ul>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
|
@ -83,19 +105,21 @@
|
||||||
<script>
|
<script>
|
||||||
import { Dialog } from '@capacitor/dialog'
|
import { Dialog } from '@capacitor/dialog'
|
||||||
import AudioDownloader from '@/plugins/audio-downloader'
|
import AudioDownloader from '@/plugins/audio-downloader'
|
||||||
|
import StorageManager from '@/plugins/storage-manager'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
downloadFolder: null,
|
|
||||||
downloadingProgress: {},
|
downloadingProgress: {},
|
||||||
totalSize: 0
|
totalSize: 0,
|
||||||
|
showingDownloads: true,
|
||||||
|
isScanning: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
async show(newValue) {
|
async show(newValue) {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
this.downloadFolder = await this.$localStore.getDownloadFolder()
|
await this.$localStore.getDownloadFolder()
|
||||||
this.setTotalSize()
|
this.setTotalSize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,11 +136,17 @@ export default {
|
||||||
hasStoragePermission() {
|
hasStoragePermission() {
|
||||||
return this.$store.state.hasStoragePermission
|
return this.$store.state.hasStoragePermission
|
||||||
},
|
},
|
||||||
|
downloadFolder() {
|
||||||
|
return this.$store.state.downloadFolder
|
||||||
|
},
|
||||||
downloadFolderSimplePath() {
|
downloadFolderSimplePath() {
|
||||||
return this.downloadFolder ? this.downloadFolder.simplePath : null
|
return this.downloadFolder ? this.downloadFolder.simplePath : null
|
||||||
},
|
},
|
||||||
|
downloadFolderUri() {
|
||||||
|
return this.downloadFolder ? this.downloadFolder.uri : null
|
||||||
|
},
|
||||||
totalDownloads() {
|
totalDownloads() {
|
||||||
return this.downloadsReady.length + this.orphanDownloads.length + this.downloadsDownloading.length
|
return this.downloadsReady.length + this.downloadsDownloading.length
|
||||||
},
|
},
|
||||||
downloadsDownloading() {
|
downloadsDownloading() {
|
||||||
return this.downloads.filter((d) => d.isDownloading || d.isPreparing)
|
return this.downloads.filter((d) => d.isDownloading || d.isPreparing)
|
||||||
|
@ -124,39 +154,11 @@ export default {
|
||||||
downloadsReady() {
|
downloadsReady() {
|
||||||
return this.downloads.filter((d) => !d.isDownloading && !d.isPreparing)
|
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() {
|
downloads() {
|
||||||
return this.$store.state.downloads.downloads
|
return this.$store.state.downloads.downloads
|
||||||
// return [
|
},
|
||||||
// {
|
mediaScanResults() {
|
||||||
// id: 'asdf1',
|
return this.$store.state.mediaScanResults
|
||||||
// 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
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -170,14 +172,54 @@ export default {
|
||||||
async changeDownloadFolderClick() {
|
async changeDownloadFolderClick() {
|
||||||
if (!this.hasStoragePermission) {
|
if (!this.hasStoragePermission) {
|
||||||
console.log('Requesting Storage Permission')
|
console.log('Requesting Storage Permission')
|
||||||
AudioDownloader.requestStoragePermission()
|
StorageManager.requestStoragePermission()
|
||||||
} else {
|
} else {
|
||||||
var folderObj = await AudioDownloader.selectFolder()
|
var folderObj = await StorageManager.selectFolder()
|
||||||
if (folderObj.error) {
|
if (folderObj.error) {
|
||||||
return this.$toast.error(`Error: ${folderObj.error || 'Unknown 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 }) {
|
updateDownloadProgress({ audiobookId, progress }) {
|
||||||
this.$set(this.downloadingProgress, audiobookId, progress)
|
this.$set(this.downloadingProgress, audiobookId, progress)
|
||||||
|
@ -186,7 +228,7 @@ export default {
|
||||||
this.show = false
|
this.show = false
|
||||||
this.$router.push(`/audiobook/${download.id}`)
|
this.$router.push(`/audiobook/${download.id}`)
|
||||||
},
|
},
|
||||||
async clickDelete(download) {
|
async clickDeleteDownload(download) {
|
||||||
const { value } = await Dialog.confirm({
|
const { value } = await Dialog.confirm({
|
||||||
title: 'Confirm',
|
title: 'Confirm',
|
||||||
message: 'Delete this download?'
|
message: 'Delete this download?'
|
||||||
|
@ -195,12 +237,18 @@ export default {
|
||||||
this.$emit('deleteDownload', download)
|
this.$emit('deleteDownload', download)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clickedOption(download) {
|
playDownload(download) {
|
||||||
console.log('Clicked download', download)
|
this.$store.commit('setPlayOnLoad', true)
|
||||||
this.$emit('selectDownload', download)
|
this.$store.commit('setPlayingDownload', download)
|
||||||
this.show = false
|
this.show = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</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 />
|
<Nuxt />
|
||||||
</div>
|
</div>
|
||||||
<app-stream-container ref="streamContainer" />
|
<app-stream-container ref="streamContainer" />
|
||||||
<modals-downloads-modal ref="downloadsModal" @selectDownload="selectDownload" @deleteDownload="deleteDownload" />
|
<modals-downloads-modal ref="downloadsModal" @deleteDownload="deleteDownload" />
|
||||||
<modals-libraries-modal />
|
<modals-libraries-modal />
|
||||||
<readers-reader />
|
<readers-reader />
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,6 +17,7 @@ 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'
|
import AudioDownloader from '@/plugins/audio-downloader'
|
||||||
import MyNativeAudio from '@/plugins/my-native-audio'
|
import MyNativeAudio from '@/plugins/my-native-audio'
|
||||||
|
import StorageManager from '@/plugins/storage-manager'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
|
@ -234,46 +235,100 @@ export default {
|
||||||
this.$localStore.setCurrent(null)
|
this.$localStore.setCurrent(null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async onMediaLoaded(items) {
|
async searchFolder(downloadFolder) {
|
||||||
var jsitems = JSON.parse(items)
|
try {
|
||||||
jsitems = jsitems.map((item) => {
|
var response = await StorageManager.searchFolder({ folderUrl: downloadFolder.uri })
|
||||||
return {
|
var searchResults = response
|
||||||
filename: item.name,
|
searchResults.folders = JSON.parse(searchResults.folders)
|
||||||
size: item.size,
|
searchResults.files = JSON.parse(searchResults.files)
|
||||||
contentUrl: item.uri,
|
|
||||||
coverUrl: item.coverUrl || null
|
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('downloads/addUpdateDownload', download)
|
||||||
this.$store.commit('audiobooks/addUpdate', download.audiobook)
|
this.$store.commit('audiobooks/addUpdate', download.audiobook)
|
||||||
} else {
|
} 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('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()
|
// var downloads = await this.$sqlStore.getAllDownloads()
|
||||||
this.$store.dispatch('audiobooks/setNativeAudiobooks')
|
|
||||||
},
|
// for (let i = 0; i < downloads.length; i++) {
|
||||||
selectDownload(download) {
|
// var download = downloads[i]
|
||||||
this.$store.commit('setPlayOnLoad', true)
|
// var jsitem = jsitems.find((item) => item.contentUrl === download.contentUrl)
|
||||||
this.$store.commit('setPlayingDownload', download)
|
// 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) {
|
async deleteDownload(download) {
|
||||||
console.log('Delete download', download.filename)
|
console.log('Delete download', download.filename)
|
||||||
|
|
||||||
|
@ -284,83 +339,63 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (download.contentUrl) {
|
if (download.contentUrl) {
|
||||||
await AudioDownloader.delete(download)
|
await StorageManager.delete(download)
|
||||||
}
|
}
|
||||||
this.$store.commit('downloads/removeDownload', 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() {
|
async initMediaStore() {
|
||||||
// Request and setup listeners for media files on native
|
// Request and setup listeners for media files on native
|
||||||
AudioDownloader.addListener('permission', (data) => {
|
|
||||||
this.onPermissionUpdate(data)
|
|
||||||
})
|
|
||||||
AudioDownloader.addListener('onDownloadComplete', (data) => {
|
AudioDownloader.addListener('onDownloadComplete', (data) => {
|
||||||
this.onDownloadComplete(data)
|
this.onDownloadComplete(data)
|
||||||
})
|
})
|
||||||
AudioDownloader.addListener('onDownloadFailed', (data) => {
|
AudioDownloader.addListener('onDownloadFailed', (data) => {
|
||||||
this.onDownloadFailed(data)
|
this.onDownloadFailed(data)
|
||||||
})
|
})
|
||||||
AudioDownloader.addListener('onMediaLoaded', (data) => {
|
// AudioDownloader.addListener('onMediaLoaded', (data) => {
|
||||||
this.onMediaLoaded(data.items)
|
// this.onMediaLoaded(data.items)
|
||||||
})
|
// })
|
||||||
AudioDownloader.addListener('onDownloadProgress', (data) => {
|
AudioDownloader.addListener('onDownloadProgress', (data) => {
|
||||||
this.onDownloadProgress(data)
|
this.onDownloadProgress(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.$localStore.loadUserAudiobooks()
|
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()
|
var userSavedSettings = await this.$localStore.getUserSettings()
|
||||||
if (userSavedSettings) {
|
if (userSavedSettings) {
|
||||||
this.$store.commit('user/setSettings', userSavedSettings)
|
this.$store.commit('user/setSettings', userSavedSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
var downloads = await this.$sqlStore.getAllDownloads()
|
// if (downloads.length) {
|
||||||
if (downloads.length) {
|
// var urls = downloads
|
||||||
var urls = downloads
|
// .map((d) => {
|
||||||
.map((d) => {
|
// return {
|
||||||
return {
|
// contentUrl: d.contentUrl,
|
||||||
contentUrl: d.contentUrl,
|
// coverUrl: d.coverUrl || '',
|
||||||
coverUrl: d.coverUrl || '',
|
// storageId: d.storageId,
|
||||||
storageId: d.storageId,
|
// basePath: d.basePath,
|
||||||
basePath: d.basePath,
|
// coverBasePath: d.coverBasePath || ''
|
||||||
coverBasePath: d.coverBasePath || ''
|
// }
|
||||||
}
|
// })
|
||||||
})
|
// .filter((d) => {
|
||||||
.filter((d) => {
|
// if (!d.contentUrl) {
|
||||||
if (!d.contentUrl) {
|
// console.error('Invalid Download no Content URL', JSON.stringify(d))
|
||||||
console.error('Invalid Download no Content URL', JSON.stringify(d))
|
// return false
|
||||||
return false
|
// }
|
||||||
}
|
// return true
|
||||||
return true
|
// })
|
||||||
})
|
// AudioDownloader.load({
|
||||||
|
// audiobookUrls: urls
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
AudioDownloader.load({
|
var checkPermission = await StorageManager.checkStoragePermission()
|
||||||
audiobookUrls: urls
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var checkPermission = await AudioDownloader.checkStoragePermission()
|
|
||||||
console.log('Storage Permission is' + checkPermission.value)
|
console.log('Storage Permission is' + checkPermission.value)
|
||||||
if (!checkPermission.value) {
|
if (!checkPermission.value) {
|
||||||
console.log('Will require permissions')
|
console.log('Will require permissions')
|
||||||
|
|
|
@ -41,6 +41,7 @@ export default {
|
||||||
'@/plugins/axios.js',
|
'@/plugins/axios.js',
|
||||||
'@/plugins/my-native-audio.js',
|
'@/plugins/my-native-audio.js',
|
||||||
'@/plugins/audio-downloader.js',
|
'@/plugins/audio-downloader.js',
|
||||||
|
'@/plugins/storage-manager.js',
|
||||||
'@/plugins/toast.js'
|
'@/plugins/toast.js'
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "v0.9.11-beta",
|
"version": "v0.9.12-beta",
|
||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt --hostname localhost --port 1337",
|
"dev": "nuxt --hostname localhost --port 1337",
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
import Path from 'path'
|
import Path from 'path'
|
||||||
import { Dialog } from '@capacitor/dialog'
|
import { Dialog } from '@capacitor/dialog'
|
||||||
import AudioDownloader from '@/plugins/audio-downloader'
|
import AudioDownloader from '@/plugins/audio-downloader'
|
||||||
|
import StorageManager from '@/plugins/storage-manager'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ store, params, redirect, app }) {
|
async asyncData({ store, params, redirect, app }) {
|
||||||
|
@ -266,21 +267,17 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.hasStoragePermission) {
|
if (!this.hasStoragePermission) {
|
||||||
await AudioDownloader.requestStoragePermission()
|
this.$store.commit('downloads/setShowModal', true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download Path
|
// Download Path
|
||||||
var dlFolder = this.$localStore.downloadFolder
|
var dlFolder = this.$localStore.downloadFolder
|
||||||
if (!dlFolder) {
|
if (!dlFolder) {
|
||||||
console.log('No download folder, request from ujser')
|
console.log('No download folder, request from user')
|
||||||
var folderObj = await AudioDownloader.selectFolder()
|
// User to select download folder from download modal to ensure permissions
|
||||||
|
this.$store.commit('downloads/setShowModal', true)
|
||||||
if (folderObj.error) {
|
return
|
||||||
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
|
|
||||||
}
|
|
||||||
dlFolder = folderObj
|
|
||||||
await this.$localStore.setDownloadFolder(folderObj)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var downloadObject = {
|
var downloadObject = {
|
||||||
|
@ -299,8 +296,11 @@ export default {
|
||||||
var track = audiobook.tracks[0]
|
var track = audiobook.tracks[0]
|
||||||
var fileext = track.ext
|
var fileext = track.ext
|
||||||
|
|
||||||
|
console.log('Download Single Track Path: ' + track.path)
|
||||||
|
|
||||||
var relTrackPath = track.path.replace('\\', '/').replace(this.audiobook.path.replace('\\', '/'), '')
|
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)
|
this.startDownload(url, fileext, downloadObject)
|
||||||
} else {
|
} else {
|
||||||
// Multi-track merge
|
// Multi-track merge
|
||||||
|
|
|
@ -32,7 +32,7 @@ Vue.prototype.$secondsToTimestamp = (seconds) => {
|
||||||
_seconds -= _minutes * 60
|
_seconds -= _minutes * 60
|
||||||
var _hours = Math.floor(_minutes / 60)
|
var _hours = Math.floor(_minutes / 60)
|
||||||
_minutes -= _hours * 60
|
_minutes -= _hours * 60
|
||||||
_seconds = Math.round(_seconds)
|
_seconds = Math.floor(_seconds)
|
||||||
if (!_hours) {
|
if (!_hours) {
|
||||||
return `${_minutes}:${_seconds.toString().padStart(2, '0')}`
|
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
|
var val = (await Storage.get({ key: 'userAudiobooks' }) || {}).value || null
|
||||||
this.userAudiobooks = val ? JSON.parse(val) : {}
|
this.userAudiobooks = val ? JSON.parse(val) : {}
|
||||||
this.userAudiobooksLoaded = true
|
this.userAudiobooksLoaded = true
|
||||||
this.vuexStore.commit('user/setLocalUserAudiobooks', this.userAudiobooks)
|
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LocalStorage] Failed to load user audiobooks', error)
|
console.error('[LocalStorage] Failed to load user audiobooks', error)
|
||||||
}
|
}
|
||||||
|
@ -458,7 +458,7 @@ class LocalStorage {
|
||||||
async setAllAudiobookProgress(progresses) {
|
async setAllAudiobookProgress(progresses) {
|
||||||
this.userAudiobooks = progresses
|
this.userAudiobooks = progresses
|
||||||
await this.saveUserAudiobooks()
|
await this.saveUserAudiobooks()
|
||||||
this.vuexStore.commit('user/setLocalUserAudiobooks', this.userAudiobooks)
|
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUserAudiobookProgress(progressPayload) {
|
async updateUserAudiobookProgress(progressPayload) {
|
||||||
|
@ -467,14 +467,14 @@ class LocalStorage {
|
||||||
}
|
}
|
||||||
console.log('[LocalStorage] Updated User Audiobook Progress ' + progressPayload.audiobookId)
|
console.log('[LocalStorage] Updated User Audiobook Progress ' + progressPayload.audiobookId)
|
||||||
await this.saveUserAudiobooks()
|
await this.saveUserAudiobooks()
|
||||||
this.vuexStore.commit('user/setLocalUserAudiobooks', this.userAudiobooks)
|
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeAudiobookProgress(audiobookId) {
|
async removeAudiobookProgress(audiobookId) {
|
||||||
if (!this.userAudiobooks[audiobookId]) return
|
if (!this.userAudiobooks[audiobookId]) return
|
||||||
delete this.userAudiobooks[audiobookId]
|
delete this.userAudiobooks[audiobookId]
|
||||||
await this.saveUserAudiobooks()
|
await this.saveUserAudiobooks()
|
||||||
this.vuexStore.commit('user/setLocalUserAudiobooks', this.userAudiobooks)
|
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserAudiobook(audiobookId) {
|
getUserAudiobook(audiobookId) {
|
||||||
|
@ -530,10 +530,13 @@ class LocalStorage {
|
||||||
if (folderObj) {
|
if (folderObj) {
|
||||||
await Storage.set({ key: 'downloadFolder', value: JSON.stringify(folderObj) })
|
await Storage.set({ key: 'downloadFolder', value: JSON.stringify(folderObj) })
|
||||||
this.downloadFolder = folderObj
|
this.downloadFolder = folderObj
|
||||||
|
this.vuexStore.commit('setDownloadFolder', { ...this.downloadFolder })
|
||||||
} else {
|
} else {
|
||||||
await Storage.remove({ key: 'downloadFolder' })
|
await Storage.remove({ key: 'downloadFolder' })
|
||||||
this.downloadFolder = null
|
this.downloadFolder = null
|
||||||
|
this.vuexStore.commit('setDownloadFolder', null)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LocalStorage] Failed to set download folder', 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
|
var _value = (await Storage.get({ key: 'downloadFolder' }) || {}).value || null
|
||||||
if (!_value) return null
|
if (!_value) return null
|
||||||
this.downloadFolder = JSON.parse(_value)
|
this.downloadFolder = JSON.parse(_value)
|
||||||
|
this.vuexStore.commit('setDownloadFolder', { ...this.downloadFolder })
|
||||||
return this.downloadFolder
|
return this.downloadFolder
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LocalStorage] Failed to get download folder', error)
|
console.error('[LocalStorage] Failed to get download folder', error)
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
downloads: [],
|
downloads: [],
|
||||||
orphanDownloads: [],
|
|
||||||
showModal: false
|
showModal: false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -45,24 +44,16 @@ export const mutations = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addUpdateDownload(state, download) {
|
addUpdateDownload(state, download) {
|
||||||
var key = download.isOrphan ? 'orphanDownloads' : 'downloads'
|
var index = state.downloads.findIndex(d => d.id === download.id)
|
||||||
var index = state[key].findIndex(d => d.id === download.id)
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
state[key].splice(index, 1, download)
|
state.downloads.splice(index, 1, download)
|
||||||
} else {
|
} else {
|
||||||
state[key].push(download)
|
state.downloads.push(download)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === 'downloads') {
|
|
||||||
this.$sqlStore.setDownload(download)
|
this.$sqlStore.setDownload(download)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
removeDownload(state, download) {
|
removeDownload(state, download) {
|
||||||
var key = download.isOrphan ? 'orphanDownloads' : 'downloads'
|
state.downloads = state.downloads.filter(d => d.id !== download.id)
|
||||||
state[key] = state[key].filter(d => d.id !== download.id)
|
|
||||||
|
|
||||||
if (key === 'downloads') {
|
|
||||||
this.$sqlStore.removeDownload(download.id)
|
this.$sqlStore.removeDownload(download.id)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -13,7 +13,9 @@ export const state = () => ({
|
||||||
isFirstLoad: true,
|
isFirstLoad: true,
|
||||||
hasStoragePermission: false,
|
hasStoragePermission: false,
|
||||||
selectedBook: null,
|
selectedBook: null,
|
||||||
showReader: false
|
showReader: false,
|
||||||
|
downloadFolder: null,
|
||||||
|
mediaScanResults: {}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
|
@ -89,5 +91,11 @@ export const mutations = {
|
||||||
},
|
},
|
||||||
setShowReader(state, val) {
|
setShowReader(state, val) {
|
||||||
state.showReader = 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