Merge branch 'master' into ios-downloads

This commit is contained in:
ronaldheft 2022-07-30 15:16:58 -04:00
commit db7a8cef77
66 changed files with 1117 additions and 285 deletions

View file

@ -21,7 +21,11 @@ kotlin {
} }
android { android {
kotlinOptions {
buildFeatures {
viewBinding true
}
kotlinOptions {
freeCompilerArgs = ['-Xjvm-default=all'] freeCompilerArgs = ['-Xjvm-default=all']
} }
compileSdkVersion rootProject.ext.compileSdkVersion compileSdkVersion rootProject.ext.compileSdkVersion
@ -29,8 +33,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 82 versionCode 86
versionName "0.9.51-beta" versionName "0.9.55-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 +115,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'

View file

@ -1,83 +1,94 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution" xmlns:dist="http://schemas.android.com/apk/distribution"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:installLocation="preferExternal" package="com.audiobookshelf.app"
package="com.audiobookshelf.app"> android:installLocation="preferExternal" >
<!-- Permissions --> <!-- Permissions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true" >
android:networkSecurityConfig="@xml/network_security_config" <!-- <receiver-->
android:requestLegacyExternalStorage="true"> <!-- android:name=".NewAppWidget"-->
<!-- android:exported="true" >-->
<!-- <intent-filter>-->
<!-- <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />-->
<!-- </intent-filter>-->
<!-- <meta-data-->
<!-- android:name="android.appwidget.provider"-->
<!-- android:resource="@xml/new_app_widget_info" />-->
<!-- </receiver>-->
<!--Used by Android Auto--> <!-- Used by Android Auto -->
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
android:resource="@drawable/icon" />
<meta-data <meta-data
android:name="com.google.android.gms.car.application" android:name="com.google.android.gms.car.notification.SmallIcon"
android:resource="@xml/automotive_app_desc"/> android:resource="@drawable/icon" />
<meta-data
<!-- Support for Cast --> android:name="com.google.android.gms.car.application"
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:resource="@xml/automotive_app_desc" /> <!-- Support for Cast -->
android:value="com.audiobookshelf.app.CastOptionsProvider"/> <meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.audiobookshelf.app.CastOptionsProvider" />
<activity <activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:name="com.audiobookshelf.app.MainActivity"
android:label="@string/title_activity_main"
android:exported="true" android:exported="true"
android:theme="@style/AppTheme.NoActionBarLaunch" android:label="@string/title_activity_main"
android:launchMode="singleTask"> android:launchMode="singleTask"
android:theme="@style/AppTheme.NoActionBarLaunch" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<intent-filter> <category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.MAIN" /> </intent-filter>
<category android:name="android.intent.category.LAUNCHER" /> <intent-filter>
</intent-filter> <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<intent-filter> <category android:name="android.intent.category.DEFAULT" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" /> </intent-filter>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- Register URL scheme --> <!-- Register URL scheme -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.DEFAULT" />
<data android:scheme="@string/custom_url_scheme" /> <category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
<data android:scheme="@string/custom_url_scheme" />
</intent-filter>
</activity> </activity>
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<receiver android:name="androidx.media.session.MediaButtonReceiver" > <service
<intent-filter> android:name=".player.PlayerNotificationService"
<action android:name="android.intent.action.MEDIA_BUTTON" /> android:enabled="true"
</intent-filter> android:exported="true" >
</receiver> <intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
<service </intent-filter>
android:exported="true" </service>
android:enabled="true"
android:name=".player.PlayerNotificationService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService"/>
</intent-filter>
</service>
</application> </application>
</manifest> </manifest>

View file

@ -19,7 +19,6 @@ 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() {
@ -58,10 +57,6 @@ class MainActivity : BridgeActivity() {
DbManager.initialize(applicationContext) DbManager.initialize(applicationContext)
// Grant full storage access for testing
// var ss = SimpleStorage(this)
// ss.requestFullStorageAccess()
val 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,
@ -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()
}
} }
} }

View file

@ -0,0 +1,96 @@
package com.audiobookshelf.app
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_PAUSE
import android.util.Log
import android.widget.RemoteViews
import androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.device.WidgetEventEmitter
import com.audiobookshelf.app.player.PlayerNotificationService
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.AppWidgetTarget
import com.bumptech.glide.request.transition.Transition
/**
* Implementation of App Widget functionality.
*/
class NewAppWidget : AppWidgetProvider() {
val tag = "NewAppWidget"
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId, null,false)
}
}
override fun onEnabled(context: Context) {
Log.w(tag, "onEnabled check context ${context.packageName}")
// Enter relevant functionality for when the first widget is created
DeviceManager.widgetUpdater = (object : WidgetEventEmitter {
override fun onPlayerChanged(pns:PlayerNotificationService) {
val isPlaying = pns.currentPlayer.isPlaying
Log.i(tag, "onPlayerChanged | Is Playing? $isPlaying")
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName = ComponentName(context, NewAppWidget::class.java)
val ids = appWidgetManager.getAppWidgetIds(componentName)
val playbackSession = pns.getCurrentPlaybackSessionCopy()
val cover = playbackSession?.getCoverUri()
for (widgetId in ids) {
updateAppWidget(context, appWidgetManager, widgetId, cover, isPlaying)
}
}
})
}
override fun onDisabled(context: Context) {
// Enter relevant functionality for when the last widget is disabled
}
}
internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, coverUri:Uri?, isPlaying:Boolean) {
val views = RemoteViews(context.packageName, R.layout.new_app_widget)
val playPausePI = buildMediaButtonPendingIntent(context, ACTION_PLAY_PAUSE)
views.setOnClickPendingIntent(R.id.playPauseIcon, playPausePI)
val wholeWidgetClickI = Intent(context, MainActivity::class.java)
wholeWidgetClickI.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
val wholeWidgetClickPI = PendingIntent.getActivity(
context,
System.currentTimeMillis().toInt(),
wholeWidgetClickI,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
views.setOnClickPendingIntent(R.id.appWidget, wholeWidgetClickPI)
val imageUri = coverUri ?: Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
val awt: AppWidgetTarget = object : AppWidgetTarget(context.applicationContext, R.id.imageView, views, appWidgetId) {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
super.onResourceReady(resource, transition)
}
}
val options = RequestOptions().override(300, 300).placeholder(R.drawable.icon).error(R.drawable.icon)
Glide.with(context.applicationContext).asBitmap().load(imageUri).apply(options).into(awt)
val playPauseResource = if (isPlaying) R.drawable.ic_media_pause_dark else R.drawable.ic_media_play_dark
views.setImageViewResource(R.id.playPauseIcon, playPauseResource)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}

View file

