From ebf628315ca4d2192e859a572440427f6093b9d3 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 28 Oct 2021 09:37:31 -0500 Subject: [PATCH] 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 --- android/app/build.gradle | 4 +- .../com/audiobookshelf/app/AudioDownloader.kt | 375 +++++------------- .../com/audiobookshelf/app/MainActivity.kt | 1 + .../app/PlayerNotificationService.kt | 1 - .../com/audiobookshelf/app/StorageManager.kt | 257 ++++++++++++ components/app/Appbar.vue | 5 +- components/modals/DownloadsModal.vue | 254 +++++++----- components/modals/downloads/DownloadItem.vue | 52 +++ layouts/default.vue | 207 ++++++---- nuxt.config.js | 1 + package.json | 2 +- pages/audiobook/_id/index.vue | 20 +- plugins/init.client.js | 2 +- plugins/storage-manager.js | 5 + plugins/store.js | 12 +- store/downloads.js | 21 +- store/index.js | 10 +- 17 files changed, 727 insertions(+), 502 deletions(-) create mode 100644 android/app/src/main/java/com/audiobookshelf/app/StorageManager.kt create mode 100644 components/modals/downloads/DownloadItem.vue create mode 100644 plugins/storage-manager.js diff --git a/android/app/build.gradle b/android/app/build.gradle index f5c79f1d..378e2149 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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. diff --git a/android/app/src/main/java/com/audiobookshelf/app/AudioDownloader.kt b/android/app/src/main/java/com/audiobookshelf/app/AudioDownloader.kt index 6f6d25c1..83107ae0 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/AudioDownloader.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/AudioDownloader.kt @@ -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 // } - } - - 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 = 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 = audiobookItems.map{ it.toJSObject() } - var mediaItemNoticePayload = JSObject() - mediaItemNoticePayload.put("items", audiobookObjs) - notifyListeners("onMediaLoaded", mediaItemNoticePayload) - } +// Log.d(tag, "CALLED LOAD $len") +// var audiobookItems:MutableList = 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 = 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 diff --git a/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt b/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt index cae74254..a7ed975f 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt @@ -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) diff --git a/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt index c04975ab..7a5101bd 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt @@ -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 diff --git a/android/app/src/main/java/com/audiobookshelf/app/StorageManager.kt b/android/app/src/main/java/com/audiobookshelf/app/StorageManager.kt new file mode 100644 index 00000000..8edb6269 --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/StorageManager.kt @@ -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) { + 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() + 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() + + 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() + var mediaFilesFound:List = 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 + } + } +} diff --git a/components/app/Appbar.vue b/components/app/Appbar.vue index 518aa2a9..ab87d86a 100644 --- a/components/app/Appbar.vue +++ b/components/app/Appbar.vue @@ -19,7 +19,7 @@
- source + source @@ -74,6 +74,9 @@ export default { } else { return process.env.IOS_APP_URL } + }, + hasDownloadsFolder() { + return !!this.$store.state.downloadFolder } }, methods: { diff --git a/components/modals/DownloadsModal.vue b/components/modals/DownloadsModal.vue index 1b2d3c2f..4ba2a04c 100644 --- a/components/modals/DownloadsModal.vue +++ b/components/modals/DownloadsModal.vue @@ -3,78 +3,100 @@

Downloads

-
- {{ hasStoragePermission ? 'folder' : 'error' }} -

{{ downloadFolderSimplePath || 'No Download Folder Selected' }}

-

No Storage Permissions. Click here

+
+
+ {{ hasStoragePermission ? 'folder' : 'error' }} +

{{ downloadFolderSimplePath || 'No Download Folder Selected' }}

+

No Storage Permissions. Click here

+
+

Total: {{ $bytesPretty(totalSize) }}

-
-
-

No Downloads

+
+
+
+
+
+

Downloads

+
+
+
+
+

Files

+
+
+
-
    -