diff --git a/Server.js b/Server.js index ec9408df..b4cb29a6 100644 --- a/Server.js +++ b/Server.js @@ -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}` } @@ -35,23 +37,34 @@ class Server extends EventEmitter { this.store.commit('user/setUser', user) if (user) { this.store.commit('user/setSettings', user.settings) - localStorage.setItem('userToken', user.token) + 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) @@ -60,7 +73,7 @@ class Server extends EventEmitter { } this.setServerUrl(serverUrl) - console.warn('Connect setting auth user', authRes) + this.setUser(authRes.user) this.connectSocket() @@ -103,6 +116,9 @@ class Server extends EventEmitter { logout() { this.setUser(null) + if (this.socket) { + this.socket.disconnect() + } } authorize(serverUrl, token) { @@ -138,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) diff --git a/android/app/build.gradle b/android/app/build.gradle index 5994b108..beea68e7 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "com.audiobookshelf.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 8 - versionName "0.4.0-beta" + versionCode 9 + versionName "0.8.0-beta" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 1ddfae6e..6295774a 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -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-network') + implementation project(':capacitor-storage') implementation project(':robingenz-capacitor-app-update') + implementation project(':capacitor-data-storage-sqlite') } diff --git a/android/app/src/main/assets/capacitor.plugins.json b/android/app/src/main/assets/capacitor.plugins.json index 9483b3b3..30c65280 100644 --- a/android/app/src/main/assets/capacitor.plugins.json +++ b/android/app/src/main/assets/capacitor.plugins.json @@ -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/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" } ] diff --git a/android/app/src/main/java/com/audiobookshelf/app/AudioDownloader.kt b/android/app/src/main/java/com/audiobookshelf/app/AudioDownloader.kt new file mode 100644 index 00000000..6146e7b2 --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/AudioDownloader.kt @@ -0,0 +1,320 @@ +package com.audiobookshelf.app + +import android.app.DownloadManager +import android.content.ContentUris +import android.content.Context +import android.net.Uri +import android.os.Environment +import android.provider.MediaStore +import android.util.Log +import android.widget.Toast +import com.getcapacitor.JSObject +import com.getcapacitor.Plugin +import com.getcapacitor.PluginCall +import com.getcapacitor.PluginMethod +import com.getcapacitor.annotation.CapacitorPlugin +import java.io.File + + +@CapacitorPlugin(name = "AudioDownloader") +class AudioDownloader : Plugin() { + private val tag = "AudioDownloader" + + lateinit var mainActivity:MainActivity + lateinit var downloadManager:DownloadManager + + data class AudiobookDownload(val url: String, val filename:String, val downloadId: Long) + var downloads:MutableList = mutableListOf() + + data class CoverItem(val name:String, val coverUrl:String) + data class AudiobookItem(val id: Long, val uri: Uri, val name: String, val size: Int, val duration: Int, val coverUrl: String) { + fun toJSObject() : JSObject { + var obj = JSObject() + obj.put("id", this.id) + obj.put("uri", this.uri) + obj.put("name", this.name) + obj.put("size", this.size) + obj.put("duration", this.duration) + obj.put("coverUrl", this.coverUrl) + return obj + } + } + var audiobookItems:MutableList = mutableListOf() + + override fun load() { + mainActivity = (activity as MainActivity) + downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + + var recieverEvent : (evt: String, id: Long) -> Unit = { evt: String, id: Long -> + Log.d(tag, "RECEIVE EVT $evt $id") + if (evt == "complete") { + var path = downloadManager.getUriForDownloadedFile(id) + + var download = downloads.find { it.downloadId == id } + var filename = download?.filename + + var jsobj = JSObject() + jsobj.put("downloadId", id) + jsobj.put("contentUrl", path) + jsobj.put("filename", filename) + notifyListeners("onDownloadComplete", jsobj) + downloads = downloads.filter { it.downloadId != id } as MutableList + } + if (evt == "clicked") { + Log.d(tag, "Clicked $id back in the audiodownloader") + } + } + + mainActivity.registerBroadcastReceiver(recieverEvent) + } + + fun loadAudiobooks() { + var covers = loadCovers() + + val projection = arrayOf( + MediaStore.Audio.Media._ID, + MediaStore.Audio.Media.DISPLAY_NAME, + MediaStore.Audio.Media.DURATION, + MediaStore.Audio.Media.SIZE, + MediaStore.Audio.Media.IS_AUDIOBOOK + ) + + var _audiobookItems:MutableList = mutableListOf() + val selection = "${MediaStore.Audio.Media.IS_AUDIOBOOK} == ?" + val selectionArgs = arrayOf("1") + val sortOrder = "${MediaStore.Audio.Media.DISPLAY_NAME} ASC" + + activity.applicationContext.contentResolver.query( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + projection, + selection, + selectionArgs, + sortOrder + )?.use { cursor -> + + val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID) + val nameColumn = + cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME) + val durationColumn = + cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION) + val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE) + val isAudiobookColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.IS_AUDIOBOOK) + + while (cursor.moveToNext()) { + val id = cursor.getLong(idColumn) + val name = cursor.getString(nameColumn) + val duration = cursor.getInt(durationColumn) + val size = cursor.getInt(sizeColumn) + var isAudiobook = cursor.getInt(isAudiobookColumn) + + if (isAudiobook == 1) { + val contentUri: Uri = ContentUris.withAppendedId( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + id + ) + + Log.d(tag, "Got Content FRom MEdia STORE $id $contentUri, Name: $name, Dur: $duration, Size: $size") + var audiobookId = File(name).nameWithoutExtension + var coverItem:CoverItem? = covers.find{it.name == audiobookId} + var coverUrl = coverItem?.coverUrl ?: "" + + _audiobookItems.add(AudiobookItem(id, contentUri, name, duration, size, coverUrl)) + } + } + audiobookItems = _audiobookItems + + var audiobookObjs:List = _audiobookItems.map{ it.toJSObject() } + + var mediaItemNoticePayload = JSObject() + mediaItemNoticePayload.put("items", audiobookObjs) + notifyListeners("onMediaLoaded", mediaItemNoticePayload) + } + } + + fun loadCovers() : MutableList { + val projection = arrayOf( + MediaStore.Images.Media._ID, + MediaStore.Images.Media.DISPLAY_NAME + ) + val sortOrder = "${MediaStore.Images.Media.DISPLAY_NAME} ASC" + + var coverItems:MutableList = mutableListOf() + + activity.applicationContext.contentResolver.query( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + projection, + null, + null, + sortOrder + )?.use { cursor -> + val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID) + val nameColumn = + cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME) + + while (cursor.moveToNext()) { + val id = cursor.getLong(idColumn) + val filename = cursor.getString(nameColumn) + val contentUri: Uri = ContentUris.withAppendedId( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + id + ) + + var name = File(filename).nameWithoutExtension + Log.d(tag, "Got IMAGE FRom Media STORE $id $contentUri, Name: $name") + + var coverItem = CoverItem(name, contentUri.toString()) + coverItems.add(coverItem) + } + } + return coverItems + } + + @PluginMethod + fun load(call: PluginCall) { + loadAudiobooks() + call.resolve() + } + + @PluginMethod + fun downloadCover(call: PluginCall) { + var url = call.data.getString("downloadUrl", "unknown").toString() + var title = call.data.getString("title", "Cover").toString() + var filename = call.data.getString("filename", "audiobook.jpg").toString() + + Log.d(tag, "Called download cover: $url") + + var dlRequest = DownloadManager.Request(Uri.parse(url)) + dlRequest.setTitle("Cover Art: $title") + dlRequest.setDescription("Cover art for audiobook") + dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION) + dlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_AUDIOBOOKS, filename) + var downloadId = downloadManager.enqueue(dlRequest) + + var progressReceiver : (prog: Long) -> Unit = { prog: Long -> + // + } + + var doneReceiver : (success: Boolean) -> Unit = { success: Boolean -> + var jsobj = JSObject() + if (success) { + var path = downloadManager.getUriForDownloadedFile(downloadId) + jsobj.put("url", path) + call.resolve(jsobj) + } else { + jsobj.put("failed", true) + call.resolve(jsobj) + } + } + + var progressUpdater = DownloadProgressUpdater(downloadManager, downloadId, progressReceiver, doneReceiver) + progressUpdater.run() + } + + @PluginMethod + fun download(call: PluginCall) { + var url = call.data.getString("downloadUrl", "unknown").toString() + var title = call.data.getString("title", "Audiobook").toString() + var filename = call.data.getString("filename", "audiobook.mp3").toString() + + Log.d(tag, "Called download: $url") + + var dlRequest = DownloadManager.Request(Uri.parse(url)) + dlRequest.setTitle(title) + dlRequest.setDescription("Downloading to Audiobooks directory") + dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION) + + dlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_AUDIOBOOKS, filename) + + var downloadId = downloadManager.enqueue(dlRequest) + + var download = AudiobookDownload(url, filename, downloadId) + downloads.add(download) + + var progressReceiver : (prog: Long) -> Unit = { prog: Long -> + var jsobj = JSObject() + jsobj.put("filename", filename) + jsobj.put("downloadId", downloadId) + jsobj.put("progress", prog) + notifyListeners("onDownloadProgress", jsobj) + } + + var doneReceiver : (success: Boolean) -> Unit = { success: Boolean -> + Log.d(tag, "RECIEVER DONE, SUCCES? $success") + } + + var progressUpdater = DownloadProgressUpdater(downloadManager, downloadId, progressReceiver, doneReceiver) + progressUpdater.run() + + val ret = JSObject() + ret.put("value", downloadId) + call.resolve(ret) + } + + @PluginMethod + fun delete(call:PluginCall) { + var filename = call.data.getString("filename", "audiobook.mp3").toString() + var url = call.data.getString("url", "").toString() + var coverUrl = call.data.getString("coverUrl", "").toString() + + Log.d(tag, "Called delete file $filename $url") + + var contentResolver = activity.applicationContext.contentResolver + contentResolver.delete(Uri.parse(url), null, null) + + if (coverUrl != "") { + contentResolver.delete(Uri.parse(coverUrl), null, null) + } + + call.resolve() + } + + internal class DownloadProgressUpdater(private val manager: DownloadManager, private val downloadId: Long, private var receiver:(Long) -> Unit, private var doneReceiver:(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 + while (keepRunning) { + Thread.sleep(500) + + 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)) + } + + val downloadStatus = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_STATUS)) + val bytesDownloadedSoFar = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)) + + if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL || downloadStatus == DownloadManager.STATUS_FAILED) { + if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL) { + doneReceiver(true) + } else { + doneReceiver(false) + } + keepRunning = false + this.interrupt() + } else { + //update progress + val percentProgress = ((bytesDownloadedSoFar * 100L) / totalBytes) + receiver(percentProgress) + } + + } else { + Log.e(TAG, "NOT FOUND IN QUERY") + keepRunning = false + } + } + } + } + + } +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/Audiobook.kt b/android/app/src/main/java/com/audiobookshelf/app/Audiobook.kt index 1dca6325..8117e547 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/Audiobook.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/Audiobook.kt @@ -16,10 +16,14 @@ class Audiobook { 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() @@ -34,7 +38,22 @@ class Audiobook { 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) + } } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt b/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt index 22a673ac..b640a3c9 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt @@ -1,15 +1,16 @@ package com.audiobookshelf.app -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection +import android.app.DownloadManager +import android.content.* import android.os.Bundle +import android.os.Handler import android.os.IBinder +import android.os.Looper import android.util.Log -import com.example.myapp.MyNativeAudio +import android.widget.Toast import com.getcapacitor.BridgeActivity + class MainActivity : BridgeActivity() { private val tag = "MainActivity" @@ -18,11 +19,45 @@ class MainActivity : BridgeActivity() { private lateinit var mConnection : ServiceConnection lateinit var pluginCallback : () -> Unit + lateinit var downloaderCallback : (String, Long) -> Unit + + val broadcastReceiver = object: BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + when (intent?.action) { + DownloadManager.ACTION_DOWNLOAD_COMPLETE -> { + var thisdlid = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0L) + + downloaderCallback("complete", thisdlid) + + Log.d(tag, "DOWNNLAOD COMPELTE $thisdlid") + Toast.makeText(this@MainActivity, "Download Completed $thisdlid", Toast.LENGTH_SHORT) + } + DownloadManager.ACTION_NOTIFICATION_CLICKED -> { + var thisdlid = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0L) + downloaderCallback("clicked", thisdlid) + + Log.d(tag, "CLICKED NOTFIFICAIONT $thisdlid") + Toast.makeText(this@MainActivity, "Download CLICKED $thisdlid", Toast.LENGTH_SHORT) + } + } + } + } 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 +97,8 @@ class MainActivity : BridgeActivity() { val stopIntent = Intent(this, PlayerNotificationService::class.java) stopService(stopIntent) } + + fun registerBroadcastReceiver(cb: (String, Long) -> Unit) { + downloaderCallback = cb + } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/MyNativeAudio.kt b/android/app/src/main/java/com/audiobookshelf/app/MyNativeAudio.kt index 225dc52c..fd9fcada 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/MyNativeAudio.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/MyNativeAudio.kt @@ -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) } } @@ -109,6 +114,7 @@ class MyNativeAudio : Plugin() { call.resolve() } } + @PluginMethod fun seekBackward(call: PluginCall) { var amount:Long = call.getString("amount", "0")!!.toLong() @@ -117,6 +123,7 @@ class MyNativeAudio : Plugin() { call.resolve() } } + @PluginMethod fun setPlaybackSpeed(call: PluginCall) { var playbackSpeed:Float = call.getFloat("speed", 1.0f)!! @@ -126,6 +133,7 @@ class MyNativeAudio : Plugin() { call.resolve() } } + @PluginMethod fun terminateStream(call: PluginCall) { Handler(Looper.getMainLooper()).post() { diff --git a/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt index e2c0aa87..89b78850 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/PlayerNotificationService.kt @@ -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 @@ -24,10 +25,15 @@ 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 @@ -217,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) @@ -280,6 +286,9 @@ class PlayerNotificationService : Service() { } } + + + private fun setPlayerListeners() { mPlayer.addListener(object : Player.Listener { override fun onPlayerError(error: PlaybackException) { @@ -351,28 +360,42 @@ 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() diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index f85ee9ce..99af1d75 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -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-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') diff --git a/assets/app.css b/assets/app.css index 986d774e..d0047f8a 100644 --- a/assets/app.css +++ b/assets/app.css @@ -1,3 +1,5 @@ +@import "./fonts.css"; + .box-shadow-md { box-shadow: 2px 8px 6px #111111aa; } diff --git a/assets/fonts.css b/assets/fonts.css new file mode 100644 index 00000000..188624eb --- /dev/null +++ b/assets/fonts.css @@ -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; +} \ No newline at end of file diff --git a/components/AudioPlayerMini.vue b/components/AudioPlayerMini.vue index 716a5b70..55a49541 100644 --- a/components/AudioPlayerMini.vue +++ b/components/AudioPlayerMini.vue @@ -202,7 +202,17 @@ export default { this.isResetting = false this.initObject = { ...audiobookStreamData } this.currentPlaybackRate = this.initObject.playbackSpeed - MyNativeAudio.initPlayer(this.initObject) + 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) { @@ -210,7 +220,17 @@ export default { return } this.isResetting = false - MyNativeAudio.initPlayer(this.initObject) + 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() diff --git a/components/app/Appbar.vue b/components/app/Appbar.vue index 1514c9ba..dcfcebe9 100644 --- a/components/app/Appbar.vue +++ b/components/app/Appbar.vue @@ -13,14 +13,18 @@
- + source + + + +
@@ -89,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); + } +} \ No newline at end of file diff --git a/components/app/Bookshelf.vue b/components/app/Bookshelf.vue index 80843d0d..4db9d3fe 100644 --- a/components/app/Bookshelf.vue +++ b/components/app/Bookshelf.vue @@ -4,7 +4,7 @@