@ -45,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 }
} }
@ -213,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) {

View file

@ -18,13 +18,19 @@ data class ServerConnectionConfig(
data class DeviceSettings( data class DeviceSettings(
var disableAutoRewind:Boolean, var disableAutoRewind:Boolean,
var enableAltView:Boolean,
var jumpBackwardsTime:Int, var jumpBackwardsTime:Int,
var jumpForwardTime:Int var jumpForwardTime:Int
) { ) {
companion object { companion object {
// Static method to get default device settings // Static method to get default device settings
fun default():DeviceSettings { fun default():DeviceSettings {
return DeviceSettings(false, 10, 10) return DeviceSettings(
disableAutoRewind = false,
enableAltView = false,
jumpBackwardsTime = 10,
jumpForwardTime = 10
)
} }
} }

View file

@ -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

View file

@ -1,9 +1,12 @@
package com.audiobookshelf.app.device package com.audiobookshelf.app.device
import android.util.Log import android.util.Log
import com.audiobookshelf.app.data.DbManager import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.data.DeviceData import com.audiobookshelf.app.player.PlayerNotificationService
import com.audiobookshelf.app.data.ServerConnectionConfig
interface WidgetEventEmitter {
fun onPlayerChanged(pns:PlayerNotificationService)
}
object DeviceManager { object DeviceManager {
const val tag = "DeviceManager" const val tag = "DeviceManager"
@ -19,6 +22,8 @@ object DeviceManager {
val isConnectedToServer get() = serverConnectionConfig != null val isConnectedToServer get() = serverConnectionConfig != null
val hasLastServerConnectionConfig get() = deviceData.getLastServerConnectionConfig() != null val hasLastServerConnectionConfig get() = deviceData.getLastServerConnectionConfig() != null
var widgetUpdater:WidgetEventEmitter? = null
init { init {
Log.d(tag, "Device Manager Singleton invoked") Log.d(tag, "Device Manager Singleton invoked")
} }

View file

@ -1,21 +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 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 serverLibraryItems = listOf<LibraryItem>() var serverLibraryItems = mutableListOf<LibraryItem>()
var selectedLibraryId = "" var selectedLibraryId = ""
var selectedLibraryItemWrapper:LibraryItemWrapper? = null var selectedLibraryItemWrapper:LibraryItemWrapper? = null
@ -26,10 +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
var userSettingsPlaybackRate:Float? = null
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
@ -39,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 = ""
} }
} }
@ -63,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)
} }
} }
@ -167,6 +199,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
if (result) { if (result) {
hasValidConn = true hasValidConn = true
DeviceManager.serverConnectionConfig = config DeviceManager.serverConnectionConfig = config
Log.d(tag, "checkSetValidServerConnectionConfig: Set server connection config ${DeviceManager.serverConnectionConfigId}")
break break
} }
} }
@ -204,21 +237,37 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
serverConfigIdUsed = DeviceManager.serverConnectionConfigId serverConfigIdUsed = DeviceManager.serverConnectionConfigId
loadLibraries { libraries -> loadLibraries { libraries ->
val library = libraries[0] if (libraries.isEmpty()) {
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}") Log.w(tag, "No libraries returned from server request")
cb(cats) // Return download category only
} else {
val library = libraries[0]
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}")
loadLibraryCategories(library.id) { libraryCategories -> loadLibraryCategories(library.id) { libraryCategories ->
// Only using book or podcast library categories for now // Only using book or podcast library categories for now
libraryCategories.forEach { libraryCategories.forEach {
// Log.d(tag, "Found library category ${it.label} with type ${it.type}")
if (it.type == library.mediaType) { // Add items in continue listening to serverLibraryItems
// Log.d(tag, "Using library category ${it.id}") if (it.id == "continue-listening") {
cats.add(it) 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}")
if (it.type == library.mediaType) {
// Log.d(tag, "Using library category ${it.id}")
cats.add(it)
}
} }
}
cb(cats) cb(cats)
}
} }
} }
} else { // Not connected/no internet sent downloaded cats only } else { // Not connected/no internet sent downloaded cats only
@ -265,19 +314,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 {

View file

@ -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

View file

@ -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
@ -20,6 +19,7 @@ data class MediaProgressSyncData(
class MediaProgressSyncer(val playerNotificationService:PlayerNotificationService, private val apiHandler: ApiHandler) { class MediaProgressSyncer(val playerNotificationService:PlayerNotificationService, private val apiHandler: ApiHandler) {
private val tag = "MediaProgressSync" private val tag = "MediaProgressSync"
private val METERED_CONNECTION_SYNC_INTERVAL = 60000
private var listeningTimerTask: TimerTask? = null private var listeningTimerTask: TimerTask? = null
var listeningTimerRunning:Boolean = false var listeningTimerRunning:Boolean = false
@ -43,88 +43,163 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
currentLocalMediaProgress = null currentLocalMediaProgress = null
listeningTimerTask?.cancel() listeningTimerTask?.cancel()
lastSyncTime = 0L lastSyncTime = 0L
Log.d(tag, "start: Set last sync time 0 $lastSyncTime")
failedSyncs = 0 failedSyncs = 0
} else { } else {
return return
} }
} else if (playerNotificationService.getCurrentPlaybackSessionId() != currentSessionId) {
currentLocalMediaProgress = null
} }
listeningTimerRunning = true listeningTimerRunning = true
lastSyncTime = System.currentTimeMillis() lastSyncTime = System.currentTimeMillis()
Log.d(tag, "start: init last sync time $lastSyncTime")
currentPlaybackSession = playerNotificationService.getCurrentPlaybackSessionCopy() currentPlaybackSession = playerNotificationService.getCurrentPlaybackSessionCopy()
listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) { listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) {
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
if (playerNotificationService.currentPlayer.isPlaying) { if (playerNotificationService.currentPlayer.isPlaying) {
// Only sync with server on unmetered connection every 5s OR sync with server if last sync time is >= 60s
val shouldSyncServer = PlayerNotificationService.isUnmeteredNetwork || System.currentTimeMillis() - lastSyncTime >= METERED_CONNECTION_SYNC_INTERVAL
val currentTime = playerNotificationService.getCurrentTimeSeconds() val currentTime = playerNotificationService.getCurrentTimeSeconds()
sync(currentTime) if (currentTime > 0) {
sync(shouldSyncServer, currentTime) {
Log.d(tag, "Sync complete")
}
}
} }
} }
} }
} }
fun stop() { fun stop(cb: () -> Unit) {
if (!listeningTimerRunning) return if (!listeningTimerRunning) return
listeningTimerTask?.cancel()
listeningTimerTask = null
listeningTimerRunning = false
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) if (currentTime > 0) { // Current time should always be > 0 on stop
reset() sync(true, currentTime) {
reset()
cb()
}
} else {
reset()
cb()
}
}
fun pause(cb: () -> Unit) {
if (!listeningTimerRunning) return
listeningTimerTask?.cancel()
listeningTimerTask = null
listeningTimerRunning = false
Log.d(tag, "pause: Pausing progress syncer for $currentDisplayTitle")
Log.d(tag, "pause: Last sync time $lastSyncTime")
val currentTime = playerNotificationService.getCurrentTimeSeconds()
if (currentTime > 0) { // Current time should always be > 0 on pause
sync(true, currentTime) {
lastSyncTime = 0L
Log.d(tag, "pause: Set last sync time 0 $lastSyncTime")
failedSyncs = 0
cb()
}
} else {
lastSyncTime = 0L
Log.d(tag, "pause: Set last sync time 0 $lastSyncTime (current time < 0)")
failedSyncs = 0
cb()
}
} }
fun syncFromServerProgress(mediaProgress: MediaProgress) { fun syncFromServerProgress(mediaProgress: MediaProgress) {
currentPlaybackSession?.let { currentPlaybackSession?.let {
it.updatedAt = mediaProgress.lastUpdate it.updatedAt = mediaProgress.lastUpdate
it.currentTime = mediaProgress.currentTime it.currentTime = mediaProgress.currentTime
DeviceManager.dbManager.saveLocalPlaybackSession(it) DeviceManager.dbManager.saveLocalPlaybackSession(it)
saveLocalProgress(it) saveLocalProgress(it)
} }
} }
fun sync(currentTime:Double) { fun sync(shouldSyncServer:Boolean, currentTime:Double, cb: () -> Unit) {
val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime if (lastSyncTime <= 0) {
if (diffSinceLastSync < 1000L) { Log.e(tag, "Last sync time is not set $lastSyncTime")
return return
} }
val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
if (diffSinceLastSync < 1000L) {
return cb()
}
val listeningTimeToAdd = diffSinceLastSync / 1000L val listeningTimeToAdd = diffSinceLastSync / 1000L
lastSyncTime = System.currentTimeMillis()
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 {
DeviceManager.dbManager.saveLocalPlaybackSession(it) DeviceManager.dbManager.saveLocalPlaybackSession(it)
saveLocalProgress(it) saveLocalProgress(it)
lastSyncTime = System.currentTimeMillis()
// 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 (shouldSyncServer && !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 if (shouldSyncServer) {
apiHandler.sendProgressSync(currentSessionId, syncData) { apiHandler.sendProgressSync(currentSessionId, syncData) {
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()
lastSyncTime = System.currentTimeMillis()
} 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()
} }
} else {
cb()
} }
} }
@ -140,19 +215,22 @@ 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}%")
}
} }
} }
fun reset() { fun reset() {
listeningTimerTask?.cancel()
listeningTimerTask = null
listeningTimerRunning = false
currentPlaybackSession = null currentPlaybackSession = null
currentLocalMediaProgress = null currentLocalMediaProgress = null
lastSyncTime = 0L lastSyncTime = 0L
Log.d(tag, "reset: Set last sync time 0 $lastSyncTime")
failedSyncs = 0 failedSyncs = 0
} }
} }

View file

