mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-05 11:34:50 +02:00
Support for android 8+, selectable download location, permissions check, recursive delete
This commit is contained in:
parent
b4ffe0fc83
commit
e124d3858f
23 changed files with 798 additions and 387 deletions
|
@ -36,7 +36,7 @@ class Server extends EventEmitter {
|
|||
this.user = user
|
||||
this.store.commit('user/setUser', user)
|
||||
if (user) {
|
||||
this.store.commit('user/setSettings', user.settings)
|
||||
// this.store.commit('user/setSettings', user.settings)
|
||||
Storage.set({ key: 'token', value: user.token })
|
||||
} else {
|
||||
Storage.remove({ key: 'token' })
|
||||
|
|
|
@ -5,13 +5,16 @@ plugins {
|
|||
}
|
||||
|
||||
android {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = ['-Xjvm-default=all']
|
||||
}
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
defaultConfig {
|
||||
applicationId "com.audiobookshelf.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 13
|
||||
versionName "0.8.4-beta"
|
||||
versionCode 15
|
||||
versionName "0.9.0-beta"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
@ -35,6 +38,7 @@ repositories {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation "com.anggrayudi:storage:0.13.0"
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||
implementation project(':capacitor-android')
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
<!-- Permissions -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />-->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
@ -14,7 +17,8 @@
|
|||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true">
|
||||
android:usesCleartextTraffic="true"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
|
|
|
@ -1,18 +1,32 @@
|
|||
package com.audiobookshelf.app
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.ContentUris
|
||||
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
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
||||
@CapacitorPlugin(name = "AudioDownloader")
|
||||
|
@ -22,281 +36,415 @@ class AudioDownloader : Plugin() {
|
|||
lateinit var mainActivity:MainActivity
|
||||
lateinit var downloadManager:DownloadManager
|
||||
|
||||
data class AudiobookDownload(val url: String, val filename: String, val downloadId: Long)
|
||||
var downloads:MutableList<AudiobookDownload> = mutableListOf()
|
||||
|
||||
data class CoverItem(val name: String, val coverUrl: String)
|
||||
data class AudiobookItem(val id: Long, val uri: Uri, val name: String, val size: Int, val duration: Int, val coverUrl: String) {
|
||||
data class AudiobookItem(val uri: Uri, val name: String, val size: Long, val coverUrl: String) {
|
||||
fun toJSObject() : JSObject {
|
||||
var obj = JSObject()
|
||||
obj.put("id", this.id)
|
||||
obj.put("uri", this.uri)
|
||||
obj.put("name", this.name)
|
||||
obj.put("size", this.size)
|
||||
obj.put("duration", this.duration)
|
||||
obj.put("coverUrl", this.coverUrl)
|
||||
return obj
|
||||
}
|
||||
}
|
||||
var audiobookItems:MutableList<AudiobookItem> = mutableListOf()
|
||||
|
||||
override fun load() {
|
||||
mainActivity = (activity as MainActivity)
|
||||
downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
// storage = SimpleStorage(mainActivity)
|
||||
|
||||
var recieverEvent : (evt: String, id: Long) -> Unit = { evt: String, id: Long ->
|
||||
Log.d(tag, "RECEIVE EVT $evt $id")
|
||||
if (evt == "complete") {
|
||||
var path = downloadManager.getUriForDownloadedFile(id)
|
||||
|
||||
var download = downloads.find { it.downloadId == id }
|
||||
var filename = download?.filename
|
||||
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("downloadId", id)
|
||||
jsobj.put("contentUrl", path)
|
||||
jsobj.put("filename", filename)
|
||||
notifyListeners("onDownloadComplete", jsobj)
|
||||
downloads = downloads.filter { it.downloadId != id } as MutableList<AudiobookDownload>
|
||||
}
|
||||
if (evt == "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")
|
||||
// }
|
||||
}
|
||||
|
||||
fun loadAudiobooks() {
|
||||
var covers = loadCovers()
|
||||
|
||||
val projection = arrayOf(
|
||||
MediaStore.Audio.Media._ID,
|
||||
MediaStore.Audio.Media.DISPLAY_NAME,
|
||||
MediaStore.Audio.Media.DURATION,
|
||||
MediaStore.Audio.Media.SIZE,
|
||||
MediaStore.Audio.Media.IS_AUDIOBOOK,
|
||||
MediaStore.Audio.Media.RELATIVE_PATH
|
||||
)
|
||||
|
||||
var _audiobookItems:MutableList<AudiobookItem> = mutableListOf()
|
||||
val selection = "${MediaStore.Audio.Media.IS_AUDIOBOOK} == ?"
|
||||
val selectionArgs = arrayOf("1")
|
||||
val sortOrder = "${MediaStore.Audio.Media.DISPLAY_NAME} ASC"
|
||||
|
||||
activity.applicationContext.contentResolver.query(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||
projection,
|
||||
selection,
|
||||
selectionArgs,
|
||||
sortOrder
|
||||
)?.use { cursor ->
|
||||
|
||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
|
||||
val nameColumn =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)
|
||||
val durationColumn =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
|
||||
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)
|
||||
val isAudiobookColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.IS_AUDIOBOOK)
|
||||
var relativePathColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.RELATIVE_PATH)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idColumn)
|
||||
val name = cursor.getString(nameColumn)
|
||||
val duration = cursor.getInt(durationColumn)
|
||||
val size = cursor.getInt(sizeColumn)
|
||||
var isAudiobook = cursor.getInt(isAudiobookColumn)
|
||||
var relativePath = cursor.getString(relativePathColumn)
|
||||
|
||||
if (isAudiobook == 1) {
|
||||
val contentUri: Uri = ContentUris.withAppendedId(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||
id
|
||||
)
|
||||
|
||||
Log.d(tag, "Got Content FRom MEdia STORE $id $contentUri, Name: $name, Dur: $duration, Size: $size, relativePath: $relativePath")
|
||||
var audiobookId = File(name).nameWithoutExtension
|
||||
var coverItem:CoverItem? = covers.find{it.name == audiobookId}
|
||||
var coverUrl = coverItem?.coverUrl ?: ""
|
||||
|
||||
_audiobookItems.add(AudiobookItem(id, contentUri, name, duration, size, coverUrl))
|
||||
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")
|
||||
}
|
||||
audiobookItems = _audiobookItems
|
||||
|
||||
var audiobookObjs:List<JSObject> = _audiobookItems.map{ it.toJSObject() }
|
||||
override fun onCanceledByUser(requestCode: Int) {
|
||||
Log.d(tag, "STORAGE ACCESS CALLBACK")
|
||||
}
|
||||
|
||||
var mediaItemNoticePayload = JSObject()
|
||||
mediaItemNoticePayload.put("items", audiobookObjs)
|
||||
notifyListeners("onMediaLoaded", mediaItemNoticePayload)
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadCovers() : MutableList<CoverItem> {
|
||||
val projection = arrayOf(
|
||||
MediaStore.Images.Media._ID,
|
||||
MediaStore.Images.Media.DISPLAY_NAME
|
||||
)
|
||||
val sortOrder = "${MediaStore.Images.Media.DISPLAY_NAME} ASC"
|
||||
|
||||
var coverItems:MutableList<CoverItem> = mutableListOf()
|
||||
|
||||
activity.applicationContext.contentResolver.query(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
projection,
|
||||
null,
|
||||
null,
|
||||
sortOrder
|
||||
)?.use { cursor ->
|
||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
|
||||
val nameColumn =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idColumn)
|
||||
val filename = cursor.getString(nameColumn)
|
||||
val contentUri: Uri = ContentUris.withAppendedId(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
id
|
||||
)
|
||||
|
||||
var name = File(filename).nameWithoutExtension
|
||||
Log.d(tag, "Got IMAGE FRom Media STORE $id $contentUri, Name: $name")
|
||||
|
||||
var coverItem = CoverItem(name, contentUri.toString())
|
||||
coverItems.add(coverItem)
|
||||
}
|
||||
}
|
||||
return coverItems
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
@PluginMethod
|
||||
fun load(call: PluginCall) {
|
||||
loadAudiobooks()
|
||||
fun requestStoragePermission(call: PluginCall) {
|
||||
Log.d(tag, "Request Storage Permissions")
|
||||
mainActivity.storageHelper.requestStorageAccess()
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun downloadCover(call: PluginCall) {
|
||||
var audiobookId = call.data.getString("audiobookId", "audiobook").toString()
|
||||
var url = call.data.getString("downloadUrl", "unknown").toString()
|
||||
var title = call.data.getString("title", "Cover").toString()
|
||||
var filename = call.data.getString("filename", "audiobook.jpg").toString()
|
||||
fun checkStoragePermission(call: PluginCall) {
|
||||
var res = false
|
||||
|
||||
Log.d(tag, "Called download cover: $url")
|
||||
|
||||
var dlRequest = DownloadManager.Request(Uri.parse(url))
|
||||
dlRequest.setTitle("Cover Art: $title")
|
||||
dlRequest.setDescription("Cover art for audiobook")
|
||||
dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION)
|
||||
|
||||
var file:File = File(audiobookId, filename)
|
||||
Log.d(tag, "FILE ${file.path} | ${file.canonicalPath}")
|
||||
dlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_AUDIOBOOKS, file.path)
|
||||
var downloadId = downloadManager.enqueue(dlRequest)
|
||||
|
||||
var progressReceiver : (prog: Long) -> Unit = { prog: Long ->
|
||||
//
|
||||
}
|
||||
|
||||
var doneReceiver : (success: Boolean) -> Unit = { success: Boolean ->
|
||||
var jsobj = JSObject()
|
||||
if (success) {
|
||||
var path = downloadManager.getUriForDownloadedFile(downloadId)
|
||||
jsobj.put("url", path)
|
||||
call.resolve(jsobj)
|
||||
if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
|
||||
res = SimpleStorage.hasStoragePermission(context)
|
||||
Log.d(tag, "Check Storage Access $res")
|
||||
} else {
|
||||
jsobj.put("failed", true)
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
var progressUpdater = DownloadProgressUpdater(downloadManager, downloadId, progressReceiver, doneReceiver)
|
||||
progressUpdater.run()
|
||||
if (audiobookFile == null) {
|
||||
Log.e(tag, "Audiobook was not found $audiobookUrl")
|
||||
} else {
|
||||
var _name = audiobookFile.name
|
||||
if (_name == null) _name = ""
|
||||
var _coverUrl = ""
|
||||
if (coverFile != null) _coverUrl = coverFile.uri.toString()
|
||||
|
||||
var size = audiobookFile.length()
|
||||
var abItem = AudiobookItem(audiobookFile.uri, _name, size, _coverUrl)
|
||||
audiobookItems.add(abItem)
|
||||
}
|
||||
}
|
||||
|
||||
var audiobookObjs:List<JSObject> = audiobookItems.map{ it.toJSObject() }
|
||||
var mediaItemNoticePayload = JSObject()
|
||||
mediaItemNoticePayload.put("items", audiobookObjs)
|
||||
notifyListeners("onMediaLoaded", mediaItemNoticePayload)
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun download(call: PluginCall) {
|
||||
var audiobookId = call.data.getString("audiobookId", "audiobook").toString()
|
||||
var url = call.data.getString("downloadUrl", "unknown").toString()
|
||||
var coverDownloadUrl = call.data.getString("coverDownloadUrl", "").toString()
|
||||
var title = call.data.getString("title", "Audiobook").toString()
|
||||
var filename = call.data.getString("filename", "audiobook.mp3").toString()
|
||||
var coverFilename = call.data.getString("coverFilename", "cover.png").toString()
|
||||
var downloadFolderUrl = call.data.getString("downloadFolderUrl", "").toString()
|
||||
var folder = DocumentFileCompat.fromUri(context, Uri.parse(downloadFolderUrl))!!
|
||||
Log.d(tag, "Called download: $url | Folder: ${folder.name} | $downloadFolderUrl")
|
||||
|
||||
Log.d(tag, "Called download: $url")
|
||||
var dlfilename = audiobookId + "." + File(filename).extension
|
||||
var coverdlfilename = audiobookId + "." + File(coverFilename).extension
|
||||
|
||||
var canWriteToFolder = folder.canWrite()
|
||||
if (!canWriteToFolder) {
|
||||
Log.e(tag, "Error Cannot Write to Folder ${folder.baseName}")
|
||||
val ret = JSObject()
|
||||
ret.put("error", "Cannot write to ${folder.baseName}")
|
||||
call.resolve(ret)
|
||||
return
|
||||
}
|
||||
|
||||
var dlRequest = DownloadManager.Request(Uri.parse(url))
|
||||
dlRequest.setTitle(title)
|
||||
dlRequest.setDescription("Downloading to Audiobooks directory")
|
||||
dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION)
|
||||
dlRequest.setDescription("Downloading to ${folder.name}")
|
||||
dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
dlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, dlfilename)
|
||||
|
||||
var file:File = File(audiobookId, filename)
|
||||
Log.d(tag, "FILE ${file.path} | ${file.canonicalPath}")
|
||||
dlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_AUDIOBOOKS, file.path)
|
||||
var audiobookDownloadId = downloadManager.enqueue(dlRequest)
|
||||
var coverDownloadId:Long? = null
|
||||
|
||||
var downloadId = downloadManager.enqueue(dlRequest)
|
||||
if (coverDownloadUrl != "") {
|
||||
var coverDlRequest = DownloadManager.Request(Uri.parse(coverDownloadUrl))
|
||||
coverDlRequest.setTitle("Cover: $title")
|
||||
coverDlRequest.setDescription("Downloading to ${folder.name}")
|
||||
coverDlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION)
|
||||
coverDlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, coverdlfilename)
|
||||
coverDownloadId = downloadManager.enqueue(coverDlRequest)
|
||||
}
|
||||
|
||||
var download = AudiobookDownload(url, filename, downloadId)
|
||||
downloads.add(download)
|
||||
|
||||
var progressReceiver : (prog: Long) -> Unit = { prog: Long ->
|
||||
var progressReceiver : (id:Long, prog: Long) -> Unit = { id:Long, prog: Long ->
|
||||
if (id == audiobookDownloadId) {
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("filename", filename)
|
||||
jsobj.put("downloadId", downloadId)
|
||||
jsobj.put("audiobookId", audiobookId)
|
||||
jsobj.put("progress", prog)
|
||||
notifyListeners("onDownloadProgress", jsobj)
|
||||
}
|
||||
|
||||
var doneReceiver : (success: Boolean) -> Unit = { success: Boolean ->
|
||||
Log.d(tag, "RECIEVER DONE, SUCCES? $success")
|
||||
}
|
||||
|
||||
var progressUpdater = DownloadProgressUpdater(downloadManager, downloadId, progressReceiver, doneReceiver)
|
||||
var coverDocFile:DocumentFile? = null
|
||||
|
||||
var doneReceiver : (id:Long, success: Boolean) -> Unit = { id:Long, success: Boolean ->
|
||||
Log.d(tag, "RECEIVER DONE $id, SUCCES? $success")
|
||||
var docfile:DocumentFile? = null
|
||||
if (id == coverDownloadId) {
|
||||
docfile = DocumentFileCompat.fromPublicFolder(context, PublicDirectory.DOWNLOADS, coverdlfilename)
|
||||
|
||||
Log.d(tag, "Move Cover File ${docfile?.name}")
|
||||
} else if (id == audiobookDownloadId) {
|
||||
docfile = DocumentFileCompat.fromPublicFolder(context, PublicDirectory.DOWNLOADS, dlfilename)
|
||||
Log.d(tag, "Move Audiobook File ${docfile?.name}")
|
||||
}
|
||||
|
||||
var callback = object : FileCallback() {
|
||||
override fun onPrepare() {
|
||||
Log.d(tag, "PREPARING MOVE FILE")
|
||||
}
|
||||
override fun onFailed(errorCode:ErrorCode) {
|
||||
Log.e(tag, "FAILED MOVE FILE $errorCode")
|
||||
docfile?.delete()
|
||||
coverDocFile?.delete()
|
||||
|
||||
if (id == audiobookDownloadId) {
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("audiobookId", audiobookId)
|
||||
jsobj.put("error", "Move failed")
|
||||
notifyListeners("onDownloadFailed", jsobj)
|
||||
}
|
||||
}
|
||||
override fun onCompleted(result:Any) {
|
||||
var resultDocFile = result as DocumentFile
|
||||
var simplePath = resultDocFile.getSimplePath(context)
|
||||
var storageId = resultDocFile.getStorageId(context)
|
||||
var size = resultDocFile.length()
|
||||
Log.d(tag, "Finished Moving File, NAME: ${resultDocFile.name} | $storageId | SimplePath: $simplePath")
|
||||
|
||||
var abFolder = folder.findFolder(title)
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("audiobookId", audiobookId)
|
||||
jsobj.put("downloadId", id)
|
||||
jsobj.put("storageId", storageId)
|
||||
jsobj.put("storageType", resultDocFile.getStorageType(context))
|
||||
jsobj.put("folderUrl", abFolder?.uri)
|
||||
jsobj.put("folderName", abFolder?.name)
|
||||
jsobj.put("downloadFolderUrl", downloadFolderUrl)
|
||||
jsobj.put("contentUrl", resultDocFile.uri)
|
||||
jsobj.put("basePath", resultDocFile.getBasePath(context))
|
||||
jsobj.put("filename", filename)
|
||||
jsobj.put("simplePath", simplePath)
|
||||
jsobj.put("size", size)
|
||||
|
||||
if (resultDocFile.name == filename) {
|
||||
Log.d(tag, "Audiobook Finishing Moving")
|
||||
} else if (resultDocFile.name == coverFilename) {
|
||||
coverDocFile = docfile
|
||||
Log.d(tag, "Audiobook Cover Finished Moving")
|
||||
jsobj.put("isCover", true)
|
||||
}
|
||||
notifyListeners("onDownloadComplete", jsobj)
|
||||
}
|
||||
}
|
||||
|
||||
val executorService: ExecutorService = Executors.newFixedThreadPool(4)
|
||||
executorService.execute {
|
||||
if (id == coverDownloadId) {
|
||||
docfile?.moveFileTo(context, folder, FileDescription(coverFilename, title, MimeType.IMAGE), callback)
|
||||
} else if (id == audiobookDownloadId) {
|
||||
docfile?.moveFileTo(context, folder, FileDescription(filename, title, MimeType.AUDIO), callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var progressUpdater = DownloadProgressUpdater(downloadManager, audiobookDownloadId, progressReceiver, doneReceiver)
|
||||
progressUpdater.run()
|
||||
if (coverDownloadId != null) {
|
||||
var coverProgressUpdater = DownloadProgressUpdater(downloadManager, coverDownloadId, progressReceiver, doneReceiver)
|
||||
coverProgressUpdater.run()
|
||||
}
|
||||
|
||||
val ret = JSObject()
|
||||
ret.put("value", downloadId)
|
||||
ret.put("audiobookDownloadId", audiobookDownloadId)
|
||||
ret.put("coverDownloadId", coverDownloadId)
|
||||
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 audiobookId = call.data.getString("audiobookId", "audiobook").toString()
|
||||
var filename = call.data.getString("filename", "audiobook.mp3").toString()
|
||||
var url = call.data.getString("url", "").toString()
|
||||
var coverUrl = call.data.getString("coverUrl", "").toString()
|
||||
var folderUrl = call.data.getString("folderUrl", "").toString()
|
||||
|
||||
// Does Not Work
|
||||
// var audiobookDirRoot = activity.applicationContext.getExternalFilesDir(Environment.DIRECTORY_AUDIOBOOKS)
|
||||
// Log.d(tag, "AUDIOBOOK DIR ROOT $audiobookDirRoot")
|
||||
// var result = audiobookDirRoot?.deleteRecursively()
|
||||
// Log.d(tag, "DONE DELETING FOLDER $result")
|
||||
|
||||
// Does Not Work
|
||||
// var audiobookDir = File(audiobookDirRoot, audiobookId + "/")
|
||||
// Log.d(tag, "Delete Audiobook DIR ${audiobookDir.path} is dir ${audiobookDir.isDirectory}")
|
||||
// var result = audiobookDir.deleteRecursively()
|
||||
//
|
||||
|
||||
// Does Not Work
|
||||
// var audiobookDir = activity.applicationContext.getExternalFilesDir(Environment.DIRECTORY_AUDIOBOOKS)
|
||||
// Log.d(tag, "AUDIOBOOK DIR ${audiobookDir?.path}")
|
||||
// var dir = File(audiobookDir, "$audiobookId/")
|
||||
// Log.d(tag, "DIR DIR ${dir.path}")
|
||||
// var res = dir.delete()
|
||||
// Log.d(tag, "DELETED $res")
|
||||
|
||||
var contentResolver = activity.applicationContext.contentResolver
|
||||
contentResolver.delete(Uri.parse(url), null, null)
|
||||
|
||||
if (coverUrl != "") {
|
||||
contentResolver.delete(Uri.parse(coverUrl), null, null)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
internal class DownloadProgressUpdater(private val manager: DownloadManager, private val downloadId: Long, private var receiver: (Long) -> Unit, private var doneReceiver: (Boolean) -> Unit) : Thread() {
|
||||
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
|
||||
private var TAG = "DownloadProgressUpdater"
|
||||
|
@ -308,34 +456,49 @@ class AudioDownloader : Plugin() {
|
|||
override fun run() {
|
||||
Log.d(TAG, "RUN FOR ID $downloadId")
|
||||
var keepRunning = true
|
||||
var increment = 0
|
||||
while (keepRunning) {
|
||||
Thread.sleep(500)
|
||||
increment++
|
||||
|
||||
if (increment % 4 == 0) {
|
||||
Log.d(TAG, "Loop $increment : $downloadId")
|
||||
}
|
||||
|
||||
manager.query(query).use {
|
||||
if (it.moveToFirst()) {
|
||||
|
||||
//get total bytes of the file
|
||||
if (totalBytes <= 0) {
|
||||
totalBytes = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
|
||||
if (totalBytes <= 0) {
|
||||
Log.e(TAG, "Download Is 0 Bytes $downloadId")
|
||||
doneReceiver(downloadId, false)
|
||||
keepRunning = false
|
||||
this.interrupt()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val downloadStatus = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||
val bytesDownloadedSoFar = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
|
||||
|
||||
if (increment % 4 == 0) {
|
||||
Log.d(TAG, "BYTES $increment : $downloadId : $bytesDownloadedSoFar : TOTAL: $totalBytes")
|
||||
}
|
||||
|
||||
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL || downloadStatus == DownloadManager.STATUS_FAILED) {
|
||||
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL) {
|
||||
doneReceiver(true)
|
||||
doneReceiver(downloadId, true)
|
||||
} else {
|
||||
doneReceiver(false)
|
||||
doneReceiver(downloadId, false)
|
||||
}
|
||||
keepRunning = false
|
||||
this.interrupt()
|
||||
} else {
|
||||
//update progress
|
||||
val percentProgress = ((bytesDownloadedSoFar * 100L) / totalBytes)
|
||||
receiver(percentProgress)
|
||||
receiver(downloadId, percentProgress)
|
||||
}
|
||||
|
||||
} else {
|
||||
Log.e(TAG, "NOT FOUND IN QUERY")
|
||||
keepRunning = false
|
||||
|
@ -343,6 +506,5 @@ class AudioDownloader : Plugin() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,10 @@ package com.audiobookshelf.app
|
|||
|
||||
import android.app.DownloadManager
|
||||
import android.content.*
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.anggrayudi.storage.SimpleStorage
|
||||
import com.anggrayudi.storage.SimpleStorageHelper
|
||||
import com.getcapacitor.BridgeActivity
|
||||
|
||||
|
||||
|
@ -21,23 +19,19 @@ class MainActivity : BridgeActivity() {
|
|||
lateinit var pluginCallback : () -> Unit
|
||||
lateinit var downloaderCallback : (String, Long) -> Unit
|
||||
|
||||
val storageHelper = SimpleStorageHelper(this)
|
||||
val storage = SimpleStorage(this)
|
||||
|
||||
val broadcastReceiver = object: BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
DownloadManager.ACTION_DOWNLOAD_COMPLETE -> {
|
||||
var thisdlid = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0L)
|
||||
|
||||
downloaderCallback("complete", thisdlid)
|
||||
|
||||
Log.d(tag, "DOWNNLAOD COMPELTE $thisdlid")
|
||||
Toast.makeText(this@MainActivity, "Download Completed $thisdlid", Toast.LENGTH_SHORT)
|
||||
}
|
||||
DownloadManager.ACTION_NOTIFICATION_CLICKED -> {
|
||||
var thisdlid = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0L)
|
||||
downloaderCallback("clicked", thisdlid)
|
||||
|
||||
Log.d(tag, "CLICKED NOTFIFICAIONT $thisdlid")
|
||||
Toast.makeText(this@MainActivity, "Download CLICKED $thisdlid", Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +39,7 @@ class MainActivity : BridgeActivity() {
|
|||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
Log.d(tag, "onCreate")
|
||||
registerPlugin(MyNativeAudio::class.java)
|
||||
registerPlugin(AudioDownloader::class.java)
|
||||
|
@ -57,7 +52,7 @@ class MainActivity : BridgeActivity() {
|
|||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
// unregisterReceiver(broadcastReceiver)
|
||||
unregisterReceiver(broadcastReceiver)
|
||||
}
|
||||
|
||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -101,4 +96,29 @@ class MainActivity : BridgeActivity() {
|
|||
fun registerBroadcastReceiver(cb: (String, Long) -> Unit) {
|
||||
downloaderCallback = cb
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
storageHelper.onSaveInstanceState(outState)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
storageHelper.onRestoreInstanceState(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
// Mandatory for Activity, but not for Fragment & ComponentActivity
|
||||
storageHelper.storage.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
Log.d(tag, "onRequestPermissionResult $requestCode")
|
||||
permissions.forEach { Log.d(tag, "PERMISSION $it") }
|
||||
grantResults.forEach { Log.d(tag, "GRANTREUSLTS $it") }
|
||||
// Mandatory for Activity, but not for Fragment & ComponentActivity
|
||||
storageHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
ext {
|
||||
minSdkVersion = 29
|
||||
minSdkVersion = 23
|
||||
compileSdkVersion = 30
|
||||
targetSdkVersion = 30
|
||||
androidxActivityVersion = '1.2.0'
|
||||
|
|
|
@ -3,12 +3,7 @@
|
|||
<template v-for="(shelf, index) in groupedBooks">
|
||||
<div :key="index" class="border-b border-opacity-10 w-full bookshelfRow py-4 flex justify-around relative">
|
||||
<template v-for="audiobook in shelf">
|
||||
<!-- <div :key="audiobook.id" class="relative px-4"> -->
|
||||
<cards-book-card :key="audiobook.id" :audiobook="audiobook" :width="cardWidth" :user-progress="userAudiobooks[audiobook.id]" :local-user-progress="localUserAudiobooks[audiobook.id]" />
|
||||
<!-- <nuxt-link :to="`/audiobook/${audiobook.id}`">
|
||||
<cards-book-cover :audiobook="audiobook" :width="cardWidth" class="mx-auto -mb-px" style="box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166" />
|
||||
</nuxt-link> -->
|
||||
<!-- </div> -->
|
||||
</template>
|
||||
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
|
||||
</div>
|
||||
|
@ -93,16 +88,22 @@ export default {
|
|||
} else {
|
||||
console.log('Disconnected - Reset to local storage')
|
||||
this.$store.commit('audiobooks/reset')
|
||||
this.$store.dispatch('downloads/loadFromStorage')
|
||||
this.$store.dispatch('audiobooks/useDownloaded')
|
||||
// this.calcShelves()
|
||||
// this.$store.dispatch('downloads/loadFromStorage')
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$store.commit('audiobooks/addListener', { id: 'bookshelf', meth: this.audiobooksUpdated })
|
||||
this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
|
||||
|
||||
window.addEventListener('resize', this.resize)
|
||||
|
||||
if (!this.$server) {
|
||||
console.error('Bookshelf mounted no server')
|
||||
return
|
||||
}
|
||||
|
||||
this.$server.on('connected', this.socketConnected)
|
||||
if (this.$server.connected) {
|
||||
this.$store.dispatch('audiobooks/load')
|
||||
|
@ -115,6 +116,12 @@ export default {
|
|||
this.$store.commit('audiobooks/removeListener', 'bookshelf')
|
||||
this.$store.commit('user/removeSettingsListener', 'bookshelf')
|
||||
window.removeEventListener('resize', this.resize)
|
||||
|
||||
if (!this.$server) {
|
||||
console.error('Bookshelf beforeDestroy no server')
|
||||
return
|
||||
}
|
||||
this.$server.off('connected', this.socketConnected)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -128,6 +128,7 @@ export default {
|
|||
if (this.$refs.audioPlayerMini) {
|
||||
this.$refs.audioPlayerMini.terminateStream()
|
||||
}
|
||||
this.download = null
|
||||
this.$store.commit('setPlayingDownload', null)
|
||||
|
||||
this.$localStore.setCurrent(null)
|
||||
|
@ -167,12 +168,6 @@ export default {
|
|||
}
|
||||
|
||||
if (this.$server.connected) {
|
||||
// var updateObj = {
|
||||
// audiobookId: this.download.id,
|
||||
// totalDuration: this.download.audiobook.duration,
|
||||
// clientCurrentTime: currentTime,
|
||||
// clientProgress: Number((currentTime / this.download.audiobook.duration).toFixed(3))
|
||||
// }
|
||||
this.$server.socket.emit('progress_update', progressUpdate)
|
||||
}
|
||||
this.$localStore.updateUserAudiobookProgress(progressUpdate).then(() => {
|
||||
|
@ -184,7 +179,6 @@ export default {
|
|||
closeStream() {},
|
||||
streamClosed(audiobookId) {
|
||||
console.log('Stream Closed')
|
||||
|
||||
if (this.stream.audiobook.id === audiobookId || audiobookId === 'n/a') {
|
||||
this.$store.commit('setStreamAudiobook', null)
|
||||
}
|
||||
|
@ -305,23 +299,15 @@ export default {
|
|||
|
||||
if (this.playingDownload) {
|
||||
console.log('[StreamContainer] Play download on audio mount')
|
||||
if (!this.download) {
|
||||
this.download = { ...this.playingDownload }
|
||||
}
|
||||
this.playDownload()
|
||||
} else if (this.$server.stream) {
|
||||
console.log('[StreamContainer] Open stream on audio mount')
|
||||
this.streamOpen(this.$server.stream)
|
||||
}
|
||||
},
|
||||
setListeners() {
|
||||
if (!this.$server.socket) {
|
||||
console.error('Invalid server socket not set')
|
||||
return
|
||||
}
|
||||
this.$server.socket.on('stream_open', this.streamOpen)
|
||||
this.$server.socket.on('stream_closed', this.streamClosed)
|
||||
this.$server.socket.on('stream_progress', this.streamProgress)
|
||||
this.$server.socket.on('stream_ready', this.streamReady)
|
||||
this.$server.socket.on('stream_reset', this.streamReset)
|
||||
},
|
||||
changePlaybackSpeed(speed) {
|
||||
this.$store.dispatch('user/updateUserSettings', { playbackRate: speed })
|
||||
},
|
||||
|
@ -342,6 +328,17 @@ export default {
|
|||
this.cancelStream()
|
||||
}
|
||||
}
|
||||
},
|
||||
setListeners() {
|
||||
if (!this.$server.socket) {
|
||||
console.error('Invalid server socket not set')
|
||||
return
|
||||
}
|
||||
this.$server.socket.on('stream_open', this.streamOpen)
|
||||
this.$server.socket.on('stream_closed', this.streamClosed)
|
||||
this.$server.socket.on('stream_progress', this.streamProgress)
|
||||
this.$server.socket.on('stream_ready', this.streamReady)
|
||||
this.$server.socket.on('stream_reset', this.streamReset)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
@ -110,6 +110,7 @@ export default {
|
|||
return this.download ? this.download.cover : null
|
||||
},
|
||||
download() {
|
||||
return null
|
||||
return this.$store.getters['downloads/getDownloadIfReady'](this.audiobookId)
|
||||
},
|
||||
errorText() {
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
<template>
|
||||
<div class="relative rounded-sm overflow-hidden" :style="{ height: width * 1.6 + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }">
|
||||
<div class="w-full h-full relative">
|
||||
<div v-if="showCoverBg" class="bg-primary absolute top-0 left-0 w-full h-full">
|
||||
<div class="w-full h-full z-0" ref="coverBg" />
|
||||
<div class="bg-primary absolute top-0 left-0 w-full h-full">
|
||||
<!-- Blurred background for covers that dont fill -->
|
||||
<div v-if="showCoverBg" class="w-full h-full z-0" ref="coverBg" />
|
||||
|
||||
<!-- Image Loading indicator -->
|
||||
<div v-if="!isImageLoaded" class="w-full h-full flex items-center justify-center text-white">
|
||||
<svg class="animate-spin w-12 h-12" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<img ref="cover" :src="fullCoverUrl" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0" :class="showCoverBg ? 'object-contain' : 'object-cover'" />
|
||||
</div>
|
||||
|
@ -42,7 +50,8 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
imageFailed: false,
|
||||
showCoverBg: false
|
||||
showCoverBg: false,
|
||||
isImageLoaded: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -142,10 +151,11 @@ export default {
|
|||
this.showCoverBg = false
|
||||
}
|
||||
}
|
||||
this.isImageLoaded = true
|
||||
},
|
||||
imageError(err) {
|
||||
console.error('ImgError', err)
|
||||
this.imageFailed = true
|
||||
console.error('ImgError', err, `SET IMAGE FAILED ${this.imageFailed}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,17 @@
|
|||
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
||||
<p class="absolute top-6 left-2 text-2xl">Downloads</p>
|
||||
|
||||
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
|
||||
<div class="absolute top-16 left-0 right-0 w-full flex items-center px-2 py-1" :class="hasStoragePermission ? '' : 'text-error'">
|
||||
<span class="material-icons" @click="changeDownloadFolderClick">{{ hasStoragePermission ? 'folder' : 'error' }}</span>
|
||||
<p v-if="hasStoragePermission" class="text-sm px-4" @click="changeDownloadFolderClick">{{ downloadFolderSimplePath || 'No Download Folder Selected' }}</p>
|
||||
<p v-else class="text-sm px-4" @click="changeDownloadFolderClick">No Storage Permissions. Click here</p>
|
||||
</div>
|
||||
|
||||
<div v-if="totalSize" class="absolute bottom-0 left-0 right-0 w-full py-3 text-center">
|
||||
<p class="text-sm text-center text-gray-300">Total: {{ $bytesPretty(totalSize) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20 mt-10" style="max-height: 75%" @click.stop>
|
||||
<div v-if="!totalDownloads" class="flex items-center justify-center h-40">
|
||||
<p>No Downloads</p>
|
||||
</div>
|
||||
|
@ -28,9 +38,10 @@
|
|||
<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-1/2">
|
||||
<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">
|
||||
|
@ -71,11 +82,22 @@
|
|||
|
||||
<script>
|
||||
import { Dialog } from '@capacitor/dialog'
|
||||
import AudioDownloader from '@/plugins/audio-downloader'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
downloadingProgress: {}
|
||||
downloadFolder: null,
|
||||
downloadingProgress: {},
|
||||
totalSize: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async show(newValue) {
|
||||
if (newValue) {
|
||||
this.downloadFolder = await this.$localStore.getDownloadFolder()
|
||||
this.setTotalSize()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -87,6 +109,12 @@ export default {
|
|||
this.$store.commit('downloads/setShowModal', val)
|
||||
}
|
||||
},
|
||||
hasStoragePermission() {
|
||||
return this.$store.state.hasStoragePermission
|
||||
},
|
||||
downloadFolderSimplePath() {
|
||||
return this.downloadFolder ? this.downloadFolder.simplePath : null
|
||||
},
|
||||
totalDownloads() {
|
||||
return this.downloadsReady.length + this.orphanDownloads.length + this.downloadsDownloading.length
|
||||
},
|
||||
|
@ -132,6 +160,25 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
setTotalSize() {
|
||||
var totalSize = 0
|
||||
this.downloadsReady.forEach((dl) => {
|
||||
totalSize += dl.size && !isNaN(dl.size) ? Number(dl.size) : 0
|
||||
})
|
||||
this.totalSize = totalSize
|
||||
},
|
||||
async changeDownloadFolderClick() {
|
||||
if (!this.hasStoragePermission) {
|
||||
console.log('Requesting Storage Permission')
|
||||
AudioDownloader.requestStoragePermission()
|
||||
} else {
|
||||
var folderObj = await AudioDownloader.selectFolder()
|
||||
if (folderObj.error) {
|
||||
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
|
||||
}
|
||||
await this.$localStore.setDownloadFolder(folderObj)
|
||||
}
|
||||
},
|
||||
updateDownloadProgress({ audiobookId, progress }) {
|
||||
this.$set(this.downloadingProgress, audiobookId, progress)
|
||||
},
|
||||
|
|
|
@ -43,6 +43,10 @@ export default {
|
|||
text: 'Added At',
|
||||
value: 'addedAt'
|
||||
},
|
||||
{
|
||||
text: 'Volume #',
|
||||
value: 'book.volumeNumber'
|
||||
},
|
||||
{
|
||||
text: 'Duration',
|
||||
value: 'duration'
|
||||
|
|
|
@ -55,14 +55,16 @@ export default {
|
|||
if (localAudiobooks[audiobookId].lastUpdate > userAudiobooks[audiobookId].lastUpdate) {
|
||||
// Local progress is more recent than user progress
|
||||
this.updateAudiobookProgressOnServer(localAudiobooks[audiobookId])
|
||||
} else {
|
||||
} else if (localAudiobooks[audiobookId].lastUpdate < userAudiobooks[audiobookId].lastUpdate) {
|
||||
// Server is more recent than local
|
||||
newestLocal[audiobookId] = userAudiobooks[audiobookId]
|
||||
console.log('SYNCUSERPROGRESS Server IS MORE RECENT for', audiobookId)
|
||||
localHasUpdates = true
|
||||
}
|
||||
} else {
|
||||
// Not on local yet - store on local
|
||||
newestLocal[audiobookId] = userAudiobooks[audiobookId]
|
||||
console.log('SYNCUSERPROGRESS LOCAL Is NOT Stored YET for', audiobookId, JSON.stringify(newestLocal[audiobookId]))
|
||||
localHasUpdates = true
|
||||
}
|
||||
}
|
||||
|
@ -111,22 +113,19 @@ export default {
|
|||
this.$store.commit('setAppUpdateInfo', result)
|
||||
if (result.updateAvailability === 2) {
|
||||
setTimeout(() => {
|
||||
this.$toast.info(`Update is available!`, {
|
||||
this.$toast.info(`Update is available! Click to update.`, {
|
||||
draggable: false,
|
||||
hideProgressBar: false,
|
||||
timeout: 4000,
|
||||
closeButton: false,
|
||||
onClick: this.clickUpdateToast()
|
||||
timeout: 20000,
|
||||
closeButton: true,
|
||||
onClick: this.clickUpdateToast
|
||||
})
|
||||
// this.showUpdateToast(result.availableVersion, !!result.immediateUpdateAllowed)
|
||||
}, 5000)
|
||||
}
|
||||
},
|
||||
onDownloadProgress(data) {
|
||||
// var downloadId = data.downloadId
|
||||
var progress = data.progress
|
||||
var filename = data.filename
|
||||
var audiobookId = filename ? Path.basename(filename, Path.extname(filename)) : ''
|
||||
var audiobookId = data.audiobookId
|
||||
|
||||
var downloadObj = this.$store.getters['downloads/getDownload'](audiobookId)
|
||||
if (downloadObj) {
|
||||
|
@ -136,31 +135,70 @@ export default {
|
|||
this.$toast.update(downloadObj.toastId, { content: `${progress}% Downloading ${downloadObj.audiobook.book.title}` })
|
||||
}
|
||||
},
|
||||
onDownloadFailed(data) {
|
||||
if (!data.audiobookId) {
|
||||
console.error('Download failed invalid audiobook id', data)
|
||||
return
|
||||
}
|
||||
var downloadObj = this.$store.getters['downloads/getDownload'](data.audiobookId)
|
||||
if (!downloadObj) {
|
||||
console.error('Failed to find download for audiobook', data.audiobookId)
|
||||
return
|
||||
}
|
||||
var message = data.error || 'Unknown Error'
|
||||
this.$toast.update(downloadObj.toastId, { content: `Failed. ${message}.`, options: { timeout: 5000, type: 'error' } }, true)
|
||||
this.$store.commit('downloads/removeDownload', downloadObj)
|
||||
},
|
||||
onDownloadComplete(data) {
|
||||
if (!data.audiobookId) {
|
||||
console.error('Download compelte invalid audiobook id', data)
|
||||
return
|
||||
}
|
||||
var downloadId = data.downloadId
|
||||
var contentUrl = data.contentUrl
|
||||
var folderUrl = data.folderUrl
|
||||
var folderName = data.folderName
|
||||
var storageId = data.storageId
|
||||
var storageType = data.storageType
|
||||
var simplePath = data.simplePath
|
||||
var filename = data.filename
|
||||
var audiobookId = filename ? Path.basename(filename, Path.extname(filename)) : ''
|
||||
var audiobookId = data.audiobookId
|
||||
var size = data.size || 0
|
||||
var isCover = !!data.isCover
|
||||
|
||||
if (audiobookId) {
|
||||
console.log('Download complete', filename, downloadId, contentUrl, 'AudiobookId:', audiobookId, 'Is Cover:', isCover)
|
||||
var downloadObj = this.$store.getters['downloads/getDownload'](audiobookId)
|
||||
if (!downloadObj) {
|
||||
console.error('Failed to find download for audiobook', audiobookId)
|
||||
return
|
||||
}
|
||||
|
||||
if (!isCover) {
|
||||
// Notify server to remove prepared download
|
||||
if (this.$server.socket) {
|
||||
this.$server.socket.emit('remove_download', audiobookId)
|
||||
}
|
||||
|
||||
console.log('Download complete', filename, downloadId, contentUrl, 'AudiobookId:', audiobookId)
|
||||
var downloadObj = this.$store.getters['downloads/getDownload'](audiobookId)
|
||||
if (!downloadObj) {
|
||||
console.error('Could not find download...')
|
||||
} else {
|
||||
this.$toast.update(downloadObj.toastId, { content: `Success! ${downloadObj.audiobook.book.title} downloaded.`, options: { timeout: 5000, type: 'success' } }, true)
|
||||
|
||||
console.log('Found download, update with content url')
|
||||
delete downloadObj.isDownloading
|
||||
delete downloadObj.isPreparing
|
||||
downloadObj.contentUrl = contentUrl
|
||||
downloadObj.simplePath = simplePath
|
||||
downloadObj.folderUrl = folderUrl
|
||||
downloadObj.folderName = folderName
|
||||
downloadObj.storageType = storageType
|
||||
downloadObj.storageId = storageId
|
||||
downloadObj.basePath = data.basePath || null
|
||||
downloadObj.size = size
|
||||
this.$store.commit('downloads/addUpdateDownload', downloadObj)
|
||||
} else {
|
||||
downloadObj.coverUrl = contentUrl
|
||||
downloadObj.cover = Capacitor.convertFileSrc(contentUrl)
|
||||
downloadObj.coverSize = size
|
||||
downloadObj.coverBasePath = data.basePath || null
|
||||
console.log('Updating download with cover', downloadObj.cover)
|
||||
this.$store.commit('downloads/addUpdateDownload', downloadObj)
|
||||
}
|
||||
}
|
||||
},
|
||||
async checkLoadCurrent() {
|
||||
|
@ -176,59 +214,38 @@ export default {
|
|||
this.$localStore.setCurrent(null)
|
||||
}
|
||||
},
|
||||
onMediaLoaded(items) {
|
||||
async onMediaLoaded(items) {
|
||||
var jsitems = JSON.parse(items)
|
||||
jsitems = jsitems.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
size: item.size,
|
||||
duration: item.duration,
|
||||
filename: item.name,
|
||||
audiobookId: item.name ? Path.basename(item.name, Path.extname(item.name)) : '',
|
||||
contentUrl: item.uri.replace(/\\\//g, '/'),
|
||||
size: item.size,
|
||||
contentUrl: item.uri,
|
||||
coverUrl: item.coverUrl || null
|
||||
}
|
||||
})
|
||||
jsitems.forEach((item) => {
|
||||
var download = this.$store.getters['downloads/getDownload'](item.audiobookId)
|
||||
if (!download) {
|
||||
console.error(`Unknown media item found for filename ${item.filename}`)
|
||||
console.log('onMediaLoaded Items', JSON.stringify(jsitems))
|
||||
|
||||
var orphanDownload = {
|
||||
id: `orphan-${item.id}`,
|
||||
contentUrl: item.contentUrl,
|
||||
coverUrl: item.coverUrl,
|
||||
cover: item.coverUrl ? Capacitor.convertFileSrc(item.coverUrl) : null,
|
||||
mediaId: item.id,
|
||||
filename: item.filename,
|
||||
size: item.size,
|
||||
duration: item.duration,
|
||||
isOrphan: true
|
||||
}
|
||||
this.$store.commit('downloads/addUpdateDownload', orphanDownload)
|
||||
} else {
|
||||
console.log(`Found media item for audiobook ${download.audiobook.book.title} (${item.audiobookId})`)
|
||||
download.contentUrl = item.contentUrl
|
||||
download.coverUrl = item.coverUrl
|
||||
download.cover = item.coverUrl ? Capacitor.convertFileSrc(item.coverUrl) : null
|
||||
download.size = item.size
|
||||
download.duration = item.duration
|
||||
var downloads = await this.$sqlStore.getAllDownloads()
|
||||
for (let i = 0; i < downloads.length; i++) {
|
||||
var download = downloads[i]
|
||||
var jsitem = jsitems.find((item) => item.contentUrl === download.contentUrl)
|
||||
if (!jsitem) {
|
||||
console.error('Removing download was not found', JSON.stringify(download))
|
||||
await this.$sqlStore.removeDownload(download.id)
|
||||
} else if (download.coverUrl && !jsitem.coverUrl) {
|
||||
console.error('Removing cover for download was not found')
|
||||
download.cover = null
|
||||
download.coverUrl = null
|
||||
download.size = jsitem.size || 0
|
||||
this.$store.commit('downloads/addUpdateDownload', download)
|
||||
this.$store.commit('audiobooks/addUpdate', download.audiobook)
|
||||
} else {
|
||||
download.size = jsitem.size || 0
|
||||
this.$store.commit('downloads/addUpdateDownload', download)
|
||||
|
||||
download.audiobook.isDownloaded = true
|
||||
this.$store.commit('audiobooks/addUpdate', download.audiobook)
|
||||
}
|
||||
})
|
||||
|
||||
var downloads = this.$store.state.downloads.downloads
|
||||
|
||||
downloads.forEach((download) => {
|
||||
var matchingItem = jsitems.find((item) => item.audiobookId === download.id)
|
||||
if (!matchingItem) {
|
||||
console.error(`Saved download not in media store ${download.audiobook.book.title} (${download.id})`)
|
||||
this.$store.commit('downloads/removeDownload', download)
|
||||
}
|
||||
})
|
||||
|
||||
this.checkLoadCurrent()
|
||||
},
|
||||
|
@ -245,32 +262,95 @@ export default {
|
|||
this.$refs.streamContainer.cancelStream()
|
||||
}
|
||||
}
|
||||
|
||||
if (download.contentUrl) {
|
||||
await AudioDownloader.delete({ audiobookId: download.id, filename: download.filename, url: download.contentUrl, coverUrl: download.coverUrl })
|
||||
await AudioDownloader.delete(download)
|
||||
}
|
||||
this.$store.commit('downloads/removeDownload', download)
|
||||
},
|
||||
async initMediaStore() {
|
||||
// Load local database of downloads
|
||||
await this.$store.dispatch('downloads/loadFromStorage')
|
||||
await this.$localStore.loadUserAudiobooks()
|
||||
async onPermissionUpdate(data) {
|
||||
var val = data.value
|
||||
console.log('Permission Update', val)
|
||||
if (val === 'required') {
|
||||
console.log('Permission Required - Making Request')
|
||||
this.$store.commit('setHasStoragePermission', false)
|
||||
} else if (val === 'canceled' || val === 'denied') {
|
||||
console.error('Permission denied by user')
|
||||
this.$store.commit('setHasStoragePermission', false)
|
||||
} else if (val === 'granted') {
|
||||
console.log('User Granted permission')
|
||||
this.$store.commit('setHasStoragePermission', true)
|
||||
|
||||
var folder = data
|
||||
delete folder.value
|
||||
console.log('Setting folder', JSON.stringify(folder))
|
||||
await this.$localStore.setDownloadFolder(folder)
|
||||
} else {
|
||||
console.warn('Other permisiso update', val)
|
||||
}
|
||||
},
|
||||
async initMediaStore() {
|
||||
// Request and setup listeners for media files on native
|
||||
console.log('Permissino SET LISTENER')
|
||||
AudioDownloader.addListener('permission', (data) => {
|
||||
this.onPermissionUpdate(data)
|
||||
})
|
||||
AudioDownloader.addListener('onDownloadComplete', (data) => {
|
||||
this.onDownloadComplete(data)
|
||||
})
|
||||
AudioDownloader.addListener('onDownloadFailed', (data) => {
|
||||
this.onDownloadFailed(data)
|
||||
})
|
||||
AudioDownloader.addListener('onMediaLoaded', (data) => {
|
||||
this.onMediaLoaded(data.items)
|
||||
})
|
||||
AudioDownloader.addListener('onDownloadProgress', (data) => {
|
||||
this.onDownloadProgress(data)
|
||||
})
|
||||
AudioDownloader.load()
|
||||
|
||||
await this.$localStore.loadUserAudiobooks()
|
||||
await this.$localStore.getDownloadFolder()
|
||||
|
||||
var userSavedSettings = await this.$localStore.getUserSettings()
|
||||
if (userSavedSettings) {
|
||||
this.$store.commit('user/setSettings', userSavedSettings)
|
||||
}
|
||||
|
||||
var downloads = await this.$sqlStore.getAllDownloads()
|
||||
if (downloads.length) {
|
||||
var urls = downloads
|
||||
.map((d) => {
|
||||
return {
|
||||
contentUrl: d.contentUrl,
|
||||
coverUrl: d.coverUrl || '',
|
||||
storageId: d.storageId,
|
||||
basePath: d.basePath,
|
||||
coverBasePath: d.coverBasePath || ''
|
||||
}
|
||||
})
|
||||
.filter((d) => {
|
||||
if (!d.contentUrl) {
|
||||
console.error('Invalid Download no Content URL', JSON.stringify(d))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
AudioDownloader.load({
|
||||
audiobookUrls: urls
|
||||
})
|
||||
}
|
||||
|
||||
var checkPermission = await AudioDownloader.checkStoragePermission()
|
||||
console.log('Storage Permission is', checkPermission.value)
|
||||
if (!checkPermission.value) {
|
||||
console.log('Will require permissions')
|
||||
} else {
|
||||
console.log('Has Storage Permission')
|
||||
this.$store.commit('setHasStoragePermission', true)
|
||||
}
|
||||
},
|
||||
async setupNetworkListener() {
|
||||
var status = await Network.getStatus()
|
||||
console.log('Network status', status.connected, status.connectionType)
|
||||
this.$store.commit('setNetworkStatus', status)
|
||||
|
||||
Network.addListener('networkStatusChange', (status) => {
|
||||
|
@ -282,13 +362,27 @@ export default {
|
|||
mounted() {
|
||||
if (!this.$server) return console.error('No Server')
|
||||
|
||||
console.log('Default Mounted')
|
||||
|
||||
this.$server.on('connected', this.connected)
|
||||
this.$server.on('initialStream', this.initialStream)
|
||||
|
||||
if (this.$store.state.isFirstLoad) {
|
||||
this.$store.commit('setIsFirstLoad', false)
|
||||
this.setupNetworkListener()
|
||||
this.checkForUpdate()
|
||||
this.initMediaStore()
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (!this.$server) {
|
||||
console.error('No Server beforeDestroy')
|
||||
return
|
||||
}
|
||||
console.log('Default Before Destroy remove listeners')
|
||||
this.$server.off('connected', this.connected)
|
||||
this.$server.off('initialStream', this.initialStream)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -297,7 +391,6 @@ export default {
|
|||
height: calc(100vh - 64px);
|
||||
}
|
||||
#content.playerOpen {
|
||||
/* height: calc(100vh - 204px); */
|
||||
height: calc(100vh - 240px);
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "audiobookshelf-app",
|
||||
"version": "v0.8.4-beta",
|
||||
"version": "v0.9.0-beta",
|
||||
"author": "advplyr",
|
||||
"scripts": {
|
||||
"dev": "nuxt --hostname localhost --port 1337",
|
||||
|
|
|
@ -68,6 +68,9 @@ export default {
|
|||
})
|
||||
this.$server.logout()
|
||||
this.$router.push('/connect')
|
||||
|
||||
this.$store.commit('audiobooks/reset')
|
||||
this.$store.dispatch('audiobooks/useDownloaded')
|
||||
},
|
||||
openAppStore() {
|
||||
AppUpdate.openAppStore()
|
||||
|
|
|
@ -162,6 +162,9 @@ export default {
|
|||
},
|
||||
downloadObj() {
|
||||
return this.$store.getters['downloads/getDownload'](this.audiobookId)
|
||||
},
|
||||
hasStoragePermission() {
|
||||
return this.$store.state.hasStoragePermission
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -229,17 +232,32 @@ export default {
|
|||
}
|
||||
},
|
||||
async prepareDownload() {
|
||||
// var audiobook = await this.$axios.$get(`/api/audiobook/${this.audiobookId}`).catch((error) => {
|
||||
// console.error('Failed', error)
|
||||
// return false
|
||||
// })
|
||||
var audiobook = this.audiobook
|
||||
if (!audiobook) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.hasStoragePermission) {
|
||||
await AudioDownloader.requestStoragePermission()
|
||||
return
|
||||
}
|
||||
|
||||
// Download Path
|
||||
var dlFolder = this.$localStore.downloadFolder
|
||||
if (!dlFolder) {
|
||||
console.log('No download folder, request from ujser')
|
||||
var folderObj = await AudioDownloader.selectFolder()
|
||||
|
||||
if (folderObj.error) {
|
||||
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
|
||||
}
|
||||
dlFolder = folderObj
|
||||
await this.$localStore.setDownloadFolder(folderObj)
|
||||
}
|
||||
|
||||
var downloadObject = {
|
||||
id: this.audiobookId,
|
||||
downloadFolderUrl: dlFolder.uri,
|
||||
audiobook: {
|
||||
...audiobook
|
||||
},
|
||||
|
@ -278,59 +296,37 @@ export default {
|
|||
}
|
||||
return _clean
|
||||
},
|
||||
async downloadCover(download) {
|
||||
var coverUrl = this.getCoverUrlForDownload()
|
||||
if (!coverUrl) {
|
||||
return null
|
||||
}
|
||||
var extname = Path.extname(coverUrl) || '.jpg'
|
||||
|
||||
var downloadRequestPayload = {
|
||||
audiobookId: download.id,
|
||||
downloadUrl: coverUrl,
|
||||
filename: `${download.id}${extname}`,
|
||||
title: download.audiobook.book.title
|
||||
}
|
||||
console.log('Starting cover download', coverUrl, downloadRequestPayload.filename)
|
||||
var downloadRes = await AudioDownloader.downloadCover(downloadRequestPayload)
|
||||
if (downloadRes && downloadRes.url) {
|
||||
console.log('Cover art downloaded', downloadRes.url)
|
||||
return downloadRes.url
|
||||
} else {
|
||||
console.error('Cover art failed to download')
|
||||
return null
|
||||
}
|
||||
},
|
||||
async startDownload(url, fileext, download) {
|
||||
this.$toast.update(download.toastId, { content: `Downloading "${download.audiobook.book.title}"...` })
|
||||
|
||||
var coverDownloadUrl = this.getCoverUrlForDownload()
|
||||
var coverFilename = null
|
||||
if (coverDownloadUrl) {
|
||||
var coverExt = Path.extname(coverDownloadUrl) || '.jpg'
|
||||
coverFilename = `cover-${download.id}${coverExt}`
|
||||
}
|
||||
|
||||
download.isDownloading = true
|
||||
download.isPreparing = false
|
||||
download.filename = `${download.id}${fileext}`
|
||||
download.filename = `${download.audiobook.book.title}${fileext}`
|
||||
this.$store.commit('downloads/addUpdateDownload', download)
|
||||
|
||||
console.log('Starting Download URL', url)
|
||||
var downloadRequestPayload = {
|
||||
audiobookId: download.id,
|
||||
filename: download.filename,
|
||||
coverFilename,
|
||||
coverDownloadUrl,
|
||||
downloadUrl: url,
|
||||
title: download.audiobook.book.title
|
||||
title: download.audiobook.book.title,
|
||||
downloadFolderUrl: download.downloadFolderUrl
|
||||
}
|
||||
var downloadRes = await AudioDownloader.download(downloadRequestPayload)
|
||||
var downloadId = downloadRes.value
|
||||
if (!downloadId) {
|
||||
console.error('Invalid download, removing')
|
||||
this.$toast.update(download.toastId, { content: `Failed download "${download.audiobook.book.title}"`, options: { timeout: 5000, type: 'error' } })
|
||||
if (downloadRes.error) {
|
||||
var errorMsg = downloadRes.error || 'Unknown error'
|
||||
console.error('Download error', errorMsg)
|
||||
this.$toast.update(download.toastId, { content: `Error: ${errorMsg}`, options: { timeout: 5000, type: 'error' } })
|
||||
this.$store.commit('downloads/removeDownload', download)
|
||||
} else {
|
||||
console.log('Download cover now')
|
||||
var coverUrl = await this.downloadCover(download)
|
||||
if (coverUrl) {
|
||||
console.log('Got cover url', coverUrl)
|
||||
download.coverUrl = coverUrl
|
||||
download.cover = Capacitor.convertFileSrc(coverUrl)
|
||||
this.$store.commit('downloads/addUpdateDownload', download)
|
||||
}
|
||||
}
|
||||
},
|
||||
downloadReady(prepareDownload) {
|
||||
|
@ -363,16 +359,24 @@ export default {
|
|||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.$server.socket) {
|
||||
console.warn('Audiobook Page mounted: Server socket not set')
|
||||
} else {
|
||||
this.$server.socket.on('download_ready', this.downloadReady)
|
||||
this.$server.socket.on('download_killed', this.downloadKilled)
|
||||
this.$server.socket.on('download_failed', this.downloadFailed)
|
||||
}
|
||||
|
||||
this.$store.commit('audiobooks/addListener', { id: 'audiobook', audiobookId: this.audiobookId, meth: this.audiobookUpdated })
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (!this.$server.socket) {
|
||||
console.warn('Audiobook Page beforeDestroy: Server socket not set')
|
||||
} else {
|
||||
this.$server.socket.off('download_ready', this.downloadReady)
|
||||
this.$server.socket.off('download_killed', this.downloadKilled)
|
||||
this.$server.socket.off('download_failed', this.downloadFailed)
|
||||
}
|
||||
|
||||
this.$store.commit('audiobooks/removeListener', 'audiobook')
|
||||
}
|
||||
|
|
|
@ -179,6 +179,13 @@ export default {
|
|||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (!this.$server) {
|
||||
console.error('Connected beforeDestroy: No Server')
|
||||
return
|
||||
}
|
||||
this.$server.off('connected', this.socketConnected)
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -2,6 +2,7 @@ import Vue from 'vue'
|
|||
Vue.prototype.$isDev = process.env.NODE_ENV !== 'production'
|
||||
|
||||
Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
|
||||
if (isNaN(bytes) || bytes === null) return 'Invalid Bytes'
|
||||
if (bytes === 0) {
|
||||
return '0 Bytes'
|
||||
}
|
||||
|
|
|
@ -408,6 +408,7 @@ class LocalStorage {
|
|||
this.vuexStore = vuexStore
|
||||
|
||||
this.userAudiobooksLoaded = false
|
||||
this.downloadFolder = null
|
||||
this.userAudiobooks = {}
|
||||
}
|
||||
|
||||
|
@ -502,6 +503,32 @@ class LocalStorage {
|
|||
}
|
||||
}
|
||||
|
||||
async setDownloadFolder(folderObj) {
|
||||
try {
|
||||
if (folderObj) {
|
||||
await Storage.set({ key: 'downloadFolder', value: JSON.stringify(folderObj) })
|
||||
this.downloadFolder = folderObj
|
||||
} else {
|
||||
await Storage.remove({ key: 'downloadFolder' })
|
||||
this.downloadFolder = null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to set download folder', error)
|
||||
}
|
||||
}
|
||||
|
||||
async getDownloadFolder() {
|
||||
try {
|
||||
var _value = (await Storage.get({ key: 'downloadFolder' }) || {}).value || null
|
||||
if (!_value) return null
|
||||
this.downloadFolder = JSON.parse(_value)
|
||||
return this.downloadFolder
|
||||
} catch (error) {
|
||||
console.error('[LocalStorage] Failed to get download folder', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async getServerUrl() {
|
||||
try {
|
||||
return (await Storage.get({ key: 'serverUrl' }) || {}).value || null
|
||||
|
|
|
@ -15,7 +15,11 @@ export const getters = {
|
|||
getAudiobook: state => id => {
|
||||
return state.audiobooks.find(ab => ab.id === id)
|
||||
},
|
||||
getFiltered: (state, getters, rootState) => () => {
|
||||
getFiltered: (state, getters, rootState, rootGetters) => () => {
|
||||
// var isDisc = !rootState.networkConnected || !rootState.socketConnected
|
||||
// console.log('GET FILETERED', isDisc)
|
||||
// var filtered = isDisc ? rootGetters['downloads/getAudiobooks'] : state.audiobooks
|
||||
// console.log('FILTERED LEN', filtered.length)
|
||||
var filtered = state.audiobooks
|
||||
var settings = rootState.user.settings || {}
|
||||
var filterBy = settings.filterBy || ''
|
||||
|
@ -36,9 +40,12 @@ export const getters = {
|
|||
var direction = settings.orderDesc ? 'desc' : 'asc'
|
||||
|
||||
var filtered = getters.getFiltered()
|
||||
var orderByNumber = settings.orderBy === 'book.volumeNumber'
|
||||
return sort(filtered)[direction]((ab) => {
|
||||
// Supports dot notation strings i.e. "book.title"
|
||||
return settings.orderBy.split('.').reduce((a, b) => a[b], ab)
|
||||
var value = settings.orderBy.split('.').reduce((a, b) => a[b], ab)
|
||||
if (orderByNumber && !isNaN(value)) return Number(value)
|
||||
return value
|
||||
})
|
||||
},
|
||||
getUniqueAuthors: (state) => {
|
||||
|
@ -63,6 +70,9 @@ export const actions = {
|
|||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
})
|
||||
},
|
||||
useDownloaded({ commit, rootGetters }) {
|
||||
commit('set', rootGetters['downloads/getAudiobooks'])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,9 @@ export const getters = {
|
|||
getDownloadIfReady: (state) => id => {
|
||||
var download = state.downloads.find(d => d.id === id)
|
||||
return !!download && !download.isDownloading && !download.isPreparing ? download : null
|
||||
},
|
||||
getAudiobooks: (state) => {
|
||||
return state.downloads.map(dl => dl.audiobook)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,8 +8,10 @@ export const state = () => ({
|
|||
appUpdateInfo: null,
|
||||
socketConnected: false,
|
||||
networkConnected: false,
|
||||
networkConnectionType: 'unknown',
|
||||
streamListener: null
|
||||
networkConnectionType: null,
|
||||
streamListener: null,
|
||||
isFirstLoad: true,
|
||||
hasStoragePermission: false
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
|
@ -27,6 +29,12 @@ export const getters = {
|
|||
export const actions = {}
|
||||
|
||||
export const mutations = {
|
||||
setHasStoragePermission(state, val) {
|
||||
state.hasStoragePermission = val
|
||||
},
|
||||
setIsFirstLoad(state, val) {
|
||||
state.isFirstLoad = val
|
||||
},
|
||||
setAppUpdateInfo(state, info) {
|
||||
state.appUpdateInfo = info
|
||||
},
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { Storage } from '@capacitor/storage'
|
||||
import Vue from 'vue'
|
||||
|
||||
export const state = () => ({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue