Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72744dcd29 | ||
|
|
6b374d923b | ||
|
|
03a47b06ee | ||
|
|
77d0974782 | ||
|
|
95e070a897 | ||
|
|
a5f483f518 | ||
|
|
b9f428fd77 | ||
|
|
94e8978ce6 | ||
|
|
62376871e3 | ||
|
|
40b731534c | ||
|
|
1aea0a18a9 | ||
|
|
b1be594932 | ||
|
|
c1b8f8519f | ||
|
|
34d4a0b61b | ||
|
|
7c3b6c9702 | ||
|
|
56c73c12db | ||
|
|
afc04d22bf | ||
|
|
b78f004f29 | ||
|
|
4a82ba7c86 | ||
|
|
719db8d3f6 | ||
|
|
db98492d4f | ||
|
|
5430c10424 | ||
|
|
92a31c246c | ||
|
|
9082b76707 | ||
|
|
408faf3665 | ||
|
|
be6c675ded | ||
|
|
87e277fad1 | ||
|
|
014588180e | ||
|
|
837df329e2 | ||
|
|
5662c1983a | ||
|
|
8cfefdcc0f | ||
|
|
7d91b95320 | ||
|
|
beb790ad17 | ||
|
|
073110376a | ||
|
|
16f6f3b7e9 | ||
|
|
4758f3e557 | ||
|
|
38cc2adf0f | ||
|
|
80fbbfc2ef | ||
|
|
ca45956479 | ||
|
|
3d64c42b14 | ||
|
|
97e7a12883 | ||
|
|
e4d2b646e7 | ||
|
|
a5a8f3b6ac | ||
|
|
41b0535213 | ||
|
|
a146a157b8 | ||
|
|
96fff469fc | ||
|
|
a60ff00f7e | ||
|
|
d52c326123 | ||
|
|
76882780a4 | ||
|
|
97eb19da86 | ||
|
|
4ed9d702e9 | ||
|
|
4c678836fb | ||
|
|
fd134097a1 | ||
|
|
2a87f1de28 | ||
|
|
1f60a552e5 | ||
|
|
3b34029ca2 | ||
|
|
5f6a1ef7e9 | ||
|
|
b51f65d2a4 | ||
|
|
dc9526beb3 | ||
|
|
7188089f63 |
@@ -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'
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
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>
|
|
||||||
|
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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
/>
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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: {},
|
||||||
|
|||||||
@@ -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 || ' ' }}</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: {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"> #{{ seriesSequence }}</span>
|
<span v-if="seriesSequence">#{{ seriesSequence }} </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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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))) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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']
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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', {
|
||||||
|
|||||||
@@ -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: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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 = "";
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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,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",
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">, </span></template
|
><span :key="`${series.id}-comma`" v-if="index < seriesList.length - 1">, </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({
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 }}. </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>
|
||||||
@@ -5,6 +5,12 @@ const DownloadStatus = {
|
|||||||
FAILED: 3
|
FAILED: 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SyncStatus = {
|
||||||
|
UNSET: 0,
|
||||||
|
SUCCESS: 1,
|
||||||
|
FAILED: 2
|
||||||
|
}
|
||||||
|
|
||||||
const CoverDestination = {
|
const CoverDestination = {
|
||||||
METADATA: 0,
|
METADATA: 0,
|
||||||
AUDIOBOOK: 1
|
AUDIOBOOK: 1
|
||||||
@@ -31,6 +37,7 @@ const PlayerState = {
|
|||||||
|
|
||||||
const Constants = {
|
const Constants = {
|
||||||
DownloadStatus,
|
DownloadStatus,
|
||||||
|
SyncStatus,
|
||||||
CoverDestination,
|
CoverDestination,
|
||||||
BookCoverAspectRatio,
|
BookCoverAspectRatio,
|
||||||
PlayMethod,
|
PlayMethod,
|
||||||
|
|||||||
@@ -2,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
maxWidth: {
|
maxWidth: {
|
||||||
'24': '6rem'
|
'24': '6rem'
|
||||||
|
},
|
||||||
|
minWidth: {
|
||||||
|
'12': '3rem'
|
||||||
|
},
|
||||||
|
minHeight: {
|
||||||
|
'12': '3rem'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||