Compare commits

...
15 Commits
154 changed files with 5017 additions and 304 deletions
+2 -1
View File
@@ -89,4 +89,5 @@ sw.*
# Vim swap files
*.swp
/resources/
/resources/
/android/app/release/
+31 -5
View File
@@ -1,4 +1,5 @@
import { io } from 'socket.io-client'
import { Storage } from '@capacitor/storage'
import axios from 'axios'
import EventEmitter from 'events'
@@ -26,6 +27,7 @@ class Server extends EventEmitter {
}
getServerUrl(url) {
if (!url) return null
var urlObject = new URL(url)
return `${urlObject.protocol}//${urlObject.hostname}:${urlObject.port}`
}
@@ -34,23 +36,35 @@ class Server extends EventEmitter {
this.user = user
this.store.commit('user/setUser', user)
if (user) {
localStorage.setItem('userToken', user.token)
// this.store.commit('user/setSettings', user.settings)
Storage.set({ key: 'token', value: user.token })
} else {
localStorage.removeItem('userToken')
Storage.remove({ key: 'token' })
}
}
setServerUrl(url) {
this.url = url
localStorage.setItem('serverUrl', url)
this.store.commit('setServerUrl', url)
if (url) {
Storage.set({ key: 'serverUrl', value: url })
} else {
Storage.remove({ key: 'serverUrl' })
}
}
async connect(url, token) {
if (!url) {
console.error('Invalid url to connect')
return false
}
var serverUrl = this.getServerUrl(url)
var res = await this.ping(serverUrl)
if (!res || !res.success) {
this.url = null
this.setServerUrl(null)
return false
}
var authRes = await this.authorize(serverUrl, token)
@@ -59,7 +73,7 @@ class Server extends EventEmitter {
}
this.setServerUrl(serverUrl)
console.warn('Connect setting auth user', authRes)
this.setUser(authRes.user)
this.connectSocket()
@@ -102,6 +116,9 @@ class Server extends EventEmitter {
logout() {
this.setUser(null)
if (this.socket) {
this.socket.disconnect()
}
}
authorize(serverUrl, token) {
@@ -137,9 +154,13 @@ class Server extends EventEmitter {
this.connected = true
this.emit('connected', true)
this.store.commit('setSocketConnected', true)
})
this.socket.on('disconnect', () => {
console.log('[Server] Socket Disconnected')
this.connected = false
this.emit('connected', false)
this.store.commit('setSocketConnected', false)
})
this.socket.on('init', (data) => {
console.log('[Server] Initial socket data received', data)
@@ -149,6 +170,11 @@ class Server extends EventEmitter {
this.emit('initialStream', data.stream)
}
})
this.socket.on('user_updated', (user) => {
if (this.user && user.id === this.user.id) {
this.setUser(user)
}
})
}
}
+7 -6
View File
@@ -1,6 +1,3 @@
//apply plugin: 'com.android.application'
//apply plugin: 'kotlin-android'
plugins {
id 'com.android.application'
id 'kotlin-android'
@@ -8,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 1
versionName "1.0"
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.
@@ -38,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')
@@ -49,7 +50,7 @@ dependencies {
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
implementation "androidx.core:core-ktx:+"
implementation "androidx.core:core-ktx:1.6.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
+5 -1
View File
@@ -9,8 +9,12 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-community-sqlite')
implementation project(':capacitor-dialog')
implementation project(':capacitor-toast')
implementation project(':capacitor-network')
implementation project(':capacitor-storage')
implementation project(':robingenz-capacitor-app-update')
implementation project(':capacitor-data-storage-sqlite')
}
+5 -1
View File
@@ -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,10 +1,26 @@
[
{
"pkg": "@capacitor-community/sqlite",
"classpath": "com.getcapacitor.community.database.sqlite.CapacitorSQLitePlugin"
},
{
"pkg": "@capacitor/dialog",
"classpath": "com.capacitorjs.plugins.dialog.DialogPlugin"
},
{
"pkg": "@capacitor/toast",
"classpath": "com.capacitorjs.plugins.toast.ToastPlugin"
"pkg": "@capacitor/network",
"classpath": "com.capacitorjs.plugins.network.NetworkPlugin"
},
{
"pkg": "@capacitor/storage",
"classpath": "com.capacitorjs.plugins.storage.StoragePlugin"
},
{
"pkg": "@robingenz/capacitor-app-update",
"classpath": "dev.robingenz.capacitor.appupdate.AppUpdatePlugin"
},
{
"pkg": "capacitor-data-storage-sqlite",
"classpath": "com.jeep.plugin.capacitor.capacitordatastoragesqlite.CapacitorDataStorageSqlitePlugin"
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,510 @@
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
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@CapacitorPlugin(name = "AudioDownloader")
class AudioDownloader : Plugin() {
private val tag = "AudioDownloader"
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
}
}
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") {}
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")
// }
}
private fun setupSimpleStorage() {
mainActivity.storageHelper.onFolderSelected = { requestCode, folder ->
Log.d(tag, "FOLDER SELECTED $requestCode ${folder.name} ${folder.uri}")
var jsobj = JSObject()
jsobj.put("value", "granted")
jsobj.put("uri", folder.uri)
jsobj.put("absolutePath", folder.getAbsolutePath(context))
jsobj.put("storageId", folder.getStorageId(context))
jsobj.put("storageType", folder.getStorageType(context))
jsobj.put("simplePath", folder.getSimplePath(context))
jsobj.put("basePath", folder.getBasePath(context))
notifyListeners("permission", jsobj)
}
mainActivity.storage.storageAccessCallback = object : StorageAccessCallback {
override fun onRootPathNotSelected(
requestCode: Int,
rootPath: String,
uri: Uri,
selectedStorageType: StorageType,
expectedStorageType: StorageType
) {
Log.d(tag, "STORAGE ACCESS CALLBACK")
}
override fun onCanceledByUser(requestCode: Int) {
Log.d(tag, "STORAGE ACCESS CALLBACK")
}
override fun onExpectedStorageNotSelected(requestCode: Int, selectedFolder: DocumentFile, selectedStorageType: StorageType, expectedBasePath: String, expectedStorageType: StorageType) {
Log.d(tag, "STORAGE ACCESS CALLBACK")
}
override fun onStoragePermissionDenied(requestCode: Int) {
Log.d(tag, "STORAGE ACCESS CALLBACK")
}
override fun onRootPathPermissionGranted(requestCode: Int, root: DocumentFile) {
Log.d(tag, "STORAGE ACCESS CALLBACK")
}
}
}
@RequiresApi(Build.VERSION_CODES.R)
@PluginMethod
fun requestStoragePermission(call: PluginCall) {
Log.d(tag, "Request Storage Permissions")
mainActivity.storageHelper.requestStorageAccess()
call.resolve()
}
@PluginMethod
fun checkStoragePermission(call: PluginCall) {
var res = false
if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
res = SimpleStorage.hasStoragePermission(context)
Log.d(tag, "Check Storage Access $res")
} else {
Log.d(tag, "Has permission on Android 10 or up")
res = true
}
var jsobj = JSObject()
jsobj.put("value", res)
call.resolve(jsobj)
}
fun checkUriExists(uri: Uri?): Boolean {
if (uri == null) return false
val resolver = context.contentResolver
//1. Check Uri
var cursor: Cursor? = null
val isUriExist: Boolean = try {
cursor = resolver.query(uri, null, null, null, null)
//cursor null: content Uri was invalid or some other error occurred
//cursor.moveToFirst() false: Uri was ok but no entry found.
(cursor != null && cursor.moveToFirst())
} catch (t: Throwable) {
false
} finally {
try {
cursor?.close()
} catch (t: Throwable) {
}
false
}
return isUriExist
}
@PluginMethod
fun load(call: PluginCall) {
var audiobookUrls = call.data.getJSONArray("audiobookUrls")
var len = audiobookUrls?.length()
if (len == null) {
len = 0
}
Log.d(tag, "CALLED LOAD $len")
var audiobookItems:MutableList<AudiobookItem> = mutableListOf()
(0 until len).forEach {
var jsobj = audiobookUrls.get(it) as JSONObject
var audiobookUrl = jsobj.get("contentUrl").toString()
var coverUrl = jsobj.get("coverUrl").toString()
var storageId = ""
if(jsobj.has("storageId")) jsobj.get("storageId").toString()
var basePath = ""
if(jsobj.has("basePath")) jsobj.get("basePath").toString()
var coverBasePath = ""
if(jsobj.has("coverBasePath")) jsobj.get("coverBasePath").toString()
Log.d(tag, "LOOKUP $storageId $basePath $audiobookUrl")
var audiobookFile: DocumentFile? = null
var coverFile: DocumentFile? = null
// Android 9 OR Below use storage id and base path
if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
audiobookFile = DocumentFileCompat.fromSimplePath(context, storageId, basePath)
if (coverUrl != null && coverUrl != "") {
coverFile = DocumentFileCompat.fromSimplePath(context, storageId, coverBasePath)
}
} else {
// Android 10 and up manually deleting will still load the file causing crash
var exists = checkUriExists(Uri.parse(audiobookUrl))
if (exists) {
Log.d(tag, "Audiobook exists")
audiobookFile = DocumentFileCompat.fromUri(context, Uri.parse(audiobookUrl))
} else {
Log.e(tag, "Audiobook does not exist")
}
var coverExists = checkUriExists(Uri.parse(coverUrl))
if (coverExists) {
Log.d(tag, "Cover Exists")
coverFile = DocumentFileCompat.fromUri(context, Uri.parse(coverUrl))
} else if (coverUrl != null && coverUrl != "") {
Log.e(tag, "Cover does not exist")
}
}
if (audiobookFile == null) {
Log.e(tag, "Audiobook was not found $audiobookUrl")
} else {
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")
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 ${folder.name}")
dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
dlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, dlfilename)
var audiobookDownloadId = downloadManager.enqueue(dlRequest)
var coverDownloadId:Long? = null
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 progressReceiver : (id:Long, prog: Long) -> Unit = { id:Long, prog: Long ->
if (id == audiobookDownloadId) {
var jsobj = JSObject()
jsobj.put("audiobookId", audiobookId)
jsobj.put("progress", prog)
notifyListeners("onDownloadProgress", jsobj)
}
}
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("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 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
private var TAG = "DownloadProgressUpdater"
init {
query.setFilterById(this.downloadId)
}
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(downloadId, true)
} else {
doneReceiver(downloadId, false)
}
keepRunning = false
this.interrupt()
} else {
//update progress
val percentProgress = ((bytesDownloadedSoFar * 100L) / totalBytes)
receiver(downloadId, percentProgress)
}
} else {
Log.e(TAG, "NOT FOUND IN QUERY")
keepRunning = false
}
}
}
}
}
}
@@ -13,12 +13,17 @@ class Audiobook {
var cover:String = ""
var playWhenReady:Boolean = false
var startTime:Long = 0
var playbackSpeed:Float = 1f
var duration:Long = 0
var isLocal:Boolean = false
var contentUrl:String = ""
var hasPlayerLoaded:Boolean = false
val playlistUri:Uri
val coverUri:Uri
var playlistUri:Uri = Uri.EMPTY
var coverUri:Uri = Uri.EMPTY
var contentUri:Uri = Uri.EMPTY // For Local only
constructor(jsondata:JSObject) {
id = jsondata.getString("id", "audiobook").toString()
@@ -30,9 +35,25 @@ class Audiobook {
playlistUrl = jsondata.getString("playlistUrl", "").toString()
playWhenReady = jsondata.getBoolean("playWhenReady", false) == true
startTime = jsondata.getString("startTime", "0")!!.toLong()
playbackSpeed = jsondata.getDouble("playbackSpeed")!!.toFloat()
duration = jsondata.getString("duration", "0")!!.toLong()
playlistUri = Uri.parse(playlistUrl)
coverUri = Uri.parse(cover)
// Local data
isLocal = jsondata.getBoolean("isLocal", false) == true
contentUrl = jsondata.getString("contentUrl", "").toString()
if (playlistUrl != "") {
playlistUri = Uri.parse(playlistUrl)
}
if (cover != "") {
coverUri = Uri.parse(cover)
} else {
coverUri = Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
cover = coverUri.toString()
}
if (contentUrl != "") {
contentUri = Uri.parse(contentUrl)
}
}
}
@@ -1,15 +1,14 @@
package com.audiobookshelf.app
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import android.app.DownloadManager
import android.content.*
import android.os.*
import android.util.Log
import com.example.myapp.MyNativeAudio
import com.anggrayudi.storage.SimpleStorage
import com.anggrayudi.storage.SimpleStorageHelper
import com.getcapacitor.BridgeActivity
class MainActivity : BridgeActivity() {
private val tag = "MainActivity"
@@ -18,11 +17,42 @@ class MainActivity : BridgeActivity() {
private lateinit var mConnection : ServiceConnection
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)
}
DownloadManager.ACTION_NOTIFICATION_CLICKED -> {
var thisdlid = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0L)
downloaderCallback("clicked", thisdlid)
}
}
}
}
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(tag, "onCreate")
registerPlugin(MyNativeAudio::class.java)
registerPlugin(AudioDownloader::class.java)
var filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE).apply {
addAction(DownloadManager.ACTION_NOTIFICATION_CLICKED)
}
registerReceiver(broadcastReceiver, filter)
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(broadcastReceiver)
}
override fun onPostCreate(savedInstanceState: Bundle?) {
@@ -62,4 +92,33 @@ class MainActivity : BridgeActivity() {
val stopIntent = Intent(this, PlayerNotificationService::class.java)
stopService(stopIntent)
}
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,13 +1,10 @@
package com.example.myapp
package com.audiobookshelf.app
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.content.ContextCompat
import com.audiobookshelf.app.Audiobook
import com.audiobookshelf.app.MainActivity
import com.audiobookshelf.app.PlayerNotificationService
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
@@ -56,12 +53,20 @@ class MyNativeAudio : Plugin() {
} else {
Log.w(tag, "Service already started --")
}
var jsobj = JSObject()
var audiobook:Audiobook = Audiobook(call.data)
if (audiobook.playlistUrl == "" && audiobook.contentUrl == "") {
Log.e(tag, "Invalid URL for init audio player")
jsobj.put("success", false)
return call.resolve(jsobj)
}
Handler(Looper.getMainLooper()).post() {
playerNotificationService.initPlayer(audiobook)
call.resolve()
jsobj.put("success", true)
call.resolve(jsobj)
}
}
@@ -102,19 +107,33 @@ class MyNativeAudio : Plugin() {
}
@PluginMethod
fun seekForward10(call: PluginCall) {
fun seekForward(call: PluginCall) {
var amount:Long = call.getString("amount", "0")!!.toLong()
Handler(Looper.getMainLooper()).post() {
playerNotificationService.seekForward10()
playerNotificationService.seekForward(amount)
call.resolve()
}
}
@PluginMethod
fun seekBackward10(call: PluginCall) {
fun seekBackward(call: PluginCall) {
var amount:Long = call.getString("amount", "0")!!.toLong()
Handler(Looper.getMainLooper()).post() {
playerNotificationService.seekBackward10()
playerNotificationService.seekBackward(amount)
call.resolve()
}
}
@PluginMethod
fun setPlaybackSpeed(call: PluginCall) {
var playbackSpeed:Float = call.getFloat("speed", 1.0f)!!
Handler(Looper.getMainLooper()).post() {
playerNotificationService.setPlaybackSpeed(playbackSpeed)
call.resolve()
}
}
@PluginMethod
fun terminateStream(call: PluginCall) {
Handler(Looper.getMainLooper()).post() {
@@ -9,6 +9,7 @@ import android.net.Uri
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.provider.MediaStore
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaControllerCompat
@@ -21,12 +22,18 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.getcapacitor.JSObject
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaExtractor
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import com.google.android.exoplayer2.upstream.*
import kotlinx.coroutines.*
import java.io.File
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
@@ -133,7 +140,16 @@ class PlayerNotificationService : Service() {
createNotificationChannel(channelId, channelName)
} else ""
mPlayer = SimpleExoPlayer.Builder(this).build()
var simpleExoPlayerBuilder = SimpleExoPlayer.Builder(this)
simpleExoPlayerBuilder.setSeekBackIncrementMs(10000)
simpleExoPlayerBuilder.setSeekForwardIncrementMs(10000)
mPlayer = simpleExoPlayerBuilder.build()
mPlayer.setHandleAudioBecomingNoisy(true)
var audioAttributes:AudioAttributes = AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).setContentType(C.CONTENT_TYPE_SPEECH).build()
mPlayer.setAudioAttributes(audioAttributes, true)
setPlayerListeners()
val sessionActivityPendingIntent =
@@ -191,6 +207,8 @@ class PlayerNotificationService : Service() {
playerNotificationManager.setUseChronometer(false)
playerNotificationManager.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
playerNotificationManager.setPriority(NotificationCompat.PRIORITY_MAX)
playerNotificationManager.setUseFastForwardActionInCompactView(true)
playerNotificationManager.setUseRewindActionInCompactView(true)
// Unknown action
playerNotificationManager.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE)
@@ -205,13 +223,13 @@ class PlayerNotificationService : Service() {
mediaSessionConnector = MediaSessionConnector(mediaSession)
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
return MediaDescriptionCompat.Builder()
var builder = MediaDescriptionCompat.Builder()
.setMediaId(currentAudiobook!!.id)
.setTitle(currentAudiobook!!.title)
.setSubtitle(currentAudiobook!!.author)
.setMediaUri(currentAudiobook!!.playlistUri)
.setIconUri(currentAudiobook!!.coverUri)
.build()
return builder.build()
}
}
mediaSessionConnector.setQueueNavigator(queueNavigator)
@@ -268,6 +286,9 @@ class PlayerNotificationService : Service() {
}
}
private fun setPlayerListeners() {
mPlayer.addListener(object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
@@ -276,14 +297,6 @@ class PlayerNotificationService : Service() {
}
override fun onEvents(player: Player, events: Player.Events) {
if (events.contains(Player.EVENT_TRACKS_CHANGED)) {
Log.d(tag, "EVENT_TRACKS_CHANGED")
}
if (events.contains(Player.EVENT_TIMELINE_CHANGED)) {
Log.d(tag, "EVENT_TIMELINE_CHANGED")
}
if (events.contains(Player.EVENT_POSITION_DISCONTINUITY)) {
Log.d(tag, "EVENT_POSITION_DISCONTINUITY")
}
@@ -347,32 +360,47 @@ class PlayerNotificationService : Service() {
Log.d(tag, "Init Player audiobook already playing")
}
val metadata = MediaMetadataCompat.Builder()
var metadataBuilder = MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, currentAudiobook!!.title)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, currentAudiobook!!.title)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, currentAudiobook!!.author)
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, currentAudiobook!!.author)
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, currentAudiobook!!.author)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, currentAudiobook!!.series)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, currentAudiobook!!.cover)
.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, currentAudiobook!!.cover)
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, currentAudiobook!!.id)
.build()
if (currentAudiobook!!.cover != "") {
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, currentAudiobook!!.cover)
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, currentAudiobook!!.cover)
}
var metadata = metadataBuilder.build()
mediaSession.setMetadata(metadata)
var mediaMetadata = MediaMetadata.Builder().build()
var mediaItem = MediaItem.Builder().setUri(currentAudiobook!!.playlistUri).setMediaMetadata(mediaMetadata).build()
var dataSourceFactory = DefaultHttpDataSource.Factory()
dataSourceFactory.setUserAgent(channelId)
dataSourceFactory.setDefaultRequestProperties(hashMapOf("Authorization" to "Bearer ${currentAudiobook!!.token}"))
var mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
var mediaSource:MediaSource
if (currentAudiobook!!.isLocal) {
Log.d(tag, "Playing Local File")
var mediaItem = MediaItem.Builder().setUri(currentAudiobook!!.contentUri).setMediaMetadata(mediaMetadata).build()
var dataSourceFactory = DefaultDataSourceFactory(ctx, channelId)
mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
} else {
Log.d(tag, "Playing HLS File")
var mediaItem = MediaItem.Builder().setUri(currentAudiobook!!.playlistUri).setMediaMetadata(mediaMetadata).build()
var dataSourceFactory = DefaultHttpDataSource.Factory()
dataSourceFactory.setUserAgent(channelId)
dataSourceFactory.setDefaultRequestProperties(hashMapOf("Authorization" to "Bearer ${currentAudiobook!!.token}"))
mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
}
mPlayer.setMediaSource(mediaSource, true)
mPlayer.prepare()
mPlayer.playWhenReady = currentAudiobook!!.playWhenReady
mPlayer.setPlaybackSpeed(audiobook.playbackSpeed)
}
@@ -396,12 +424,16 @@ class PlayerNotificationService : Service() {
mPlayer.seekTo(time)
}
fun seekForward10() {
mPlayer.seekTo(mPlayer.currentPosition + 10000)
fun seekForward(amount:Long) {
mPlayer.seekTo(mPlayer.currentPosition + amount)
}
fun seekBackward10() {
mPlayer.seekTo(mPlayer.currentPosition - 10000)
fun seekBackward(amount:Long) {
mPlayer.seekTo(mPlayer.currentPosition - amount)
}
fun setPlaybackSpeed(speed:Float) {
mPlayer.setPlaybackSpeed(speed)
}
fun terminateStream() {
@@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group android:scaleX="1.127451"
android:scaleY="1.127451"
android:translateX="-1.5294118"
android:translateY="-1.5294118">
<path
android:fillColor="@android:color/white"
android:pathData="M18,13c0,3.31 -2.69,6 -6,6s-6,-2.69 -6,-6s2.69,-6 6,-6v4l5,-5l-5,-5v4c-4.42,0 -8,3.58 -8,8c0,4.42 3.58,8 8,8s8,-3.58 8,-8H18z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M10.86,15.94l0,-4.27l-0.09,0l-1.77,0.63l0,0.69l1.01,-0.31l0,3.26z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M12.25,13.44v0.74c0,1.9 1.31,1.82 1.44,1.82c0.14,0 1.44,0.09 1.44,-1.82v-0.74c0,-1.9 -1.31,-1.82 -1.44,-1.82C13.55,11.62 12.25,11.53 12.25,13.44zM14.29,13.32v0.97c0,0.77 -0.21,1.03 -0.59,1.03c-0.38,0 -0.6,-0.26 -0.6,-1.03v-0.97c0,-0.75 0.22,-1.01 0.59,-1.01C14.07,12.3 14.29,12.57 14.29,13.32z"/>
</group>
</vector>
@@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group android:scaleX="1.127451"
android:scaleY="1.127451"
android:translateX="-1.5294118"
android:translateY="-1.5294118">
<path
android:fillColor="@android:color/white"
android:pathData="M11.99,5V1l-5,5l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6s-6,-2.69 -6,-6h-2c0,4.42 3.58,8 8,8s8,-3.58 8,-8S16.41,5 11.99,5z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M10.89,16h-0.85v-3.26l-1.01,0.31v-0.69l1.77,-0.63h0.09V16z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M15.17,14.24c0,0.32 -0.03,0.6 -0.1,0.82s-0.17,0.42 -0.29,0.57s-0.28,0.26 -0.45,0.33s-0.37,0.1 -0.59,0.1s-0.41,-0.03 -0.59,-0.1s-0.33,-0.18 -0.46,-0.33s-0.23,-0.34 -0.3,-0.57s-0.11,-0.5 -0.11,-0.82V13.5c0,-0.32 0.03,-0.6 0.1,-0.82s0.17,-0.42 0.29,-0.57s0.28,-0.26 0.45,-0.33s0.37,-0.1 0.59,-0.1s0.41,0.03 0.59,0.1c0.18,0.07 0.33,0.18 0.46,0.33s0.23,0.34 0.3,0.57s0.11,0.5 0.11,0.82V14.24zM14.32,13.38c0,-0.19 -0.01,-0.35 -0.04,-0.48s-0.07,-0.23 -0.12,-0.31s-0.11,-0.14 -0.19,-0.17s-0.16,-0.05 -0.25,-0.05s-0.18,0.02 -0.25,0.05s-0.14,0.09 -0.19,0.17s-0.09,0.18 -0.12,0.31s-0.04,0.29 -0.04,0.48v0.97c0,0.19 0.01,0.35 0.04,0.48s0.07,0.24 0.12,0.32s0.11,0.14 0.19,0.17s0.16,0.05 0.25,0.05s0.18,-0.02 0.25,-0.05s0.14,-0.09 0.19,-0.17s0.09,-0.19 0.11,-0.32s0.04,-0.29 0.04,-0.48V13.38z"/>
</group>
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

@@ -0,0 +1,7 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M18,13c0,3.31 -2.69,6 -6,6s-6,-2.69 -6,-6s2.69,-6 6,-6v4l5,-5l-5,-5v4c-4.42,0 -8,3.58 -8,8c0,4.42 3.58,8 8,8s8,-3.58 8,-8H18z"/>
<path android:fillColor="@android:color/white" android:pathData="M10.86,15.94l0,-4.27l-0.09,0l-1.77,0.63l0,0.69l1.01,-0.31l0,3.26z"/>
<path android:fillColor="@android:color/white" android:pathData="M12.25,13.44v0.74c0,1.9 1.31,1.82 1.44,1.82c0.14,0 1.44,0.09 1.44,-1.82v-0.74c0,-1.9 -1.31,-1.82 -1.44,-1.82C13.55,11.62 12.25,11.53 12.25,13.44zM14.29,13.32v0.97c0,0.77 -0.21,1.03 -0.59,1.03c-0.38,0 -0.6,-0.26 -0.6,-1.03v-0.97c0,-0.75 0.22,-1.01 0.59,-1.01C14.07,12.3 14.29,12.57 14.29,13.32z"/>
</vector>
@@ -0,0 +1,7 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M11.99,5V1l-5,5l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6s-6,-2.69 -6,-6h-2c0,4.42 3.58,8 8,8s8,-3.58 8,-8S16.41,5 11.99,5z"/>
<path android:fillColor="@android:color/white" android:pathData="M10.89,16h-0.85v-3.26l-1.01,0.31v-0.69l1.77,-0.63h0.09V16z"/>
<path android:fillColor="@android:color/white" android:pathData="M15.17,14.24c0,0.32 -0.03,0.6 -0.1,0.82s-0.17,0.42 -0.29,0.57s-0.28,0.26 -0.45,0.33s-0.37,0.1 -0.59,0.1s-0.41,-0.03 -0.59,-0.1s-0.33,-0.18 -0.46,-0.33s-0.23,-0.34 -0.3,-0.57s-0.11,-0.5 -0.11,-0.82V13.5c0,-0.32 0.03,-0.6 0.1,-0.82s0.17,-0.42 0.29,-0.57s0.28,-0.26 0.45,-0.33s0.37,-0.1 0.59,-0.1s0.41,0.03 0.59,0.1c0.18,0.07 0.33,0.18 0.46,0.33s0.23,0.34 0.3,0.57s0.11,0.5 0.11,0.82V14.24zM14.32,13.38c0,-0.19 -0.01,-0.35 -0.04,-0.48s-0.07,-0.23 -0.12,-0.31s-0.11,-0.14 -0.19,-0.17s-0.16,-0.05 -0.25,-0.05s-0.18,0.02 -0.25,0.05s-0.14,0.09 -0.19,0.17s-0.09,0.18 -0.12,0.31s-0.04,0.29 -0.04,0.48v0.97c0,0.19 0.01,0.35 0.04,0.48s0.07,0.24 0.12,0.32s0.11,0.14 0.19,0.17s0.16,0.05 0.25,0.05s0.18,-0.02 0.25,-0.05s0.14,-0.09 0.19,-0.17s0.09,-0.19 0.11,-0.32s0.04,-0.29 0.04,-0.48V13.38z"/>
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 B

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 B

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 536 B

After

Width:  |  Height:  |  Size: 1011 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 923 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+2 -2
View File
@@ -17,6 +17,6 @@
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
<item name="android:background">@drawable/splash</item>
<item name="android:background">@drawable/screen</item>
</style>
</resources>
</resources>
-9
View File
@@ -2,14 +2,5 @@
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<access origin="*" />
<feature name="File">
<param name="android-package" value="org.apache.cordova.file.FileUtils"/>
<param name="onload" value="true"/>
</feature>
<feature name="Media">
<param name="android-package" value="org.apache.cordova.media.AudioHandler"/>
</feature>
</widget>
+14 -2
View File
@@ -2,8 +2,20 @@
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
include ':capacitor-community-sqlite'
project(':capacitor-community-sqlite').projectDir = new File('../node_modules/@capacitor-community/sqlite/android')
include ':capacitor-dialog'
project(':capacitor-dialog').projectDir = new File('../node_modules/@capacitor/dialog/android')
include ':capacitor-toast'
project(':capacitor-toast').projectDir = new File('../node_modules/@capacitor/toast/android')
include ':capacitor-network'
project(':capacitor-network').projectDir = new File('../node_modules/@capacitor/network/android')
include ':capacitor-storage'
project(':capacitor-storage').projectDir = new File('../node_modules/@capacitor/storage/android')
include ':robingenz-capacitor-app-update'
project(':robingenz-capacitor-app-update').projectDir = new File('../node_modules/@robingenz/capacitor-app-update/android')
include ':capacitor-data-storage-sqlite'
project(':capacitor-data-storage-sqlite').projectDir = new File('../node_modules/capacitor-data-storage-sqlite/android')
+4 -3
View File
@@ -1,11 +1,12 @@
ext {
minSdkVersion = 26
minSdkVersion = 23
compileSdkVersion = 30
targetSdkVersion = 30
androidxActivityVersion = '1.2.0'
androidxAppCompatVersion = '1.2.0'
androidxCoordinatorLayoutVersion = '1.1.0'
androidxCoreVersion = '1.3.2'
androidxCoreVersion = '1.6.0'
androidPlayCore = '1.9.0'
androidxFragmentVersion = '1.3.0'
junitVersion = '4.13.1'
androidxJunitVersion = '1.1.2'
@@ -13,7 +14,7 @@ ext {
cordovaAndroidVersion = '7.0.0'
androidx_app_compat_version = '1.2.0'
androidx_car_version = '1.0.0-alpha7'
androidx_core_ktx_version = '1.3.1'
androidx_core_ktx_version = '1.6.0'
androidx_media_version = '1.0.1'
androidx_preference_version = '1.1.1'
androidx_test_runner_version = '1.3.0'
+17
View File
@@ -0,0 +1,17 @@
@import "./fonts.css";
.box-shadow-md {
box-shadow: 2px 8px 6px #111111aa;
}
.box-shadow-lg-up {
box-shadow: 0px -12px 8px #111111ee;
}
.box-shadow-xl {
box-shadow: 2px 14px 8px #111111aa;
}
.box-shadow-book {
box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166;
}
+41
View File
@@ -0,0 +1,41 @@
/* fallback */
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(/material-icons.woff2) format('woff2');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}
@font-face {
font-family: 'Gentium Book Basic';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/GentiumBookBasic.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Gentium Book Basic';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/GentiumBookBasic.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
+44 -9
View File
@@ -16,8 +16,8 @@
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="forward10">
<span class="material-icons text-3xl">forward_10</span>
</div>
<div class="cursor-pointer flex items-center justify-center text-gray-300 ml-8" @mousedown.prevent @mouseup.prevent>
<span class="font-mono text-lg uppercase">1x</span>
<div class="cursor-pointer flex items-center justify-center text-gray-300 ml-7 w-10 text-center" @mousedown.prevent @mouseup.prevent @click="$emit('selectPlaybackSpeed')">
<span class="font-mono text-lg">{{ playbackRate }}x</span>
</div>
</template>
<template v-else>
@@ -58,8 +58,9 @@ export default {
data() {
return {
totalDuration: 0,
currentPlaybackRate: 1,
currentTime: 0,
isTerminated: false,
isResetting: false,
initObject: null,
stateName: 'idle',
playInterval: null,
@@ -77,17 +78,24 @@ export default {
computed: {
totalDurationPretty() {
return this.$secondsToTimestamp(this.totalDuration)
},
playbackRate() {
return this.$store.getters['user/getUserSetting']('playbackRate')
}
},
methods: {
updatePlaybackRate() {
this.currentPlaybackRate = this.playbackRate
MyNativeAudio.setPlaybackSpeed({ speed: this.playbackRate })
},
restart() {
this.seek(0)
},
backward10() {
MyNativeAudio.seekBackward10()
MyNativeAudio.seekBackward({ amount: '10000' })
},
forward10() {
MyNativeAudio.seekForward10()
MyNativeAudio.seekForward({ amount: '10000' })
},
sendStreamUpdate() {
this.$emit('updateTime', this.currentTime)
@@ -191,15 +199,38 @@ export default {
}
},
set(audiobookStreamData) {
this.isResetting = false
this.initObject = { ...audiobookStreamData }
MyNativeAudio.initPlayer(this.initObject)
this.currentPlaybackRate = this.initObject.playbackSpeed
MyNativeAudio.initPlayer(this.initObject).then((res) => {
if (res && res.success) {
console.log('Success init audio player')
} else {
console.error('Failed to init audio player')
}
})
if (audiobookStreamData.isLocal) {
this.setStreamReady()
}
},
setFromObj() {
if (!this.initObject) {
console.error('Cannot set from obj')
return
}
MyNativeAudio.initPlayer(this.initObject)
this.isResetting = false
MyNativeAudio.initPlayer(this.initObject).then((res) => {
if (res && res.success) {
console.log('Success init audio player')
} else {
console.error('Failed to init audio player')
}
})
if (audiobookStreamData.isLocal) {
this.setStreamReady()
}
},
play() {
MyNativeAudio.playPlayer()
@@ -220,13 +251,17 @@ export default {
stopPlayInterval() {
clearInterval(this.playInterval)
},
terminateStream(startTime) {
resetStream(startTime) {
var _time = String(Math.floor(startTime * 1000))
if (!this.initObject) {
console.error('Terminate stream when no init object is set...')
return
}
this.isResetting = true
this.initObject.currentTime = _time
this.terminateStream()
},
terminateStream() {
MyNativeAudio.terminateStream()
},
init() {
@@ -245,7 +280,7 @@ export default {
this.currentTime = Number((data.currentTime / 1000).toFixed(2))
this.stateName = data.stateName
if (this.stateName === 'ended' && this.isTerminated) {
if (this.stateName === 'ended' && this.isResetting) {
this.setFromObj()
}
+71 -6
View File
@@ -1,15 +1,30 @@
<template>
<div class="w-full h-16 bg-primary relative">
<div id="appbar" class="absolute top-0 left-0 w-full h-full z-30 flex items-center px-2">
<nuxt-link v-show="!showBack" to="/" class="mr-4">
<img src="/Logo.png" class="h-12 w-12" />
<nuxt-link v-show="!showBack" to="/" class="mr-3">
<img src="/Logo.png" class="h-10 w-10" />
</nuxt-link>
<a v-if="showBack" @click="back" class="rounded-full h-12 w-12 flex items-center justify-center hover:bg-white hover:bg-opacity-10 mr-2 cursor-pointer">
<span class="material-icons text-4xl text-white">arrow_back</span>
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 mr-2 cursor-pointer">
<span class="material-icons text-3xl text-white">arrow_back</span>
</a>
<p class="text-xl font-book">AudioBookshelf</p>
<div>
<p class="text-lg font-book leading-4">AudioBookshelf</p>
</div>
<div class="flex-grow" />
<ui-menu :label="username" :items="menuItems" @action="menuAction" class="ml-5" />
<!-- <ui-menu :label="username" :items="menuItems" @action="menuAction" class="ml-5" /> -->
<span class="material-icons cursor-pointer mx-4" @click="$store.commit('downloads/setShowModal', true)">source</span>
<widgets-connection-icon />
<!-- <nuxt-link to="/account" class="relative w-28 bg-fg border border-gray-500 rounded shadow-sm ml-5 pl-3 pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer hover:bg-bg hover:bg-opacity-40" aria-haspopup="listbox" aria-expanded="true">
<span class="flex items-center">
<span class="block truncate">{{ username }}</span>
</span>
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<span class="material-icons text-gray-100">person</span>
</span>
</nuxt-link> -->
</div>
</div>
</template>
@@ -40,6 +55,13 @@ export default {
},
username() {
return this.user ? this.user.username : 'err'
},
appListingUrl() {
if (this.$platform === 'android') {
return process.env.ANDROID_APP_URL
} else {
return process.env.IOS_APP_URL
}
}
},
methods: {
@@ -71,4 +93,47 @@ export default {
#appbar {
box-shadow: 0px 5px 5px #11111155;
}
.loader-dots div {
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.loader-dots div:nth-child(1) {
left: 0px;
animation: loader-dots1 0.6s infinite;
}
.loader-dots div:nth-child(2) {
left: 0px;
animation: loader-dots2 0.6s infinite;
}
.loader-dots div:nth-child(3) {
left: 10px;
animation: loader-dots2 0.6s infinite;
}
.loader-dots div:nth-child(4) {
left: 20px;
animation: loader-dots3 0.6s infinite;
}
@keyframes loader-dots1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes loader-dots3 {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
@keyframes loader-dots2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(10px, 0);
}
}
</style>
+38 -13
View File
@@ -3,16 +3,12 @@
<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">
<nuxt-link :to="`/audiobook/${audiobook.id}`">
<cards-book-cover :audiobook="audiobook" :width="cardWidth" class="mx-auto -mb-px" />
</nuxt-link>
</div>
<cards-book-card :key="audiobook.id" :audiobook="audiobook" :width="cardWidth" :user-progress="userAudiobooks[audiobook.id]" :local-user-progress="localUserAudiobooks[audiobook.id]" />
</template>
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
</div>
</template>
<div class="w-full py-16 text-center text-xl">
<div v-show="!groupedBooks.length" class="w-full py-16 text-center text-xl">
<div class="py-4">No Audiobooks</div>
<ui-btn v-if="hasFilters" @click="clearFilter">Clear Filter</ui-btn>
</div>
@@ -40,6 +36,12 @@ export default {
},
hasFilters() {
return this.$store.getters['user/getUserSetting']('filterBy') !== 'all'
},
userAudiobooks() {
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
},
localUserAudiobooks() {
return this.$store.state.user.localUserAudiobooks || {}
}
},
methods: {
@@ -48,11 +50,6 @@ export default {
filterBy: 'all'
})
},
playAudiobook(audiobook) {
console.log('Play Audiobook', audiobook)
this.$store.commit('setStreamAudiobook', audiobook)
this.$server.socket.emit('open_stream', audiobook.id)
},
calcShelves() {
var booksPerShelf = Math.floor(this.pageWidth / (this.cardWidth + 32))
var groupedBooks = []
@@ -83,20 +80,48 @@ export default {
if (this.currFilterOrderKey !== this.filterOrderKey) {
this.calcShelves()
}
},
socketConnected(isConnected) {
if (isConnected) {
console.log('Connected - Load from server')
this.$store.dispatch('audiobooks/load')
} else {
console.log('Disconnected - Reset to local storage')
this.$store.commit('audiobooks/reset')
this.$store.dispatch('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)
this.$store.dispatch('audiobooks/load')
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')
} else {
console.log('Bookshelf - Server not connected using downloaded')
}
this.init()
},
beforeDestroy() {
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>
+212 -35
View File
@@ -1,19 +1,24 @@
<template>
<div class="w-full p-4 pointer-events-none fixed bottom-0 left-0 right-0 z-20">
<div v-if="streamAudiobook" class="w-full bg-primary absolute bottom-0 left-0 right-0 z-50 p-2 pointer-events-auto" @click.stop @mousedown.stop @mouseup.stop>
<div v-if="audiobook" class="w-full bg-primary absolute bottom-0 left-0 right-0 z-50 p-2 pointer-events-auto" @click.stop @mousedown.stop @mouseup.stop>
<div class="pl-16 pr-2 flex items-center pb-2">
<div>
<p class="px-2">{{ title }}</p>
<p class="px-2 text-xs text-gray-400">by {{ author }}</p>
</div>
<div class="flex-grow" />
<div v-if="chapters.length" class="cursor-pointer flex items-center justify-center mr-6 w-6 text-center" :class="chapters.length ? 'text-gray-300' : 'text-gray-400'" @mousedown.prevent @mouseup.prevent @click="clickChapterBtn">
<span class="material-icons text-2xl">format_list_bulleted</span>
</div>
<span class="material-icons" @click="cancelStream">close</span>
</div>
<div class="absolute left-2 -top-10">
<cards-book-cover :audiobook="streamAudiobook" :width="64" />
<cards-book-cover :audiobook="audiobook" :download-cover="downloadedCover" :width="64" />
</div>
<audio-player-mini ref="audioPlayerMini" :loading="!stream || currStreamAudiobookId !== streamAudiobookId" @updateTime="updateTime" @hook:mounted="audioPlayerMounted" />
<audio-player-mini ref="audioPlayerMini" :loading="isLoading" @updateTime="updateTime" @selectPlaybackSpeed="showPlaybackSpeedModal = true" @hook:mounted="audioPlayerMounted" />
</div>
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-speed.sync="playbackSpeed" @change="changePlaybackSpeed" />
<modals-chapters-modal v-model="showChapterModal" :chapters="chapters" @select="selectChapter" />
</div>
</template>
@@ -25,21 +30,45 @@ export default {
return {
audioPlayerReady: false,
stream: null,
lastServerUpdateSentSeconds: 0
download: null,
lastProgressTimeUpdate: 0,
showPlaybackSpeedModal: false,
playbackSpeed: 1,
showChapterModal: false
}
},
watch: {
socketConnected(newVal) {
if (newVal) {
console.log('Socket Connected set listeners')
this.setListeners()
}
}
},
computed: {
socketConnected() {
return this.$store.state.socketConnected
},
isLoading() {
if (this.playingDownload) return false
if (!this.streamAudiobook) return false
return !this.stream || this.streamAudiobook.id !== this.stream.audiobook.id
},
playingDownload() {
return this.$store.state.playingDownload
},
audiobook() {
if (this.playingDownload) return this.playingDownload.audiobook
return this.streamAudiobook
},
audiobookId() {
return this.audiobook ? this.audiobook.id : null
},
streamAudiobook() {
return this.$store.state.streamAudiobook
},
streamAudiobookId() {
return this.streamAudiobook ? this.streamAudiobook.id : null
},
currStreamAudiobookId() {
return this.stream ? this.stream.audiobook.id : null
},
book() {
return this.streamAudiobook ? this.streamAudiobook.book || {} : {}
return this.audiobook ? this.audiobook.book || {} : {}
},
title() {
return this.book ? this.book.title : ''
@@ -50,9 +79,15 @@ export default {
cover() {
return this.book ? this.book.cover : ''
},
downloadedCover() {
return this.download ? this.download.cover : null
},
series() {
return this.book ? this.book.series : ''
},
chapters() {
return this.audiobook ? this.audiobook.chapters || [] : []
},
volumeNumber() {
return this.book ? this.book.volumeNumber : ''
},
@@ -62,7 +97,7 @@ export default {
return `${this.series} #${this.volumeNumber}`
},
duration() {
return this.streamAudiobook ? this.streamAudiobook.duration || 0 : 0
return this.audiobook ? this.audiobook.duration || 0 : 0
},
coverForNative() {
if (!this.cover) {
@@ -78,30 +113,72 @@ export default {
}
},
methods: {
clickChapterBtn() {
if (!this.chapters.length) return
this.showChapterModal = true
},
selectChapter(chapter) {
if (this.$refs.audioPlayerMini) {
this.$refs.audioPlayerMini.seek(chapter.start)
}
this.showChapterModal = false
},
async cancelStream() {
const { value } = await Dialog.confirm({
title: 'Confirm',
message: 'Cancel this stream?'
})
if (value) {
this.$server.socket.emit('close_stream')
if (this.download) {
if (this.$refs.audioPlayerMini) {
this.$refs.audioPlayerMini.terminateStream()
}
this.download = null
this.$store.commit('setPlayingDownload', null)
this.$localStore.setCurrent(null)
} else {
const { value } = await Dialog.confirm({
title: 'Confirm',
message: 'Cancel this stream?'
})
if (value) {
this.$server.socket.emit('close_stream')
this.$store.commit('setStreamAudiobook', null)
if (this.$refs.audioPlayerMini) {
this.$refs.audioPlayerMini.terminateStream()
}
}
}
},
updateTime(currentTime) {
var diff = currentTime - this.lastServerUpdateSentSeconds
var diff = currentTime - this.lastProgressTimeUpdate
if (diff > 4 || diff < 0) {
this.lastServerUpdateSentSeconds = currentTime
var updatePayload = {
currentTime,
streamId: this.stream.id
this.lastProgressTimeUpdate = currentTime
if (this.stream) {
var updatePayload = {
currentTime,
streamId: this.stream.id
}
this.$server.socket.emit('stream_update', updatePayload)
} else if (this.download) {
var progressUpdate = {
audiobookId: this.download.id,
currentTime: currentTime,
totalDuration: this.download.audiobook.duration,
progress: Number((currentTime / this.download.audiobook.duration).toFixed(3)),
lastUpdate: Date.now(),
isRead: false
}
if (this.$server.connected) {
this.$server.socket.emit('progress_update', progressUpdate)
}
this.$localStore.updateUserAudiobookProgress(progressUpdate).then(() => {
console.log('Updated user audiobook progress', currentTime)
})
}
this.$server.socket.emit('stream_update', updatePayload)
}
},
closeStream() {},
streamClosed(audiobookId) {
console.log('Stream Closed')
if (this.stream.audiobook.id === audiobookId || audiobookId === 'n/a') {
this.$store.commit('setStreamAudiobook', null)
}
@@ -122,23 +199,81 @@ export default {
streamReset({ streamId, startTime }) {
if (this.$refs.audioPlayerMini) {
if (this.stream && this.stream.id === streamId) {
this.$refs.audioPlayerMini.terminateStream(startTime)
this.$refs.audioPlayerMini.resetStream(startTime)
}
}
},
streamOpen(stream) {
console.log('[StreamContainer] Stream Open', stream)
async getDownloadStartTime() {
var userAudiobook = await this.$localStore.getMostRecentUserAudiobook(this.audiobookId)
if (!userAudiobook) {
console.log('[StreamContainer] getDownloadStartTime no user audiobook record found')
return 0
}
return userAudiobook.currentTime
},
async playDownload() {
if (this.stream) {
if (this.$refs.audioPlayerMini) {
this.$refs.audioPlayerMini.terminateStream()
}
this.stream = null
}
this.lastProgressTimeUpdate = 0
console.log('[StreamContainer] Playing local', this.playingDownload)
if (!this.$refs.audioPlayerMini) {
console.error('No Audio Player Mini')
return
}
if (this.stream && this.stream.id !== stream.id) {
console.error('STREAM CHANGED', this.stream.id, stream.id)
var playOnLoad = this.$store.state.playOnLoad
if (playOnLoad) this.$store.commit('setPlayOnLoad', false)
var currentTime = await this.getDownloadStartTime()
if (isNaN(currentTime) || currentTime === null) currentTime = 0
// Update local current time
this.$localStore.setCurrent({
audiobookId: this.download.id,
lastUpdate: Date.now()
})
var audiobookStreamData = {
title: this.title,
author: this.author,
playWhenReady: !!playOnLoad,
startTime: String(Math.floor(currentTime * 1000)),
playbackSpeed: this.playbackSpeed || 1,
cover: this.download.coverUrl || null,
duration: String(Math.floor(this.duration * 1000)),
series: this.seriesTxt,
token: this.$store.getters['user/getToken'],
contentUrl: this.playingDownload.contentUrl,
isLocal: true
}
this.$refs.audioPlayerMini.set(audiobookStreamData)
},
streamOpen(stream) {
if (this.download) {
if (this.$refs.audioPlayerMini) {
this.$refs.audioPlayerMini.terminateStream()
}
}
this.lastProgressTimeUpdate = 0
console.log('[StreamContainer] Stream Open: ' + this.title)
if (!this.$refs.audioPlayerMini) {
console.error('No Audio Player Mini')
return
}
this.stream = stream
// Update local remove current
this.$localStore.setCurrent(null)
var playlistUrl = stream.clientPlaylistUri
var currentTime = stream.clientCurrentTime || 0
var playOnLoad = this.$store.state.playOnLoad
@@ -149,24 +284,51 @@ export default {
author: this.author,
playWhenReady: !!playOnLoad,
startTime: String(Math.floor(currentTime * 1000)),
playbackSpeed: this.playbackSpeed || 1,
cover: this.coverForNative,
duration: String(Math.floor(this.duration * 1000)),
series: this.seriesTxt,
playlistUrl: this.$server.url + playlistUrl,
token: this.$store.getters['user/getToken']
}
console.log('audiobook stream data', audiobookStreamData.token, JSON.stringify(audiobookStreamData))
this.$refs.audioPlayerMini.set(audiobookStreamData)
},
audioPlayerMounted() {
console.log('Audio Player Mounted', this.$server.stream)
this.audioPlayerReady = true
if (this.$server.stream) {
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)
}
},
changePlaybackSpeed(speed) {
this.$store.dispatch('user/updateUserSettings', { playbackRate: speed })
},
settingsUpdated(settings) {
if (this.$refs.audioPlayerMini && this.$refs.audioPlayerMini.currentPlaybackRate !== settings.playbackRate) {
this.playbackSpeed = settings.playbackRate
this.$refs.audioPlayerMini.updatePlaybackRate()
}
},
streamUpdated(type, data) {
if (type === 'download') {
if (data) {
this.download = { ...data }
if (this.audioPlayerReady) {
this.playDownload()
}
} else if (this.download) {
this.cancelStream()
}
}
},
setListeners() {
if (!this.$server.socket) {
console.error('Invalid server socket not set')
@@ -180,8 +342,23 @@ export default {
}
},
mounted() {
console.warn('Stream Container Mounted')
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
this.setListeners()
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
this.$store.commit('setStreamListener', this.streamUpdated)
},
beforeDestroy() {
if (this.$server.socket) {
this.$server.socket.off('stream_open', this.streamOpen)
this.$server.socket.off('stream_closed', this.streamClosed)
this.$server.socket.off('stream_progress', this.streamProgress)
this.$server.socket.off('stream_ready', this.streamReady)
this.$server.socket.off('stream_reset', this.streamReset)
}
this.$store.commit('user/removeSettingsListener', 'streamContainer')
this.$store.commit('removeStreamListener')
}
}
</script>
+131
View File
@@ -0,0 +1,131 @@
<template>
<div class="relative">
<!-- New Book Flag -->
<div v-if="isNew" class="absolute top-4 left-0 w-4 h-10 pr-2 bg-darkgreen box-shadow-xl">
<div class="absolute top-0 left-0 w-full h-full transform -rotate-90 flex items-center justify-center">
<p class="text-center text-sm">New</p>
</div>
<div class="absolute -bottom-4 left-0 triangle-right" />
</div>
<div class="rounded-sm h-full overflow-hidden relative box-shadow-book">
<nuxt-link :to="`/audiobook/${audiobookId}`" class="cursor-pointer">
<div class="w-full relative" :style="{ height: height + 'px' }">
<cards-book-cover :audiobook="audiobook" :download-cover="downloadCover" :author-override="authorFormat" :width="width" />
<div v-if="download" class="absolute" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }">
<span class="material-icons text-success" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">download_done</span>
</div>
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm" :style="{ width: width * userProgressPercent + 'px' }"></div>
<div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
</div>
</div>
</nuxt-link>
</div>
</div>
</template>
<script>
export default {
props: {
audiobook: {
type: Object,
default: () => null
},
userProgress: {
type: Object,
default: () => null
},
localUserProgress: {
type: Object,
default: () => null
},
width: {
type: Number,
default: 140
}
},
data() {
return {}
},
computed: {
isNew() {
return this.tags.includes('new')
},
tags() {
return this.audiobook.tags || []
},
audiobookId() {
return this.audiobook.id
},
book() {
return this.audiobook.book || {}
},
height() {
return this.width * 1.6
},
sizeMultiplier() {
return this.width / 120
},
paddingX() {
return 16 * this.sizeMultiplier
},
author() {
return this.book.author
},
authorFL() {
return this.book.authorFL || this.author
},
authorLF() {
return this.book.authorLF || this.author
},
authorFormat() {
if (!this.orderBy || !this.orderBy.startsWith('book.author')) return null
return this.orderBy === 'book.authorLF' ? this.authorLF : this.authorFL
},
orderBy() {
return this.$store.getters['user/getUserSetting']('orderBy')
},
mostRecentUserProgress() {
if (!this.localUserProgress) return this.userProgress
if (!this.userProgress) return this.localUserProgress
return this.localUserProgress.lastUpdate > this.userProgress.lastUpdate ? this.localUserProgress : this.userProgress
},
userProgressPercent() {
return this.mostRecentUserProgress ? this.mostRecentUserProgress.progress || 0 : 0
},
showError() {
return this.hasMissingParts || this.hasInvalidParts
},
hasMissingParts() {
return this.audiobook.hasMissingParts
},
hasInvalidParts() {
return this.audiobook.hasInvalidParts
},
downloadCover() {
return this.download ? this.download.cover : null
},
download() {
return null
return this.$store.getters['downloads/getDownloadIfReady'](this.audiobookId)
},
errorText() {
var txt = ''
if (this.hasMissingParts) {
txt = `${this.hasMissingParts} missing parts.`
}
if (this.hasInvalidParts) {
if (this.hasMissingParts) txt += ' '
txt += `${this.hasInvalidParts} invalid parts.`
}
return txt || 'Unknown Error'
}
},
methods: {},
mounted() {}
}
</script>
+23 -5
View File
@@ -1,15 +1,23 @@
<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>
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
<img src="/LogoTransparent.png" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
<img src="/Logo.png" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
<p class="text-center font-book text-error" :style="{ fontSize: titleFontSize + 'rem' }">Invalid Cover</p>
</div>
</div>
@@ -32,6 +40,7 @@ export default {
type: Object,
default: () => {}
},
downloadCover: String,
authorOverride: String,
width: {
type: Number,
@@ -41,7 +50,8 @@ export default {
data() {
return {
imageFailed: false,
showCoverBg: false
showCoverBg: false,
isImageLoaded: false
}
},
watch: {
@@ -78,7 +88,13 @@ export default {
serverUrl() {
return this.$store.state.serverUrl
},
networkConnected() {
return this.$store.state.networkConnected
},
fullCoverUrl() {
if (this.downloadCover) return this.downloadCover
else if (!this.networkConnected) return this.placeholderUrl
if (this.cover.startsWith('http')) return this.cover
var _clean = this.cover.replace(/\\/g, '/')
if (_clean.startsWith('/local')) {
@@ -91,6 +107,7 @@ export default {
return this.book.cover || this.placeholderUrl
},
hasCover() {
if (!this.networkConnected && !this.downloadCover) return false
return !!this.book.cover
},
sizeMultiplier() {
@@ -134,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}`)
}
}
}
+50
View File
@@ -0,0 +1,50 @@
<template>
<modals-modal v-model="show" :width="300" height="100%">
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="chapter in chapters">
<li :key="chapter.id" class="text-gray-50 select-none relative py-3 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption(chapter)">
<div class="flex items-center justify-center px-3">
<span class="font-normal block truncate text-lg">{{ chapter.title }}</span>
<div class="flex-grow" />
<span class="font-mono text-gray-300">{{ $secondsToTimestamp(chapter.start) }}</span>
</div>
</li>
</template>
</ul>
</div>
</div>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean,
chapters: {
type: Array,
default: () => []
}
},
data() {
return {}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}
},
methods: {
clickedOption(chapter) {
this.$emit('select', chapter)
}
},
mounted() {}
}
</script>
+206
View File
@@ -0,0 +1,206 @@
<template>
<modals-modal v-model="show" width="100%" height="100%">
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
<p class="absolute top-6 left-2 text-2xl">Downloads</p>
<div class="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>
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="download in downloadsDownloading">
<li :key="download.id" class="text-gray-400 select-none relative px-4 py-5 border-b border-white border-opacity-10 bg-black bg-opacity-10">
<div class="flex items-center justify-center">
<div class="w-3/4">
<span class="text-xs">({{ downloadingProgress[download.id] || 0 }}%) {{ download.isPreparing ? 'Preparing' : 'Downloading' }}...</span>
<p class="font-normal truncate text-sm">{{ download.audiobook.book.title }}</p>
</div>
<div class="flex-grow" />
<div class="shadow-sm text-white flex items-center justify-center rounded-full animate-spin">
<span class="material-icons">refresh</span>
</div>
</div>
</li>
</template>
<template v-for="download in downloadsReady">
<li :key="download.id" class="text-gray-50 select-none relative pr-4 pl-2 py-5 border-b border-white border-opacity-10" @click="jumpToAudiobook(download)">
<div class="flex items-center justify-center">
<img v-if="download.cover" :src="download.cover" class="w-10 h-16 object-contain" />
<img v-else src="/book_placeholder.jpg" class="w-10 h-16 object-contain" />
<div class="pl-2 w-2/3">
<p class="font-normal truncate text-sm">{{ download.audiobook.book.title }}</p>
<p class="font-normal truncate text-xs text-gray-400">{{ download.audiobook.book.author }}</p>
<p class="font-normal truncate text-xs text-gray-400">{{ $bytesPretty(download.size) }}</p>
</div>
<div class="flex-grow" />
<div v-if="download.isIncomplete" class="shadow-sm text-warning flex items-center justify-center rounded-full mr-4">
<span class="material-icons">error_outline</span>
</div>
<button class="shadow-sm text-accent flex items-center justify-center rounded-full" @click.stop="clickedOption(download)">
<span class="material-icons" style="font-size: 2rem">play_arrow</span>
</button>
<div class="shadow-sm text-error flex items-center justify-center rounded-ful ml-4" @click.stop="clickDelete(download)">
<span class="material-icons" style="font-size: 1.2rem">delete</span>
</div>
</div>
</li>
</template>
<template v-for="download in orphanDownloads">
<li :key="download.id" class="text-gray-50 select-none relative cursor-pointer px-4 py-5 border-b border-white border-opacity-10">
<div class="flex items-center justify-center">
<div class="w-3/4">
<span class="text-xs text-gray-400">Unknown Audio File</span>
<p class="font-normal truncate text-sm">{{ download.filename }}</p>
</div>
<!-- <span class="font-normal block truncate text-sm pr-2">{{ download.filename }}</span> -->
<div class="flex-grow" />
<div class="shadow-sm text-warning flex items-center justify-center rounded-full">
<span class="material-icons">error_outline</span>
</div>
<div class="shadow-sm text-error flex items-center justify-center rounded-ful ml-4" @click="clickDelete(download)">
<span class="material-icons" style="font-size: 1.2rem">delete</span>
</div>
</div>
</li>
</template>
</ul>
</div>
</div>
</modals-modal>
</template>
<script>
import { Dialog } from '@capacitor/dialog'
import AudioDownloader from '@/plugins/audio-downloader'
export default {
data() {
return {
downloadFolder: null,
downloadingProgress: {},
totalSize: 0
}
},
watch: {
async show(newValue) {
if (newValue) {
this.downloadFolder = await this.$localStore.getDownloadFolder()
this.setTotalSize()
}
}
},
computed: {
show: {
get() {
return this.$store.state.downloads.showModal
},
set(val) {
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
},
downloadsDownloading() {
return this.downloads.filter((d) => d.isDownloading || d.isPreparing)
},
downloadsReady() {
return this.downloads.filter((d) => !d.isDownloading && !d.isPreparing)
},
orphanDownloads() {
return this.$store.state.downloads.orphanDownloads
// return [
// {
// id: 'asdf',
// filename: 'Test Title 1 another long title on the downloading widget.jpg'
// }
// ]
},
downloads() {
return this.$store.state.downloads.downloads
// return [
// {
// id: 'asdf1',
// audiobook: {
// book: {
// title: 'Test Title 1 another long title on the downloading widget',
// author: 'Test Author 1'
// }
// },
// isDownloading: true
// },
// {
// id: 'asdf2',
// audiobook: {
// book: {
// title: 'Test Title 2',
// author: 'Test Author 2 long test author to test the overflow capabilities'
// }
// },
// isReady: true
// }
// ]
}
},
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)
},
jumpToAudiobook(download) {
this.show = false
this.$router.push(`/audiobook/${download.id}`)
},
async clickDelete(download) {
const { value } = await Dialog.confirm({
title: 'Confirm',
message: 'Delete this download?'
})
if (value) {
this.$emit('deleteDownload', download)
}
},
clickedOption(download) {
console.log('Clicked download', download)
this.$emit('selectDownload', download)
this.show = false
}
},
mounted() {}
}
</script>
+18 -14
View File
@@ -31,13 +31,13 @@
</li>
<li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="option">
<div class="flex items-center justify-center">
<span class="font-normal block truncate py-3">No {{ sublist }}</span>
<span class="font-normal block truncate py-5 text-lg">No {{ sublist }} items</span>
</div>
</li>
<template v-for="item in sublistItems">
<li :key="item" class="text-gray-50 select-none relative px-2 cursor-pointer hover:bg-black-400" :class="`${sublist}.${item}` === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedSublistOption(item)">
<li :key="item.value" class="text-gray-50 select-none relative px-4 cursor-pointer hover:bg-black-400" :class="`${sublist}.${item.value}` === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedSublistOption(item.value)">
<div class="flex items-center">
<span class="font-normal truncate py-3 text-base">{{ snakeToNormal(item) }}</span>
<span class="font-normal truncate py-3 text-base">{{ item.text }}</span>
</div>
</li>
</template>
@@ -75,6 +75,11 @@ export default {
text: 'Series',
value: 'series',
sublist: true
},
{
text: 'Authors',
value: 'authors',
sublist: true
}
]
}
@@ -108,7 +113,7 @@ export default {
return this.selected && this.selected.includes('.') ? this.selected.split('.')[0] : false
},
genres() {
return this.$store.state.audiobooks.genres
return this.$store.getters['audiobooks/getGenresUsed']
},
tags() {
return this.$store.state.audiobooks.tags
@@ -116,8 +121,16 @@ export default {
series() {
return this.$store.state.audiobooks.series
},
authors() {
return this.$store.getters['audiobooks/getUniqueAuthors']
},
sublistItems() {
return this[this.sublist] || []
return (this[this.sublist] || []).map((item) => {
return {
text: item,
value: this.$encode(item)
}
})
}
},
methods: {
@@ -126,15 +139,6 @@ export default {
this.show = false
this.$nextTick(() => this.$emit('change', 'all'))
},
snakeToNormal(kebab) {
if (!kebab) {
return 'err'
}
return String(kebab)
.split('_')
.map((t) => t.slice(0, 1).toUpperCase() + t.slice(1))
.join(' ')
},
clickedSublistOption(item) {
this.clickedOption({ value: `${this.sublist}.${item}` })
},
+2 -2
View File
@@ -1,12 +1,12 @@
<template>
<div ref="wrapper" class="modal modal-bg w-full h-full max-h-screen fixed top-0 left-0 bg-primary bg-opacity-75 flex items-center justify-center z-30 opacity-0">
<div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" />
<div class="absolute top-0 left-0 w-full h-36 bg-gradient-to-b from-black to-transparent opacity-70 pointer-events-none" />
<div class="absolute z-40 top-4 right-4 h-12 w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" @click="show = false">
<span class="material-icons text-4xl">close</span>
</div>
<slot name="outer" />
<div ref="content" style="min-width: 90%; min-height: 200px" class="relative text-white max-h-screen" :style="{ height: modalHeight, width: modalWidth }" v-click-outside="clickBg">
<div ref="content" style="max-width: 90%; min-height: 200px" class="relative text-white max-h-screen" :style="{ height: modalHeight, width: modalWidth }" v-click-outside="clickBg">
<slot />
</div>
</div>
+4
View File
@@ -43,6 +43,10 @@ export default {
text: 'Added At',
value: 'addedAt'
},
{
text: 'Volume #',
value: 'book.volumeNumber'
},
{
text: 'Duration',
value: 'duration'
+62
View File
@@ -0,0 +1,62 @@
<template>
<modals-modal v-model="show" :width="200" height="100%">
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="rate in rates">
<li :key="rate" class="text-gray-50 select-none relative py-4 pr-9 cursor-pointer hover:bg-black-400" :class="rate === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedOption(rate)">
<div class="flex items-center justify-center">
<span class="font-normal ml-3 block truncate text-lg">{{ rate }}x</span>
</div>
</li>
</template>
</ul>
</div>
</div>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean,
playbackSpeed: Number
},
data() {
return {}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
selected: {
get() {
return this.playbackSpeed
},
set(val) {
this.$emit('update:playbackSpeed', val)
}
},
rates() {
return [0.25, 0.5, 0.8, 1, 1.3, 1.5, 2, 2.5, 3]
}
},
methods: {
clickedOption(speed) {
if (this.selected === speed) {
this.show = false
return
}
this.selected = speed
this.show = false
this.$nextTick(() => this.$emit('change', speed))
}
},
mounted() {}
}
</script>
+100
View File
@@ -0,0 +1,100 @@
<template>
<modals-modal v-model="show" width="90%" height="100%">
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20 p-8" style="max-height: 75%" @click.stop>
<ui-text-input ref="input" v-model="search" @input="updateSearch" placeholder="Search" class="w-full text-lg" />
<div v-show="isFetching" class="w-full py-8 flex justify-center">
<p class="text-lg text-gray-400">Fetching...</p>
</div>
<div v-if="!isFetching && lastSearch && !items.length" class="w-full py-8 flex justify-center">
<p class="text-lg text-gray-400">Nothing found</p>
</div>
<template v-for="item in items">
<div class="py-2 border-b border-bg flex" :key="item.id" @click="clickItem(item)">
<cards-book-cover :audiobook="item.data" :width="50" />
<div class="flex-grow px-4 h-full">
<div class="w-full h-full">
<p class="text-base truncate">{{ item.data.book.title }}</p>
<p class="text-sm text-gray-400 truncate">{{ item.data.book.author }}</p>
</div>
</div>
</div>
</template>
</div>
</div>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean
},
data() {
return {
search: null,
searchTimeout: null,
lastSearch: null,
isFetching: false,
items: []
}
},
watch: {
value(newVal) {
if (newVal) {
this.$nextTick(this.setFocus())
}
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}
},
methods: {
clickItem(item) {
this.show = false
this.$router.push(`/audiobook/${item.id}`)
},
async runSearch(value) {
this.lastSearch = value
if (!this.lastSearch) {
this.items = []
return
}
this.isFetching = true
var results = await this.$axios.$get(`/api/audiobooks?q=${value}`).catch((error) => {
console.error('Search error', error)
return []
})
this.isFetching = false
this.items = results.map((res) => {
return {
id: res.id,
data: res,
type: 'audiobook'
}
})
},
updateSearch(val) {
clearTimeout(this.searchTimeout)
this.searchTimeout = setTimeout(() => {
this.runSearch(val)
}, 500)
},
setFocus() {
setTimeout(() => {
if (this.$refs.input) {
this.$refs.input.focus()
}
}, 100)
}
},
mounted() {}
}
</script>
+3 -2
View File
@@ -1,5 +1,5 @@
<template>
<button class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="loading" :type="type" :class="classList" @click="click">
<button class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @click="click">
<slot />
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
<!-- <span class="material-icons animate-spin">refresh</span> -->
@@ -23,7 +23,8 @@ export default {
},
paddingX: Number,
small: Boolean,
loading: Boolean
loading: Boolean,
disabled: Boolean
},
data() {
return {}
+14 -2
View File
@@ -1,5 +1,5 @@
<template>
<input v-model="input" :type="type" :disabled="disabled" :placeholder="placeholder" class="px-2 py-1 bg-bg border border-gray-600 outline-none rounded-sm" :class="disabled ? 'text-gray-300' : 'text-white'" />
<input v-model="input" ref="input" autofocus :type="type" :disabled="disabled" autocorrect="off" autocapitalize="none" autocomplete="off" :placeholder="placeholder" class="px-2 py-1 bg-bg border border-gray-600 outline-none rounded-sm" :class="disabled ? 'text-gray-300' : 'text-white'" @keyup="keyup" />
</template>
<script>
@@ -23,7 +23,19 @@ export default {
}
}
},
methods: {},
methods: {
focus() {
if (this.$refs.input) {
this.$refs.input.focus()
this.$refs.input.click()
}
},
keyup() {
if (this.$refs.input) {
this.input = this.$refs.input.value
}
}
},
mounted() {}
}
</script>
+103
View File
@@ -0,0 +1,103 @@
<template>
<div>
<nuxt-link v-if="isConnected" to="/account" class="p-2 bg-white bg-opacity-10 border border-white border-opacity-40 rounded-full h-11 w-11 flex items-center justify-center">
<span class="material-icons">person</span>
</nuxt-link>
<div v-else-if="processing" class="relative p-2 bg-warning bg-opacity-10 border border-warning border-opacity-40 rounded-full h-11 w-11 flex items-center justify-center">
<div class="loader-dots block relative w-10 h-2.5">
<div class="absolute top-0 mt-0.5 w-1.5 h-1.5 rounded-full bg-warning"></div>
<div class="absolute top-0 mt-0.5 w-1.5 h-1.5 rounded-full bg-warning"></div>
<div class="absolute top-0 mt-0.5 w-1.5 h-1.5 rounded-full bg-warning"></div>
<div class="absolute top-0 mt-0.5 w-1.5 h-1.5 rounded-full bg-warning"></div>
</div>
</div>
<nuxt-link v-else to="/connect" class="relative p-2 bg-warning bg-opacity-10 border border-warning border-opacity-40 rounded-full h-11 w-11 flex items-center justify-center">
<span class="material-icons">{{ networkIcon }}</span>
<!-- <div class="absolute top-0 left-0"> -->
<!-- <div class="absolute -top-5 -right-5 overflow-hidden">
<svg class="w-20 h-20 animate-spin" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path clip-rule="evenodd" d="M15.165 8.53a.5.5 0 01-.404.58A7 7 0 1023 16a.5.5 0 011 0 8 8 0 11-9.416-7.874.5.5 0 01.58.404z" fill="currentColor" fill-rule="evenodd" />
</svg>
</div> -->
</nuxt-link>
</div>
</template>
<script>
export default {
props: {},
data() {
return {
processing: false,
serverUrl: null,
isConnected: false
}
},
watch: {
networkConnected(newVal) {
if (newVal) {
this.init()
}
}
},
computed: {
user() {
return this.$store.state.user.user
},
networkIcon() {
if (!this.networkConnected) return 'signal_wifi_connected_no_internet_4'
return 'cloud_off'
},
networkConnected() {
return this.$store.state.networkConnected
}
},
methods: {
socketConnected(val) {
this.processing = false
this.isConnected = val
},
async init() {
if (this.isConnected) {
return
}
if (!this.$server) {
console.error('Invalid server not initialized')
return
}
if (!this.networkConnected) return
this.$server.on('connected', this.socketConnected)
var localServerUrl = await this.$localStore.getServerUrl()
var localUserToken = await this.$localStore.getToken()
if (localServerUrl) {
this.serverUrl = localServerUrl
// Server and Token are stored
if (localUserToken) {
this.processing = true
var success = await this.$server.connect(localServerUrl, localUserToken)
if (!success && !this.$server.url) {
this.processing = false
this.serverUrl = null
} else if (!success) {
this.processing = false
}
} else {
// Server only is stored
var success = await this.$server.check(this.serverUrl)
if (!success) {
console.error('Invalid server')
this.$server.setServerUrl(null)
}
}
}
}
},
mounted() {
this.init()
}
}
</script>
+9
View File
@@ -0,0 +1,9 @@
App/build
App/Pods
App/Podfile.lock
App/App/public
DerivedData
xcuserdata
# Cordova plugins for Capacitor
capacitor-cordova-ios-plugins
+417
View File
@@ -0,0 +1,417 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 48;
objects = {
/* Begin PBXBuildFile section */
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
4D8D410C26E17C3A00BA5F0D /* MyNativeAudio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D8D410B26E17C3A00BA5F0D /* MyNativeAudio.swift */; };
4D8D412E26E187E500BA5F0D /* MyNativeAudio.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D8D412D26E187E500BA5F0D /* MyNativeAudio.m */; };
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
4D8D410B26E17C3A00BA5F0D /* MyNativeAudio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyNativeAudio.swift; sourceTree = "<group>"; };
4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = "<group>"; };
4D8D412D26E187E500BA5F0D /* MyNativeAudio.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MyNativeAudio.m; sourceTree = "<group>"; };
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
504EC3011FED79650016851F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
isa = PBXGroup;
children = (
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
504EC2FB1FED79650016851F = {
isa = PBXGroup;
children = (
504EC3061FED79650016851F /* App */,
504EC3051FED79650016851F /* Products */,
7F8756D8B27F46E3366F6CEA /* Pods */,
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
);
sourceTree = "<group>";
};
504EC3051FED79650016851F /* Products */ = {
isa = PBXGroup;
children = (
504EC3041FED79650016851F /* App.app */,
);
name = Products;
sourceTree = "<group>";
};
504EC3061FED79650016851F /* App */ = {
isa = PBXGroup;
children = (
50379B222058CBB4000EE86E /* capacitor.config.json */,
504EC3071FED79650016851F /* AppDelegate.swift */,
504EC30B1FED79650016851F /* Main.storyboard */,
504EC30E1FED79650016851F /* Assets.xcassets */,
504EC3101FED79650016851F /* LaunchScreen.storyboard */,
504EC3131FED79650016851F /* Info.plist */,
2FAD9762203C412B000D30F8 /* config.xml */,
50B271D01FEDC1A000F3C39B /* public */,
4D8D410B26E17C3A00BA5F0D /* MyNativeAudio.swift */,
4D8D412D26E187E500BA5F0D /* MyNativeAudio.m */,
4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */,
);
path = App;
sourceTree = "<group>";
};
7F8756D8B27F46E3366F6CEA /* Pods */ = {
isa = PBXGroup;
children = (
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
);
name = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
504EC3031FED79650016851F /* App */ = {
isa = PBXNativeTarget;
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
buildPhases = (
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
504EC3001FED79650016851F /* Sources */,
504EC3011FED79650016851F /* Frameworks */,
504EC3021FED79650016851F /* Resources */,
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = App;
productName = App;
productReference = 504EC3041FED79650016851F /* App.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
504EC2FC1FED79650016851F /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 0920;
TargetAttributes = {
504EC3031FED79650016851F = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1240;
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
compatibilityVersion = "Xcode 8.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 504EC2FB1FED79650016851F;
productRefGroup = 504EC3051FED79650016851F /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
504EC3031FED79650016851F /* App */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
504EC3021FED79650016851F /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
50B271D11FEDC1A000F3C39B /* public in Resources */,
504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
504EC30D1FED79650016851F /* Main.storyboard in Resources */,
2FAD9763203C412B000D30F8 /* config.xml in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
504EC3001FED79650016851F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
4D8D412E26E187E500BA5F0D /* MyNativeAudio.m in Sources */,
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
4D8D410C26E17C3A00BA5F0D /* MyNativeAudio.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
504EC30B1FED79650016851F /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
504EC30C1FED79650016851F /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
504EC3111FED79650016851F /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
504EC3141FED79650016851F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
504EC3151FED79650016851F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
504EC3171FED79650016851F /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OBJC_BRIDGING_HEADER = "App/App-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
504EC3181FED79650016851F /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
SWIFT_OBJC_BRIDGING_HEADER = "App/App-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
isa = XCConfigurationList;
buildConfigurations = (
504EC3141FED79650016851F /* Debug */,
504EC3151FED79650016851F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = {
isa = XCConfigurationList;
buildConfigurations = (
504EC3171FED79650016851F /* Debug */,
504EC3181FED79650016851F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 504EC2FC1FED79650016851F /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:App.xcodeproj">
</FileRef>
</Workspace>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:App.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
+4
View File
@@ -0,0 +1,4 @@
//
// Use this file to import your target's public headers that you would like to expose to Swift.
//
+60
View File
@@ -0,0 +1,60 @@
import UIKit
import Capacitor
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
// Called when the app was launched with a url. Feel free to add additional processing here,
// but if you want the App API to support tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
// Called when the app was launched with an activity, including Universal Links.
// Feel free to add additional processing here, but if you want the App API to support
// tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
let statusBarRect = UIApplication.shared.statusBarFrame
guard let touchPoint = event?.allTouches?.first?.location(in: self.window) else { return }
if statusBarRect.contains(touchPoint) {
NotificationCenter.default.post(name: .capacitorStatusBarTapped, object: nil)
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 774 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Some files were not shown because too many files have changed in this diff Show More