@ -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)
} }
} }
} }
@ -127,8 +132,25 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
} }
fun handleCallMediaButton(intent: Intent): Boolean { fun handleCallMediaButton(intent: Intent): Boolean {
Log.w(tag, "handleCallMediaButton $intent | ${intent.action}")
if(Intent.ACTION_MEDIA_BUTTON == intent.action) { if(Intent.ACTION_MEDIA_BUTTON == intent.action) {
val keyEvent = intent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT) val keyEvent = intent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
Log.d(tag, "handleCallMediaButton keyEvent = $keyEvent | action ${keyEvent?.action}")
if (keyEvent?.action == KeyEvent.ACTION_DOWN) {
Log.d(tag, "handleCallMediaButton: key action_down for ${keyEvent.keyCode}")
when (keyEvent.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
Log.d(tag, "handleCallMediaButton: Media Play/Pause")
if (playerNotificationService.mPlayer.isPlaying) {
playerNotificationService.pause()
} else {
playerNotificationService.play()
}
}
}
}
if (keyEvent?.action == KeyEvent.ACTION_UP) { if (keyEvent?.action == KeyEvent.ACTION_UP) {
Log.d(tag, "handleCallMediaButton: key action_up for ${keyEvent.keyCode}") Log.d(tag, "handleCallMediaButton: key action_up for ${keyEvent.keyCode}")
@ -214,4 +236,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) {
//
// }
// }
} }

View file

@ -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)
} }
} }
} }

View file

@ -71,32 +71,36 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
if (player.isPlaying) { if (player.isPlaying) {
Log.d(tag, "SeekBackTime: Player is playing") Log.d(tag, "SeekBackTime: Player is playing")
if (lastPauseTime > 0 && DeviceManager.deviceData.deviceSettings?.disableAutoRewind != true) { if (lastPauseTime > 0 && DeviceManager.deviceData.deviceSettings?.disableAutoRewind != true) {
var seekBackTime = 0L
if (onSeekBack) onSeekBack = false if (onSeekBack) onSeekBack = false
else { else {
Log.d(tag, "SeekBackTime: playing started now set seek back time $lastPauseTime") Log.d(tag, "SeekBackTime: playing started now set seek back time $lastPauseTime")
var backTime = calcPauseSeekBackTime() seekBackTime = calcPauseSeekBackTime()
if (backTime > 0) { if (seekBackTime > 0) {
// Current chapter is used so that seek back does not go back to the previous chapter // Current chapter is used so that seek back does not go back to the previous chapter
val currentChapter = playerNotificationService.getCurrentBookChapter() val currentChapter = playerNotificationService.getCurrentBookChapter()
val minSeekBackTime = currentChapter?.startMs ?: 0 val minSeekBackTime = currentChapter?.startMs ?: 0
val currentTime = playerNotificationService.getCurrentTime() val currentTime = playerNotificationService.getCurrentTime()
val newTime = currentTime - backTime val newTime = currentTime - seekBackTime
if (newTime < minSeekBackTime) { if (newTime < minSeekBackTime) {
backTime = currentTime - minSeekBackTime seekBackTime = currentTime - minSeekBackTime
} }
Log.d(tag, "SeekBackTime $backTime") Log.d(tag, "SeekBackTime $seekBackTime")
onSeekBack = true onSeekBack = true
playerNotificationService.seekBackward(backTime)
} }
} }
// Check if playback session still exists or sync media progress if updated // Check if playback session still exists or sync media progress if updated
val pauseLength: Long = System.currentTimeMillis() - lastPauseTime val pauseLength: Long = System.currentTimeMillis() - lastPauseTime
if (pauseLength > PAUSE_LEN_BEFORE_RECHECK) { if (pauseLength > PAUSE_LEN_BEFORE_RECHECK) {
val shouldCarryOn = playerNotificationService.checkCurrentSessionProgress() val shouldCarryOn = playerNotificationService.checkCurrentSessionProgress(seekBackTime)
if (!shouldCarryOn) return if (!shouldCarryOn) return
} }
if (seekBackTime > 0L) {
playerNotificationService.seekBackward(seekBackTime)
}
} }
} else { } else {
Log.d(tag, "SeekBackTime: Player not playing set last pause time") Log.d(tag, "SeekBackTime: Player not playing set last pause time")
@ -104,15 +108,18 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
} }
// Start/stop progress sync interval // Start/stop progress sync interval
Log.d(tag, "Playing ${playerNotificationService.getCurrentBookTitle()}")
if (player.isPlaying) { if (player.isPlaying) {
player.volume = 1F // Volume on sleep timer might have decreased this player.volume = 1F // Volume on sleep timer might have decreased this
playerNotificationService.mediaProgressSyncer.start() playerNotificationService.mediaProgressSyncer.start()
} else { } else {
playerNotificationService.mediaProgressSyncer.stop() playerNotificationService.mediaProgressSyncer.pause {
Log.d(tag, "Media Progress Syncer paused and synced")
}
} }
playerNotificationService.clientEventEmitter?.onPlayingUpdate(player.isPlaying) playerNotificationService.clientEventEmitter?.onPlayingUpdate(player.isPlaying)
DeviceManager.widgetUpdater?.onPlayerChanged(playerNotificationService)
} }
} }

View file

