mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-09 05:18:40 +02:00
Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f60a552e5 | ||
|
|
3b34029ca2 | ||
|
|
5f6a1ef7e9 | ||
|
|
b51f65d2a4 | ||
|
|
dc9526beb3 | ||
|
|
7188089f63 | ||
|
|
db1d6da491 | ||
|
|
329a51f886 | ||
|
|
0c0badbae4 | ||
|
|
2da272c270 | ||
|
|
25ec5b3dcb | ||
|
|
d9cdc16ff9 | ||
|
|
066b72d54d | ||
|
|
e7c913643a | ||
|
|
9847ed9fcb | ||
|
|
721f812df0 | ||
|
|
a58965306a | ||
|
|
2b5377db04 | ||
|
|
7fe3849705 | ||
|
|
f998deb725 | ||
|
|
cb2aaede67 | ||
|
|
e07e7f70d6 | ||
|
|
48313b60c1 | ||
|
|
3be13025a8 | ||
|
|
505717e580 | ||
|
|
b4bb6bf81b | ||
|
|
6a8d901ce1 | ||
|
|
68f3f0e276 | ||
|
|
ddf8e66a08 | ||
|
|
5b7a492a48 |
@@ -29,8 +29,8 @@ android {
|
|||||||
applicationId "com.audiobookshelf.app"
|
applicationId "com.audiobookshelf.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 81
|
versionCode 84
|
||||||
versionName "0.9.50-beta"
|
versionName "0.9.53-beta"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
@@ -111,7 +111,7 @@ dependencies {
|
|||||||
implementation 'io.github.pilgr:paperdb:2.7.2'
|
implementation 'io.github.pilgr:paperdb:2.7.2'
|
||||||
|
|
||||||
// Simple Storage
|
// Simple Storage
|
||||||
implementation "com.anggrayudi:storage:0.13.0"
|
implementation "com.anggrayudi:storage:0.14.0"
|
||||||
|
|
||||||
// OK HTTP
|
// OK HTTP
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
|
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import androidx.core.app.ActivityCompat
|
|||||||
import com.anggrayudi.storage.SimpleStorage
|
import com.anggrayudi.storage.SimpleStorage
|
||||||
import com.anggrayudi.storage.SimpleStorageHelper
|
import com.anggrayudi.storage.SimpleStorageHelper
|
||||||
import com.audiobookshelf.app.data.AbsDatabase
|
import com.audiobookshelf.app.data.AbsDatabase
|
||||||
|
import com.audiobookshelf.app.data.DbManager
|
||||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||||
import com.audiobookshelf.app.plugins.AbsAudioPlayer
|
import com.audiobookshelf.app.plugins.AbsAudioPlayer
|
||||||
import com.audiobookshelf.app.plugins.AbsDownloader
|
import com.audiobookshelf.app.plugins.AbsDownloader
|
||||||
import com.audiobookshelf.app.plugins.AbsFileSystem
|
import com.audiobookshelf.app.plugins.AbsFileSystem
|
||||||
import com.getcapacitor.BridgeActivity
|
import com.getcapacitor.BridgeActivity
|
||||||
import io.paperdb.Paper
|
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : BridgeActivity() {
|
class MainActivity : BridgeActivity() {
|
||||||
@@ -53,14 +53,11 @@ class MainActivity : BridgeActivity() {
|
|||||||
// .build())
|
// .build())
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
Log.d(tag, "onCreate")
|
Log.d(tag, "onCreate")
|
||||||
|
|
||||||
// Grant full storage access for testing
|
DbManager.initialize(applicationContext)
|
||||||
// var ss = SimpleStorage(this)
|
|
||||||
// ss.requestFullStorageAccess()
|
|
||||||
|
|
||||||
var permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
val permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
if (permission != PackageManager.PERMISSION_GRANTED) {
|
if (permission != PackageManager.PERMISSION_GRANTED) {
|
||||||
ActivityCompat.requestPermissions(this,
|
ActivityCompat.requestPermissions(this,
|
||||||
PERMISSIONS_ALL,
|
PERMISSIONS_ALL,
|
||||||
@@ -71,8 +68,6 @@ class MainActivity : BridgeActivity() {
|
|||||||
registerPlugin(AbsDownloader::class.java)
|
registerPlugin(AbsDownloader::class.java)
|
||||||
registerPlugin(AbsFileSystem::class.java)
|
registerPlugin(AbsFileSystem::class.java)
|
||||||
registerPlugin(AbsDatabase::class.java)
|
registerPlugin(AbsDatabase::class.java)
|
||||||
|
|
||||||
Paper.init(applicationContext)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
@@ -97,9 +92,7 @@ class MainActivity : BridgeActivity() {
|
|||||||
foregroundService = mLocalBinder.getService()
|
foregroundService = mLocalBinder.getService()
|
||||||
|
|
||||||
// Let NativeAudio know foreground service is ready and setup event listener
|
// Let NativeAudio know foreground service is ready and setup event listener
|
||||||
if (pluginCallback != null) {
|
pluginCallback()
|
||||||
pluginCallback()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
package com.audiobookshelf.app.data
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.plugins.AbsDownloader
|
import com.audiobookshelf.app.plugins.AbsDownloader
|
||||||
import io.paperdb.Paper
|
import io.paperdb.Paper
|
||||||
import org.json.JSONObject
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class DbManager {
|
class DbManager {
|
||||||
val tag = "DbManager"
|
val tag = "DbManager"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var isDbInitialized = false
|
||||||
|
|
||||||
|
fun initialize(ctx: Context) {
|
||||||
|
if (isDbInitialized) return
|
||||||
|
Paper.init(ctx)
|
||||||
|
isDbInitialized = true
|
||||||
|
Log.i("DbManager", "Initialized Paper db")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getDeviceData(): DeviceData {
|
fun getDeviceData(): DeviceData {
|
||||||
return Paper.book("device").read("data") ?: DeviceData(mutableListOf(), null, null, DeviceSettings.default())
|
return Paper.book("device").read("data") ?: DeviceData(mutableListOf(), null, null, DeviceSettings.default())
|
||||||
}
|
}
|
||||||
@@ -34,7 +45,7 @@ class DbManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLocalLibraryItemByLLId(libraryItemId:String):LocalLibraryItem? {
|
fun getLocalLibraryItemByLId(libraryItemId:String):LocalLibraryItem? {
|
||||||
return getLocalLibraryItems().find { it.libraryItemId == libraryItemId }
|
return getLocalLibraryItems().find { it.libraryItemId == libraryItemId }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +213,11 @@ class DbManager {
|
|||||||
val localLibraryItems = getLocalLibraryItems()
|
val localLibraryItems = getLocalLibraryItems()
|
||||||
localMediaProgress.forEach {
|
localMediaProgress.forEach {
|
||||||
val matchingLLI = localLibraryItems.find { lli -> lli.id == it.localLibraryItemId }
|
val matchingLLI = localLibraryItems.find { lli -> lli.id == it.localLibraryItemId }
|
||||||
if (matchingLLI == null) {
|
if (!it.id.startsWith("local")) {
|
||||||
|
// A bug on the server when syncing local media progress was replacing the media progress id causing duplicate progress. Remove them.
|
||||||
|
Log.d(tag, "cleanLocalMediaProgress: Invalid local media progress does not start with 'local' (fixed on server 2.0.24)")
|
||||||
|
Paper.book("localMediaProgress").delete(it.id)
|
||||||
|
} else if (matchingLLI == null) {
|
||||||
Log.d(tag, "cleanLocalMediaProgress: No matching local library item for local media progress ${it.id} - removing")
|
Log.d(tag, "cleanLocalMediaProgress: No matching local library item for local media progress ${it.id} - removing")
|
||||||
Paper.book("localMediaProgress").delete(it.id)
|
Paper.book("localMediaProgress").delete(it.id)
|
||||||
} else if (matchingLLI.isPodcast) {
|
} else if (matchingLLI.isPodcast) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.audiobookshelf.app.data
|
|||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class LocalMediaProgress(
|
data class LocalMediaProgress(
|
||||||
@@ -22,6 +23,9 @@ data class LocalMediaProgress(
|
|||||||
var libraryItemId:String?,
|
var libraryItemId:String?,
|
||||||
var episodeId:String?
|
var episodeId:String?
|
||||||
) {
|
) {
|
||||||
|
@get:JsonIgnore
|
||||||
|
val progressPercent get() = if (progress.isNaN()) 0 else (progress * 100).roundToInt()
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun updateIsFinished(finished:Boolean) {
|
fun updateIsFinished(finished:Boolean) {
|
||||||
if (isFinished != finished) { // If finished changed then set progress
|
if (isFinished != finished) { // If finished changed then set progress
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
package com.audiobookshelf.app.media
|
package com.audiobookshelf.app.media
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.support.v4.media.MediaBrowserCompat
|
import android.support.v4.media.MediaBrowserCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
|
import com.getcapacitor.JSObject
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import io.paperdb.Paper
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.json.JSONException
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
||||||
val tag = "MediaManager"
|
val tag = "MediaManager"
|
||||||
|
|
||||||
var isPaperInitialized = false
|
var serverLibraryItems = mutableListOf<LibraryItem>()
|
||||||
|
|
||||||
var serverLibraryItems = listOf<LibraryItem>()
|
|
||||||
var selectedLibraryId = ""
|
var selectedLibraryId = ""
|
||||||
|
|
||||||
var selectedLibraryItemWrapper:LibraryItemWrapper? = null
|
var selectedLibraryItemWrapper:LibraryItemWrapper? = null
|
||||||
@@ -29,18 +29,35 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
var serverLibraries = listOf<Library>()
|
var serverLibraries = listOf<Library>()
|
||||||
var serverConfigIdUsed:String? = null
|
var serverConfigIdUsed:String? = null
|
||||||
|
|
||||||
fun initializeAndroidAuto() {
|
var userSettingsPlaybackRate:Float? = null
|
||||||
if (!isPaperInitialized) {
|
|
||||||
Log.d(tag, "Android Auto started when MainActivity was never started - initializing Paper")
|
|
||||||
Paper.init(ctx)
|
|
||||||
isPaperInitialized = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getIsLibrary(id:String) : Boolean {
|
fun getIsLibrary(id:String) : Boolean {
|
||||||
return serverLibraries.find { it.id == id } != null
|
return serverLibraries.find { it.id == id } != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getSavedPlaybackRate():Float {
|
||||||
|
if (userSettingsPlaybackRate != null) {
|
||||||
|
return userSettingsPlaybackRate ?: 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
val sharedPrefs = ctx.getSharedPreferences("CapacitorStorage", Activity.MODE_PRIVATE)
|
||||||
|
if (sharedPrefs != null) {
|
||||||
|
val userSettingsPref = sharedPrefs.getString("userSettings", null)
|
||||||
|
if (userSettingsPref != null) {
|
||||||
|
try {
|
||||||
|
val userSettings = JSObject(userSettingsPref)
|
||||||
|
if (userSettings.has("playbackRate")) {
|
||||||
|
userSettingsPlaybackRate = userSettings.getDouble("playbackRate").toFloat()
|
||||||
|
return userSettingsPlaybackRate ?: 1f
|
||||||
|
}
|
||||||
|
} catch(je:JSONException) {
|
||||||
|
Log.e(tag, "Failed to parse userSettings JSON ${je.localizedMessage}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1f
|
||||||
|
}
|
||||||
|
|
||||||
fun checkResetServerItems() {
|
fun checkResetServerItems() {
|
||||||
// When opening android auto need to check if still connected to server
|
// When opening android auto need to check if still connected to server
|
||||||
// and reset any server data already set
|
// and reset any server data already set
|
||||||
@@ -50,7 +67,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
serverPodcastEpisodes = listOf()
|
serverPodcastEpisodes = listOf()
|
||||||
serverLibraryCategories = listOf()
|
serverLibraryCategories = listOf()
|
||||||
serverLibraries = listOf()
|
serverLibraries = listOf()
|
||||||
serverLibraryItems = listOf()
|
serverLibraryItems = mutableListOf()
|
||||||
selectedLibraryId = ""
|
selectedLibraryId = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,7 +91,11 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
|
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
|
||||||
if (libraryItemsWithAudio.isNotEmpty()) selectedLibraryId = libraryId
|
if (libraryItemsWithAudio.isNotEmpty()) selectedLibraryId = libraryId
|
||||||
|
|
||||||
serverLibraryItems = libraryItemsWithAudio
|
libraryItemsWithAudio.forEach { libraryItem ->
|
||||||
|
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||||
|
serverLibraryItems.add(libraryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
cb(libraryItemsWithAudio)
|
cb(libraryItemsWithAudio)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,6 +243,17 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
|
|
||||||
// Only using book or podcast library categories for now
|
// Only using book or podcast library categories for now
|
||||||
libraryCategories.forEach {
|
libraryCategories.forEach {
|
||||||
|
|
||||||
|
// Add items in continue listening to serverLibraryItems
|
||||||
|
if (it.id == "continue-listening") {
|
||||||
|
it.entities.forEach { libraryItemWrapper ->
|
||||||
|
val libraryItem = libraryItemWrapper as LibraryItem
|
||||||
|
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||||
|
serverLibraryItems.add(libraryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Log.d(tag, "Found library category ${it.label} with type ${it.type}")
|
// Log.d(tag, "Found library category ${it.label} with type ${it.type}")
|
||||||
if (it.type == library.mediaType) {
|
if (it.type == library.mediaType) {
|
||||||
// Log.d(tag, "Using library category ${it.id}")
|
// Log.d(tag, "Using library category ${it.id}")
|
||||||
@@ -276,19 +308,18 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun play(libraryItemWrapper:LibraryItemWrapper, episode:PodcastEpisode?, playItemRequestPayload:PlayItemRequestPayload, cb: (PlaybackSession?) -> Unit) {
|
fun play(libraryItemWrapper:LibraryItemWrapper, episode:PodcastEpisode?, playItemRequestPayload:PlayItemRequestPayload, cb: (PlaybackSession?) -> Unit) {
|
||||||
if (libraryItemWrapper is LocalLibraryItem) {
|
if (libraryItemWrapper is LocalLibraryItem) {
|
||||||
val localLibraryItem = libraryItemWrapper as LocalLibraryItem
|
cb(libraryItemWrapper.getPlaybackSession(episode))
|
||||||
cb(localLibraryItem.getPlaybackSession(episode))
|
} else {
|
||||||
} else {
|
val libraryItem = libraryItemWrapper as LibraryItem
|
||||||
val libraryItem = libraryItemWrapper as LibraryItem
|
apiHandler.playLibraryItem(libraryItem.id,episode?.id ?: "", playItemRequestPayload) {
|
||||||
apiHandler.playLibraryItem(libraryItem.id,episode?.id ?: "",playItemRequestPayload) {
|
if (it == null) {
|
||||||
if (it == null) {
|
cb(null)
|
||||||
cb(null)
|
} else {
|
||||||
} else {
|
cb(it)
|
||||||
cb(it)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun levenshtein(lhs : CharSequence, rhs : CharSequence) : Int {
|
private fun levenshtein(lhs : CharSequence, rhs : CharSequence) : Int {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.content.ContentResolver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.AnyRes
|
import androidx.annotation.AnyRes
|
||||||
import com.audiobookshelf.app.R
|
import com.audiobookshelf.app.R
|
||||||
import com.audiobookshelf.app.data.Library
|
import com.audiobookshelf.app.data.Library
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import com.audiobookshelf.app.device.DeviceManager
|
|||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
data class MediaProgressSyncData(
|
data class MediaProgressSyncData(
|
||||||
var timeListened:Long, // seconds
|
var timeListened:Long, // seconds
|
||||||
@@ -56,7 +55,9 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
if (playerNotificationService.currentPlayer.isPlaying) {
|
if (playerNotificationService.currentPlayer.isPlaying) {
|
||||||
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
||||||
sync(currentTime)
|
sync(currentTime) {
|
||||||
|
Log.d(tag, "Sync complete")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,8 +68,9 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
|
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
|
||||||
|
|
||||||
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
||||||
sync(currentTime)
|
sync(currentTime) {
|
||||||
reset()
|
reset()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun syncFromServerProgress(mediaProgress: MediaProgress) {
|
fun syncFromServerProgress(mediaProgress: MediaProgress) {
|
||||||
@@ -81,10 +83,10 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sync(currentTime:Double) {
|
fun sync(currentTime:Double, cb: () -> Unit) {
|
||||||
val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
|
val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
|
||||||
if (diffSinceLastSync < 1000L) {
|
if (diffSinceLastSync < 1000L) {
|
||||||
return
|
return cb()
|
||||||
}
|
}
|
||||||
val listeningTimeToAdd = diffSinceLastSync / 1000L
|
val listeningTimeToAdd = diffSinceLastSync / 1000L
|
||||||
lastSyncTime = System.currentTimeMillis()
|
lastSyncTime = System.currentTimeMillis()
|
||||||
@@ -92,6 +94,12 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
val syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime)
|
val syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime)
|
||||||
|
|
||||||
currentPlaybackSession?.syncData(syncData)
|
currentPlaybackSession?.syncData(syncData)
|
||||||
|
|
||||||
|
if (currentPlaybackSession?.progress?.isNaN() == true) {
|
||||||
|
Log.e(tag, "Current Playback Session invalid progress ${currentPlaybackSession?.progress} | Current Time: ${currentPlaybackSession?.currentTime} | Duration: ${currentPlaybackSession?.getTotalDuration()}")
|
||||||
|
return cb()
|
||||||
|
}
|
||||||
|
|
||||||
if (currentIsLocal) {
|
if (currentIsLocal) {
|
||||||
// Save local progress sync
|
// Save local progress sync
|
||||||
currentPlaybackSession?.let {
|
currentPlaybackSession?.let {
|
||||||
@@ -99,16 +107,29 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
saveLocalProgress(it)
|
saveLocalProgress(it)
|
||||||
|
|
||||||
// Local library item is linked to a server library item
|
// Local library item is linked to a server library item
|
||||||
if (!it.libraryItemId.isNullOrEmpty()) {
|
// Send sync to server also if connected to this server and local item belongs to this server
|
||||||
// Send sync to server also if connected to this server and local item belongs to this server
|
if (!it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
|
||||||
if (it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
|
apiHandler.sendLocalProgressSync(it) { syncSuccess ->
|
||||||
apiHandler.sendLocalProgressSync(it) {
|
Log.d(
|
||||||
Log.d(
|
tag,
|
||||||
tag,
|
"Local progress sync data sent to server $currentDisplayTitle for time $currentTime"
|
||||||
"Local progress sync data sent to server $currentDisplayTitle for time $currentTime"
|
)
|
||||||
)
|
if (syncSuccess) {
|
||||||
|
failedSyncs = 0
|
||||||
|
playerNotificationService.alertSyncSuccess()
|
||||||
|
} else {
|
||||||
|
failedSyncs++
|
||||||
|
if (failedSyncs == 2) {
|
||||||
|
playerNotificationService.alertSyncFailing() // Show alert in client
|
||||||
|
failedSyncs = 0
|
||||||
|
}
|
||||||
|
Log.e(tag, "Local Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cb()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
cb()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -116,14 +137,16 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
if (it) {
|
if (it) {
|
||||||
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
|
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
|
||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
|
playerNotificationService.alertSyncSuccess()
|
||||||
} else {
|
} else {
|
||||||
failedSyncs++
|
failedSyncs++
|
||||||
if (failedSyncs == 2) {
|
if (failedSyncs == 2) {
|
||||||
playerNotificationService.alertSyncFailing() // Show alert in client
|
playerNotificationService.alertSyncFailing() // Show alert in client
|
||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
}
|
}
|
||||||
Log.d(tag, "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
|
Log.e(tag, "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
|
||||||
}
|
}
|
||||||
|
cb()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,9 +163,14 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession)
|
currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession)
|
||||||
}
|
}
|
||||||
currentLocalMediaProgress?.let {
|
currentLocalMediaProgress?.let {
|
||||||
DeviceManager.dbManager.saveLocalMediaProgress(it)
|
if (it.progress.isNaN()) {
|
||||||
playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it)
|
Log.e(tag, "Invalid progress on local media progress")
|
||||||
Log.d(tag, "Saved Local Progress Current Time: ID ${it.id} | ${it.currentTime} | Duration ${it.duration} | Progress ${(it.progress * 100).roundToInt()}%")
|
} else {
|
||||||
|
DeviceManager.dbManager.saveLocalMediaProgress(it)
|
||||||
|
playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it)
|
||||||
|
|
||||||
|
Log.d(tag, "Saved Local Progress Current Time: ID ${it.id} | ${it.currentTime} | Duration ${it.duration} | Progress ${it.progressPercent}%")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import android.os.Handler
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.os.Message
|
import android.os.Message
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
|
import com.audiobookshelf.app.R
|
||||||
import com.audiobookshelf.app.data.LibraryItemWrapper
|
import com.audiobookshelf.app.data.LibraryItemWrapper
|
||||||
import com.audiobookshelf.app.data.PodcastEpisode
|
import com.audiobookshelf.app.data.PodcastEpisode
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -28,8 +30,9 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
if (it == null) {
|
if (it == null) {
|
||||||
Log.e(tag, "Failed to play library item")
|
Log.e(tag, "Failed to play library item")
|
||||||
} else {
|
} else {
|
||||||
|
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,true,null)
|
playerNotificationService.preparePlayer(it,true, playbackRate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,8 +56,9 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
if (it == null) {
|
if (it == null) {
|
||||||
Log.e(tag, "Failed to play library item")
|
Log.e(tag, "Failed to play library item")
|
||||||
} else {
|
} else {
|
||||||
|
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it, true, null)
|
playerNotificationService.preparePlayer(it, true, playbackRate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,8 +118,9 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
if (it == null) {
|
if (it == null) {
|
||||||
Log.e(tag, "Failed to play library item")
|
Log.e(tag, "Failed to play library item")
|
||||||
} else {
|
} else {
|
||||||
|
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it, true, null)
|
playerNotificationService.preparePlayer(it, true, playbackRate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,4 +219,12 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Example Using a custom action in android auto
|
||||||
|
// override fun onCustomAction(action: String?, extras: Bundle?) {
|
||||||
|
// super.onCustomAction(action, extras)
|
||||||
|
//
|
||||||
|
// if ("com.audiobookshelf.app.PLAYBACK_RATE" == action) {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-3
@@ -34,8 +34,9 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
|||||||
if (it == null) {
|
if (it == null) {
|
||||||
Log.e(tag, "Failed to play library item")
|
Log.e(tag, "Failed to play library item")
|
||||||
} else {
|
} else {
|
||||||
|
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it, playWhenReady, null)
|
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,8 +62,9 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
|||||||
if (it == null) {
|
if (it == null) {
|
||||||
Log.e(tag, "Failed to play library item")
|
Log.e(tag, "Failed to play library item")
|
||||||
} else {
|
} else {
|
||||||
|
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it, playWhenReady, null)
|
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,8 +78,9 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
|||||||
if (it == null) {
|
if (it == null) {
|
||||||
Log.e(tag, "Failed to play library item")
|
Log.e(tag, "Failed to play library item")
|
||||||
} else {
|
} else {
|
||||||
|
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it, playWhenReady, null)
|
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-4
@@ -15,9 +15,12 @@ import android.support.v4.media.session.PlaybackStateCompat
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.media.MediaBrowserServiceCompat
|
import androidx.media.MediaBrowserServiceCompat
|
||||||
import androidx.media.utils.MediaConstants
|
import androidx.media.utils.MediaConstants
|
||||||
import com.audiobookshelf.app.BuildConfig
|
import com.audiobookshelf.app.BuildConfig
|
||||||
|
import com.audiobookshelf.app.R
|
||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.data.DeviceInfo
|
import com.audiobookshelf.app.data.DeviceInfo
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
@@ -55,6 +58,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
fun onPlaybackFailed(errorMessage:String)
|
fun onPlaybackFailed(errorMessage:String)
|
||||||
fun onMediaPlayerChanged(mediaPlayer:String)
|
fun onMediaPlayerChanged(mediaPlayer:String)
|
||||||
fun onProgressSyncFailing()
|
fun onProgressSyncFailing()
|
||||||
|
fun onProgressSyncSuccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val tag = "PlayerService"
|
private val tag = "PlayerService"
|
||||||
@@ -65,7 +69,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
private lateinit var ctx:Context
|
private lateinit var ctx:Context
|
||||||
private lateinit var mediaSessionConnector: MediaSessionConnector
|
private lateinit var mediaSessionConnector: MediaSessionConnector
|
||||||
private lateinit var playerNotificationManager: PlayerNotificationManager
|
private lateinit var playerNotificationManager: PlayerNotificationManager
|
||||||
private lateinit var mediaSession: MediaSessionCompat
|
lateinit var mediaSession: MediaSessionCompat
|
||||||
private lateinit var transportControls:MediaControllerCompat.TransportControls
|
private lateinit var transportControls:MediaControllerCompat.TransportControls
|
||||||
|
|
||||||
lateinit var mediaManager: MediaManager
|
lateinit var mediaManager: MediaManager
|
||||||
@@ -160,6 +164,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
ctx = this
|
ctx = this
|
||||||
|
|
||||||
|
DbManager.initialize(ctx)
|
||||||
|
|
||||||
// Initialize API
|
// Initialize API
|
||||||
apiHandler = ApiHandler(ctx)
|
apiHandler = ApiHandler(ctx)
|
||||||
|
|
||||||
@@ -224,6 +230,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
mediaSessionConnector = MediaSessionConnector(mediaSession)
|
mediaSessionConnector = MediaSessionConnector(mediaSession)
|
||||||
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
|
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
|
||||||
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
||||||
|
if (currentPlaybackSession == null) {
|
||||||
|
Log.e(tag,"Playback session is not set - returning blank MediaDescriptionCompat")
|
||||||
|
return MediaDescriptionCompat.Builder().build()
|
||||||
|
}
|
||||||
|
|
||||||
val coverUri = currentPlaybackSession!!.getCoverUri()
|
val coverUri = currentPlaybackSession!!.getCoverUri()
|
||||||
|
|
||||||
// Fix for local images crashing on Android 11 for specific devices
|
// Fix for local images crashing on Android 11 for specific devices
|
||||||
@@ -254,6 +265,18 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
mediaSessionConnector.setQueueNavigator(queueNavigator)
|
mediaSessionConnector.setQueueNavigator(queueNavigator)
|
||||||
mediaSessionConnector.setPlaybackPreparer(MediaSessionPlaybackPreparer(this))
|
mediaSessionConnector.setPlaybackPreparer(MediaSessionPlaybackPreparer(this))
|
||||||
|
|
||||||
|
// Example adding custom action with icon in android auto
|
||||||
|
// mediaSessionConnector.setCustomActionProviders(object : MediaSessionConnector.CustomActionProvider {
|
||||||
|
// override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
|
||||||
|
// }
|
||||||
|
// override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
|
||||||
|
// var icon = R.drawable.exo_icon_rewind
|
||||||
|
// return PlaybackStateCompat.CustomAction.Builder(
|
||||||
|
// "com.audiobookshelf.app.PLAYBACK_RATE", "Playback Rate", icon)
|
||||||
|
// .build()
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
mediaSession.setCallback(MediaSessionCallback(this))
|
mediaSession.setCallback(MediaSessionCallback(this))
|
||||||
|
|
||||||
initializeMPlayer()
|
initializeMPlayer()
|
||||||
@@ -293,6 +316,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
User callable methods
|
User callable methods
|
||||||
*/
|
*/
|
||||||
fun preparePlayer(playbackSession: PlaybackSession, playWhenReady:Boolean, playbackRate:Float?) {
|
fun preparePlayer(playbackSession: PlaybackSession, playWhenReady:Boolean, playbackRate:Float?) {
|
||||||
|
if (!isStarted) {
|
||||||
|
Log.i(tag, "preparePlayer: foreground service not started - Starting service --")
|
||||||
|
Intent(ctx, PlayerNotificationService::class.java).also { intent ->
|
||||||
|
ContextCompat.startForegroundService(ctx, intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isClosed = false
|
isClosed = false
|
||||||
val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f
|
val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f
|
||||||
initialPlaybackRate = playbackRate
|
initialPlaybackRate = playbackRate
|
||||||
@@ -641,13 +671,24 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
fun closePlayback() {
|
fun closePlayback() {
|
||||||
Log.d(tag, "closePlayback")
|
Log.d(tag, "closePlayback")
|
||||||
currentPlayer.clearMediaItems()
|
if (mediaProgressSyncer.listeningTimerRunning) {
|
||||||
currentPlayer.stop()
|
Log.i(tag, "About to close playback so stopping media progress syncer first")
|
||||||
|
mediaProgressSyncer.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
currentPlayer.stop()
|
||||||
|
currentPlayer.clearMediaItems()
|
||||||
|
} catch(e:Exception) {
|
||||||
|
Log.e(tag, "Exception clearing exoplayer $e")
|
||||||
|
}
|
||||||
|
|
||||||
currentPlaybackSession = null
|
currentPlaybackSession = null
|
||||||
clientEventEmitter?.onPlaybackClosed()
|
clientEventEmitter?.onPlaybackClosed()
|
||||||
PlayerListener.lastPauseTime = 0
|
PlayerListener.lastPauseTime = 0
|
||||||
isClosed = true
|
isClosed = true
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendClientMetadata(playerState: PlayerState) {
|
fun sendClientMetadata(playerState: PlayerState) {
|
||||||
@@ -682,6 +723,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
clientEventEmitter?.onProgressSyncFailing()
|
clientEventEmitter?.onProgressSyncFailing()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun alertSyncSuccess() {
|
||||||
|
clientEventEmitter?.onProgressSyncSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
||||||
//
|
//
|
||||||
@@ -716,7 +761,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
isStarted = true
|
isStarted = true
|
||||||
mediaManager.initializeAndroidAuto()
|
|
||||||
mediaManager.checkResetServerItems() // Reset any server items if no longer connected to server
|
mediaManager.checkResetServerItems() // Reset any server items if no longer connected to server
|
||||||
|
|
||||||
isAndroidAuto = true
|
isAndroidAuto = true
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package com.audiobookshelf.app.plugins
|
package com.audiobookshelf.app.plugins
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.audiobookshelf.app.MainActivity
|
import com.audiobookshelf.app.MainActivity
|
||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
@@ -82,6 +80,10 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
override fun onProgressSyncFailing() {
|
override fun onProgressSyncFailing() {
|
||||||
emit("onProgressSyncFailing", "")
|
emit("onProgressSyncFailing", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onProgressSyncSuccess() {
|
||||||
|
emit("onProgressSyncSuccess", "")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
mainActivity.pluginCallback = foregroundServiceReady
|
mainActivity.pluginCallback = foregroundServiceReady
|
||||||
@@ -150,14 +152,6 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun prepareLibraryItem(call: PluginCall) {
|
fun prepareLibraryItem(call: PluginCall) {
|
||||||
// Need to make sure the player service has been started
|
|
||||||
if (!PlayerNotificationService.isStarted) {
|
|
||||||
Log.w(tag, "prepareLibraryItem: PlayerService not started - Starting foreground service --")
|
|
||||||
Intent(mainActivity, PlayerNotificationService::class.java).also { intent ->
|
|
||||||
ContextCompat.startForegroundService(mainActivity, intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val libraryItemId = call.getString("libraryItemId", "").toString()
|
val libraryItemId = call.getString("libraryItemId", "").toString()
|
||||||
val episodeId = call.getString("episodeId", "").toString()
|
val episodeId = call.getString("episodeId", "").toString()
|
||||||
val playWhenReady = call.getBoolean("playWhenReady") == true
|
val playWhenReady = call.getBoolean("playWhenReady") == true
|
||||||
|
|||||||
@@ -77,10 +77,10 @@ class AbsDatabase : Plugin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun getLocalLibraryItemByLLId(call:PluginCall) {
|
fun getLocalLibraryItemByLId(call:PluginCall) {
|
||||||
val libraryItemId = call.getString("libraryItemId", "").toString()
|
val libraryItemId = call.getString("libraryItemId", "").toString()
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLLId(libraryItemId)
|
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItemId)
|
||||||
if (localLibraryItem == null) {
|
if (localLibraryItem == null) {
|
||||||
call.resolve()
|
call.resolve()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -401,9 +401,13 @@ class AbsDownloader : Plugin() {
|
|||||||
|
|
||||||
Log.d(tag, "DOWNLOAD: Move file to final destination path: ${downloadItemPart.finalDestinationPath}")
|
Log.d(tag, "DOWNLOAD: Move file to final destination path: ${downloadItemPart.finalDestinationPath}")
|
||||||
val localFolderFile = DocumentFileCompat.fromUri(mainActivity,Uri.parse(downloadItemPart.localFolderUrl))
|
val localFolderFile = DocumentFileCompat.fromUri(mainActivity,Uri.parse(downloadItemPart.localFolderUrl))
|
||||||
val mimetype = if (downloadItemPart.audioTrack != null) MimeType.AUDIO else MimeType.IMAGE
|
if (localFolderFile == null) {
|
||||||
val fileDescription = FileDescription(downloadItemPart.filename, downloadItemPart.itemTitle, mimetype)
|
Log.e(tag, "Local Folder File from uri is null")
|
||||||
file?.moveFileTo(mainActivity,localFolderFile!!,fileDescription,fcb)
|
} else {
|
||||||
|
val mimetype = if (downloadItemPart.audioTrack != null) MimeType.AUDIO else MimeType.IMAGE
|
||||||
|
val fileDescription = FileDescription(downloadItemPart.filename, downloadItemPart.itemTitle, mimetype)
|
||||||
|
file?.moveFileTo(mainActivity,localFolderFile,fileDescription,fcb)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Why is kotlin requiring an else here..
|
// Why is kotlin requiring an else here..
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.audiobookshelf.app.plugins
|
package com.audiobookshelf.app.plugins
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -65,39 +66,59 @@ class AbsFileSystem : Plugin() {
|
|||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun selectFolder(call: PluginCall) {
|
fun selectFolder(call: PluginCall) {
|
||||||
var mediaType = call.data.getString("mediaType", "book").toString()
|
val mediaType = call.data.getString("mediaType", "book").toString()
|
||||||
|
val REQUEST_CODE_SELECT_FOLDER = 6
|
||||||
|
val REQUEST_CODE_SDCARD_ACCESS = 7
|
||||||
|
|
||||||
mainActivity.storage.folderPickerCallback = object : FolderPickerCallback {
|
mainActivity.storage.folderPickerCallback = object : FolderPickerCallback {
|
||||||
override fun onFolderSelected(requestCode: Int, folder: DocumentFile) {
|
override fun onFolderSelected(requestCode: Int, folder: DocumentFile) {
|
||||||
Log.d(TAG, "ON FOLDER SELECTED ${folder.uri} ${folder.name}")
|
Log.d(TAG, "ON FOLDER SELECTED ${folder.uri} ${folder.name}")
|
||||||
var absolutePath = folder.getAbsolutePath(activity)
|
val absolutePath = folder.getAbsolutePath(activity)
|
||||||
var storageType = folder.getStorageType(activity)
|
val storageType = folder.getStorageType(activity)
|
||||||
var simplePath = folder.getSimplePath(activity)
|
val simplePath = folder.getSimplePath(activity)
|
||||||
var basePath = folder.getBasePath(activity)
|
val basePath = folder.getBasePath(activity)
|
||||||
var folderId = android.util.Base64.encodeToString(folder.id.toByteArray(), android.util.Base64.DEFAULT)
|
val folderId = android.util.Base64.encodeToString(folder.id.toByteArray(), android.util.Base64.DEFAULT)
|
||||||
|
|
||||||
var localFolder = LocalFolder(folderId, folder.name ?: "", folder.uri.toString(),basePath,absolutePath, simplePath, storageType.toString(), mediaType)
|
val localFolder = LocalFolder(folderId, folder.name ?: "", folder.uri.toString(),basePath,absolutePath, simplePath, storageType.toString(), mediaType)
|
||||||
|
|
||||||
DeviceManager.dbManager.saveLocalFolder(localFolder)
|
DeviceManager.dbManager.saveLocalFolder(localFolder)
|
||||||
call.resolve(JSObject(jacksonMapper.writeValueAsString(localFolder)))
|
call.resolve(JSObject(jacksonMapper.writeValueAsString(localFolder)))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType) {
|
override fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType) {
|
||||||
Log.e(TAG, "STORAGE ACCESS DENIED")
|
val jsobj = JSObject()
|
||||||
var jsobj = JSObject()
|
if (requestCode == REQUEST_CODE_SELECT_FOLDER) {
|
||||||
jsobj.put("error", "Access Denied")
|
|
||||||
call.resolve(jsobj)
|
val builder: AlertDialog.Builder = AlertDialog.Builder(mainActivity)
|
||||||
|
builder.setMessage(
|
||||||
|
"You have no write access to this storage, thus selecting this folder is useless." +
|
||||||
|
"\nWould you like to grant access to this folder?")
|
||||||
|
builder.setNegativeButton("Dont Allow") { _, _ ->
|
||||||
|
run {
|
||||||
|
jsobj.put("error", "User Canceled, Access Denied")
|
||||||
|
call.resolve(jsobj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.setPositiveButton("Allow.") { _, _ -> mainActivity.storageHelper.requestStorageAccess(REQUEST_CODE_SDCARD_ACCESS, storageType) }
|
||||||
|
builder.show()
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "STORAGE ACCESS DENIED $requestCode")
|
||||||
|
jsobj.put("error", "Access Denied")
|
||||||
|
call.resolve(jsobj)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onStoragePermissionDenied(requestCode: Int) {
|
override fun onStoragePermissionDenied(requestCode: Int) {
|
||||||
Log.d(TAG, "STORAGE PERMISSION DENIED $requestCode")
|
Log.d(TAG, "STORAGE PERMISSION DENIED $requestCode")
|
||||||
var jsobj = JSObject()
|
val jsobj = JSObject()
|
||||||
jsobj.put("error", "Permission Denied")
|
jsobj.put("error", "Permission Denied")
|
||||||
call.resolve(jsobj)
|
call.resolve(jsobj)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mainActivity.storage.openFolderPicker(6)
|
mainActivity.storage.openFolderPicker(REQUEST_CODE_SELECT_FOLDER)
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.audiobookshelf.app.server
|
package com.audiobookshelf.app.server
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -17,6 +16,7 @@ import com.getcapacitor.JSObject
|
|||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import org.json.JSONException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -28,8 +28,6 @@ class ApiHandler(var ctx:Context) {
|
|||||||
private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build()
|
private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build()
|
||||||
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||||
|
|
||||||
var storageSharedPreferences: SharedPreferences? = null
|
|
||||||
|
|
||||||
data class LocalMediaProgressSyncPayload(val localMediaProgress:List<LocalMediaProgress>)
|
data class LocalMediaProgressSyncPayload(val localMediaProgress:List<LocalMediaProgress>)
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class MediaProgressSyncResponsePayload(val numServerProgressUpdates:Int, val localProgressUpdates:List<LocalMediaProgress>)
|
data class MediaProgressSyncResponsePayload(val numServerProgressUpdates:Int, val localProgressUpdates:List<LocalMediaProgress>)
|
||||||
@@ -106,14 +104,21 @@ class ApiHandler(var ctx:Context) {
|
|||||||
if (bodyString == "OK") {
|
if (bodyString == "OK") {
|
||||||
cb(JSObject())
|
cb(JSObject())
|
||||||
} else {
|
} else {
|
||||||
var jsonObj = JSObject()
|
try {
|
||||||
if (bodyString.startsWith("[")) {
|
var jsonObj = JSObject()
|
||||||
val array = JSArray(bodyString)
|
if (bodyString.startsWith("[")) {
|
||||||
jsonObj.put("value", array)
|
val array = JSArray(bodyString)
|
||||||
} else {
|
jsonObj.put("value", array)
|
||||||
jsonObj = JSObject(bodyString)
|
} else {
|
||||||
|
jsonObj = JSObject(bodyString)
|
||||||
|
}
|
||||||
|
cb(jsonObj)
|
||||||
|
} catch(je:JSONException) {
|
||||||
|
Log.e(tag, "Invalid JSON response ${je.localizedMessage} from body $bodyString")
|
||||||
|
val jsobj = JSObject()
|
||||||
|
jsobj.put("error", "Invalid response body")
|
||||||
|
cb(jsobj)
|
||||||
}
|
}
|
||||||
cb(jsonObj)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,11 +230,15 @@ class ApiHandler(var ctx:Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: () -> Unit) {
|
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: (Boolean) -> Unit) {
|
||||||
val payload = JSObject(jacksonMapper.writeValueAsString(playbackSession))
|
val payload = JSObject(jacksonMapper.writeValueAsString(playbackSession))
|
||||||
|
|
||||||
postRequest("/api/session/local", payload) {
|
postRequest("/api/session/local", payload) {
|
||||||
cb()
|
if (!it.getString("error").isNullOrEmpty()) {
|
||||||
|
cb(false)
|
||||||
|
} else {
|
||||||
|
cb(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,25 +8,28 @@
|
|||||||
<span class="material-icons text-3xl text-white">arrow_back</span>
|
<span class="material-icons text-3xl text-white">arrow_back</span>
|
||||||
</a>
|
</a>
|
||||||
<div v-if="user && currentLibrary">
|
<div v-if="user && currentLibrary">
|
||||||
<div class="pl-3 pr-4 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
<div class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
||||||
<widgets-library-icon :icon="currentLibraryIcon" :size="4" />
|
<widgets-library-icon :icon="currentLibraryIcon" :size="4" />
|
||||||
<p class="text-base font-book leading-4 ml-2 mt-0.5">{{ currentLibraryName }}</p>
|
<p class="text-sm font-book leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<widgets-connection-indicator />
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<widgets-download-progress-indicator />
|
<widgets-download-progress-indicator />
|
||||||
|
|
||||||
<!-- Must be connected to a server to cast, only supports media items on server -->
|
<!-- Must be connected to a server to cast, only supports media items on server -->
|
||||||
<div v-show="isCastAvailable && user" class="mx-2 cursor-pointer mt-1.5">
|
<div v-show="isCastAvailable && user" class="mx-2 cursor-pointer mt-1.5">
|
||||||
<span class="material-icons" :class="isCasting ? 'text-success' : ''" style="font-size: 1.6rem" @click="castClick">cast</span>
|
<span class="material-icons text-2xl" :class="isCasting ? 'text-success' : ''" @click="castClick">cast</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nuxt-link v-if="user" class="h-7 mx-2" to="/search">
|
<nuxt-link v-if="user" class="h-7 mx-1.5" to="/search">
|
||||||
<span class="material-icons" style="font-size: 1.75rem">search</span>
|
<span class="material-icons" style="font-size: 1.75rem">search</span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<div class="h-7 mx-2">
|
<div class="h-7 mx-1.5">
|
||||||
<span class="material-icons" style="font-size: 1.75rem" @click="clickShowSideDrawer">menu</span>
|
<span class="material-icons" style="font-size: 1.75rem" @click="clickShowSideDrawer">menu</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,6 +34,10 @@
|
|||||||
<div class="cover-container bookCoverWrapper bg-black bg-opacity-75 w-full h-full">
|
<div class="cover-container bookCoverWrapper bg-black bg-opacity-75 w-full h-full">
|
||||||
<covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="syncStatus === $constants.SyncStatus.FAILED" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-30">
|
||||||
|
<span class="material-icons text-error text-3xl">error</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor">
|
<div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor">
|
||||||
@@ -129,18 +133,22 @@ export default {
|
|||||||
onPlaybackClosedListener: null,
|
onPlaybackClosedListener: null,
|
||||||
onPlayingUpdateListener: null,
|
onPlayingUpdateListener: null,
|
||||||
onMetadataListener: null,
|
onMetadataListener: null,
|
||||||
|
onProgressSyncFailing: null,
|
||||||
|
onProgressSyncSuccess: null,
|
||||||
touchStartY: 0,
|
touchStartY: 0,
|
||||||
touchStartTime: 0,
|
touchStartTime: 0,
|
||||||
touchEndY: 0,
|
touchEndY: 0,
|
||||||
useChapterTrack: false,
|
useChapterTrack: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
touchTrackStart: false,
|
touchTrackStart: false,
|
||||||
dragPercent: 0
|
dragPercent: 0,
|
||||||
|
syncStatus: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
showFullscreen() {
|
showFullscreen(val) {
|
||||||
this.updateScreenSize()
|
this.updateScreenSize()
|
||||||
|
this.$store.commit('setPlayerFullscreen', !!val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -446,7 +454,7 @@ export default {
|
|||||||
if (this.useChapterTrack && this.currentChapter) {
|
if (this.useChapterTrack && this.currentChapter) {
|
||||||
var currChapTime = this.currentTime - this.currentChapter.start
|
var currChapTime = this.currentTime - this.currentChapter.start
|
||||||
percentDone = currChapTime / this.currentChapterDuration
|
percentDone = currChapTime / this.currentChapterDuration
|
||||||
bufferedPercent = (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration
|
bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration))
|
||||||
}
|
}
|
||||||
var ptWidth = Math.round(percentDone * this.trackWidth)
|
var ptWidth = Math.round(percentDone * this.trackWidth)
|
||||||
this.$refs.playedTrack.style.width = ptWidth + 'px'
|
this.$refs.playedTrack.style.width = ptWidth + 'px'
|
||||||
@@ -522,7 +530,6 @@ export default {
|
|||||||
var data = await AbsAudioPlayer.getCurrentTime()
|
var data = await AbsAudioPlayer.getCurrentTime()
|
||||||
this.currentTime = Number(data.value.toFixed(2))
|
this.currentTime = Number(data.value.toFixed(2))
|
||||||
this.bufferedTime = Number(data.bufferedTime.toFixed(2))
|
this.bufferedTime = Number(data.bufferedTime.toFixed(2))
|
||||||
console.log('[AudioPlayer] Got Current Time', this.currentTime)
|
|
||||||
this.timeupdate()
|
this.timeupdate()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
},
|
},
|
||||||
@@ -675,6 +682,7 @@ export default {
|
|||||||
|
|
||||||
this.isEnded = false
|
this.isEnded = false
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
this.syncStatus = 0
|
||||||
this.$store.commit('setPlayerItem', this.playbackSession)
|
this.$store.commit('setPlayerItem', this.playbackSession)
|
||||||
|
|
||||||
// Set track width
|
// Set track width
|
||||||
@@ -703,6 +711,8 @@ export default {
|
|||||||
this.onPlaybackFailedListener = AbsAudioPlayer.addListener('onPlaybackFailed', this.onPlaybackFailed)
|
this.onPlaybackFailedListener = AbsAudioPlayer.addListener('onPlaybackFailed', this.onPlaybackFailed)
|
||||||
this.onPlayingUpdateListener = AbsAudioPlayer.addListener('onPlayingUpdate', this.onPlayingUpdate)
|
this.onPlayingUpdateListener = AbsAudioPlayer.addListener('onPlayingUpdate', this.onPlayingUpdate)
|
||||||
this.onMetadataListener = AbsAudioPlayer.addListener('onMetadata', this.onMetadata)
|
this.onMetadataListener = AbsAudioPlayer.addListener('onMetadata', this.onMetadata)
|
||||||
|
this.onProgressSyncFailing = AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
|
||||||
|
this.onProgressSyncSuccess = AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
|
||||||
},
|
},
|
||||||
screenOrientationChange() {
|
screenOrientationChange() {
|
||||||
setTimeout(this.updateScreenSize, 50)
|
setTimeout(this.updateScreenSize, 50)
|
||||||
@@ -712,6 +722,16 @@ export default {
|
|||||||
var coverHeight = this.fullscreenBookCoverWidth * this.bookCoverAspectRatio
|
var coverHeight = this.fullscreenBookCoverWidth * this.bookCoverAspectRatio
|
||||||
document.documentElement.style.setProperty('--cover-image-width', this.fullscreenBookCoverWidth + 'px')
|
document.documentElement.style.setProperty('--cover-image-width', this.fullscreenBookCoverWidth + 'px')
|
||||||
document.documentElement.style.setProperty('--cover-image-height', coverHeight + 'px')
|
document.documentElement.style.setProperty('--cover-image-height', coverHeight + 'px')
|
||||||
|
},
|
||||||
|
minimizePlayerEvt() {
|
||||||
|
console.log('Minimize Player Evt')
|
||||||
|
this.showFullscreen = false
|
||||||
|
},
|
||||||
|
showProgressSyncIsFailing() {
|
||||||
|
this.syncStatus = this.$constants.SyncStatus.FAILED
|
||||||
|
},
|
||||||
|
showProgressSyncSuccess() {
|
||||||
|
this.syncStatus = this.$constants.SyncStatus.SUCCESS
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -719,6 +739,8 @@ export default {
|
|||||||
if (screen.orientation) {
|
if (screen.orientation) {
|
||||||
screen.orientation.addEventListener('change', this.screenOrientationChange)
|
screen.orientation.addEventListener('change', this.screenOrientationChange)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$eventBus.$on('minimize-player', this.minimizePlayerEvt)
|
||||||
document.body.addEventListener('touchstart', this.touchstart)
|
document.body.addEventListener('touchstart', this.touchstart)
|
||||||
document.body.addEventListener('touchend', this.touchend)
|
document.body.addEventListener('touchend', this.touchend)
|
||||||
document.body.addEventListener('touchmove', this.touchmove)
|
document.body.addEventListener('touchmove', this.touchmove)
|
||||||
@@ -735,6 +757,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.forceCloseDropdownMenu()
|
this.forceCloseDropdownMenu()
|
||||||
|
this.$eventBus.$off('minimize-player', this.minimizePlayerEvt)
|
||||||
document.body.removeEventListener('touchstart', this.touchstart)
|
document.body.removeEventListener('touchstart', this.touchstart)
|
||||||
document.body.removeEventListener('touchend', this.touchend)
|
document.body.removeEventListener('touchend', this.touchend)
|
||||||
document.body.removeEventListener('touchmove', this.touchmove)
|
document.body.removeEventListener('touchmove', this.touchmove)
|
||||||
@@ -744,6 +767,8 @@ export default {
|
|||||||
if (this.onPlaybackSessionListener) this.onPlaybackSessionListener.remove()
|
if (this.onPlaybackSessionListener) this.onPlaybackSessionListener.remove()
|
||||||
if (this.onPlaybackClosedListener) this.onPlaybackClosedListener.remove()
|
if (this.onPlaybackClosedListener) this.onPlaybackClosedListener.remove()
|
||||||
if (this.onPlaybackFailedListener) this.onPlaybackFailedListener.remove()
|
if (this.onPlaybackFailedListener) this.onPlaybackFailedListener.remove()
|
||||||
|
if (this.onProgressSyncFailing) this.onProgressSyncFailing.remove()
|
||||||
|
if (this.onProgressSyncSuccess) this.onProgressSyncSuccess.remove()
|
||||||
clearInterval(this.playInterval)
|
clearInterval(this.playInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,11 +30,9 @@ export default {
|
|||||||
onSleepTimerEndedListener: null,
|
onSleepTimerEndedListener: null,
|
||||||
onSleepTimerSetListener: null,
|
onSleepTimerSetListener: null,
|
||||||
onMediaPlayerChangedListener: null,
|
onMediaPlayerChangedListener: null,
|
||||||
onProgressSyncFailing: null,
|
|
||||||
sleepInterval: null,
|
sleepInterval: null,
|
||||||
currentEndOfChapterTime: 0,
|
currentEndOfChapterTime: 0,
|
||||||
serverLibraryItemId: null,
|
serverLibraryItemId: null
|
||||||
syncFailedToast: null
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -255,10 +253,6 @@ export default {
|
|||||||
onMediaPlayerChanged(data) {
|
onMediaPlayerChanged(data) {
|
||||||
var mediaPlayer = data.value
|
var mediaPlayer = data.value
|
||||||
this.$store.commit('setMediaPlayer', mediaPlayer)
|
this.$store.commit('setMediaPlayer', mediaPlayer)
|
||||||
},
|
|
||||||
showProgressSyncIsFailing() {
|
|
||||||
if (!isNaN(this.syncFailedToast)) this.$toast.dismiss(this.syncFailedToast)
|
|
||||||
this.syncFailedToast = this.$toast('Progress is not being synced', { timeout: false, type: 'error' })
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -266,7 +260,6 @@ export default {
|
|||||||
this.onSleepTimerEndedListener = AbsAudioPlayer.addListener('onSleepTimerEnded', this.onSleepTimerEnded)
|
this.onSleepTimerEndedListener = AbsAudioPlayer.addListener('onSleepTimerEnded', this.onSleepTimerEnded)
|
||||||
this.onSleepTimerSetListener = AbsAudioPlayer.addListener('onSleepTimerSet', this.onSleepTimerSet)
|
this.onSleepTimerSetListener = AbsAudioPlayer.addListener('onSleepTimerSet', this.onSleepTimerSet)
|
||||||
this.onMediaPlayerChangedListener = AbsAudioPlayer.addListener('onMediaPlayerChanged', this.onMediaPlayerChanged)
|
this.onMediaPlayerChangedListener = AbsAudioPlayer.addListener('onMediaPlayerChanged', this.onMediaPlayerChanged)
|
||||||
this.onProgressSyncFailing = AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
|
|
||||||
|
|
||||||
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
|
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
|
||||||
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
|
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
|
||||||
@@ -283,7 +276,6 @@ export default {
|
|||||||
if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove()
|
if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove()
|
||||||
if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove()
|
if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove()
|
||||||
if (this.onMediaPlayerChangedListener) this.onMediaPlayerChangedListener.remove()
|
if (this.onMediaPlayerChangedListener) this.onMediaPlayerChangedListener.remove()
|
||||||
if (this.onProgressSyncFailing) this.onProgressSyncFailing.remove()
|
|
||||||
|
|
||||||
// if (this.$server.socket) {
|
// if (this.$server.socket) {
|
||||||
// this.$server.socket.off('stream_open', this.streamOpen)
|
// this.$server.socket.off('stream_open', this.streamOpen)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="absolute bottom-0 left-0 w-full py-6 px-6 text-gray-300">
|
<div class="absolute bottom-0 left-0 w-full py-6 px-6 text-gray-300">
|
||||||
<div v-if="serverConnectionConfig" class="mb-4 flex justify-center">
|
<div v-if="serverConnectionConfig" class="mb-4 flex justify-center">
|
||||||
<p class="text-xs" style="word-break: break-word">{{ serverConnectionConfig.address }} (v{{ serverSettings.version }})</p>
|
<p class="text-xs text-gray-400" style="word-break: break-word">{{ serverConnectionConfig.address }} (v{{ serverSettings.version }})</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<p class="text-xs">{{ $config.version }}</p>
|
<p class="text-xs">{{ $config.version }}</p>
|
||||||
|
|||||||
@@ -16,8 +16,11 @@
|
|||||||
</template>
|
</template>
|
||||||
<div v-else class="w-full">
|
<div v-else class="w-full">
|
||||||
<form v-show="!showAuth" @submit.prevent="submit" novalidate class="w-full">
|
<form v-show="!showAuth" @submit.prevent="submit" novalidate class="w-full">
|
||||||
|
<div v-if="serverConnectionConfigs.length" class="flex items-center mb-4" @click="showServerList">
|
||||||
|
<span class="material-icons text-gray-300">arrow_back</span>
|
||||||
|
</div>
|
||||||
<h2 class="text-lg leading-7 mb-2">Server address</h2>
|
<h2 class="text-lg leading-7 mb-2">Server address</h2>
|
||||||
<ui-text-input v-model="serverConfig.address" :disabled="processing || !networkConnected || serverConfig.id" placeholder="http://55.55.55.55:13378" type="url" class="w-full h-10" />
|
<ui-text-input v-model="serverConfig.address" :disabled="processing || !networkConnected || !!serverConfig.id" placeholder="http://55.55.55.55:13378" type="url" class="w-full h-10" />
|
||||||
<div class="flex justify-end items-center mt-6">
|
<div class="flex justify-end items-center mt-6">
|
||||||
<!-- <div class="relative flex">
|
<!-- <div class="relative flex">
|
||||||
<button class="outline-none uppercase tracking-wide font-semibold text-xs text-gray-300" type="button" @click="addCustomHeaders">Add Custom Headers</button>
|
<button class="outline-none uppercase tracking-wide font-semibold text-xs text-gray-300" type="button" @click="addCustomHeaders">Add Custom Headers</button>
|
||||||
@@ -76,12 +79,6 @@
|
|||||||
import { Dialog } from '@capacitor/dialog'
|
import { Dialog } from '@capacitor/dialog'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
deviceData: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
@@ -99,6 +96,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
deviceData() {
|
||||||
|
return this.$store.state.deviceData || {}
|
||||||
|
},
|
||||||
networkConnected() {
|
networkConnected() {
|
||||||
return this.$store.state.networkConnected
|
return this.$store.state.networkConnected
|
||||||
},
|
},
|
||||||
@@ -152,7 +152,7 @@ export default {
|
|||||||
var payload = await this.authenticateToken()
|
var payload = await this.authenticateToken()
|
||||||
|
|
||||||
if (payload) {
|
if (payload) {
|
||||||
this.setUserAndConnection(payload.user, payload.userDefaultLibraryId)
|
this.setUserAndConnection(payload)
|
||||||
} else {
|
} else {
|
||||||
this.showAuth = true
|
this.showAuth = true
|
||||||
}
|
}
|
||||||
@@ -167,7 +167,10 @@ export default {
|
|||||||
if (value) {
|
if (value) {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
await this.$db.removeServerConnectionConfig(this.serverConfig.id)
|
await this.$db.removeServerConnectionConfig(this.serverConfig.id)
|
||||||
this.deviceData.serverConnectionConfigs = this.deviceData.serverConnectionConfigs.filter((scc) => scc.id != this.serverConfig.id)
|
const updatedDeviceData = { ...this.deviceData }
|
||||||
|
updatedDeviceData.serverConnectionConfigs = this.deviceData.serverConnectionConfigs.filter((scc) => scc.id != this.serverConfig.id)
|
||||||
|
this.$store.commit('setDeviceData', updatedDeviceData)
|
||||||
|
|
||||||
this.serverConfig = {
|
this.serverConfig = {
|
||||||
address: null,
|
address: null,
|
||||||
userId: null,
|
userId: null,
|
||||||
@@ -185,7 +188,6 @@ export default {
|
|||||||
}
|
}
|
||||||
this.showForm = true
|
this.showForm = true
|
||||||
this.showAuth = true
|
this.showAuth = true
|
||||||
console.log('Edit server config', serverConfig)
|
|
||||||
},
|
},
|
||||||
newServerConfigClick() {
|
newServerConfigClick() {
|
||||||
this.serverConfig = {
|
this.serverConfig = {
|
||||||
@@ -274,20 +276,29 @@ export default {
|
|||||||
this.error = 'Invalid username'
|
this.error = 'Invalid username'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const duplicateConfig = this.serverConnectionConfigs.find((scc) => scc.address === this.serverConfig.address && scc.username === this.serverConfig.username && this.serverConfig.id !== scc.id)
|
||||||
|
if (duplicateConfig) {
|
||||||
|
this.error = 'Config already exists for this address and username'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.error = null
|
this.error = null
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|
||||||
var payload = await this.requestServerLogin()
|
var payload = await this.requestServerLogin()
|
||||||
this.processing = false
|
this.processing = false
|
||||||
if (payload) {
|
if (payload) {
|
||||||
this.setUserAndConnection(payload.user, payload.userDefaultLibraryId)
|
this.setUserAndConnection(payload)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async setUserAndConnection(user, userDefaultLibraryId) {
|
async setUserAndConnection({ user, userDefaultLibraryId, serverSettings }) {
|
||||||
if (!user) return
|
if (!user) return
|
||||||
|
|
||||||
console.log('Successfully logged in', JSON.stringify(user))
|
console.log('Successfully logged in', JSON.stringify(user))
|
||||||
|
|
||||||
|
this.$store.commit('setServerSettings', serverSettings)
|
||||||
|
|
||||||
// Set library - Use last library if set and available fallback to default user library
|
// Set library - Use last library if set and available fallback to default user library
|
||||||
var lastLibraryId = await this.$localStore.getLastLibraryId()
|
var lastLibraryId = await this.$localStore.getLastLibraryId()
|
||||||
if (lastLibraryId && (!user.librariesAccessible.length || user.librariesAccessible.includes(lastLibraryId))) {
|
if (lastLibraryId && (!user.librariesAccessible.length || user.librariesAccessible.includes(lastLibraryId))) {
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
setShow() {
|
setShow() {
|
||||||
|
this.$store.commit('globals/setIsModalOpen', true)
|
||||||
|
|
||||||
document.body.appendChild(this.el)
|
document.body.appendChild(this.el)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.content.style.transform = 'scale(1)'
|
this.content.style.transform = 'scale(1)'
|
||||||
@@ -84,18 +86,28 @@ export default {
|
|||||||
document.documentElement.classList.add('modal-open')
|
document.documentElement.classList.add('modal-open')
|
||||||
},
|
},
|
||||||
setHide() {
|
setHide() {
|
||||||
|
this.$store.commit('globals/setIsModalOpen', false)
|
||||||
|
|
||||||
this.content.style.transform = 'scale(0)'
|
this.content.style.transform = 'scale(0)'
|
||||||
this.el.remove()
|
this.el.remove()
|
||||||
document.documentElement.classList.remove('modal-open')
|
document.documentElement.classList.remove('modal-open')
|
||||||
|
},
|
||||||
|
closeModalEvt() {
|
||||||
|
console.log('Close modal event')
|
||||||
|
this.show = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.$eventBus.$on('close-modal', this.closeModalEvt)
|
||||||
this.el = this.$refs.wrapper
|
this.el = this.$refs.wrapper
|
||||||
this.content = this.$refs.content
|
this.content = this.$refs.content
|
||||||
this.content.style.transform = 'scale(0)'
|
this.content.style.transform = 'scale(0)'
|
||||||
this.content.style.transition = 'transform 0.25s cubic-bezier(0.16, 1, 0.3, 1)'
|
this.content.style.transition = 'transform 0.25s cubic-bezier(0.16, 1, 0.3, 1)'
|
||||||
this.el.style.opacity = 1
|
this.el.style.opacity = 1
|
||||||
this.el.remove()
|
this.el.remove()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$eventBus.$off('close-modal', this.closeModalEvt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full px-2 py-2 overflow-hidden relative">
|
<div class="w-full px-2 py-2 overflow-hidden relative">
|
||||||
<div v-if="book" class="flex h-20">
|
<div v-if="book" class="flex w-full">
|
||||||
<div class="h-full relative" :style="{ width: bookWidth + 'px' }">
|
<div class="h-full relative" :style="{ width: bookWidth + 'px' }">
|
||||||
<covers-book-cover :library-item="book" :width="bookWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :library-item="book" :width="bookWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-80 h-full px-2 flex items-center">
|
<div class="flex-grow book-table-content h-full px-2 flex items-center">
|
||||||
<div>
|
<div class="max-w-full">
|
||||||
<nuxt-link :to="`/item/${book.id}`" class="truncate text-sm">{{ bookTitle }}</nuxt-link>
|
<nuxt-link :to="`/item/${book.id}`" class="truncate block text-sm">{{ bookTitle }}</nuxt-link>
|
||||||
<p class="truncate block text-gray-400 text-xs">{{ bookAuthor }}</p>
|
<p class="truncate block text-gray-400 text-xs">{{ bookAuthor }}</p>
|
||||||
|
<p class="text-xxs text-gray-500">{{ bookDuration }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,13 +47,13 @@ export default {
|
|||||||
return this.mediaMetadata.authorName || ''
|
return this.mediaMetadata.authorName || ''
|
||||||
},
|
},
|
||||||
bookDuration() {
|
bookDuration() {
|
||||||
return this.$secondsToTimestamp(this.media.duration)
|
return this.$elapsedPretty(this.media.duration)
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
bookWidth() {
|
bookWidth() {
|
||||||
if (this.bookCoverAspectRatio === 1) return 80
|
if (this.bookCoverAspectRatio === 1) return 50
|
||||||
return 50
|
return 50
|
||||||
},
|
},
|
||||||
isMissing() {
|
isMissing() {
|
||||||
@@ -78,4 +79,11 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.book-table-content {
|
||||||
|
width: calc(100% - 50px);
|
||||||
|
max-width: calc(100% - 50px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="icon" class="flex h-full items-center px-2">
|
||||||
|
<span class="material-icons-outlined text-lg" :class="iconClass" @click="showAlertDialog">{{ icon }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Dialog } from '@capacitor/dialog'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
user() {
|
||||||
|
return this.$store.state.user.user
|
||||||
|
},
|
||||||
|
socketConnected() {
|
||||||
|
return this.$store.state.socketConnected
|
||||||
|
},
|
||||||
|
networkConnected() {
|
||||||
|
return this.$store.state.networkConnected
|
||||||
|
},
|
||||||
|
networkConnectionType() {
|
||||||
|
return this.$store.state.networkConnectionType
|
||||||
|
},
|
||||||
|
isCellular() {
|
||||||
|
return this.networkConnectionType === 'cellular'
|
||||||
|
},
|
||||||
|
icon() {
|
||||||
|
if (!this.user) return null // hide when not connected to server
|
||||||
|
|
||||||
|
if (!this.networkConnected) {
|
||||||
|
return 'wifi_off'
|
||||||
|
} else if (!this.socketConnected) {
|
||||||
|
return 'cloud_off'
|
||||||
|
} else if (this.isCellular) {
|
||||||
|
return 'signal_cellular_alt'
|
||||||
|
} else {
|
||||||
|
return 'cloud_done'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
iconClass() {
|
||||||
|
if (!this.networkConnected) return 'text-error'
|
||||||
|
else if (!this.socketConnected) return 'text-warning'
|
||||||
|
else if (this.isCellular) return 'text-gray-200'
|
||||||
|
else return 'text-success'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showAlertDialog() {
|
||||||
|
var msg = ''
|
||||||
|
if (!this.networkConnected) {
|
||||||
|
msg = 'No internet'
|
||||||
|
} else if (!this.socketConnected) {
|
||||||
|
msg = 'Socket not connected'
|
||||||
|
} else if (this.isCellular) {
|
||||||
|
msg = 'Socket connected over cellular'
|
||||||
|
} else {
|
||||||
|
msg = 'Socket connected over wifi'
|
||||||
|
}
|
||||||
|
Dialog.alert({
|
||||||
|
title: 'Connection Status',
|
||||||
|
message: msg
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -479,12 +479,12 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 10;
|
CURRENT_PROJECT_VERSION = 11;
|
||||||
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 0.9.50;
|
MARKETING_VERSION = 0.9.51;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
|
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -503,12 +503,12 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 10;
|
CURRENT_PROJECT_VERSION = 11;
|
||||||
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 0.9.50;
|
MARKETING_VERSION = 0.9.51;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ public class AbsAudioPlayer: CAPPlugin {
|
|||||||
NotificationCenter.default.addObserver(self, selector: #selector(onPlaybackFailed), name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(onPlaybackFailed), name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
|
||||||
|
|
||||||
self.bridge?.webView?.allowsBackForwardNavigationGestures = true;
|
self.bridge?.webView?.allowsBackForwardNavigationGestures = true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func prepareLibraryItem(_ call: CAPPluginCall) {
|
@objc func prepareLibraryItem(_ call: CAPPluginCall) {
|
||||||
let libraryItemId = call.getString("libraryItemId")
|
let libraryItemId = call.getString("libraryItemId")
|
||||||
let episodeId = call.getString("episodeId")
|
let episodeId = call.getString("episodeId")
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ CAP_PLUGIN(AbsDatabase, "AbsDatabase",
|
|||||||
|
|
||||||
CAP_PLUGIN_METHOD(getLocalLibraryItems, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(getLocalLibraryItems, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(getLocalLibraryItem, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(getLocalLibraryItem, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(getLocalLibraryItemByLLId, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(getLocalLibraryItemByLId, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(getLocalLibraryItemsInFolder, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(getLocalLibraryItemsInFolder, CAPPluginReturnPromise);
|
||||||
CAP_PLUGIN_METHOD(updateDeviceSettings, CAPPluginReturnPromise);
|
CAP_PLUGIN_METHOD(updateDeviceSettings, CAPPluginReturnPromise);
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ public class AbsDatabase: CAPPlugin {
|
|||||||
@objc func getLocalLibraryItem(_ call: CAPPluginCall) {
|
@objc func getLocalLibraryItem(_ call: CAPPluginCall) {
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
@objc func getLocalLibraryItemByLLId(_ call: CAPPluginCall) {
|
@objc func getLocalLibraryItemByLId(_ call: CAPPluginCall) {
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
@objc func getLocalLibraryItemsInFolder(_ call: CAPPluginCall) {
|
@objc func getLocalLibraryItemsInFolder(_ call: CAPPluginCall) {
|
||||||
|
|||||||
+6
-6
@@ -9,12 +9,12 @@ install! 'cocoapods', :disable_input_output_paths => true
|
|||||||
def capacitor_pods
|
def capacitor_pods
|
||||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
pod 'CapacitorApp', :path => '..\..\node_modules\@capacitor\app'
|
||||||
pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog'
|
pod 'CapacitorDialog', :path => '..\..\node_modules\@capacitor\dialog'
|
||||||
pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
|
pod 'CapacitorHaptics', :path => '..\..\node_modules\@capacitor\haptics'
|
||||||
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
|
pod 'CapacitorNetwork', :path => '..\..\node_modules\@capacitor\network'
|
||||||
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
pod 'CapacitorStatusBar', :path => '..\..\node_modules\@capacitor\status-bar'
|
||||||
pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage'
|
pod 'CapacitorStorage', :path => '..\..\node_modules\@capacitor\storage'
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'App' do
|
target 'App' do
|
||||||
|
|||||||
+10
-15
@@ -66,9 +66,6 @@ export default {
|
|||||||
},
|
},
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
|
||||||
isSocketConnected() {
|
|
||||||
return this.$store.state.socketConnected
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -154,15 +151,9 @@ export default {
|
|||||||
// Only cancels stream if streamining not playing downloaded
|
// Only cancels stream if streamining not playing downloaded
|
||||||
this.$eventBus.$emit('close-stream')
|
this.$eventBus.$emit('close-stream')
|
||||||
},
|
},
|
||||||
socketConnectionUpdate(isConnected) {
|
|
||||||
console.log('Socket connection update', isConnected)
|
|
||||||
},
|
|
||||||
socketConnectionFailed(err) {
|
socketConnectionFailed(err) {
|
||||||
this.$toast.error('Socket connection error: ' + err.message)
|
this.$toast.error('Socket connection error: ' + err.message)
|
||||||
},
|
},
|
||||||
socketInit(data) {
|
|
||||||
console.log('Socket init', data)
|
|
||||||
},
|
|
||||||
async initLibraries() {
|
async initLibraries() {
|
||||||
if (this.inittingLibraries) {
|
if (this.inittingLibraries) {
|
||||||
return
|
return
|
||||||
@@ -177,6 +168,7 @@ export default {
|
|||||||
async syncLocalMediaProgress() {
|
async syncLocalMediaProgress() {
|
||||||
if (!this.user) {
|
if (!this.user) {
|
||||||
console.log('[default] No need to sync local media progress - not connected to server')
|
console.log('[default] No need to sync local media progress - not connected to server')
|
||||||
|
this.$store.commit('setLastLocalMediaSyncResults', null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,10 +176,15 @@ export default {
|
|||||||
var response = await this.$db.syncLocalMediaProgressWithServer()
|
var response = await this.$db.syncLocalMediaProgressWithServer()
|
||||||
if (!response) {
|
if (!response) {
|
||||||
if (this.$platform != 'web') this.$toast.error('Failed to sync local media with server')
|
if (this.$platform != 'web') this.$toast.error('Failed to sync local media with server')
|
||||||
|
this.$store.commit('setLastLocalMediaSyncResults', null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { numLocalMediaProgressForServer, numServerProgressUpdates, numLocalProgressUpdates } = response
|
const { numLocalMediaProgressForServer, numServerProgressUpdates, numLocalProgressUpdates } = response
|
||||||
if (numLocalMediaProgressForServer > 0) {
|
if (numLocalMediaProgressForServer > 0) {
|
||||||
|
response.syncedAt = Date.now()
|
||||||
|
response.serverConfigName = this.$store.getters['user/getServerConfigName']
|
||||||
|
this.$store.commit('setLastLocalMediaSyncResults', response)
|
||||||
|
|
||||||
if (numServerProgressUpdates > 0 || numLocalProgressUpdates > 0) {
|
if (numServerProgressUpdates > 0 || numLocalProgressUpdates > 0) {
|
||||||
console.log(`[default] ${numServerProgressUpdates} Server progress updates | ${numLocalProgressUpdates} Local progress updates`)
|
console.log(`[default] ${numServerProgressUpdates} Server progress updates | ${numLocalProgressUpdates} Local progress updates`)
|
||||||
} else {
|
} else {
|
||||||
@@ -195,6 +192,7 @@ export default {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('[default] syncLocalMediaProgress No local media progress to sync')
|
console.log('[default] syncLocalMediaProgress No local media progress to sync')
|
||||||
|
this.$store.commit('setLastLocalMediaSyncResults', null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async userUpdated(user) {
|
async userUpdated(user) {
|
||||||
@@ -216,9 +214,10 @@ export default {
|
|||||||
mediaProgress: prog
|
mediaProgress: prog
|
||||||
}
|
}
|
||||||
newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress(payload)
|
newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress(payload)
|
||||||
} else {
|
} else if (!localProg) {
|
||||||
// Check if local library item exists
|
// Check if local library item exists
|
||||||
var localLibraryItem = await this.$db.getLocalLibraryItemByLLId(prog.libraryItemId)
|
// local media progress may not exist yet if it hasn't been played
|
||||||
|
var localLibraryItem = await this.$db.getLocalLibraryItemByLId(prog.libraryItemId)
|
||||||
if (localLibraryItem) {
|
if (localLibraryItem) {
|
||||||
if (prog.episodeId) {
|
if (prog.episodeId) {
|
||||||
// If episode check if local episode exists
|
// If episode check if local episode exists
|
||||||
@@ -253,8 +252,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.$socket.on('connection-update', this.socketConnectionUpdate)
|
|
||||||
this.$socket.on('initialized', this.socketInit)
|
|
||||||
this.$socket.on('user_updated', this.userUpdated)
|
this.$socket.on('user_updated', this.userUpdated)
|
||||||
this.$socket.on('user_media_progress_updated', this.userMediaProgressUpdated)
|
this.$socket.on('user_media_progress_updated', this.userMediaProgressUpdated)
|
||||||
|
|
||||||
@@ -282,8 +279,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$socket.off('connection-update', this.socketConnectionUpdate)
|
|
||||||
this.$socket.off('initialized', this.socketInit)
|
|
||||||
this.$socket.off('user_updated', this.userUpdated)
|
this.$socket.off('user_updated', this.userUpdated)
|
||||||
this.$socket.off('user_media_progress_updated', this.userMediaProgressUpdated)
|
this.$socket.off('user_media_progress_updated', this.userMediaProgressUpdated)
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "0.9.50-beta",
|
"version": "0.9.53-beta",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "0.9.50-beta",
|
"version": "0.9.53-beta",
|
||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt --hostname 0.0.0.0 --port 1337",
|
"dev": "nuxt --hostname 0.0.0.0 --port 1337",
|
||||||
|
|||||||
@@ -183,12 +183,10 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
initListeners() {
|
initListeners() {
|
||||||
// this.$server.on('initialized', this.socketInit)
|
|
||||||
this.$eventBus.$on('library-changed', this.libraryChanged)
|
this.$eventBus.$on('library-changed', this.libraryChanged)
|
||||||
// this.$eventBus.$on('downloads-loaded', this.downloadsLoaded)
|
// this.$eventBus.$on('downloads-loaded', this.downloadsLoaded)
|
||||||
},
|
},
|
||||||
removeListeners() {
|
removeListeners() {
|
||||||
// this.$server.off('initialized', this.socketInit)
|
|
||||||
this.$eventBus.$off('library-changed', this.libraryChanged)
|
this.$eventBus.$off('library-changed', this.libraryChanged)
|
||||||
// this.$eventBus.$off('downloads-loaded', this.downloadsLoaded)
|
// this.$eventBus.$off('downloads-loaded', this.downloadsLoaded)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
<covers-collection-cover :book-items="bookItems" :width="240" :height="120 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-collection-cover :book-items="bookItems" :width="240" :height="120 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-2 py-6 md:py-0 md:px-10">
|
<div class="flex-grow py-6">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center px-2">
|
||||||
<h1 class="text-xl font-sans">
|
<h1 class="text-xl font-sans">
|
||||||
{{ collectionName }}
|
{{ collectionName }}
|
||||||
</h1>
|
</h1>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
</ui-btn>
|
</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-8 max-w-2xl">
|
<div class="my-8 max-w-2xl px-2">
|
||||||
<p class="text-base text-gray-100">{{ description }}</p>
|
<p class="text-base text-gray-100">{{ description }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<!-- <p class="absolute bottom-16 left-0 right-0 px-2 text-center text-error"><strong>Important!</strong> This app requires that you are running <u>your own server</u> and does not provide any content.</p> -->
|
<!-- <p class="absolute bottom-16 left-0 right-0 px-2 text-center text-error"><strong>Important!</strong> This app requires that you are running <u>your own server</u> and does not provide any content.</p> -->
|
||||||
|
|
||||||
<connection-server-connect-form v-if="deviceData" :device-data="deviceData" />
|
<connection-server-connect-form v-if="deviceData" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-center pt-4 fixed bottom-4 left-0 right-0">
|
<div class="flex items-center justify-center pt-4 fixed bottom-4 left-0 right-0">
|
||||||
|
|||||||
+1
-1
@@ -136,7 +136,7 @@ export default {
|
|||||||
})
|
})
|
||||||
// Check if
|
// Check if
|
||||||
if (libraryItem) {
|
if (libraryItem) {
|
||||||
var localLibraryItem = await app.$db.getLocalLibraryItemByLLId(libraryItemId)
|
var localLibraryItem = await app.$db.getLocalLibraryItemByLId(libraryItemId)
|
||||||
if (localLibraryItem) {
|
if (localLibraryItem) {
|
||||||
console.log('Library item has local library item also', localLibraryItem.id)
|
console.log('Library item has local library item also', localLibraryItem.id)
|
||||||
libraryItem.localLibraryItem = localLibraryItem
|
libraryItem.localLibraryItem = localLibraryItem
|
||||||
|
|||||||
@@ -1,5 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full py-6">
|
<div class="w-full h-full py-6">
|
||||||
|
<div v-if="lastLocalMediaSyncResults" class="px-2 mb-4">
|
||||||
|
<div class="w-full pl-2 pr-2 py-2 bg-black bg-opacity-25 rounded-lg relative">
|
||||||
|
<div class="flex items-center mb-1">
|
||||||
|
<span class="material-icons text-success text-xl">sync</span>
|
||||||
|
<p class="text-sm text-gray-300 pl-2">Local media progress synced with server</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between mb-1.5">
|
||||||
|
<p class="text-xs text-gray-400 font-semibold">{{ syncedServerConfigName }}</p>
|
||||||
|
<p class="text-xs text-gray-400 italic">{{ $dateDistanceFromNow(syncedAt) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!numLocalProgressUpdates && !numServerProgressUpdates">
|
||||||
|
<p class="text-sm text-gray-300">Local media progress was up-to-date with server ({{ numLocalMediaSynced }} item{{ numLocalMediaSynced == 1 ? '' : 's' }})</p>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<p v-if="numServerProgressUpdates" class="text-sm text-gray-300">- {{ numServerProgressUpdates }} local media item{{ numServerProgressUpdates === 1 ? '' : 's' }} progress was updated on the server (local more recent).</p>
|
||||||
|
<p v-else class="text-sm text-gray-300">- No local media progress had to be synced on the server.</p>
|
||||||
|
<p v-if="numLocalProgressUpdates" class="text-sm text-gray-300">- {{ numLocalProgressUpdates }} local media item{{ numLocalProgressUpdates === 1 ? '' : 's' }} progress was updated to match the server (server more recent).</p>
|
||||||
|
<p v-else class="text-sm text-gray-300">- No server progress had to be synced with local media progress.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h1 class="text-base font-semibold px-3 mb-2">Local Folders</h1>
|
<h1 class="text-base font-semibold px-3 mb-2">Local Folders</h1>
|
||||||
|
|
||||||
<div v-if="!isIos" class="w-full max-w-full px-3 py-2">
|
<div v-if="!isIos" class="w-full max-w-full px-3 py-2">
|
||||||
@@ -49,8 +72,28 @@ export default {
|
|||||||
isIos() {
|
isIos() {
|
||||||
return this.$platform === 'ios'
|
return this.$platform === 'ios'
|
||||||
},
|
},
|
||||||
isSocketConnected() {
|
lastLocalMediaSyncResults() {
|
||||||
return this.$store.state.socketConnected
|
return this.$store.state.lastLocalMediaSyncResults
|
||||||
|
},
|
||||||
|
numLocalMediaSynced() {
|
||||||
|
if (!this.lastLocalMediaSyncResults) return 0
|
||||||
|
return this.lastLocalMediaSyncResults.numLocalMediaProgressForServer || 0
|
||||||
|
},
|
||||||
|
syncedAt() {
|
||||||
|
if (!this.lastLocalMediaSyncResults) return 0
|
||||||
|
return this.lastLocalMediaSyncResults.syncedAt || 0
|
||||||
|
},
|
||||||
|
syncedServerConfigName() {
|
||||||
|
if (!this.lastLocalMediaSyncResults) return ''
|
||||||
|
return this.lastLocalMediaSyncResults.serverConfigName
|
||||||
|
},
|
||||||
|
numLocalProgressUpdates() {
|
||||||
|
if (!this.lastLocalMediaSyncResults) return 0
|
||||||
|
return this.lastLocalMediaSyncResults.numLocalProgressUpdates || 0
|
||||||
|
},
|
||||||
|
numServerProgressUpdates() {
|
||||||
|
if (!this.lastLocalMediaSyncResults) return 0
|
||||||
|
return this.lastLocalMediaSyncResults.numServerProgressUpdates || 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -131,6 +131,11 @@ class AbsAudioPlayerWeb extends WebPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PluginMethod
|
||||||
|
async getIsCastAvailable() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
initializePlayer() {
|
initializePlayer() {
|
||||||
if (document.getElementById('audio-player')) {
|
if (document.getElementById('audio-player')) {
|
||||||
document.getElementById('audio-player').remove()
|
document.getElementById('audio-player').remove()
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class AbsDatabaseWeb extends WebPlugin {
|
|||||||
async removeServerConnectionConfig(serverConnectionConfigCallObject) {
|
async removeServerConnectionConfig(serverConnectionConfigCallObject) {
|
||||||
var serverConnectionConfigId = serverConnectionConfigCallObject.serverConnectionConfigId
|
var serverConnectionConfigId = serverConnectionConfigCallObject.serverConnectionConfigId
|
||||||
var deviceData = await this.getDeviceData()
|
var deviceData = await this.getDeviceData()
|
||||||
deviceData.serverConnectionConfigs = deviceData.serverConnectionConfigs.filter(ssc => ssc.id == serverConnectionConfigId)
|
deviceData.serverConnectionConfigs = deviceData.serverConnectionConfigs.filter(ssc => ssc.id != serverConnectionConfigId)
|
||||||
localStorage.setItem('device', JSON.stringify(deviceData))
|
localStorage.setItem('device', JSON.stringify(deviceData))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ class AbsDatabaseWeb extends WebPlugin {
|
|||||||
async getLocalLibraryItem({ id }) {
|
async getLocalLibraryItem({ id }) {
|
||||||
return this.getLocalLibraryItems().then((data) => data.value[0])
|
return this.getLocalLibraryItems().then((data) => data.value[0])
|
||||||
}
|
}
|
||||||
async getLocalLibraryItemByLLId({ libraryItemId }) {
|
async getLocalLibraryItemByLId({ libraryItemId }) {
|
||||||
return this.getLocalLibraryItems().then((data) => data.value.find(lli => lli.libraryItemId == libraryItemId))
|
return this.getLocalLibraryItems().then((data) => data.value.find(lli => lli.libraryItemId == libraryItemId))
|
||||||
}
|
}
|
||||||
async getAllLocalMediaProgress() {
|
async getAllLocalMediaProgress() {
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ const DownloadStatus = {
|
|||||||
FAILED: 3
|
FAILED: 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SyncStatus = {
|
||||||
|
UNSET: 0,
|
||||||
|
SUCCESS: 1,
|
||||||
|
FAILED: 2
|
||||||
|
}
|
||||||
|
|
||||||
const CoverDestination = {
|
const CoverDestination = {
|
||||||
METADATA: 0,
|
METADATA: 0,
|
||||||
AUDIOBOOK: 1
|
AUDIOBOOK: 1
|
||||||
@@ -31,6 +37,7 @@ const PlayerState = {
|
|||||||
|
|
||||||
const Constants = {
|
const Constants = {
|
||||||
DownloadStatus,
|
DownloadStatus,
|
||||||
|
SyncStatus,
|
||||||
CoverDestination,
|
CoverDestination,
|
||||||
BookCoverAspectRatio,
|
BookCoverAspectRatio,
|
||||||
PlayMethod,
|
PlayMethod,
|
||||||
|
|||||||
+2
-2
@@ -54,8 +54,8 @@ class DbService {
|
|||||||
return AbsDatabase.getLocalLibraryItem({ id })
|
return AbsDatabase.getLocalLibraryItem({ id })
|
||||||
}
|
}
|
||||||
|
|
||||||
getLocalLibraryItemByLLId(libraryItemId) {
|
getLocalLibraryItemByLId(libraryItemId) {
|
||||||
return AbsDatabase.getLocalLibraryItemByLLId({ libraryItemId })
|
return AbsDatabase.getLocalLibraryItemByLId({ libraryItemId })
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllLocalMediaProgress() {
|
getAllLocalMediaProgress() {
|
||||||
|
|||||||
+46
-19
@@ -14,21 +14,6 @@ if (Capacitor.getPlatform() != 'web') {
|
|||||||
setStatusBarStyleDark()
|
setStatusBarStyleDark()
|
||||||
}
|
}
|
||||||
|
|
||||||
App.addListener('backButton', async ({ canGoBack }) => {
|
|
||||||
if (!canGoBack) {
|
|
||||||
const { value } = await Dialog.confirm({
|
|
||||||
title: 'Confirm',
|
|
||||||
message: `Did you want to exit the app?`,
|
|
||||||
})
|
|
||||||
if (value) {
|
|
||||||
App.exitApp()
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
window.history.back()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Vue.prototype.$isDev = process.env.NODE_ENV !== 'production'
|
Vue.prototype.$isDev = process.env.NODE_ENV !== 'production'
|
||||||
|
|
||||||
Vue.prototype.$dateDistanceFromNow = (unixms) => {
|
Vue.prototype.$dateDistanceFromNow = (unixms) => {
|
||||||
@@ -52,17 +37,20 @@ Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
|
|||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.prototype.$elapsedPretty = (seconds) => {
|
Vue.prototype.$elapsedPretty = (seconds, useFullNames = false) => {
|
||||||
|
if (seconds < 60) {
|
||||||
|
return `${Math.floor(seconds)} sec${useFullNames ? 'onds' : ''}`
|
||||||
|
}
|
||||||
var minutes = Math.floor(seconds / 60)
|
var minutes = Math.floor(seconds / 60)
|
||||||
if (minutes < 70) {
|
if (minutes < 70) {
|
||||||
return `${minutes} min`
|
return `${minutes} min${useFullNames ? `ute${minutes === 1 ? '' : 's'}` : ''}`
|
||||||
}
|
}
|
||||||
var hours = Math.floor(minutes / 60)
|
var hours = Math.floor(minutes / 60)
|
||||||
minutes -= hours * 60
|
minutes -= hours * 60
|
||||||
if (!minutes) {
|
if (!minutes) {
|
||||||
return `${hours} hr`
|
return `${hours} ${useFullNames ? 'hours' : 'hr'}`
|
||||||
}
|
}
|
||||||
return `${hours} hr ${minutes} min`
|
return `${hours} ${useFullNames ? `hour${hours === 1 ? '' : 's'}` : 'hr'} ${minutes} ${useFullNames ? `minute${minutes === 1 ? '' : 's'}` : 'min'}`
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.prototype.$secondsToTimestamp = (seconds) => {
|
Vue.prototype.$secondsToTimestamp = (seconds) => {
|
||||||
@@ -132,6 +120,45 @@ Vue.prototype.$encode = encode
|
|||||||
const decode = (text) => Buffer.from(decodeURIComponent(text), 'base64').toString()
|
const decode = (text) => Buffer.from(decodeURIComponent(text), 'base64').toString()
|
||||||
Vue.prototype.$decode = decode
|
Vue.prototype.$decode = decode
|
||||||
|
|
||||||
|
export default ({ store, app }) => {
|
||||||
|
// iOS Only
|
||||||
|
// backButton event does not work with iOS swipe navigation so use this workaround
|
||||||
|
if (app.router && Capacitor.getPlatform() === 'ios') {
|
||||||
|
app.router.beforeEach((to, from, next) => {
|
||||||
|
if (store.state.globals.isModalOpen) {
|
||||||
|
Vue.prototype.$eventBus.$emit('close-modal')
|
||||||
|
}
|
||||||
|
if (store.state.playerIsFullscreen) {
|
||||||
|
Vue.prototype.$eventBus.$emit('minimize-player')
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android only
|
||||||
|
App.addListener('backButton', async ({ canGoBack }) => {
|
||||||
|
if (store.state.globals.isModalOpen) {
|
||||||
|
Vue.prototype.$eventBus.$emit('close-modal')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (store.state.playerIsFullscreen) {
|
||||||
|
Vue.prototype.$eventBus.$emit('minimize-player')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!canGoBack) {
|
||||||
|
const { value } = await Dialog.confirm({
|
||||||
|
title: 'Confirm',
|
||||||
|
message: `Did you want to exit the app?`,
|
||||||
|
})
|
||||||
|
if (value) {
|
||||||
|
App.exitApp()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.history.back()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
encode,
|
encode,
|
||||||
decode
|
decode
|
||||||
|
|||||||
+9
-8
@@ -39,6 +39,7 @@ class ServerSocket extends EventEmitter {
|
|||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
if (this.socket) this.socket.disconnect()
|
if (this.socket) this.socket.disconnect()
|
||||||
|
this.removeListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
setSocketListeners() {
|
setSocketListeners() {
|
||||||
@@ -54,6 +55,14 @@ class ServerSocket extends EventEmitter {
|
|||||||
// })
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeListeners() {
|
||||||
|
if (!this.socket) return
|
||||||
|
this.socket.removeAllListeners()
|
||||||
|
if (this.socket.io && this.socket.io.removeAllListeners) {
|
||||||
|
this.socket.io.removeAllListeners()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onConnect() {
|
onConnect() {
|
||||||
console.log('[SOCKET] Socket Connected ' + this.socket.id)
|
console.log('[SOCKET] Socket Connected ' + this.socket.id)
|
||||||
this.connected = true
|
this.connected = true
|
||||||
@@ -67,18 +76,10 @@ class ServerSocket extends EventEmitter {
|
|||||||
this.connected = false
|
this.connected = false
|
||||||
this.$store.commit('setSocketConnected', false)
|
this.$store.commit('setSocketConnected', false)
|
||||||
this.emit('connection-update', false)
|
this.emit('connection-update', false)
|
||||||
|
|
||||||
this.socket.removeAllListeners()
|
|
||||||
if (this.socket.io && this.socket.io.removeAllListeners) {
|
|
||||||
this.socket.io.removeAllListeners()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onInit(data) {
|
onInit(data) {
|
||||||
console.log('[SOCKET] Initial socket data received', data)
|
console.log('[SOCKET] Initial socket data received', data)
|
||||||
if (data.serverSettings) {
|
|
||||||
this.$store.commit('setServerSettings', data.serverSettings)
|
|
||||||
}
|
|
||||||
this.emit('initialized', true)
|
this.emit('initialized', true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
|
isModalOpen: false,
|
||||||
itemDownloads: [],
|
itemDownloads: [],
|
||||||
bookshelfListView: false,
|
bookshelfListView: false,
|
||||||
series: null,
|
series: null,
|
||||||
@@ -93,6 +94,9 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
|
setIsModalOpen(state, val) {
|
||||||
|
state.isModalOpen = val
|
||||||
|
},
|
||||||
addUpdateItemDownload(state, downloadItem) {
|
addUpdateItemDownload(state, downloadItem) {
|
||||||
var index = state.itemDownloads.findIndex(i => i.id == downloadItem.id)
|
var index = state.itemDownloads.findIndex(i => i.id == downloadItem.id)
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
|
|||||||
+9
-1
@@ -6,6 +6,7 @@ export const state = () => ({
|
|||||||
playerEpisodeId: null,
|
playerEpisodeId: null,
|
||||||
playerIsLocal: false,
|
playerIsLocal: false,
|
||||||
playerIsPlaying: false,
|
playerIsPlaying: false,
|
||||||
|
playerIsFullscreen: false,
|
||||||
isCasting: false,
|
isCasting: false,
|
||||||
isCastAvailable: false,
|
isCastAvailable: false,
|
||||||
socketConnected: false,
|
socketConnected: false,
|
||||||
@@ -18,7 +19,8 @@ export const state = () => ({
|
|||||||
showSideDrawer: false,
|
showSideDrawer: false,
|
||||||
isNetworkListenerInit: false,
|
isNetworkListenerInit: false,
|
||||||
serverSettings: null,
|
serverSettings: null,
|
||||||
lastBookshelfScrollData: {}
|
lastBookshelfScrollData: {},
|
||||||
|
lastLocalMediaSyncResults: null
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
@@ -93,6 +95,9 @@ export const mutations = {
|
|||||||
setPlayerPlaying(state, val) {
|
setPlayerPlaying(state, val) {
|
||||||
state.playerIsPlaying = val
|
state.playerIsPlaying = val
|
||||||
},
|
},
|
||||||
|
setPlayerFullscreen(state, val) {
|
||||||
|
state.playerIsFullscreen = val
|
||||||
|
},
|
||||||
setHasStoragePermission(state, val) {
|
setHasStoragePermission(state, val) {
|
||||||
state.hasStoragePermission = val
|
state.hasStoragePermission = val
|
||||||
},
|
},
|
||||||
@@ -122,5 +127,8 @@ export const mutations = {
|
|||||||
setServerSettings(state, val) {
|
setServerSettings(state, val) {
|
||||||
state.serverSettings = val
|
state.serverSettings = val
|
||||||
this.$localStore.setServerSettings(state.serverSettings)
|
this.$localStore.setServerSettings(state.serverSettings)
|
||||||
|
},
|
||||||
|
setLastLocalMediaSyncResults(state, val) {
|
||||||
|
state.lastLocalMediaSyncResults = val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,9 @@ export const getters = {
|
|||||||
getServerAddress: (state) => {
|
getServerAddress: (state) => {
|
||||||
return state.serverConnectionConfig ? state.serverConnectionConfig.address : null
|
return state.serverConnectionConfig ? state.serverConnectionConfig.address : null
|
||||||
},
|
},
|
||||||
|
getServerConfigName: (state) => {
|
||||||
|
return state.serverConnectionConfig ? state.serverConnectionConfig.name : null
|
||||||
|
},
|
||||||
getCustomHeaders: (state) => {
|
getCustomHeaders: (state) => {
|
||||||
return state.serverConnectionConfig ? state.serverConnectionConfig.customHeaders : null
|
return state.serverConnectionConfig ? state.serverConnectionConfig.customHeaders : null
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ module.exports = {
|
|||||||
sans: ['Source Sans Pro', ...defaultTheme.fontFamily.sans],
|
sans: ['Source Sans Pro', ...defaultTheme.fontFamily.sans],
|
||||||
mono: ['Ubuntu Mono', ...defaultTheme.fontFamily.mono],
|
mono: ['Ubuntu Mono', ...defaultTheme.fontFamily.mono],
|
||||||
book: ['Gentium Book Basic', 'serif']
|
book: ['Gentium Book Basic', 'serif']
|
||||||
|
},
|
||||||
|
fontSize: {
|
||||||
|
xxs: '0.625rem'
|
||||||
|
},
|
||||||
|
maxWidth: {
|
||||||
|
'24': '6rem'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user