Compare commits

...
Author SHA1 Message Date
advplyr 72744dcd29 iOS version bump 0.9.56 2022-08-11 19:11:25 -05:00
advplyr 6b374d923b Version bump 0.9.56 2022-08-11 19:08:18 -05:00
advplyr 03a47b06ee Fix:iOS sleep timer chapter end #291 2022-08-11 18:49:04 -05:00
advplyr 77d0974782 Update:default podcast episode table sort descending 2022-08-11 17:37:13 -05:00
advplyr 95e070a897 Add:Podcast item page play button to play next unfinished episode #139 2022-08-11 17:36:27 -05:00
advplyr a5f483f518 Fix:Home page podcast episode progress bar #264 2022-08-11 16:49:38 -05:00
advplyr b9f428fd77 Fix:Android auto separate all library items list from selected library items list 2022-08-10 18:56:12 -05:00
advplyr 94e8978ce6 Fix:Unregister android network listener on destroy 2022-08-10 18:48:47 -05:00
advplyr 62376871e3 Fix:Android get track offset out of bounds crash 2022-08-10 18:33:30 -05:00
advplyr 40b731534c Fix:Android crash when attempting sync after exoplayer failed 2022-08-10 18:27:37 -05:00
advplyr 1aea0a18a9 Fix:Android catch json parse exception from ffprobe caused by XMP id3 tag 2022-08-10 18:02:16 -05:00
advplyr b1be594932 Fix:Android catch exception when grantUriPermission fails 2022-08-10 17:31:38 -05:00
advplyr c1b8f8519f Fix:Android auto library items mixing with other libraries #309 2022-08-09 18:50:59 -05:00
advplyr 34d4a0b61b Fix:When stopping cast progress reverts back to initial progress #314 2022-08-09 18:14:07 -05:00
advplyr 7c3b6c9702 Fix:Starting cast player and stopping cast closing audio player in app #314 2022-08-09 17:35:42 -05:00
advplyr 56c73c12db Update:Android auto show progress bar 2022-08-07 17:52:37 -05:00
advplyr afc04d22bf Remove old android splash screen 2022-08-07 12:11:38 -05:00
advplyr b78f004f29 Update:Sort local books shelf show unfinished books first #302 2022-08-07 11:00:35 -05:00
advplyr 4a82ba7c86 Add:Subtitle to item page 2022-08-07 10:35:01 -05:00
advplyr 719db8d3f6 Add:Setting to disable shake to reset sleep timer #298 #169 2022-08-07 09:13:10 -05:00
advplyr db98492d4f Update:Remove limit on home page local media shelves #301 2022-08-07 08:38:30 -05:00
advplyr 5430c10424 Add:Filter by language #312 2022-08-07 08:21:17 -05:00
advplyr 92a31c246c Update:Collections page show total duration #303 2022-08-04 18:23:09 -05:00
advplyr 9082b76707 Update:Collections table row book count center #306 2022-08-04 18:16:05 -05:00
advplyrandGitHub 408faf3665 Merge pull request #308 from benonymity/stats
Add Statistics
2022-08-04 17:23:51 -05:00
advplyr be6c675ded Show user stats nav only when connected to server, remove tooltip, remove icons on stats 2022-08-04 17:22:56 -05:00
benonymity 87e277fad1 fix: lint comments 2022-08-03 22:20:20 -04:00
benonymity 014588180e feat: add stats page to mobile 2022-08-03 22:17:11 -04:00
advplyr 837df329e2 Update:List view/grid view icon swapped 2022-08-01 18:03:54 -05:00
advplyr 5662c1983a Update:Series book list view show sequence before title 2022-07-30 08:42:49 -05:00
advplyr 8cfefdcc0f Fix:Show duration in alt bookshelf view sort 2022-07-30 08:39:50 -05:00
advplyr 7d91b95320 iOS version bump 0.9.55 2022-07-28 19:45:23 -05:00
advplyr beb790ad17 iOS add getAllLocalMediaProgress capacitor func 2022-07-28 19:44:18 -05:00
advplyr 073110376a Fix:iOS sending multiple sync requests at the same time, Update:iOS sync every 5s 2022-07-28 19:30:18 -05:00
advplyr 16f6f3b7e9 iOS Realm migration to add new altViewEnabled device setting 2022-07-28 19:14:29 -05:00
advplyr 4758f3e557 Version bump 0.9.55-beta 2022-07-28 18:36:43 -05:00
advplyr 38cc2adf0f Comment out widget in dev 2022-07-28 18:29:20 -05:00
advplyr 80fbbfc2ef Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2022-07-28 18:25:47 -05:00
advplyr ca45956479 Fix:Media progress syncer to not sync media with currentTime 0 and to not sync when lastSyncTime is not set, cancel timer right away on stop/pause syncer #268 2022-07-28 18:25:36 -05:00
advplyrandGitHub 3d64c42b14 Merge pull request #299 from samblooms/master
Added alternative view setting
2022-07-27 18:24:25 -05:00
advplyr 97e7a12883 Updates to make alt bookshelf view look like the web app 2022-07-27 18:21:10 -05:00
Sam Bloomingdale e4d2b646e7 remove modified .gitignore 2022-07-27 15:01:30 -05:00
Sam Bloomingdale a5a8f3b6ac alt view series card 2022-07-27 14:54:29 -05:00
Sam Bloomingdale 41b0535213 fix typo in class name 2022-07-27 13:46:00 -05:00
Sam Bloomingdale a146a157b8 add alternative view to Shelf.vue 2022-07-27 13:37:20 -05:00
Sam Bloomingdale 96fff469fc add alternate view to LazybookShelf.vue 2022-07-27 13:29:07 -05:00
Sam Bloomingdale a60ff00f7e add getter method for 'enableAltView' 2022-07-27 13:15:03 -05:00
Sam Bloomingdale d52c326123 add 'Alternative Bookshelf View' setting 2022-07-27 12:58:06 -05:00
advplyr 76882780a4 Add:Start of app widget to control media playback 2022-07-23 18:09:10 -05:00
advplyr 97eb19da86 Fix:Load local media progress on launch before attempting server connection 2022-07-22 17:00:49 -05:00
advplyr 4ed9d702e9 Version bump 0.9.54-beta 2022-07-22 08:25:48 -05:00
advplyr 4c678836fb Update:Metered network connections to send progress syncs every 60s instead of every 5s and show metered/unmetered in connection status icon #238 2022-07-21 19:18:32 -05:00
advplyr fd134097a1 Fix:Android auto load libraries handle no libraries returned, Update:Local media items cover images width alignment #279 2022-07-21 18:06:33 -05:00
advplyr 2a87f1de28 Fix:Android check local media progress after a pause reverting to previous progress #290 2022-07-21 17:54:00 -05:00
advplyr 1f60a552e5 Version bump 0.9.53-beta 2022-07-20 18:02:07 -05:00
advplyr 3b34029ca2 Fix:M4B downloads adding MP3 extension by downloading to SimpleStorage v0.14.0 #292 2022-07-20 17:38:13 -05:00
advplyr 5f6a1ef7e9 Update:Show error icon on player cover when local media progress fails to sync & remove sync failure toast 2022-07-19 18:50:14 -05:00
advplyr b51f65d2a4 Fix:Closing playback first closes media progress syncer #268 2022-07-19 18:09:55 -05:00
advplyr dc9526beb3 Fix:Edit server config showing duplicate config already exists 2022-07-19 17:32:49 -05:00
advplyr 7188089f63 Fix:Set server settings after user logs in 2022-07-19 16:40:50 -05:00
87 changed files with 1604 additions and 534 deletions
+9 -5
View File
@@ -21,7 +21,11 @@ kotlin {
} }
android { android {
kotlinOptions {
buildFeatures {
viewBinding true
}
kotlinOptions {
freeCompilerArgs = ['-Xjvm-default=all'] freeCompilerArgs = ['-Xjvm-default=all']
} }
compileSdkVersion rootProject.ext.compileSdkVersion compileSdkVersion rootProject.ext.compileSdkVersion
@@ -29,8 +33,8 @@ android {
applicationId "com.audiobookshelf.app" applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 83 versionCode 87
versionName "0.9.52-beta" versionName "0.9.56-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -111,13 +115,13 @@ dependencies {
implementation 'io.github.pilgr:paperdb:2.7.2' implementation 'io.github.pilgr:paperdb:2.7.2'
// Simple Storage // Simple Storage
implementation "com.anggrayudi:storage:1.3.0" implementation "com.anggrayudi:storage:0.14.0"
// OK HTTP // OK HTTP
implementation 'com.squareup.okhttp3:okhttp:4.9.2' implementation 'com.squareup.okhttp3:okhttp:4.9.2'
// Jackson for JSON // Jackson for JSON
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.12.1' implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.12.2'
// FFMPEG-Kit // FFMPEG-Kit
implementation 'com.arthenica:ffmpeg-kit-min:4.5.1' implementation 'com.arthenica:ffmpeg-kit-min:4.5.1'
+63 -52
View File
@@ -1,83 +1,94 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution" xmlns:dist="http://schemas.android.com/apk/distribution"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:installLocation="preferExternal" package="com.audiobookshelf.app"
package="com.audiobookshelf.app"> android:installLocation="preferExternal" >
<!-- Permissions --> <!-- Permissions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true" >
android:networkSecurityConfig="@xml/network_security_config" <!-- <receiver-->
android:requestLegacyExternalStorage="true"> <!-- android:name=".NewAppWidget"-->
<!-- android:exported="true" >-->
<!-- <intent-filter>-->
<!-- <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />-->
<!-- </intent-filter>-->
<!-- <meta-data-->
<!-- android:name="android.appwidget.provider"-->
<!-- android:resource="@xml/new_app_widget_info" />-->
<!-- </receiver>-->
<!--Used by Android Auto--> <!-- Used by Android Auto -->
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
android:resource="@drawable/icon" />
<meta-data <meta-data
android:name="com.google.android.gms.car.application" android:name="com.google.android.gms.car.notification.SmallIcon"
android:resource="@xml/automotive_app_desc"/> android:resource="@drawable/icon" />
<meta-data
<!-- Support for Cast --> android:name="com.google.android.gms.car.application"
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:resource="@xml/automotive_app_desc" /> <!-- Support for Cast -->
android:value="com.audiobookshelf.app.CastOptionsProvider"/> <meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.audiobookshelf.app.CastOptionsProvider" />
<activity <activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:name="com.audiobookshelf.app.MainActivity"
android:label="@string/title_activity_main"
android:exported="true" android:exported="true"
android:theme="@style/AppTheme.NoActionBarLaunch" android:label="@string/title_activity_main"
android:launchMode="singleTask"> android:launchMode="singleTask"
android:theme="@style/AppTheme.NoActionBarLaunch" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<intent-filter> <category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.MAIN" /> </intent-filter>
<category android:name="android.intent.category.LAUNCHER" /> <intent-filter>
</intent-filter> <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<intent-filter> <category android:name="android.intent.category.DEFAULT" />
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" /> </intent-filter>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- Register URL scheme --> <!-- Register URL scheme -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.DEFAULT" />
<data android:scheme="@string/custom_url_scheme" /> <category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
<data android:scheme="@string/custom_url_scheme" />
</intent-filter>
</activity> </activity>
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<receiver android:name="androidx.media.session.MediaButtonReceiver" > <service
<intent-filter> android:name=".player.PlayerNotificationService"
<action android:name="android.intent.action.MEDIA_BUTTON" /> android:enabled="true"
</intent-filter> android:exported="true" >
</receiver> <intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
<service </intent-filter>
android:exported="true" </service>
android:enabled="true"
android:name=".player.PlayerNotificationService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService"/>
</intent-filter>
</service>
</application> </application>
</manifest> </manifest>
@@ -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)
}
@@ -59,8 +59,6 @@ data class LibraryItem(
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName) putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, getCoverUri().toString()) putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, getCoverUri().toString())
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getCoverUri().toString())
putString(MediaMetadataCompat.METADATA_KEY_ART_URI, getCoverUri().toString())
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName) putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName)
}.build() }.build()
} }
@@ -309,7 +307,6 @@ data class PodcastEpisode(
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title) putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, podcast.metadata.getAuthorDisplayName())
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, podcast.metadata.getAuthorDisplayName()) putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, podcast.metadata.getAuthorDisplayName())
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString()) putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString())
@@ -407,18 +404,25 @@ data class BookChapter(
} }
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class MediaProgress( class MediaProgress(
var id:String, var id:String,
var libraryItemId:String, var libraryItemId:String,
var episodeId:String?, var episodeId:String?,
var duration:Double, // seconds var duration:Double, // seconds
var progress:Double, // 0 to 1 progress:Double, // 0 to 1
var currentTime:Double, var currentTime:Double,
var isFinished:Boolean, isFinished:Boolean,
var lastUpdate:Long, var lastUpdate:Long,
var startedAt:Long, var startedAt:Long,
var finishedAt:Long? var finishedAt:Long?
) : MediaProgressWrapper(isFinished, progress)
@JsonTypeInfo(use= JsonTypeInfo.Id.DEDUCTION, defaultImpl = MediaProgress::class)
@JsonSubTypes(
JsonSubTypes.Type(MediaProgress::class),
JsonSubTypes.Type(LocalMediaProgress::class)
) )
open class MediaProgressWrapper(var isFinished:Boolean, var progress:Double)
// Helper class // Helper class
data class LibraryItemWithEpisode( data class LibraryItemWithEpisode(
@@ -18,13 +18,21 @@ data class ServerConnectionConfig(
data class DeviceSettings( data class DeviceSettings(
var disableAutoRewind:Boolean, var disableAutoRewind:Boolean,
var enableAltView:Boolean,
var jumpBackwardsTime:Int, var jumpBackwardsTime:Int,
var jumpForwardTime:Int var jumpForwardTime:Int,
var disableShakeToResetSleepTimer:Boolean
) { ) {
companion object { companion object {
// Static method to get default device settings // Static method to get default device settings
fun default():DeviceSettings { fun default():DeviceSettings {
return DeviceSettings(false, 10, 10) return DeviceSettings(
disableAutoRewind = false,
enableAltView = false,
jumpBackwardsTime = 10,
jumpForwardTime = 10,
disableShakeToResetSleepTimer = false
)
} }
} }
@@ -2,8 +2,11 @@ package com.audiobookshelf.app.data
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.MediaMetadataCompat
import android.util.Log import android.util.Log
import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.R import com.audiobookshelf.app.R
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
@@ -95,7 +98,7 @@ data class LocalLibraryItem(
} }
@JsonIgnore @JsonIgnore
fun getMediaMetadata(ctx: Context): MediaMetadataCompat { fun getMediaMetadata(): MediaMetadataCompat {
val coverUri = getCoverUri() val coverUri = getCoverUri()
return MediaMetadataCompat.Builder().apply { return MediaMetadataCompat.Builder().apply {
@@ -104,8 +107,6 @@ data class LocalLibraryItem(
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName) putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString()) putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString())
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, coverUri.toString())
putString(MediaMetadataCompat.METADATA_KEY_ART_URI, coverUri.toString())
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName) putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName)
}.build() }.build()
} }
@@ -5,14 +5,14 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import kotlin.math.roundToInt import kotlin.math.roundToInt
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class LocalMediaProgress( class LocalMediaProgress(
var id:String, var id:String,
var localLibraryItemId:String, var localLibraryItemId:String,
var localEpisodeId:String?, var localEpisodeId:String?,
var duration:Double, var duration:Double,
var progress:Double, // 0 to 1 progress:Double, // 0 to 1
var currentTime:Double, var currentTime:Double,
var isFinished:Boolean, isFinished:Boolean,
var lastUpdate:Long, var lastUpdate:Long,
var startedAt:Long, var startedAt:Long,
var finishedAt:Long?, var finishedAt:Long?,
@@ -22,7 +22,7 @@ data class LocalMediaProgress(
var serverUserId:String?, var serverUserId:String?,
var libraryItemId:String?, var libraryItemId:String?,
var episodeId:String? var episodeId:String?
) { ) : MediaProgressWrapper(isFinished, progress) {
@get:JsonIgnore @get:JsonIgnore
val progressPercent get() = if (progress.isNaN()) 0 else (progress * 100).roundToInt() val progressPercent get() = if (progress.isNaN()) 0 else (progress * 100).roundToInt()
@@ -91,6 +91,7 @@ class PlaybackSession(
@JsonIgnore @JsonIgnore
fun getTrackStartOffsetMs(index:Int):Long { fun getTrackStartOffsetMs(index:Int):Long {
if (index < 0 || index >= audioTracks.size) return 0L
val currentTrack = audioTracks[index] val currentTrack = audioTracks[index]
return (currentTrack.startOffset * 1000L).toLong() return (currentTrack.startOffset * 1000L).toLong()
} }
@@ -123,8 +124,6 @@ class PlaybackSession(
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle) .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displayAuthor) .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displayAuthor)
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, displayAuthor) .putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, displayAuthor)
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, displayAuthor)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, "series")
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
return metadataBuilder.build() return metadataBuilder.build()
} }
@@ -1,9 +1,12 @@
package com.audiobookshelf.app.device package com.audiobookshelf.app.device
import android.util.Log import android.util.Log
import com.audiobookshelf.app.data.DbManager import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.data.DeviceData import com.audiobookshelf.app.player.PlayerNotificationService
import com.audiobookshelf.app.data.ServerConnectionConfig
interface WidgetEventEmitter {
fun onPlayerChanged(pns:PlayerNotificationService)
}
object DeviceManager { object DeviceManager {
const val tag = "DeviceManager" const val tag = "DeviceManager"
@@ -19,6 +22,8 @@ object DeviceManager {
val isConnectedToServer get() = serverConnectionConfig != null val isConnectedToServer get() = serverConnectionConfig != null
val hasLastServerConnectionConfig get() = deviceData.getLastServerConnectionConfig() != null val hasLastServerConnectionConfig get() = deviceData.getLastServerConnectionConfig() != null
var widgetUpdater:WidgetEventEmitter? = null
init { init {
Log.d(tag, "Device Manager Singleton invoked") Log.d(tag, "Device Manager Singleton invoked")
} }
@@ -14,6 +14,7 @@ import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.getcapacitor.JSObject import com.getcapacitor.JSObject
import org.json.JSONException
class FolderScanner(var ctx: Context) { class FolderScanner(var ctx: Context) {
private val tag = "FolderScanner" private val tag = "FolderScanner"
@@ -465,11 +466,17 @@ class FolderScanner(var ctx: Context) {
fun probeAudioFile(absolutePath:String):AudioProbeResult? { fun probeAudioFile(absolutePath:String):AudioProbeResult? {
val session = FFprobeKit.execute("-i \"${absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet") val session = FFprobeKit.execute("-i \"${absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
Log.d(tag, "FFprobe output ${JSObject(session.output)}")
val probeObject = JSObject(session.output) var probeObject:JSObject? = null
if (!probeObject.has("streams")) { // Check if output is empty try {
Log.d(tag, "probeAudioFile Probe audio file $absolutePath is empty") probeObject = JSObject(session.output)
} catch(error:JSONException) {
Log.e(tag, "Failed to parse probe result $error")
}
Log.d(tag, "FFprobe output $probeObject")
if (probeObject == null || !probeObject.has("streams")) { // Check if output is empty
Log.d(tag, "probeAudioFile Probe audio file $absolutePath failed or invalid")
return null return null
} else { } else {
val audioProbeResult = jacksonMapper.readValue<AudioProbeResult>(session.output) val audioProbeResult = jacksonMapper.readValue<AudioProbeResult>(session.output)
@@ -2,8 +2,12 @@ package com.audiobookshelf.app.media
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.os.Bundle
import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
import android.util.Log import android.util.Log
import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.server.ApiHandler import com.audiobookshelf.app.server.ApiHandler
@@ -18,7 +22,8 @@ import kotlin.coroutines.suspendCoroutine
class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
val tag = "MediaManager" val tag = "MediaManager"
var serverLibraryItems = mutableListOf<LibraryItem>() var serverLibraryItems = mutableListOf<LibraryItem>() // Store all items here
var selectedLibraryItems = mutableListOf<LibraryItem>()
var selectedLibraryId = "" var selectedLibraryId = ""
var selectedLibraryItemWrapper:LibraryItemWrapper? = null var selectedLibraryItemWrapper:LibraryItemWrapper? = null
@@ -28,6 +33,8 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
var serverLibraryCategories = listOf<LibraryCategory>() var serverLibraryCategories = listOf<LibraryCategory>()
var serverLibraries = listOf<Library>() var serverLibraries = listOf<Library>()
var serverConfigIdUsed:String? = null var serverConfigIdUsed:String? = null
var serverConfigLastPing:Long = 0L
var serverUserMediaProgress:MutableList<MediaProgress> = mutableListOf()
var userSettingsPlaybackRate:Float? = null var userSettingsPlaybackRate:Float? = null
@@ -68,6 +75,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
serverLibraryCategories = listOf() serverLibraryCategories = listOf()
serverLibraries = listOf() serverLibraries = listOf()
serverLibraryItems = mutableListOf() serverLibraryItems = mutableListOf()
selectedLibraryItems = mutableListOf()
selectedLibraryId = "" selectedLibraryId = ""
} }
} }
@@ -84,14 +92,18 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
} }
fun loadLibraryItemsWithAudio(libraryId:String, cb: (List<LibraryItem>) -> Unit) { fun loadLibraryItemsWithAudio(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
if (serverLibraryItems.isNotEmpty() && selectedLibraryId == libraryId) { if (selectedLibraryItems.isNotEmpty() && selectedLibraryId == libraryId) {
cb(serverLibraryItems) cb(selectedLibraryItems)
} else { } else {
apiHandler.getLibraryItems(libraryId) { libraryItems -> apiHandler.getLibraryItems(libraryId) { libraryItems ->
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() } val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
if (libraryItemsWithAudio.isNotEmpty()) selectedLibraryId = libraryId if (libraryItemsWithAudio.isNotEmpty()) {
selectedLibraryId = libraryId
}
selectedLibraryItems = mutableListOf()
libraryItemsWithAudio.forEach { libraryItem -> libraryItemsWithAudio.forEach { libraryItem ->
selectedLibraryItems.add(libraryItem)
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) { if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem) serverLibraryItems.add(libraryItem)
} }
@@ -132,7 +144,11 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
val children = podcast.episodes?.map { podcastEpisode -> val children = podcast.episodes?.map { podcastEpisode ->
Log.d(tag, "Local Podcast Episode ${podcastEpisode.title} | ${podcastEpisode.id}") Log.d(tag, "Local Podcast Episode ${podcastEpisode.title} | ${podcastEpisode.id}")
MediaBrowserCompat.MediaItem(podcastEpisode.getMediaMetadata(libraryItemWrapper).description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
val mediaMetadata = podcastEpisode.getMediaMetadata(libraryItemWrapper)
val progress = DeviceManager.dbManager.getLocalMediaProgress("${libraryItemWrapper.id}-${podcastEpisode.id}")
val description = getMediaDescriptionFromMediaMetadata(mediaMetadata, progress)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
} }
children?.let { cb(children as MutableList) } ?: cb(mutableListOf()) children?.let { cb(children as MutableList) } ?: cb(mutableListOf())
} }
@@ -147,7 +163,11 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
selectedPodcast = podcast selectedPodcast = podcast
val children = podcast.episodes?.map { podcastEpisode -> val children = podcast.episodes?.map { podcastEpisode ->
MediaBrowserCompat.MediaItem(podcastEpisode.getMediaMetadata(libraryItemWrapper).description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
val mediaMetadata = podcastEpisode.getMediaMetadata(libraryItemWrapper)
val progress = serverUserMediaProgress.find { it.libraryItemId == libraryItemWrapper.id && it.episodeId == podcastEpisode.id }
val description = getMediaDescriptionFromMediaMetadata(mediaMetadata, progress)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
} }
children?.let { cb(children as MutableList) } ?: cb(mutableListOf()) children?.let { cb(children as MutableList) } ?: cb(mutableListOf())
} }
@@ -179,16 +199,49 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
return successfulPing return successfulPing
} }
fun checkSetValidServerConnectionConfig(cb: (Boolean) -> Unit) = runBlocking { suspend fun authorize(config:ServerConnectionConfig) : MutableList<MediaProgress> {
if (!apiHandler.isOnline()) cb(false) var mediaProgress:MutableList<MediaProgress> = mutableListOf()
else { suspendCoroutine<MutableList<MediaProgress>> { cont ->
coroutineScope { apiHandler.authorize(config) {
var hasValidConn = false Log.d(tag, "authorize: Authorized server config ${config.address} result = $it")
if (!it.isNullOrEmpty()) {
mediaProgress = it
}
cont.resume(mediaProgress)
}
}
return mediaProgress
}
// First check if the current selected config is pingable fun checkSetValidServerConnectionConfig(cb: (Boolean) -> Unit) = runBlocking {
DeviceManager.serverConnectionConfig?.let { Log.d(tag, "checkSetValidServerConnectionConfig | $serverConfigIdUsed")
hasValidConn = checkServerConnection(it)
Log.d(tag, "checkSetValidServerConnectionConfig: Current config ${DeviceManager.serverAddress} is pingable? $hasValidConn") coroutineScope {
if (!apiHandler.isOnline()) {
serverUserMediaProgress = mutableListOf()
cb(false)
} else {
var hasValidConn = false
var lookupMediaProgress = true
if (!serverConfigIdUsed.isNullOrEmpty() && serverConfigLastPing > 0L && System.currentTimeMillis() - serverConfigLastPing < 5000) {
Log.d(tag, "checkSetValidServerConnectionConfig last ping less than a 5 seconds ago")
hasValidConn = true
lookupMediaProgress = false
} else {
serverUserMediaProgress = mutableListOf()
}
if (!hasValidConn) {
// First check if the current selected config is pingable
DeviceManager.serverConnectionConfig?.let {
hasValidConn = checkServerConnection(it)
Log.d(
tag,
"checkSetValidServerConnectionConfig: Current config ${DeviceManager.serverAddress} is pingable? $hasValidConn"
)
}
} }
if (!hasValidConn) { if (!hasValidConn) {
@@ -199,14 +252,27 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
if (result) { if (result) {
hasValidConn = true hasValidConn = true
DeviceManager.serverConnectionConfig = config DeviceManager.serverConnectionConfig = config
Log.d(tag, "checkSetValidServerConnectionConfig: Set server connection config ${DeviceManager.serverConnectionConfigId}")
break break
} }
} }
} }
if (hasValidConn) {
serverConfigLastPing = System.currentTimeMillis()
if (lookupMediaProgress) {
Log.d(tag, "Has valid conn now get user media progress")
DeviceManager.serverConnectionConfig?.let {
serverUserMediaProgress = authorize(it)
}
}
}
cb(hasValidConn) cb(hasValidConn)
} }
} }
} }
// TODO: Load currently listening category for local items // TODO: Load currently listening category for local items
@@ -236,32 +302,38 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
serverConfigIdUsed = DeviceManager.serverConnectionConfigId serverConfigIdUsed = DeviceManager.serverConnectionConfigId
loadLibraries { libraries -> loadLibraries { libraries ->
val library = libraries[0] if (libraries.isEmpty()) {
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}") Log.w(tag, "No libraries returned from server request")
cb(cats) // Return download category only
} else {
val library = libraries[0]
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}")
loadLibraryCategories(library.id) { libraryCategories -> loadLibraryCategories(library.id) { libraryCategories ->
// Only using book or podcast library categories for now // Only using book or podcast library categories for now
libraryCategories.forEach { libraryCategories.forEach {
// Add items in continue listening to serverLibraryItems // Add items in continue listening to serverLibraryItems
if (it.id == "continue-listening") { if (it.id == "continue-listening") {
it.entities.forEach { libraryItemWrapper -> it.entities.forEach { libraryItemWrapper ->
val libraryItem = libraryItemWrapper as LibraryItem val libraryItem = libraryItemWrapper as LibraryItem
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) { if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem) serverLibraryItems.add(libraryItem)
}
} }
} }
// Log.d(tag, "Found library category ${it.label} with type ${it.type}")
if (it.type == library.mediaType) {
// Log.d(tag, "Using library category ${it.id}")
cats.add(it)
}
} }
// Log.d(tag, "Found library category ${it.label} with type ${it.type}") cb(cats)
if (it.type == library.mediaType) {
// Log.d(tag, "Using library category ${it.id}")
cats.add(it)
}
} }
cb(cats)
} }
} }
} else { // Not connected/no internet sent downloaded cats only } else { // Not connected/no internet sent downloaded cats only
@@ -322,6 +394,41 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
} }
} }
fun getMediaDescriptionFromMediaMetadata(item: MediaMetadataCompat, progress:MediaProgressWrapper?): MediaDescriptionCompat {
val extras = Bundle()
if (progress != null) {
Log.d(tag, "Has media progress for ${item.description.title} | ${progress}")
if (progress.isFinished) {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
)
} else {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
)
extras.putDouble(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress
)
}
} else {
Log.d(tag, "No media progress for ${item.description.title} | ${item.description.mediaId}")
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
)
}
return MediaDescriptionCompat.Builder()
.setMediaId(item.description.mediaId)
.setTitle(item.description.title)
.setIconUri(item.description.iconUri)
.setSubtitle(item.description.subtitle)
.setExtras(extras).build()
}
private fun levenshtein(lhs : CharSequence, rhs : CharSequence) : Int { private fun levenshtein(lhs : CharSequence, rhs : CharSequence) : Int {
val lhsLength = lhs.length + 1 val lhsLength = lhs.length + 1
val rhsLength = rhs.length + 1 val rhsLength = rhs.length + 1
@@ -6,10 +6,7 @@ import android.net.Uri
import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.MediaMetadataCompat
import androidx.annotation.AnyRes import androidx.annotation.AnyRes
import com.audiobookshelf.app.R import com.audiobookshelf.app.R
import com.audiobookshelf.app.data.Library import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.data.LibraryCategory
import com.audiobookshelf.app.data.LibraryItem
import com.audiobookshelf.app.data.LocalLibraryItem
class BrowseTree( class BrowseTree(
val context: Context, val context: Context,
@@ -85,7 +82,7 @@ class BrowseTree(
localBooksCat.entities.forEach { libc -> localBooksCat.entities.forEach { libc ->
val libraryItem = libc as LocalLibraryItem val libraryItem = libc as LocalLibraryItem
val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf() val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf()
children += libraryItem.getMediaMetadata(context) children += libraryItem.getMediaMetadata()
mediaIdToChildren[DOWNLOADS_ROOT] = children mediaIdToChildren[DOWNLOADS_ROOT] = children
} }
} }
@@ -94,7 +91,7 @@ class BrowseTree(
localPodcastsCat.entities.forEach { libc -> localPodcastsCat.entities.forEach { libc ->
val libraryItem = libc as LocalLibraryItem val libraryItem = libc as LocalLibraryItem
val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf() val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf()
children += libraryItem.getMediaMetadata(context) children += libraryItem.getMediaMetadata()
mediaIdToChildren[DOWNLOADS_ROOT] = children mediaIdToChildren[DOWNLOADS_ROOT] = children
} }
} }
@@ -19,6 +19,7 @@ data class MediaProgressSyncData(
class MediaProgressSyncer(val playerNotificationService:PlayerNotificationService, private val apiHandler: ApiHandler) { class MediaProgressSyncer(val playerNotificationService:PlayerNotificationService, private val apiHandler: ApiHandler) {
private val tag = "MediaProgressSync" private val tag = "MediaProgressSync"
private val METERED_CONNECTION_SYNC_INTERVAL = 60000
private var listeningTimerTask: TimerTask? = null private var listeningTimerTask: TimerTask? = null
var listeningTimerRunning:Boolean = false var listeningTimerRunning:Boolean = false
@@ -42,54 +43,101 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
currentLocalMediaProgress = null currentLocalMediaProgress = null
listeningTimerTask?.cancel() listeningTimerTask?.cancel()
lastSyncTime = 0L lastSyncTime = 0L
Log.d(tag, "start: Set last sync time 0 $lastSyncTime")
failedSyncs = 0 failedSyncs = 0
} else { } else {
return return
} }
} else if (playerNotificationService.getCurrentPlaybackSessionId() != currentSessionId) {
currentLocalMediaProgress = null
} }
listeningTimerRunning = true listeningTimerRunning = true
lastSyncTime = System.currentTimeMillis() lastSyncTime = System.currentTimeMillis()
Log.d(tag, "start: init last sync time $lastSyncTime")
currentPlaybackSession = playerNotificationService.getCurrentPlaybackSessionCopy() currentPlaybackSession = playerNotificationService.getCurrentPlaybackSessionCopy()
listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) { listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) {
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
if (playerNotificationService.currentPlayer.isPlaying) { if (playerNotificationService.currentPlayer.isPlaying) {
// Only sync with server on unmetered connection every 5s OR sync with server if last sync time is >= 60s
val shouldSyncServer = PlayerNotificationService.isUnmeteredNetwork || System.currentTimeMillis() - lastSyncTime >= METERED_CONNECTION_SYNC_INTERVAL
val currentTime = playerNotificationService.getCurrentTimeSeconds() val currentTime = playerNotificationService.getCurrentTimeSeconds()
sync(currentTime) { if (currentTime > 0) {
Log.d(tag, "Sync complete") sync(shouldSyncServer, currentTime) {
Log.d(tag, "Sync complete")
}
} }
} }
} }
} }
} }
fun stop() { fun stop(shouldSync:Boolean? = true, cb: () -> Unit) {
if (!listeningTimerRunning) return if (!listeningTimerRunning) return
listeningTimerTask?.cancel()
listeningTimerTask = null
listeningTimerRunning = false
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle") Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
val currentTime = playerNotificationService.getCurrentTimeSeconds() val currentTime = if (shouldSync == true) playerNotificationService.getCurrentTimeSeconds() else 0.0
sync(currentTime) { if (currentTime > 0) { // Current time should always be > 0 on stop
sync(true, currentTime) {
reset()
cb()
}
} else {
reset() reset()
cb()
} }
} }
fun pause(cb: () -> Unit) {
if (!listeningTimerRunning) return
listeningTimerTask?.cancel()
listeningTimerTask = null
listeningTimerRunning = false
Log.d(tag, "pause: Pausing progress syncer for $currentDisplayTitle")
Log.d(tag, "pause: Last sync time $lastSyncTime")
val currentTime = playerNotificationService.getCurrentTimeSeconds()
if (currentTime > 0) { // Current time should always be > 0 on pause
sync(true, currentTime) {
lastSyncTime = 0L
Log.d(tag, "pause: Set last sync time 0 $lastSyncTime")
failedSyncs = 0
cb()
}
} else {
lastSyncTime = 0L
Log.d(tag, "pause: Set last sync time 0 $lastSyncTime (current time < 0)")
failedSyncs = 0
cb()
}
}
fun syncFromServerProgress(mediaProgress: MediaProgress) { fun syncFromServerProgress(mediaProgress: MediaProgress) {
currentPlaybackSession?.let { currentPlaybackSession?.let {
it.updatedAt = mediaProgress.lastUpdate it.updatedAt = mediaProgress.lastUpdate
it.currentTime = mediaProgress.currentTime it.currentTime = mediaProgress.currentTime
DeviceManager.dbManager.saveLocalPlaybackSession(it) DeviceManager.dbManager.saveLocalPlaybackSession(it)
saveLocalProgress(it) saveLocalProgress(it)
} }
} }
fun sync(currentTime:Double, cb: () -> Unit) { 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 val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
if (diffSinceLastSync < 1000L) { if (diffSinceLastSync < 1000L) {
return cb() return cb()
} }
val listeningTimeToAdd = diffSinceLastSync / 1000L val listeningTimeToAdd = diffSinceLastSync / 1000L
lastSyncTime = System.currentTimeMillis()
val syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime) val syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime)
@@ -105,36 +153,53 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
currentPlaybackSession?.let { currentPlaybackSession?.let {
DeviceManager.dbManager.saveLocalPlaybackSession(it) DeviceManager.dbManager.saveLocalPlaybackSession(it)
saveLocalProgress(it) saveLocalProgress(it)
lastSyncTime = System.currentTimeMillis()
// Local library item is linked to a server library item // Local library item is linked to a server library item
// Send sync to server also if connected to this server and local item belongs to this server // Send sync to server also if connected to this server and local item belongs to this server
if (!it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) { if (shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
apiHandler.sendLocalProgressSync(it) { apiHandler.sendLocalProgressSync(it) { syncSuccess ->
Log.d( Log.d(
tag, tag,
"Local progress sync data sent to server $currentDisplayTitle for time $currentTime" "Local progress sync data sent to server $currentDisplayTitle for time $currentTime"
) )
if (syncSuccess) {
failedSyncs = 0
playerNotificationService.alertSyncSuccess()
} else {
failedSyncs++
if (failedSyncs == 2) {
playerNotificationService.alertSyncFailing() // Show alert in client
failedSyncs = 0
}
Log.e(tag, "Local Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
}
cb() cb()
} }
} else { } else {
cb() cb()
} }
} }
} else { } else if (shouldSyncServer) {
apiHandler.sendProgressSync(currentSessionId, syncData) { apiHandler.sendProgressSync(currentSessionId, syncData) {
if (it) { if (it) {
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime") Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
failedSyncs = 0 failedSyncs = 0
playerNotificationService.alertSyncSuccess()
lastSyncTime = System.currentTimeMillis()
} else { } else {
failedSyncs++ failedSyncs++
if (failedSyncs == 2) { if (failedSyncs == 2) {
playerNotificationService.alertSyncFailing() // Show alert in client playerNotificationService.alertSyncFailing() // Show alert in client
failedSyncs = 0 failedSyncs = 0
} }
Log.d(tag, "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime") Log.e(tag, "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
} }
cb() cb()
} }
} else {
cb()
} }
} }
@@ -162,12 +227,10 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
} }
fun reset() { fun reset() {
listeningTimerTask?.cancel()
listeningTimerTask = null
listeningTimerRunning = false
currentPlaybackSession = null currentPlaybackSession = null
currentLocalMediaProgress = null currentLocalMediaProgress = null
lastSyncTime = 0L lastSyncTime = 0L
Log.d(tag, "reset: Set last sync time 0 $lastSyncTime")
failedSyncs = 0 failedSyncs = 0
} }
} }
@@ -132,8 +132,25 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
} }
fun handleCallMediaButton(intent: Intent): Boolean { fun handleCallMediaButton(intent: Intent): Boolean {
Log.w(tag, "handleCallMediaButton $intent | ${intent.action}")
if(Intent.ACTION_MEDIA_BUTTON == intent.action) { if(Intent.ACTION_MEDIA_BUTTON == intent.action) {
val keyEvent = intent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT) val keyEvent = intent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
Log.d(tag, "handleCallMediaButton keyEvent = $keyEvent | action ${keyEvent?.action}")
if (keyEvent?.action == KeyEvent.ACTION_DOWN) {
Log.d(tag, "handleCallMediaButton: key action_down for ${keyEvent.keyCode}")
when (keyEvent.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
Log.d(tag, "handleCallMediaButton: Media Play/Pause")
if (playerNotificationService.mPlayer.isPlaying) {
playerNotificationService.pause()
} else {
playerNotificationService.play()
}
}
}
}
if (keyEvent?.action == KeyEvent.ACTION_UP) { if (keyEvent?.action == KeyEvent.ACTION_UP) {
Log.d(tag, "handleCallMediaButton: key action_up for ${keyEvent.keyCode}") Log.d(tag, "handleCallMediaButton: key action_up for ${keyEvent.keyCode}")
@@ -71,32 +71,36 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
if (player.isPlaying) { if (player.isPlaying) {
Log.d(tag, "SeekBackTime: Player is playing") Log.d(tag, "SeekBackTime: Player is playing")
if (lastPauseTime > 0 && DeviceManager.deviceData.deviceSettings?.disableAutoRewind != true) { if (lastPauseTime > 0 && DeviceManager.deviceData.deviceSettings?.disableAutoRewind != true) {
var seekBackTime = 0L
if (onSeekBack) onSeekBack = false if (onSeekBack) onSeekBack = false
else { else {
Log.d(tag, "SeekBackTime: playing started now set seek back time $lastPauseTime") Log.d(tag, "SeekBackTime: playing started now set seek back time $lastPauseTime")
var backTime = calcPauseSeekBackTime() seekBackTime = calcPauseSeekBackTime()
if (backTime > 0) { if (seekBackTime > 0) {
// Current chapter is used so that seek back does not go back to the previous chapter // Current chapter is used so that seek back does not go back to the previous chapter
val currentChapter = playerNotificationService.getCurrentBookChapter() val currentChapter = playerNotificationService.getCurrentBookChapter()
val minSeekBackTime = currentChapter?.startMs ?: 0 val minSeekBackTime = currentChapter?.startMs ?: 0
val currentTime = playerNotificationService.getCurrentTime() val currentTime = playerNotificationService.getCurrentTime()
val newTime = currentTime - backTime val newTime = currentTime - seekBackTime
if (newTime < minSeekBackTime) { if (newTime < minSeekBackTime) {
backTime = currentTime - minSeekBackTime seekBackTime = currentTime - minSeekBackTime
} }
Log.d(tag, "SeekBackTime $backTime") Log.d(tag, "SeekBackTime $seekBackTime")
onSeekBack = true onSeekBack = true
playerNotificationService.seekBackward(backTime)
} }
} }
// Check if playback session still exists or sync media progress if updated // Check if playback session still exists or sync media progress if updated
val pauseLength: Long = System.currentTimeMillis() - lastPauseTime val pauseLength: Long = System.currentTimeMillis() - lastPauseTime
if (pauseLength > PAUSE_LEN_BEFORE_RECHECK) { if (pauseLength > PAUSE_LEN_BEFORE_RECHECK) {
val shouldCarryOn = playerNotificationService.checkCurrentSessionProgress() val shouldCarryOn = playerNotificationService.checkCurrentSessionProgress(seekBackTime)
if (!shouldCarryOn) return if (!shouldCarryOn) return
} }
if (seekBackTime > 0L) {
playerNotificationService.seekBackward(seekBackTime)
}
} }
} else { } else {
Log.d(tag, "SeekBackTime: Player not playing set last pause time") Log.d(tag, "SeekBackTime: Player not playing set last pause time")
@@ -104,15 +108,18 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
} }
// Start/stop progress sync interval // Start/stop progress sync interval
Log.d(tag, "Playing ${playerNotificationService.getCurrentBookTitle()}")
if (player.isPlaying) { if (player.isPlaying) {
player.volume = 1F // Volume on sleep timer might have decreased this player.volume = 1F // Volume on sleep timer might have decreased this
playerNotificationService.mediaProgressSyncer.start() playerNotificationService.mediaProgressSyncer.start()
} else { } else {
playerNotificationService.mediaProgressSyncer.stop() playerNotificationService.mediaProgressSyncer.pause {
Log.d(tag, "Media Progress Syncer paused and synced")
}
} }
playerNotificationService.clientEventEmitter?.onPlayingUpdate(player.isPlaying) playerNotificationService.clientEventEmitter?.onPlayingUpdate(player.isPlaying)
DeviceManager.widgetUpdater?.onPlayerChanged(playerNotificationService)
} }
} }
@@ -29,9 +29,14 @@ class PlayerNotificationListener(var playerNotificationService:PlayerNotificatio
Log.d(tag, "onNotificationCancelled not dismissed by user") Log.d(tag, "onNotificationCancelled not dismissed by user")
// When stop button is pressed on the notification I guess it isn't considered "dismissedByUser" so we need to close playback ourselves // When stop button is pressed on the notification I guess it isn't considered "dismissedByUser" so we need to close playback ourselves
if (!PlayerNotificationService.isClosed) { if (!PlayerNotificationService.isClosed && !PlayerNotificationService.isSwitchingPlayer) {
Log.d(tag, "PNS is not closed - closing it now") Log.d(tag, "PNS is not closed - closing it now")
playerNotificationService.closePlayback() playerNotificationService.closePlayback()
} else if (PlayerNotificationService.isSwitchingPlayer) {
// When switching from cast player to exo player and vice versa the notification is cancelled and posted again
// so we don't want to cancel the playback during this switch
Log.d(tag, "PNS is switching player")
PlayerNotificationService.isSwitchingPlayer = false
} }
} }
} }
@@ -6,6 +6,10 @@ import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.hardware.Sensor import android.hardware.Sensor
import android.hardware.SensorManager import android.hardware.SensorManager
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.* import android.os.*
import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.MediaDescriptionCompat
@@ -45,6 +49,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
companion object { companion object {
var isStarted = false var isStarted = false
var isClosed = false var isClosed = false
var isUnmeteredNetwork = false
var isSwitchingPlayer = false // Used when switching between cast player and exoplayer
} }
interface ClientEventEmitter { interface ClientEventEmitter {
@@ -58,6 +64,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
fun onPlaybackFailed(errorMessage:String) fun onPlaybackFailed(errorMessage:String)
fun onMediaPlayerChanged(mediaPlayer:String) fun onMediaPlayerChanged(mediaPlayer:String)
fun onProgressSyncFailing() fun onProgressSyncFailing()
fun onProgressSyncSuccess()
fun onNetworkMeteredChanged(isUnmetered:Boolean)
} }
private val tag = "PlayerService" private val tag = "PlayerService"
@@ -140,6 +148,14 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
// detach player // detach player
override fun onDestroy() { override fun onDestroy() {
try {
val connectivityManager = getSystemService(ConnectivityManager::class.java) as ConnectivityManager
connectivityManager.unregisterNetworkCallback(networkCallback)
} catch(error:Exception) {
Log.e(tag, "Error unregistering network listening callback $error")
}
Log.d(tag, "onDestroy")
playerNotificationManager.setPlayer(null) playerNotificationManager.setPlayer(null)
mPlayer.release() mPlayer.release()
castPlayer?.release() castPlayer?.release()
@@ -163,6 +179,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
super.onCreate() super.onCreate()
ctx = this ctx = this
// To listen for network change from metered to unmetered
val networkRequest = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.build()
val connectivityManager = getSystemService(ConnectivityManager::class.java) as ConnectivityManager
connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
DbManager.initialize(ctx) DbManager.initialize(ctx)
// Initialize API // Initialize API
@@ -194,6 +219,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
.apply { .apply {
setSessionActivity(sessionActivityPendingIntent) setSessionActivity(sessionActivityPendingIntent)
isActive = true isActive = true
setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
} }
val mediaController = MediaControllerCompat(ctx, mediaSession.sessionToken) val mediaController = MediaControllerCompat(ctx, mediaSession.sessionToken)
@@ -238,11 +264,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
// Fix for local images crashing on Android 11 for specific devices // Fix for local images crashing on Android 11 for specific devices
// https://stackoverflow.com/questions/64186578/android-11-mediastyle-notification-crash/64232958#64232958 // https://stackoverflow.com/questions/64186578/android-11-mediastyle-notification-crash/64232958#64232958
ctx.grantUriPermission( try {
"com.android.systemui", ctx.grantUriPermission(
coverUri, "com.android.systemui",
Intent.FLAG_GRANT_READ_URI_PERMISSION coverUri,
) Intent.FLAG_GRANT_READ_URI_PERMISSION
)
} catch(error:Exception) {
Log.e(tag, "Grant uri permission error $error")
}
return MediaDescriptionCompat.Builder() return MediaDescriptionCompat.Builder()
.setMediaId(currentPlaybackSession!!.id) .setMediaId(currentPlaybackSession!!.id)
@@ -292,8 +322,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
val seekBackTime = DeviceManager.deviceData.deviceSettings?.jumpBackwardsTimeMs ?: 10000 val seekBackTime = DeviceManager.deviceData.deviceSettings?.jumpBackwardsTimeMs ?: 10000
val seekForwardTime = DeviceManager.deviceData.deviceSettings?.jumpForwardTimeMs ?: 10000 val seekForwardTime = DeviceManager.deviceData.deviceSettings?.jumpForwardTimeMs ?: 10000
Log.d(tag, "Seek Back Time $seekBackTime")
Log.d(tag, "Seek Forward Time $seekForwardTime")
mPlayer = ExoPlayer.Builder(this) mPlayer = ExoPlayer.Builder(this)
.setLoadControl(customLoadControl) .setLoadControl(customLoadControl)
@@ -396,7 +424,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
currentPlayer.setPlaybackSpeed(playbackRateToUse) currentPlayer.setPlaybackSpeed(playbackRateToUse)
currentPlayer.prepare() currentPlayer.prepare()
} else if (castPlayer != null) { } else if (castPlayer != null) {
val currentTrackIndex = playbackSession.getCurrentTrackIndex() val currentTrackIndex = playbackSession.getCurrentTrackIndex()
val currentTrackTime = playbackSession.getCurrentTrackTimeMs() val currentTrackTime = playbackSession.getCurrentTrackTimeMs()
@@ -419,7 +446,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) { apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
if (it == null) { // Play request failed if (it == null) { // Play request failed
clientEventEmitter?.onPlaybackFailed(errorMessage) clientEventEmitter?.onPlaybackFailed(errorMessage)
closePlayback() closePlayback(true)
} else { } else {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
preparePlayer(it, true, null) preparePlayer(it, true, null)
@@ -428,7 +455,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} }
} else { } else {
clientEventEmitter?.onPlaybackFailed(errorMessage) clientEventEmitter?.onPlaybackFailed(errorMessage)
closePlayback() closePlayback(true)
} }
} }
} }
@@ -472,6 +499,18 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} }
} }
if (currentPlaybackSession == null) {
Log.e(tag, "switchToPlayer: No Current playback session")
} else {
isSwitchingPlayer = true
}
// Playback session in progress syncer is a copy that is up-to-date so replace current here with that
// TODO: bad design here implemented to prevent the session in MediaProgressSyncer from changing while syncing
if (mediaProgressSyncer.currentPlaybackSession != null) {
currentPlaybackSession = mediaProgressSyncer.currentPlaybackSession?.clone()
}
currentPlayer = if (useCastPlayer) { currentPlayer = if (useCastPlayer) {
Log.d(tag, "switchToPlayer: Using Cast Player " + castPlayer?.deviceInfo) Log.d(tag, "switchToPlayer: Using Cast Player " + castPlayer?.deviceInfo)
mediaSessionConnector.setPlayer(castPlayer) mediaSessionConnector.setPlayer(castPlayer)
@@ -486,15 +525,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
clientEventEmitter?.onMediaPlayerChanged(getMediaPlayer()) clientEventEmitter?.onMediaPlayerChanged(getMediaPlayer())
if (currentPlaybackSession == null) {
Log.d(tag, "switchToPlayer: No Current playback session")
}
currentPlaybackSession?.let { currentPlaybackSession?.let {
Log.d(tag, "switchToPlayer: Preparing current playback session ${it.displayTitle}") Log.d(tag, "switchToPlayer: Starting new playback session ${it.displayTitle}")
if (wasPlaying) { // media is paused when switching players if (wasPlaying) { // media is paused when switching players
clientEventEmitter?.onPlayingUpdate(false) clientEventEmitter?.onPlayingUpdate(false)
} }
// TODO: Start a new playback session here instead of using the existing
preparePlayer(it, false, null) preparePlayer(it, false, null)
} }
} }
@@ -531,10 +568,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
return currentPlaybackSession?.totalDurationMs ?: 0L return currentPlaybackSession?.totalDurationMs ?: 0L
} }
fun getCurrentBookTitle() : String? {
return currentPlaybackSession?.displayTitle
}
fun getCurrentPlaybackSessionCopy() :PlaybackSession? { fun getCurrentPlaybackSessionCopy() :PlaybackSession? {
return currentPlaybackSession?.clone() return currentPlaybackSession?.clone()
} }
@@ -549,10 +582,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
// Called from PlayerListener play event // Called from PlayerListener play event
// check with server if progress has updated since last play and sync progress update // check with server if progress has updated since last play and sync progress update
fun checkCurrentSessionProgress():Boolean { fun checkCurrentSessionProgress(seekBackTime:Long):Boolean {
if (currentPlaybackSession == null) return true if (currentPlaybackSession == null) return true
currentPlaybackSession?.let { playbackSession -> mediaProgressSyncer.currentPlaybackSession?.let { playbackSession ->
if (!apiHandler.isOnline() || playbackSession.isLocalLibraryItemOnly) { if (!apiHandler.isOnline() || playbackSession.isLocalLibraryItemOnly) {
return true // carry on return true // carry on
} }
@@ -574,16 +607,28 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
Log.d(tag, "checkCurrentSessionProgress: Media progress was updated since last play time updating from ${playbackSession.currentTime} to ${mediaProgress.currentTime}") Log.d(tag, "checkCurrentSessionProgress: Media progress was updated since last play time updating from ${playbackSession.currentTime} to ${mediaProgress.currentTime}")
mediaProgressSyncer.syncFromServerProgress(mediaProgress) mediaProgressSyncer.syncFromServerProgress(mediaProgress)
// Update current playback session stored in PNS since MediaProgressSyncer version is a copy
mediaProgressSyncer.currentPlaybackSession?.let { updatedPlaybackSession ->
currentPlaybackSession = updatedPlaybackSession
}
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
seekPlayer(playbackSession.currentTimeMs) seekPlayer(playbackSession.currentTimeMs)
// Should already be playing
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
mediaProgressSyncer.start()
clientEventEmitter?.onPlayingUpdate(true)
}
} else {
Handler(Looper.getMainLooper()).post {
if (seekBackTime > 0L) {
seekBackward(seekBackTime)
}
// Should already be playing
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
mediaProgressSyncer.start()
clientEventEmitter?.onPlayingUpdate(true)
} }
}
Handler(Looper.getMainLooper()).post {
// Should already be playing
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
mediaProgressSyncer.start()
clientEventEmitter?.onPlayingUpdate(true)
} }
} }
} else { } else {
@@ -606,6 +651,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} else { } else {
Log.d(tag, "checkCurrentSessionProgress: Playback session still available on server") Log.d(tag, "checkCurrentSessionProgress: Playback session still available on server")
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
if (seekBackTime > 0L) {
seekBackward(seekBackTime)
}
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
mediaProgressSyncer.start() mediaProgressSyncer.start()
clientEventEmitter?.onPlayingUpdate(true) clientEventEmitter?.onPlayingUpdate(true)
@@ -623,10 +672,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
return return
} }
currentPlayer.volume = 1F currentPlayer.volume = 1F
if (currentPlayer == castPlayer) {
Log.d(tag, "CAST Player set on play ${currentPlayer.isLoading} || ${currentPlayer.duration} | ${currentPlayer.currentPosition}")
}
currentPlayer.play() currentPlayer.play()
} }
@@ -668,8 +713,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
currentPlayer.setPlaybackSpeed(speed) currentPlayer.setPlaybackSpeed(speed)
} }
fun closePlayback() { fun closePlayback(calledOnError:Boolean? = false) {
Log.d(tag, "closePlayback") Log.d(tag, "closePlayback")
if (mediaProgressSyncer.listeningTimerRunning) {
Log.i(tag, "About to close playback so stopping media progress syncer first")
mediaProgressSyncer.stop(calledOnError == false) { // If closing on error then do not sync progress (causes exception)
Log.d(tag, "Media Progress syncer stopped")
}
}
try { try {
currentPlayer.stop() currentPlayer.stop()
currentPlayer.clearMediaItems() currentPlayer.clearMediaItems()
@@ -717,6 +769,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
clientEventEmitter?.onProgressSyncFailing() clientEventEmitter?.onProgressSyncFailing()
} }
fun alertSyncSuccess() {
clientEventEmitter?.onProgressSyncSuccess()
}
// //
// MEDIA BROWSER STUFF (ANDROID AUTO) // MEDIA BROWSER STUFF (ANDROID AUTO)
// //
@@ -725,7 +781,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private val ANDROID_WEARABLE_PKG_NAME = "com.google.android.wearable.app" private val ANDROID_WEARABLE_PKG_NAME = "com.google.android.wearable.app"
private val ANDROID_GSEARCH_PKG_NAME = "com.google.android.googlequicksearchbox" private val ANDROID_GSEARCH_PKG_NAME = "com.google.android.googlequicksearchbox"
private val ANDROID_AUTOMOTIVE_PKG_NAME = "com.google.android.carassistant" private val ANDROID_AUTOMOTIVE_PKG_NAME = "com.google.android.carassistant"
private val VALID_MEDIA_BROWSERS = mutableListOf<String>(ANDROID_AUTO_PKG_NAME, ANDROID_AUTO_SIMULATOR_PKG_NAME, ANDROID_WEARABLE_PKG_NAME, ANDROID_GSEARCH_PKG_NAME, ANDROID_AUTOMOTIVE_PKG_NAME) private val VALID_MEDIA_BROWSERS = mutableListOf("com.audiobookshelf.app", ANDROID_AUTO_PKG_NAME, ANDROID_AUTO_SIMULATOR_PKG_NAME, ANDROID_WEARABLE_PKG_NAME, ANDROID_GSEARCH_PKG_NAME, ANDROID_AUTOMOTIVE_PKG_NAME)
private val AUTO_MEDIA_ROOT = "/" private val AUTO_MEDIA_ROOT = "/"
private val ALL_ROOT = "__ALL__" private val ALL_ROOT = "__ALL__"
@@ -772,7 +828,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
override fun onLoadChildren(parentMediaId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) { override fun onLoadChildren(parentMediaId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
Log.d(tag, "ON LOAD CHILDREN $parentMediaId") Log.d(tag, "ON LOAD CHILDREN $parentMediaId")
var flag = if (parentMediaId == AUTO_MEDIA_ROOT || parentMediaId == LIBRARIES_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE val flag = if (parentMediaId == AUTO_MEDIA_ROOT || parentMediaId == LIBRARIES_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
result.detach() result.detach()
@@ -788,10 +844,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
val libraryItemMediaMetadata = libraryItem.getMediaMetadata() val libraryItemMediaMetadata = libraryItem.getMediaMetadata()
if (libraryItem.mediaType == "podcast") { // Podcasts are browseable if (libraryItem.mediaType == "podcast") { // Podcasts are browseable
flag = MediaBrowserCompat.MediaItem.FLAG_BROWSABLE MediaBrowserCompat.MediaItem(libraryItemMediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
} else {
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItemMediaMetadata.description.mediaId }
val description = mediaManager.getMediaDescriptionFromMediaMetadata(libraryItemMediaMetadata, progress)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
} }
MediaBrowserCompat.MediaItem(libraryItemMediaMetadata.description, flag)
} }
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?) result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} }
@@ -802,26 +860,34 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf() val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
localBooks.forEach { localLibraryItem -> localBooks.forEach { localLibraryItem ->
val mediaMetadata = localLibraryItem.getMediaMetadata(ctx) val mediaMetadata = localLibraryItem.getMediaMetadata()
localBrowseItems += MediaBrowserCompat.MediaItem(mediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) val progress = DeviceManager.dbManager.getLocalMediaProgress(mediaMetadata.description.mediaId ?: "")
val description = mediaManager.getMediaDescriptionFromMediaMetadata(mediaMetadata, progress)
localBrowseItems += MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
} }
localPodcasts.forEach { localLibraryItem -> localPodcasts.forEach { localLibraryItem ->
val mediaMetadata = localLibraryItem.getMediaMetadata(ctx) val mediaMetadata = localLibraryItem.getMediaMetadata()
localBrowseItems += MediaBrowserCompat.MediaItem(mediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) localBrowseItems += MediaBrowserCompat.MediaItem(mediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
} }
result.sendResult(localBrowseItems) result.sendResult(localBrowseItems)
} else { // Load categories } else { // Load categories
mediaManager.loadAndroidAutoItems { libraryCategories ->
mediaManager.loadAndroidAutoItems() { libraryCategories ->
browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries) browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries)
val children = browseTree[parentMediaId]?.map { item -> val children = browseTree[parentMediaId]?.map { item ->
Log.d(tag, "Loading Browser Media Item ${item.description.title} $flag") Log.d(tag, "Loading Browser Media Item ${item.description.title} $flag")
MediaBrowserCompat.MediaItem(item.description, flag) if (flag == MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) {
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == item.description.mediaId }
val description = mediaManager.getMediaDescriptionFromMediaMetadata(item, progress)
MediaBrowserCompat.MediaItem(description, flag)
} else {
MediaBrowserCompat.MediaItem(item.description, flag)
}
} }
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?) result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} }
@@ -886,5 +952,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)
}
}
} }
@@ -3,6 +3,7 @@ package com.audiobookshelf.app.player
import android.content.Context import android.content.Context
import android.os.* import android.os.*
import android.util.Log import android.util.Log
import com.audiobookshelf.app.device.DeviceManager
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -203,8 +204,13 @@ class SleepTimerManager constructor(val playerNotificationService:PlayerNotifica
} }
fun handleShake() { fun handleShake() {
Log.d(tag, "HANDLE SHAKE HERE") if (sleepTimerRunning || sleepTimerFinishedAt > 0L) {
if (sleepTimerRunning || sleepTimerFinishedAt > 0L) checkShouldExtendSleepTimer() if (DeviceManager.deviceData.deviceSettings?.disableShakeToResetSleepTimer == true) {
Log.d(tag, "Shake to reset sleep timer is disabled")
return
}
checkShouldExtendSleepTimer()
}
} }
fun increaseSleepTime(time: Long) { fun increaseSleepTime(time: Long) {
@@ -80,6 +80,14 @@ class AbsAudioPlayer : Plugin() {
override fun onProgressSyncFailing() { override fun onProgressSyncFailing() {
emit("onProgressSyncFailing", "") emit("onProgressSyncFailing", "")
} }
override fun onProgressSyncSuccess() {
emit("onProgressSyncSuccess", "")
}
override fun onNetworkMeteredChanged(isUnmetered:Boolean) {
emit("onNetworkMeteredChanged", isUnmetered)
}
}) })
} }
mainActivity.pluginCallback = foregroundServiceReady mainActivity.pluginCallback = foregroundServiceReady
@@ -173,7 +181,21 @@ class AbsAudioPlayer : Plugin() {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
Log.d(tag, "prepareLibraryItem: Preparing Local Media item ${jacksonMapper.writeValueAsString(it)}") Log.d(tag, "prepareLibraryItem: Preparing Local Media item ${jacksonMapper.writeValueAsString(it)}")
val playbackSession = it.getPlaybackSession(episode) val playbackSession = it.getPlaybackSession(episode)
playerNotificationService.preparePlayer(playbackSession, playWhenReady, playbackRate)
if (playerNotificationService.mediaProgressSyncer.listeningTimerRunning) { // If progress syncing then first stop before preparing next
playerNotificationService.mediaProgressSyncer.stop {
Log.d(tag, "Media progress syncer was already syncing - stopped")
Handler(Looper.getMainLooper()).post { // TODO: This was needed again which is probably a design a flaw
playerNotificationService.preparePlayer(
playbackSession,
playWhenReady,
playbackRate
)
}
}
} else {
playerNotificationService.preparePlayer(playbackSession, playWhenReady, playbackRate)
}
} }
return call.resolve(JSObject()) return call.resolve(JSObject())
} }
@@ -184,9 +206,20 @@ class AbsAudioPlayer : Plugin() {
if (it == null) { if (it == null) {
call.resolve(JSObject("{\"error\":\"Server play request failed\"}")) call.resolve(JSObject("{\"error\":\"Server play request failed\"}"))
} else { } else {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
Log.d(tag, "Preparing Player TEST ${jacksonMapper.writeValueAsString(it)}") Log.d(tag, "Preparing Player playback session ${jacksonMapper.writeValueAsString(it)}")
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
if (playerNotificationService.mediaProgressSyncer.listeningTimerRunning) { // If progress syncing then first stop before preparing next
playerNotificationService.mediaProgressSyncer.stop {
Log.d(tag, "Media progress syncer was already syncing - stopped")
Handler(Looper.getMainLooper()).post { // TODO: This was needed again which is probably a design a flaw
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
}
}
} else {
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
}
} }
call.resolve(JSObject(jacksonMapper.writeValueAsString(it))) call.resolve(JSObject(jacksonMapper.writeValueAsString(it)))
@@ -16,6 +16,7 @@ import com.getcapacitor.JSObject
import okhttp3.* import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
@@ -43,11 +44,13 @@ class ApiHandler(var ctx:Context) {
makeRequest(request, httpClient, cb) makeRequest(request, httpClient, cb)
} }
fun postRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) { fun postRequest(endpoint:String, payload: JSObject, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) {
val address = config?.address ?: DeviceManager.serverAddress
val token = config?.token ?: DeviceManager.token
val mediaType = "application/json; charset=utf-8".toMediaType() val mediaType = "application/json; charset=utf-8".toMediaType()
val requestBody = payload.toString().toRequestBody(mediaType) val requestBody = payload.toString().toRequestBody(mediaType)
val request = Request.Builder().post(requestBody) val request = Request.Builder().post(requestBody)
.url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}") .url("${address}$endpoint").addHeader("Authorization", "Bearer ${token}")
.build() .build()
makeRequest(request, null, cb) makeRequest(request, null, cb)
} }
@@ -79,6 +82,12 @@ class ApiHandler(var ctx:Context) {
return false return false
} }
fun isUsingCellularData(): Boolean {
val connectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
return capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true
}
fun makeRequest(request:Request, httpClient:OkHttpClient?, cb: (JSObject) -> Unit) { fun makeRequest(request:Request, httpClient:OkHttpClient?, cb: (JSObject) -> Unit) {
val client = httpClient ?: defaultClient val client = httpClient ?: defaultClient
client.newCall(request).enqueue(object : Callback { client.newCall(request).enqueue(object : Callback {
@@ -205,7 +214,7 @@ class ApiHandler(var ctx:Context) {
val payload = JSObject(jacksonMapper.writeValueAsString(playItemRequestPayload)) val payload = JSObject(jacksonMapper.writeValueAsString(playItemRequestPayload))
val endpoint = if (episodeId.isNullOrEmpty()) "/api/items/$libraryItemId/play" else "/api/items/$libraryItemId/play/$episodeId" val endpoint = if (episodeId.isNullOrEmpty()) "/api/items/$libraryItemId/play" else "/api/items/$libraryItemId/play/$episodeId"
postRequest(endpoint, payload) { postRequest(endpoint, payload, null) {
if (it.has("error")) { if (it.has("error")) {
Log.e(tag, it.getString("error") ?: "Play Library Item Failed") Log.e(tag, it.getString("error") ?: "Play Library Item Failed")
cb(null) cb(null)
@@ -221,7 +230,7 @@ class ApiHandler(var ctx:Context) {
fun sendProgressSync(sessionId:String, syncData: MediaProgressSyncData, cb: (Boolean) -> Unit) { fun sendProgressSync(sessionId:String, syncData: MediaProgressSyncData, cb: (Boolean) -> Unit) {
val payload = JSObject(jacksonMapper.writeValueAsString(syncData)) val payload = JSObject(jacksonMapper.writeValueAsString(syncData))
postRequest("/api/session/$sessionId/sync", payload) { postRequest("/api/session/$sessionId/sync", payload, null) {
if (!it.getString("error").isNullOrEmpty()) { if (!it.getString("error").isNullOrEmpty()) {
cb(false) cb(false)
} else { } else {
@@ -230,11 +239,15 @@ class ApiHandler(var ctx:Context) {
} }
} }
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: () -> Unit) { fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: (Boolean) -> Unit) {
val payload = JSObject(jacksonMapper.writeValueAsString(playbackSession)) val payload = JSObject(jacksonMapper.writeValueAsString(playbackSession))
postRequest("/api/session/local", payload) { postRequest("/api/session/local", payload, null) {
cb() if (!it.getString("error").isNullOrEmpty()) {
cb(false)
} else {
cb(true)
}
} }
} }
@@ -255,7 +268,7 @@ class ApiHandler(var ctx:Context) {
if (localMediaProgress.isNotEmpty()) { if (localMediaProgress.isNotEmpty()) {
Log.d(tag, "Sending sync local progress request with ${localMediaProgress.size} progress items") Log.d(tag, "Sending sync local progress request with ${localMediaProgress.size} progress items")
val payload = JSObject(jacksonMapper.writeValueAsString(LocalMediaProgressSyncPayload(localMediaProgress))) val payload = JSObject(jacksonMapper.writeValueAsString(LocalMediaProgressSyncPayload(localMediaProgress)))
postRequest("/api/me/sync-local-progress", payload) { postRequest("/api/me/sync-local-progress", payload, null) {
Log.d(tag, "Media Progress Sync payload $payload - response ${it}") Log.d(tag, "Media Progress Sync payload $payload - response ${it}")
if (it.toString() == "{}") { if (it.toString() == "{}") {
@@ -333,4 +346,25 @@ class ApiHandler(var ctx:Context) {
} }
} }
} }
fun authorize(config:ServerConnectionConfig, cb: (MutableList<MediaProgress>?) -> Unit) {
Log.d(tag, "authorize: Authorizing ${config.address}")
postRequest("/api/authorize", JSObject(), config) {
val error = it.getString("error")
if (!error.isNullOrEmpty()) {
Log.d(tag, "authorize: Authorize ${config.address} Failed: $error")
cb(null)
} else {
val mediaProgressList:MutableList<MediaProgress> = mutableListOf()
val user = it.getJSObject("user")
val mediaProgress = user?.getJSONArray("mediaProgress") ?: JSONArray()
for (i in 0 until mediaProgress.length()) {
val mediaProg = jacksonMapper.readValue<MediaProgress>(mediaProgress.getJSONObject(i).toString())
mediaProgressList.add(mediaProg)
}
Log.d(tag, "authorize: Authorize ${config.address} Successful")
cb(mediaProgressList)
}
}
}
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

@@ -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>
@@ -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>
@@ -1,34 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>
@@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

@@ -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>
@@ -0,0 +1,30 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<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: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>
@@ -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>
@@ -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>
@@ -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>
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
@@ -4,4 +4,6 @@
<string name="title_activity_main">audiobookshelf</string> <string name="title_activity_main">audiobookshelf</string>
<string name="package_name">com.audiobookshelf.app</string> <string name="package_name">com.audiobookshelf.app</string>
<string name="custom_url_scheme">com.audiobookshelf.app</string> <string name="custom_url_scheme">com.audiobookshelf.app</string>
<string name="add_widget">Add widget</string>
<string name="app_widget_description">Simple widget for audiobookshelf playback</string>
</resources> </resources>
+10 -1
View File
@@ -17,6 +17,15 @@
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar"> <style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
<item name="android:background">@drawable/screen</item> <!-- <item name="android:background">@drawable/screen</item>-->
</style>
<style 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> </style>
</resources> </resources>
@@ -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>
@@ -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"
/>
+14
View File
@@ -63,6 +63,12 @@ body {
box-shadow: 2px 10px 8px #1111117e; box-shadow: 2px 10px 8px #1111117e;
} }
.altBookshelfDivider {
background: rgb(38 38 38);
/*background: linear-gradient(180deg, rgba(191, 193, 195, 1) 0%, rgb(156, 158, 159) 17%, rgb(114, 115, 117) 88%, rgb(120, 120, 122) 100%);*/
box-shadow: 2px 10px 8px #1111117e;
}
/* /*
Bookshelf Label Bookshelf Label
*/ */
@@ -78,6 +84,14 @@ Bookshelf Label
color: #fce3a6; color: #fce3a6;
} }
.altBookshelfLabel {
background-color: #2d3436;
background-image: linear-gradient(315deg, #19191a 0%, rgb(15, 15, 15) 74%);
border-color: rgb(255, 255, 255);
border-style: solid;
color: #ffffff;
}
.cover-bg { .cover-bg {
width: calc(100% + 40px); width: calc(100% + 40px);
height: calc(100% + 40px); height: calc(100% + 40px);
+19 -1
View File
@@ -34,6 +34,10 @@
<div class="cover-container bookCoverWrapper bg-black bg-opacity-75 w-full h-full"> <div class="cover-container bookCoverWrapper bg-black bg-opacity-75 w-full h-full">
<covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div> </div>
<div v-if="syncStatus === $constants.SyncStatus.FAILED" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-30">
<span class="material-icons text-error text-3xl">error</span>
</div>
</div> </div>
<div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor"> <div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor">
@@ -129,13 +133,16 @@ export default {
onPlaybackClosedListener: null, onPlaybackClosedListener: null,
onPlayingUpdateListener: null, onPlayingUpdateListener: null,
onMetadataListener: null, onMetadataListener: null,
onProgressSyncFailing: null,
onProgressSyncSuccess: null,
touchStartY: 0, touchStartY: 0,
touchStartTime: 0, touchStartTime: 0,
touchEndY: 0, touchEndY: 0,
useChapterTrack: false, useChapterTrack: false,
isLoading: false, isLoading: false,
touchTrackStart: false, touchTrackStart: false,
dragPercent: 0 dragPercent: 0,
syncStatus: 0
} }
}, },
watch: { watch: {
@@ -675,6 +682,7 @@ export default {
this.isEnded = false this.isEnded = false
this.isLoading = true this.isLoading = true
this.syncStatus = 0
this.$store.commit('setPlayerItem', this.playbackSession) this.$store.commit('setPlayerItem', this.playbackSession)
// Set track width // Set track width
@@ -703,6 +711,8 @@ export default {
this.onPlaybackFailedListener = AbsAudioPlayer.addListener('onPlaybackFailed', this.onPlaybackFailed) this.onPlaybackFailedListener = AbsAudioPlayer.addListener('onPlaybackFailed', this.onPlaybackFailed)
this.onPlayingUpdateListener = AbsAudioPlayer.addListener('onPlayingUpdate', this.onPlayingUpdate) this.onPlayingUpdateListener = AbsAudioPlayer.addListener('onPlayingUpdate', this.onPlayingUpdate)
this.onMetadataListener = AbsAudioPlayer.addListener('onMetadata', this.onMetadata) this.onMetadataListener = AbsAudioPlayer.addListener('onMetadata', this.onMetadata)
this.onProgressSyncFailing = AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
this.onProgressSyncSuccess = AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
}, },
screenOrientationChange() { screenOrientationChange() {
setTimeout(this.updateScreenSize, 50) setTimeout(this.updateScreenSize, 50)
@@ -716,6 +726,12 @@ export default {
minimizePlayerEvt() { minimizePlayerEvt() {
console.log('Minimize Player Evt') console.log('Minimize Player Evt')
this.showFullscreen = false this.showFullscreen = false
},
showProgressSyncIsFailing() {
this.syncStatus = this.$constants.SyncStatus.FAILED
},
showProgressSyncSuccess() {
this.syncStatus = this.$constants.SyncStatus.SUCCESS
} }
}, },
mounted() { mounted() {
@@ -751,6 +767,8 @@ export default {
if (this.onPlaybackSessionListener) this.onPlaybackSessionListener.remove() if (this.onPlaybackSessionListener) this.onPlaybackSessionListener.remove()
if (this.onPlaybackClosedListener) this.onPlaybackClosedListener.remove() if (this.onPlaybackClosedListener) this.onPlaybackClosedListener.remove()
if (this.onPlaybackFailedListener) this.onPlaybackFailedListener.remove() if (this.onPlaybackFailedListener) this.onPlaybackFailedListener.remove()
if (this.onProgressSyncFailing) this.onProgressSyncFailing.remove()
if (this.onProgressSyncSuccess) this.onProgressSyncSuccess.remove()
clearInterval(this.playInterval) clearInterval(this.playInterval)
} }
} }
+11 -11
View File
@@ -30,11 +30,10 @@ export default {
onSleepTimerEndedListener: null, onSleepTimerEndedListener: null,
onSleepTimerSetListener: null, onSleepTimerSetListener: null,
onMediaPlayerChangedListener: null, onMediaPlayerChangedListener: null,
onProgressSyncFailing: null,
sleepInterval: null, sleepInterval: null,
currentEndOfChapterTime: 0, currentEndOfChapterTime: 0,
serverLibraryItemId: null, serverLibraryItemId: null,
syncFailedToast: null serverEpisodeId: null
} }
}, },
watch: { watch: {
@@ -175,15 +174,15 @@ export default {
this.$toast.error(`Cannot cast locally downloaded media`) this.$toast.error(`Cannot cast locally downloaded media`)
} else { } else {
// Change to server library item // Change to server library item
this.playServerLibraryItemAndCast(this.serverLibraryItemId) this.playServerLibraryItemAndCast(this.serverLibraryItemId, this.serverEpisodeId)
} }
}, },
playServerLibraryItemAndCast(libraryItemId) { playServerLibraryItemAndCast(libraryItemId, episodeId) {
var playbackRate = 1 var playbackRate = 1
if (this.$refs.audioPlayer) { if (this.$refs.audioPlayer) {
playbackRate = this.$refs.audioPlayer.currentPlaybackRate || 1 playbackRate = this.$refs.audioPlayer.currentPlaybackRate || 1
} }
AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId: null, playWhenReady: false, playbackRate }) AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId, playWhenReady: false, playbackRate })
.then((data) => { .then((data) => {
if (data.error) { if (data.error) {
const errorMsg = data.error || 'Failed to play' const errorMsg = data.error || 'Failed to play'
@@ -205,6 +204,7 @@ export default {
// When playing local library item and can also play this item from the server // When playing local library item and can also play this item from the server
// then store the server library item id so it can be used if a cast is made // then store the server library item id so it can be used if a cast is made
var serverLibraryItemId = payload.serverLibraryItemId || null var serverLibraryItemId = payload.serverLibraryItemId || null
var serverEpisodeId = payload.serverEpisodeId || null
if (libraryItemId.startsWith('local') && this.$store.state.isCasting) { if (libraryItemId.startsWith('local') && this.$store.state.isCasting) {
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
@@ -217,6 +217,7 @@ export default {
} }
this.serverLibraryItemId = null this.serverLibraryItemId = null
this.serverEpisodeId = null
var playbackRate = 1 var playbackRate = 1
if (this.$refs.audioPlayer) { if (this.$refs.audioPlayer) {
@@ -236,6 +237,11 @@ export default {
} else { } else {
this.serverLibraryItemId = serverLibraryItemId this.serverLibraryItemId = serverLibraryItemId
} }
if (episodeId && !episodeId.startsWith('local')) {
this.serverEpisodeId = episodeId
} else {
this.serverEpisodeId = serverEpisodeId
}
} }
}) })
.catch((error) => { .catch((error) => {
@@ -255,10 +261,6 @@ export default {
onMediaPlayerChanged(data) { onMediaPlayerChanged(data) {
var mediaPlayer = data.value var mediaPlayer = data.value
this.$store.commit('setMediaPlayer', mediaPlayer) this.$store.commit('setMediaPlayer', mediaPlayer)
},
showProgressSyncIsFailing() {
if (!isNaN(this.syncFailedToast)) this.$toast.dismiss(this.syncFailedToast)
this.syncFailedToast = this.$toast('Progress is not being synced', { timeout: false, type: 'error' })
} }
}, },
mounted() { mounted() {
@@ -266,7 +268,6 @@ export default {
this.onSleepTimerEndedListener = AbsAudioPlayer.addListener('onSleepTimerEnded', this.onSleepTimerEnded) this.onSleepTimerEndedListener = AbsAudioPlayer.addListener('onSleepTimerEnded', this.onSleepTimerEnded)
this.onSleepTimerSetListener = AbsAudioPlayer.addListener('onSleepTimerSet', this.onSleepTimerSet) this.onSleepTimerSetListener = AbsAudioPlayer.addListener('onSleepTimerSet', this.onSleepTimerSet)
this.onMediaPlayerChangedListener = AbsAudioPlayer.addListener('onMediaPlayerChanged', this.onMediaPlayerChanged) this.onMediaPlayerChangedListener = AbsAudioPlayer.addListener('onMediaPlayerChanged', this.onMediaPlayerChanged)
this.onProgressSyncFailing = AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate') this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`) console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
@@ -283,7 +284,6 @@ export default {
if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove() if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove()
if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove() if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove()
if (this.onMediaPlayerChangedListener) this.onMediaPlayerChangedListener.remove() if (this.onMediaPlayerChangedListener) this.onMediaPlayerChangedListener.remove()
if (this.onProgressSyncFailing) this.onProgressSyncFailing.remove()
// if (this.$server.socket) { // if (this.$server.socket) {
// this.$server.socket.off('stream_open', this.streamOpen) // this.$server.socket.off('stream_open', this.streamOpen)
+6 -1
View File
@@ -99,6 +99,11 @@ export default {
text: 'Account', text: 'Account',
to: '/account' to: '/account'
}) })
items.push({
icon: 'equalizer',
text: 'User Stats',
to: '/stats'
})
} }
if (this.$platform !== 'ios') { if (this.$platform !== 'ios') {
@@ -162,4 +167,4 @@ export default {
this.show = false this.show = false
} }
} }
</script> </script>
+14 -3
View File
@@ -1,9 +1,9 @@
<template> <template>
<div id="bookshelf" class="w-full max-w-full h-full"> <div id="bookshelf" class="w-full max-w-full h-full">
<template v-for="shelf in totalShelves"> <template v-for="shelf in totalShelves">
<div :key="shelf" class="w-full px-2 relative" :class="showBookshelfListView ? '' : 'bookshelfRow'" :id="`shelf-${shelf - 1}`" :style="{ height: shelfHeight + 'px' }"> <div :key="shelf" class="w-full px-2 relative" :class="showBookshelfListView || altViewEnabled ? '' : 'bookshelfRow'" :id="`shelf-${shelf - 1}`" :style="{ height: shelfHeight + 'px' }">
<div v-if="!showBookshelfListView" class="bookshelfDivider w-full absolute bottom-0 left-0 z-30" style="min-height: 16px" :class="`h-${shelfDividerHeightIndex}`" /> <div v-if="!showBookshelfListView && !altViewEnabled" class="w-full absolute bottom-0 left-0 z-30 bookshelfDivider" style="min-height: 16px" :class="`h-${shelfDividerHeightIndex}`" />
<div v-else class="flex border-t border-white border-opacity-10" /> <div v-else-if="showBookshelfListView" class="flex border-t border-white border-opacity-10" />
</div> </div>
</template> </template>
@@ -119,12 +119,23 @@ export default {
}, },
shelfHeight() { shelfHeight() {
if (this.showBookshelfListView) return this.entityHeight + 16 if (this.showBookshelfListView) return this.entityHeight + 16
if (this.altViewEnabled) {
var extraTitleSpace = this.isBookEntity ? 80 : 40
return this.entityHeight + extraTitleSpace * this.sizeMultiplier
}
return this.entityHeight + 40 return this.entityHeight + 40
}, },
totalEntityCardWidth() { totalEntityCardWidth() {
if (this.showBookshelfListView) return this.entityWidth if (this.showBookshelfListView) return this.entityWidth
// Includes margin // Includes margin
return this.entityWidth + 24 return this.entityWidth + 24
},
altViewEnabled() {
return this.$store.getters['getAltViewEnabled']
},
sizeMultiplier() {
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
return this.entityWidth / baseSize
} }
}, },
methods: { methods: {
+33 -9
View File
@@ -1,20 +1,24 @@
<template> <template>
<div class="w-full relative"> <div class="w-full relative">
<div class="bookshelfRow flex items-end px-3 max-w-full overflow-x-auto" :style="{ height: shelfHeight + 'px' }"> <div v-if="altViewEnabled" class="px-5 pb-3 pt-4">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ label }}</p>
</div>
<div class="flex items-end px-3 max-w-full overflow-x-auto" :class="altViewEnabled ? '' : 'bookshelfRow'" :style="{ height: shelfHeight + 'px', paddingBottom: entityPaddingBottom + 'px' }">
<template v-for="(entity, index) in entities"> <template v-for="(entity, index) in entities">
<cards-lazy-book-card v-if="type === 'book' || type === 'podcast'" :key="entity.id" :index="index" :book-mount="entity" :width="bookWidth" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" is-categorized class="mx-2 relative" /> <cards-lazy-book-card v-if="type === 'book' || type === 'podcast'" :key="entity.id" :index="index" :book-mount="entity" :width="bookWidth" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :is-alt-view-enabled="altViewEnabled" class="mx-2 relative" />
<cards-lazy-book-card v-if="type === 'episode'" :key="entity.recentEpisode.id" :index="index" :book-mount="entity" :width="bookWidth" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" is-categorized class="mx-2 relative" /> <cards-lazy-book-card v-if="type === 'episode'" :key="entity.recentEpisode.id" :index="index" :book-mount="entity" :width="bookWidth" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :is-alt-view-enabled="altViewEnabled" class="mx-2 relative" />
<cards-lazy-series-card v-else-if="type === 'series'" :key="entity.id" :index="index" :series-mount="entity" :width="bookWidth * 2" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" is-categorized class="mx-2 relative" /> <cards-lazy-series-card v-else-if="type === 'series'" :key="entity.id" :index="index" :series-mount="entity" :width="bookWidth * 2" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :is-alt-view-enabled="altViewEnabled" is-categorized class="mx-2 relative" />
<cards-author-card v-else-if="type === 'authors'" :key="entity.id" :width="bookWidth / 1.25" :height="bookWidth" :author="entity" :size-multiplier="1" class="mx-2" /> <cards-author-card v-else-if="type === 'authors'" :key="entity.id" :width="bookWidth / 1.25" :height="bookWidth" :author="entity" :size-multiplier="1" class="mx-2" />
</template> </template>
</div> </div>
<div class="absolute text-center categoryPlacard font-book transform z-30 bottom-0.5 left-4 md:left-8 w-36 rounded-md" style="height: 18px"> <div v-if="!altViewEnabled" class="absolute text-center categoryPlacard font-book transform z-30 bottom-0.5 left-4 md:left-8 w-36 rounded-md" style="height: 18px">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border"> <div class="w-full h-full flex items-center justify-center rounded-sm border shinyBlack">
<p class="transform text-xs">{{ label }}</p> <p class="transform text-xs">{{ label }}</p>
</div> </div>
</div> </div>
<div class="w-full h-5 z-40 bookshelfDivider"></div> <div v-if="!altViewEnabled" class="w-full h-5 z-40 bookshelfDivider"></div>
</div> </div>
</template> </template>
@@ -32,23 +36,43 @@ export default {
return {} return {}
}, },
computed: { computed: {
entityPaddingBottom() {
if (!this.altViewEnabled) return 0
if (this.type === 'authors') return 10
else if (this.type === 'series') return 40
return 60 * this.sizeMultiplier
},
shelfHeight() { shelfHeight() {
if (this.altViewEnabled) {
var extraTitleSpace = this.type === 'authors' ? 10 : this.type === 'series' ? 50 : 60
return this.entityHeight + extraTitleSpace * this.sizeMultiplier
}
return this.entityHeight + 40 return this.entityHeight + 40
}, },
bookWidth() { bookWidth() {
var coverSize = 100 var coverSize = 100
if (this.bookCoverAspectRatio === 1) return coverSize * 1.6 if (this.isCoverSquareAspectRatio) return coverSize * 1.6
return coverSize return coverSize
}, },
bookHeight() { bookHeight() {
if (this.bookCoverAspectRatio === 1) return this.bookWidth if (this.isCoverSquareAspectRatio) return this.bookWidth
return this.bookWidth * 1.6 return this.bookWidth * 1.6
}, },
entityHeight() { entityHeight() {
return this.bookHeight return this.bookHeight
}, },
sizeMultiplier() {
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
return this.bookWidth / baseSize
},
isCoverSquareAspectRatio() {
return this.bookCoverAspectRatio === 1
},
bookCoverAspectRatio() { bookCoverAspectRatio() {
return this.$store.getters['getBookCoverAspectRatio'] return this.$store.getters['getBookCoverAspectRatio']
},
altViewEnabled() {
return this.$store.getters['getAltViewEnabled']
} }
}, },
methods: {}, methods: {},
+11 -6
View File
@@ -6,13 +6,13 @@
</div> </div>
<!-- Alternative bookshelf title/author/sort --> <!-- Alternative bookshelf title/author/sort -->
<!-- <div v-if="isAlternativeBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }"> <div v-if="isAltViewEnabled" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }"> <p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
{{ displayTitle }} {{ displayTitle }}
</p> </p>
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p> <p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayAuthor || '&nbsp;' }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p> <p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
</div> --> </div>
<div v-if="booksInSeries" class="absolute z-20 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ booksInSeries }}</div> <div v-if="booksInSeries" class="absolute z-20 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ booksInSeries }}</div>
@@ -84,7 +84,7 @@ export default {
}, },
bookCoverAspectRatio: Number, bookCoverAspectRatio: Number,
showSequence: Boolean, showSequence: Boolean,
bookshelfView: Number, isAltViewEnabled: Boolean,
bookMount: { bookMount: {
// Book can be passed as prop or set with setEntity() // Book can be passed as prop or set with setEntity()
type: Object, type: Object,
@@ -239,7 +239,7 @@ export default {
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs) if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs) if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt) if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt)
if (this.orderBy === 'duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false) if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size) if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
return null return null
}, },
@@ -250,7 +250,7 @@ export default {
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id) return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
}, },
userProgress() { userProgress() {
if (this.episodeProgress) return this.episodeProgress if (this.recentEpisode) return this.episodeProgress || null
if (this.isLocal) return this.store.getters['globals/getLocalMediaProgressById'](this.libraryItemId) if (this.isLocal) return this.store.getters['globals/getLocalMediaProgressById'](this.libraryItemId)
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId) return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
}, },
@@ -346,6 +346,11 @@ export default {
return this.author.slice(0, 27) + '...' return this.author.slice(0, 27) + '...'
} }
return this.author return this.author
},
titleDisplayBottomOffset() {
if (!this.isAltViewEnabled) return 0
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
return 4.25 * this.sizeMultiplier
} }
}, },
methods: { methods: {
+4 -3
View File
@@ -5,8 +5,8 @@
<covers-collection-cover ref="cover" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-collection-cover ref="cover" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div> </div>
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }"> <div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(240, width) + 'px' }">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }"> <div class="w-full h-full flex items-center justify-center rounded-sm border" :class="isAltViewEnabled ? 'altBookshelfLabel' : 'shinyBlack'" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p> <p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div> </div>
</div> </div>
@@ -19,7 +19,8 @@ export default {
index: Number, index: Number,
width: Number, width: Number,
height: Number, height: Number,
bookCoverAspectRatio: Number bookCoverAspectRatio: Number,
isAltViewEnabled: Boolean
}, },
data() { data() {
return { return {
+3 -3
View File
@@ -16,7 +16,7 @@
</div> </div>
<div class="flex-grow px-2"> <div class="flex-grow px-2">
<p class="whitespace-normal" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }"> <p class="whitespace-normal" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">
{{ displayTitle }}<span v-if="seriesSequence">&nbsp;#{{ seriesSequence }}</span> <span v-if="seriesSequence">#{{ seriesSequence }}&nbsp;</span>{{ displayTitle }}
</p> </p>
<p class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">by {{ displayAuthor }}</p> <p class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">by {{ displayAuthor }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p> <p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
@@ -109,8 +109,8 @@ export default {
}, },
episodes() { episodes() {
if (this.isPodcast) { if (this.isPodcast) {
if (this.media.numEpisodes==1) { if (this.media.numEpisodes == 1) {
return "1 episode" return '1 episode'
} else { } else {
return this.media.numEpisodes + ' episodes' return this.media.numEpisodes + ' episodes'
} }
+6 -2
View File
@@ -5,8 +5,11 @@
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="title" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="title" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div> </div>
<div v-if="!isCategorized" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }"> <div v-if="isAltViewEnabled && isCategorized" class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }"> <p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
<div v-if="!isCategorized" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(240, width) + 'px' }">
<div class="w-full h-full flex items-center justify-center rounded-sm border" :class="isAltViewEnabled ? 'altBookshelfLabel' : 'shinyBlack'" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p> <p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div> </div>
</div> </div>
@@ -24,6 +27,7 @@ export default {
type: Object, type: Object,
default: () => null default: () => null
}, },
isAltViewEnabled: Boolean,
isCategorized: Boolean isCategorized: Boolean
}, },
data() { data() {
+10 -4
View File
@@ -16,6 +16,9 @@
</template> </template>
<div v-else class="w-full"> <div v-else class="w-full">
<form v-show="!showAuth" @submit.prevent="submit" novalidate class="w-full"> <form v-show="!showAuth" @submit.prevent="submit" novalidate class="w-full">
<div v-if="serverConnectionConfigs.length" class="flex items-center mb-4" @click="showServerList">
<span class="material-icons text-gray-300">arrow_back</span>
</div>
<h2 class="text-lg leading-7 mb-2">Server address</h2> <h2 class="text-lg leading-7 mb-2">Server address</h2>
<ui-text-input v-model="serverConfig.address" :disabled="processing || !networkConnected || !!serverConfig.id" placeholder="http://55.55.55.55:13378" type="url" class="w-full h-10" /> <ui-text-input v-model="serverConfig.address" :disabled="processing || !networkConnected || !!serverConfig.id" placeholder="http://55.55.55.55:13378" type="url" class="w-full h-10" />
<div class="flex justify-end items-center mt-6"> <div class="flex justify-end items-center mt-6">
@@ -149,7 +152,7 @@ export default {
var payload = await this.authenticateToken() var payload = await this.authenticateToken()
if (payload) { if (payload) {
this.setUserAndConnection(payload.user, payload.userDefaultLibraryId) this.setUserAndConnection(payload)
} else { } else {
this.showAuth = true this.showAuth = true
} }
@@ -273,7 +276,8 @@ export default {
this.error = 'Invalid username' this.error = 'Invalid username'
return return
} }
const duplicateConfig = this.serverConnectionConfigs.find((scc) => scc.address === this.serverConfig.address && scc.username === this.serverConfig.username)
const duplicateConfig = this.serverConnectionConfigs.find((scc) => scc.address === this.serverConfig.address && scc.username === this.serverConfig.username && this.serverConfig.id !== scc.id)
if (duplicateConfig) { if (duplicateConfig) {
this.error = 'Config already exists for this address and username' this.error = 'Config already exists for this address and username'
return return
@@ -285,14 +289,16 @@ export default {
var payload = await this.requestServerLogin() var payload = await this.requestServerLogin()
this.processing = false this.processing = false
if (payload) { if (payload) {
this.setUserAndConnection(payload.user, payload.userDefaultLibraryId) this.setUserAndConnection(payload)
} }
}, },
async setUserAndConnection(user, userDefaultLibraryId) { async setUserAndConnection({ user, userDefaultLibraryId, serverSettings }) {
if (!user) return if (!user) return
console.log('Successfully logged in', JSON.stringify(user)) console.log('Successfully logged in', JSON.stringify(user))
this.$store.commit('setServerSettings', serverSettings)
// Set library - Use last library if set and available fallback to default user library // Set library - Use last library if set and available fallback to default user library
var lastLibraryId = await this.$localStore.getLastLibraryId() var lastLibraryId = await this.$localStore.getLastLibraryId()
if (lastLibraryId && (!user.librariesAccessible.length || user.librariesAccessible.includes(lastLibraryId))) { if (lastLibraryId && (!user.librariesAccessible.length || user.librariesAccessible.includes(lastLibraryId))) {
+1 -1
View File
@@ -8,7 +8,7 @@
<p v-show="!selectedSeriesName" class="font-book pt-1">{{ totalEntities }} {{ entityTitle }}</p> <p v-show="!selectedSeriesName" class="font-book pt-1">{{ totalEntities }} {{ entityTitle }}</p>
<p v-show="selectedSeriesName" class="ml-2 font-book pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p> <p v-show="selectedSeriesName" class="ml-2 font-book pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p>
<div class="flex-grow" /> <div class="flex-grow" />
<span v-if="page == 'library' || seriesBookPage" class="material-icons px-2" @click="bookshelfListView = !bookshelfListView">{{ bookshelfListView ? 'view_list' : 'grid_view' }}</span> <span v-if="page == 'library' || seriesBookPage" class="material-icons px-2" @click="bookshelfListView = !bookshelfListView">{{ !bookshelfListView ? 'view_list' : 'grid_view' }}</span>
<template v-if="page === 'library'"> <template v-if="page === 'library'">
<div class="relative flex items-center px-2"> <div class="relative flex items-center px-2">
<span class="material-icons" @click="showFilterModal = true">filter_alt</span> <span class="material-icons" @click="showFilterModal = true">filter_alt</span>
+8
View File
@@ -86,6 +86,11 @@ export default {
value: 'narrators', value: 'narrators',
sublist: true sublist: true
}, },
{
text: 'Language',
value: 'languages',
sublist: true
},
{ {
text: 'Progress', text: 'Progress',
value: 'progress', value: 'progress',
@@ -165,6 +170,9 @@ export default {
narrators() { narrators() {
return this.filterData.narrators || [] return this.filterData.narrators || []
}, },
languages() {
return this.filterData.languages || []
},
progress() { progress() {
return ['Finished', 'In Progress', 'Not Started', 'Not Finished'] return ['Finished', 'In Progress', 'Not Started', 'Not Finished']
}, },
+219
View File
@@ -0,0 +1,219 @@
<template>
<div class="w-96 my-6 mx-auto">
<h1 class="text-2xl mb-4 font-book">Minutes Listening <span class="text-white text-opacity-60 text-lg">(Last 7 days)</span></h1>
<div class="relative w-96 h-72">
<div class="absolute top-0 left-0">
<template v-for="lbl in yAxisLabels">
<div :key="lbl" :style="{ height: lineSpacing + 'px' }" class="flex items-center justify-end">
<p class="text-xs font-semibold">{{ lbl }}</p>
</div>
</template>
</div>
<template v-for="n in 7">
<div :key="n" class="absolute pointer-events-none left-0 h-px bg-white bg-opacity-10" :style="{ top: n * lineSpacing - lineSpacing / 2 + 'px', width: '360px', marginLeft: '24px' }" />
<div :key="`dot-${n}`" class="absolute z-10" :style="{ left: points[n - 1].x + 'px', bottom: points[n - 1].y + 'px' }">
<div class="h-2 w-2 bg-yellow-400 hover:bg-yellow-300 rounded-full transform duration-150 transition-transform hover:scale-125" />
</div>
</template>
<template v-for="(line, index) in pointLines">
<div :key="`line-${index}`" class="absolute h-0.5 bg-yellow-400 origin-bottom-left pointer-events-none" :style="{ width: line.width + 'px', left: line.x + 'px', bottom: line.y + 'px', transform: `rotate(${line.angle}deg)` }" />
</template>
<div class="absolute -bottom-2 left-0 flex ml-6">
<template v-for="dayObj in last7Days">
<div :key="dayObj.date" :style="{ width: daySpacing + daySpacing / 14 + 'px' }">
<p class="text-sm font-book">{{ dayObj.dayOfWeek.slice(0, 3) }}</p>
</div>
</template>
</div>
</div>
<div class="flex justify-between pt-12">
<div>
<p class="text-sm text-center">Week Listening</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ totalMinutesListeningThisWeek }}</p>
<p class="text-sm text-center">minutes</p>
</div>
<div>
<p class="text-sm text-center">Daily Average</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ averageMinutesPerDay }}</p>
<p class="text-sm text-center">minutes</p>
</div>
<div>
<p class="text-sm text-center">Best Day</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ mostListenedDay }}</p>
<p class="text-sm text-center">minutes</p>
</div>
<div>
<p class="text-sm text-center">Days</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ daysInARow }}</p>
<p class="text-sm text-center">in a row</p>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
listeningStats: {
type: Object,
default: () => {}
}
},
data() {
return {
// test: [111, 120, 4, 156, 273, 76, 12],
chartHeight: 288,
chartWidth: 384,
chartContentWidth: 360,
chartContentHeight: 268
}
},
computed: {
yAxisLabels() {
var lbls = []
for (let i = 6; i >= 0; i--) {
lbls.push(i * this.yAxisFactor)
}
return lbls
},
chartContentMarginLeft() {
return this.chartWidth - this.chartContentWidth
},
chartContentMarginBottom() {
return this.chartHeight - this.chartContentHeight
},
lineSpacing() {
return this.chartHeight / 7
},
daySpacing() {
return this.chartContentWidth / 7
},
linePositions() {
var poses = []
for (let i = 7; i > 0; i--) {
poses.push(i * this.lineSpacing)
}
poses.push(0)
return poses
},
last7Days() {
var days = []
for (let i = 6; i >= 0; i--) {
var _date = this.$addDaysToToday(i * -1)
days.push({
dayOfWeek: this.$formatJsDate(_date, 'EEEE'),
date: this.$formatJsDate(_date, 'yyyy-MM-dd')
})
}
return days
},
last7DaysOfListening() {
var listeningDays = {}
var _index = 0
this.last7Days.forEach((dayObj) => {
listeningDays[_index++] = {
dayOfWeek: dayObj.dayOfWeek,
// minutesListening: this.test[_index - 1]
minutesListening: this.getMinutesListeningForDate(dayObj.date)
}
})
return listeningDays
},
mostListenedDay() {
var sorted = Object.values(this.last7DaysOfListening)
.map((dl) => ({ ...dl }))
.sort((a, b) => b.minutesListening - a.minutesListening)
return sorted[0].minutesListening
},
yAxisFactor() {
var factor = Math.ceil(this.mostListenedDay / 5)
if (factor > 25) {
// Use nearest multiple of 5
return Math.ceil(factor / 5) * 5
}
return Math.max(1, factor)
},
points() {
var data = []
for (let i = 0; i < 7; i++) {
var listeningObj = this.last7DaysOfListening[String(i)]
var minutesListening = listeningObj.minutesListening || 0
var yPercent = minutesListening / (this.yAxisFactor * 7)
data.push({
x: 4 + this.chartContentMarginLeft + (this.daySpacing + this.daySpacing / 14) * i,
y: this.chartContentMarginBottom + this.chartHeight * yPercent - 2
})
}
return data
},
pointLines() {
var lines = []
for (let i = 1; i < 7; i++) {
var lastPoint = this.points[i - 1]
var nextPoint = this.points[i]
var x1 = lastPoint.x
var x2 = nextPoint.x
var y1 = lastPoint.y
var y2 = nextPoint.y
lines.push({
x: x1 + 4,
y: y1 + 2,
angle: this.getAngleBetweenPoints(x1, y1, x2, y2),
width: Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)) - 2
})
}
return lines
},
totalMinutesListeningThisWeek() {
var _total = 0
Object.values(this.last7DaysOfListening).forEach((listeningObj) => (_total += listeningObj.minutesListening))
return _total
},
averageMinutesPerDay() {
return Math.round(this.totalMinutesListeningThisWeek / 7)
},
daysInARow() {
var count = 0
while (true) {
var _date = this.$addDaysToToday(count * -1)
var datestr = this.$formatJsDate(_date, 'yyyy-MM-dd')
if (!this.listeningStatsDays[datestr] || this.listeningStatsDays[datestr] === 0) {
return count
}
count++
if (count > 9999) {
console.error('Overflow protection')
return 0
}
}
},
listeningStatsDays() {
return this.listeningStats ? this.listeningStats.days || [] : []
}
},
methods: {
getAngleBetweenPoints(cx, cy, ex, ey) {
var dy = ey - cy
var dx = ex - cx
var theta = Math.atan2(dy, dx)
theta *= 180 / Math.PI // convert to degrees
return theta * -1
},
getMinutesListeningForDate(date) {
if (!this.listeningStats || !this.listeningStats.days) return 0
return Math.round((this.listeningStats.days[date] || 0) / 60)
}
},
mounted() {}
}
</script>
+8 -6
View File
@@ -1,12 +1,14 @@
<template> <template>
<div class="w-full bg-primary bg-opacity-40"> <div class="w-full bg-primary bg-opacity-40">
<div class="w-full h-14 flex items-center px-4 bg-primary"> <div class="w-full h-14 flex items-center px-4 bg-primary">
<p>Collection List</p> <p class="pr-4">Collection List</p>
<div class="w-6 h-6 bg-white bg-opacity-10 flex items-center justify-center rounded-full ml-2">
<p class="font-mono text-sm">{{ books.length }}</p> <div class="w-6 h-6 md:w-7 md:h-7 bg-white bg-opacity-10 rounded-full flex items-center justify-center">
<span class="text-xs md:text-sm font-mono leading-none">{{ books.length }}</span>
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
<p v-if="totalDuration">{{ totalDurationPretty }}</p> <p v-if="totalDuration" class="text-sm text-gray-200">{{ totalDurationPretty }}</p>
</div> </div>
<template v-for="book in booksCopy"> <template v-for="book in booksCopy">
<tables-collection-book-table-row :key="book.id" :book="book" :collection-id="collectionId" class="item collection-book-item" @edit="editBook" /> <tables-collection-book-table-row :key="book.id" :book="book" :collection-id="collectionId" class="item collection-book-item" @edit="editBook" />
@@ -39,12 +41,12 @@ export default {
totalDuration() { totalDuration() {
var _total = 0 var _total = 0
this.books.forEach((book) => { this.books.forEach((book) => {
_total += book.duration _total += book.media.duration
}) })
return _total return _total
}, },
totalDurationPretty() { totalDurationPretty() {
return this.$elapsedPretty(this.totalDuration) return this.$elapsedPrettyExtended(this.totalDuration)
} }
}, },
methods: { methods: {
+3 -1
View File
@@ -199,7 +199,9 @@ export default {
this.$eventBus.$emit('play-item', { this.$eventBus.$emit('play-item', {
libraryItemId: this.localLibraryItemId, libraryItemId: this.localLibraryItemId,
episodeId: this.localEpisode.id episodeId: this.localEpisode.id,
serverLibraryItemId: this.libraryItemId,
serverEpisodeId: this.episode.id
}) })
} else { } else {
this.$eventBus.$emit('play-item', { this.$eventBus.$emit('play-item', {
+1 -1
View File
@@ -46,7 +46,7 @@ export default {
episodesCopy: [], episodesCopy: [],
showFiltersModal: false, showFiltersModal: false,
sortKey: 'publishedAt', sortKey: 'publishedAt',
sortDesc: false, sortDesc: true,
filterKey: 'incomplete', filterKey: 'incomplete',
episodeSortItems: [ episodeSortItems: [
{ {
+7 -2
View File
@@ -24,6 +24,9 @@ export default {
networkConnectionType() { networkConnectionType() {
return this.$store.state.networkConnectionType return this.$store.state.networkConnectionType
}, },
isNetworkUnmetered() {
return this.$store.state.isNetworkUnmetered
},
isCellular() { isCellular() {
return this.networkConnectionType === 'cellular' return this.networkConnectionType === 'cellular'
}, },
@@ -43,6 +46,7 @@ export default {
iconClass() { iconClass() {
if (!this.networkConnected) return 'text-error' if (!this.networkConnected) return 'text-error'
else if (!this.socketConnected) return 'text-warning' else if (!this.socketConnected) return 'text-warning'
else if (!this.isNetworkUnmetered) return 'text-yellow-400'
else if (this.isCellular) return 'text-gray-200' else if (this.isCellular) return 'text-gray-200'
else return 'text-success' else return 'text-success'
} }
@@ -50,14 +54,15 @@ export default {
methods: { methods: {
showAlertDialog() { showAlertDialog() {
var msg = '' var msg = ''
var meteredString = this.isNetworkUnmetered ? 'unmetered' : 'metered'
if (!this.networkConnected) { if (!this.networkConnected) {
msg = 'No internet' msg = 'No internet'
} else if (!this.socketConnected) { } else if (!this.socketConnected) {
msg = 'Socket not connected' msg = 'Socket not connected'
} else if (this.isCellular) { } else if (this.isCellular) {
msg = 'Socket connected over cellular' msg = `Socket connected over ${meteredString} cellular`
} else { } else {
msg = 'Socket connected over wifi' msg = `Socket connected over ${meteredString} wifi`
} }
Dialog.alert({ Dialog.alert({
title: 'Connection Status', title: 'Connection Status',
+4 -4
View File
@@ -479,12 +479,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons; ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 11; CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_TEAM = 7UFJ7D8V6A; DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.51; MARKETING_VERSION = 0.9.56;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev; PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -503,12 +503,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons; ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 11; CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_TEAM = 7UFJ7D8V6A; DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.51; MARKETING_VERSION = 0.9.56;
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app; PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
+15
View File
@@ -1,5 +1,6 @@
import UIKit import UIKit
import Capacitor import Capacitor
import RealmSwift
@UIApplicationMain @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
@@ -8,6 +9,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch. // Override point for customization after application launch.
let configuration = Realm.Configuration(
schemaVersion: 1,
migrationBlock: { migration, oldSchemaVersion in
if (oldSchemaVersion < 1) {
NSLog("Realm schema version was \(oldSchemaVersion)")
migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in
newObject?["enableAltView"] = false
}
}
}
)
Realm.Configuration.defaultConfiguration = configuration
return true return true
} }
+6 -4
View File
@@ -134,24 +134,26 @@ public class AbsAudioPlayer: CAPPlugin {
@objc func setSleepTimer(_ call: CAPPluginCall) { @objc func setSleepTimer(_ call: CAPPluginCall) {
guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) } guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) }
guard let time = Int(timeString) else { return call.resolve([ "success": false ]) } guard let time = Int(timeString) else { return call.resolve([ "success": false ]) }
let timeSeconds = time / 1000
NSLog("chapter time: \(call.getBool("isChapterTime", false))") NSLog("chapter time: \(call.getBool("isChapterTime", false))")
if call.getBool("isChapterTime", false) { if call.getBool("isChapterTime", false) {
let timeToPause = time / 1000 - Int(PlayerHandler.getCurrentTime() ?? 0) let timeToPause = timeSeconds - Int(PlayerHandler.getCurrentTime() ?? 0)
if timeToPause < 0 { return call.resolve([ "success": false ]) } if timeToPause < 0 { return call.resolve([ "success": false ]) }
NSLog("oof \(timeToPause)") PlayerHandler.sleepTimerChapterStopTime = timeSeconds
PlayerHandler.remainingSleepTime = timeToPause PlayerHandler.remainingSleepTime = timeToPause
return call.resolve([ "success": true ]) return call.resolve([ "success": true ])
} }
PlayerHandler.remainingSleepTime = time / 1000 PlayerHandler.sleepTimerChapterStopTime = nil
PlayerHandler.remainingSleepTime = timeSeconds
call.resolve([ "success": true ]) call.resolve([ "success": true ])
} }
@objc func cancelSleepTimer(_ call: CAPPluginCall) { @objc func cancelSleepTimer(_ call: CAPPluginCall) {
PlayerHandler.remainingSleepTime = nil PlayerHandler.remainingSleepTime = nil
PlayerHandler.sleepTimerChapterStopTime = nil
call.resolve() call.resolve()
} }
@objc func getSleepTimerTime(_ call: CAPPluginCall) { @objc func getSleepTimerTime(_ call: CAPPluginCall) {
+1
View File
@@ -19,6 +19,7 @@ CAP_PLUGIN(AbsDatabase, "AbsDatabase",
CAP_PLUGIN_METHOD(getLocalLibraryItem, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getLocalLibraryItem, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getLocalLibraryItemByLId, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getLocalLibraryItemByLId, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getLocalLibraryItemsInFolder, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getLocalLibraryItemsInFolder, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getAllLocalMediaProgress, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(updateDeviceSettings, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(updateDeviceSettings, CAPPluginReturnPromise);
) )
+5
View File
@@ -86,12 +86,17 @@ public class AbsDatabase: CAPPlugin {
@objc func getLocalLibraryItemsInFolder(_ call: CAPPluginCall) { @objc func getLocalLibraryItemsInFolder(_ call: CAPPluginCall) {
call.resolve([ "value": [] ]) call.resolve([ "value": [] ])
} }
@objc func getAllLocalMediaProgress(_ call: CAPPluginCall) {
call.resolve([ "value": [] ])
}
@objc func updateDeviceSettings(_ call: CAPPluginCall) { @objc func updateDeviceSettings(_ call: CAPPluginCall) {
let disableAutoRewind = call.getBool("disableAutoRewind") ?? false let disableAutoRewind = call.getBool("disableAutoRewind") ?? false
let enableAltView = call.getBool("enableAltView") ?? false
let jumpBackwardsTime = call.getInt("jumpBackwardsTime") ?? 10 let jumpBackwardsTime = call.getInt("jumpBackwardsTime") ?? 10
let jumpForwardTime = call.getInt("jumpForwardTime") ?? 10 let jumpForwardTime = call.getInt("jumpForwardTime") ?? 10
let settings = DeviceSettings() let settings = DeviceSettings()
settings.disableAutoRewind = disableAutoRewind settings.disableAutoRewind = disableAutoRewind
settings.enableAltView = enableAltView
settings.jumpBackwardsTime = jumpBackwardsTime settings.jumpBackwardsTime = jumpBackwardsTime
settings.jumpForwardTime = jumpForwardTime settings.jumpForwardTime = jumpForwardTime
+6 -6
View File
@@ -9,12 +9,12 @@ install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorApp', :path => '..\..\node_modules\@capacitor\app' pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
pod 'CapacitorDialog', :path => '..\..\node_modules\@capacitor\dialog' pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog'
pod 'CapacitorHaptics', :path => '..\..\node_modules\@capacitor\haptics' pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
pod 'CapacitorNetwork', :path => '..\..\node_modules\@capacitor\network' pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
pod 'CapacitorStatusBar', :path => '..\..\node_modules\@capacitor\status-bar' pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
pod 'CapacitorStorage', :path => '..\..\node_modules\@capacitor\storage' pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage'
end end
target 'App' do target 'App' do
@@ -10,6 +10,7 @@ import RealmSwift
class DeviceSettings: Object { class DeviceSettings: Object {
@Persisted var disableAutoRewind: Bool @Persisted var disableAutoRewind: Bool
@Persisted var enableAltView: Bool
@Persisted var jumpBackwardsTime: Int @Persisted var jumpBackwardsTime: Int
@Persisted var jumpForwardTime: Int @Persisted var jumpForwardTime: Int
} }
@@ -17,6 +18,7 @@ class DeviceSettings: Object {
func getDefaultDeviceSettings() -> DeviceSettings { func getDefaultDeviceSettings() -> DeviceSettings {
let settings = DeviceSettings() let settings = DeviceSettings()
settings.disableAutoRewind = false settings.disableAutoRewind = false
settings.enableAltView = false
settings.jumpForwardTime = 10 settings.jumpForwardTime = 10
settings.jumpBackwardsTime = 10 settings.jumpBackwardsTime = 10
return settings return settings
@@ -26,6 +28,7 @@ func deviceSettingsToJSON(settings: DeviceSettings) -> Dictionary<String, Any> {
return Database.realmQueue.sync { return Database.realmQueue.sync {
return [ return [
"disableAutoRewind": settings.disableAutoRewind, "disableAutoRewind": settings.disableAutoRewind,
"enableAltView": settings.enableAltView,
"jumpBackwardsTime": settings.jumpBackwardsTime, "jumpBackwardsTime": settings.jumpBackwardsTime,
"jumpForwardTime": settings.jumpForwardTime "jumpForwardTime": settings.jumpForwardTime
] ]
+31 -10
View File
@@ -11,7 +11,9 @@ class PlayerHandler {
private static var player: AudioPlayer? private static var player: AudioPlayer?
private static var session: PlaybackSession? private static var session: PlaybackSession?
private static var timer: Timer? private static var timer: Timer?
private static var lastSyncTime:Double = 0.0
public static var sleepTimerChapterStopTime: Int? = nil
private static var _remainingSleepTime: Int? = nil private static var _remainingSleepTime: Int? = nil
public static var remainingSleepTime: Int? { public static var remainingSleepTime: Int? {
get { get {
@@ -126,17 +128,27 @@ class PlayerHandler {
private static func tick() { private static func tick() {
if !paused { if !paused {
listeningTimePassedSinceLastSync += 1 listeningTimePassedSinceLastSync += 1
}
if remainingSleepTime != nil {
if listeningTimePassedSinceLastSync > 3 { if sleepTimerChapterStopTime != nil {
syncProgress() let timeUntilChapterEnd = Double(sleepTimerChapterStopTime ?? 0) - (getCurrentTime() ?? 0)
} if timeUntilChapterEnd <= 0 {
paused = true
if remainingSleepTime != nil { remainingSleepTime = nil
if remainingSleepTime! == 0 { } else {
paused = true remainingSleepTime = Int(timeUntilChapterEnd.rounded())
}
} else {
if remainingSleepTime! <= 0 {
paused = true
}
remainingSleepTime! -= 1
}
} }
remainingSleepTime! -= 1 }
if listeningTimePassedSinceLastSync >= 5 {
syncProgress()
} }
} }
public static func syncProgress() { public static func syncProgress() {
@@ -149,6 +161,15 @@ class PlayerHandler {
return return
} }
// Prevent multiple sync requests
let timeSinceLastSync = Date().timeIntervalSince1970 - lastSyncTime
if (lastSyncTime > 0 && timeSinceLastSync < 1) {
NSLog("syncProgress last sync time was < 1 second so not syncing")
return
}
lastSyncTime = Date().timeIntervalSince1970 // seconds
let report = PlaybackReport(currentTime: playerCurrentTime, duration: player.getDuration(), timeListened: listeningTimePassedSinceLastSync) let report = PlaybackReport(currentTime: playerCurrentTime, duration: player.getDuration(), timeListened: listeningTimePassedSinceLastSync)
session!.currentTime = playerCurrentTime session!.currentTime = playerCurrentTime
+6 -8
View File
@@ -109,7 +109,7 @@ export default {
console.log(`[default] Got server config, attempt authorize ${serverConfig.address}`) console.log(`[default] Got server config, attempt authorize ${serverConfig.address}`)
var authRes = await this.$axios.$post(`${serverConfig.address}/api/authorize`, null, { headers: { Authorization: `Bearer ${serverConfig.token}` } }).catch((error) => { var authRes = await this.$axios.$post(`${serverConfig.address}/api/authorize`, null, { headers: { Authorization: `Bearer ${serverConfig.token}` }, timeout: 3000 }).catch((error) => {
console.error('[Server] Server auth failed', error) console.error('[Server] Server auth failed', error)
var errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error' var errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
this.error = errorMsg this.error = errorMsg
@@ -120,7 +120,8 @@ export default {
return return
} }
const { user, userDefaultLibraryId } = authRes const { user, userDefaultLibraryId, serverSettings } = authRes
this.$store.commit('setServerSettings', serverSettings)
// Set library - Use last library if set and available fallback to default user library // Set library - Use last library if set and available fallback to default user library
var lastLibraryId = await this.$localStore.getLastLibraryId() var lastLibraryId = await this.$localStore.getLastLibraryId()
@@ -154,9 +155,6 @@ export default {
socketConnectionFailed(err) { socketConnectionFailed(err) {
this.$toast.error('Socket connection error: ' + err.message) this.$toast.error('Socket connection error: ' + err.message)
}, },
socketInit(data) {
console.log('Socket init', data)
},
async initLibraries() { async initLibraries() {
if (this.inittingLibraries) { if (this.inittingLibraries) {
return return
@@ -255,7 +253,6 @@ export default {
} }
}, },
async mounted() { async mounted() {
this.$socket.on('initialized', this.socketInit)
this.$socket.on('user_updated', this.userUpdated) this.$socket.on('user_updated', this.userUpdated)
this.$socket.on('user_media_progress_updated', this.userMediaProgressUpdated) this.$socket.on('user_media_progress_updated', this.userMediaProgressUpdated)
@@ -267,6 +264,8 @@ export default {
await this.$store.dispatch('setupNetworkListener') await this.$store.dispatch('setupNetworkListener')
await this.$store.dispatch('globals/loadLocalMediaProgress')
if (this.$store.state.user.serverConnectionConfig) { if (this.$store.state.user.serverConnectionConfig) {
console.log(`[default] server connection config set - call init libraries`) console.log(`[default] server connection config set - call init libraries`)
await this.initLibraries() await this.initLibraries()
@@ -277,13 +276,12 @@ export default {
console.log(`[default] finished connection attempt or already connected ${!!this.user}`) console.log(`[default] finished connection attempt or already connected ${!!this.user}`)
await this.syncLocalMediaProgress() await this.syncLocalMediaProgress()
this.$store.dispatch('globals/loadLocalMediaProgress')
this.loadSavedSettings() this.loadSavedSettings()
this.hasMounted = true this.hasMounted = true
} }
}, },
beforeDestroy() { beforeDestroy() {
this.$socket.off('initialized', this.socketInit)
this.$socket.off('user_updated', this.userUpdated) this.$socket.off('user_updated', this.userUpdated)
this.$socket.off('user_media_progress_updated', this.userMediaProgressUpdated) this.$socket.off('user_media_progress_updated', this.userMediaProgressUpdated)
} }
+3 -2
View File
@@ -45,7 +45,8 @@ export default {
index, index,
width: this.entityWidth, width: this.entityWidth,
height: this.entityHeight, height: this.entityHeight,
bookCoverAspectRatio: this.bookCoverAspectRatio bookCoverAspectRatio: this.bookCoverAspectRatio,
isAltViewEnabled: this.altViewEnabled
} }
if (this.entityName === 'series-books') props.showSequence = true if (this.entityName === 'series-books') props.showSequence = true
if (this.entityName === 'books') { if (this.entityName === 'books') {
@@ -54,7 +55,7 @@ export default {
props.sortingIgnorePrefix = !!this.sortingIgnorePrefix props.sortingIgnorePrefix = !!this.sortingIgnorePrefix
} }
var _this = this // var _this = this
var instance = new ComponentClass({ var instance = new ComponentClass({
propsData: props, propsData: props,
created() { created() {
+3 -2
View File
@@ -1,11 +1,12 @@
{ {
"name": "audiobookshelf-app", "name": "audiobookshelf-app",
"version": "0.9.52-beta", "version": "0.9.56-beta",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"version": "0.9.44-beta", "name": "audiobookshelf-app",
"version": "0.9.56-beta",
"dependencies": { "dependencies": {
"@capacitor/android": "^3.4.3", "@capacitor/android": "^3.4.3",
"@capacitor/app": "^1.1.1", "@capacitor/app": "^1.1.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-app", "name": "audiobookshelf-app",
"version": "0.9.52-beta", "version": "0.9.56-beta",
"author": "advplyr", "author": "advplyr",
"scripts": { "scripts": {
"dev": "nuxt --hostname 0.0.0.0 --port 1337", "dev": "nuxt --hostname 0.0.0.0 --port 1337",
+14 -5
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="w-full h-full min-h-full relative"> <div class="w-full h-full min-h-full relative">
<div v-if="!loading" class="w-full"> <div v-if="!loading" class="w-full" :class="{ 'py-6': altViewEnabled }">
<template v-for="(shelf, index) in shelves"> <template v-for="(shelf, index) in shelves">
<bookshelf-shelf :key="shelf.id" :label="shelf.label" :entities="shelf.entities" :type="shelf.type" :style="{ zIndex: shelves.length - index }" /> <bookshelf-shelf :key="shelf.id" :label="shelf.label" :entities="shelf.entities" :type="shelf.type" :style="{ zIndex: shelves.length - index }" />
</template> </template>
@@ -53,6 +53,12 @@ export default {
}, },
currentLibraryId() { currentLibraryId() {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
},
altViewEnabled() {
return this.$store.getters['getAltViewEnabled']
},
localMediaProgress() {
return this.$store.state.globals.localMediaProgress
} }
}, },
methods: { methods: {
@@ -66,6 +72,7 @@ export default {
var podcasts = [] var podcasts = []
localMedia.forEach((item) => { localMedia.forEach((item) => {
if (item.mediaType == 'book') { if (item.mediaType == 'book') {
item.progress = this.localMediaProgress.find((lmp) => lmp.id === item.id)
books.push(item) books.push(item)
} else if (item.mediaType == 'podcast') { } else if (item.mediaType == 'podcast') {
podcasts.push(item) podcasts.push(item)
@@ -77,7 +84,11 @@ export default {
id: 'local-books', id: 'local-books',
label: 'Local Books', label: 'Local Books',
type: 'book', type: 'book',
entities: books.slice(0, 10) entities: books.sort((a, b) => {
if (a.progress && a.progress.isFinished) return 1
else if (b.progress && b.progress.isFinished) return -1
return 0
})
}) })
} }
if (podcasts.length) { if (podcasts.length) {
@@ -85,7 +96,7 @@ export default {
id: 'local-podcasts', id: 'local-podcasts',
label: 'Local Podcasts', label: 'Local Podcasts',
type: 'podcast', type: 'podcast',
entities: podcasts.slice(0, 10) entities: podcasts
}) })
} }
@@ -183,12 +194,10 @@ export default {
}) })
}, },
initListeners() { initListeners() {
// this.$server.on('initialized', this.socketInit)
this.$eventBus.$on('library-changed', this.libraryChanged) this.$eventBus.$on('library-changed', this.libraryChanged)
// this.$eventBus.$on('downloads-loaded', this.downloadsLoaded) // this.$eventBus.$on('downloads-loaded', this.downloadsLoaded)
}, },
removeListeners() { removeListeners() {
// this.$server.off('initialized', this.socketInit)
this.$eventBus.$off('library-changed', this.libraryChanged) this.$eventBus.$off('library-changed', this.libraryChanged)
// this.$eventBus.$off('downloads-loaded', this.downloadsLoaded) // this.$eventBus.$off('downloads-loaded', this.downloadsLoaded)
} }
+61 -12
View File
@@ -9,20 +9,21 @@
</div> </div>
<div class="title-container flex-grow pl-2"> <div class="title-container flex-grow pl-2">
<div class="flex relative pr-6"> <div class="flex relative pr-6">
<h1 class="text-base">{{ title }}</h1> <h1 class="text-base font-semibold">{{ title }}</h1>
<button class="absolute top-0 right-0 h-full px-1 outline-none" @click="moreButtonPress"> <button class="absolute top-0 right-0 h-full px-1 outline-none" @click="moreButtonPress">
<span class="material-icons text-xl">more_vert</span> <span class="material-icons text-xl">more_vert</span>
</button> </button>
</div> </div>
<p v-if="subtitle" class="text-gray-100 text-sm py-0.5">{{ subtitle }}</p>
<p v-if="seriesList && seriesList.length" class="text-sm text-gray-300 py-0.5"> <p v-if="seriesList && seriesList.length" class="text-sm text-gray-300 py-0.5">
<template v-for="(series, index) in seriesList" <template v-for="(series, index) in seriesList"
><nuxt-link :key="series.id" :to="`/bookshelf/series/${series.id}`">{{ series.text }}</nuxt-link ><nuxt-link :key="series.id" :to="`/bookshelf/series/${series.id}`">{{ series.text }}</nuxt-link
><span :key="`${series.id}-comma`" v-if="index < seriesList.length - 1">,&nbsp;</span></template ><span :key="`${series.id}-comma`" v-if="index < seriesList.length - 1">,&nbsp;</span></template
> >
</p> </p>
<p v-if="podcastAuthor" class="text-sm text-gray-400 py-0.5">By {{ podcastAuthor }}</p> <p v-if="podcastAuthor" class="text-sm text-gray-300 py-0.5">By {{ podcastAuthor }}</p>
<p v-else-if="bookAuthors && bookAuthors.length" class="text-sm text-gray-400 py-0.5"> <p v-else-if="bookAuthors && bookAuthors.length" class="text-sm text-gray-300 py-0.5">
By By
<template v-for="(author, index) in bookAuthors" <template v-for="(author, index) in bookAuthors"
><nuxt-link :key="author.id" :to="`/bookshelf/library?filter=authors.${$encode(author.id)}`">{{ author.name }}</nuxt-link ><nuxt-link :key="author.id" :to="`/bookshelf/library?filter=authors.${$encode(author.id)}`">{{ author.name }}</nuxt-link
@@ -84,7 +85,7 @@
<div v-else-if="(user && (showPlay || showRead)) || hasLocal" class="flex mt-4"> <div v-else-if="(user && (showPlay || showRead)) || hasLocal" class="flex mt-4">
<ui-btn v-if="showPlay" color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick"> <ui-btn v-if="showPlay" color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick">
<span v-show="!isPlaying" class="material-icons">play_arrow</span> <span v-show="!isPlaying" class="material-icons">play_arrow</span>
<span class="px-1 text-sm">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : hasLocal ? 'Play' : 'Stream' }}</span> <span class="px-1 text-sm">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : isPodcast ? 'Next Episode' : hasLocal ? 'Play' : 'Stream' }}</span>
</ui-btn> </ui-btn>
<ui-btn v-if="showRead && user" color="info" class="flex items-center justify-center mr-2" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook"> <ui-btn v-if="showRead && user" color="info" class="flex items-center justify-center mr-2" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
<span class="material-icons">auto_stories</span> <span class="material-icons">auto_stories</span>
@@ -218,6 +219,9 @@ export default {
title() { title() {
return this.mediaMetadata.title return this.mediaMetadata.title
}, },
subtitle() {
return this.mediaMetadata.subtitle
},
podcastAuthor() { podcastAuthor() {
if (!this.isPodcast) return null if (!this.isPodcast) return null
return this.mediaMetadata.author || '' return this.mediaMetadata.author || ''
@@ -301,13 +305,14 @@ export default {
return this.libraryItem.isIncomplete return this.libraryItem.isIncomplete
}, },
showPlay() { showPlay() {
return !this.isMissing && !this.isIncomplete && this.numTracks return !this.isMissing && !this.isIncomplete && (this.numTracks || this.episodes.length)
}, },
showRead() { showRead() {
return this.ebookFile && this.ebookFormat !== 'pdf' return this.ebookFile && this.ebookFormat !== 'pdf'
}, },
showDownload() { showDownload() {
if (this.isIos) return false if (this.isIos) return false
if (this.isPodcast) return false
return this.user && this.userCanDownload && this.showPlay && !this.hasLocal return this.user && this.userCanDownload && this.showPlay && !this.hasLocal
}, },
ebookFile() { ebookFile() {
@@ -364,14 +369,58 @@ export default {
this.$store.commit('openReader', this.libraryItem) this.$store.commit('openReader', this.libraryItem)
}, },
playClick() { playClick() {
// Todo: Allow playing local or streaming var episodeId = null
if (this.hasLocal && this.serverLibraryItemId && this.isCasting) {
// If casting and connected to server for local library item then send server library item id if (this.isPodcast) {
this.$eventBus.$emit('play-item', { libraryItemId: this.serverLibraryItemId }) this.episodes.sort((a, b) => {
return return String(b.publishedAt).localeCompare(String(a.publishedAt), undefined, { numeric: true, sensitivity: 'base' })
})
var episode = this.episodes.find((ep) => {
var podcastProgress = null
if (!this.isLocal) {
podcastProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, ep.id)
} else {
podcastProgress = this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, ep.id)
}
return !podcastProgress || !podcastProgress.isFinished
})
if (!episode) episode = this.episodes[0]
episodeId = episode.id
var localEpisode = null
if (this.hasLocal && !this.isLocal) {
localEpisode = this.localLibraryItem.media.episodes.find((ep) => ep.serverEpisodeId == episodeId)
} else if (this.isLocal) {
localEpisode = episode
}
var serverEpisodeId = !this.isLocal ? episodeId : localEpisode ? localEpisode.serverEpisodeId : null
if (serverEpisodeId && this.serverLibraryItemId && this.isCasting) {
// If casting and connected to server for local library item then send server library item id
this.$eventBus.$emit('play-item', { libraryItemId: this.serverLibraryItemId, episodeId: serverEpisodeId })
return
}
if (localEpisode) {
this.$eventBus.$emit('play-item', { libraryItemId: this.localLibraryItem.id, episodeId: localEpisode.id, serverLibraryItemId: this.serverLibraryItemId, serverEpisodeId })
return
}
} else {
// Audiobook
if (this.hasLocal && this.serverLibraryItemId && this.isCasting) {
// If casting and connected to server for local library item then send server library item id
this.$eventBus.$emit('play-item', { libraryItemId: this.serverLibraryItemId })
return
}
if (this.hasLocal) {
this.$eventBus.$emit('play-item', { libraryItemId: this.localLibraryItem.id, serverLibraryItemId: this.serverLibraryItemId })
return
}
} }
if (this.hasLocal) return this.$eventBus.$emit('play-item', { libraryItemId: this.localLibraryItem.id, serverLibraryItemId: this.serverLibraryItemId })
this.$eventBus.$emit('play-item', { libraryItemId: this.libraryItemId }) this.$eventBus.$emit('play-item', { libraryItemId: this.libraryItemId, episodeId })
}, },
async clearProgressClick() { async clearProgressClick() {
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
+1 -1
View File
@@ -17,7 +17,7 @@
<div v-else class="w-full media-item-container overflow-y-auto"> <div v-else class="w-full media-item-container overflow-y-auto">
<template v-for="mediaItem in localLibraryItems"> <template v-for="mediaItem in localLibraryItems">
<nuxt-link :to="`/localMedia/item/${mediaItem.id}`" :key="mediaItem.id" class="flex my-1"> <nuxt-link :to="`/localMedia/item/${mediaItem.id}`" :key="mediaItem.id" class="flex my-1">
<div class="w-12 h-12 bg-primary"> <div class="w-12 h-12 min-w-12 min-h-12 bg-primary">
<img v-if="mediaItem.coverPathSrc" :src="mediaItem.coverPathSrc" class="w-full h-full object-contain" /> <img v-if="mediaItem.coverPathSrc" :src="mediaItem.coverPathSrc" class="w-full h-full object-contain" />
</div> </div>
<div class="flex-grow px-2"> <div class="flex-grow px-2">
+52 -3
View File
@@ -1,10 +1,19 @@
<template> <template>
<div class="w-full h-full p-8"> <div class="w-full h-full p-8">
<div v-if="$platform !== 'ios'" class="flex items-center py-3" @click="toggleDisableAutoRewind"> <p class="uppercase text-xs font-semibold text-gray-300 mb-2">Display Settings</p>
<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>
<p class="uppercase text-xs font-semibold text-gray-300 mb-2 mt-6">Playback Settings</p>
<div v-if="!isiOS" class="flex items-center py-3" @click="toggleDisableAutoRewind">
<div class="w-10 flex justify-center"> <div class="w-10 flex justify-center">
<ui-toggle-switch v-model="settings.disableAutoRewind" @input="saveSettings" /> <ui-toggle-switch v-model="settings.disableAutoRewind" @input="saveSettings" />
</div> </div>
<p class="pl-4">Disable Auto Rewind</p> <p class="pl-4">Disable auto rewind</p>
</div> </div>
<div class="flex items-center py-3" @click="toggleJumpBackwards"> <div class="flex items-center py-3" @click="toggleJumpBackwards">
<div class="w-10 flex justify-center"> <div class="w-10 flex justify-center">
@@ -18,22 +27,44 @@
</div> </div>
<p class="pl-4">Jump forwards time</p> <p class="pl-4">Jump forwards time</p>
</div> </div>
<p v-if="!isiOS" class="uppercase text-xs font-semibold text-gray-300 mb-2 mt-6">Sleep Timer Settings</p>
<div v-if="!isiOS" class="flex items-center py-3" @click="toggleDisableShakeToResetSleepTimer">
<div class="w-10 flex justify-center">
<ui-toggle-switch v-model="settings.disableShakeToResetSleepTimer" @input="saveSettings" />
</div>
<p class="pl-4">Disable shake to reset</p>
<span class="material-icons-outlined ml-2" @click.stop="showInfo('disableShakeToResetSleepTimer')">info</span>
</div>
</div> </div>
</template> </template>
<script> <script>
import { Dialog } from '@capacitor/dialog'
export default { export default {
data() { data() {
return { return {
deviceData: null, deviceData: null,
settings: { settings: {
disableAutoRewind: false, disableAutoRewind: false,
enableAltView: false,
jumpForwardTime: 10, jumpForwardTime: 10,
jumpBackwardsTime: 10 jumpBackwardsTime: 10,
disableShakeToResetSleepTimer: false
},
settingInfo: {
disableShakeToResetSleepTimer: {
name: 'Disable shake to reset sleep timer',
message: 'The sleep timer will start fading out when 30s is remaining. Shaking your device will reset the timer if it is within 30s OR has finished less than 2 mintues ago. Enable this setting to disable that feature.'
}
} }
} }
}, },
computed: { computed: {
isiOS() {
return this.$platform === 'ios'
},
jumpForwardItems() { jumpForwardItems() {
return this.$store.state.globals.jumpForwardItems || [] return this.$store.state.globals.jumpForwardItems || []
}, },
@@ -56,10 +87,26 @@ export default {
} }
}, },
methods: { methods: {
showInfo(setting) {
if (this.settingInfo[setting]) {
Dialog.alert({
title: this.settingInfo[setting].name,
message: this.settingInfo[setting].message
})
}
},
toggleDisableShakeToResetSleepTimer() {
this.settings.disableShakeToResetSleepTimer = !this.settings.disableShakeToResetSleepTimer
this.saveSettings()
},
toggleDisableAutoRewind() { toggleDisableAutoRewind() {
this.settings.disableAutoRewind = !this.settings.disableAutoRewind this.settings.disableAutoRewind = !this.settings.disableAutoRewind
this.saveSettings() this.saveSettings()
}, },
toggleEnableAltView() {
this.settings.enableAltView = !this.settings.enableAltView
this.saveSettings()
},
toggleJumpForward() { toggleJumpForward() {
var next = (this.currentJumpForwardTimeIndex + 1) % 3 var next = (this.currentJumpForwardTimeIndex + 1) % 3
this.settings.jumpForwardTime = this.jumpForwardItems[next].value this.settings.jumpForwardTime = this.jumpForwardItems[next].value
@@ -85,8 +132,10 @@ export default {
const deviceSettings = this.deviceData.deviceSettings || {} const deviceSettings = this.deviceData.deviceSettings || {}
this.settings.disableAutoRewind = !!deviceSettings.disableAutoRewind this.settings.disableAutoRewind = !!deviceSettings.disableAutoRewind
this.settings.enableAltView = !!deviceSettings.enableAltView
this.settings.jumpForwardTime = deviceSettings.jumpForwardTime || 10 this.settings.jumpForwardTime = deviceSettings.jumpForwardTime || 10
this.settings.jumpBackwardsTime = deviceSettings.jumpBackwardsTime || 10 this.settings.jumpBackwardsTime = deviceSettings.jumpBackwardsTime || 10
this.settings.disableShakeToResetSleepTimer = !!deviceSettings.disableShakeToResetSleepTimer
} }
}, },
mounted() { mounted() {
+114
View File
@@ -0,0 +1,114 @@
<template>
<div class="w-full h-full px-0 py-4 overflow-y-auto">
<h1 class="text-xl px-4">
Stats for <b>{{ username }}</b>
</h1>
<div class="flex text-center justify-center">
<div class="flex p-2">
<div class="px-3">
<p class="text-4xl md:text-5xl font-bold">{{ userItemsFinished.length }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Items Finished</p>
</div>
</div>
<div class="flex p-2">
<div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalDaysListened }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Days Listened</p>
</div>
</div>
<div class="flex p-2">
<div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalMinutesListening }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Minutes Listening</p>
</div>
</div>
</div>
<div class="flex flex-col md:flex-row overflow-hidden max-w-full">
<stats-daily-listening-chart :listening-stats="listeningStats" class="lg:scale-100 transform scale-90 px-0" />
<div class="w-80 my-6 mx-auto">
<div class="flex mb-4 items-center">
<h1 class="text-2xl font-book">Recent Sessions</h1>
<div class="flex-grow" />
</div>
<p v-if="!mostRecentListeningSessions.length">No Listening Sessions</p>
<template v-for="(item, index) in mostRecentListeningSessions">
<div :key="item.id" class="w-full py-0.5">
<div class="flex items-center mb-1">
<p class="text-sm font-book text-white text-opacity-70 w-8">{{ index + 1 }}.&nbsp;</p>
<div class="w-56">
<p class="text-sm font-book text-white text-opacity-80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
</div>
<div class="flex-grow" />
<div class="w-18 text-right">
<p class="text-sm font-bold">{{ $elapsedPretty(item.timeListening) }}</p>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
listeningStats: null,
windowWidth: 0
}
},
watch: {
currentLibraryId(newVal) {
if (newVal) {
this.init()
}
}
},
computed: {
user() {
return this.$store.state.user.user
},
username() {
return this.user ? this.user.username : ''
},
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
userMediaProgress() {
return this.user ? this.user.mediaProgress : []
},
userItemsFinished() {
return this.userMediaProgress.filter((lip) => !!lip.isFinished)
},
mostRecentListeningSessions() {
if (!this.listeningStats) return []
return this.listeningStats.recentSessions || []
},
totalMinutesListening() {
if (!this.listeningStats) return 0
return Math.round(this.listeningStats.totalTime / 60)
},
totalDaysListened() {
if (!this.listeningStats) return 0
return Object.values(this.listeningStats.days).length
}
},
methods: {
async init() {
this.listeningStats = await this.$axios.$get(`/api/me/listening-stats`).catch((err) => {
console.error('Failed to load listening sesions', err)
return []
})
console.log('Loaded user listening data', this.listeningStats)
}
},
mounted() {
this.init()
}
}
</script>
+7
View File
@@ -5,6 +5,12 @@ const DownloadStatus = {
FAILED: 3 FAILED: 3
} }
const SyncStatus = {
UNSET: 0,
SUCCESS: 1,
FAILED: 2
}
const CoverDestination = { const CoverDestination = {
METADATA: 0, METADATA: 0,
AUDIOBOOK: 1 AUDIOBOOK: 1
@@ -31,6 +37,7 @@ const PlayerState = {
const Constants = { const Constants = {
DownloadStatus, DownloadStatus,
SyncStatus,
CoverDestination, CoverDestination,
BookCoverAspectRatio, BookCoverAspectRatio,
PlayMethod, PlayMethod,
+39 -3
View File
@@ -2,7 +2,7 @@ import Vue from 'vue'
import { App } from '@capacitor/app' import { App } from '@capacitor/app'
import { Dialog } from '@capacitor/dialog' import { Dialog } from '@capacitor/dialog'
import { StatusBar, Style } from '@capacitor/status-bar'; import { StatusBar, Style } from '@capacitor/status-bar';
import { formatDistance, format } from 'date-fns' import { formatDistance, format, addDays, isDate } from 'date-fns'
import { Capacitor } from '@capacitor/core'; import { Capacitor } from '@capacitor/core';
Vue.prototype.$eventBus = new Vue() Vue.prototype.$eventBus = new Vue()
@@ -24,7 +24,20 @@ Vue.prototype.$formatDate = (unixms, fnsFormat = 'MM/dd/yyyy HH:mm') => {
if (!unixms) return '' if (!unixms) return ''
return format(unixms, fnsFormat) return format(unixms, fnsFormat)
} }
Vue.prototype.$formatJsDate = (jsdate, fnsFormat = 'MM/dd/yyyy HH:mm') => {
if (!jsdate || !isDate(jsdate)) return ''
return format(jsdate, fnsFormat)
}
Vue.prototype.$addDaysToToday = (daysToAdd) => {
var date = addDays(new Date(), daysToAdd)
if (!date || !isDate(date)) return null
return date
}
Vue.prototype.$addDaysToDate = (jsdate, daysToAdd) => {
var date = addDays(jsdate, daysToAdd)
if (!date || !isDate(date)) return null
return date
}
Vue.prototype.$bytesPretty = (bytes, decimals = 2) => { Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
if (isNaN(bytes) || bytes === null) return 'Invalid Bytes' if (isNaN(bytes) || bytes === null) return 'Invalid Bytes'
if (bytes === 0) { if (bytes === 0) {
@@ -53,6 +66,29 @@ Vue.prototype.$elapsedPretty = (seconds, useFullNames = false) => {
return `${hours} ${useFullNames ? `hour${hours === 1 ? '' : 's'}` : 'hr'} ${minutes} ${useFullNames ? `minute${minutes === 1 ? '' : 's'}` : 'min'}` return `${hours} ${useFullNames ? `hour${hours === 1 ? '' : 's'}` : 'hr'} ${minutes} ${useFullNames ? `minute${minutes === 1 ? '' : 's'}` : 'min'}`
} }
Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true) => {
if (isNaN(seconds) || seconds === null) return ''
seconds = Math.round(seconds)
var minutes = Math.floor(seconds / 60)
seconds -= minutes * 60
var hours = Math.floor(minutes / 60)
minutes -= hours * 60
var days = 0
if (useDays || Math.floor(hours / 24) >= 100) {
days = Math.floor(hours / 24)
hours -= days * 24
}
var strs = []
if (days) strs.push(`${days}d`)
if (hours) strs.push(`${hours}h`)
if (minutes) strs.push(`${minutes}m`)
if (seconds) strs.push(`${seconds}s`)
return strs.join(' ')
}
Vue.prototype.$secondsToTimestamp = (seconds) => { Vue.prototype.$secondsToTimestamp = (seconds) => {
var _seconds = seconds var _seconds = seconds
var _minutes = Math.floor(seconds / 60) var _minutes = Math.floor(seconds / 60)
@@ -162,4 +198,4 @@ export default ({ store, app }) => {
export { export {
encode, encode,
decode decode
} }
-3
View File
@@ -80,9 +80,6 @@ class ServerSocket extends EventEmitter {
onInit(data) { onInit(data) {
console.log('[SOCKET] Initial socket data received', data) console.log('[SOCKET] Initial socket data received', data)
if (data.serverSettings) {
this.$store.commit('setServerSettings', data.serverSettings)
}
this.emit('initialized', true) this.emit('initialized', true)
} }
+5 -5
View File
@@ -1,10 +1,10 @@
import Vue from "vue"; import Vue from "vue"
import Toast from "vue-toastification"; import Toast from "vue-toastification"
import "vue-toastification/dist/index.css"; import "vue-toastification/dist/index.css"
const options = { const options = {
hideProgressBar: true, hideProgressBar: true,
position: 'bottom-center' position: 'bottom-center'
}; }
Vue.use(Toast, options); Vue.use(Toast, options)
+15
View File
@@ -1,4 +1,5 @@
import { Network } from '@capacitor/network' import { Network } from '@capacitor/network'
import { AbsAudioPlayer } from '@/plugins/capacitor'
export const state = () => ({ export const state = () => ({
deviceData: null, deviceData: null,
@@ -12,6 +13,7 @@ export const state = () => ({
socketConnected: false, socketConnected: false,
networkConnected: false, networkConnected: false,
networkConnectionType: null, networkConnectionType: null,
isNetworkUnmetered: true,
isFirstLoad: true, isFirstLoad: true,
hasStoragePermission: false, hasStoragePermission: false,
selectedLibraryItem: null, selectedLibraryItem: null,
@@ -45,6 +47,10 @@ export const getters = {
getJumpBackwardsTime: state => { getJumpBackwardsTime: state => {
if (!state.deviceData || !state.deviceData.deviceSettings) return 10 if (!state.deviceData || !state.deviceData.deviceSettings) return 10
return state.deviceData.deviceSettings.jumpBackwardsTime || 10 return state.deviceData.deviceSettings.jumpBackwardsTime || 10
},
getAltViewEnabled: state => {
if (!state.deviceData || !state.deviceData.deviceSettings) return false
return state.deviceData.deviceSettings.enableAltView
} }
} }
@@ -62,6 +68,12 @@ export const actions = {
console.log('Network status changed', status.connected, status.connectionType) console.log('Network status changed', status.connected, status.connectionType)
commit('setNetworkStatus', status) commit('setNetworkStatus', status)
}) })
AbsAudioPlayer.addListener('onNetworkMeteredChanged', (payload) => {
const isUnmetered = payload.value
console.log('On network metered changed', isUnmetered)
commit('setIsNetworkUnmetered', isUnmetered)
})
} }
} }
@@ -114,6 +126,9 @@ export const mutations = {
state.networkConnected = val.connected state.networkConnected = val.connected
state.networkConnectionType = val.connectionType state.networkConnectionType = val.connectionType
}, },
setIsNetworkUnmetered(state, val) {
state.isNetworkUnmetered = val
},
openReader(state, libraryItem) { openReader(state, libraryItem) {
state.selectedLibraryItem = libraryItem state.selectedLibraryItem = libraryItem
state.showReader = true state.showReader = true
+6
View File
@@ -39,6 +39,12 @@ module.exports = {
}, },
maxWidth: { maxWidth: {
'24': '6rem' '24': '6rem'
},
minWidth: {
'12': '3rem'
},
minHeight: {
'12': '3rem'
} }
} }
}, },