@ -6,6 +6,10 @@ import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.hardware.Sensor import android.hardware.Sensor
import android.hardware.SensorManager import android.hardware.SensorManager
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.* import android.os.*
import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.MediaDescriptionCompat
@ -15,9 +19,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
@ -42,6 +49,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
companion object { companion object {
var isStarted = false var isStarted = false
var isClosed = false var isClosed = false
var isUnmeteredNetwork = false
} }
interface ClientEventEmitter { interface ClientEventEmitter {
@ -55,6 +63,8 @@ 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()
fun onNetworkMeteredChanged(isUnmetered:Boolean)
} }
private val tag = "PlayerService" private val tag = "PlayerService"
@ -65,7 +75,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 +170,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
super.onCreate() super.onCreate()
ctx = this ctx = this
// To listen for network change from metered to unmetered
val networkRequest = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.build()
val connectivityManager = getSystemService(ConnectivityManager::class.java) as ConnectivityManager
connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
DbManager.initialize(ctx) DbManager.initialize(ctx)
// Initialize API // Initialize API
@ -191,6 +210,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
.apply { .apply {
setSessionActivity(sessionActivityPendingIntent) setSessionActivity(sessionActivityPendingIntent)
isActive = true isActive = true
setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
} }
val mediaController = MediaControllerCompat(ctx, mediaSession.sessionToken) val mediaController = MediaControllerCompat(ctx, mediaSession.sessionToken)
@ -261,6 +281,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()
@ -300,6 +332,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
@ -527,10 +566,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
// Called from PlayerListener play event // Called from PlayerListener play event
// check with server if progress has updated since last play and sync progress update // check with server if progress has updated since last play and sync progress update
fun checkCurrentSessionProgress():Boolean { fun checkCurrentSessionProgress(seekBackTime:Long):Boolean {
if (currentPlaybackSession == null) return true if (currentPlaybackSession == null) return true
currentPlaybackSession?.let { playbackSession -> mediaProgressSyncer.currentPlaybackSession?.let { playbackSession ->
if (!apiHandler.isOnline() || playbackSession.isLocalLibraryItemOnly) { if (!apiHandler.isOnline() || playbackSession.isLocalLibraryItemOnly) {
return true // carry on return true // carry on
} }
@ -552,16 +591,28 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
Log.d(tag, "checkCurrentSessionProgress: Media progress was updated since last play time updating from ${playbackSession.currentTime} to ${mediaProgress.currentTime}") Log.d(tag, "checkCurrentSessionProgress: Media progress was updated since last play time updating from ${playbackSession.currentTime} to ${mediaProgress.currentTime}")
mediaProgressSyncer.syncFromServerProgress(mediaProgress) mediaProgressSyncer.syncFromServerProgress(mediaProgress)
// Update current playback session stored in PNS since MediaProgressSyncer version is a copy
mediaProgressSyncer.currentPlaybackSession?.let { updatedPlaybackSession ->
currentPlaybackSession = updatedPlaybackSession
}
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
seekPlayer(playbackSession.currentTimeMs) seekPlayer(playbackSession.currentTimeMs)
// Should already be playing
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
mediaProgressSyncer.start()
clientEventEmitter?.onPlayingUpdate(true)
}
} else {
Handler(Looper.getMainLooper()).post {
if (seekBackTime > 0L) {
seekBackward(seekBackTime)
}
// Should already be playing
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
mediaProgressSyncer.start()
clientEventEmitter?.onPlayingUpdate(true)
} }
}
Handler(Looper.getMainLooper()).post {
// Should already be playing
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
mediaProgressSyncer.start()
clientEventEmitter?.onPlayingUpdate(true)
} }
} }
} else { } else {
@ -584,6 +635,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} else { } else {
Log.d(tag, "checkCurrentSessionProgress: Playback session still available on server") Log.d(tag, "checkCurrentSessionProgress: Playback session still available on server")
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
if (seekBackTime > 0L) {
seekBackward(seekBackTime)
}
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
mediaProgressSyncer.start() mediaProgressSyncer.start()
clientEventEmitter?.onPlayingUpdate(true) clientEventEmitter?.onPlayingUpdate(true)
@ -648,6 +703,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
fun closePlayback() { fun closePlayback() {
Log.d(tag, "closePlayback") Log.d(tag, "closePlayback")
if (mediaProgressSyncer.listeningTimerRunning) {
Log.i(tag, "About to close playback so stopping media progress syncer first")
mediaProgressSyncer.stop {
Log.d(tag, "Media Progress syncer stopped and synced")
}
}
try { try {
currentPlayer.stop() currentPlayer.stop()
currentPlayer.clearMediaItems() currentPlayer.clearMediaItems()
@ -660,6 +722,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
PlayerListener.lastPauseTime = 0 PlayerListener.lastPauseTime = 0
isClosed = true isClosed = true
stopForeground(true) stopForeground(true)
stopSelf()
} }
fun sendClientMetadata(playerState: PlayerState) { fun sendClientMetadata(playerState: PlayerState) {
@ -694,6 +757,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
clientEventEmitter?.onProgressSyncFailing() clientEventEmitter?.onProgressSyncFailing()
} }
fun alertSyncSuccess() {
clientEventEmitter?.onProgressSyncSuccess()
}
// //
// MEDIA BROWSER STUFF (ANDROID AUTO) // MEDIA BROWSER STUFF (ANDROID AUTO)
// //
@ -702,7 +769,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private val ANDROID_WEARABLE_PKG_NAME = "com.google.android.wearable.app" private val ANDROID_WEARABLE_PKG_NAME = "com.google.android.wearable.app"
private val ANDROID_GSEARCH_PKG_NAME = "com.google.android.googlequicksearchbox" private val ANDROID_GSEARCH_PKG_NAME = "com.google.android.googlequicksearchbox"
private val ANDROID_AUTOMOTIVE_PKG_NAME = "com.google.android.carassistant" private val ANDROID_AUTOMOTIVE_PKG_NAME = "com.google.android.carassistant"
private val VALID_MEDIA_BROWSERS = mutableListOf<String>(ANDROID_AUTO_PKG_NAME, ANDROID_AUTO_SIMULATOR_PKG_NAME, ANDROID_WEARABLE_PKG_NAME, ANDROID_GSEARCH_PKG_NAME, ANDROID_AUTOMOTIVE_PKG_NAME) private val VALID_MEDIA_BROWSERS = mutableListOf("com.audiobookshelf.app", ANDROID_AUTO_PKG_NAME, ANDROID_AUTO_SIMULATOR_PKG_NAME, ANDROID_WEARABLE_PKG_NAME, ANDROID_GSEARCH_PKG_NAME, ANDROID_AUTOMOTIVE_PKG_NAME)
private val AUTO_MEDIA_ROOT = "/" private val AUTO_MEDIA_ROOT = "/"
private val ALL_ROOT = "__ALL__" private val ALL_ROOT = "__ALL__"
@ -863,5 +930,19 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} }
} }
} }
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
// Network capabilities have changed for the network
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
super.onCapabilitiesChanged(network, networkCapabilities)
val unmetered = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
Log.i(tag, "Network capabilities changed is unmetered = $unmetered")
isUnmeteredNetwork = unmetered
clientEventEmitter?.onNetworkMeteredChanged(unmetered)
}
}
} }

View file

