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,6 +21,10 @@ kotlin {
}
android {
buildFeatures {
viewBinding true
}
kotlinOptions {
freeCompilerArgs = ['-Xjvm-default=all']
}
@ -29,8 +33,8 @@ android {
applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 82
versionName "0.9.51-beta"
versionCode 86
versionName "0.9.55-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// 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'
// Simple Storage
implementation "com.anggrayudi:storage:0.13.0"
implementation "com.anggrayudi:storage:0.14.0"
// OK HTTP
implementation 'com.squareup.okhttp3:okhttp:4.9.2'

View file

@ -2,67 +2,79 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="preferExternal"
package="com.audiobookshelf.app">
package="com.audiobookshelf.app"
android:installLocation="preferExternal" >
<!-- Permissions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true">
android:usesCleartextTraffic="true" >
<!-- <receiver-->
<!-- 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-->
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
<!-- Used by Android Auto -->
<meta-data
android:name="com.google.android.gms.car.notification.SmallIcon"
android:resource="@drawable/icon" />
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc"/>
<!-- Support for Cast -->
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.audiobookshelf.app.CastOptionsProvider"/>
android:resource="@xml/automotive_app_desc" /> <!-- Support for Cast -->
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.audiobookshelf.app.CastOptionsProvider" />
<activity
android:name=".MainActivity"
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:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask">
android:label="@string/title_activity_main"
android:launchMode="singleTask"
android:theme="@style/AppTheme.NoActionBarLaunch" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- Register URL scheme -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="@string/custom_url_scheme" />
</intent-filter>
</activity>
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
@ -70,14 +82,13 @@
</receiver>
<service
android:exported="true"
android:name=".player.PlayerNotificationService"
android:enabled="true"
android:name=".player.PlayerNotificationService">
android:exported="true" >
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService"/>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
</application>
</manifest>

View file

@ -19,7 +19,6 @@ import com.audiobookshelf.app.plugins.AbsAudioPlayer
import com.audiobookshelf.app.plugins.AbsDownloader
import com.audiobookshelf.app.plugins.AbsFileSystem
import com.getcapacitor.BridgeActivity
import io.paperdb.Paper
class MainActivity : BridgeActivity() {
@ -58,10 +57,6 @@ class MainActivity : BridgeActivity() {
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)
if (permission != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
@ -97,11 +92,9 @@ class MainActivity : BridgeActivity() {
foregroundService = mLocalBinder.getService()
// Let NativeAudio know foreground service is ready and setup event listener
if (pluginCallback != null) {
pluginCallback()
}
}
}
Intent(this, PlayerNotificationService::class.java).also { intent ->
Log.d(tag, "Binding PlayerNotificationService")

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 }
}
@ -213,7 +213,11 @@ class DbManager {
val localLibraryItems = getLocalLibraryItems()
localMediaProgress.forEach {
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")
Paper.book("localMediaProgress").delete(it.id)
} else if (matchingLLI.isPodcast) {

View file

@ -18,13 +18,19 @@ data class ServerConnectionConfig(
data class DeviceSettings(
var disableAutoRewind:Boolean,
var enableAltView:Boolean,
var jumpBackwardsTime:Int,
var jumpForwardTime:Int
) {
companion object {
// Static method to get default device settings
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.JsonIgnoreProperties
import kotlin.math.roundToInt
@JsonIgnoreProperties(ignoreUnknown = true)
data class LocalMediaProgress(
@ -22,6 +23,9 @@ data class LocalMediaProgress(
var libraryItemId:String?,
var episodeId:String?
) {
@get:JsonIgnore
val progressPercent get() = if (progress.isNaN()) 0 else (progress * 100).roundToInt()
@JsonIgnore
fun updateIsFinished(finished:Boolean) {
if (isFinished != finished) { // If finished changed then set progress

View file

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

View file

@ -1,21 +1,24 @@
package com.audiobookshelf.app.media
import android.app.Activity
import android.content.Context
import android.support.v4.media.MediaBrowserCompat
import android.util.Log
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.server.ApiHandler
import com.getcapacitor.JSObject
import java.util.*
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import org.json.JSONException
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
val tag = "MediaManager"
var serverLibraryItems = listOf<LibraryItem>()
var serverLibraryItems = mutableListOf<LibraryItem>()
var selectedLibraryId = ""
var selectedLibraryItemWrapper:LibraryItemWrapper? = null
@ -26,10 +29,35 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
var serverLibraries = listOf<Library>()
var serverConfigIdUsed:String? = null
var userSettingsPlaybackRate:Float? = null
fun getIsLibrary(id:String) : Boolean {
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() {
// When opening android auto need to check if still connected to server
// and reset any server data already set
@ -39,7 +67,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
serverPodcastEpisodes = listOf()
serverLibraryCategories = listOf()
serverLibraries = listOf()
serverLibraryItems = listOf()
serverLibraryItems = mutableListOf()
selectedLibraryId = ""
}
}
@ -63,7 +91,11 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
if (libraryItemsWithAudio.isNotEmpty()) selectedLibraryId = libraryId
serverLibraryItems = libraryItemsWithAudio
libraryItemsWithAudio.forEach { libraryItem ->
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
cb(libraryItemsWithAudio)
}
}
@ -167,6 +199,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
if (result) {
hasValidConn = true
DeviceManager.serverConnectionConfig = config
Log.d(tag, "checkSetValidServerConnectionConfig: Set server connection config ${DeviceManager.serverConnectionConfigId}")
break
}
}
@ -204,6 +237,10 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
serverConfigIdUsed = DeviceManager.serverConnectionConfigId
loadLibraries { libraries ->
if (libraries.isEmpty()) {
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}")
@ -211,6 +248,17 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
// Only using book or podcast library categories for now
libraryCategories.forEach {
// Add items in continue listening to serverLibraryItems
if (it.id == "continue-listening") {
it.entities.forEach { libraryItemWrapper ->
val libraryItem = libraryItemWrapper as LibraryItem
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
}
// Log.d(tag, "Found library category ${it.label} with type ${it.type}")
if (it.type == library.mediaType) {
// Log.d(tag, "Using library category ${it.id}")
@ -221,6 +269,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
cb(cats)
}
}
}
} else { // Not connected/no internet sent downloaded cats only
cb(cats)
}
@ -266,11 +315,10 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
fun play(libraryItemWrapper:LibraryItemWrapper, episode:PodcastEpisode?, playItemRequestPayload:PlayItemRequestPayload, cb: (PlaybackSession?) -> Unit) {
if (libraryItemWrapper is LocalLibraryItem) {
val localLibraryItem = libraryItemWrapper as LocalLibraryItem
cb(localLibraryItem.getPlaybackSession(episode))
cb(libraryItemWrapper.getPlaybackSession(episode))
} else {
val libraryItem = libraryItemWrapper as LibraryItem
apiHandler.playLibraryItem(libraryItem.id,episode?.id ?: "",playItemRequestPayload) {
apiHandler.playLibraryItem(libraryItem.id,episode?.id ?: "", playItemRequestPayload) {
if (it == null) {
cb(null)
} else {

View file

@ -4,7 +4,6 @@ import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.support.v4.media.MediaMetadataCompat
import android.util.Log
import androidx.annotation.AnyRes
import com.audiobookshelf.app.R
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 java.util.*
import kotlin.concurrent.schedule
import kotlin.math.roundToInt
data class MediaProgressSyncData(
var timeListened:Long, // seconds
@ -20,6 +19,7 @@ data class MediaProgressSyncData(
class MediaProgressSyncer(val playerNotificationService:PlayerNotificationService, private val apiHandler: ApiHandler) {
private val tag = "MediaProgressSync"
private val METERED_CONNECTION_SYNC_INTERVAL = 60000
private var listeningTimerTask: TimerTask? = null
var listeningTimerRunning:Boolean = false
@ -43,88 +43,163 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
currentLocalMediaProgress = null
listeningTimerTask?.cancel()
lastSyncTime = 0L
Log.d(tag, "start: Set last sync time 0 $lastSyncTime")
failedSyncs = 0
} else {
return
}
} else if (playerNotificationService.getCurrentPlaybackSessionId() != currentSessionId) {
currentLocalMediaProgress = null
}
listeningTimerRunning = true
lastSyncTime = System.currentTimeMillis()
Log.d(tag, "start: init last sync time $lastSyncTime")
currentPlaybackSession = playerNotificationService.getCurrentPlaybackSessionCopy()
listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) {
Handler(Looper.getMainLooper()).post() {
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()
sync(currentTime)
if (currentTime > 0) {
sync(shouldSyncServer, currentTime) {
Log.d(tag, "Sync complete")
}
}
}
}
}
}
fun stop() {
fun stop(cb: () -> Unit) {
if (!listeningTimerRunning) return
listeningTimerTask?.cancel()
listeningTimerTask = null
listeningTimerRunning = false
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
val currentTime = playerNotificationService.getCurrentTimeSeconds()
sync(currentTime)
if (currentTime > 0) { // Current time should always be > 0 on stop
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) {
currentPlaybackSession?.let {
it.updatedAt = mediaProgress.lastUpdate
it.currentTime = mediaProgress.currentTime
DeviceManager.dbManager.saveLocalPlaybackSession(it)
saveLocalProgress(it)
}
}
fun sync(currentTime:Double) {
val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
if (diffSinceLastSync < 1000L) {
fun sync(shouldSyncServer:Boolean, currentTime:Double, cb: () -> Unit) {
if (lastSyncTime <= 0) {
Log.e(tag, "Last sync time is not set $lastSyncTime")
return
}
val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
if (diffSinceLastSync < 1000L) {
return cb()
}
val listeningTimeToAdd = diffSinceLastSync / 1000L
lastSyncTime = System.currentTimeMillis()
val syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime)
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) {
// Save local progress sync
currentPlaybackSession?.let {
DeviceManager.dbManager.saveLocalPlaybackSession(it)
saveLocalProgress(it)
lastSyncTime = System.currentTimeMillis()
// 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
if (it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
apiHandler.sendLocalProgressSync(it) {
if (shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
apiHandler.sendLocalProgressSync(it) { syncSuccess ->
Log.d(
tag,
"Local progress sync data sent to server $currentDisplayTitle for time $currentTime"
)
}
}
}
}
} else {
apiHandler.sendProgressSync(currentSessionId, syncData) {
if (it) {
Log.d(tag, "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.d(tag, "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
Log.e(tag, "Local Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
}
cb()
}
} else {
cb()
}
}
} else if (shouldSyncServer) {
apiHandler.sendProgressSync(currentSessionId, syncData) {
if (it) {
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
failedSyncs = 0
playerNotificationService.alertSyncSuccess()
lastSyncTime = System.currentTimeMillis()
} else {
failedSyncs++
if (failedSyncs == 2) {
playerNotificationService.alertSyncFailing() // Show alert in client
failedSyncs = 0
}
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?.let {
if (it.progress.isNaN()) {
Log.e(tag, "Invalid progress on local media progress")
} 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.progress * 100).roundToInt()}%")
Log.d(tag, "Saved Local Progress Current Time: ID ${it.id} | ${it.currentTime} | Duration ${it.duration} | Progress ${it.progressPercent}%")
}
}
}
fun reset() {
listeningTimerTask?.cancel()
listeningTimerTask = null
listeningTimerRunning = false
currentPlaybackSession = null
currentLocalMediaProgress = null
lastSyncTime = 0L
Log.d(tag, "reset: Set last sync time 0 $lastSyncTime")
failedSyncs = 0
}
}

View file

@ -7,8 +7,10 @@ import android.os.Handler
import android.os.Looper
import android.os.Message
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import android.view.KeyEvent
import com.audiobookshelf.app.R
import com.audiobookshelf.app.data.LibraryItemWrapper
import com.audiobookshelf.app.data.PodcastEpisode
import java.util.*
@ -28,8 +30,9 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
if (it == null) {
Log.e(tag, "Failed to play library item")
} else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
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) {
Log.e(tag, "Failed to play library item")
} else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
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) {
Log.e(tag, "Failed to play library item")
} else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
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 {
Log.w(tag, "handleCallMediaButton $intent | ${intent.action}")
if(Intent.ACTION_MEDIA_BUTTON == intent.action) {
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) {
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) {
Log.e(tag, "Failed to play library item")
} else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
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) {
Log.e(tag, "Failed to play library item")
} else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
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) {
Log.e(tag, "Failed to play library item")
} else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
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) {
Log.d(tag, "SeekBackTime: Player is playing")
if (lastPauseTime > 0 && DeviceManager.deviceData.deviceSettings?.disableAutoRewind != true) {
var seekBackTime = 0L
if (onSeekBack) onSeekBack = false
else {
Log.d(tag, "SeekBackTime: playing started now set seek back time $lastPauseTime")
var backTime = calcPauseSeekBackTime()
if (backTime > 0) {
seekBackTime = calcPauseSeekBackTime()
if (seekBackTime > 0) {
// Current chapter is used so that seek back does not go back to the previous chapter
val currentChapter = playerNotificationService.getCurrentBookChapter()
val minSeekBackTime = currentChapter?.startMs ?: 0
val currentTime = playerNotificationService.getCurrentTime()
val newTime = currentTime - backTime
val newTime = currentTime - seekBackTime
if (newTime < minSeekBackTime) {
backTime = currentTime - minSeekBackTime
seekBackTime = currentTime - minSeekBackTime
}
Log.d(tag, "SeekBackTime $backTime")
Log.d(tag, "SeekBackTime $seekBackTime")
onSeekBack = true
playerNotificationService.seekBackward(backTime)
}
}
// Check if playback session still exists or sync media progress if updated
val pauseLength: Long = System.currentTimeMillis() - lastPauseTime
if (pauseLength > PAUSE_LEN_BEFORE_RECHECK) {
val shouldCarryOn = playerNotificationService.checkCurrentSessionProgress()
val shouldCarryOn = playerNotificationService.checkCurrentSessionProgress(seekBackTime)
if (!shouldCarryOn) return
}
if (seekBackTime > 0L) {
playerNotificationService.seekBackward(seekBackTime)
}
}
} else {
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
Log.d(tag, "Playing ${playerNotificationService.getCurrentBookTitle()}")
if (player.isPlaying) {
player.volume = 1F // Volume on sleep timer might have decreased this
playerNotificationService.mediaProgressSyncer.start()
} else {
playerNotificationService.mediaProgressSyncer.stop()
playerNotificationService.mediaProgressSyncer.pause {
Log.d(tag, "Media Progress Syncer paused and synced")
}
}
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.hardware.Sensor
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.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat
@ -15,9 +19,12 @@ import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.media.MediaBrowserServiceCompat
import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.R
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.data.DeviceInfo
import com.audiobookshelf.app.device.DeviceManager
@ -42,6 +49,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
companion object {
var isStarted = false
var isClosed = false
var isUnmeteredNetwork = false
}
interface ClientEventEmitter {
@ -55,6 +63,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
fun onPlaybackFailed(errorMessage:String)
fun onMediaPlayerChanged(mediaPlayer:String)
fun onProgressSyncFailing()
fun onProgressSyncSuccess()
fun onNetworkMeteredChanged(isUnmetered:Boolean)
}
private val tag = "PlayerService"
@ -65,7 +75,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private lateinit var ctx:Context
private lateinit var mediaSessionConnector: MediaSessionConnector
private lateinit var playerNotificationManager: PlayerNotificationManager
private lateinit var mediaSession: MediaSessionCompat
lateinit var mediaSession: MediaSessionCompat
private lateinit var transportControls:MediaControllerCompat.TransportControls
lateinit var mediaManager: MediaManager
@ -160,6 +170,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
super.onCreate()
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)
// Initialize API
@ -191,6 +210,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
.apply {
setSessionActivity(sessionActivityPendingIntent)
isActive = true
setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
}
val mediaController = MediaControllerCompat(ctx, mediaSession.sessionToken)
@ -261,6 +281,18 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
mediaSessionConnector.setQueueNavigator(queueNavigator)
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))
initializeMPlayer()
@ -300,6 +332,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
User callable methods
*/
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
val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f
initialPlaybackRate = playbackRate
@ -527,10 +566,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
// Called from PlayerListener play event
// 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
currentPlaybackSession?.let { playbackSession ->
mediaProgressSyncer.currentPlaybackSession?.let { playbackSession ->
if (!apiHandler.isOnline() || playbackSession.isLocalLibraryItemOnly) {
return true // carry on
}
@ -552,17 +591,29 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
Log.d(tag, "checkCurrentSessionProgress: Media progress was updated since last play time updating from ${playbackSession.currentTime} to ${mediaProgress.currentTime}")
mediaProgressSyncer.syncFromServerProgress(mediaProgress)
Handler(Looper.getMainLooper()).post {
seekPlayer(playbackSession.currentTimeMs)
}
// Update current playback session stored in PNS since MediaProgressSyncer version is a copy
mediaProgressSyncer.currentPlaybackSession?.let { updatedPlaybackSession ->
currentPlaybackSession = updatedPlaybackSession
}
Handler(Looper.getMainLooper()).post {
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)
}
}
}
} else {
// Streaming from server so check if playback session still exists on server
@ -584,6 +635,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} else {
Log.d(tag, "checkCurrentSessionProgress: Playback session still available on server")
Handler(Looper.getMainLooper()).post {
if (seekBackTime > 0L) {
seekBackward(seekBackTime)
}
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
mediaProgressSyncer.start()
clientEventEmitter?.onPlayingUpdate(true)
@ -648,6 +703,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
fun 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 {
currentPlayer.stop()
currentPlayer.clearMediaItems()
@ -660,6 +722,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
PlayerListener.lastPauseTime = 0
isClosed = true
stopForeground(true)
stopSelf()
}
fun sendClientMetadata(playerState: PlayerState) {
@ -694,6 +757,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
clientEventEmitter?.onProgressSyncFailing()
}
fun alertSyncSuccess() {
clientEventEmitter?.onProgressSyncSuccess()
}
//
// 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_GSEARCH_PKG_NAME = "com.google.android.googlequicksearchbox"
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 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
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.content.ContextCompat
import com.audiobookshelf.app.MainActivity
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager
@ -82,6 +80,14 @@ class AbsAudioPlayer : Plugin() {
override fun onProgressSyncFailing() {
emit("onProgressSyncFailing", "")
}
override fun onProgressSyncSuccess() {
emit("onProgressSyncSuccess", "")
}
override fun onNetworkMeteredChanged(isUnmetered:Boolean) {
emit("onNetworkMeteredChanged", isUnmetered)
}
})
}
mainActivity.pluginCallback = foregroundServiceReady
@ -150,14 +156,6 @@ class AbsAudioPlayer : Plugin() {
@PluginMethod
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 episodeId = call.getString("episodeId", "").toString()
val playWhenReady = call.getBoolean("playWhenReady") == true
@ -183,8 +181,22 @@ class AbsAudioPlayer : Plugin() {
Handler(Looper.getMainLooper()).post {
Log.d(tag, "prepareLibraryItem: Preparing Local Media item ${jacksonMapper.writeValueAsString(it)}")
val playbackSession = it.getPlaybackSession(episode)
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())
}
} else { // Play library item from server
@ -194,10 +206,21 @@ class AbsAudioPlayer : Plugin() {
if (it == null) {
call.resolve(JSObject("{\"error\":\"Server play request failed\"}"))
} else {
Handler(Looper.getMainLooper()).post {
Log.d(tag, "Preparing Player TEST ${jacksonMapper.writeValueAsString(it)}")
Log.d(tag, "Preparing Player playback session ${jacksonMapper.writeValueAsString(it)}")
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)))
}

View file

@ -77,10 +77,10 @@ class AbsDatabase : Plugin() {
}
@PluginMethod
fun getLocalLibraryItemByLLId(call:PluginCall) {
fun getLocalLibraryItemByLId(call:PluginCall) {
val libraryItemId = call.getString("libraryItemId", "").toString()
GlobalScope.launch(Dispatchers.IO) {
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLLId(libraryItemId)
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItemId)
if (localLibraryItem == null) {
call.resolve()
} else {

View file

@ -1,5 +1,6 @@
package com.audiobookshelf.app.plugins
import android.app.AlertDialog
import android.database.Cursor
import android.net.Uri
import android.os.Build
@ -65,39 +66,59 @@ class AbsFileSystem : Plugin() {
@PluginMethod
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 {
override fun onFolderSelected(requestCode: Int, folder: DocumentFile) {
Log.d(TAG, "ON FOLDER SELECTED ${folder.uri} ${folder.name}")
var absolutePath = folder.getAbsolutePath(activity)
var storageType = folder.getStorageType(activity)
var simplePath = folder.getSimplePath(activity)
var basePath = folder.getBasePath(activity)
var folderId = android.util.Base64.encodeToString(folder.id.toByteArray(), android.util.Base64.DEFAULT)
val absolutePath = folder.getAbsolutePath(activity)
val storageType = folder.getStorageType(activity)
val simplePath = folder.getSimplePath(activity)
val basePath = folder.getBasePath(activity)
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)
call.resolve(JSObject(jacksonMapper.writeValueAsString(localFolder)))
}
override fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType) {
Log.e(TAG, "STORAGE ACCESS DENIED")
var jsobj = JSObject()
val jsobj = JSObject()
if (requestCode == REQUEST_CODE_SELECT_FOLDER) {
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) {
Log.d(TAG, "STORAGE PERMISSION DENIED $requestCode")
var jsobj = JSObject()
val jsobj = JSObject()
jsobj.put("error", "Permission Denied")
call.resolve(jsobj)
}
}
mainActivity.storage.openFolderPicker(6)
mainActivity.storage.openFolderPicker(REQUEST_CODE_SELECT_FOLDER)
}
@RequiresApi(Build.VERSION_CODES.R)

View file

@ -1,7 +1,6 @@
package com.audiobookshelf.app.server
import android.content.Context
import android.content.SharedPreferences
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.util.Log
@ -17,6 +16,7 @@ import com.getcapacitor.JSObject
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
import java.util.concurrent.TimeUnit
@ -28,8 +28,6 @@ class ApiHandler(var ctx:Context) {
private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build()
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
var storageSharedPreferences: SharedPreferences? = null
data class LocalMediaProgressSyncPayload(val localMediaProgress:List<LocalMediaProgress>)
@JsonIgnoreProperties(ignoreUnknown = true)
data class MediaProgressSyncResponsePayload(val numServerProgressUpdates:Int, val localProgressUpdates:List<LocalMediaProgress>)
@ -81,6 +79,12 @@ class ApiHandler(var ctx:Context) {
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) {
val client = httpClient ?: defaultClient
client.newCall(request).enqueue(object : Callback {
@ -106,6 +110,7 @@ class ApiHandler(var ctx:Context) {
if (bodyString == "OK") {
cb(JSObject())
} else {
try {
var jsonObj = JSObject()
if (bodyString.startsWith("[")) {
val array = JSArray(bodyString)
@ -114,6 +119,12 @@ class ApiHandler(var ctx:Context) {
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)
}
}
}
}
@ -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))
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="package_name">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>

View file

@ -19,4 +19,13 @@
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
<item name="android:background">@drawable/screen</item>
</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>

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;
}
.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
*/
@ -78,6 +84,14 @@ Bookshelf Label
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 {
width: calc(100% + 40px);
height: calc(100% + 40px);

View file

@ -8,25 +8,28 @@
<span class="material-icons text-3xl text-white">arrow_back</span>
</a>
<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" />
<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>
<widgets-connection-indicator />
<div class="flex-grow" />
<widgets-download-progress-indicator />
<!-- 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">
<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>
<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>
</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>
</div>
</div>

View file

@ -34,6 +34,10 @@
<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" />
</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 class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor">
@ -129,13 +133,16 @@ export default {
onPlaybackClosedListener: null,
onPlayingUpdateListener: null,
onMetadataListener: null,
onProgressSyncFailing: null,
onProgressSyncSuccess: null,
touchStartY: 0,
touchStartTime: 0,
touchEndY: 0,
useChapterTrack: false,
isLoading: false,
touchTrackStart: false,
dragPercent: 0
dragPercent: 0,
syncStatus: 0
}
},
watch: {
@ -523,7 +530,6 @@ export default {
var data = await AbsAudioPlayer.getCurrentTime()
this.currentTime = Number(data.value.toFixed(2))
this.bufferedTime = Number(data.bufferedTime.toFixed(2))
console.log('[AudioPlayer] Got Current Time', this.currentTime)
this.timeupdate()
}, 1000)
},
@ -676,6 +682,7 @@ export default {
this.isEnded = false
this.isLoading = true
this.syncStatus = 0
this.$store.commit('setPlayerItem', this.playbackSession)
// Set track width
@ -704,6 +711,8 @@ export default {
this.onPlaybackFailedListener = AbsAudioPlayer.addListener('onPlaybackFailed', this.onPlaybackFailed)
this.onPlayingUpdateListener = AbsAudioPlayer.addListener('onPlayingUpdate', this.onPlayingUpdate)
this.onMetadataListener = AbsAudioPlayer.addListener('onMetadata', this.onMetadata)
this.onProgressSyncFailing = AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
this.onProgressSyncSuccess = AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
},
screenOrientationChange() {
setTimeout(this.updateScreenSize, 50)
@ -717,6 +726,12 @@ export default {
minimizePlayerEvt() {
console.log('Minimize Player Evt')
this.showFullscreen = false
},
showProgressSyncIsFailing() {
this.syncStatus = this.$constants.SyncStatus.FAILED
},
showProgressSyncSuccess() {
this.syncStatus = this.$constants.SyncStatus.SUCCESS
}
},
mounted() {
@ -752,6 +767,8 @@ export default {
if (this.onPlaybackSessionListener) this.onPlaybackSessionListener.remove()
if (this.onPlaybackClosedListener) this.onPlaybackClosedListener.remove()
if (this.onPlaybackFailedListener) this.onPlaybackFailedListener.remove()
if (this.onProgressSyncFailing) this.onProgressSyncFailing.remove()
if (this.onProgressSyncSuccess) this.onProgressSyncSuccess.remove()
clearInterval(this.playInterval)
}
}

View file

@ -30,11 +30,9 @@ export default {
onSleepTimerEndedListener: null,
onSleepTimerSetListener: null,
onMediaPlayerChangedListener: null,
onProgressSyncFailing: null,
sleepInterval: null,
currentEndOfChapterTime: 0,
serverLibraryItemId: null,
syncFailedToast: null
serverLibraryItemId: null
}
},
watch: {
@ -255,10 +253,6 @@ export default {
onMediaPlayerChanged(data) {
var mediaPlayer = data.value
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() {
@ -266,7 +260,6 @@ export default {
this.onSleepTimerEndedListener = AbsAudioPlayer.addListener('onSleepTimerEnded', this.onSleepTimerEnded)
this.onSleepTimerSetListener = AbsAudioPlayer.addListener('onSleepTimerSet', this.onSleepTimerSet)
this.onMediaPlayerChangedListener = AbsAudioPlayer.addListener('onMediaPlayerChanged', this.onMediaPlayerChanged)
this.onProgressSyncFailing = AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
@ -283,7 +276,6 @@ export default {
if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove()
if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove()
if (this.onMediaPlayerChangedListener) this.onMediaPlayerChangedListener.remove()
if (this.onProgressSyncFailing) this.onProgressSyncFailing.remove()
// if (this.$server.socket) {
// this.$server.socket.off('stream_open', this.streamOpen)

View file

@ -19,7 +19,7 @@
</div>
<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">
<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 class="flex items-center">
<p class="text-xs">{{ $config.version }}</p>

View file

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

View file

@ -1,20 +1,24 @@
<template>
<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">
<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 === '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-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-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-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-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" />
</template>
</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 class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border">
<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 flex items-center justify-center rounded-sm border shinyBlack">
<p class="transform text-xs">{{ label }}</p>
</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>
</template>
@ -32,23 +36,43 @@ export default {
return {}
},
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() {
if (this.altViewEnabled) {
var extraTitleSpace = this.type === 'authors' ? 10 : this.type === 'series' ? 50 : 60
return this.entityHeight + extraTitleSpace * this.sizeMultiplier
}
return this.entityHeight + 40
},
bookWidth() {
var coverSize = 100
if (this.bookCoverAspectRatio === 1) return coverSize * 1.6
if (this.isCoverSquareAspectRatio) return coverSize * 1.6
return coverSize
},
bookHeight() {
if (this.bookCoverAspectRatio === 1) return this.bookWidth
if (this.isCoverSquareAspectRatio) return this.bookWidth
return this.bookWidth * 1.6
},
entityHeight() {
return this.bookHeight
},
sizeMultiplier() {
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
return this.bookWidth / baseSize
},
isCoverSquareAspectRatio() {
return this.bookCoverAspectRatio === 1
},
bookCoverAspectRatio() {
return this.$store.getters['getBookCoverAspectRatio']
},
altViewEnabled() {
return this.$store.getters['getAltViewEnabled']
}
},
methods: {},

View file

@ -6,13 +6,13 @@
</div>
<!-- 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' }">
{{ displayTitle }}
</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>
</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>
@ -84,7 +84,7 @@ export default {
},
bookCoverAspectRatio: Number,
showSequence: Boolean,
bookshelfView: Number,
isAltViewEnabled: Boolean,
bookMount: {
// Book can be passed as prop or set with setEntity()
type: Object,
@ -239,7 +239,7 @@ export default {
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 === '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)
return null
},
@ -346,6 +346,11 @@ export default {
return this.author.slice(0, 27) + '...'
}
return this.author
},
titleDisplayBottomOffset() {
if (!this.isAltViewEnabled) return 0
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
return 4.25 * this.sizeMultiplier
}
},
methods: {

View file

@ -5,8 +5,8 @@
<covers-collection-cover ref="cover" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</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="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
<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 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>
</div>
</div>
@ -19,7 +19,8 @@ export default {
index: Number,
width: Number,
height: Number,
bookCoverAspectRatio: Number
bookCoverAspectRatio: Number,
isAltViewEnabled: Boolean
},
data() {
return {

View file

@ -16,7 +16,7 @@
</div>
<div class="flex-grow px-2">
<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 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>
@ -109,8 +109,8 @@ export default {
},
episodes() {
if (this.isPodcast) {
if (this.media.numEpisodes==1) {
return "1 episode"
if (this.media.numEpisodes == 1) {
return '1 episode'
} else {
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" />
</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 class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
<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">
<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>
</div>
</div>
@ -24,6 +27,7 @@ export default {
type: Object,
default: () => null
},
isAltViewEnabled: Boolean,
isCategorized: Boolean
},
data() {

View file

@ -16,6 +16,9 @@
</template>
<div v-else 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>
<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">
@ -149,7 +152,7 @@ export default {
var payload = await this.authenticateToken()
if (payload) {
this.setUserAndConnection(payload.user, payload.userDefaultLibraryId)
this.setUserAndConnection(payload)
} else {
this.showAuth = true
}
@ -273,7 +276,8 @@ export default {
this.error = 'Invalid username'
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) {
this.error = 'Config already exists for this address and username'
return
@ -285,14 +289,16 @@ export default {
var payload = await this.requestServerLogin()
this.processing = false
if (payload) {
this.setUserAndConnection(payload.user, payload.userDefaultLibraryId)
this.setUserAndConnection(payload)
}
},
async setUserAndConnection(user, userDefaultLibraryId) {
async setUserAndConnection({ user, userDefaultLibraryId, serverSettings }) {
if (!user) return
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
var lastLibraryId = await this.$localStore.getLastLibraryId()
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;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 11;
CURRENT_PROJECT_VERSION = 12;
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
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\"";
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -511,12 +511,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 11;
CURRENT_PROJECT_VERSION = 12;
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
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_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";

View file

@ -1,5 +1,6 @@
import UIKit
import Capacitor
import RealmSwift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
@ -8,6 +9,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 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
}

View file

@ -17,8 +17,9 @@ CAP_PLUGIN(AbsDatabase, "AbsDatabase",
CAP_PLUGIN_METHOD(getLocalLibraryItems, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getLocalLibraryItem, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getLocalLibraryItemByLLId, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getLocalLibraryItemByLId, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getLocalLibraryItemsInFolder, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getAllLocalMediaProgress, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(updateDeviceSettings, CAPPluginReturnPromise);
)

View file

@ -86,18 +86,23 @@ public class AbsDatabase: CAPPlugin {
@objc func getLocalLibraryItem(_ call: CAPPluginCall) {
call.resolve()
}
@objc func getLocalLibraryItemByLLId(_ call: CAPPluginCall) {
@objc func getLocalLibraryItemByLId(_ call: CAPPluginCall) {
call.resolve()
}
@objc func getLocalLibraryItemsInFolder(_ call: CAPPluginCall) {
call.resolve([ "value": [] ])
}
@objc func getAllLocalMediaProgress(_ call: CAPPluginCall) {
call.resolve([ "value": [] ])
}
@objc func updateDeviceSettings(_ call: CAPPluginCall) {
let disableAutoRewind = call.getBool("disableAutoRewind") ?? false
let enableAltView = call.getBool("enableAltView") ?? false
let jumpBackwardsTime = call.getInt("jumpBackwardsTime") ?? 10
let jumpForwardTime = call.getInt("jumpForwardTime") ?? 10
let settings = DeviceSettings()
settings.disableAutoRewind = disableAutoRewind
settings.enableAltView = enableAltView
settings.jumpBackwardsTime = jumpBackwardsTime
settings.jumpForwardTime = jumpForwardTime

View file

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

View file

@ -11,6 +11,7 @@ class PlayerHandler {
private static var player: AudioPlayer?
private static var session: PlaybackSession?
private static var timer: Timer?
private static var lastSyncTime:Double = 0.0
private static var _remainingSleepTime: Int? = nil
public static var remainingSleepTime: Int? {
@ -128,7 +129,7 @@ class PlayerHandler {
listeningTimePassedSinceLastSync += 1
}
if listeningTimePassedSinceLastSync > 3 {
if listeningTimePassedSinceLastSync >= 5 {
syncProgress()
}
@ -149,6 +150,15 @@ class PlayerHandler {
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)
session!.currentTime = playerCurrentTime

View file

@ -66,9 +66,6 @@ export default {
},
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
isSocketConnected() {
return this.$store.state.socketConnected
}
},
methods: {
@ -112,7 +109,7 @@ export default {
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)
var errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
this.error = errorMsg
@ -123,7 +120,8 @@ export default {
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
var lastLibraryId = await this.$localStore.getLastLibraryId()
@ -154,15 +152,9 @@ export default {
// Only cancels stream if streamining not playing downloaded
this.$eventBus.$emit('close-stream')
},
socketConnectionUpdate(isConnected) {
console.log('Socket connection update', isConnected)
},
socketConnectionFailed(err) {
this.$toast.error('Socket connection error: ' + err.message)
},
socketInit(data) {
console.log('Socket init', data)
},
async initLibraries() {
if (this.inittingLibraries) {
return
@ -177,6 +169,7 @@ export default {
async syncLocalMediaProgress() {
if (!this.user) {
console.log('[default] No need to sync local media progress - not connected to server')
this.$store.commit('setLastLocalMediaSyncResults', null)
return
}
@ -184,10 +177,15 @@ export default {
var response = await this.$db.syncLocalMediaProgressWithServer()
if (!response) {
if (this.$platform != 'web') this.$toast.error('Failed to sync local media with server')
this.$store.commit('setLastLocalMediaSyncResults', null)
return
}
const { numLocalMediaProgressForServer, numServerProgressUpdates, numLocalProgressUpdates } = response
if (numLocalMediaProgressForServer > 0) {
response.syncedAt = Date.now()
response.serverConfigName = this.$store.getters['user/getServerConfigName']
this.$store.commit('setLastLocalMediaSyncResults', response)
if (numServerProgressUpdates > 0 || numLocalProgressUpdates > 0) {
console.log(`[default] ${numServerProgressUpdates} Server progress updates | ${numLocalProgressUpdates} Local progress updates`)
} else {
@ -195,6 +193,7 @@ export default {
}
} else {
console.log('[default] syncLocalMediaProgress No local media progress to sync')
this.$store.commit('setLastLocalMediaSyncResults', null)
}
},
async userUpdated(user) {
@ -216,9 +215,10 @@ export default {
mediaProgress: prog
}
newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress(payload)
} else {
} else if (!localProg) {
// 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 (prog.episodeId) {
// If episode check if local episode exists
@ -253,8 +253,6 @@ export default {
}
},
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_media_progress_updated', this.userMediaProgressUpdated)
@ -266,6 +264,8 @@ export default {
await this.$store.dispatch('setupNetworkListener')
await this.$store.dispatch('globals/loadLocalMediaProgress')
if (this.$store.state.user.serverConnectionConfig) {
console.log(`[default] server connection config set - call init libraries`)
await this.initLibraries()
@ -276,14 +276,12 @@ export default {
console.log(`[default] finished connection attempt or already connected ${!!this.user}`)
await this.syncLocalMediaProgress()
this.$store.dispatch('globals/loadLocalMediaProgress')
this.loadSavedSettings()
this.hasMounted = true
}
},
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_media_progress_updated', this.userMediaProgressUpdated)
}

View file

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

5
package-lock.json generated
View file

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

View file

@ -1,6 +1,6 @@
{
"name": "audiobookshelf-app",
"version": "0.9.51-beta",
"version": "0.9.55-beta",
"author": "advplyr",
"scripts": {
"dev": "nuxt --hostname 0.0.0.0 --port 1337",

View file

@ -1,6 +1,6 @@
<template>
<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">
<bookshelf-shelf :key="shelf.id" :label="shelf.label" :entities="shelf.entities" :type="shelf.type" :style="{ zIndex: shelves.length - index }" />
</template>
@ -53,6 +53,9 @@ export default {
},
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
altViewEnabled() {
return this.$store.getters['getAltViewEnabled']
}
},
methods: {
@ -183,12 +186,10 @@ export default {
})
},
initListeners() {
// this.$server.on('initialized', this.socketInit)
this.$eventBus.$on('library-changed', this.libraryChanged)
// this.$eventBus.$on('downloads-loaded', this.downloadsLoaded)
},
removeListeners() {
// this.$server.off('initialized', this.socketInit)
this.$eventBus.$off('library-changed', this.libraryChanged)
// this.$eventBus.$off('downloads-loaded', this.downloadsLoaded)
}

View file

@ -136,7 +136,7 @@ export default {
})
// Check if
if (libraryItem) {
var localLibraryItem = await app.$db.getLocalLibraryItemByLLId(libraryItemId)
var localLibraryItem = await app.$db.getLocalLibraryItemByLId(libraryItemId)
if (localLibraryItem) {
console.log('Library item has local library item also', localLibraryItem.id)
libraryItem.localLibraryItem = localLibraryItem

View file

@ -17,7 +17,7 @@
<div v-else class="w-full media-item-container overflow-y-auto">
<template v-for="mediaItem in localLibraryItems">
<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" />
</div>
<div class="flex-grow px-2">

View file

@ -1,5 +1,28 @@
<template>
<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>
<div v-if="!isIos" class="w-full max-w-full px-3 py-2">
@ -49,8 +72,28 @@ export default {
isIos() {
return this.$platform === 'ios'
},
isSocketConnected() {
return this.$store.state.socketConnected
lastLocalMediaSyncResults() {
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: {

View file

@ -1,5 +1,11 @@
<template>
<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 class="w-10 flex justify-center">
<ui-toggle-switch v-model="settings.disableAutoRewind" @input="saveSettings" />
@ -28,6 +34,7 @@ export default {
deviceData: null,
settings: {
disableAutoRewind: false,
enableAltView: false,
jumpForwardTime: 10,
jumpBackwardsTime: 10
}
@ -60,6 +67,10 @@ export default {
this.settings.disableAutoRewind = !this.settings.disableAutoRewind
this.saveSettings()
},
toggleEnableAltView() {
this.settings.enableAltView = !this.settings.enableAltView
this.saveSettings()
},
toggleJumpForward() {
var next = (this.currentJumpForwardTimeIndex + 1) % 3
this.settings.jumpForwardTime = this.jumpForwardItems[next].value
@ -85,6 +96,7 @@ export default {
const deviceSettings = this.deviceData.deviceSettings || {}
this.settings.disableAutoRewind = !!deviceSettings.disableAutoRewind
this.settings.enableAltView = !!deviceSettings.enableAltView
this.settings.jumpForwardTime = deviceSettings.jumpForwardTime || 10
this.settings.jumpBackwardsTime = deviceSettings.jumpBackwardsTime || 10
}

View file

@ -164,7 +164,7 @@ class AbsDatabaseWeb extends WebPlugin {
async getLocalLibraryItem({ id }) {
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))
}
async getAllLocalMediaProgress() {

View file

@ -5,6 +5,12 @@ const DownloadStatus = {
FAILED: 3
}
const SyncStatus = {
UNSET: 0,
SUCCESS: 1,
FAILED: 2
}
const CoverDestination = {
METADATA: 0,
AUDIOBOOK: 1
@ -31,6 +37,7 @@ const PlayerState = {
const Constants = {
DownloadStatus,
SyncStatus,
CoverDestination,
BookCoverAspectRatio,
PlayMethod,

View file

@ -54,8 +54,8 @@ class DbService {
return AbsDatabase.getLocalLibraryItem({ id })
}
getLocalLibraryItemByLLId(libraryItemId) {
return AbsDatabase.getLocalLibraryItemByLLId({ libraryItemId })
getLocalLibraryItemByLId(libraryItemId) {
return AbsDatabase.getLocalLibraryItemByLId({ libraryItemId })
}
getAllLocalMediaProgress() {

View file

@ -39,6 +39,7 @@ class ServerSocket extends EventEmitter {
logout() {
if (this.socket) this.socket.disconnect()
this.removeListeners()
}
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() {
console.log('[SOCKET] Socket Connected ' + this.socket.id)
this.connected = true
@ -67,18 +76,10 @@ class ServerSocket extends EventEmitter {
this.connected = false
this.$store.commit('setSocketConnected', false)
this.emit('connection-update', false)
this.socket.removeAllListeners()
if (this.socket.io && this.socket.io.removeAllListeners) {
this.socket.io.removeAllListeners()
}
}
onInit(data) {
console.log('[SOCKET] Initial socket data received', data)
if (data.serverSettings) {
this.$store.commit('setServerSettings', data.serverSettings)
}
this.emit('initialized', true)
}

View file

@ -1,4 +1,5 @@
import { Network } from '@capacitor/network'
import { AbsAudioPlayer } from '@/plugins/capacitor'
export const state = () => ({
deviceData: null,
@ -12,6 +13,7 @@ export const state = () => ({
socketConnected: false,
networkConnected: false,
networkConnectionType: null,
isNetworkUnmetered: true,
isFirstLoad: true,
hasStoragePermission: false,
selectedLibraryItem: null,
@ -19,7 +21,8 @@ export const state = () => ({
showSideDrawer: false,
isNetworkListenerInit: false,
serverSettings: null,
lastBookshelfScrollData: {}
lastBookshelfScrollData: {},
lastLocalMediaSyncResults: null
})
export const getters = {
@ -44,6 +47,10 @@ export const getters = {
getJumpBackwardsTime: state => {
if (!state.deviceData || !state.deviceData.deviceSettings) return 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)
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.networkConnectionType = val.connectionType
},
setIsNetworkUnmetered(state, val) {
state.isNetworkUnmetered = val
},
openReader(state, libraryItem) {
state.selectedLibraryItem = libraryItem
state.showReader = true
@ -126,5 +142,8 @@ export const mutations = {
setServerSettings(state, val) {
state.serverSettings = val
this.$localStore.setServerSettings(state.serverSettings)
},
setLastLocalMediaSyncResults(state, val) {
state.lastLocalMediaSyncResults = val
}
}

View file

@ -22,6 +22,9 @@ export const getters = {
getServerAddress: (state) => {
return state.serverConnectionConfig ? state.serverConnectionConfig.address : null
},
getServerConfigName: (state) => {
return state.serverConnectionConfig ? state.serverConnectionConfig.name : null
},
getCustomHeaders: (state) => {
return state.serverConnectionConfig ? state.serverConnectionConfig.customHeaders : null
},

View file

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