@ -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,14 @@ class AbsAudioPlayer : Plugin() {
override fun onProgressSyncFailing() { override fun onProgressSyncFailing() {
emit("onProgressSyncFailing", "") emit("onProgressSyncFailing", "")
} }
override fun onProgressSyncSuccess() {
emit("onProgressSyncSuccess", "")
}
override fun onNetworkMeteredChanged(isUnmetered:Boolean) {
emit("onNetworkMeteredChanged", isUnmetered)
}
}) })
} }
mainActivity.pluginCallback = foregroundServiceReady mainActivity.pluginCallback = foregroundServiceReady
@ -150,14 +156,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
@ -183,7 +181,21 @@ class AbsAudioPlayer : Plugin() {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
Log.d(tag, "prepareLibraryItem: Preparing Local Media item ${jacksonMapper.writeValueAsString(it)}") Log.d(tag, "prepareLibraryItem: Preparing Local Media item ${jacksonMapper.writeValueAsString(it)}")
val playbackSession = it.getPlaybackSession(episode) val playbackSession = it.getPlaybackSession(episode)
playerNotificationService.preparePlayer(playbackSession, playWhenReady, playbackRate)
if (playerNotificationService.mediaProgressSyncer.listeningTimerRunning) { // If progress syncing then first stop before preparing next
playerNotificationService.mediaProgressSyncer.stop {
Log.d(tag, "Media progress syncer was already syncing - stopped")
Handler(Looper.getMainLooper()).post { // TODO: This was needed again which is probably a design a flaw
playerNotificationService.preparePlayer(
playbackSession,
playWhenReady,
playbackRate
)
}
}
} else {
playerNotificationService.preparePlayer(playbackSession, playWhenReady, playbackRate)
}
} }
return call.resolve(JSObject()) return call.resolve(JSObject())
} }
@ -194,9 +206,20 @@ class AbsAudioPlayer : Plugin() {
if (it == null) { if (it == null) {
call.resolve(JSObject("{\"error\":\"Server play request failed\"}")) call.resolve(JSObject("{\"error\":\"Server play request failed\"}"))
} else { } else {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
Log.d(tag, "Preparing Player TEST ${jacksonMapper.writeValueAsString(it)}") Log.d(tag, "Preparing Player playback session ${jacksonMapper.writeValueAsString(it)}")
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
if (playerNotificationService.mediaProgressSyncer.listeningTimerRunning) { // If progress syncing then first stop before preparing next
playerNotificationService.mediaProgressSyncer.stop {
Log.d(tag, "Media progress syncer was already syncing - stopped")
Handler(Looper.getMainLooper()).post { // TODO: This was needed again which is probably a design a flaw
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
}
}
} else {
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
}
} }
call.resolve(JSObject(jacksonMapper.writeValueAsString(it))) call.resolve(JSObject(jacksonMapper.writeValueAsString(it)))

View file

@ -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 {

View file

@ -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)

View file

@ -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>)
@ -81,6 +79,12 @@ class ApiHandler(var ctx:Context) {
return false return false
} }
fun isUsingCellularData(): Boolean {
val connectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
return capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true
}
fun makeRequest(request:Request, httpClient:OkHttpClient?, cb: (JSObject) -> Unit) { fun makeRequest(request:Request, httpClient:OkHttpClient?, cb: (JSObject) -> Unit) {
val client = httpClient ?: defaultClient val client = httpClient ?: defaultClient
client.newCall(request).enqueue(object : Callback { client.newCall(request).enqueue(object : Callback {
@ -106,14 +110,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 +236,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)
}
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Background for widgets to make the rounded corners based on the
appWidgetRadius attribute value
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="?attr/appWidgetRadius" />
<solid android:color="?android:attr/colorBackground" />
</shape>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Background for views inside widgets to make the rounded corners based on the
appWidgetInnerRadius attribute value
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="?attr/appWidgetInnerRadius" />
<solid android:color="?android:attr/colorPrimaryDark" />
</shape>

View file

@ -0,0 +1,26 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/Widget.Android.AppWidget.Container"
android:id="@+id/appWidget"
android:theme="@style/AppTheme.AppWidgetContainer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:adjustViewBounds="true"
android:alpha="0.75"
android:contentDescription="Cover image"
android:visibility="visible" />
<ImageView
android:id="@+id/playPauseIcon"
android:layout_width="71dp"
android:layout_height="55dp"
android:layout_centerInParent="true"
app:srcCompat="@drawable/cast_ic_expanded_controller_play" />
</RelativeLayout>

View file

@ -0,0 +1,11 @@
<resources>
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
<item name="android:padding">?attr/appWidgetPadding</item>
<item name="android:background">@drawable/app_widget_background</item>
</style>
<style name="Widget.Android.AppWidget.InnerView" parent="android:Widget">
<item name="android:padding">?attr/appWidgetPadding</item>
<item name="android:background">@drawable/app_widget_inner_view_background</item>
</style>
</resources>

View file

@ -0,0 +1,7 @@
<resources>
<declare-styleable name="AppWidgetAttrs">
<attr name="appWidgetPadding" format="dimension" />
<attr name="appWidgetInnerRadius" format="dimension" />
<attr name="appWidgetRadius" format="dimension" />
</declare-styleable>
</resources>

View file

@ -0,0 +1,6 @@
<resources>
<color name="light_blue_50">#FFE1F5FE</color>
<color name="light_blue_200">#FF81D4FA</color>
<color name="light_blue_600">#FF039BE5</color>
<color name="light_blue_900">#FF01579B</color>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Refer to App Widget Documentation for margin information
http://developer.android.com/guide/topics/appwidgets/index.html#CreatingLayout
-->
<dimen name="widget_margin">0dp</dimen>
</resources>

View file

@ -4,4 +4,6 @@
<string name="title_activity_main">audiobookshelf</string> <string name="title_activity_main">audiobookshelf</string>
<string name="package_name">com.audiobookshelf.app</string> <string name="package_name">com.audiobookshelf.app</string>
<string name="custom_url_scheme">com.audiobookshelf.app</string> <string name="custom_url_scheme">com.audiobookshelf.app</string>
<string name="add_widget">Add widget</string>
<string name="app_widget_description">Simple widget for audiobookshelf playback</string>
</resources> </resources>

View file

@ -19,4 +19,13 @@
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar"> <style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
<item name="android:background">@drawable/screen</item> <item name="android:background">@drawable/screen</item>
</style> </style>
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
<item name="android:id">@android:id/background</item>
<item name="android:background">?android:attr/colorBackground</item>
</style>
<style name="Widget.Android.AppWidget.InnerView" parent="android:Widget">
<item name="android:background">?android:attr/colorBackground</item>
<item name="android:textColor">?android:attr/textColorPrimary</item>
</style>
</resources> </resources>

View file

@ -0,0 +1,17 @@
<resources>
<style name="AppTheme.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault">
<!-- Radius of the outer bound of widgets to make the rounded corners -->
<item name="appWidgetRadius">16dp</item>
<!--
Radius of the inner view's bound of widgets to make the rounded corners.
It needs to be 8dp or less than the value of appWidgetRadius
-->
<item name="appWidgetInnerRadius">8dp</item>
</style>
<style name="AppTheme.AppWidgetContainer"
parent="AppTheme.AppWidgetContainerParent">
<!-- Apply padding to avoid the content of the widget colliding with the rounded corners -->
<item name="appWidgetPadding">16dp</item>
</style>
</resources>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="110dp"
android:minHeight="40dp"
android:updatePeriodMillis="86400000"
android:previewImage="@drawable/example_appwidget_preview"
android:initialLayout="@layout/new_app_widget"
android:description="@string/app_widget_description"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen"
android:initialKeyguardLayout="@layout/new_app_widget"
/>

View file

@ -63,6 +63,12 @@ body {
box-shadow: 2px 10px 8px #1111117e; box-shadow: 2px 10px 8px #1111117e;
} }
.altBookshelfDivider {
background: rgb(38 38 38);
/*background: linear-gradient(180deg, rgba(191, 193, 195, 1) 0%, rgb(156, 158, 159) 17%, rgb(114, 115, 117) 88%, rgb(120, 120, 122) 100%);*/
box-shadow: 2px 10px 8px #1111117e;
}
/* /*
Bookshelf Label Bookshelf Label
*/ */
@ -78,6 +84,14 @@ Bookshelf Label
color: #fce3a6; color: #fce3a6;
} }
.altBookshelfLabel {
background-color: #2d3436;
background-image: linear-gradient(315deg, #19191a 0%, rgb(15, 15, 15) 74%);
border-color: rgb(255, 255, 255);
border-style: solid;
color: #ffffff;
}
.cover-bg { .cover-bg {
width: calc(100% + 40px); width: calc(100% + 40px);
height: calc(100% + 40px); height: calc(100% + 40px);

View file

@ -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>

View file

@ -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,13 +133,16 @@ 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: {
@ -523,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)
}, },
@ -676,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
@ -704,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)
@ -717,6 +726,12 @@ export default {
minimizePlayerEvt() { minimizePlayerEvt() {
console.log('Minimize Player Evt') console.log('Minimize Player Evt')
this.showFullscreen = false this.showFullscreen = false
},
showProgressSyncIsFailing() {
this.syncStatus = this.$constants.SyncStatus.FAILED
},
showProgressSyncSuccess() {
this.syncStatus = this.$constants.SyncStatus.SUCCESS
} }
}, },
mounted() { mounted() {
@ -752,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)
} }
} }

View file

@ -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)

View file

@ -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>

View file

@ -1,9 +1,9 @@
<template> <template>
<div id="bookshelf" class="w-full max-w-full h-full"> <div id="bookshelf" class="w-full max-w-full h-full">
<template v-for="shelf in totalShelves"> <template v-for="shelf in totalShelves">
<div :key="shelf" class="w-full px-2 relative" :class="showBookshelfListView ? '' : 'bookshelfRow'" :id="`shelf-${shelf - 1}`" :style="{ height: shelfHeight + 'px' }"> <div :key="shelf" class="w-full px-2 relative" :class="showBookshelfListView || altViewEnabled ? '' : 'bookshelfRow'" :id="`shelf-${shelf - 1}`" :style="{ height: shelfHeight + 'px' }">
<div v-if="!showBookshelfListView" class="bookshelfDivider w-full absolute bottom-0 left-0 z-30" style="min-height: 16px" :class="`h-${shelfDividerHeightIndex}`" /> <div v-if="!showBookshelfListView && !altViewEnabled" class="w-full absolute bottom-0 left-0 z-30 bookshelfDivider" style="min-height: 16px" :class="`h-${shelfDividerHeightIndex}`" />
<div v-else class="flex border-t border-white border-opacity-10" /> <div v-else-if="showBookshelfListView" class="flex border-t border-white border-opacity-10" />
</div> </div>
</template> </template>
@ -119,12 +119,23 @@ export default {
}, },
shelfHeight() { shelfHeight() {
if (this.showBookshelfListView) return this.entityHeight + 16 if (this.showBookshelfListView) return this.entityHeight + 16
if (this.altViewEnabled) {
var extraTitleSpace = this.isBookEntity ? 80 : 40
return this.entityHeight + extraTitleSpace * this.sizeMultiplier
}
return this.entityHeight + 40 return this.entityHeight + 40
}, },
totalEntityCardWidth() { totalEntityCardWidth() {
if (this.showBookshelfListView) return this.entityWidth if (this.showBookshelfListView) return this.entityWidth
// Includes margin // Includes margin
return this.entityWidth + 24 return this.entityWidth + 24
},
altViewEnabled() {
return this.$store.getters['getAltViewEnabled']
},
sizeMultiplier() {
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
return this.entityWidth / baseSize
} }
}, },
methods: { methods: {

View file

@ -1,20 +1,24 @@
<template> <template>
<div class="w-full relative"> <div class="w-full relative">
<div class="bookshelfRow flex items-end px-3 max-w-full overflow-x-auto" :style="{ height: shelfHeight + 'px' }"> <div v-if="altViewEnabled" class="px-5 pb-3 pt-4">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ label }}</p>
</div>
<div class="flex items-end px-3 max-w-full overflow-x-auto" :class="altViewEnabled ? '' : 'bookshelfRow'" :style="{ height: shelfHeight + 'px', paddingBottom: entityPaddingBottom + 'px' }">
<template v-for="(entity, index) in entities"> <template v-for="(entity, index) in entities">
<cards-lazy-book-card v-if="type === 'book' || type === 'podcast'" :key="entity.id" :index="index" :book-mount="entity" :width="bookWidth" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" is-categorized class="mx-2 relative" /> <cards-lazy-book-card v-if="type === 'book' || type === 'podcast'" :key="entity.id" :index="index" :book-mount="entity" :width="bookWidth" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :is-alt-view-enabled="altViewEnabled" class="mx-2 relative" />
<cards-lazy-book-card v-if="type === 'episode'" :key="entity.recentEpisode.id" :index="index" :book-mount="entity" :width="bookWidth" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" is-categorized class="mx-2 relative" /> <cards-lazy-book-card v-if="type === 'episode'" :key="entity.recentEpisode.id" :index="index" :book-mount="entity" :width="bookWidth" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :is-alt-view-enabled="altViewEnabled" class="mx-2 relative" />
<cards-lazy-series-card v-else-if="type === 'series'" :key="entity.id" :index="index" :series-mount="entity" :width="bookWidth * 2" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" is-categorized class="mx-2 relative" /> <cards-lazy-series-card v-else-if="type === 'series'" :key="entity.id" :index="index" :series-mount="entity" :width="bookWidth * 2" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :is-alt-view-enabled="altViewEnabled" is-categorized class="mx-2 relative" />
<cards-author-card v-else-if="type === 'authors'" :key="entity.id" :width="bookWidth / 1.25" :height="bookWidth" :author="entity" :size-multiplier="1" class="mx-2" /> <cards-author-card v-else-if="type === 'authors'" :key="entity.id" :width="bookWidth / 1.25" :height="bookWidth" :author="entity" :size-multiplier="1" class="mx-2" />
</template> </template>
</div> </div>
<div class="absolute text-center categoryPlacard font-book transform z-30 bottom-0.5 left-4 md:left-8 w-36 rounded-md" style="height: 18px"> <div v-if="!altViewEnabled" class="absolute text-center categoryPlacard font-book transform z-30 bottom-0.5 left-4 md:left-8 w-36 rounded-md" style="height: 18px">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border"> <div class="w-full h-full flex items-center justify-center rounded-sm border shinyBlack">
<p class="transform text-xs">{{ label }}</p> <p class="transform text-xs">{{ label }}</p>
</div> </div>
</div> </div>
<div class="w-full h-5 z-40 bookshelfDivider"></div> <div v-if="!altViewEnabled" class="w-full h-5 z-40 bookshelfDivider"></div>
</div> </div>
</template> </template>
@ -32,23 +36,43 @@ export default {
return {} return {}
}, },
computed: { computed: {
entityPaddingBottom() {
if (!this.altViewEnabled) return 0
if (this.type === 'authors') return 10
else if (this.type === 'series') return 40
return 60 * this.sizeMultiplier
},
shelfHeight() { shelfHeight() {
if (this.altViewEnabled) {
var extraTitleSpace = this.type === 'authors' ? 10 : this.type === 'series' ? 50 : 60
return this.entityHeight + extraTitleSpace * this.sizeMultiplier
}
return this.entityHeight + 40 return this.entityHeight + 40
}, },
bookWidth() { bookWidth() {
var coverSize = 100 var coverSize = 100
if (this.bookCoverAspectRatio === 1) return coverSize * 1.6 if (this.isCoverSquareAspectRatio) return coverSize * 1.6
return coverSize return coverSize
}, },
bookHeight() { bookHeight() {
if (this.bookCoverAspectRatio === 1) return this.bookWidth if (this.isCoverSquareAspectRatio) return this.bookWidth
return this.bookWidth * 1.6 return this.bookWidth * 1.6
}, },
entityHeight() { entityHeight() {
return this.bookHeight return this.bookHeight
}, },
sizeMultiplier() {
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
return this.bookWidth / baseSize
},
isCoverSquareAspectRatio() {
return this.bookCoverAspectRatio === 1
},
bookCoverAspectRatio() { bookCoverAspectRatio() {
return this.$store.getters['getBookCoverAspectRatio'] return this.$store.getters['getBookCoverAspectRatio']
},
altViewEnabled() {
return this.$store.getters['getAltViewEnabled']
} }
}, },
methods: {}, methods: {},

View file

@ -6,13 +6,13 @@
</div> </div>
<!-- Alternative bookshelf title/author/sort --> <!-- Alternative bookshelf title/author/sort -->
<!-- <div v-if="isAlternativeBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }"> <div v-if="isAltViewEnabled" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }"> <p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
{{ displayTitle }} {{ displayTitle }}
</p> </p>
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p> <p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayAuthor || '&nbsp;' }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p> <p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
</div> --> </div>
<div v-if="booksInSeries" class="absolute z-20 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ booksInSeries }}</div> <div v-if="booksInSeries" class="absolute z-20 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ booksInSeries }}</div>
@ -84,7 +84,7 @@ export default {
}, },
bookCoverAspectRatio: Number, bookCoverAspectRatio: Number,
showSequence: Boolean, showSequence: Boolean,
bookshelfView: Number, isAltViewEnabled: Boolean,
bookMount: { bookMount: {
// Book can be passed as prop or set with setEntity() // Book can be passed as prop or set with setEntity()
type: Object, type: Object,
@ -239,7 +239,7 @@ export default {
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs) if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs) if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt) if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt)
if (this.orderBy === 'duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false) if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size) if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
return null return null
}, },
@ -346,6 +346,11 @@ export default {
return this.author.slice(0, 27) + '...' return this.author.slice(0, 27) + '...'
} }
return this.author return this.author
},
titleDisplayBottomOffset() {
if (!this.isAltViewEnabled) return 0
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
return 4.25 * this.sizeMultiplier
} }
}, },
methods: { methods: {

View file

@ -5,8 +5,8 @@
<covers-collection-cover ref="cover" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-collection-cover ref="cover" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div> </div>
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }"> <div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(240, width) + 'px' }">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }"> <div class="w-full h-full flex items-center justify-center rounded-sm border" :class="isAltViewEnabled ? 'altBookshelfLabel' : 'shinyBlack'" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p> <p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div> </div>
</div> </div>
@ -19,7 +19,8 @@ export default {
index: Number, index: Number,
width: Number, width: Number,
height: Number, height: Number,
bookCoverAspectRatio: Number bookCoverAspectRatio: Number,
isAltViewEnabled: Boolean
}, },
data() { data() {
return { return {

View file

@ -16,7 +16,7 @@
</div> </div>
<div class="flex-grow px-2"> <div class="flex-grow px-2">
<p class="whitespace-normal" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }"> <p class="whitespace-normal" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">
{{ displayTitle }}<span v-if="seriesSequence">&nbsp;#{{ seriesSequence }}</span> <span v-if="seriesSequence">#{{ seriesSequence }}&nbsp;</span>{{ displayTitle }}
</p> </p>
<p class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">by {{ displayAuthor }}</p> <p class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">by {{ displayAuthor }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p> <p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
@ -109,8 +109,8 @@ export default {
}, },
episodes() { episodes() {
if (this.isPodcast) { if (this.isPodcast) {
if (this.media.numEpisodes==1) { if (this.media.numEpisodes == 1) {
return "1 episode" return '1 episode'
} else { } else {
return this.media.numEpisodes + ' episodes' return this.media.numEpisodes + ' episodes'
} }

View file

@ -5,8 +5,11 @@
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="title" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="title" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div> </div>
<div v-if="!isCategorized" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }"> <div v-if="isAltViewEnabled && isCategorized" class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }"> <p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
<div v-if="!isCategorized" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(240, width) + 'px' }">
<div class="w-full h-full flex items-center justify-center rounded-sm border" :class="isAltViewEnabled ? 'altBookshelfLabel' : 'shinyBlack'" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p> <p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div> </div>
</div> </div>
@ -24,6 +27,7 @@ export default {
type: Object, type: Object,
default: () => null default: () => null
}, },
isAltViewEnabled: Boolean,
isCategorized: Boolean isCategorized: Boolean
}, },
data() { data() {

View file

@ -16,6 +16,9 @@
</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">
@ -149,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
} }
@ -273,7 +276,8 @@ 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)
const duplicateConfig = this.serverConnectionConfigs.find((scc) => scc.address === this.serverConfig.address && scc.username === this.serverConfig.username && this.serverConfig.id !== scc.id)
if (duplicateConfig) { if (duplicateConfig) {
this.error = 'Config already exists for this address and username' this.error = 'Config already exists for this address and username'
return return
@ -285,14 +289,16 @@ export default {
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))) {

View file

@ -0,0 +1,76 @@
<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
},
isNetworkUnmetered() {
return this.$store.state.isNetworkUnmetered
},
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.isNetworkUnmetered) return 'text-yellow-400'
else if (this.isCellular) return 'text-gray-200'
else return 'text-success'
}
},
methods: {
showAlertDialog() {
var msg = ''
var meteredString = this.isNetworkUnmetered ? 'unmetered' : 'metered'
if (!this.networkConnected) {
msg = 'No internet'
} else if (!this.socketConnected) {
msg = 'Socket not connected'
} else if (this.isCellular) {
msg = `Socket connected over ${meteredString} cellular`
} else {
msg = `Socket connected over ${meteredString} wifi`
}
Dialog.alert({
title: 'Connection Status',
message: msg
})
}
},
mounted() {},
beforeDestroy() {}
}
</script>

View file

@ -487,12 +487,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 = 11; CURRENT_PROJECT_VERSION = 12;
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.51; MARKETING_VERSION = 0.9.55;
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)";
@ -511,12 +511,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 = 11; CURRENT_PROJECT_VERSION = 12;
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.51; MARKETING_VERSION = 0.9.55;
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 = "";

View file

@ -1,5 +1,6 @@
import UIKit import UIKit
import Capacitor import Capacitor
import RealmSwift
@UIApplicationMain @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
@ -8,6 +9,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch. // Override point for customization after application launch.
let configuration = Realm.Configuration(
schemaVersion: 1,
migrationBlock: { migration, oldSchemaVersion in
if (oldSchemaVersion < 1) {
NSLog("Realm schema version was \(oldSchemaVersion)")
migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in
newObject?["enableAltView"] = false
}
}
}
)
Realm.Configuration.defaultConfiguration = configuration
return true return true
} }

View file

@ -17,8 +17,9 @@ 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(getAllLocalMediaProgress, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(updateDeviceSettings, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(updateDeviceSettings, CAPPluginReturnPromise);
) )

View file

@ -86,18 +86,23 @@ 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) {
call.resolve([ "value": [] ]) call.resolve([ "value": [] ])
} }
@objc func getAllLocalMediaProgress(_ call: CAPPluginCall) {
call.resolve([ "value": [] ])
}
@objc func updateDeviceSettings(_ call: CAPPluginCall) { @objc func updateDeviceSettings(_ call: CAPPluginCall) {
let disableAutoRewind = call.getBool("disableAutoRewind") ?? false let disableAutoRewind = call.getBool("disableAutoRewind") ?? false
let enableAltView = call.getBool("enableAltView") ?? false
let jumpBackwardsTime = call.getInt("jumpBackwardsTime") ?? 10 let jumpBackwardsTime = call.getInt("jumpBackwardsTime") ?? 10
let jumpForwardTime = call.getInt("jumpForwardTime") ?? 10 let jumpForwardTime = call.getInt("jumpForwardTime") ?? 10
let settings = DeviceSettings() let settings = DeviceSettings()
settings.disableAutoRewind = disableAutoRewind settings.disableAutoRewind = disableAutoRewind
settings.enableAltView = enableAltView
settings.jumpBackwardsTime = jumpBackwardsTime settings.jumpBackwardsTime = jumpBackwardsTime
settings.jumpForwardTime = jumpForwardTime settings.jumpForwardTime = jumpForwardTime

View file

@ -10,6 +10,7 @@ import RealmSwift
class DeviceSettings: Object { class DeviceSettings: Object {
@Persisted var disableAutoRewind: Bool @Persisted var disableAutoRewind: Bool
@Persisted var enableAltView: Bool
@Persisted var jumpBackwardsTime: Int @Persisted var jumpBackwardsTime: Int
@Persisted var jumpForwardTime: Int @Persisted var jumpForwardTime: Int
} }
@ -17,6 +18,7 @@ class DeviceSettings: Object {
func getDefaultDeviceSettings() -> DeviceSettings { func getDefaultDeviceSettings() -> DeviceSettings {
let settings = DeviceSettings() let settings = DeviceSettings()
settings.disableAutoRewind = false settings.disableAutoRewind = false
settings.enableAltView = false
settings.jumpForwardTime = 10 settings.jumpForwardTime = 10
settings.jumpBackwardsTime = 10 settings.jumpBackwardsTime = 10
return settings return settings
@ -26,6 +28,7 @@ func deviceSettingsToJSON(settings: DeviceSettings) -> Dictionary<String, Any> {
return Database.realmQueue.sync { return Database.realmQueue.sync {
return [ return [
"disableAutoRewind": settings.disableAutoRewind, "disableAutoRewind": settings.disableAutoRewind,
"enableAltView": settings.enableAltView,
"jumpBackwardsTime": settings.jumpBackwardsTime, "jumpBackwardsTime": settings.jumpBackwardsTime,
"jumpForwardTime": settings.jumpForwardTime "jumpForwardTime": settings.jumpForwardTime
] ]

View file

@ -11,6 +11,7 @@ class PlayerHandler {
private static var player: AudioPlayer? private static var player: AudioPlayer?
private static var session: PlaybackSession? private static var session: PlaybackSession?
private static var timer: Timer? private static var timer: Timer?
private static var lastSyncTime:Double = 0.0
private static var _remainingSleepTime: Int? = nil private static var _remainingSleepTime: Int? = nil
public static var remainingSleepTime: Int? { public static var remainingSleepTime: Int? {
@ -128,7 +129,7 @@ class PlayerHandler {
listeningTimePassedSinceLastSync += 1 listeningTimePassedSinceLastSync += 1
} }
if listeningTimePassedSinceLastSync > 3 { if listeningTimePassedSinceLastSync >= 5 {
syncProgress() syncProgress()
} }
@ -149,6 +150,15 @@ class PlayerHandler {
return return
} }
// Prevent multiple sync requests
let timeSinceLastSync = Date().timeIntervalSince1970 - lastSyncTime
if (lastSyncTime > 0 && timeSinceLastSync < 1) {
NSLog("syncProgress last sync time was < 1 second so not syncing")
return
}
lastSyncTime = Date().timeIntervalSince1970 // seconds
let report = PlaybackReport(currentTime: playerCurrentTime, duration: player.getDuration(), timeListened: listeningTimePassedSinceLastSync) let report = PlaybackReport(currentTime: playerCurrentTime, duration: player.getDuration(), timeListened: listeningTimePassedSinceLastSync)
session!.currentTime = playerCurrentTime session!.currentTime = playerCurrentTime

View file

@ -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: {
@ -112,7 +109,7 @@ export default {
console.log(`[default] Got server config, attempt authorize ${serverConfig.address}`) console.log(`[default] Got server config, attempt authorize ${serverConfig.address}`)
var authRes = await this.$axios.$post(`${serverConfig.address}/api/authorize`, null, { headers: { Authorization: `Bearer ${serverConfig.token}` } }).catch((error) => { var authRes = await this.$axios.$post(`${serverConfig.address}/api/authorize`, null, { headers: { Authorization: `Bearer ${serverConfig.token}` }, timeout: 3000 }).catch((error) => {
console.error('[Server] Server auth failed', error) console.error('[Server] Server auth failed', error)
var errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error' var errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
this.error = errorMsg this.error = errorMsg
@ -123,7 +120,8 @@ export default {
return return
} }
const { user, userDefaultLibraryId } = authRes const { user, userDefaultLibraryId, serverSettings } = authRes
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()
@ -154,15 +152,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 +169,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 +177,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 +193,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 +215,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 +253,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)
@ -266,6 +264,8 @@ export default {
await this.$store.dispatch('setupNetworkListener') await this.$store.dispatch('setupNetworkListener')
await this.$store.dispatch('globals/loadLocalMediaProgress')
if (this.$store.state.user.serverConnectionConfig) { if (this.$store.state.user.serverConnectionConfig) {
console.log(`[default] server connection config set - call init libraries`) console.log(`[default] server connection config set - call init libraries`)
await this.initLibraries() await this.initLibraries()
@ -276,14 +276,12 @@ export default {
console.log(`[default] finished connection attempt or already connected ${!!this.user}`) console.log(`[default] finished connection attempt or already connected ${!!this.user}`)
await this.syncLocalMediaProgress() await this.syncLocalMediaProgress()
this.$store.dispatch('globals/loadLocalMediaProgress')
this.loadSavedSettings() this.loadSavedSettings()
this.hasMounted = true this.hasMounted = true
} }
}, },
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)
} }

View file

@ -45,7 +45,8 @@ export default {
index, index,
width: this.entityWidth, width: this.entityWidth,
height: this.entityHeight, height: this.entityHeight,
bookCoverAspectRatio: this.bookCoverAspectRatio bookCoverAspectRatio: this.bookCoverAspectRatio,
isAltViewEnabled: this.altViewEnabled
} }
if (this.entityName === 'series-books') props.showSequence = true if (this.entityName === 'series-books') props.showSequence = true
if (this.entityName === 'books') { if (this.entityName === 'books') {
@ -54,7 +55,7 @@ export default {
props.sortingIgnorePrefix = !!this.sortingIgnorePrefix props.sortingIgnorePrefix = !!this.sortingIgnorePrefix
} }
var _this = this // var _this = this
var instance = new ComponentClass({ var instance = new ComponentClass({
propsData: props, propsData: props,
created() { created() {

5
package-lock.json generated
View file

@ -1,11 +1,12 @@
{ {
"name": "audiobookshelf-app", "name": "audiobookshelf-app",
"version": "0.9.51-beta", "version": "0.9.55-beta",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"version": "0.9.44-beta", "name": "audiobookshelf-app",
"version": "0.9.55-beta",
"dependencies": { "dependencies": {
"@capacitor/android": "^3.4.3", "@capacitor/android": "^3.4.3",
"@capacitor/app": "^1.1.1", "@capacitor/app": "^1.1.1",

View file

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-app", "name": "audiobookshelf-app",
"version": "0.9.51-beta", "version": "0.9.55-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",

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="w-full h-full min-h-full relative"> <div class="w-full h-full min-h-full relative">
<div v-if="!loading" class="w-full"> <div v-if="!loading" class="w-full" :class="{ 'py-6': altViewEnabled }">
<template v-for="(shelf, index) in shelves"> <template v-for="(shelf, index) in shelves">
<bookshelf-shelf :key="shelf.id" :label="shelf.label" :entities="shelf.entities" :type="shelf.type" :style="{ zIndex: shelves.length - index }" /> <bookshelf-shelf :key="shelf.id" :label="shelf.label" :entities="shelf.entities" :type="shelf.type" :style="{ zIndex: shelves.length - index }" />
</template> </template>
@ -53,6 +53,9 @@ export default {
}, },
currentLibraryId() { currentLibraryId() {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
},
altViewEnabled() {
return this.$store.getters['getAltViewEnabled']
} }
}, },
methods: { methods: {
@ -183,12 +186,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)
} }

View file

@ -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

View file

@ -17,7 +17,7 @@
<div v-else class="w-full media-item-container overflow-y-auto"> <div v-else class="w-full media-item-container overflow-y-auto">
<template v-for="mediaItem in localLibraryItems"> <template v-for="mediaItem in localLibraryItems">
<nuxt-link :to="`/localMedia/item/${mediaItem.id}`" :key="mediaItem.id" class="flex my-1"> <nuxt-link :to="`/localMedia/item/${mediaItem.id}`" :key="mediaItem.id" class="flex my-1">
<div class="w-12 h-12 bg-primary"> <div class="w-12 h-12 min-w-12 min-h-12 bg-primary">
<img v-if="mediaItem.coverPathSrc" :src="mediaItem.coverPathSrc" class="w-full h-full object-contain" /> <img v-if="mediaItem.coverPathSrc" :src="mediaItem.coverPathSrc" class="w-full h-full object-contain" />
</div> </div>
<div class="flex-grow px-2"> <div class="flex-grow px-2">

View file

@ -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: {

View file

@ -1,5 +1,11 @@
<template> <template>
<div class="w-full h-full p-8"> <div class="w-full h-full p-8">
<div class="flex items-center py-3" @click="toggleEnableAltView">
<div class="w-10 flex justify-center">
<ui-toggle-switch v-model="settings.enableAltView" @input="saveSettings" />
</div>
<p class="pl-4">Alternative Bookshelf View</p>
</div>
<div v-if="$platform !== 'ios'" class="flex items-center py-3" @click="toggleDisableAutoRewind"> <div v-if="$platform !== 'ios'" class="flex items-center py-3" @click="toggleDisableAutoRewind">
<div class="w-10 flex justify-center"> <div class="w-10 flex justify-center">
<ui-toggle-switch v-model="settings.disableAutoRewind" @input="saveSettings" /> <ui-toggle-switch v-model="settings.disableAutoRewind" @input="saveSettings" />
@ -28,6 +34,7 @@ export default {
deviceData: null, deviceData: null,
settings: { settings: {
disableAutoRewind: false, disableAutoRewind: false,
enableAltView: false,
jumpForwardTime: 10, jumpForwardTime: 10,
jumpBackwardsTime: 10 jumpBackwardsTime: 10
} }
@ -60,6 +67,10 @@ export default {
this.settings.disableAutoRewind = !this.settings.disableAutoRewind this.settings.disableAutoRewind = !this.settings.disableAutoRewind
this.saveSettings() this.saveSettings()
}, },
toggleEnableAltView() {
this.settings.enableAltView = !this.settings.enableAltView
this.saveSettings()
},
toggleJumpForward() { toggleJumpForward() {
var next = (this.currentJumpForwardTimeIndex + 1) % 3 var next = (this.currentJumpForwardTimeIndex + 1) % 3
this.settings.jumpForwardTime = this.jumpForwardItems[next].value this.settings.jumpForwardTime = this.jumpForwardItems[next].value
@ -85,6 +96,7 @@ export default {
const deviceSettings = this.deviceData.deviceSettings || {} const deviceSettings = this.deviceData.deviceSettings || {}
this.settings.disableAutoRewind = !!deviceSettings.disableAutoRewind this.settings.disableAutoRewind = !!deviceSettings.disableAutoRewind
this.settings.enableAltView = !!deviceSettings.enableAltView
this.settings.jumpForwardTime = deviceSettings.jumpForwardTime || 10 this.settings.jumpForwardTime = deviceSettings.jumpForwardTime || 10
this.settings.jumpBackwardsTime = deviceSettings.jumpBackwardsTime || 10 this.settings.jumpBackwardsTime = deviceSettings.jumpBackwardsTime || 10
} }

View file

@ -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() {

View file

@ -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,

View file

@ -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() {

View file

@ -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)
} }

View file

@ -1,4 +1,5 @@
import { Network } from '@capacitor/network' import { Network } from '@capacitor/network'
import { AbsAudioPlayer } from '@/plugins/capacitor'
export const state = () => ({ export const state = () => ({
deviceData: null, deviceData: null,
@ -12,6 +13,7 @@ export const state = () => ({
socketConnected: false, socketConnected: false,
networkConnected: false, networkConnected: false,
networkConnectionType: null, networkConnectionType: null,
isNetworkUnmetered: true,
isFirstLoad: true, isFirstLoad: true,
hasStoragePermission: false, hasStoragePermission: false,
selectedLibraryItem: null, selectedLibraryItem: null,
@ -19,7 +21,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 = {
@ -44,6 +47,10 @@ export const getters = {
getJumpBackwardsTime: state => { getJumpBackwardsTime: state => {
if (!state.deviceData || !state.deviceData.deviceSettings) return 10 if (!state.deviceData || !state.deviceData.deviceSettings) return 10
return state.deviceData.deviceSettings.jumpBackwardsTime || 10 return state.deviceData.deviceSettings.jumpBackwardsTime || 10
},
getAltViewEnabled: state => {
if (!state.deviceData || !state.deviceData.deviceSettings) return false
return state.deviceData.deviceSettings.enableAltView
} }
} }
@ -61,6 +68,12 @@ export const actions = {
console.log('Network status changed', status.connected, status.connectionType) console.log('Network status changed', status.connected, status.connectionType)
commit('setNetworkStatus', status) commit('setNetworkStatus', status)
}) })
AbsAudioPlayer.addListener('onNetworkMeteredChanged', (payload) => {
const isUnmetered = payload.value
console.log('On network metered changed', isUnmetered)
commit('setIsNetworkUnmetered', isUnmetered)
})
} }
} }
@ -113,6 +126,9 @@ export const mutations = {
state.networkConnected = val.connected state.networkConnected = val.connected
state.networkConnectionType = val.connectionType state.networkConnectionType = val.connectionType
}, },
setIsNetworkUnmetered(state, val) {
state.isNetworkUnmetered = val
},
openReader(state, libraryItem) { openReader(state, libraryItem) {
state.selectedLibraryItem = libraryItem state.selectedLibraryItem = libraryItem
state.showReader = true state.showReader = true
@ -126,5 +142,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
} }
} }

View file

@ -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
}, },

View file

@ -36,6 +36,15 @@ module.exports = {
}, },
fontSize: { fontSize: {
xxs: '0.625rem' xxs: '0.625rem'
},
maxWidth: {
'24': '6rem'
},
minWidth: {
'12': '3rem'
},
minHeight: {
'12': '3rem'
} }
} }
}, },