Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7031c895b5 | ||
|
|
7c739bf3bf | ||
|
|
facf56930e | ||
|
|
2c3dff3544 | ||
|
|
b1bf68b8cd | ||
|
|
78aade7694 | ||
|
|
83a4474375 | ||
|
|
1b47c38efa | ||
|
|
922d9f22ce | ||
|
|
d7feb52dd8 | ||
|
|
fafaad8b5f | ||
|
|
928f33005a | ||
|
|
0448d20583 | ||
|
|
f9d408b33f | ||
|
|
9386ae95a3 | ||
|
|
c530e0f7e3 | ||
|
|
e39a5e1ba3 | ||
|
|
cb9e317e74 | ||
|
|
8061ac0ced | ||
|
|
daff8355d7 | ||
|
|
231289e9ac | ||
|
|
87df022af0 | ||
|
|
07fdac9746 | ||
|
|
0d482aaa42 | ||
|
|
d48d1fdde3 | ||
|
|
65b69cc020 | ||
|
|
a3794a6a6a | ||
|
|
695ab84940 | ||
|
|
5399ec67d7 | ||
|
|
d9a6ec65df | ||
|
|
e842de78dd | ||
|
|
260363bf4f | ||
|
|
6d7dffd4de | ||
|
|
ef1d8a8eb4 | ||
|
|
cdd7a1a636 | ||
|
|
6584cd1522 | ||
|
|
d01b602306 | ||
|
|
c90f2c136a | ||
|
|
15ccc7192e | ||
|
|
e4345a4438 | ||
|
|
3f4d564bad | ||
|
|
3311f66da8 | ||
|
|
e711ada0b3 | ||
|
|
840979d635 | ||
|
|
53c9109534 | ||
|
|
87ca6018d3 | ||
|
|
c590087578 | ||
|
|
3963ffa0e2 | ||
|
|
eaa0c9914a | ||
|
|
d9b0b8c33d | ||
|
|
fb4e7e6b55 | ||
|
|
78a063a669 | ||
|
|
cb5d32bbb9 | ||
|
|
f594b734c0 | ||
|
|
bf62b82037 | ||
|
|
39fbb0902e | ||
|
|
7818500f0f | ||
|
|
1ea984b915 | ||
|
|
f28405081a | ||
|
|
317f00e1b8 | ||
|
|
f8ce0b8355 | ||
|
|
9fd6ff9fd2 | ||
|
|
3931020c83 | ||
|
|
8bdb6b0b66 | ||
|
|
33bdee66a3 | ||
|
|
a81f50878e | ||
|
|
322f7d75dd | ||
|
|
2f34da4e99 | ||
|
|
5a9e30b08c | ||
|
|
04e233816d | ||
|
|
3ad1042b13 | ||
|
|
4fb9201823 | ||
|
|
02089e1489 | ||
|
|
da442f4a17 | ||
|
|
a0f8e6fb93 | ||
|
|
20cf5a6132 | ||
|
|
f5ca30620f | ||
|
|
72ec9d81b1 | ||
|
|
29568412e6 | ||
|
|
f94140ec6b | ||
|
|
51062e7403 | ||
|
|
0942bd2548 | ||
|
|
5939fee66f | ||
|
|
68560fd5f5 | ||
|
|
a38b826898 | ||
|
|
0158ccc4c8 | ||
|
|
a5ba6c64c9 | ||
|
|
d67454db9a | ||
|
|
fb35d8b1a0 | ||
|
|
960f75bfba | ||
|
|
83027a5787 | ||
|
|
bc6ef96465 | ||
|
|
ff77dbf3f8 | ||
|
|
3f78fb7a14 | ||
|
|
152904b7c7 | ||
|
|
9214fecc86 | ||
|
|
049397f7ab | ||
|
|
0f42a411a9 | ||
|
|
4575c61118 | ||
|
|
7575a4f883 | ||
|
|
8d4c322bce | ||
|
|
28989f536a | ||
|
|
3318f3501c | ||
|
|
c9ad0c7897 | ||
|
|
018a988124 | ||
|
|
750c370692 | ||
|
|
08ee08c698 | ||
|
|
d899fd4d89 | ||
|
|
aab44d8cee | ||
|
|
2c2fac6e34 | ||
|
|
47ccc54749 | ||
|
|
05d7d0d0d8 | ||
|
|
210fb7b88f | ||
|
|
4eeb7f24d8 | ||
|
|
9496ee1d97 | ||
|
|
f4b05a3236 | ||
|
|
b73763a9ec | ||
|
|
e3aad8c44f | ||
|
|
1e7c298aa2 | ||
|
|
b9652c7cd8 | ||
|
|
04f00d2994 | ||
|
|
3da6728f4c | ||
|
|
5704ca2ae8 | ||
|
|
6cd04e7098 | ||
|
|
290c3560d2 | ||
|
|
40bda3d48b | ||
|
|
63a838dc9d | ||
|
|
127bf7d4e0 | ||
|
|
9af4947232 | ||
|
|
3233a2a078 | ||
|
|
28ede56c7c | ||
|
|
5f6c1f57b0 | ||
|
|
ec704bf501 | ||
|
|
5e0c740f02 | ||
|
|
a185c3abe9 | ||
|
|
e8c6fc635f | ||
|
|
f362db3855 | ||
|
|
9ae584665e | ||
|
|
1aa852c3dc | ||
|
|
ef46ad7a93 | ||
|
|
cbd74d08e1 | ||
|
|
851d0357a0 | ||
|
|
fb5c23d190 | ||
|
|
1743d0e13f | ||
|
|
25af0c4170 | ||
|
|
4bcb77250d | ||
|
|
6114e03cb7 | ||
|
|
9866a787bd | ||
|
|
20e64523b2 | ||
|
|
50a53ec0d1 | ||
|
|
40b2ff5a97 | ||
|
|
48cc000b95 | ||
|
|
75aaf4d784 | ||
|
|
23220ecae6 | ||
|
|
94b4dda94f | ||
|
|
86c5d1a3e9 | ||
|
|
d7be01935f | ||
|
|
b2b647e2f7 | ||
|
|
405cd21c32 | ||
|
|
17a894f5ef | ||
|
|
23c90f4118 | ||
|
|
f215efdcd0 | ||
|
|
2f243787ce | ||
|
|
39909a398e | ||
|
|
5e98a4ff2f | ||
|
|
8710775872 | ||
|
|
99d60eaf9a | ||
|
|
55b7f05685 | ||
|
|
944aa5de66 | ||
|
|
a12eb7efdf | ||
|
|
62a5a2d069 | ||
|
|
ebd65b4fa7 | ||
|
|
152cce2e9e | ||
|
|
ea847a62ff | ||
|
|
2a5ec618f0 | ||
|
|
3fc89377b4 | ||
|
|
93c16e6a62 |
@@ -0,0 +1,81 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang=en>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv=content-type content="text/html; charset=utf-8" />
|
||||||
|
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||||
|
<title>Audiobookshelf :: Testers</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: rgb(55, 56, 56);
|
||||||
|
font-family: Sans;
|
||||||
|
margin: 40px auto;
|
||||||
|
max-width: 500px;
|
||||||
|
color: #ddd;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
#apk {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
justify-content: center;
|
||||||
|
column-gap: 15px;
|
||||||
|
text-align: left;
|
||||||
|
font-family: monospace;
|
||||||
|
background-color: #222;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<a href="https://audiobookshelf.org">
|
||||||
|
<img src=logo.png />
|
||||||
|
</a>
|
||||||
|
<h1>Audiobookshelf :: Testers</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Here you can download the latest version of Audiobookshelf for Android for <b>testing</b>.
|
||||||
|
The app is automatically built for every new patch added to the project.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div id=apk>
|
||||||
|
<div>Built on</div>
|
||||||
|
<div>__DATE__</div>
|
||||||
|
|
||||||
|
<div>Git commit</div>
|
||||||
|
<a href="https://github.com/advplyr/audiobookshelf-app/commit/__COMMIT__">__COMMIT__</a>
|
||||||
|
|
||||||
|
<div>Download</div>
|
||||||
|
<a href="__APK__">__APK__</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Using this requires some technical knowledge to install the APK manually.
|
||||||
|
Regular users should use the releases published on the <a href="https://play.google.com/store/apps/details?id=com.audiobookshelf.app">Play Store</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<p>Report bugs, request features, provide feedback, and contribute on <a href="https://github.com/advplyr/audiobookshelf-app/issues">GitHub</a></p>
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/advplyr/audiobookshelf-app/issues">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" width=32 height=32 viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
name: Build APK
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths-ignore:
|
||||||
|
- 'ios/**'
|
||||||
|
- 'readme.md'
|
||||||
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- 'ios/**'
|
||||||
|
- 'readme.md'
|
||||||
|
jobs:
|
||||||
|
main:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout sources
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: use Node.js 16.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
|
||||||
|
- name: install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: build Nuxt project
|
||||||
|
run: npm run generate
|
||||||
|
|
||||||
|
- name: copy to Android project
|
||||||
|
run: npx cap sync
|
||||||
|
|
||||||
|
- name: build Android app
|
||||||
|
run: ./android/gradlew assembleDebug -p android --no-daemon
|
||||||
|
|
||||||
|
- name: rename apk
|
||||||
|
working-directory: android/app/build/outputs/apk/debug/
|
||||||
|
run: |
|
||||||
|
build="$(date +%Y%m%d-%H%M%S)-$(git rev-parse --short HEAD)"
|
||||||
|
name="audiobookshelf-${build}.apk"
|
||||||
|
mv -v app-debug.apk "${name}"
|
||||||
|
|
||||||
|
- name: upload app
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: audiobookshelf-apk
|
||||||
|
path: android/app/build/outputs/apk/debug/*.apk
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
name: Publish Test App
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths-ignore:
|
||||||
|
- 'ios/**'
|
||||||
|
- 'readme.md'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout sources
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: use Node.js 16.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
|
||||||
|
- name: install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: build Nuxt project
|
||||||
|
run: npm run generate
|
||||||
|
|
||||||
|
- name: copy to Android project
|
||||||
|
run: npx cap sync
|
||||||
|
|
||||||
|
- name: build Android app
|
||||||
|
run: ./android/gradlew assembleDebug -p android --no-daemon
|
||||||
|
|
||||||
|
- name: rename apk
|
||||||
|
working-directory: android/app/build/outputs/apk/debug/
|
||||||
|
run: |
|
||||||
|
build="$(date +%Y%m%d-%H%M%S)-$(git rev-parse --short HEAD)"
|
||||||
|
name="audiobookshelf-${build}.apk"
|
||||||
|
mv -v app-debug.apk "${name}"
|
||||||
|
|
||||||
|
- name: prepare test page ressources
|
||||||
|
run: |
|
||||||
|
mkdir ghpages
|
||||||
|
cp android/app/build/outputs/apk/debug/*apk ghpages/
|
||||||
|
cp static/Logo.png ghpages/logo.png
|
||||||
|
cp .github/testing-page-template.html ghpages/index.html
|
||||||
|
|
||||||
|
- name: build test page
|
||||||
|
working-directory: ghpages
|
||||||
|
run: |
|
||||||
|
sed -i "s/__DATE__/$(date)/g" index.html
|
||||||
|
sed -i "s/__COMMIT__/$(git rev-parse --short HEAD)/g" index.html
|
||||||
|
sed -i "s/__APK__/$(ls *apk)/g" index.html
|
||||||
|
|
||||||
|
- name: upload test page artifact
|
||||||
|
uses: actions/upload-pages-artifact@v1
|
||||||
|
with:
|
||||||
|
path: ./ghpages
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v1
|
||||||
@@ -35,8 +35,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 92
|
versionCode 95
|
||||||
versionName "0.9.61-beta"
|
versionName "0.9.64-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.
|
||||||
@@ -74,8 +74,6 @@ configurations.all {
|
|||||||
force("androidx.media:media:$androidx_media_version")
|
force("androidx.media:media:$androidx_media_version")
|
||||||
force("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version")
|
force("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version")
|
||||||
force("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version")
|
force("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version")
|
||||||
|
|
||||||
// failOnVersionConflict()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,18 +21,21 @@
|
|||||||
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"
|
||||||
<!-- <receiver-->
|
android:largeHeap="true" >
|
||||||
<!-- android:name=".NewAppWidget"-->
|
|
||||||
<!-- android:exported="true" >-->
|
|
||||||
<!-- <intent-filter>-->
|
|
||||||
<!-- <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />-->
|
|
||||||
<!-- </intent-filter>-->
|
|
||||||
|
|
||||||
<!-- <meta-data-->
|
<!-- Android app widget -->
|
||||||
<!-- android:name="android.appwidget.provider"-->
|
<receiver
|
||||||
<!-- android:resource="@xml/new_app_widget_info" />-->
|
android:name="MediaPlayerWidget"
|
||||||
<!-- </receiver>-->
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/media_player_widget_info" />
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<!-- Used by Android Auto -->
|
<!-- Used by Android Auto -->
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -74,7 +77,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<receiver android:name="androidx.media.session.MediaButtonReceiver" android:exported="false">
|
<receiver android:name="androidx.media.session.MediaButtonReceiver" android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -83,7 +86,8 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".player.PlayerNotificationService"
|
android:name=".player.PlayerNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="true" >
|
android:exported="true"
|
||||||
|
android:foregroundServiceType="mediaPlayback">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.media.browse.MediaBrowserService" />
|
<action android:name="android.media.browse.MediaBrowserService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
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
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
import androidx.media.session.MediaButtonReceiver
|
||||||
|
import com.audiobookshelf.app.data.PlaybackSession
|
||||||
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
import com.audiobookshelf.app.device.WidgetEventEmitter
|
||||||
|
import com.audiobookshelf.app.managers.DbManager
|
||||||
|
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 MediaPlayerWidget : AppWidgetProvider() {
|
||||||
|
val tag = "MediaPlayerWidget"
|
||||||
|
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, PlayerNotificationService.isClosed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEnabled(context: Context) {
|
||||||
|
Log.i(tag, "onEnabled check context ${context.packageName}")
|
||||||
|
|
||||||
|
DbManager.initialize(context)
|
||||||
|
|
||||||
|
DeviceManager.deviceData.lastPlaybackSession?.let {
|
||||||
|
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||||
|
val componentName = ComponentName(context, MediaPlayerWidget::class.java)
|
||||||
|
val ids = appWidgetManager.getAppWidgetIds(componentName)
|
||||||
|
Log.d(tag, "Setting initial widget state with last playback session ${it.displayTitle}")
|
||||||
|
for (widgetId in ids) {
|
||||||
|
updateAppWidget(context, appWidgetManager, widgetId, it, false, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter relevant functionality for when the first widget is created
|
||||||
|
DeviceManager.widgetUpdater = (object : WidgetEventEmitter {
|
||||||
|
override fun onPlayerChanged(pns: PlayerNotificationService) {
|
||||||
|
val isPlaying = pns.currentPlayer.isPlaying
|
||||||
|
|
||||||
|
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||||
|
val componentName = ComponentName(context, MediaPlayerWidget::class.java)
|
||||||
|
val ids = appWidgetManager.getAppWidgetIds(componentName)
|
||||||
|
|
||||||
|
val playbackSession = pns.getCurrentPlaybackSessionCopy()
|
||||||
|
|
||||||
|
for (widgetId in ids) {
|
||||||
|
updateAppWidget(context, appWidgetManager, widgetId, playbackSession, isPlaying, PlayerNotificationService.isClosed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisabled(context: Context) {
|
||||||
|
// Enter relevant functionality for when the last widget is disabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, playbackSession: PlaybackSession?, isPlaying:Boolean, isAppClosed:Boolean) {
|
||||||
|
val tag = "MediaPlayerWidget"
|
||||||
|
val views = RemoteViews(context.packageName, R.layout.media_player_widget)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
val playPausePI = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE)
|
||||||
|
views.setOnClickPendingIntent(R.id.widgetPlayPauseButton, playPausePI)
|
||||||
|
|
||||||
|
val fastForwardPI = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_FAST_FORWARD)
|
||||||
|
views.setOnClickPendingIntent(R.id.widgetFastForwardButton, fastForwardPI)
|
||||||
|
|
||||||
|
val rewindPI = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_REWIND)
|
||||||
|
views.setOnClickPendingIntent(R.id.widgetRewindButton, rewindPI)
|
||||||
|
|
||||||
|
// Show/Hide button container
|
||||||
|
views.setViewVisibility(R.id.widgetButtonContainer, if (isAppClosed) View.GONE else View.VISIBLE)
|
||||||
|
|
||||||
|
views.setOnClickPendingIntent(R.id.widgetBackground, wholeWidgetClickPI)
|
||||||
|
|
||||||
|
val imageUri = playbackSession?.getCoverUri() ?: Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
|
||||||
|
val awt: AppWidgetTarget = object : AppWidgetTarget(context.applicationContext, R.id.widgetAlbumArt, views, appWidgetId) {
|
||||||
|
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
||||||
|
super.onResourceReady(resource, transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val artist = playbackSession?.displayAuthor ?: "Unknown"
|
||||||
|
views.setTextViewText(R.id.widgetArtistText, artist)
|
||||||
|
|
||||||
|
val title = playbackSession?.displayTitle ?: "Unknown"
|
||||||
|
views.setTextViewText(R.id.widgetMediaTitle, title)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
Log.i(tag, "Update App Widget | Is Playing=$isPlaying | isAppClosed=$isAppClosed")
|
||||||
|
|
||||||
|
val playPauseResource = if (isPlaying) R.drawable.ic_media_pause_dark else R.drawable.ic_media_play_dark
|
||||||
|
views.setImageViewResource(R.id.widgetPlayPauseButton, playPauseResource)
|
||||||
|
|
||||||
|
// Instruct the widget manager to update the widget
|
||||||
|
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||||
|
}
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -126,6 +126,7 @@ class Book(
|
|||||||
var audioFiles:List<AudioFile>?,
|
var audioFiles:List<AudioFile>?,
|
||||||
var chapters:List<BookChapter>?,
|
var chapters:List<BookChapter>?,
|
||||||
var tracks:MutableList<AudioTrack>?,
|
var tracks:MutableList<AudioTrack>?,
|
||||||
|
var ebookFile: EBookFile?,
|
||||||
var size:Long?,
|
var size:Long?,
|
||||||
var duration:Double?,
|
var duration:Double?,
|
||||||
var numTracks:Int?
|
var numTracks:Int?
|
||||||
@@ -179,7 +180,7 @@ class Book(
|
|||||||
}
|
}
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getLocalCopy(): Book {
|
override fun getLocalCopy(): Book {
|
||||||
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(),null,null, 0)
|
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(), ebookFile, null,null, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,20 @@ data class LocalFile(
|
|||||||
if (mimeType == "video/mp4") return true
|
if (mimeType == "video/mp4") return true
|
||||||
return mimeType?.startsWith("audio") == true
|
return mimeType?.startsWith("audio") == true
|
||||||
}
|
}
|
||||||
|
@JsonIgnore
|
||||||
|
fun isEBookFile():Boolean {
|
||||||
|
return getEBookFormat() != null
|
||||||
|
}
|
||||||
|
@JsonIgnore
|
||||||
|
fun getEBookFormat():String? {
|
||||||
|
if (mimeType == "application/epub+zip") return "epub"
|
||||||
|
if (mimeType == "application/pdf") return "pdf"
|
||||||
|
if (mimeType == "application/x-mobipocket-ebook") return "mobi"
|
||||||
|
if (mimeType == "application/vnd.comicbook+zip") return "cbz"
|
||||||
|
if (mimeType == "application/vnd.comicbook-rar") return "cbr"
|
||||||
|
if (mimeType == "application/vnd.amazon.mobi8-ebook") return "azw3"
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
@@ -74,6 +88,7 @@ open class LibraryItemWrapper(var id:String) {
|
|||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class DeviceInfo(
|
data class DeviceInfo(
|
||||||
|
var deviceId:String,
|
||||||
var manufacturer:String,
|
var manufacturer:String,
|
||||||
var model:String,
|
var model:String,
|
||||||
var brand:String,
|
var brand:String,
|
||||||
@@ -102,7 +117,11 @@ data class DeviceSettings(
|
|||||||
var autoSleepTimer: Boolean,
|
var autoSleepTimer: Boolean,
|
||||||
var autoSleepTimerStartTime: String,
|
var autoSleepTimerStartTime: String,
|
||||||
var autoSleepTimerEndTime: String,
|
var autoSleepTimerEndTime: String,
|
||||||
var sleepTimerLength: Long // Time in milliseconds
|
var autoSleepTimerAutoRewind: Boolean,
|
||||||
|
var autoSleepTimerAutoRewindTime: Long, //Time in milliseconds
|
||||||
|
var sleepTimerLength: Long, // Time in milliseconds
|
||||||
|
var disableSleepTimerFadeOut: Boolean,
|
||||||
|
var disableSleepTimerResetFeedback: Boolean
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
// Static method to get default device settings
|
// Static method to get default device settings
|
||||||
@@ -119,7 +138,11 @@ data class DeviceSettings(
|
|||||||
autoSleepTimer = false,
|
autoSleepTimer = false,
|
||||||
autoSleepTimerStartTime = "22:00",
|
autoSleepTimerStartTime = "22:00",
|
||||||
autoSleepTimerEndTime = "06:00",
|
autoSleepTimerEndTime = "06:00",
|
||||||
sleepTimerLength = 900000L // 15 minutes
|
sleepTimerLength = 900000L, // 15 minutes
|
||||||
|
autoSleepTimerAutoRewind = false,
|
||||||
|
autoSleepTimerAutoRewindTime = 300000L, // 5 minutes
|
||||||
|
disableSleepTimerFadeOut = false,
|
||||||
|
disableSleepTimerResetFeedback = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,6 +160,7 @@ data class DeviceSettings(
|
|||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val autoSleepTimerEndMinute get() = autoSleepTimerEndTime.split(":")[1].toInt()
|
val autoSleepTimerEndMinute get() = autoSleepTimerEndTime.split(":")[1].toInt()
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getShakeThresholdGravity() : Float { // Used in ShakeDetector
|
fun getShakeThresholdGravity() : Float { // Used in ShakeDetector
|
||||||
return if (shakeSensitivity == ShakeSensitivitySetting.VERY_HIGH) 1.2f
|
return if (shakeSensitivity == ShakeSensitivitySetting.VERY_HIGH) 1.2f
|
||||||
@@ -154,8 +178,8 @@ data class DeviceSettings(
|
|||||||
data class DeviceData(
|
data class DeviceData(
|
||||||
var serverConnectionConfigs:MutableList<ServerConnectionConfig>,
|
var serverConnectionConfigs:MutableList<ServerConnectionConfig>,
|
||||||
var lastServerConnectionConfigId:String?,
|
var lastServerConnectionConfigId:String?,
|
||||||
var currentLocalPlaybackSession: PlaybackSession?, // Stored to open up where left off for local media
|
var deviceSettings: DeviceSettings?,
|
||||||
var deviceSettings: DeviceSettings?
|
var lastPlaybackSession: PlaybackSession?
|
||||||
) {
|
) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getLastServerConnectionConfig(): ServerConnectionConfig? {
|
fun getLastServerConnectionConfig(): ServerConnectionConfig? {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
data class EBookFile(
|
||||||
|
var ino:String,
|
||||||
|
var metadata:FileMetadata?,
|
||||||
|
var ebookFormat:String,
|
||||||
|
var isLocal:Boolean,
|
||||||
|
var localFileId:String?,
|
||||||
|
var contentUrl:String?
|
||||||
|
)
|
||||||
@@ -5,6 +5,7 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
import androidx.media.utils.MediaConstants
|
import androidx.media.utils.MediaConstants
|
||||||
|
import com.audiobookshelf.app.BuildConfig
|
||||||
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
|
||||||
@@ -41,7 +42,7 @@ class LibraryItem(
|
|||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getCoverUri(): Uri {
|
fun getCoverUri(): Uri {
|
||||||
if (media.coverPath == null) {
|
if (media.coverPath == null) {
|
||||||
return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")
|
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import android.provider.MediaStore
|
|||||||
import android.support.v4.media.MediaDescriptionCompat
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.media.utils.MediaConstants
|
import androidx.media.utils.MediaConstants
|
||||||
|
import com.audiobookshelf.app.BuildConfig
|
||||||
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
|
||||||
@@ -46,7 +47,7 @@ class LocalLibraryItem(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getCoverUri(): Uri {
|
fun getCoverUri(): Uri {
|
||||||
return if (coverContentUrl != null) Uri.parse(coverContentUrl) else Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
return if (coverContentUrl != null) Uri.parse(coverContentUrl) else Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
@@ -82,18 +83,21 @@ class LocalLibraryItem(
|
|||||||
val mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(mediaProgressId)
|
val mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(mediaProgressId)
|
||||||
val currentTime = mediaProgress?.currentTime ?: 0.0
|
val currentTime = mediaProgress?.currentTime ?: 0.0
|
||||||
|
|
||||||
// TODO: Clean up add mediaType methods for displayTitle and displayAuthor
|
|
||||||
val mediaMetadata = media.metadata
|
val mediaMetadata = media.metadata
|
||||||
val chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf()
|
val chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf()
|
||||||
var audioTracks = media.getAudioTracks() as MutableList<AudioTrack>
|
var audioTracks = media.getAudioTracks() as MutableList<AudioTrack>
|
||||||
val authorName = mediaMetadata.getAuthorDisplayName()
|
val authorName = mediaMetadata.getAuthorDisplayName()
|
||||||
|
val displayTitle = episode?.title ?: mediaMetadata.title
|
||||||
|
var duration = getDuration()
|
||||||
if (episode != null) { // Get podcast episode audio track
|
if (episode != null) { // Get podcast episode audio track
|
||||||
episode.audioTrack?.let { at -> mutableListOf(at) }?.let { tracks -> audioTracks = tracks }
|
episode.audioTrack?.let { at -> mutableListOf(at) }?.let { tracks -> audioTracks = tracks }
|
||||||
|
duration = episode.audioTrack?.duration ?: 0.0
|
||||||
Log.d("LocalLibraryItem", "getPlaybackSession: Got podcast episode audio track ${audioTracks.size}")
|
Log.d("LocalLibraryItem", "getPlaybackSession: Got podcast episode audio track ${audioTracks.size}")
|
||||||
}
|
}
|
||||||
|
|
||||||
val dateNow = System.currentTimeMillis()
|
val dateNow = System.currentTimeMillis()
|
||||||
return PlaybackSession(sessionId,serverUserId,libraryItemId,episode?.serverEpisodeId, mediaType, mediaMetadata, chapters ?: mutableListOf(), mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL,dateNow,0L,0L, audioTracks,currentTime,null,this,localEpisodeId,serverConnectionConfigId, serverAddress, "exo-player")
|
return PlaybackSession(sessionId,serverUserId,libraryItemId,episode?.serverEpisodeId, mediaType, mediaMetadata, chapters ?: mutableListOf(), displayTitle, authorName,null,duration,PLAYMETHOD_LOCAL,dateNow,0L,0L, audioTracks,currentTime,null,this,localEpisodeId,serverConnectionConfigId, serverAddress, "exo-player")
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ data class LocalMediaItem(
|
|||||||
var basePath:String,
|
var basePath:String,
|
||||||
var absolutePath:String,
|
var absolutePath:String,
|
||||||
var audioTracks:MutableList<AudioTrack>,
|
var audioTracks:MutableList<AudioTrack>,
|
||||||
|
var ebookFile:EBookFile?,
|
||||||
var localFiles:MutableList<LocalFile>,
|
var localFiles:MutableList<LocalFile>,
|
||||||
var coverContentUrl:String?,
|
var coverContentUrl:String?,
|
||||||
var coverAbsolutePath:String?
|
var coverAbsolutePath:String?
|
||||||
@@ -61,7 +62,7 @@ data class LocalMediaItem(
|
|||||||
val mediaMetadata = getMediaMetadata()
|
val mediaMetadata = getMediaMetadata()
|
||||||
if (mediaType == "book") {
|
if (mediaType == "book") {
|
||||||
val chapters = getAudiobookChapters()
|
val chapters = getAudiobookChapters()
|
||||||
val book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration(),audioTracks.size)
|
val book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,ebookFile,getTotalSize(),getDuration(),audioTracks.size)
|
||||||
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true,null,null,null,null)
|
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true,null,null,null,null)
|
||||||
} else {
|
} else {
|
||||||
val podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false, 0)
|
val podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false, 0)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.audiobookshelf.app.data
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -14,6 +13,8 @@ class LocalMediaProgress(
|
|||||||
progress:Double, // 0 to 1
|
progress:Double, // 0 to 1
|
||||||
currentTime:Double,
|
currentTime:Double,
|
||||||
isFinished:Boolean,
|
isFinished:Boolean,
|
||||||
|
var ebookLocation:String?, // cfi tag
|
||||||
|
var ebookProgress:Double?, // 0 to 1
|
||||||
var lastUpdate:Long,
|
var lastUpdate:Long,
|
||||||
var startedAt:Long,
|
var startedAt:Long,
|
||||||
var finishedAt:Long?,
|
var finishedAt:Long?,
|
||||||
@@ -33,6 +34,11 @@ class LocalMediaProgress(
|
|||||||
if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun isMatch(mediaProgress:MediaProgress):Boolean {
|
||||||
|
if (episodeId != null) return libraryItemId == mediaProgress.libraryItemId && episodeId == mediaProgress.episodeId
|
||||||
|
return libraryItemId == mediaProgress.libraryItemId
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun updateIsFinished(finished:Boolean) {
|
fun updateIsFinished(finished:Boolean) {
|
||||||
@@ -54,11 +60,20 @@ class LocalMediaProgress(
|
|||||||
finishedAt = if (isFinished) lastUpdate else null
|
finishedAt = if (isFinished) lastUpdate else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun updateEbookProgress(ebookLocation:String, ebookProgress:Double) {
|
||||||
|
lastUpdate = System.currentTimeMillis()
|
||||||
|
this.ebookProgress = ebookProgress
|
||||||
|
this.ebookLocation = ebookLocation
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun updateFromServerMediaProgress(serverMediaProgress:MediaProgress) {
|
fun updateFromServerMediaProgress(serverMediaProgress:MediaProgress) {
|
||||||
isFinished = serverMediaProgress.isFinished
|
isFinished = serverMediaProgress.isFinished
|
||||||
progress = serverMediaProgress.progress
|
progress = serverMediaProgress.progress
|
||||||
currentTime = serverMediaProgress.currentTime
|
currentTime = serverMediaProgress.currentTime
|
||||||
|
ebookProgress = serverMediaProgress.ebookProgress
|
||||||
|
ebookLocation = serverMediaProgress.ebookLocation
|
||||||
duration = serverMediaProgress.duration
|
duration = serverMediaProgress.duration
|
||||||
lastUpdate = serverMediaProgress.lastUpdate
|
lastUpdate = serverMediaProgress.lastUpdate
|
||||||
finishedAt = serverMediaProgress.finishedAt
|
finishedAt = serverMediaProgress.finishedAt
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class MediaProgress(
|
|||||||
progress:Double, // 0 to 1
|
progress:Double, // 0 to 1
|
||||||
currentTime:Double,
|
currentTime:Double,
|
||||||
isFinished:Boolean,
|
isFinished:Boolean,
|
||||||
|
var ebookLocation:String?, // cfi tag
|
||||||
|
var ebookProgress:Double?, // 0 to 1
|
||||||
var lastUpdate:Long,
|
var lastUpdate:Long,
|
||||||
var startedAt:Long,
|
var startedAt:Long,
|
||||||
var finishedAt:Long?
|
var finishedAt:Long?
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
|
import com.audiobookshelf.app.BuildConfig
|
||||||
|
import com.audiobookshelf.app.R
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.player.MediaProgressSyncData
|
import com.audiobookshelf.app.media.MediaProgressSyncData
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
import com.google.android.exoplayer2.MediaItem
|
import com.google.android.exoplayer2.MediaItem
|
||||||
@@ -136,9 +138,9 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getCoverUri(): Uri {
|
fun getCoverUri(): Uri {
|
||||||
if (localLibraryItem?.coverContentUrl != null) return Uri.parse(localLibraryItem?.coverContentUrl) ?: Uri.parse("android.resource://com.audiobookshelf.app/" + com.audiobookshelf.app.R.drawable.icon)
|
if (localLibraryItem?.coverContentUrl != null) return Uri.parse(localLibraryItem?.coverContentUrl) ?: Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
|
||||||
|
|
||||||
if (coverPath == null) return Uri.parse("android.resource://com.audiobookshelf.app/" + com.audiobookshelf.app.R.drawable.icon)
|
if (coverPath == null) return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
|
||||||
return Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")
|
return Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +256,7 @@ class PlaybackSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun syncData(syncData:MediaProgressSyncData) {
|
fun syncData(syncData: MediaProgressSyncData) {
|
||||||
timeListening += syncData.timeListened
|
timeListening += syncData.timeListened
|
||||||
updatedAt = System.currentTimeMillis()
|
updatedAt = System.currentTimeMillis()
|
||||||
currentTime = syncData.currentTime
|
currentTime = syncData.currentTime
|
||||||
@@ -262,6 +264,6 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getNewLocalMediaProgress():LocalMediaProgress {
|
fun getNewLocalMediaProgress():LocalMediaProgress {
|
||||||
return LocalMediaProgress(localMediaProgressId,localLibraryItemId,localEpisodeId,getTotalDuration(),progress,currentTime,false,updatedAt,startedAt,null,serverConnectionConfigId,serverAddress,userId,libraryItemId,episodeId)
|
return LocalMediaProgress(localMediaProgressId,localLibraryItemId,localEpisodeId,getTotalDuration(),progress,currentTime,false,null,null,updatedAt,startedAt,null,serverConnectionConfigId,serverAddress,userId,libraryItemId,episodeId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package com.audiobookshelf.app.device
|
package com.audiobookshelf.app.device
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.managers.DbManager
|
import com.audiobookshelf.app.managers.DbManager
|
||||||
@@ -36,6 +39,10 @@ object DeviceManager {
|
|||||||
if (deviceData.deviceSettings?.shakeSensitivity == null) {
|
if (deviceData.deviceSettings?.shakeSensitivity == null) {
|
||||||
deviceData.deviceSettings?.shakeSensitivity = ShakeSensitivitySetting.MEDIUM
|
deviceData.deviceSettings?.shakeSensitivity = ShakeSensitivitySetting.MEDIUM
|
||||||
}
|
}
|
||||||
|
// Initialize auto sleep timer auto rewind added in v0.9.64
|
||||||
|
if (deviceData.deviceSettings?.autoSleepTimerAutoRewindTime == null) {
|
||||||
|
deviceData.deviceSettings?.autoSleepTimerAutoRewindTime = 300000L // 5 minutes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBase64Id(id:String):String {
|
fun getBase64Id(id:String):String {
|
||||||
@@ -46,4 +53,27 @@ object DeviceManager {
|
|||||||
if (id == null) return null
|
if (id == null) return null
|
||||||
return deviceData.serverConnectionConfigs.find { it.id == id }
|
return deviceData.serverConnectionConfigs.find { it.id == id }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun checkConnectivity(ctx:Context): Boolean {
|
||||||
|
val connectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||||
|
if (capabilities != null) {
|
||||||
|
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
|
||||||
|
Log.i("Internet", "NetworkCapabilities.TRANSPORT_CELLULAR")
|
||||||
|
return true
|
||||||
|
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
|
||||||
|
Log.i("Internet", "NetworkCapabilities.TRANSPORT_WIFI")
|
||||||
|
return true
|
||||||
|
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
|
||||||
|
Log.i("Internet", "NetworkCapabilities.TRANSPORT_ETHERNET")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLastPlaybackSession(playbackSession:PlaybackSession) {
|
||||||
|
deviceData.lastPlaybackSession = playbackSession
|
||||||
|
dbManager.saveDeviceData(deviceData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
var mediaItemsUpToDate = 0
|
var mediaItemsUpToDate = 0
|
||||||
|
|
||||||
// Search for files in media item folder
|
// Search for files in media item folder
|
||||||
val foldersFound = df.search(false, DocumentFileType.FOLDER)
|
val foldersFound = df.search(true, DocumentFileType.FOLDER)
|
||||||
|
|
||||||
// Match folders found with local library items already saved in db
|
// Match folders found with local library items already saved in db
|
||||||
var existingLocalLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id)
|
var existingLocalLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id)
|
||||||
@@ -72,11 +72,16 @@ class FolderScanner(var ctx: Context) {
|
|||||||
Log.d(tag, "Iterating over Folder Found ${itemFolder.name} | ${itemFolder.getSimplePath(ctx)} | URI: ${itemFolder.uri}")
|
Log.d(tag, "Iterating over Folder Found ${itemFolder.name} | ${itemFolder.getSimplePath(ctx)} | URI: ${itemFolder.uri}")
|
||||||
val existingItem = existingLocalLibraryItems.find { emi -> emi.id == getLocalLibraryItemId(itemFolder.id) }
|
val existingItem = existingLocalLibraryItems.find { emi -> emi.id == getLocalLibraryItemId(itemFolder.id) }
|
||||||
|
|
||||||
when (scanLibraryItemFolder(itemFolder, localFolder, existingItem, forceAudioProbe)) {
|
val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/*"))
|
||||||
ItemScanResult.REMOVED -> mediaItemsRemoved++
|
|
||||||
ItemScanResult.UPDATED -> mediaItemsUpdated++
|
// Do not scan folders that have no media items and not an existing item already
|
||||||
ItemScanResult.ADDED -> mediaItemsAdded++
|
if (existingItem != null || filesInFolder.isNotEmpty()) {
|
||||||
else -> mediaItemsUpToDate++
|
when (scanLibraryItemFolder(itemFolder, filesInFolder, localFolder, existingItem, forceAudioProbe)) {
|
||||||
|
ItemScanResult.REMOVED -> mediaItemsRemoved++
|
||||||
|
ItemScanResult.UPDATED -> mediaItemsUpdated++
|
||||||
|
ItemScanResult.ADDED -> mediaItemsAdded++
|
||||||
|
else -> mediaItemsUpToDate++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +96,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scanLibraryItemFolder(itemFolder:DocumentFile, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult {
|
private fun scanLibraryItemFolder(itemFolder:DocumentFile, filesInFolder:List<DocumentFile>, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult {
|
||||||
val itemFolderName = itemFolder.name ?: ""
|
val itemFolderName = itemFolder.name ?: ""
|
||||||
val itemId = getLocalLibraryItemId(itemFolder.id)
|
val itemId = getLocalLibraryItemId(itemFolder.id)
|
||||||
|
|
||||||
@@ -105,8 +110,8 @@ class FolderScanner(var ctx: Context) {
|
|||||||
var startOffset = 0.0
|
var startOffset = 0.0
|
||||||
var coverContentUrl:String? = null
|
var coverContentUrl:String? = null
|
||||||
var coverAbsolutePath:String? = null
|
var coverAbsolutePath:String? = null
|
||||||
|
var hasEBookFile = false
|
||||||
val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
|
var newEBookFile:EBookFile? = null
|
||||||
|
|
||||||
val existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
|
val existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
|
||||||
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
|
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
|
||||||
@@ -119,7 +124,6 @@ class FolderScanner(var ctx: Context) {
|
|||||||
filesInFolder.forEach { file ->
|
filesInFolder.forEach { file ->
|
||||||
val mimeType = file.mimeType ?: ""
|
val mimeType = file.mimeType ?: ""
|
||||||
val filename = file.name ?: ""
|
val filename = file.name ?: ""
|
||||||
val isAudio = mimeType.startsWith("audio") || mimeType == "video/mp4"
|
|
||||||
Log.d(tag, "Found $mimeType file $filename in folder $itemFolderName")
|
Log.d(tag, "Found $mimeType file $filename in folder $itemFolderName")
|
||||||
|
|
||||||
val localFileId = DeviceManager.getBase64Id(file.id)
|
val localFileId = DeviceManager.getBase64Id(file.id)
|
||||||
@@ -129,7 +133,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
|
|
||||||
Log.d(tag, "File attributes Id:${localFileId}|ContentUrl:${localFile.contentUrl}|isDownloadsDocument:${file.isDownloadsDocument}")
|
Log.d(tag, "File attributes Id:${localFileId}|ContentUrl:${localFile.contentUrl}|isDownloadsDocument:${file.isDownloadsDocument}")
|
||||||
|
|
||||||
if (isAudio) {
|
if (localFile.isAudioFile()) {
|
||||||
val audioTrackToAdd:AudioTrack?
|
val audioTrackToAdd:AudioTrack?
|
||||||
|
|
||||||
val existingAudioTrack = existingAudioTracks.find { eat -> eat.localFileId == localFileId }
|
val existingAudioTrack = existingAudioTracks.find { eat -> eat.localFileId == localFileId }
|
||||||
@@ -171,6 +175,15 @@ class FolderScanner(var ctx: Context) {
|
|||||||
startOffset += audioTrackToAdd.duration
|
startOffset += audioTrackToAdd.duration
|
||||||
index++
|
index++
|
||||||
audioTracks.add(audioTrackToAdd)
|
audioTracks.add(audioTrackToAdd)
|
||||||
|
} else if (localFile.isEBookFile()) {
|
||||||
|
val existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId }
|
||||||
|
|
||||||
|
if (localFolder.mediaType == "book") {
|
||||||
|
hasEBookFile = true
|
||||||
|
if (existingLocalFile == null) {
|
||||||
|
newEBookFile = EBookFile(localFileId, null, localFile.getEBookFormat() ?: "", true, localFileId, localFile.contentUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
val existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId }
|
val existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId }
|
||||||
|
|
||||||
@@ -195,7 +208,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingItem != null && audioTracks.isEmpty()) {
|
if (existingItem != null && audioTracks.isEmpty() && !hasEBookFile) {
|
||||||
Log.d(tag, "Local library item ${existingItem.media.metadata.title} no longer has audio tracks - removing item")
|
Log.d(tag, "Local library item ${existingItem.media.metadata.title} no longer has audio tracks - removing item")
|
||||||
DeviceManager.dbManager.removeLocalLibraryItem(existingItem.id)
|
DeviceManager.dbManager.removeLocalLibraryItem(existingItem.id)
|
||||||
return ItemScanResult.REMOVED
|
return ItemScanResult.REMOVED
|
||||||
@@ -207,9 +220,9 @@ class FolderScanner(var ctx: Context) {
|
|||||||
existingItem.updateFromScan(audioTracks,localFiles)
|
existingItem.updateFromScan(audioTracks,localFiles)
|
||||||
DeviceManager.dbManager.saveLocalLibraryItem(existingItem)
|
DeviceManager.dbManager.saveLocalLibraryItem(existingItem)
|
||||||
return ItemScanResult.UPDATED
|
return ItemScanResult.UPDATED
|
||||||
} else if (audioTracks.isNotEmpty()) {
|
} else if (audioTracks.isNotEmpty() || newEBookFile != null) {
|
||||||
Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files")
|
Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files")
|
||||||
val localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, itemFolder.uri.toString(), itemFolder.getSimplePath(ctx), itemFolder.getBasePath(ctx), itemFolder.getAbsolutePath(ctx),audioTracks,localFiles,coverContentUrl,coverAbsolutePath)
|
val localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, itemFolder.uri.toString(), itemFolder.getSimplePath(ctx), itemFolder.getBasePath(ctx), itemFolder.getAbsolutePath(ctx),audioTracks,newEBookFile,localFiles,coverContentUrl,coverAbsolutePath)
|
||||||
val localLibraryItem = localMediaItem.getLocalLibraryItem()
|
val localLibraryItem = localMediaItem.getLocalLibraryItem()
|
||||||
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
|
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
|
||||||
return ItemScanResult.ADDED
|
return ItemScanResult.ADDED
|
||||||
@@ -254,7 +267,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
|
|
||||||
// Search for files in media item folder
|
// Search for files in media item folder
|
||||||
// m4b files showing as mimeType application/octet-stream on Android 10 and earlier see #154
|
// m4b files showing as mimeType application/octet-stream on Android 10 and earlier see #154
|
||||||
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
|
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/*"))
|
||||||
Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}")
|
Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}")
|
||||||
|
|
||||||
var localEpisodeId:String? = null
|
var localEpisodeId:String? = null
|
||||||
@@ -271,6 +284,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val audioTracks:MutableList<AudioTrack> = mutableListOf()
|
val audioTracks:MutableList<AudioTrack> = mutableListOf()
|
||||||
|
var foundEBookFile = false
|
||||||
|
|
||||||
filesFound.forEach { docFile ->
|
filesFound.forEach { docFile ->
|
||||||
val itemPart = downloadItem.downloadItemParts.find { itemPart ->
|
val itemPart = downloadItem.downloadItemParts.find { itemPart ->
|
||||||
@@ -301,6 +315,16 @@ class FolderScanner(var ctx: Context) {
|
|||||||
localEpisodeId = newEpisode.id
|
localEpisodeId = newEpisode.id
|
||||||
Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}")
|
Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}")
|
||||||
}
|
}
|
||||||
|
} else if (itemPart.ebookFile != null) { // Ebook
|
||||||
|
foundEBookFile = true
|
||||||
|
Log.d(tag, "scanDownloadItem: Ebook file found with mimetype=${docFile.mimeType}")
|
||||||
|
val localFileId = DeviceManager.getBase64Id(docFile.id)
|
||||||
|
val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
|
||||||
|
localLibraryItem.localFiles.add(localFile)
|
||||||
|
|
||||||
|
val ebookFile = EBookFile(itemPart.ebookFile.ino, itemPart.ebookFile.metadata, itemPart.ebookFile.ebookFormat, true, localFileId, localFile.contentUrl)
|
||||||
|
(localLibraryItem.media as Book).ebookFile = ebookFile
|
||||||
|
Log.d(tag, "scanDownloadItem: Ebook file added to lli ${localFile.contentUrl}")
|
||||||
} else { // Cover image
|
} else { // Cover image
|
||||||
val localFileId = DeviceManager.getBase64Id(docFile.id)
|
val localFileId = DeviceManager.getBase64Id(docFile.id)
|
||||||
val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
|
val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
|
||||||
@@ -311,8 +335,8 @@ class FolderScanner(var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioTracks.isEmpty()) {
|
if (audioTracks.isEmpty() && !foundEBookFile) {
|
||||||
Log.d(tag, "scanDownloadItem did not find any audio tracks in folder for ${downloadItem.itemFolderPath}")
|
Log.d(tag, "scanDownloadItem did not find any audio tracks or ebook file in folder for ${downloadItem.itemFolderPath}")
|
||||||
return cb(null)
|
return cb(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,6 +371,8 @@ class FolderScanner(var ctx: Context) {
|
|||||||
progress = mediaProgress.progress,
|
progress = mediaProgress.progress,
|
||||||
currentTime = mediaProgress.currentTime,
|
currentTime = mediaProgress.currentTime,
|
||||||
isFinished = false,
|
isFinished = false,
|
||||||
|
ebookLocation = mediaProgress.ebookLocation,
|
||||||
|
ebookProgress = mediaProgress.ebookProgress,
|
||||||
lastUpdate = mediaProgress.lastUpdate,
|
lastUpdate = mediaProgress.lastUpdate,
|
||||||
startedAt = mediaProgress.startedAt,
|
startedAt = mediaProgress.startedAt,
|
||||||
finishedAt = mediaProgress.finishedAt,
|
finishedAt = mediaProgress.finishedAt,
|
||||||
@@ -378,7 +404,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
var wasUpdated = false
|
var wasUpdated = false
|
||||||
|
|
||||||
// Search for files in media item folder
|
// Search for files in media item folder
|
||||||
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
|
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/*"))
|
||||||
Log.d(tag, "scanLocalLibraryItem ${filesFound.size} files found in ${localLibraryItem.absolutePath}")
|
Log.d(tag, "scanLocalLibraryItem ${filesFound.size} files found in ${localLibraryItem.absolutePath}")
|
||||||
|
|
||||||
filesFound.forEach {
|
filesFound.forEach {
|
||||||
@@ -424,7 +450,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
val audioProbeResult = probeAudioFile(localFile.absolutePath)
|
val audioProbeResult = probeAudioFile(localFile.absolutePath)
|
||||||
|
|
||||||
val existingTrack = existingAudioTracks.find { audioTrack ->
|
val existingTrack = existingAudioTracks.find { audioTrack ->
|
||||||
audioTrack.localFileId == localFile.id
|
audioTrack.localFileId == localFileId
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingTrack == null) {
|
if (existingTrack == null) {
|
||||||
@@ -443,6 +469,16 @@ class FolderScanner(var ctx: Context) {
|
|||||||
|
|
||||||
wasUpdated = true
|
wasUpdated = true
|
||||||
}
|
}
|
||||||
|
} else if (localFile.isEBookFile()) {
|
||||||
|
if (localLibraryItem.mediaType == "book") {
|
||||||
|
val existingEbookFile = (localLibraryItem.media as Book).ebookFile
|
||||||
|
if (existingEbookFile == null || existingEbookFile.localFileId != localFileId) {
|
||||||
|
val ebookFile = EBookFile(localFileId, null, localFile.getEBookFormat() ?: "", true, localFileId, localFile.contentUrl)
|
||||||
|
(localLibraryItem.media as Book).ebookFile = ebookFile
|
||||||
|
Log.d(tag, "scanLocalLibraryItem: Ebook file added to lli ${localFile.contentUrl}")
|
||||||
|
wasUpdated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
} else { // Check if cover is empty
|
} else { // Check if cover is empty
|
||||||
if (localLibraryItem.coverContentUrl == null) {
|
if (localLibraryItem.coverContentUrl == null) {
|
||||||
Log.d(tag, "scanLocalLibraryItem setting cover for ${localLibraryItem.media.metadata.title}")
|
Log.d(tag, "scanLocalLibraryItem setting cover for ${localLibraryItem.media.metadata.title}")
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class DbManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getDeviceData(): DeviceData {
|
fun getDeviceData(): DeviceData {
|
||||||
return Paper.book("device").read("data") ?: DeviceData(mutableListOf(), null, null, DeviceSettings.default())
|
return Paper.book("device").read("data") ?: DeviceData(mutableListOf(), null, DeviceSettings.default(), null)
|
||||||
}
|
}
|
||||||
fun saveDeviceData(deviceData: DeviceData) {
|
fun saveDeviceData(deviceData: DeviceData) {
|
||||||
Paper.book("device").write("data", deviceData)
|
Paper.book("device").write("data", deviceData)
|
||||||
@@ -237,18 +237,26 @@ class DbManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveLocalPlaybackSession(playbackSession: PlaybackSession) {
|
|
||||||
Paper.book("localPlaybackSession").write(playbackSession.id,playbackSession)
|
|
||||||
}
|
|
||||||
fun getLocalPlaybackSession(playbackSessionId:String): PlaybackSession? {
|
|
||||||
return Paper.book("localPlaybackSession").read(playbackSessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun saveMediaItemHistory(mediaItemHistory: MediaItemHistory) {
|
fun saveMediaItemHistory(mediaItemHistory: MediaItemHistory) {
|
||||||
Paper.book("mediaItemHistory").write(mediaItemHistory.id,mediaItemHistory)
|
Paper.book("mediaItemHistory").write(mediaItemHistory.id,mediaItemHistory)
|
||||||
}
|
}
|
||||||
fun getMediaItemHistory(id:String): MediaItemHistory? {
|
fun getMediaItemHistory(id:String): MediaItemHistory? {
|
||||||
return Paper.book("mediaItemHistory").read(id)
|
return Paper.book("mediaItemHistory").read(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun savePlaybackSession(playbackSession: PlaybackSession) {
|
||||||
|
Paper.book("playbackSession").write(playbackSession.id,playbackSession)
|
||||||
|
}
|
||||||
|
fun removePlaybackSession(playbackSessionId:String) {
|
||||||
|
Paper.book("playbackSession").delete(playbackSessionId)
|
||||||
|
}
|
||||||
|
fun getPlaybackSessions():List<PlaybackSession> {
|
||||||
|
val sessions:MutableList<PlaybackSession> = mutableListOf()
|
||||||
|
Paper.book("playbackSession").allKeys.forEach { playbackSessionId ->
|
||||||
|
Paper.book("playbackSession").read<PlaybackSession>(playbackSessionId)?.let {
|
||||||
|
sessions.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sessions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class DownloadItemManager(var downloadManager:DownloadManager, private var folderScanner: FolderScanner, var mainActivity: MainActivity, private var clientEventEmitter:DownloadEventEmitter) {
|
class DownloadItemManager(var downloadManager:DownloadManager, private var folderScanner: FolderScanner, var mainActivity: MainActivity, private var clientEventEmitter:DownloadEventEmitter) {
|
||||||
val tag = "DownloadItemManager"
|
val tag = "DownloadItemManager"
|
||||||
private val maxSimultaneousDownloads = 1
|
private val maxSimultaneousDownloads = 3
|
||||||
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||||
|
|
||||||
enum class DownloadCheckStatus {
|
enum class DownloadCheckStatus {
|
||||||
@@ -188,7 +189,8 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
|
|||||||
|
|
||||||
// Rename to fix appended .mp3 on m4b/m4a files
|
// Rename to fix appended .mp3 on m4b/m4a files
|
||||||
// REF: https://github.com/anggrayudi/SimpleStorage/issues/94
|
// REF: https://github.com/anggrayudi/SimpleStorage/issues/94
|
||||||
if (resultDocFile.name?.endsWith(".m4b.mp3") == true || resultDocFile.name?.endsWith(".m4a.mp3") == true) {
|
val docNameLowerCase = resultDocFile.name?.lowercase(Locale.getDefault()) ?: ""
|
||||||
|
if (docNameLowerCase.endsWith(".m4b.mp3")|| docNameLowerCase.endsWith(".m4a.mp3")) {
|
||||||
resultDocFile.renameTo(downloadItemPart.filename)
|
resultDocFile.renameTo(downloadItemPart.filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,23 +225,27 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
|
|||||||
if (downloadItem.isDownloadFinished) {
|
if (downloadItem.isDownloadFinished) {
|
||||||
Log.i(tag, "Download Item finished ${downloadItem.media.metadata.title}")
|
Log.i(tag, "Download Item finished ${downloadItem.media.metadata.title}")
|
||||||
|
|
||||||
folderScanner.scanDownloadItem(downloadItem) { downloadItemScanResult ->
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
Log.d(tag, "Item download complete ${downloadItem.itemTitle} | local library item id: ${downloadItemScanResult?.localLibraryItem?.id}")
|
folderScanner.scanDownloadItem(downloadItem) { downloadItemScanResult ->
|
||||||
|
Log.d(tag, "Item download complete ${downloadItem.itemTitle} | local library item id: ${downloadItemScanResult?.localLibraryItem?.id}")
|
||||||
|
|
||||||
val jsobj = JSObject()
|
val jsobj = JSObject()
|
||||||
jsobj.put("libraryItemId", downloadItem.id)
|
jsobj.put("libraryItemId", downloadItem.id)
|
||||||
jsobj.put("localFolderId", downloadItem.localFolder.id)
|
jsobj.put("localFolderId", downloadItem.localFolder.id)
|
||||||
|
|
||||||
downloadItemScanResult?.localLibraryItem?.let { localLibraryItem ->
|
downloadItemScanResult?.localLibraryItem?.let { localLibraryItem ->
|
||||||
jsobj.put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
|
jsobj.put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
|
||||||
|
}
|
||||||
|
downloadItemScanResult?.localMediaProgress?.let { localMediaProgress ->
|
||||||
|
jsobj.put("localMediaProgress", JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
|
||||||
|
}
|
||||||
|
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
clientEventEmitter.onDownloadItemComplete(jsobj)
|
||||||
|
downloadItemQueue.remove(downloadItem)
|
||||||
|
DeviceManager.dbManager.removeDownloadItem(downloadItem.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
downloadItemScanResult?.localMediaProgress?.let { localMediaProgress ->
|
|
||||||
jsobj.put("localMediaProgress", JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
|
|
||||||
}
|
|
||||||
|
|
||||||
clientEventEmitter.onDownloadItemComplete(jsobj)
|
|
||||||
downloadItemQueue.remove(downloadItem)
|
|
||||||
DeviceManager.dbManager.removeDownloadItem(downloadItem.id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.audiobookshelf.app.managers
|
package com.audiobookshelf.app.managers
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.media.metrics.PlaybackSession
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
@@ -20,6 +21,9 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
private var sleepTimerLength:Long = 0L
|
private var sleepTimerLength:Long = 0L
|
||||||
private var sleepTimerElapsed:Long = 0L
|
private var sleepTimerElapsed:Long = 0L
|
||||||
private var sleepTimerFinishedAt:Long = 0L
|
private var sleepTimerFinishedAt:Long = 0L
|
||||||
|
private var isAutoSleepTimer:Boolean = false // When timer was auto-set
|
||||||
|
private var isFirstAutoSleepTimer: Boolean = true
|
||||||
|
private var sleepTimerSessionId:String = ""
|
||||||
|
|
||||||
private fun getCurrentTime():Long {
|
private fun getCurrentTime():Long {
|
||||||
return playerNotificationService.getCurrentTime()
|
return playerNotificationService.getCurrentTime()
|
||||||
@@ -54,12 +58,13 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
return (((sleepTimerEndTime - getCurrentTime()) / 1000).toDouble()).roundToInt()
|
return (((sleepTimerEndTime - getCurrentTime()) / 1000).toDouble()).roundToInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSleepTimer(time: Long, isChapterTime: Boolean) : Boolean {
|
private fun setSleepTimer(time: Long, isChapterTime: Boolean) : Boolean {
|
||||||
Log.d(tag, "Setting Sleep Timer for $time is chapter time $isChapterTime")
|
Log.d(tag, "Setting Sleep Timer for $time is chapter time $isChapterTime")
|
||||||
sleepTimerTask?.cancel()
|
sleepTimerTask?.cancel()
|
||||||
sleepTimerRunning = true
|
sleepTimerRunning = true
|
||||||
sleepTimerFinishedAt = 0L
|
sleepTimerFinishedAt = 0L
|
||||||
sleepTimerElapsed = 0L
|
sleepTimerElapsed = 0L
|
||||||
|
setVolume(1f)
|
||||||
|
|
||||||
// Register shake sensor
|
// Register shake sensor
|
||||||
playerNotificationService.registerSensor()
|
playerNotificationService.registerSensor()
|
||||||
@@ -79,13 +84,9 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
} else {
|
} else {
|
||||||
sleepTimerLength = time
|
sleepTimerLength = time
|
||||||
sleepTimerEndTime = 0L
|
sleepTimerEndTime = 0L
|
||||||
|
|
||||||
if (sleepTimerLength + getCurrentTime() > getDuration()) {
|
|
||||||
sleepTimerLength = getDuration() - getCurrentTime()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds(), isAutoSleepTimer)
|
||||||
|
|
||||||
sleepTimerTask = Timer("SleepTimer", false).schedule(0L, 1000L) {
|
sleepTimerTask = Timer("SleepTimer", false).schedule(0L, 1000L) {
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
@@ -96,7 +97,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
Log.d(tag, "Timer Elapsed $sleepTimerElapsed | Sleep TIMER time remaining $sleepTimeSecondsRemaining s")
|
Log.d(tag, "Timer Elapsed $sleepTimerElapsed | Sleep TIMER time remaining $sleepTimeSecondsRemaining s")
|
||||||
|
|
||||||
if (sleepTimeSecondsRemaining > 0) {
|
if (sleepTimeSecondsRemaining > 0) {
|
||||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(sleepTimeSecondsRemaining)
|
playerNotificationService.clientEventEmitter?.onSleepTimerSet(sleepTimeSecondsRemaining, isAutoSleepTimer)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sleepTimeSecondsRemaining <= 0) {
|
if (sleepTimeSecondsRemaining <= 0) {
|
||||||
@@ -106,11 +107,14 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
playerNotificationService.clientEventEmitter?.onSleepTimerEnded(getCurrentTime())
|
playerNotificationService.clientEventEmitter?.onSleepTimerEnded(getCurrentTime())
|
||||||
clearSleepTimer()
|
clearSleepTimer()
|
||||||
sleepTimerFinishedAt = System.currentTimeMillis()
|
sleepTimerFinishedAt = System.currentTimeMillis()
|
||||||
} else if (sleepTimeSecondsRemaining <= 60) {
|
} else if (sleepTimeSecondsRemaining <= 60 && DeviceManager.deviceData.deviceSettings?.disableSleepTimerFadeOut != true) {
|
||||||
// Start fading out audio
|
// Start fading out audio down to 10% volume
|
||||||
val volume = sleepTimeSecondsRemaining / 60F
|
val percentToReduce = 1 - (sleepTimeSecondsRemaining / 60F)
|
||||||
|
val volume = 1f - (percentToReduce * 0.9f)
|
||||||
Log.d(tag, "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining")
|
Log.d(tag, "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining")
|
||||||
setVolume(volume)
|
setVolume(volume)
|
||||||
|
} else {
|
||||||
|
setVolume(1f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,12 +122,20 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setManualSleepTimer(playbackSessionId:String, time: Long, isChapterTime:Boolean):Boolean {
|
||||||
|
sleepTimerSessionId = playbackSessionId
|
||||||
|
isAutoSleepTimer = false
|
||||||
|
return setSleepTimer(time, isChapterTime)
|
||||||
|
}
|
||||||
|
|
||||||
private fun clearSleepTimer() {
|
private fun clearSleepTimer() {
|
||||||
sleepTimerTask?.cancel()
|
sleepTimerTask?.cancel()
|
||||||
sleepTimerTask = null
|
sleepTimerTask = null
|
||||||
sleepTimerEndTime = 0
|
sleepTimerEndTime = 0
|
||||||
sleepTimerRunning = false
|
sleepTimerRunning = false
|
||||||
playerNotificationService.unregisterSensor()
|
playerNotificationService.unregisterSensor()
|
||||||
|
|
||||||
|
setVolume(1f)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSleepTimerTime():Long {
|
fun getSleepTimerTime():Long {
|
||||||
@@ -132,12 +144,21 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
|
|
||||||
fun cancelSleepTimer() {
|
fun cancelSleepTimer() {
|
||||||
Log.d(tag, "Canceling Sleep Timer")
|
Log.d(tag, "Canceling Sleep Timer")
|
||||||
|
|
||||||
|
if (isAutoSleepTimer) {
|
||||||
|
Log.i(tag, "Disabling auto sleep timer")
|
||||||
|
DeviceManager.deviceData.deviceSettings?.autoSleepTimer = false
|
||||||
|
DeviceManager.dbManager.saveDeviceData(DeviceManager.deviceData)
|
||||||
|
}
|
||||||
|
|
||||||
clearSleepTimer()
|
clearSleepTimer()
|
||||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(0)
|
playerNotificationService.clientEventEmitter?.onSleepTimerSet(0, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vibrate when extending sleep timer by shaking
|
// Vibrate when resetting sleep timer
|
||||||
private fun vibrate() {
|
private fun vibrateFeedback() {
|
||||||
|
if (DeviceManager.deviceData.deviceSettings?.disableSleepTimerResetFeedback == true) return
|
||||||
|
|
||||||
val context = playerNotificationService.getContext()
|
val context = playerNotificationService.getContext()
|
||||||
val vibrator:Vibrator
|
val vibrator:Vibrator
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
@@ -187,7 +208,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
private fun resetChapterTimer() {
|
private fun resetChapterTimer() {
|
||||||
this.getChapterEndTime()?.let { chapterEndTime ->
|
this.getChapterEndTime()?.let { chapterEndTime ->
|
||||||
Log.d(tag, "Resetting stopped sleep timer to end of chapter $chapterEndTime")
|
Log.d(tag, "Resetting stopped sleep timer to end of chapter $chapterEndTime")
|
||||||
vibrate()
|
vibrateFeedback()
|
||||||
setSleepTimer(chapterEndTime, true)
|
setSleepTimer(chapterEndTime, true)
|
||||||
play()
|
play()
|
||||||
}
|
}
|
||||||
@@ -205,6 +226,17 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Automatically Rewind in the book if settings is enabled
|
||||||
|
if (isAutoSleepTimer) {
|
||||||
|
DeviceManager.deviceData.deviceSettings?.let { deviceSettings ->
|
||||||
|
if (deviceSettings.autoSleepTimerAutoRewind && !isFirstAutoSleepTimer) {
|
||||||
|
Log.i(tag, "Auto sleep timer auto rewind seeking back ${deviceSettings.autoSleepTimerAutoRewindTime}ms")
|
||||||
|
playerNotificationService.seekBackward(deviceSettings.autoSleepTimerAutoRewindTime)
|
||||||
|
}
|
||||||
|
isFirstAutoSleepTimer = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set sleep timer
|
// Set sleep timer
|
||||||
// When sleepTimerLength is 0 then use end of chapter/track time
|
// When sleepTimerLength is 0 then use end of chapter/track time
|
||||||
if (sleepTimerLength == 0L) {
|
if (sleepTimerLength == 0L) {
|
||||||
@@ -212,7 +244,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
resetChapterTimer()
|
resetChapterTimer()
|
||||||
} else {
|
} else {
|
||||||
Log.d(tag, "Resetting stopped sleep timer to length $sleepTimerLength")
|
Log.d(tag, "Resetting stopped sleep timer to length $sleepTimerLength")
|
||||||
vibrate()
|
vibrateFeedback()
|
||||||
setSleepTimer(sleepTimerLength, false)
|
setSleepTimer(sleepTimerLength, false)
|
||||||
play()
|
play()
|
||||||
}
|
}
|
||||||
@@ -222,14 +254,14 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
// Does not apply to chapter sleep timers and timer must be running for at least 3 seconds
|
// Does not apply to chapter sleep timers and timer must be running for at least 3 seconds
|
||||||
if (sleepTimerLength > 0L && sleepTimerElapsed > 3000L) {
|
if (sleepTimerLength > 0L && sleepTimerElapsed > 3000L) {
|
||||||
Log.d(tag, "Resetting running sleep timer to length $sleepTimerLength")
|
Log.d(tag, "Resetting running sleep timer to length $sleepTimerLength")
|
||||||
vibrate()
|
vibrateFeedback()
|
||||||
setSleepTimer(sleepTimerLength, false)
|
setSleepTimer(sleepTimerLength, false)
|
||||||
} else if (sleepTimerLength == 0L) {
|
} else if (sleepTimerLength == 0L) {
|
||||||
// When navigating to previous chapters make sure this is still the end of the current chapter
|
// When navigating to previous chapters make sure this is still the end of the current chapter
|
||||||
this.getChapterEndTime()?.let { chapterEndTime ->
|
this.getChapterEndTime()?.let { chapterEndTime ->
|
||||||
if (chapterEndTime != sleepTimerEndTime) {
|
if (chapterEndTime != sleepTimerEndTime) {
|
||||||
Log.d(tag, "Resetting chapter sleep timer to end of chapter $chapterEndTime from $sleepTimerEndTime")
|
Log.d(tag, "Resetting chapter sleep timer to end of chapter $chapterEndTime from $sleepTimerEndTime")
|
||||||
vibrate()
|
vibrateFeedback()
|
||||||
setSleepTimer(chapterEndTime, true)
|
setSleepTimer(chapterEndTime, true)
|
||||||
play()
|
play()
|
||||||
}
|
}
|
||||||
@@ -264,7 +296,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
}
|
}
|
||||||
|
|
||||||
setVolume(1F)
|
setVolume(1F)
|
||||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds(), isAutoSleepTimer)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decreaseSleepTime(time: Long) {
|
fun decreaseSleepTime(time: Long) {
|
||||||
@@ -286,7 +318,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
}
|
}
|
||||||
|
|
||||||
setVolume(1F)
|
setVolume(1F)
|
||||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds(), isAutoSleepTimer)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkAutoSleepTimer() {
|
fun checkAutoSleepTimer() {
|
||||||
@@ -303,18 +335,31 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
endCalendar.set(Calendar.HOUR_OF_DAY, deviceSettings.autoSleepTimerEndHour)
|
endCalendar.set(Calendar.HOUR_OF_DAY, deviceSettings.autoSleepTimerEndHour)
|
||||||
endCalendar.set(Calendar.MINUTE, deviceSettings.autoSleepTimerEndMinute)
|
endCalendar.set(Calendar.MINUTE, deviceSettings.autoSleepTimerEndMinute)
|
||||||
|
|
||||||
// In cases where end time is earlier then start time then we add a day to end time
|
val currentCalendar = Calendar.getInstance()
|
||||||
// e.g. start time 22:00 and end time 06:00. End time will be treated as 6am the next day.
|
|
||||||
// e.g. start time 08:00 and end time 22:00. Start and end time will be the same day.
|
// In cases where end time is before start time then we shift the time window forward or backward based on the current time.
|
||||||
|
// e.g. start time 22:00 and end time 06:00.
|
||||||
|
// If current time is less than start time (e.g. 00:30) then start time will be the previous day.
|
||||||
|
// If current time is greater than start time (e.g. 23:00) then end time will be the next day.
|
||||||
if (endCalendar.before(startCalendar)) {
|
if (endCalendar.before(startCalendar)) {
|
||||||
endCalendar.add(Calendar.DAY_OF_MONTH, 1)
|
if (currentCalendar.before(startCalendar)) { // Shift start back a day
|
||||||
|
startCalendar.add(Calendar.DAY_OF_MONTH, -1)
|
||||||
|
} else { // Shift end forward a day
|
||||||
|
endCalendar.add(Calendar.DAY_OF_MONTH, 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentCalendar = Calendar.getInstance()
|
|
||||||
val currentHour = SimpleDateFormat("HH:mm", Locale.getDefault()).format(currentCalendar.time)
|
val currentHour = SimpleDateFormat("HH:mm", Locale.getDefault()).format(currentCalendar.time)
|
||||||
if (currentCalendar.after(startCalendar) && currentCalendar.before(endCalendar)) {
|
if (currentCalendar.after(startCalendar) && currentCalendar.before(endCalendar)) {
|
||||||
Log.i(tag, "Current hour $currentHour is between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime} - starting sleep timer")
|
Log.i(tag, "Current hour $currentHour is between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime} - starting sleep timer")
|
||||||
|
|
||||||
|
// Automatically Rewind in the book if settings is enabled
|
||||||
|
if (deviceSettings.autoSleepTimerAutoRewind && !isFirstAutoSleepTimer) {
|
||||||
|
Log.i(tag, "Auto sleep timer auto rewind seeking back ${deviceSettings.autoSleepTimerAutoRewindTime}ms")
|
||||||
|
playerNotificationService.seekBackward(deviceSettings.autoSleepTimerAutoRewindTime)
|
||||||
|
}
|
||||||
|
isFirstAutoSleepTimer = false
|
||||||
|
|
||||||
// Set sleep timer
|
// Set sleep timer
|
||||||
// When sleepTimerLength is 0 then use end of chapter/track time
|
// When sleepTimerLength is 0 then use end of chapter/track time
|
||||||
if (deviceSettings.sleepTimerLength == 0L) {
|
if (deviceSettings.sleepTimerLength == 0L) {
|
||||||
@@ -322,19 +367,31 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
if (chapterEndTimeMs == null) {
|
if (chapterEndTimeMs == null) {
|
||||||
Log.e(tag, "Setting auto sleep timer to end of chapter/track but there is no current session")
|
Log.e(tag, "Setting auto sleep timer to end of chapter/track but there is no current session")
|
||||||
} else {
|
} else {
|
||||||
|
isAutoSleepTimer = true
|
||||||
setSleepTimer(chapterEndTimeMs, true)
|
setSleepTimer(chapterEndTimeMs, true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
isAutoSleepTimer = true
|
||||||
setSleepTimer(deviceSettings.sleepTimerLength, false)
|
setSleepTimer(deviceSettings.sleepTimerLength, false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
isFirstAutoSleepTimer = true
|
||||||
Log.d(tag, "Current hour $currentHour is NOT between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime}")
|
Log.d(tag, "Current hour $currentHour is NOT between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleMediaPlayEvent() {
|
fun handleMediaPlayEvent(playbackSessionId:String) {
|
||||||
checkShouldResetSleepTimer()
|
// Check if the playback session has changed
|
||||||
|
// If it hasn't changed OR the sleep timer is running then check reset the timer
|
||||||
|
// e.g. You set a manual sleep timer for 10 mins, then decide to change books, the sleep timer will stay on and reset to 10 mins
|
||||||
|
if (sleepTimerSessionId == playbackSessionId || sleepTimerRunning) {
|
||||||
|
checkShouldResetSleepTimer()
|
||||||
|
} else {
|
||||||
|
isFirstAutoSleepTimer = true
|
||||||
|
}
|
||||||
|
sleepTimerSessionId = playbackSessionId
|
||||||
|
|
||||||
checkAutoSleepTimer()
|
checkAutoSleepTimer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.util.Log
|
|||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||||
import com.audiobookshelf.app.player.SyncResult
|
|
||||||
|
|
||||||
object MediaEventManager {
|
object MediaEventManager {
|
||||||
const val tag = "MediaEventManager"
|
const val tag = "MediaEventManager"
|
||||||
@@ -69,7 +68,7 @@ object MediaEventManager {
|
|||||||
clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory)
|
clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addPlaybackEvent(eventName:String, playbackSession:PlaybackSession, syncResult:SyncResult?) {
|
private fun addPlaybackEvent(eventName:String, playbackSession:PlaybackSession, syncResult: SyncResult?) {
|
||||||
val mediaItemHistory = getMediaItemHistoryMediaItem(playbackSession.mediaItemId) ?: createMediaItemHistoryForSession(playbackSession)
|
val mediaItemHistory = getMediaItemHistoryMediaItem(playbackSession.mediaItemId) ?: createMediaItemHistoryForSession(playbackSession)
|
||||||
|
|
||||||
val mediaItemEvent = MediaItemEvent(
|
val mediaItemEvent = MediaItemEvent(
|
||||||
@@ -103,8 +102,7 @@ object MediaEventManager {
|
|||||||
libraryItemId,
|
libraryItemId,
|
||||||
episodeId,
|
episodeId,
|
||||||
isLocalOnly,
|
isLocalOnly,
|
||||||
playbackSession.
|
playbackSession.serverConnectionConfigId,
|
||||||
serverConnectionConfigId,
|
|
||||||
playbackSession.serverAddress,
|
playbackSession.serverAddress,
|
||||||
playbackSession.userId,
|
playbackSession.userId,
|
||||||
createdAt = System.currentTimeMillis(),
|
createdAt = System.currentTimeMillis(),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import java.util.*
|
|||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
var serverItemsInProgress = listOf<ItemInProgress>()
|
var serverItemsInProgress = listOf<ItemInProgress>()
|
||||||
var serverLibraries = listOf<Library>()
|
var serverLibraries = listOf<Library>()
|
||||||
|
|
||||||
private var userSettingsPlaybackRate:Float? = null
|
var userSettingsPlaybackRate:Float? = null
|
||||||
|
|
||||||
fun getIsLibrary(id:String) : Boolean {
|
fun getIsLibrary(id:String) : Boolean {
|
||||||
return serverLibraries.find { it.id == id } != null
|
return serverLibraries.find { it.id == id } != null
|
||||||
@@ -61,12 +62,41 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
return 1f
|
return 1f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setSavedPlaybackRate(newRate: Float) {
|
||||||
|
val sharedPrefs = ctx.getSharedPreferences("CapacitorStorage", Activity.MODE_PRIVATE)
|
||||||
|
val sharedPrefEditor = sharedPrefs.edit()
|
||||||
|
if (sharedPrefs != null) {
|
||||||
|
val userSettingsPref = sharedPrefs.getString("userSettings", null)
|
||||||
|
if (userSettingsPref != null) {
|
||||||
|
try {
|
||||||
|
val userSettings = JSObject(userSettingsPref)
|
||||||
|
// toString().toDouble() to prevent float conversion issues (ex 1.2f becomes 1.2000000476837158d)
|
||||||
|
userSettings.put("playbackRate", newRate.toString().toDouble())
|
||||||
|
sharedPrefEditor.putString("userSettings", userSettings.toString())
|
||||||
|
sharedPrefEditor.apply()
|
||||||
|
userSettingsPlaybackRate = newRate
|
||||||
|
Log.d(tag, "Saved userSettings JSON from Android Auto with playbackRate=$newRate")
|
||||||
|
} catch(je:JSONException) {
|
||||||
|
Log.e(tag, "Failed to save userSettings JSON ${je.localizedMessage}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not sure if this is the best place for this, but if a user has not changed any user settings in the app
|
||||||
|
// the object will not exist yet, could be moved to a centralized place or created on first app load
|
||||||
|
val userSettings = JSONObject()
|
||||||
|
userSettings.put("playbackRate", newRate.toString().toDouble())
|
||||||
|
sharedPrefEditor.putString("userSettings", userSettings.toString())
|
||||||
|
userSettingsPlaybackRate = newRate
|
||||||
|
Log.d(tag, "Created and saved userSettings JSON from Android Auto with playbackRate=$newRate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun checkResetServerItems() {
|
fun checkResetServerItems() {
|
||||||
// When opening android auto need to check if still connected to server
|
// When opening android auto need to check if still connected to server
|
||||||
// and reset any server data already set
|
// and reset any server data already set
|
||||||
val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig()
|
val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig()
|
||||||
|
|
||||||
if (!DeviceManager.isConnectedToServer || !apiHandler.isOnline() || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) {
|
if (!DeviceManager.isConnectedToServer || !DeviceManager.checkConnectivity(ctx) || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) {
|
||||||
podcastEpisodeLibraryItemMap = mutableMapOf()
|
podcastEpisodeLibraryItemMap = mutableMapOf()
|
||||||
serverLibraryCategories = listOf()
|
serverLibraryCategories = listOf()
|
||||||
serverLibraries = listOf()
|
serverLibraries = listOf()
|
||||||
@@ -217,7 +247,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
Log.d(tag, "checkSetValidServerConnectionConfig | $serverConfigIdUsed")
|
Log.d(tag, "checkSetValidServerConnectionConfig | $serverConfigIdUsed")
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
if (!apiHandler.isOnline()) {
|
if (!DeviceManager.checkConnectivity(ctx)) {
|
||||||
serverUserMediaProgress = mutableListOf()
|
serverUserMediaProgress = mutableListOf()
|
||||||
cb(false)
|
cb(false)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.audiobookshelf.app.player
|
package com.audiobookshelf.app.media
|
||||||
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
@@ -6,9 +6,8 @@ import android.util.Log
|
|||||||
import com.audiobookshelf.app.data.LocalMediaProgress
|
import com.audiobookshelf.app.data.LocalMediaProgress
|
||||||
import com.audiobookshelf.app.data.MediaProgress
|
import com.audiobookshelf.app.data.MediaProgress
|
||||||
import com.audiobookshelf.app.data.PlaybackSession
|
import com.audiobookshelf.app.data.PlaybackSession
|
||||||
import com.audiobookshelf.app.data.Podcast
|
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.media.MediaEventManager
|
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
@@ -25,7 +24,7 @@ data class SyncResult(
|
|||||||
var serverSyncMessage:String?
|
var serverSyncMessage:String?
|
||||||
)
|
)
|
||||||
|
|
||||||
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 val METERED_CONNECTION_SYNC_INTERVAL = 60000
|
||||||
|
|
||||||
@@ -40,7 +39,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
|
|
||||||
private val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset"
|
private val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset"
|
||||||
private val currentIsLocal get() = currentPlaybackSession?.isLocal == true
|
private val currentIsLocal get() = currentPlaybackSession?.isLocal == true
|
||||||
val currentSessionId get() = currentPlaybackSession?.id ?: ""
|
private val currentSessionId get() = currentPlaybackSession?.id ?: ""
|
||||||
private val currentPlaybackDuration get() = currentPlaybackSession?.duration ?: 0.0
|
private val currentPlaybackDuration get() = currentPlaybackSession?.duration ?: 0.0
|
||||||
|
|
||||||
fun start(playbackSession:PlaybackSession) {
|
fun start(playbackSession:PlaybackSession) {
|
||||||
@@ -62,8 +61,8 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
|
|
||||||
listeningTimerRunning = true
|
listeningTimerRunning = true
|
||||||
lastSyncTime = System.currentTimeMillis()
|
lastSyncTime = System.currentTimeMillis()
|
||||||
Log.d(tag, "start: init last sync time $lastSyncTime")
|
|
||||||
currentPlaybackSession = playbackSession.clone()
|
currentPlaybackSession = playbackSession.clone()
|
||||||
|
Log.d(tag, "start: init last sync time $lastSyncTime with playback session id=${currentPlaybackSession?.id}")
|
||||||
|
|
||||||
listeningTimerTask = Timer("ListeningTimer", false).schedule(15000L, 15000L) {
|
listeningTimerTask = Timer("ListeningTimer", false).schedule(15000L, 15000L) {
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
@@ -199,8 +198,6 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
it.currentTime = mediaProgress.currentTime
|
it.currentTime = mediaProgress.currentTime
|
||||||
|
|
||||||
MediaEventManager.syncEvent(mediaProgress, "Received from server get media progress request while playback session open")
|
MediaEventManager.syncEvent(mediaProgress, "Received from server get media progress request while playback session open")
|
||||||
|
|
||||||
DeviceManager.dbManager.saveLocalPlaybackSession(it)
|
|
||||||
saveLocalProgress(it)
|
saveLocalProgress(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,16 +223,25 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
return cb(null)
|
return cb(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val hasNetworkConnection = DeviceManager.checkConnectivity(playerNotificationService)
|
||||||
|
|
||||||
|
// Save playback session to db (server linked sessions only)
|
||||||
|
// Sessions are removed once successfully synced with the server
|
||||||
|
currentPlaybackSession?.let {
|
||||||
|
if (!it.isLocalLibraryItemOnly) {
|
||||||
|
DeviceManager.dbManager.savePlaybackSession(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (currentIsLocal) {
|
if (currentIsLocal) {
|
||||||
// Save local progress sync
|
// Save local progress sync
|
||||||
currentPlaybackSession?.let {
|
currentPlaybackSession?.let {
|
||||||
DeviceManager.dbManager.saveLocalPlaybackSession(it)
|
|
||||||
saveLocalProgress(it)
|
saveLocalProgress(it)
|
||||||
lastSyncTime = System.currentTimeMillis()
|
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 (shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
|
if (hasNetworkConnection && shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
|
||||||
apiHandler.sendLocalProgressSync(it) { syncSuccess, errorMsg ->
|
apiHandler.sendLocalProgressSync(it) { syncSuccess, errorMsg ->
|
||||||
Log.d(
|
Log.d(
|
||||||
tag,
|
tag,
|
||||||
@@ -245,13 +251,14 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
if (syncSuccess) {
|
if (syncSuccess) {
|
||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
playerNotificationService.alertSyncSuccess()
|
playerNotificationService.alertSyncSuccess()
|
||||||
|
DeviceManager.dbManager.removePlaybackSession(it.id) // Remove session from db
|
||||||
} 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.e(tag, "Local Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
|
Log.e(tag, "Local Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime with session id=${it.id}")
|
||||||
}
|
}
|
||||||
|
|
||||||
cb(SyncResult(true, syncSuccess, errorMsg))
|
cb(SyncResult(true, syncSuccess, errorMsg))
|
||||||
@@ -260,7 +267,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
cb(SyncResult(false, null, null))
|
cb(SyncResult(false, null, null))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (shouldSyncServer) {
|
} else if (hasNetworkConnection && shouldSyncServer) {
|
||||||
Log.d(tag, "sync: currentSessionId=$currentSessionId")
|
Log.d(tag, "sync: currentSessionId=$currentSessionId")
|
||||||
apiHandler.sendProgressSync(currentSessionId, syncData) { syncSuccess, errorMsg ->
|
apiHandler.sendProgressSync(currentSessionId, syncData) { syncSuccess, errorMsg ->
|
||||||
if (syncSuccess) {
|
if (syncSuccess) {
|
||||||
@@ -268,13 +275,14 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
playerNotificationService.alertSyncSuccess()
|
playerNotificationService.alertSyncSuccess()
|
||||||
lastSyncTime = System.currentTimeMillis()
|
lastSyncTime = System.currentTimeMillis()
|
||||||
|
DeviceManager.dbManager.removePlaybackSession(currentSessionId) // Remove session from db
|
||||||
} 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.e(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 with session id=${currentSessionId}")
|
||||||
}
|
}
|
||||||
cb(SyncResult(true, syncSuccess, errorMsg))
|
cb(SyncResult(true, syncSuccess, errorMsg))
|
||||||
}
|
}
|
||||||
@@ -307,6 +315,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
currentPlaybackSession = null
|
currentPlaybackSession = null
|
||||||
currentLocalMediaProgress = null
|
currentLocalMediaProgress = null
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ data class DownloadItem(
|
|||||||
for (it in downloadItemParts) {
|
for (it in downloadItemParts) {
|
||||||
if (!it.completed && it.downloadId == null) {
|
if (!it.completed && it.downloadId == null) {
|
||||||
itemParts.add(it)
|
itemParts.add(it)
|
||||||
if (itemParts.size > limit) break
|
if (itemParts.size >= limit) break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.app.DownloadManager
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.data.AudioTrack
|
import com.audiobookshelf.app.data.AudioTrack
|
||||||
|
import com.audiobookshelf.app.data.EBookFile
|
||||||
import com.audiobookshelf.app.data.LocalFolder
|
import com.audiobookshelf.app.data.LocalFolder
|
||||||
import com.audiobookshelf.app.data.PodcastEpisode
|
import com.audiobookshelf.app.data.PodcastEpisode
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
@@ -20,6 +21,7 @@ data class DownloadItemPart(
|
|||||||
val localFolderName: String,
|
val localFolderName: String,
|
||||||
val localFolderUrl: String,
|
val localFolderUrl: String,
|
||||||
val localFolderId: String,
|
val localFolderId: String,
|
||||||
|
val ebookFile: EBookFile?,
|
||||||
val audioTrack: AudioTrack?,
|
val audioTrack: AudioTrack?,
|
||||||
val episode: PodcastEpisode?,
|
val episode: PodcastEpisode?,
|
||||||
var completed:Boolean,
|
var completed:Boolean,
|
||||||
@@ -35,12 +37,12 @@ data class DownloadItemPart(
|
|||||||
var bytesDownloaded: Long
|
var bytesDownloaded: Long
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun make(downloadItemId:String, filename:String, fileSize: Long, destinationFile: File, finalDestinationFile: File, subfolder:String, serverPath:String, localFolder: LocalFolder, audioTrack: AudioTrack?, episode: PodcastEpisode?) :DownloadItemPart {
|
fun make(downloadItemId:String, filename:String, fileSize: Long, destinationFile: File, finalDestinationFile: File, subfolder:String, serverPath:String, localFolder: LocalFolder, ebookFile: EBookFile?, audioTrack: AudioTrack?, episode: PodcastEpisode?) :DownloadItemPart {
|
||||||
val destinationUri = Uri.fromFile(destinationFile)
|
val destinationUri = Uri.fromFile(destinationFile)
|
||||||
val finalDestinationUri = Uri.fromFile(finalDestinationFile)
|
val finalDestinationUri = Uri.fromFile(finalDestinationFile)
|
||||||
|
|
||||||
var downloadUrl = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}"
|
var downloadUrl = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}"
|
||||||
if (serverPath.endsWith("/cover")) downloadUrl += "&format=jpeg" // For cover images force to jpeg
|
if (serverPath.endsWith("/cover")) downloadUrl += "&format=jpeg&raw=1" // For cover images force to jpeg
|
||||||
val downloadUri = Uri.parse(downloadUrl)
|
val downloadUri = Uri.parse(downloadUrl)
|
||||||
Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Download URI $downloadUri")
|
Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Download URI $downloadUri")
|
||||||
return DownloadItemPart(
|
return DownloadItemPart(
|
||||||
@@ -53,6 +55,7 @@ data class DownloadItemPart(
|
|||||||
localFolderName = localFolder.name,
|
localFolderName = localFolder.name,
|
||||||
localFolderUrl = localFolder.contentUrl,
|
localFolderUrl = localFolder.contentUrl,
|
||||||
localFolderId = localFolder.id,
|
localFolderId = localFolder.id,
|
||||||
|
ebookFile = ebookFile,
|
||||||
audioTrack = audioTrack,
|
audioTrack = audioTrack,
|
||||||
episode = episode,
|
episode = episode,
|
||||||
completed = false,
|
completed = false,
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.audiobookshelf.app.models
|
||||||
|
|
||||||
|
import com.audiobookshelf.app.data.MediaProgress
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
data class User(
|
||||||
|
val id:String,
|
||||||
|
val username: String,
|
||||||
|
val mediaProgress:List<MediaProgress>
|
||||||
|
)
|
||||||
@@ -8,6 +8,7 @@ import android.os.Build
|
|||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.support.v4.media.session.MediaControllerCompat
|
import android.support.v4.media.session.MediaControllerCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.audiobookshelf.app.BuildConfig
|
||||||
import com.audiobookshelf.app.R
|
import com.audiobookshelf.app.R
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
@@ -83,7 +84,7 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
|
|||||||
|
|
||||||
Glide.with(playerNotificationService)
|
Glide.with(playerNotificationService)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.load(Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon))
|
.load(Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon))
|
||||||
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
|
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
|
||||||
.get()
|
.get()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.util.Log
|
|||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import com.audiobookshelf.app.data.LibraryItemWrapper
|
import com.audiobookshelf.app.data.LibraryItemWrapper
|
||||||
import com.audiobookshelf.app.data.PodcastEpisode
|
import com.audiobookshelf.app.data.PodcastEpisode
|
||||||
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
@@ -88,6 +89,25 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
playerNotificationService.seekPlayer(pos)
|
playerNotificationService.seekPlayer(pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onChangeSpeed() {
|
||||||
|
// cycle to next speed, only contains preset android app options, as each increment needs it's own icon
|
||||||
|
// Rounding values in the event a non preset value (.5, 1, 1.2, 1.5, 2, 3) is selected in the phone app
|
||||||
|
val mediaManager = playerNotificationService.mediaManager
|
||||||
|
val newSpeed = when (mediaManager.getSavedPlaybackRate()) {
|
||||||
|
in 0.5f..0.7f -> 1.0f
|
||||||
|
in 0.8f..1.0f -> 1.2f
|
||||||
|
in 1.1f..1.2f -> 1.5f
|
||||||
|
in 1.3f..1.5f -> 2.0f
|
||||||
|
in 1.6f..2.0f -> 3.0f
|
||||||
|
in 2.1f..3.0f -> 0.5f
|
||||||
|
// anything set above 3 (can happen in the android app) will be reset to 1
|
||||||
|
else -> 1.0f
|
||||||
|
}
|
||||||
|
mediaManager.setSavedPlaybackRate(newSpeed)
|
||||||
|
playerNotificationService.setPlaybackSpeed(newSpeed)
|
||||||
|
playerNotificationService.clientEventEmitter?.onPlaybackSpeedChanged(newSpeed)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
||||||
Log.d(tag, "ON PLAY FROM MEDIA ID $mediaId")
|
Log.d(tag, "ON PLAY FROM MEDIA ID $mediaId")
|
||||||
val libraryItemWrapper: LibraryItemWrapper?
|
val libraryItemWrapper: LibraryItemWrapper?
|
||||||
@@ -138,17 +158,46 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
|
|
||||||
Log.d(tag, "handleCallMediaButton keyEvent = $keyEvent | action ${keyEvent?.action}")
|
Log.d(tag, "handleCallMediaButton keyEvent = $keyEvent | action ${keyEvent?.action}")
|
||||||
|
|
||||||
|
// Widget button intent is only sending the action down event
|
||||||
if (keyEvent?.action == KeyEvent.ACTION_DOWN) {
|
if (keyEvent?.action == KeyEvent.ACTION_DOWN) {
|
||||||
Log.d(tag, "handleCallMediaButton: key action_down for ${keyEvent.keyCode}")
|
Log.d(tag, "handleCallMediaButton: key action_down for ${keyEvent.keyCode}")
|
||||||
when (keyEvent.keyCode) {
|
when (keyEvent.keyCode) {
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
|
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
|
||||||
Log.d(tag, "handleCallMediaButton: Media Play/Pause")
|
Log.d(tag, "handleCallMediaButton: Media Play/Pause")
|
||||||
|
|
||||||
|
// TODO: Play/pause event sent from widget when app is closed. Currently the service gets destroyed before anything can happen
|
||||||
|
// if (playerNotificationService.currentPlaybackSession == null && DeviceManager.deviceData.lastPlaybackSession != null) {
|
||||||
|
// Log.i(tag, "No playback session but had one in the db")
|
||||||
|
//
|
||||||
|
// val connectionConfig = DeviceManager.deviceData.serverConnectionConfigs.find { it.id == DeviceManager.deviceData.lastPlaybackSession?.serverConnectionConfigId }
|
||||||
|
// connectionConfig?.let {
|
||||||
|
// Log.i(tag, "Setting playback session from db $it")
|
||||||
|
// DeviceManager.serverConnectionConfig = it
|
||||||
|
//
|
||||||
|
// playerNotificationService.currentPlaybackSession = DeviceManager.deviceData.lastPlaybackSession
|
||||||
|
// playerNotificationService.startNewPlaybackSession()
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
if (playerNotificationService.mPlayer.isPlaying) {
|
if (playerNotificationService.mPlayer.isPlaying) {
|
||||||
playerNotificationService.pause()
|
if (0 == mediaButtonClickCount) playerNotificationService.pause()
|
||||||
|
handleMediaButtonClickCount()
|
||||||
} else {
|
} else {
|
||||||
playerNotificationService.play()
|
if (0 == mediaButtonClickCount) {
|
||||||
|
playerNotificationService.play()
|
||||||
|
}
|
||||||
|
handleMediaButtonClickCount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
|
||||||
|
Log.d(tag, "handleCallMediaButton: Media Fast Forward")
|
||||||
|
playerNotificationService.jumpForward()
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_MEDIA_REWIND -> {
|
||||||
|
Log.d(tag, "handleCallMediaButton: Media Rewind")
|
||||||
|
playerNotificationService.jumpBackward()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,27 +232,9 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
|
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
|
||||||
playerNotificationService.jumpBackward()
|
playerNotificationService.jumpBackward()
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
|
|
||||||
playerNotificationService.jumpForward()
|
|
||||||
}
|
|
||||||
KeyEvent.KEYCODE_MEDIA_REWIND -> {
|
|
||||||
playerNotificationService.jumpBackward()
|
|
||||||
}
|
|
||||||
KeyEvent.KEYCODE_MEDIA_STOP -> {
|
KeyEvent.KEYCODE_MEDIA_STOP -> {
|
||||||
playerNotificationService.closePlayback()
|
playerNotificationService.closePlayback()
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
|
|
||||||
Log.d(tag, "handleCallMediaButton: Media Play/Pause")
|
|
||||||
if (playerNotificationService.mPlayer.isPlaying) {
|
|
||||||
if (0 == mediaButtonClickCount) playerNotificationService.pause()
|
|
||||||
handleMediaButtonClickCount()
|
|
||||||
} else {
|
|
||||||
if (0 == mediaButtonClickCount) {
|
|
||||||
playerNotificationService.play()
|
|
||||||
}
|
|
||||||
handleMediaButtonClickCount()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
else -> {
|
||||||
Log.d(tag, "KeyCode:${keyEvent.keyCode}")
|
Log.d(tag, "KeyCode:${keyEvent.keyCode}")
|
||||||
return false
|
return false
|
||||||
@@ -248,6 +279,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
CUSTOM_ACTION_JUMP_BACKWARD -> onRewind()
|
CUSTOM_ACTION_JUMP_BACKWARD -> onRewind()
|
||||||
CUSTOM_ACTION_SKIP_FORWARD -> onSkipToNext()
|
CUSTOM_ACTION_SKIP_FORWARD -> onSkipToNext()
|
||||||
CUSTOM_ACTION_SKIP_BACKWARD -> onSkipToPrevious()
|
CUSTOM_ACTION_SKIP_BACKWARD -> onSkipToPrevious()
|
||||||
|
CUSTOM_ACTION_CHANGE_SPEED -> onChangeSpeed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const val CUSTOM_ACTION_JUMP_FORWARD = "com.audiobookshelf.customAction.jump_for
|
|||||||
const val CUSTOM_ACTION_JUMP_BACKWARD = "com.audiobookshelf.customAction.jump_backward";
|
const val CUSTOM_ACTION_JUMP_BACKWARD = "com.audiobookshelf.customAction.jump_backward";
|
||||||
const val CUSTOM_ACTION_SKIP_FORWARD = "com.audiobookshelf.customAction.skip_forward";
|
const val CUSTOM_ACTION_SKIP_FORWARD = "com.audiobookshelf.customAction.skip_forward";
|
||||||
const val CUSTOM_ACTION_SKIP_BACKWARD = "com.audiobookshelf.customAction.skip_backward";
|
const val CUSTOM_ACTION_SKIP_BACKWARD = "com.audiobookshelf.customAction.skip_backward";
|
||||||
|
const val CUSTOM_ACTION_CHANGE_SPEED = "com.audiobookshelf.customAction.change_speed";
|
||||||
|
|
||||||
const val PLAYMETHOD_DIRECTPLAY = 0
|
const val PLAYMETHOD_DIRECTPLAY = 0
|
||||||
const val PLAYMETHOD_DIRECTSTREAM = 1
|
const val PLAYMETHOD_DIRECTSTREAM = 1
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import com.audiobookshelf.app.device.DeviceManager
|
|||||||
import com.google.android.exoplayer2.PlaybackException
|
import com.google.android.exoplayer2.PlaybackException
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
|
|
||||||
const val PAUSE_LEN_BEFORE_RECHECK = 30000 // 30 seconds
|
//const val PAUSE_LEN_BEFORE_RECHECK = 30000 // 30 seconds
|
||||||
|
|
||||||
class PlayerListener(var playerNotificationService:PlayerNotificationService) : Player.Listener {
|
class PlayerListener(var playerNotificationService:PlayerNotificationService) : Player.Listener {
|
||||||
var tag = "PlayerListener"
|
var tag = "PlayerListener"
|
||||||
@@ -74,12 +74,13 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
Log.d(tag, "SeekBackTime $seekBackTime")
|
Log.d(tag, "SeekBackTime $seekBackTime")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: this needs to be reworked so that the audio doesn't start playing before it checks for updated progress
|
||||||
// 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(seekBackTime)
|
// val shouldCarryOn = playerNotificationService.checkCurrentSessionProgress(seekBackTime)
|
||||||
if (!shouldCarryOn) return
|
// if (!shouldCarryOn) return
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (seekBackTime > 0L) {
|
if (seekBackTime > 0L) {
|
||||||
playerNotificationService.seekBackward(seekBackTime)
|
playerNotificationService.seekBackward(seekBackTime)
|
||||||
@@ -92,12 +93,15 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
|
|
||||||
// Start/stop progress sync interval
|
// Start/stop progress sync interval
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
// Handles auto-starting sleep timer and resetting sleep timer
|
|
||||||
playerNotificationService.sleepTimerManager.handleMediaPlayEvent()
|
|
||||||
|
|
||||||
player.volume = 1F // Volume on sleep timer might have decreased this
|
|
||||||
val playbackSession: PlaybackSession? = playerNotificationService.mediaProgressSyncer.currentPlaybackSession ?: playerNotificationService.currentPlaybackSession
|
val playbackSession: PlaybackSession? = playerNotificationService.mediaProgressSyncer.currentPlaybackSession ?: playerNotificationService.currentPlaybackSession
|
||||||
playbackSession?.let { playerNotificationService.mediaProgressSyncer.play(it) }
|
playbackSession?.let {
|
||||||
|
// Handles auto-starting sleep timer and resetting sleep timer
|
||||||
|
playerNotificationService.sleepTimerManager.handleMediaPlayEvent(it.id)
|
||||||
|
|
||||||
|
player.volume = 1F // Volume on sleep timer might have decreased this
|
||||||
|
|
||||||
|
playerNotificationService.mediaProgressSyncer.play(it)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
playerNotificationService.mediaProgressSyncer.pause {
|
playerNotificationService.mediaProgressSyncer.pause {
|
||||||
Log.d(tag, "Media Progress Syncer paused and synced")
|
Log.d(tag, "Media Progress Syncer paused and synced")
|
||||||
|
|||||||
@@ -1,21 +1,37 @@
|
|||||||
package com.audiobookshelf.app.player
|
package com.audiobookshelf.app.player
|
||||||
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
||||||
|
|
||||||
class PlayerNotificationListener(var playerNotificationService:PlayerNotificationService) : PlayerNotificationManager.NotificationListener {
|
class PlayerNotificationListener(var playerNotificationService:PlayerNotificationService) : PlayerNotificationManager.NotificationListener {
|
||||||
var tag = "PlayerNotificationListener"
|
var tag = "PlayerNotificationListener"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var isForegroundService = false
|
||||||
|
}
|
||||||
|
|
||||||
override fun onNotificationPosted(
|
override fun onNotificationPosted(
|
||||||
notificationId: Int,
|
notificationId: Int,
|
||||||
notification: Notification,
|
notification: Notification,
|
||||||
onGoing: Boolean) {
|
onGoing: Boolean) {
|
||||||
|
|
||||||
// Start foreground service
|
if (onGoing && !isForegroundService) {
|
||||||
Log.d(tag, "Notification Posted $notificationId - Start Foreground | $notification")
|
// Start foreground service
|
||||||
PlayerNotificationService.isClosed = false
|
Log.d(tag, "Notification Posted $notificationId - Start Foreground | $notification")
|
||||||
playerNotificationService.startForeground(notificationId, notification)
|
PlayerNotificationService.isClosed = false
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
playerNotificationService.startForeground(notificationId, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
|
||||||
|
} else {
|
||||||
|
playerNotificationService.startForeground(notificationId, notification)
|
||||||
|
}
|
||||||
|
isForegroundService = true
|
||||||
|
} else {
|
||||||
|
Log.d(tag, "Notification posted $notificationId, not starting foreground - onGoing=$onGoing | isForegroundService=$isForegroundService")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNotificationCancelled(
|
override fun onNotificationCancelled(
|
||||||
@@ -39,5 +55,6 @@ class PlayerNotificationListener(var playerNotificationService:PlayerNotificatio
|
|||||||
PlayerNotificationService.isSwitchingPlayer = false
|
PlayerNotificationService.isSwitchingPlayer = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
isForegroundService = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package com.audiobookshelf.app.player
|
package com.audiobookshelf.app.player
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.*
|
import android.app.*
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
@@ -14,6 +17,7 @@ import android.net.NetworkCapabilities
|
|||||||
import android.net.NetworkRequest
|
import android.net.NetworkRequest
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
|
import android.provider.Settings
|
||||||
import android.support.v4.media.MediaBrowserCompat
|
import android.support.v4.media.MediaBrowserCompat
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
@@ -21,6 +25,8 @@ import android.support.v4.media.session.MediaControllerCompat
|
|||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.RemoteViews
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@@ -34,6 +40,7 @@ import com.audiobookshelf.app.device.DeviceManager
|
|||||||
import com.audiobookshelf.app.managers.DbManager
|
import com.audiobookshelf.app.managers.DbManager
|
||||||
import com.audiobookshelf.app.managers.SleepTimerManager
|
import com.audiobookshelf.app.managers.SleepTimerManager
|
||||||
import com.audiobookshelf.app.media.MediaManager
|
import com.audiobookshelf.app.media.MediaManager
|
||||||
|
import com.audiobookshelf.app.media.MediaProgressSyncer
|
||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
import com.google.android.exoplayer2.*
|
import com.google.android.exoplayer2.*
|
||||||
import com.google.android.exoplayer2.audio.AudioAttributes
|
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||||
@@ -59,6 +66,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
var isStarted = false
|
var isStarted = false
|
||||||
var isClosed = false
|
var isClosed = false
|
||||||
var isUnmeteredNetwork = false
|
var isUnmeteredNetwork = false
|
||||||
|
var hasNetworkConnectivity = false // Not 100% reliable has internet
|
||||||
var isSwitchingPlayer = false // Used when switching between cast player and exoplayer
|
var isSwitchingPlayer = false // Used when switching between cast player and exoplayer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +78,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
fun onPlayingUpdate(isPlaying: Boolean)
|
fun onPlayingUpdate(isPlaying: Boolean)
|
||||||
fun onMetadata(metadata: PlaybackMetadata)
|
fun onMetadata(metadata: PlaybackMetadata)
|
||||||
fun onSleepTimerEnded(currentPosition: Long)
|
fun onSleepTimerEnded(currentPosition: Long)
|
||||||
fun onSleepTimerSet(sleepTimeRemaining: Int)
|
fun onSleepTimerSet(sleepTimeRemaining: Int, isAutoSleepTimer:Boolean)
|
||||||
fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress)
|
fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress)
|
||||||
fun onPlaybackFailed(errorMessage:String)
|
fun onPlaybackFailed(errorMessage:String)
|
||||||
fun onMediaPlayerChanged(mediaPlayer:String)
|
fun onMediaPlayerChanged(mediaPlayer:String)
|
||||||
@@ -78,6 +86,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
fun onProgressSyncSuccess()
|
fun onProgressSyncSuccess()
|
||||||
fun onNetworkMeteredChanged(isUnmetered:Boolean)
|
fun onNetworkMeteredChanged(isUnmetered:Boolean)
|
||||||
fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory)
|
fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory)
|
||||||
|
fun onPlaybackSpeedChanged(playbackSpeed:Float)
|
||||||
}
|
}
|
||||||
private val binder = LocalBinder()
|
private val binder = LocalBinder()
|
||||||
|
|
||||||
@@ -97,7 +106,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
var castPlayer:CastPlayer? = null
|
var castPlayer:CastPlayer? = null
|
||||||
|
|
||||||
lateinit var sleepTimerManager:SleepTimerManager
|
lateinit var sleepTimerManager:SleepTimerManager
|
||||||
lateinit var mediaProgressSyncer:MediaProgressSyncer
|
lateinit var mediaProgressSyncer: MediaProgressSyncer
|
||||||
|
|
||||||
private var notificationId = 10
|
private var notificationId = 10
|
||||||
private var channelId = "audiobookshelf_channel"
|
private var channelId = "audiobookshelf_channel"
|
||||||
@@ -167,13 +176,16 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Log.d(tag, "onDestroy")
|
Log.d(tag, "onDestroy")
|
||||||
|
isStarted = false
|
||||||
|
isClosed = true
|
||||||
|
DeviceManager.widgetUpdater?.onPlayerChanged(this)
|
||||||
|
|
||||||
playerNotificationManager.setPlayer(null)
|
playerNotificationManager.setPlayer(null)
|
||||||
mPlayer.release()
|
mPlayer.release()
|
||||||
castPlayer?.release()
|
castPlayer?.release()
|
||||||
mediaSession.release()
|
mediaSession.release()
|
||||||
mediaProgressSyncer.reset()
|
mediaProgressSyncer.reset()
|
||||||
Log.d(tag, "onDestroy")
|
|
||||||
isStarted = false
|
|
||||||
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
@@ -182,6 +194,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
super.onTaskRemoved(rootIntent)
|
super.onTaskRemoved(rootIntent)
|
||||||
Log.d(tag, "onTaskRemoved")
|
Log.d(tag, "onTaskRemoved")
|
||||||
|
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,8 +206,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
// To listen for network change from metered to unmetered
|
// To listen for network change from metered to unmetered
|
||||||
val networkRequest = NetworkRequest.Builder()
|
val networkRequest = NetworkRequest.Builder()
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||||
.build()
|
.build()
|
||||||
val connectivityManager = getSystemService(ConnectivityManager::class.java) as ConnectivityManager
|
val connectivityManager = getSystemService(ConnectivityManager::class.java) as ConnectivityManager
|
||||||
connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
|
connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
|
||||||
@@ -256,7 +269,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
playerNotificationManager.setPriority(NotificationCompat.PRIORITY_MAX)
|
playerNotificationManager.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||||
playerNotificationManager.setUseFastForwardActionInCompactView(true)
|
playerNotificationManager.setUseFastForwardActionInCompactView(true)
|
||||||
playerNotificationManager.setUseRewindActionInCompactView(true)
|
playerNotificationManager.setUseRewindActionInCompactView(true)
|
||||||
playerNotificationManager.setSmallIcon(R.drawable.exo_icon_localaudio)
|
playerNotificationManager.setSmallIcon(R.drawable.icon_monochrome)
|
||||||
|
|
||||||
// Unknown action
|
// Unknown action
|
||||||
playerNotificationManager.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE)
|
playerNotificationManager.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE)
|
||||||
@@ -378,23 +391,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isClosed = false
|
isClosed = false
|
||||||
val customActionProviders = mutableListOf(
|
|
||||||
JumpBackwardCustomActionProvider(),
|
|
||||||
JumpForwardCustomActionProvider(),
|
|
||||||
)
|
|
||||||
val metadata = playbackSession.getMediaMetadataCompat(ctx)
|
val metadata = playbackSession.getMediaMetadataCompat(ctx)
|
||||||
mediaSession.setMetadata(metadata)
|
mediaSession.setMetadata(metadata)
|
||||||
val mediaItems = playbackSession.getMediaItems()
|
val mediaItems = playbackSession.getMediaItems()
|
||||||
val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f
|
val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f
|
||||||
initialPlaybackRate = playbackRate
|
initialPlaybackRate = playbackRate
|
||||||
|
|
||||||
if (playbackSession.mediaPlayer != PLAYER_CAST && mediaItems.size > 1) {
|
// Set actions on Android Auto like jump forward/backward
|
||||||
customActionProviders.addAll(listOf(
|
setMediaSessionConnectorCustomActions(playbackSession)
|
||||||
SkipBackwardCustomActionProvider(),
|
|
||||||
SkipForwardCustomActionProvider(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
mediaSessionConnector.setCustomActionProviders(*customActionProviders.toTypedArray())
|
|
||||||
|
|
||||||
playbackSession.mediaPlayer = getMediaPlayer()
|
playbackSession.mediaPlayer = getMediaPlayer()
|
||||||
|
|
||||||
@@ -412,10 +417,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentPlaybackSession = playbackSession
|
currentPlaybackSession = playbackSession
|
||||||
Log.d(tag, "Set CurrentPlaybackSession MediaPlayer ${currentPlaybackSession?.mediaPlayer}")
|
DeviceManager.setLastPlaybackSession(playbackSession) // Save playback session to use when app is closed
|
||||||
|
|
||||||
|
Log.d(tag, "Set CurrentPlaybackSession MediaPlayer ${currentPlaybackSession?.mediaPlayer}")
|
||||||
|
// Notify client
|
||||||
clientEventEmitter?.onPlaybackSession(playbackSession)
|
clientEventEmitter?.onPlaybackSession(playbackSession)
|
||||||
|
|
||||||
|
// Update widget
|
||||||
|
DeviceManager.widgetUpdater?.onPlayerChanged(this)
|
||||||
|
|
||||||
if (mediaItems.isEmpty()) {
|
if (mediaItems.isEmpty()) {
|
||||||
Log.e(tag, "Invalid playback session no media items to play")
|
Log.e(tag, "Invalid playback session no media items to play")
|
||||||
currentPlaybackSession = null
|
currentPlaybackSession = null
|
||||||
@@ -472,6 +482,22 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setMediaSessionConnectorCustomActions(playbackSession:PlaybackSession) {
|
||||||
|
val mediaItems = playbackSession.getMediaItems()
|
||||||
|
val customActionProviders = mutableListOf(
|
||||||
|
JumpBackwardCustomActionProvider(),
|
||||||
|
JumpForwardCustomActionProvider(),
|
||||||
|
ChangePlaybackSpeedCustomActionProvider() // Will be pushed to far left
|
||||||
|
)
|
||||||
|
if (playbackSession.mediaPlayer != PLAYER_CAST && mediaItems.size > 1) {
|
||||||
|
customActionProviders.addAll(listOf(
|
||||||
|
SkipBackwardCustomActionProvider(),
|
||||||
|
SkipForwardCustomActionProvider(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
mediaSessionConnector.setCustomActionProviders(*customActionProviders.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
fun handlePlayerPlaybackError(errorMessage:String) {
|
fun handlePlayerPlaybackError(errorMessage:String) {
|
||||||
// On error and was attempting to direct play - fallback to transcode
|
// On error and was attempting to direct play - fallback to transcode
|
||||||
currentPlaybackSession?.let { playbackSession ->
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
@@ -481,16 +507,19 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
val libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
|
val libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
|
||||||
val episodeId = playbackSession.episodeId
|
val episodeId = playbackSession.episodeId
|
||||||
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
|
mediaProgressSyncer.stop(false) {
|
||||||
if (it == null) { // Play request failed
|
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
|
||||||
clientEventEmitter?.onPlaybackFailed(errorMessage)
|
if (it == null) { // Play request failed
|
||||||
closePlayback(true)
|
clientEventEmitter?.onPlaybackFailed(errorMessage)
|
||||||
} else {
|
closePlayback(true)
|
||||||
Handler(Looper.getMainLooper()).post {
|
} else {
|
||||||
preparePlayer(it, true, null)
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
preparePlayer(it, true, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
clientEventEmitter?.onPlaybackFailed(errorMessage)
|
clientEventEmitter?.onPlaybackFailed(errorMessage)
|
||||||
closePlayback(true)
|
closePlayback(true)
|
||||||
@@ -530,17 +559,22 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
fun startNewPlaybackSession() {
|
fun startNewPlaybackSession() {
|
||||||
currentPlaybackSession?.let { playbackSession ->
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
|
Log.i(tag, "Starting new playback session for ${playbackSession.displayTitle}")
|
||||||
|
|
||||||
val forceTranscode = playbackSession.isHLS // If already HLS then force
|
val forceTranscode = playbackSession.isHLS // If already HLS then force
|
||||||
val playItemRequestPayload = getPlayItemRequestPayload(forceTranscode)
|
val playItemRequestPayload = getPlayItemRequestPayload(forceTranscode)
|
||||||
|
|
||||||
val libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
|
val libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
|
||||||
val episodeId = playbackSession.episodeId
|
val episodeId = playbackSession.episodeId
|
||||||
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
|
mediaProgressSyncer.stop(false) {
|
||||||
if (it == null) {
|
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
|
||||||
Log.e(tag, "Failed to start new playback session")
|
if (it == null) {
|
||||||
} else {
|
Log.e(tag, "Failed to start new playback session")
|
||||||
Handler(Looper.getMainLooper()).post {
|
} else {
|
||||||
preparePlayer(it, true, null)
|
Log.d(tag, "New playback session response from server with session id ${it.id} for \"${it.displayTitle}\"")
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
preparePlayer(it, true, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -662,7 +696,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
if (currentPlaybackSession == null) return true
|
if (currentPlaybackSession == null) return true
|
||||||
|
|
||||||
mediaProgressSyncer.currentPlaybackSession?.let { playbackSession ->
|
mediaProgressSyncer.currentPlaybackSession?.let { playbackSession ->
|
||||||
if (!apiHandler.isOnline() || playbackSession.isLocalLibraryItemOnly) {
|
if (!DeviceManager.checkConnectivity(ctx) || playbackSession.isLocalLibraryItemOnly) {
|
||||||
return true // carry on
|
return true // carry on
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -703,7 +737,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
// Should already be playing
|
// Should already be playing
|
||||||
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
||||||
currentPlaybackSession?.let { mediaProgressSyncer.play(it) }
|
mediaProgressSyncer.currentPlaybackSession?.let { playbackSession ->
|
||||||
|
mediaProgressSyncer.play(playbackSession)
|
||||||
|
}
|
||||||
clientEventEmitter?.onPlayingUpdate(true)
|
clientEventEmitter?.onPlayingUpdate(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -733,7 +769,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
||||||
mediaProgressSyncer.play(it)
|
mediaProgressSyncer.currentPlaybackSession?.let { playbackSession ->
|
||||||
|
mediaProgressSyncer.play(playbackSession)
|
||||||
|
}
|
||||||
|
|
||||||
clientEventEmitter?.onPlayingUpdate(true)
|
clientEventEmitter?.onPlayingUpdate(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -781,6 +820,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
currentPlaybackSession?.currentTime = timeToSeek / 1000.0
|
currentPlaybackSession?.currentTime = timeToSeek / 1000.0
|
||||||
val newWindowIndex = currentPlaybackSession?.getCurrentTrackIndex() ?: 0
|
val newWindowIndex = currentPlaybackSession?.getCurrentTrackIndex() ?: 0
|
||||||
val newTimeOffset = currentPlaybackSession?.getCurrentTrackTimeMs() ?: 0
|
val newTimeOffset = currentPlaybackSession?.getCurrentTrackTimeMs() ?: 0
|
||||||
|
Log.d(tag, "seekPlayer seekTo $newWindowIndex | $newTimeOffset")
|
||||||
currentPlayer.seekTo(newWindowIndex, newTimeOffset)
|
currentPlayer.seekTo(newWindowIndex, newTimeOffset)
|
||||||
} else {
|
} else {
|
||||||
currentPlayer.seekTo(timeToSeek)
|
currentPlayer.seekTo(timeToSeek)
|
||||||
@@ -812,7 +852,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setPlaybackSpeed(speed: Float) {
|
fun setPlaybackSpeed(speed: Float) {
|
||||||
|
mediaManager.userSettingsPlaybackRate = speed
|
||||||
currentPlayer.setPlaybackSpeed(speed)
|
currentPlayer.setPlaybackSpeed(speed)
|
||||||
|
|
||||||
|
// Refresh Android Auto actions
|
||||||
|
mediaProgressSyncer.currentPlaybackSession?.let {
|
||||||
|
setMediaSessionConnectorCustomActions(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun closePlayback(calledOnError:Boolean? = false) {
|
fun closePlayback(calledOnError:Boolean? = false) {
|
||||||
@@ -849,7 +895,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
return if(currentPlayer == castPlayer) PLAYER_CAST else PLAYER_EXO
|
return if(currentPlayer == castPlayer) PLAYER_CAST else PLAYER_EXO
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDeviceInfo(): DeviceInfo {
|
@SuppressLint("HardwareIds")
|
||||||
|
private fun getDeviceInfo(): DeviceInfo {
|
||||||
/* EXAMPLE
|
/* EXAMPLE
|
||||||
manufacturer: Google
|
manufacturer: Google
|
||||||
model: Pixel 6
|
model: Pixel 6
|
||||||
@@ -857,7 +904,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
sdkVersion: 32
|
sdkVersion: 32
|
||||||
appVersion: 0.9.46-beta
|
appVersion: 0.9.46-beta
|
||||||
*/
|
*/
|
||||||
return DeviceInfo(Build.MANUFACTURER, Build.MODEL, Build.BRAND, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
|
val deviceId = Settings.Secure.getString(ctx.contentResolver, Settings.Secure.ANDROID_ID)
|
||||||
|
return DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.BRAND, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val deviceSettings get() = DeviceManager.deviceData.deviceSettings ?: DeviceSettings.default()
|
private val deviceSettings get() = DeviceManager.deviceData.deviceSettings ?: DeviceSettings.default()
|
||||||
@@ -881,7 +929,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
//
|
//
|
||||||
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
||||||
//
|
//
|
||||||
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 VALID_MEDIA_BROWSERS = mutableListOf("com.audiobookshelf.app", "com.audiobookshelf.app.debug", "com.android.systemui", 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 LIBRARIES_ROOT = "__LIBRARIES__"
|
private val LIBRARIES_ROOT = "__LIBRARIES__"
|
||||||
@@ -1086,10 +1134,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
networkCapabilities: NetworkCapabilities
|
networkCapabilities: NetworkCapabilities
|
||||||
) {
|
) {
|
||||||
super.onCapabilitiesChanged(network, networkCapabilities)
|
super.onCapabilitiesChanged(network, networkCapabilities)
|
||||||
val unmetered = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
|
||||||
Log.i(tag, "Network capabilities changed is unmetered = $unmetered")
|
isUnmeteredNetwork = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||||
isUnmeteredNetwork = unmetered
|
hasNetworkConnectivity = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
clientEventEmitter?.onNetworkMeteredChanged(unmetered)
|
Log.i(tag, "Network capabilities changed. hasNetworkConnectivity=$hasNetworkConnectivity | isUnmeteredNetwork=$isUnmeteredNetwork")
|
||||||
|
clientEventEmitter?.onNetworkMeteredChanged(isUnmeteredNetwork)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1160,5 +1209,39 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
).build()
|
).build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inner class ChangePlaybackSpeedCustomActionProvider : CustomActionProvider {
|
||||||
|
override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
|
||||||
|
/*
|
||||||
|
This does not appear to ever get called. Instead, MediaSessionCallback.onCustomAction() is
|
||||||
|
responsible to reacting to a custom action.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
|
||||||
|
val playbackRate = mediaManager.getSavedPlaybackRate()
|
||||||
|
|
||||||
|
// Rounding values in the event a non preset value (.5, 1, 1.2, 1.5, 2, 3) is selected in the phone app
|
||||||
|
val drawable: Int = when (playbackRate) {
|
||||||
|
in 0.5f..0.7f -> R.drawable.ic_play_speed_0_5x
|
||||||
|
in 0.8f..1.0f -> R.drawable.ic_play_speed_1_0x
|
||||||
|
in 1.1f..1.3f -> R.drawable.ic_play_speed_1_2x
|
||||||
|
in 1.4f..1.7f -> R.drawable.ic_play_speed_1_5x
|
||||||
|
in 1.8f..2.4f -> R.drawable.ic_play_speed_2_0x
|
||||||
|
in 2.5f..3.0f -> R.drawable.ic_play_speed_3_0x
|
||||||
|
// anything set above 3 will be show the 3x to save from creating 100 icons
|
||||||
|
else -> R.drawable.ic_play_speed_3_0x
|
||||||
|
}
|
||||||
|
val customActionExtras = Bundle()
|
||||||
|
customActionExtras.putFloat("speed", playbackRate)
|
||||||
|
return PlaybackStateCompat.CustomAction.Builder(
|
||||||
|
CUSTOM_ACTION_CHANGE_SPEED,
|
||||||
|
getContext().getString(R.string.action_change_speed),
|
||||||
|
drawable
|
||||||
|
)
|
||||||
|
.setExtras(customActionExtras)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,11 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
mainActivity = (activity as MainActivity)
|
mainActivity = (activity as MainActivity)
|
||||||
apiHandler = ApiHandler(mainActivity)
|
apiHandler = ApiHandler(mainActivity)
|
||||||
|
|
||||||
initCastManager()
|
try {
|
||||||
|
initCastManager()
|
||||||
|
} catch(e:Exception) {
|
||||||
|
Log.e(tag, "initCastManager exception ${e.printStackTrace()}")
|
||||||
|
}
|
||||||
|
|
||||||
val foregroundServiceReady : () -> Unit = {
|
val foregroundServiceReady : () -> Unit = {
|
||||||
playerNotificationService = mainActivity.foregroundService
|
playerNotificationService = mainActivity.foregroundService
|
||||||
@@ -63,8 +67,11 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
emit("onSleepTimerEnded", currentPosition)
|
emit("onSleepTimerEnded", currentPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSleepTimerSet(sleepTimeRemaining: Int) {
|
override fun onSleepTimerSet(sleepTimeRemaining: Int, isAutoSleepTimer:Boolean) {
|
||||||
emit("onSleepTimerSet", sleepTimeRemaining)
|
val ret = JSObject()
|
||||||
|
ret.put("value", sleepTimeRemaining)
|
||||||
|
ret.put("isAuto", isAutoSleepTimer)
|
||||||
|
notifyListeners("onSleepTimerSet", ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress) {
|
override fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress) {
|
||||||
@@ -94,6 +101,10 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
override fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory) {
|
override fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory) {
|
||||||
notifyListeners("onMediaItemHistoryUpdated", JSObject(jacksonMapper.writeValueAsString(mediaItemHistory)))
|
notifyListeners("onMediaItemHistoryUpdated", JSObject(jacksonMapper.writeValueAsString(mediaItemHistory)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPlaybackSpeedChanged(playbackSpeed:Float) {
|
||||||
|
emit("onPlaybackSpeedChanged", playbackSpeed)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
MediaEventManager.clientEventEmitter = playerNotificationService.clientEventEmitter
|
MediaEventManager.clientEventEmitter = playerNotificationService.clientEventEmitter
|
||||||
@@ -169,7 +180,7 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
val playWhenReady = call.getBoolean("playWhenReady") == true
|
val playWhenReady = call.getBoolean("playWhenReady") == true
|
||||||
val playbackRate = call.getFloat("playbackRate",1f) ?: 1f
|
val playbackRate = call.getFloat("playbackRate",1f) ?: 1f
|
||||||
val startTimeOverride = call.getDouble("startTime")
|
val startTimeOverride = call.getDouble("startTime")
|
||||||
Log.d(tag, "prepareLibraryItem lid=$libraryItemId, startTimeOverride=$startTimeOverride")
|
Log.d(tag, "prepareLibraryItem lid=$libraryItemId, startTimeOverride=$startTimeOverride, playbackRate=$playbackRate")
|
||||||
|
|
||||||
if (libraryItemId.isEmpty()) {
|
if (libraryItemId.isEmpty()) {
|
||||||
Log.e(tag, "Invalid call to play library item no library item id")
|
Log.e(tag, "Invalid call to play library item no library item id")
|
||||||
@@ -330,7 +341,8 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
val isChapterTime:Boolean = call.getBoolean("isChapterTime", false) == true
|
val isChapterTime:Boolean = call.getBoolean("isChapterTime", false) == true
|
||||||
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
val success:Boolean = playerNotificationService.sleepTimerManager.setSleepTimer(time, isChapterTime)
|
val playbackSession: PlaybackSession? = playerNotificationService.mediaProgressSyncer.currentPlaybackSession ?: playerNotificationService.currentPlaybackSession
|
||||||
|
val success:Boolean = playerNotificationService.sleepTimerManager.setManualSleepTimer(playbackSession?.id ?: "", time, isChapterTime)
|
||||||
val ret = JSObject()
|
val ret = JSObject()
|
||||||
ret.put("success", success)
|
ret.put("success", success)
|
||||||
call.resolve(ret)
|
call.resolve(ret)
|
||||||
@@ -371,7 +383,9 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun cancelSleepTimer(call: PluginCall) {
|
fun cancelSleepTimer(call: PluginCall) {
|
||||||
playerNotificationService.sleepTimerManager.cancelSleepTimer()
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
playerNotificationService.sleepTimerManager.cancelSleepTimer()
|
||||||
|
}
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -216,13 +216,25 @@ class AbsDatabase : Plugin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun syncLocalMediaProgressWithServer(call:PluginCall) {
|
fun syncLocalSessionsWithServer(call:PluginCall) {
|
||||||
if (DeviceManager.serverConnectionConfig == null) {
|
if (DeviceManager.serverConnectionConfig == null) {
|
||||||
Log.e(tag, "syncLocalMediaProgressWithServer not connected to server")
|
Log.e(tag, "syncLocalSessionsWithServer not connected to server")
|
||||||
return call.resolve()
|
return call.resolve()
|
||||||
}
|
}
|
||||||
apiHandler.syncMediaProgress {
|
|
||||||
call.resolve(JSObject(jacksonMapper.writeValueAsString(it)))
|
apiHandler.syncLocalMediaProgressForUser {
|
||||||
|
Log.d(tag, "Finished syncing local media progress for user")
|
||||||
|
val savedSessions = DeviceManager.dbManager.getPlaybackSessions().filter { it.serverConnectionConfigId == DeviceManager.serverConnectionConfigId }
|
||||||
|
|
||||||
|
if (savedSessions.isNotEmpty()) {
|
||||||
|
apiHandler.sendSyncLocalSessions(savedSessions) { success, errorMsg ->
|
||||||
|
if (!success) {
|
||||||
|
call.resolve(JSObject("{\"error\":\"$errorMsg\"}"))
|
||||||
|
} else {
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +265,8 @@ class AbsDatabase : Plugin() {
|
|||||||
progress = mediaProgress.progress,
|
progress = mediaProgress.progress,
|
||||||
currentTime = mediaProgress.currentTime,
|
currentTime = mediaProgress.currentTime,
|
||||||
isFinished = mediaProgress.isFinished,
|
isFinished = mediaProgress.isFinished,
|
||||||
|
ebookLocation = mediaProgress.ebookLocation,
|
||||||
|
ebookProgress = mediaProgress.ebookProgress,
|
||||||
lastUpdate = mediaProgress.lastUpdate,
|
lastUpdate = mediaProgress.lastUpdate,
|
||||||
startedAt = mediaProgress.startedAt,
|
startedAt = mediaProgress.startedAt,
|
||||||
finishedAt = mediaProgress.finishedAt,
|
finishedAt = mediaProgress.finishedAt,
|
||||||
@@ -333,6 +347,8 @@ class AbsDatabase : Plugin() {
|
|||||||
progress = if (isFinished) 1.0 else 0.0,
|
progress = if (isFinished) 1.0 else 0.0,
|
||||||
currentTime = 0.0,
|
currentTime = 0.0,
|
||||||
isFinished = isFinished,
|
isFinished = isFinished,
|
||||||
|
ebookLocation = null,
|
||||||
|
ebookProgress = null,
|
||||||
lastUpdate = currentTime,
|
lastUpdate = currentTime,
|
||||||
startedAt = if (isFinished) currentTime else 0L,
|
startedAt = if (isFinished) currentTime else 0L,
|
||||||
finishedAt = if (isFinished) currentTime else null,
|
finishedAt = if (isFinished) currentTime else null,
|
||||||
@@ -377,6 +393,55 @@ class AbsDatabase : Plugin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun updateLocalEbookProgress(call:PluginCall) {
|
||||||
|
val localLibraryItemId = call.getString("localLibraryItemId", "").toString()
|
||||||
|
val ebookLocation = call.getString("ebookLocation", "").toString()
|
||||||
|
val ebookProgress = call.getDouble("ebookProgress") ?: 0.0
|
||||||
|
|
||||||
|
val localMediaProgressId = localLibraryItemId
|
||||||
|
var localMediaProgress = DeviceManager.dbManager.getLocalMediaProgress(localMediaProgressId)
|
||||||
|
|
||||||
|
if (localMediaProgress == null) {
|
||||||
|
Log.d(tag, "updateLocalEbookProgress Local Media Progress not found $localMediaProgressId - Creating new")
|
||||||
|
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
||||||
|
?: return call.resolve(JSObject("{\"error\":\"Library Item not found\"}"))
|
||||||
|
|
||||||
|
val book = localLibraryItem.media as Book
|
||||||
|
|
||||||
|
localMediaProgress = LocalMediaProgress(
|
||||||
|
id = localMediaProgressId,
|
||||||
|
localLibraryItemId = localLibraryItemId,
|
||||||
|
localEpisodeId = null,
|
||||||
|
duration = book.duration ?: 0.0,
|
||||||
|
progress = 0.0,
|
||||||
|
currentTime = 0.0,
|
||||||
|
isFinished = false,
|
||||||
|
ebookLocation = ebookLocation,
|
||||||
|
ebookProgress = ebookProgress,
|
||||||
|
lastUpdate = System.currentTimeMillis(),
|
||||||
|
startedAt = 0L,
|
||||||
|
finishedAt = null,
|
||||||
|
serverConnectionConfigId = localLibraryItem.serverConnectionConfigId,
|
||||||
|
serverAddress = localLibraryItem.serverAddress,
|
||||||
|
serverUserId = localLibraryItem.serverUserId,
|
||||||
|
libraryItemId = localLibraryItem.libraryItemId,
|
||||||
|
episodeId = null)
|
||||||
|
} else {
|
||||||
|
localMediaProgress.updateEbookProgress(ebookLocation, ebookProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save local media progress locally
|
||||||
|
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
|
||||||
|
|
||||||
|
val lmpstring = jacksonMapper.writeValueAsString(localMediaProgress)
|
||||||
|
Log.d(tag, "updateLocalEbookProgress: Local Media Progress String $lmpstring")
|
||||||
|
|
||||||
|
val jsobj = JSObject()
|
||||||
|
jsobj.put("localMediaProgress", JSObject(lmpstring))
|
||||||
|
call.resolve(jsobj)
|
||||||
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun updateLocalTrackOrder(call:PluginCall) {
|
fun updateLocalTrackOrder(call:PluginCall) {
|
||||||
val localLibraryItemId = call.getString("localLibraryItemId", "") ?: ""
|
val localLibraryItemId = call.getString("localLibraryItemId", "") ?: ""
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.audiobookshelf.app.plugins
|
|||||||
|
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.MainActivity
|
import com.audiobookshelf.app.MainActivity
|
||||||
@@ -51,7 +50,6 @@ class AbsDownloader : Plugin() {
|
|||||||
folderScanner = FolderScanner(mainActivity)
|
folderScanner = FolderScanner(mainActivity)
|
||||||
apiHandler = ApiHandler(mainActivity)
|
apiHandler = ApiHandler(mainActivity)
|
||||||
downloadItemManager = DownloadItemManager(downloadManager, folderScanner, mainActivity, clientEventEmitter)
|
downloadItemManager = DownloadItemManager(downloadManager, folderScanner, mainActivity, clientEventEmitter)
|
||||||
Log.d(tag, "Build SDK ${Build.VERSION.SDK_INT}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
@@ -143,6 +141,28 @@ class AbsDownloader : Plugin() {
|
|||||||
val itemFolderPath = "${localFolder.absolutePath}/$itemSubfolder"
|
val itemFolderPath = "${localFolder.absolutePath}/$itemSubfolder"
|
||||||
val downloadItem = DownloadItem(libraryItem.id, libraryItem.id, null, libraryItem.userMediaProgress,DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, itemSubfolder, libraryItem.media, mutableListOf())
|
val downloadItem = DownloadItem(libraryItem.id, libraryItem.id, null, libraryItem.userMediaProgress,DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, itemSubfolder, libraryItem.media, mutableListOf())
|
||||||
|
|
||||||
|
val book = libraryItem.media as Book
|
||||||
|
book.ebookFile?.let { ebookFile ->
|
||||||
|
val fileSize = ebookFile.metadata?.size ?: 0
|
||||||
|
val serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(ebookFile.metadata?.relPath ?: "")}"
|
||||||
|
val destinationFilename = getFilenameFromRelPath(ebookFile.metadata?.relPath ?: "")
|
||||||
|
val finalDestinationFile = File("$itemFolderPath/$destinationFilename")
|
||||||
|
val destinationFile = File("$tempFolderPath/$destinationFilename")
|
||||||
|
|
||||||
|
if (destinationFile.exists()) {
|
||||||
|
Log.d(tag, "TEMP ebook file already exists, removing it from ${destinationFile.absolutePath}")
|
||||||
|
destinationFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalDestinationFile.exists()) {
|
||||||
|
Log.d(tag, "ebook file already exists, removing it from ${finalDestinationFile.absolutePath}")
|
||||||
|
finalDestinationFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,ebookFile,null,null)
|
||||||
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
|
}
|
||||||
|
|
||||||
// Create download item part for each audio track
|
// Create download item part for each audio track
|
||||||
tracks.forEach { audioTrack ->
|
tracks.forEach { audioTrack ->
|
||||||
val fileSize = audioTrack.metadata?.size ?: 0
|
val fileSize = audioTrack.metadata?.size ?: 0
|
||||||
@@ -163,7 +183,7 @@ class AbsDownloader : Plugin() {
|
|||||||
finalDestinationFile.delete()
|
finalDestinationFile.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,audioTrack,null)
|
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,audioTrack,null)
|
||||||
downloadItem.downloadItemParts.add(downloadItemPart)
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +208,7 @@ class AbsDownloader : Plugin() {
|
|||||||
finalDestinationFile.delete()
|
finalDestinationFile.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, coverFileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,null)
|
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, coverFileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,null,null)
|
||||||
downloadItem.downloadItemParts.add(downloadItemPart)
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +237,7 @@ class AbsDownloader : Plugin() {
|
|||||||
finalDestinationFile.delete()
|
finalDestinationFile.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
var downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,fileSize, destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,episode)
|
var downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,fileSize, destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,audioTrack,episode)
|
||||||
downloadItem.downloadItemParts.add(downloadItemPart)
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
|
|
||||||
if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) {
|
if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) {
|
||||||
@@ -233,7 +253,7 @@ class AbsDownloader : Plugin() {
|
|||||||
if (finalDestinationFile.exists()) {
|
if (finalDestinationFile.exists()) {
|
||||||
Log.d(tag, "Podcast cover already exists - not downloading cover again")
|
Log.d(tag, "Podcast cover already exists - not downloading cover again")
|
||||||
} else {
|
} else {
|
||||||
downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,coverFileSize,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null)
|
downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,coverFileSize,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null,null)
|
||||||
downloadItem.downloadItemParts.add(downloadItemPart)
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import com.getcapacitor.annotation.CapacitorPlugin
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
@CapacitorPlugin(name = "AbsFileSystem")
|
@CapacitorPlugin(name = "AbsFileSystem")
|
||||||
class AbsFileSystem : Plugin() {
|
class AbsFileSystem : Plugin() {
|
||||||
@@ -226,8 +227,37 @@ class AbsFileSystem : Plugin() {
|
|||||||
val contentUrl = call.data.getString("contentUrl", "").toString()
|
val contentUrl = call.data.getString("contentUrl", "").toString()
|
||||||
Log.d(tag, "deleteItem $absolutePath | $contentUrl")
|
Log.d(tag, "deleteItem $absolutePath | $contentUrl")
|
||||||
|
|
||||||
|
var subfolderPathToDelete = ""
|
||||||
|
|
||||||
|
// Check if should delete subfolder
|
||||||
|
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
||||||
|
localLibraryItem?.folderId?.let { folderId ->
|
||||||
|
val folder = DeviceManager.dbManager.getLocalFolder(folderId)
|
||||||
|
folder?.absolutePath?.let { folderPath ->
|
||||||
|
val splitAbsolutePath = absolutePath.split("/")
|
||||||
|
val fullSubDir = splitAbsolutePath.subList(0, splitAbsolutePath.size - 1).joinToString("/")
|
||||||
|
if (fullSubDir != folderPath) {
|
||||||
|
val subdirHasAnItem = DeviceManager.dbManager.getLocalLibraryItems().any { _localLibraryItem ->
|
||||||
|
if (_localLibraryItem.id == localLibraryItemId) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
_localLibraryItem.absolutePath.startsWith(fullSubDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subfolderPathToDelete = if (subdirHasAnItem) "" else fullSubDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val docfile = DocumentFileCompat.fromUri(mainActivity, Uri.parse(contentUrl))
|
val docfile = DocumentFileCompat.fromUri(mainActivity, Uri.parse(contentUrl))
|
||||||
val success = docfile?.delete() == true
|
val success = docfile?.delete() == true
|
||||||
|
|
||||||
|
if (subfolderPathToDelete != "") {
|
||||||
|
Log.d(tag, "Deleting empty subfolder at $subfolderPathToDelete")
|
||||||
|
val docfilesub = DocumentFileCompat.fromFullPath(mainActivity, subfolderPathToDelete)
|
||||||
|
docfilesub?.delete()
|
||||||
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
DeviceManager.dbManager.removeLocalLibraryItem(localLibraryItemId)
|
DeviceManager.dbManager.removeLocalLibraryItem(localLibraryItemId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package com.audiobookshelf.app.server
|
package com.audiobookshelf.app.server
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.media.MediaEventManager
|
import com.audiobookshelf.app.media.MediaEventManager
|
||||||
import com.audiobookshelf.app.player.MediaProgressSyncData
|
import com.audiobookshelf.app.media.MediaProgressSyncData
|
||||||
|
import com.audiobookshelf.app.media.SyncResult
|
||||||
|
import com.audiobookshelf.app.models.User
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
import com.fasterxml.jackson.core.json.JsonReadFeature
|
import com.fasterxml.jackson.core.json.JsonReadFeature
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
@@ -28,14 +28,14 @@ class ApiHandler(var ctx:Context) {
|
|||||||
|
|
||||||
private var defaultClient = OkHttpClient()
|
private var defaultClient = OkHttpClient()
|
||||||
private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build()
|
private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build()
|
||||||
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||||
|
|
||||||
data class LocalMediaProgressSyncPayload(val localMediaProgress:List<LocalMediaProgress>)
|
data class LocalSessionsSyncRequestPayload(val sessions:List<PlaybackSession>)
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class MediaProgressSyncResponsePayload(val numServerProgressUpdates:Int, val localProgressUpdates:List<LocalMediaProgress>, val serverProgressUpdates:List<MediaProgress>)
|
data class LocalSessionSyncResult(val id:String, val success:Boolean, val progressSynced:Boolean?, val error:String?)
|
||||||
data class LocalMediaProgressSyncResultsPayload(var numLocalMediaProgressForServer:Int, var numServerProgressUpdates:Int, var numLocalProgressUpdates:Int, var serverProgressUpdates:List<MediaProgress>)
|
data class LocalSessionsSyncResponsePayload(val results:List<LocalSessionSyncResult>)
|
||||||
|
|
||||||
fun getRequest(endpoint:String, httpClient:OkHttpClient?, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) {
|
private fun getRequest(endpoint:String, httpClient:OkHttpClient?, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) {
|
||||||
val address = config?.address ?: DeviceManager.serverAddress
|
val address = config?.address ?: DeviceManager.serverAddress
|
||||||
val token = config?.token ?: DeviceManager.token
|
val token = config?.token ?: DeviceManager.token
|
||||||
|
|
||||||
@@ -45,18 +45,20 @@ class ApiHandler(var ctx:Context) {
|
|||||||
makeRequest(request, httpClient, cb)
|
makeRequest(request, httpClient, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun postRequest(endpoint:String, payload: JSObject, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) {
|
private fun postRequest(endpoint:String, payload: JSObject, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) {
|
||||||
val address = config?.address ?: DeviceManager.serverAddress
|
val address = config?.address ?: DeviceManager.serverAddress
|
||||||
val token = config?.token ?: DeviceManager.token
|
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 requestUrl = "${address}$endpoint"
|
||||||
|
Log.d(tag, "postRequest to $requestUrl")
|
||||||
val request = Request.Builder().post(requestBody)
|
val request = Request.Builder().post(requestBody)
|
||||||
.url("${address}$endpoint").addHeader("Authorization", "Bearer ${token}")
|
.url(requestUrl).addHeader("Authorization", "Bearer ${token}")
|
||||||
.build()
|
.build()
|
||||||
makeRequest(request, null, cb)
|
makeRequest(request, null, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun patchRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) {
|
private fun patchRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) {
|
||||||
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().patch(requestBody)
|
val request = Request.Builder().patch(requestBody)
|
||||||
@@ -65,31 +67,7 @@ class ApiHandler(var ctx:Context) {
|
|||||||
makeRequest(request, null, cb)
|
makeRequest(request, null, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isOnline(): Boolean {
|
private fun makeRequest(request:Request, httpClient:OkHttpClient?, cb: (JSObject) -> Unit) {
|
||||||
val connectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
||||||
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
|
||||||
if (capabilities != null) {
|
|
||||||
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
|
|
||||||
Log.i("Internet", "NetworkCapabilities.TRANSPORT_CELLULAR")
|
|
||||||
return true
|
|
||||||
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
|
|
||||||
Log.i("Internet", "NetworkCapabilities.TRANSPORT_WIFI")
|
|
||||||
return true
|
|
||||||
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
|
|
||||||
Log.i("Internet", "NetworkCapabilities.TRANSPORT_ETHERNET")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isUsingCellularData(): Boolean {
|
|
||||||
val connectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
||||||
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
|
||||||
return capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun makeRequest(request:Request, httpClient:OkHttpClient?, cb: (JSObject) -> Unit) {
|
|
||||||
val client = httpClient ?: defaultClient
|
val client = httpClient ?: defaultClient
|
||||||
client.newCall(request).enqueue(object : Callback {
|
client.newCall(request).enqueue(object : Callback {
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
@@ -135,6 +113,18 @@ class ApiHandler(var ctx:Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getCurrentUser(cb: (User?) -> Unit) {
|
||||||
|
getRequest("/api/me", null, null) {
|
||||||
|
if (it.has("error")) {
|
||||||
|
Log.e(tag, it.getString("error") ?: "getCurrentUser Failed")
|
||||||
|
cb(null)
|
||||||
|
} else {
|
||||||
|
val user = jacksonMapper.readValue<User>(it.toString())
|
||||||
|
cb(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getLibraries(cb: (List<Library>) -> Unit) {
|
fun getLibraries(cb: (List<Library>) -> Unit) {
|
||||||
val mapper = jacksonMapper
|
val mapper = jacksonMapper
|
||||||
getRequest("/api/libraries", null,null) {
|
getRequest("/api/libraries", null,null) {
|
||||||
@@ -251,59 +241,6 @@ class ApiHandler(var ctx:Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun syncMediaProgress(cb: (LocalMediaProgressSyncResultsPayload) -> Unit) {
|
|
||||||
if (!isOnline()) {
|
|
||||||
Log.d(tag, "Error not online")
|
|
||||||
cb(LocalMediaProgressSyncResultsPayload(0,0,0, mutableListOf()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all local media progress connected to items on the current connected server
|
|
||||||
val localMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress().filter {
|
|
||||||
it.serverConnectionConfigId == DeviceManager.serverConnectionConfig?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
val localSyncResultsPayload = LocalMediaProgressSyncResultsPayload(localMediaProgress.size,0, 0, mutableListOf())
|
|
||||||
|
|
||||||
if (localMediaProgress.isNotEmpty()) {
|
|
||||||
Log.d(tag, "Sending sync local progress request with ${localMediaProgress.size} progress items")
|
|
||||||
val payload = JSObject(jacksonMapper.writeValueAsString(LocalMediaProgressSyncPayload(localMediaProgress)))
|
|
||||||
postRequest("/api/me/sync-local-progress", payload, null) {
|
|
||||||
Log.d(tag, "Media Progress Sync payload $payload - response ${it}")
|
|
||||||
|
|
||||||
if (it.toString() == "{}") {
|
|
||||||
Log.e(tag, "Progress sync received empty object")
|
|
||||||
} else if (it.has("error")) {
|
|
||||||
Log.e(tag, it.getString("error") ?: "Progress sync error")
|
|
||||||
} else {
|
|
||||||
val progressSyncResponsePayload = jacksonMapper.readValue<MediaProgressSyncResponsePayload>(it.toString())
|
|
||||||
|
|
||||||
localSyncResultsPayload.numLocalProgressUpdates = progressSyncResponsePayload.localProgressUpdates.size
|
|
||||||
localSyncResultsPayload.serverProgressUpdates = progressSyncResponsePayload.serverProgressUpdates
|
|
||||||
localSyncResultsPayload.numServerProgressUpdates = progressSyncResponsePayload.numServerProgressUpdates
|
|
||||||
Log.d(tag, "Media Progress Sync | Local Updates: $localSyncResultsPayload")
|
|
||||||
if (progressSyncResponsePayload.localProgressUpdates.isNotEmpty()) {
|
|
||||||
// Update all local media progress
|
|
||||||
progressSyncResponsePayload.localProgressUpdates.forEach { localMediaProgress ->
|
|
||||||
MediaEventManager.syncEvent(localMediaProgress, "Local progress updated. Received from server sync local API request")
|
|
||||||
|
|
||||||
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
progressSyncResponsePayload.serverProgressUpdates.forEach { localMediaProgress ->
|
|
||||||
MediaEventManager.syncEvent(localMediaProgress, "Server progress updated. Received from server sync local API request")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cb(localSyncResultsPayload)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.d(tag, "No local media progress to sync")
|
|
||||||
cb(localSyncResultsPayload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateMediaProgress(libraryItemId:String,episodeId:String?,updatePayload:JSObject, cb: () -> Unit) {
|
fun updateMediaProgress(libraryItemId:String,episodeId:String?,updatePayload:JSObject, cb: () -> Unit) {
|
||||||
Log.d(tag, "updateMediaProgress $libraryItemId $episodeId $updatePayload")
|
Log.d(tag, "updateMediaProgress $libraryItemId $episodeId $updatePayload")
|
||||||
val endpoint = if(episodeId.isNullOrEmpty()) "/api/me/progress/$libraryItemId" else "/api/me/progress/$libraryItemId/$episodeId"
|
val endpoint = if(episodeId.isNullOrEmpty()) "/api/me/progress/$libraryItemId" else "/api/me/progress/$libraryItemId/$episodeId"
|
||||||
@@ -375,4 +312,65 @@ class ApiHandler(var ctx:Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendSyncLocalSessions(playbackSessions:List<PlaybackSession>, cb: (Boolean, String?) -> Unit) {
|
||||||
|
val payload = JSObject(jacksonMapper.writeValueAsString(LocalSessionsSyncRequestPayload(playbackSessions)))
|
||||||
|
|
||||||
|
postRequest("/api/session/local-all", payload, null) {
|
||||||
|
if (!it.getString("error").isNullOrEmpty()) {
|
||||||
|
cb(false, it.getString("error"))
|
||||||
|
} else {
|
||||||
|
val response = jacksonMapper.readValue<LocalSessionsSyncResponsePayload>(it.toString())
|
||||||
|
response.results.forEach { localSessionSyncResult ->
|
||||||
|
playbackSessions.find { ps -> ps.id == localSessionSyncResult.id }?.let { session ->
|
||||||
|
if (localSessionSyncResult.progressSynced == true) {
|
||||||
|
val syncResult = SyncResult(true, true, "Progress synced on server")
|
||||||
|
MediaEventManager.saveEvent(session, syncResult)
|
||||||
|
DeviceManager.dbManager.removePlaybackSession(session.id)
|
||||||
|
Log.i(tag, "Successfully synced session ${session.displayTitle} with server")
|
||||||
|
} else if (!localSessionSyncResult.success) {
|
||||||
|
Log.e(tag, "Failed to sync session ${session.displayTitle} with server. Error: ${localSessionSyncResult.error}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cb(true, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncLocalMediaProgressForUser(cb: () -> Unit) {
|
||||||
|
// Get all local media progress for this server
|
||||||
|
val allLocalMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress().filter { it.serverConnectionConfigId == DeviceManager.serverConnectionConfigId }
|
||||||
|
if (allLocalMediaProgress.isEmpty()) {
|
||||||
|
Log.d(tag, "No local media progress to sync")
|
||||||
|
return cb()
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentUser { _user ->
|
||||||
|
_user?.let { user->
|
||||||
|
// Compare server user progress with local progress
|
||||||
|
user.mediaProgress.forEach { mediaProgress ->
|
||||||
|
// Get matching local media progress
|
||||||
|
allLocalMediaProgress.find { it.isMatch(mediaProgress) }?.let { localMediaProgress ->
|
||||||
|
if (mediaProgress.lastUpdate > localMediaProgress.lastUpdate) {
|
||||||
|
Log.d(tag, "Server progress for media item id=\"${mediaProgress.mediaItemId}\" is more recent then local. Updating local current time ${localMediaProgress.currentTime} to ${mediaProgress.currentTime}")
|
||||||
|
localMediaProgress.updateFromServerMediaProgress(mediaProgress)
|
||||||
|
MediaEventManager.syncEvent(mediaProgress, "Sync on server connection")
|
||||||
|
} else if (localMediaProgress.lastUpdate > mediaProgress.lastUpdate && localMediaProgress.ebookLocation != null && localMediaProgress.ebookLocation != mediaProgress.ebookLocation) {
|
||||||
|
// Patch ebook progress to server
|
||||||
|
val endpoint = "/api/me/progress/${localMediaProgress.libraryItemId}"
|
||||||
|
val updatePayload = JSObject()
|
||||||
|
updatePayload.put("ebookLocation", localMediaProgress.ebookLocation)
|
||||||
|
updatePayload.put("ebookProgress", localMediaProgress.ebookProgress)
|
||||||
|
updatePayload.put("lastUpdate", localMediaProgress.lastUpdate)
|
||||||
|
patchRequest(endpoint,updatePayload) {
|
||||||
|
Log.d(tag, "syncLocalMediaProgressForUser patched ebook progress")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="272"
|
||||||
|
android:viewportHeight="181.125"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleY="0.6659007"
|
||||||
|
android:translateY="30.256865">
|
||||||
|
<group android:translateY="144.77344">
|
||||||
|
<path android:pathData="M7,-50.96875Q7,-69.375,10.6875,-80.84375Q14.390625,-92.328125,21.671875,-98.15625Q28.953125,-104,40,-104Q48.140625,-104,54.53125,-100.625Q60.9375,-97.265625,64.6875,-90.921875Q68.4375,-84.59375,70.71875,-75.5Q73,-66.40625,73,-50.96875Q73,-32.703125,69.34375,-21.21875Q65.6875,-9.75,58.390625,-3.875Q51.109375,2,40,2Q25.359375,2,17.015625,-8.265625Q7,-21.71875,7,-50.96875ZM20,-51Q20,-25.828125,25.765625,-17.90625Q31.53125,-10,40,-10Q48.46875,-10,54.234375,-17.953125Q60,-25.90625,60,-51Q60,-75.734375,54.359375,-83.859375Q48.71875,-92,39.859375,-92Q31.40625,-92,26.09375,-84.828125Q20,-75.671875,20,-51Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.6"/>
|
||||||
|
<path android:pathData="M93,0L93,-13L106,-13L106,0L93,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.6"/>
|
||||||
|
<path android:pathData="M127,-27L140,-28Q141.42188,-19.03125,146.32812,-14.515625Q151.25,-10,159.04688,-10Q168.59375,-10,174.29688,-16.53125Q180,-23.0625,180,-33.859375Q180,-44.140625,174.70312,-50.0625Q169.42188,-56,159.5,-56Q153.29688,-56,148.95312,-53.5Q144.625,-51,142,-47L129,-49L140,-102L188,-102L188,-89L150.54688,-89L145.28125,-61.0625Q153.875,-68,163.3125,-68Q175.8125,-68,184.40625,-58.65625Q193,-49.328125,193,-34.65625Q193,-20.671875,185.125,-10.5Q175.54688,2,158.98438,2Q145.40625,2,136.8125,-5.9375Q128.21875,-13.875,127,-27Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.6"/>
|
||||||
|
<path android:pathData="M201,0L228.85938,-38L203,-75L218.57812,-75L230.82812,-57.484375Q234.23438,-52.609375,236.84375,-48.890625Q239.65625,-52.71875,242.28125,-56.296875L256,-75L271.875,-75L244.59375,-37.796875L271,0L255.42188,0L239.48438,-22.796875L236.60938,-26.90625L216.875,0L201,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.6"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="272"
|
||||||
|
android:viewportHeight="181.125"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleX="1.0434783"
|
||||||
|
android:scaleY="0.69485295"
|
||||||
|
android:translateX="-11.826087"
|
||||||
|
android:translateY="27.63488">
|
||||||
|
<group android:translateY="144.77344">
|
||||||
|
<path android:pathData="M54,-0L41,-0L41,-81.03125Q36.484375,-76.65625,29.15625,-72.265625Q21.828125,-67.890625,16,-65.703125L16,-78Q26.546875,-83.015625,34.4375,-90.140625Q42.34375,-97.28125,45.625,-104L54,-104L54,-0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M93,0L93,-13L106,-13L106,0L93,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M127,-50.96875Q127,-69.375,130.6875,-80.84375Q134.39062,-92.328125,141.67188,-98.15625Q148.95312,-104,160,-104Q168.14062,-104,174.53125,-100.625Q180.9375,-97.265625,184.6875,-90.921875Q188.4375,-84.59375,190.71875,-75.5Q193,-66.40625,193,-50.96875Q193,-32.703125,189.34375,-21.21875Q185.6875,-9.75,178.39062,-3.875Q171.10938,2,160,2Q145.35938,2,137.01562,-8.265625Q127,-21.71875,127,-50.96875ZM140,-51Q140,-25.828125,145.76562,-17.90625Q151.53125,-10,160,-10Q168.46875,-10,174.23438,-17.953125Q180,-25.90625,180,-51Q180,-75.734375,174.35938,-83.859375Q168.71875,-92,159.85938,-92Q151.40625,-92,146.09375,-84.828125Q140,-75.671875,140,-51Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M201,0L228.85938,-38L203,-75L218.57812,-75L230.82812,-57.484375Q234.23438,-52.609375,236.84375,-48.890625Q239.65625,-52.71875,242.28125,-56.296875L256,-75L271.875,-75L244.59375,-37.796875L271,0L255.42188,0L239.48438,-22.796875L236.60938,-26.90625L216.875,0L201,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="272"
|
||||||
|
android:viewportHeight="181.125"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleX="1.0434783"
|
||||||
|
android:scaleY="0.69485295"
|
||||||
|
android:translateX="-11.826087"
|
||||||
|
android:translateY="27.63488">
|
||||||
|
<group android:translateY="144.77344">
|
||||||
|
<path android:pathData="M54,-0L41,-0L41,-81.03125Q36.484375,-76.65625,29.15625,-72.265625Q21.828125,-67.890625,16,-65.703125L16,-78Q26.546875,-83.015625,34.4375,-90.140625Q42.34375,-97.28125,45.625,-104L54,-104L54,-0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M93,0L93,-13L106,-13L106,0L93,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M192,-13L192,0L124,0Q123.859375,-4.75,125.421875,-9.125Q127.921875,-16.359375,133.4375,-23.25Q138.96875,-30.140625,149.64062,-38.6875Q166.21875,-52.625,172.10938,-60.28125Q178,-67.9375,178,-75.015625Q178,-82.171875,172.70312,-87.078125Q167.40625,-92,158.875,-92Q149.875,-92,144.46875,-86.953125Q139.0625,-81.921875,139,-73L126,-75Q127.328125,-89.15625,135.95312,-96.578125Q144.59375,-104,159.15625,-104Q173.85938,-104,182.42188,-95.71875Q191,-87.4375,191,-75.203125Q191,-68.96875,188.59375,-62.953125Q186.20312,-56.9375,180.64062,-50.28125Q175.09375,-43.640625,161.73438,-32.375Q150.35938,-23.03125,146.96875,-19.6875Q143.59375,-16.359375,141.39062,-13L192,-13Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M201,0L228.85938,-38L203,-75L218.57812,-75L230.82812,-57.484375Q234.23438,-52.609375,236.84375,-48.890625Q239.65625,-52.71875,242.28125,-56.296875L256,-75L271.875,-75L244.59375,-37.796875L271,0L255.42188,0L239.48438,-22.796875L236.60938,-26.90625L216.875,0L201,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="272"
|
||||||
|
android:viewportHeight="181.125"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleX="1.0434783"
|
||||||
|
android:scaleY="0.69485295"
|
||||||
|
android:translateX="-11.826087"
|
||||||
|
android:translateY="27.63488">
|
||||||
|
<group android:translateY="144.77344">
|
||||||
|
<path android:pathData="M54,-0L41,-0L41,-81.03125Q36.484375,-76.65625,29.15625,-72.265625Q21.828125,-67.890625,16,-65.703125L16,-78Q26.546875,-83.015625,34.4375,-90.140625Q42.34375,-97.28125,45.625,-104L54,-104L54,-0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M93,0L93,-13L106,-13L106,0L93,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M127,-27L140,-28Q141.42188,-19.03125,146.32812,-14.515625Q151.25,-10,159.04688,-10Q168.59375,-10,174.29688,-16.53125Q180,-23.0625,180,-33.859375Q180,-44.140625,174.70312,-50.0625Q169.42188,-56,159.5,-56Q153.29688,-56,148.95312,-53.5Q144.625,-51,142,-47L129,-49L140,-102L188,-102L188,-89L150.54688,-89L145.28125,-61.0625Q153.875,-68,163.3125,-68Q175.8125,-68,184.40625,-58.65625Q193,-49.328125,193,-34.65625Q193,-20.671875,185.125,-10.5Q175.54688,2,158.98438,2Q145.40625,2,136.8125,-5.9375Q128.21875,-13.875,127,-27Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M201,0L228.85938,-38L203,-75L218.57812,-75L230.82812,-57.484375Q234.23438,-52.609375,236.84375,-48.890625Q239.65625,-52.71875,242.28125,-56.296875L256,-75L271.875,-75L244.59375,-37.796875L271,0L255.42188,0L239.48438,-22.796875L236.60938,-26.90625L216.875,0L201,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="272"
|
||||||
|
android:viewportHeight="181.125"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleY="0.6659007"
|
||||||
|
android:translateY="30.256865">
|
||||||
|
<group android:translateY="144.77344">
|
||||||
|
<path android:pathData="M72,-13L72,-0L4,-0Q3.859375,-4.75,5.421875,-9.125Q7.921875,-16.359375,13.4375,-23.25Q18.96875,-30.140625,29.640625,-38.6875Q46.21875,-52.625,52.109375,-60.28125Q58,-67.9375,58,-75.015625Q58,-82.171875,52.703125,-87.078125Q47.40625,-92,38.875,-92Q29.875,-92,24.46875,-86.953125Q19.0625,-81.921875,19,-73L6,-75Q7.328125,-89.15625,15.953125,-96.578125Q24.59375,-104,39.15625,-104Q53.859375,-104,62.421875,-95.71875Q71,-87.4375,71,-75.203125Q71,-68.96875,68.59375,-62.953125Q66.203125,-56.9375,60.640625,-50.28125Q55.09375,-43.640625,41.734375,-32.375Q30.359375,-23.03125,26.96875,-19.6875Q23.59375,-16.359375,21.390625,-13L72,-13Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M93,0L93,-13L106,-13L106,0L93,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M127,-50.96875Q127,-69.375,130.6875,-80.84375Q134.39062,-92.328125,141.67188,-98.15625Q148.95312,-104,160,-104Q168.14062,-104,174.53125,-100.625Q180.9375,-97.265625,184.6875,-90.921875Q188.4375,-84.59375,190.71875,-75.5Q193,-66.40625,193,-50.96875Q193,-32.703125,189.34375,-21.21875Q185.6875,-9.75,178.39062,-3.875Q171.10938,2,160,2Q145.35938,2,137.01562,-8.265625Q127,-21.71875,127,-50.96875ZM140,-51Q140,-25.828125,145.76562,-17.90625Q151.53125,-10,160,-10Q168.46875,-10,174.23438,-17.953125Q180,-25.90625,180,-51Q180,-75.734375,174.35938,-83.859375Q168.71875,-92,159.85938,-92Q151.40625,-92,146.09375,-84.828125Q140,-75.671875,140,-51Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M201,0L228.85938,-38L203,-75L218.57812,-75L230.82812,-57.484375Q234.23438,-52.609375,236.84375,-48.890625Q239.65625,-52.71875,242.28125,-56.296875L256,-75L271.875,-75L244.59375,-37.796875L271,0L255.42188,0L239.48438,-22.796875L236.60938,-26.90625L216.875,0L201,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="272"
|
||||||
|
android:viewportHeight="181.125"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleY="0.6659007"
|
||||||
|
android:translateY="30.256865">
|
||||||
|
<group android:translateY="144.77344">
|
||||||
|
<path android:pathData="M7,-27L20,-29Q22.109375,-18.90625,26.90625,-14.453125Q31.71875,-10,39.484375,-10Q48.609375,-10,54.296875,-16Q60,-22.015625,60,-30.90625Q60,-39.375,54.5,-44.890625Q49,-50.40625,40.515625,-50.40625Q37.046875,-50.40625,31.890625,-49L33.25,-61Q34.46875,-60.859375,35.203125,-60.859375Q42.8125,-60.859375,48.90625,-64.796875Q55,-68.75,55,-76.953125Q55,-83.46875,50.484375,-87.734375Q45.984375,-92,38.84375,-92Q31.765625,-92,27.046875,-87.75Q22.34375,-83.5,21,-75L8,-77Q10.28125,-89.859375,18.34375,-96.921875Q26.40625,-104,38.40625,-104Q46.6875,-104,53.65625,-100.390625Q60.625,-96.796875,64.3125,-90.578125Q68,-84.359375,68,-77.359375Q68,-70.71875,64.40625,-65.265625Q60.828125,-59.8125,53.796875,-56.59375Q62.9375,-54.453125,67.96875,-47.6875Q73,-40.9375,73,-30.796875Q73,-17.078125,63.375,-7.53125Q53.75,2,39.03125,2Q25.765625,2,17,-6.078125Q8.234375,-14.15625,7,-27Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M93,0L93,-13L106,-13L106,0L93,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M127,-50.96875Q127,-69.375,130.6875,-80.84375Q134.39062,-92.328125,141.67188,-98.15625Q148.95312,-104,160,-104Q168.14062,-104,174.53125,-100.625Q180.9375,-97.265625,184.6875,-90.921875Q188.4375,-84.59375,190.71875,-75.5Q193,-66.40625,193,-50.96875Q193,-32.703125,189.34375,-21.21875Q185.6875,-9.75,178.39062,-3.875Q171.10938,2,160,2Q145.35938,2,137.01562,-8.265625Q127,-21.71875,127,-50.96875ZM140,-51Q140,-25.828125,145.76562,-17.90625Q151.53125,-10,160,-10Q168.46875,-10,174.23438,-17.953125Q180,-25.90625,180,-51Q180,-75.734375,174.35938,-83.859375Q168.71875,-92,159.85938,-92Q151.40625,-92,146.09375,-84.828125Q140,-75.671875,140,-51Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M201,0L228.85938,-38L203,-75L218.57812,-75L230.82812,-57.484375Q234.23438,-52.609375,236.84375,-48.890625Q239.65625,-52.71875,242.28125,-56.296875L256,-75L271.875,-75L244.59375,-37.796875L271,0L255.42188,0L239.48438,-22.796875L236.60938,-26.90625L216.875,0L201,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
|
After Width: | Height: | Size: 500 B |
|
After Width: | Height: | Size: 447 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 465 B |
|
After Width: | Height: | Size: 512 B |
|
After Width: | Height: | Size: 496 B |
|
After Width: | Height: | Size: 323 B |
|
After Width: | Height: | Size: 311 B |
|
After Width: | Height: | Size: 303 B |
|
After Width: | Height: | Size: 315 B |
|
After Width: | Height: | Size: 338 B |
|
After Width: | Height: | Size: 346 B |
|
Before Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 664 B |
|
After Width: | Height: | Size: 608 B |
|
After Width: | Height: | Size: 583 B |
|
After Width: | Height: | Size: 599 B |
|
After Width: | Height: | Size: 684 B |
|
After Width: | Height: | Size: 712 B |
|
After Width: | Height: | Size: 1013 B |
|
After Width: | Height: | Size: 895 B |
|
After Width: | Height: | Size: 866 B |
|
After Width: | Height: | Size: 892 B |
|
After Width: | Height: | Size: 1011 B |
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="862dp"
|
||||||
|
android:height="1024dp"
|
||||||
|
android:viewportWidth="862"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M855,504.4c-5.3,-4.4 -13.5,-10.9 -24.7,-18.6v-86.5c0,-220.5 -178.8,-399.3 -399.3,-399.3v0c-220.5,0 -399.3,178.8 -399.3,399.3v86.5c-11.1,7.7 -19.4,14.2 -24.7,18.6 -4.4,3.7 -7.1,9.1 -7.1,15.2 0,0 0,0 0,0v-0,103.6c0,0 0,0 0,0 0,6.1 2.8,11.6 7.1,15.2l0,0c12.3,10.3 40.6,31.8 84.7,53.8v10.1c0,27.2 17.5,49.2 39.1,49.2v0c21.6,0 39.1,-22 39.1,-49.2v-248.5c0,-27.2 -17.5,-49.2 -39.1,-49.2v0c-20.7,0 -37.7,20.2 -39,45.8l-0.1,0v-51.2c0,-187.4 151.9,-339.3 339.3,-339.3v0c187.4,0 339.3,151.9 339.3,339.3v51.2l-0.1,-0c-1.4,-25.6 -18.3,-45.8 -39,-45.8v0c-21.6,0 -39.1,22 -39.1,49.2v248.5c0,27.2 17.5,49.2 39.1,49.2v0c21.6,0 39.1,-22 39.1,-49.2v-10.1c44,-22.1 72.3,-43.6 84.7,-53.8 4.4,-3.7 7.1,-9.1 7.1,-15.2 0,-0 0,-0 0,-0v0,-103.6c0,-0 0,-0 0,-0 0,-6.1 -2.8,-11.6 -7.1,-15.2l-0,-0zM291.8,936.4c26.1,0 47.3,-21.2 47.3,-47.3v0,-529.4c0,-0 0,-0 0,-0 0,-26.1 -21.2,-47.3 -47.3,-47.3h-48.8c-26.1,0 -47.3,21.2 -47.3,47.3v0,529.4c0,26.1 21.2,47.3 47.3,47.3 0,0 0,0 0,0v0zM213.5,439.4h107.8v11.3h-107.8zM455.5,936.4c26.1,0 47.3,-21.2 47.3,-47.3v0,-529.4c0,-0 0,-0 0,-0 0,-26.1 -21.2,-47.3 -47.3,-47.3h-48.8c-26.1,0 -47.3,21.2 -47.3,47.3v0,529.4c0,26.1 21.2,47.3 47.3,47.3 0,0 0,0 0,0v0zM377.2,439.4h107.8v11.3h-107.8zM619.1,936.4c26.1,0 47.3,-21.2 47.3,-47.3v0,-529.4c0,-0 0,-0 0,-0 0,-26.1 -21.2,-47.3 -47.3,-47.3h-48.8c-26.1,0 -47.3,21.2 -47.3,47.3v0,529.4c0,26.1 21.2,47.3 47.3,47.3 0,0 0,0 0,0v0zM540.8,439.4h107.8v11.3h-107.8zM114.1,952.7h633.9c19.7,0 35.6,15.9 35.6,35.6v0c0,19.7 -15.9,35.6 -35.6,35.6h-633.9c-19.7,0 -35.6,-15.9 -35.6,-35.6v-0c0,-19.7 15.9,-35.6 35.6,-35.6z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/widgetBackground"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:theme="@style/AppTheme.AppWidgetContainer"
|
||||||
|
style="@style/Widget.Android.AppWidget.Container">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="3"
|
||||||
|
android:background="?android:attr/colorAccent"
|
||||||
|
android:theme="@style/MediaPlayerWidgetTheme"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/widgetAlbumArt"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="2"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:src="@drawable/icon" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="3"
|
||||||
|
android:layout_marginVertical="8dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/widgetArtistText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="2"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:gravity="bottom"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="Artist"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/widgetMediaTitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="2"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:gravity="top"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="Title"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/widgetButtonContainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="3"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:background="?android:attr/colorAccent"
|
||||||
|
android:theme="@style/MediaPlayerWidgetTheme">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/widgetRewindButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:src="@drawable/exo_icon_rewind"
|
||||||
|
style="@style/Widget.Android.AppWidget.InnerView"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/widgetPlayPauseButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_margin="6dp"
|
||||||
|
android:src="@drawable/ic_media_play_dark" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/widgetFastForwardButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:src="@drawable/exo_icon_fastforward" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/tinyCornerIcon"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:layout_margin="6dp"
|
||||||
|
android:adjustViewBounds ="true"
|
||||||
|
android:src="@drawable/icon_monochrome" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<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,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!--
|
||||||
|
Having themes.xml for night-v31 because of the priority order of the resource qualifiers.
|
||||||
|
-->
|
||||||
|
<style name="AppTheme.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
|
||||||
|
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
|
||||||
|
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
|
||||||
|
<item name="android:id">@android:id/background</item>
|
||||||
|
<item name="android:padding">?attr/appWidgetPadding</item>
|
||||||
|
<item name="android:background">@drawable/app_widget_background</item>
|
||||||
|
<item name="android:clipToOutline">true</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>-->
|
||||||
|
<!-- <item name="android:textColor">?android:attr/textColorPrimary</item>-->
|
||||||
|
<item name="android:clipToOutline">true</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!--
|
||||||
|
Having themes.xml for v31 variant because @android:dimen/system_app_widget_background_radius
|
||||||
|
and @android:dimen/system_app_widget_internal_padding requires API level 31
|
||||||
|
-->
|
||||||
|
<style name="AppTheme.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
|
||||||
|
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
|
||||||
|
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<declare-styleable name="AppWidgetAttrs">
|
<declare-styleable name="AppWidgetAttrs">
|
||||||
<attr name="appWidgetPadding" format="dimension" />
|
<attr name="appWidgetPadding" format="dimension" />
|
||||||
<attr name="appWidgetInnerRadius" format="dimension" />
|
<attr name="appWidgetInnerRadius" format="dimension" />
|
||||||
<attr name="appWidgetRadius" format="dimension" />
|
<attr name="appWidgetRadius" format="dimension" />
|
||||||
</declare-styleable>
|
<attr name="backgroundRadius" format="dimension" />
|
||||||
|
<attr name="appWidgetViewPadding" format="dimension" />
|
||||||
|
</declare-styleable>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -10,4 +10,5 @@
|
|||||||
<string name="action_jump_backward">Jump Backward</string>
|
<string name="action_jump_backward">Jump Backward</string>
|
||||||
<string name="action_skip_forward">Skip Forward</string>
|
<string name="action_skip_forward">Skip Forward</string>
|
||||||
<string name="action_skip_backward">Skip Backward</string>
|
<string name="action_skip_backward">Skip Backward</string>
|
||||||
|
<string name="action_change_speed">Change Playback Speed</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,33 +1,42 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
<item name="colorAccent">@color/colorAccent</item>
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
<item name="windowActionBar">false</item>
|
<item name="windowActionBar">false</item>
|
||||||
<item name="windowNoTitle">true</item>
|
<item name="windowNoTitle">true</item>
|
||||||
<item name="android:background">@null</item>
|
<item name="android:background">@null</item>
|
||||||
<item name="android:statusBarColor">@color/background_dark</item>
|
<item name="android:statusBarColor">@color/background_dark</item>
|
||||||
<item name="android:navigationBarColor">@color/background_dark</item>
|
<item name="android:navigationBarColor">@color/background_dark</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||||
<!-- <item name="android:background">@drawable/screen</item>-->
|
<!-- <item name="android:background">@drawable/screen</item>-->
|
||||||
</style>
|
</style>
|
||||||
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
|
|
||||||
<item name="android:id">@android:id/background</item>
|
|
||||||
<item name="android:background">?android:attr/colorBackground</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Widget.Android.AppWidget.InnerView" parent="android:Widget">
|
<style name="MediaPlayerWidgetTheme" parent="Theme.AppCompat.DayNight">
|
||||||
<item name="android:background">?android:attr/colorBackground</item>
|
<!-- These colors match the vue pages -->
|
||||||
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
<item name="android:colorBackground">#232323</item>
|
||||||
</style>
|
<item name="android:colorAccent">#373838</item>
|
||||||
|
<item name="android:textColor">#ffffff</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>-->
|
||||||
|
<item name="android:padding">0dp</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<style name="AppTheme.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault">
|
|
||||||
<!-- Radius of the outer bound of widgets to make the rounded corners -->
|
<style name="AppTheme.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault">
|
||||||
<item name="appWidgetRadius">16dp</item>
|
<!-- Radius of the outer bound of widgets to make the rounded corners -->
|
||||||
<!--
|
<item name="appWidgetRadius">0dp</item>
|
||||||
|
<!--
|
||||||
Radius of the inner view's bound of widgets to make the rounded corners.
|
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
|
It needs to be 8dp or less than the value of appWidgetRadius
|
||||||
-->
|
-->
|
||||||
<item name="appWidgetInnerRadius">8dp</item>
|
<item name="appWidgetInnerRadius">0dp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.AppWidgetContainer"
|
<style name="AppTheme.AppWidgetContainer" parent="AppTheme.AppWidgetContainerParent">
|
||||||
parent="AppTheme.AppWidgetContainerParent">
|
<!-- Apply padding to avoid the content of the widget colliding with the rounded corners -->
|
||||||
<!-- Apply padding to avoid the content of the widget colliding with the rounded corners -->
|
<item name="appWidgetPadding">0dp</item>
|
||||||
<item name="appWidgetPadding">16dp</item>
|
</style>
|
||||||
</style>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:description="@string/app_widget_description"
|
||||||
|
android:initialKeyguardLayout="@layout/media_player_widget"
|
||||||
|
android:initialLayout="@layout/media_player_widget"
|
||||||
|
android:minWidth="275dp"
|
||||||
|
android:minHeight="50dp"
|
||||||
|
android:previewImage="@drawable/media_player_widget_preview"
|
||||||
|
android:previewLayout="@layout/media_player_widget"
|
||||||
|
android:resizeMode="horizontal"
|
||||||
|
android:targetCellWidth="4"
|
||||||
|
android:targetCellHeight="1"
|
||||||
|
android:updatePeriodMillis="86400000"
|
||||||
|
android:widgetCategory="home_screen" />
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:minWidth="110dp"
|
|
||||||
android:minHeight="40dp"
|
|
||||||
android:updatePeriodMillis="86400000"
|
|
||||||
android:previewImage="@drawable/example_appwidget_preview"
|
|
||||||
android:initialLayout="@layout/new_app_widget"
|
|
||||||
android:description="@string/app_widget_description"
|
|
||||||
android:resizeMode="horizontal|vertical"
|
|
||||||
android:widgetCategory="home_screen"
|
|
||||||
android:initialKeyguardLayout="@layout/new_app_widget"
|
|
||||||
/>
|
|
||||||
@@ -14,7 +14,7 @@ ext {
|
|||||||
cordovaAndroidVersion = '10.1.1'
|
cordovaAndroidVersion = '10.1.1'
|
||||||
androidx_car_version = '1.0.0-alpha7'
|
androidx_car_version = '1.0.0-alpha7'
|
||||||
androidx_core_ktx_version = '1.7.0'
|
androidx_core_ktx_version = '1.7.0'
|
||||||
androidx_media_version = '1.5.0'
|
androidx_media_version = '1.6.0'
|
||||||
androidx_preference_version = '1.1.1'
|
androidx_preference_version = '1.1.1'
|
||||||
androidx_test_runner_version = '1.3.0'
|
androidx_test_runner_version = '1.3.0'
|
||||||
arch_lifecycle_version = '2.2.0'
|
arch_lifecycle_version = '2.2.0'
|
||||||
|
|||||||
@@ -2,6 +2,19 @@
|
|||||||
@import './defaultStyles.css';
|
@import './defaultStyles.css';
|
||||||
@import './absicons.css';
|
@import './absicons.css';
|
||||||
|
|
||||||
|
* {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
-webkit-touch-callout: auto;
|
||||||
|
-webkit-user-select: auto;
|
||||||
|
user-select: auto;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #262626;
|
background-color: #262626;
|
||||||
}
|
}
|
||||||
@@ -20,9 +33,9 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#content.playerOpen {
|
#content.playerOpen {
|
||||||
height: calc(100% - 164px);
|
height: calc(100% - 184px);
|
||||||
min-height: calc(100% - 164px);
|
min-height: calc(100% - 184px);
|
||||||
max-height: calc(100% - 164px);
|
max-height: calc(100% - 184px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#bookshelf {
|
#bookshelf {
|
||||||
@@ -103,3 +116,31 @@ Bookshelf Label
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
filter: blur(20px);
|
filter: blur(20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.episode-subtitle {
|
||||||
|
word-break: break-word;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
line-height: 16px;
|
||||||
|
/* fallback */
|
||||||
|
max-height: 32px;
|
||||||
|
/* fallback */
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
/* number of lines to show */
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-clamp-2 {
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-clamp-4 {
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 4;
|
||||||
|
}
|
||||||
@@ -48,25 +48,6 @@
|
|||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Gentium Book Basic';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url(/fonts/GentiumBookBasic.woff2) format('woff2');
|
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Gentium Book Basic';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url(/fonts/GentiumBookBasic.woff2) format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* cyrillic-ext */
|
/* cyrillic-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<div v-if="user && currentLibrary">
|
<div v-if="user && currentLibrary">
|
||||||
<div class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
<div class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
||||||
<ui-library-icon :icon="currentLibraryIcon" :size="4" font-size="base" />
|
<ui-library-icon :icon="currentLibraryIcon" :size="4" font-size="base" />
|
||||||
<p class="text-sm font-book leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
|
<p class="text-smleading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -12,27 +12,27 @@
|
|||||||
<div class="top-6 right-4 absolute cursor-pointer">
|
<div class="top-6 right-4 absolute cursor-pointer">
|
||||||
<span class="material-icons text-3xl" :class="{ 'text-black text-opacity-75': coverBgIsLight }" @click="showMoreMenuDialog = true">more_vert</span>
|
<span class="material-icons text-3xl" :class="{ 'text-black text-opacity-75': coverBgIsLight }" @click="showMoreMenuDialog = true">more_vert</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" style="font-size: 10px" :class="{ 'text-success': isLocalPlayMethod, 'text-accent': !isLocalPlayMethod }">{{ isDirectPlayMethod ? 'Direct' : isLocalPlayMethod ? 'Local' : 'Transcode' }}</p>
|
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" :class="{ 'text-black text-opacity-75': coverBgIsLight }" style="font-size: 10px">{{ isDirectPlayMethod ? 'Direct' : isLocalPlayMethod ? 'Local' : 'Transcode' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="useChapterTrack && useTotalTrack && showFullscreen" class="absolute total-track w-full px-3 z-30">
|
<div v-if="useChapterTrack && useTotalTrack && showFullscreen" class="absolute total-track w-full z-30 px-6">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ currentTimePretty }}</p>
|
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ currentTimePretty }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ totalTimeRemainingPretty }}</p>
|
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ totalTimeRemainingPretty }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div class="h-1 w-full bg-gray-500 bg-opacity-50 relative">
|
<div class="h-1 w-full bg-gray-500 bg-opacity-50 relative rounded-full">
|
||||||
<div ref="totalReadyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
<div ref="totalReadyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none rounded-full" />
|
||||||
<div ref="totalBufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none" />
|
<div ref="totalBufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none rounded-full" />
|
||||||
<div ref="totalPlayedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
<div ref="totalPlayedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cover-wrapper absolute z-30 pointer-events-auto" @click="clickContainer">
|
<div class="cover-wrapper absolute z-30 pointer-events-auto" @click="clickContainer">
|
||||||
<div class="w-full h-full flex justify-center">
|
<div class="w-full h-full flex justify-center">
|
||||||
<covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" ref="cover" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" @imageLoaded="coverImageLoaded" />
|
<covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" ref="cover" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" raw @imageLoaded="coverImageLoaded" />
|
||||||
</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">
|
<div v-if="syncStatus === $constants.SyncStatus.FAILED" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-30">
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="playerContent" class="playerContainer w-full z-20 absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" :style="{ backgroundColor: showFullscreen ? '' : coverRgb }" @click="clickContainer">
|
<div id="playerContent" class="playerContainer w-full z-20 absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" :style="{ backgroundColor: showFullscreen ? '' : coverRgb }" @click="clickContainer">
|
||||||
<div v-if="showFullscreen" class="absolute bottom-4 left-0 right-0 w-full py-3 mx-auto px-3" style="max-width: 380px">
|
<div v-if="showFullscreen" class="absolute bottom-4 left-0 right-0 w-full pb-4 pt-2 mx-auto px-6" style="max-width: 414px">
|
||||||
<div class="flex items-center justify-between pointer-events-auto">
|
<div class="flex items-center justify-between pointer-events-auto">
|
||||||
<span v-if="!isPodcast && isServerItem && networkConnected" class="material-icons text-3xl text-white text-opacity-75 cursor-pointer" @click="$emit('showBookmarks')">{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }}</span>
|
<span v-if="!isPodcast && isServerItem && networkConnected" class="material-icons text-3xl text-white text-opacity-75 cursor-pointer" @click="$emit('showBookmarks')">{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }}</span>
|
||||||
<!-- hidden for podcasts but still using this as a placeholder -->
|
<!-- hidden for podcasts but still using this as a placeholder -->
|
||||||
@@ -60,13 +60,13 @@
|
|||||||
<p class="text-xl font-mono text-success">{{ sleepTimeRemainingPretty }}</p>
|
<p class="text-xl font-mono text-success">{{ sleepTimeRemainingPretty }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="material-icons text-3xl text-white cursor-pointer" :class="chapters.length ? 'text-opacity-75' : 'text-opacity-10'" @click="showChapterModal = true">format_list_bulleted</span>
|
<span class="material-icons text-3xl text-white cursor-pointer" :class="chapters.length ? 'text-opacity-75' : 'text-opacity-10'" @click="clickChaptersBtn">format_list_bulleted</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: linear-gradient(145deg, rgba(38, 38, 38, 0.5) 0%, rgba(38, 38, 38, 0.9) 20%, rgb(38, 38, 38) 60%)" />
|
<div v-else class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: linear-gradient(145deg, rgba(38, 38, 38, 0.5) 0%, rgba(38, 38, 38, 0.9) 20%, rgb(38, 38, 38) 60%)" />
|
||||||
|
|
||||||
<div id="playerControls" class="absolute right-0 bottom-0 py-2">
|
<div id="playerControls" class="absolute right-0 bottom-0 mx-auto" style="max-width: 414px">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center max-w-full" :class="lockUi ? 'justify-center' : 'justify-between'">
|
||||||
<span v-show="showFullscreen && !lockUi" class="material-icons next-icon text-white text-opacity-75 cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
|
<span v-show="showFullscreen && !lockUi" class="material-icons next-icon text-white text-opacity-75 cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
|
||||||
<span v-show="!lockUi" class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">{{ jumpBackwardsIcon }}</span>
|
<span v-show="!lockUi" class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">{{ jumpBackwardsIcon }}</span>
|
||||||
<div class="play-btn cursor-pointer shadow-sm flex items-center justify-center rounded-full text-primary mx-4 relative overflow-hidden" :style="{ backgroundColor: coverRgb }" :class="{ 'animate-spin': seekLoading }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
|
<div class="play-btn cursor-pointer shadow-sm flex items-center justify-center rounded-full text-primary mx-4 relative overflow-hidden" :style="{ backgroundColor: coverRgb }" :class="{ 'animate-spin': seekLoading }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
|
||||||
@@ -80,20 +80,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="playerTrack" class="absolute left-0 w-full px-3">
|
<div id="playerTrack" class="absolute left-0 w-full px-6">
|
||||||
<div class="flex pb-0.5">
|
<div class="flex pointer-events-none">
|
||||||
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem" ref="currentTimestamp">0:00</p>
|
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem" ref="currentTimestamp">0:00</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p v-show="showFullscreen" class="text-sm truncate text-white text-opacity-75" style="max-width: 65%">{{ currentChapterTitle }}</p>
|
|
||||||
<div class="flex-grow" />
|
|
||||||
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ timeRemainingPretty }}</p>
|
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ timeRemainingPretty }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div ref="track" class="h-1.5 w-full bg-gray-500 bg-opacity-50 relative" :class="{ 'animate-pulse': isLoading }" @touchstart="touchstartTrack" @click="clickTrack">
|
<div ref="track" class="h-1.5 w-full bg-gray-500 bg-opacity-50 relative rounded-full" :class="{ 'animate-pulse': isLoading }" @click.stop>
|
||||||
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 rounded-full pointer-events-none" />
|
||||||
<div ref="bufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none" />
|
<div ref="bufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 rounded-full pointer-events-none" />
|
||||||
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 rounded-full pointer-events-none" />
|
||||||
<div ref="draggingTrack" class="h-full bg-warning bg-opacity-25 absolute top-0 left-0 pointer-events-none" />
|
<div ref="trackCursor" class="h-3.5 w-3.5 -top-1 rounded-full bg-gray-200 absolute pointer-events-auto" :class="{ 'opacity-0': lockUi || !showFullscreen }" @touchstart="touchstartCursor" />
|
||||||
<div ref="trackCursor" class="h-3.5 w-3.5 rounded-full bg-gray-200 absolute -top-1 pointer-events-none" :class="{ 'opacity-0': lockUi || !showFullscreen }" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -143,6 +140,7 @@ export default {
|
|||||||
onMetadataListener: null,
|
onMetadataListener: null,
|
||||||
onProgressSyncFailing: null,
|
onProgressSyncFailing: null,
|
||||||
onProgressSyncSuccess: null,
|
onProgressSyncSuccess: null,
|
||||||
|
onPlaybackSpeedChangedListener: null,
|
||||||
touchStartY: 0,
|
touchStartY: 0,
|
||||||
touchStartTime: 0,
|
touchStartTime: 0,
|
||||||
touchEndY: 0,
|
touchEndY: 0,
|
||||||
@@ -150,8 +148,10 @@ export default {
|
|||||||
useTotalTrack: true,
|
useTotalTrack: true,
|
||||||
lockUi: false,
|
lockUi: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
touchTrackStart: false,
|
isDraggingCursor: false,
|
||||||
dragPercent: 0,
|
draggingTouchStartX: 0,
|
||||||
|
draggingTouchStartTime: 0,
|
||||||
|
draggingCurrentTime: 0,
|
||||||
syncStatus: 0,
|
syncStatus: 0,
|
||||||
showMoreMenuDialog: false,
|
showMoreMenuDialog: false,
|
||||||
coverRgb: 'rgb(55, 56, 56)',
|
coverRgb: 'rgb(55, 56, 56)',
|
||||||
@@ -286,6 +286,7 @@ export default {
|
|||||||
return this.playMethod == this.$constants.PlayMethod.DIRECTPLAY
|
return this.playMethod == this.$constants.PlayMethod.DIRECTPLAY
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
|
if (this.currentChapterTitle && this.showFullscreen) return this.currentChapterTitle
|
||||||
if (this.playbackSession) return this.playbackSession.displayTitle
|
if (this.playbackSession) return this.playbackSession.displayTitle
|
||||||
return this.mediaMetadata ? this.mediaMetadata.title : 'Title'
|
return this.mediaMetadata ? this.mediaMetadata.title : 'Title'
|
||||||
},
|
},
|
||||||
@@ -317,17 +318,20 @@ export default {
|
|||||||
return this.$secondsToTimestamp(this.totalDuration)
|
return this.$secondsToTimestamp(this.totalDuration)
|
||||||
},
|
},
|
||||||
currentTimePretty() {
|
currentTimePretty() {
|
||||||
return this.$secondsToTimestamp(this.currentTime)
|
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
||||||
|
return this.$secondsToTimestamp(currentTimeToUse / this.currentPlaybackRate)
|
||||||
},
|
},
|
||||||
timeRemaining() {
|
timeRemaining() {
|
||||||
|
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
||||||
if (this.useChapterTrack && this.currentChapter) {
|
if (this.useChapterTrack && this.currentChapter) {
|
||||||
var currChapTime = this.currentTime - this.currentChapter.start
|
var currChapTime = currentTimeToUse - this.currentChapter.start
|
||||||
return (this.currentChapterDuration - currChapTime) / this.currentPlaybackRate
|
return (this.currentChapterDuration - currChapTime) / this.currentPlaybackRate
|
||||||
}
|
}
|
||||||
return this.totalTimeRemaining
|
return this.totalTimeRemaining
|
||||||
},
|
},
|
||||||
totalTimeRemaining() {
|
totalTimeRemaining() {
|
||||||
return (this.totalDuration - this.currentTime) / this.currentPlaybackRate
|
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
||||||
|
return (this.totalDuration - currentTimeToUse) / this.currentPlaybackRate
|
||||||
},
|
},
|
||||||
totalTimeRemainingPretty() {
|
totalTimeRemainingPretty() {
|
||||||
if (this.totalTimeRemaining < 0) {
|
if (this.totalTimeRemaining < 0) {
|
||||||
@@ -341,10 +345,6 @@ export default {
|
|||||||
}
|
}
|
||||||
return '-' + this.$secondsToTimestamp(this.timeRemaining)
|
return '-' + this.$secondsToTimestamp(this.timeRemaining)
|
||||||
},
|
},
|
||||||
timeLeftInChapter() {
|
|
||||||
if (!this.currentChapter) return 0
|
|
||||||
return this.currentChapter.end - this.currentTime
|
|
||||||
},
|
|
||||||
sleepTimeRemainingPretty() {
|
sleepTimeRemainingPretty() {
|
||||||
if (!this.sleepTimeRemaining) return '0s'
|
if (!this.sleepTimeRemaining) return '0s'
|
||||||
var secondsRemaining = Math.round(this.sleepTimeRemaining)
|
var secondsRemaining = Math.round(this.sleepTimeRemaining)
|
||||||
@@ -369,6 +369,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
clickChaptersBtn() {
|
||||||
|
if (!this.chapters.length) return
|
||||||
|
this.showChapterModal = true
|
||||||
|
},
|
||||||
async coverImageLoaded(fullCoverUrl) {
|
async coverImageLoaded(fullCoverUrl) {
|
||||||
if (!fullCoverUrl) return
|
if (!fullCoverUrl) return
|
||||||
|
|
||||||
@@ -391,11 +395,6 @@ export default {
|
|||||||
this.showFullscreen = false
|
this.showFullscreen = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async touchstartTrack(e) {
|
|
||||||
await this.$hapticsImpact()
|
|
||||||
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.lockUi) return
|
|
||||||
this.touchTrackStart = true
|
|
||||||
},
|
|
||||||
async selectChapter(chapter) {
|
async selectChapter(chapter) {
|
||||||
await this.$hapticsImpact()
|
await this.$hapticsImpact()
|
||||||
this.seek(chapter.start)
|
this.seek(chapter.start)
|
||||||
@@ -449,9 +448,9 @@ export default {
|
|||||||
this.$emit('showSleepTimer')
|
this.$emit('showSleepTimer')
|
||||||
},
|
},
|
||||||
async setPlaybackSpeed(speed) {
|
async setPlaybackSpeed(speed) {
|
||||||
await this.$hapticsImpact()
|
|
||||||
console.log(`[AudioPlayer] Set Playback Rate: ${speed}`)
|
console.log(`[AudioPlayer] Set Playback Rate: ${speed}`)
|
||||||
this.currentPlaybackRate = speed
|
this.currentPlaybackRate = speed
|
||||||
|
this.updateTimestamp()
|
||||||
AbsAudioPlayer.setPlaybackSpeed({ value: speed })
|
AbsAudioPlayer.setPlaybackSpeed({ value: speed })
|
||||||
},
|
},
|
||||||
restart() {
|
restart() {
|
||||||
@@ -502,19 +501,18 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateTimestamp() {
|
updateTimestamp() {
|
||||||
var ts = this.$refs.currentTimestamp
|
const ts = this.$refs.currentTimestamp
|
||||||
if (!ts) {
|
if (!ts) {
|
||||||
console.error('No timestamp el')
|
console.error('No timestamp el')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var currTimeStr = ''
|
|
||||||
|
let currentTime = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
||||||
if (this.useChapterTrack && this.currentChapter) {
|
if (this.useChapterTrack && this.currentChapter) {
|
||||||
var currChapTime = Math.max(0, this.currentTime - this.currentChapter.start)
|
currentTime = Math.max(0, currentTime - this.currentChapter.start)
|
||||||
currTimeStr = this.$secondsToTimestamp(currChapTime)
|
|
||||||
} else {
|
|
||||||
currTimeStr = this.$secondsToTimestamp(this.currentTime)
|
|
||||||
}
|
}
|
||||||
ts.innerText = currTimeStr
|
|
||||||
|
ts.innerText = this.$secondsToTimestamp(currentTime / this.currentPlaybackRate)
|
||||||
},
|
},
|
||||||
timeupdate() {
|
timeupdate() {
|
||||||
if (!this.$refs.playedTrack) {
|
if (!this.$refs.playedTrack) {
|
||||||
@@ -536,22 +534,24 @@ export default {
|
|||||||
},
|
},
|
||||||
updateTrack() {
|
updateTrack() {
|
||||||
// Update progress track UI
|
// Update progress track UI
|
||||||
let percentDone = this.currentTime / this.totalDuration
|
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
||||||
|
let percentDone = currentTimeToUse / this.totalDuration
|
||||||
const totalPercentDone = percentDone
|
const totalPercentDone = percentDone
|
||||||
let bufferedPercent = this.bufferedTime / this.totalDuration
|
let bufferedPercent = this.bufferedTime / this.totalDuration
|
||||||
const totalBufferedPercent = bufferedPercent
|
const totalBufferedPercent = bufferedPercent
|
||||||
|
|
||||||
if (this.useChapterTrack && this.currentChapter) {
|
if (this.useChapterTrack && this.currentChapter) {
|
||||||
const currChapTime = this.currentTime - this.currentChapter.start
|
const currChapTime = currentTimeToUse - this.currentChapter.start
|
||||||
percentDone = currChapTime / this.currentChapterDuration
|
percentDone = currChapTime / this.currentChapterDuration
|
||||||
bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration))
|
bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration))
|
||||||
}
|
}
|
||||||
|
|
||||||
const ptWidth = Math.round(percentDone * this.trackWidth)
|
const ptWidth = Math.round(percentDone * this.trackWidth)
|
||||||
this.$refs.playedTrack.style.width = ptWidth + 'px'
|
this.$refs.playedTrack.style.width = ptWidth + 'px'
|
||||||
this.$refs.bufferedTrack.style.width = Math.round(bufferedPercent * this.trackWidth) + 'px'
|
this.$refs.bufferedTrack.style.width = Math.round(bufferedPercent * this.trackWidth) + 'px'
|
||||||
|
|
||||||
if (this.$refs.trackCursor) {
|
if (this.$refs.trackCursor) {
|
||||||
this.$refs.trackCursor.style.left = ptWidth - 8 + 'px'
|
this.$refs.trackCursor.style.left = ptWidth - 7 + 'px'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.useChapterTrack) {
|
if (this.useChapterTrack) {
|
||||||
@@ -580,27 +580,15 @@ export default {
|
|||||||
this.$refs.playedTrack.classList.add('bg-yellow-300')
|
this.$refs.playedTrack.classList.add('bg-yellow-300')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clickTrack(e) {
|
async touchstartCursor(e) {
|
||||||
if (this.isLoading || this.lockUi) return
|
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.lockUi) return
|
||||||
if (!this.showFullscreen) {
|
|
||||||
// Track not clickable on mini-player
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (e) e.stopPropagation()
|
|
||||||
|
|
||||||
var offsetX = e.offsetX
|
await this.$hapticsImpact()
|
||||||
var perc = offsetX / this.trackWidth
|
this.isDraggingCursor = true
|
||||||
var time = 0
|
this.draggingTouchStartX = e.touches[0].pageX
|
||||||
if (this.useChapterTrack && this.currentChapter) {
|
this.draggingTouchStartTime = this.currentTime
|
||||||
time = perc * this.currentChapterDuration + this.currentChapter.start
|
this.draggingCurrentTime = this.currentTime
|
||||||
} else {
|
this.updateTrack()
|
||||||
time = perc * this.totalDuration
|
|
||||||
}
|
|
||||||
if (isNaN(time) || time === null) {
|
|
||||||
console.error('Invalid time', perc, time)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.seek(time)
|
|
||||||
},
|
},
|
||||||
async playPauseClick() {
|
async playPauseClick() {
|
||||||
await this.$hapticsImpact()
|
await this.$hapticsImpact()
|
||||||
@@ -653,24 +641,11 @@ export default {
|
|||||||
touchend(e) {
|
touchend(e) {
|
||||||
if (!e.changedTouches) return
|
if (!e.changedTouches) return
|
||||||
|
|
||||||
if (this.touchTrackStart) {
|
if (this.isDraggingCursor) {
|
||||||
var touch = e.changedTouches[0]
|
if (this.draggingCurrentTime !== this.currentTime) {
|
||||||
const touchOnTrackPos = touch.pageX - 12
|
this.seek(this.draggingCurrentTime)
|
||||||
const dragPercent = Math.max(0, Math.min(1, touchOnTrackPos / this.trackWidth))
|
|
||||||
|
|
||||||
var seekToTime = 0
|
|
||||||
if (this.useChapterTrack && this.currentChapter) {
|
|
||||||
const currChapTime = dragPercent * this.currentChapterDuration
|
|
||||||
seekToTime = this.currentChapter.start + currChapTime
|
|
||||||
} else {
|
|
||||||
seekToTime = dragPercent * this.totalDuration
|
|
||||||
}
|
}
|
||||||
this.seek(seekToTime)
|
this.isDraggingCursor = false
|
||||||
|
|
||||||
if (this.$refs.draggingTrack) {
|
|
||||||
this.$refs.draggingTrack.style.width = '0px'
|
|
||||||
}
|
|
||||||
this.touchTrackStart = false
|
|
||||||
} else if (this.showFullscreen) {
|
} else if (this.showFullscreen) {
|
||||||
this.touchEndY = e.changedTouches[0].screenY
|
this.touchEndY = e.changedTouches[0].screenY
|
||||||
var touchDuration = Date.now() - this.touchStartTime
|
var touchDuration = Date.now() - this.touchStartTime
|
||||||
@@ -682,29 +657,24 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
touchmove(e) {
|
touchmove(e) {
|
||||||
if (!this.touchTrackStart) return
|
if (!this.isDraggingCursor || !e.touches) return
|
||||||
|
|
||||||
var touch = e.touches[0]
|
const distanceMoved = e.touches[0].pageX - this.draggingTouchStartX
|
||||||
const touchOnTrackPos = touch.pageX - 12
|
let duration = this.totalDuration
|
||||||
const dragPercent = Math.max(0, Math.min(1, touchOnTrackPos / this.trackWidth))
|
let minTime = 0
|
||||||
this.dragPercent = dragPercent
|
let maxTime = duration
|
||||||
|
if (this.useChapterTrack && this.currentChapter) {
|
||||||
if (this.$refs.draggingTrack) {
|
duration = this.currentChapterDuration
|
||||||
this.$refs.draggingTrack.style.width = this.dragPercent * this.trackWidth + 'px'
|
minTime = this.currentChapter.start
|
||||||
|
maxTime = minTime + duration
|
||||||
}
|
}
|
||||||
|
|
||||||
var ts = this.$refs.currentTimestamp
|
const timePerPixel = duration / this.trackWidth
|
||||||
if (ts) {
|
const newTime = this.draggingTouchStartTime + timePerPixel * distanceMoved
|
||||||
var currTimeStr = ''
|
this.draggingCurrentTime = Math.min(maxTime, Math.max(minTime, newTime))
|
||||||
if (this.useChapterTrack && this.currentChapter) {
|
|
||||||
const currChapTime = dragPercent * this.currentChapterDuration
|
this.updateTimestamp()
|
||||||
currTimeStr = this.$secondsToTimestamp(currChapTime)
|
this.updateTrack()
|
||||||
} else {
|
|
||||||
const dragTime = dragPercent * this.totalDuration
|
|
||||||
currTimeStr = this.$secondsToTimestamp(dragTime)
|
|
||||||
}
|
|
||||||
ts.innerText = currTimeStr
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async clickMenuAction(action) {
|
async clickMenuAction(action) {
|
||||||
await this.$hapticsImpact()
|
await this.$hapticsImpact()
|
||||||
@@ -783,8 +753,6 @@ export default {
|
|||||||
|
|
||||||
console.log('received metadata update', data)
|
console.log('received metadata update', data)
|
||||||
|
|
||||||
if (data.currentRate && data.currentRate > 0) this.playbackSpeed = data.currentRate
|
|
||||||
|
|
||||||
this.timeupdate()
|
this.timeupdate()
|
||||||
},
|
},
|
||||||
// When a playback session is started the native android/ios will send the session
|
// When a playback session is started the native android/ios will send the session
|
||||||
@@ -815,6 +783,11 @@ export default {
|
|||||||
this.$toast.error(`Playback Failed: ${errorMessage}`)
|
this.$toast.error(`Playback Failed: ${errorMessage}`)
|
||||||
this.endPlayback()
|
this.endPlayback()
|
||||||
},
|
},
|
||||||
|
onPlaybackSpeedChanged(data) {
|
||||||
|
if (!data.value || isNaN(data.value)) return
|
||||||
|
this.currentPlaybackRate = Number(data.value)
|
||||||
|
this.updateTimestamp()
|
||||||
|
},
|
||||||
async init() {
|
async init() {
|
||||||
this.useChapterTrack = await this.$localStore.getUseChapterTrack()
|
this.useChapterTrack = await this.$localStore.getUseChapterTrack()
|
||||||
this.lockUi = await this.$localStore.getPlayerLock()
|
this.lockUi = await this.$localStore.getPlayerLock()
|
||||||
@@ -826,6 +799,7 @@ export default {
|
|||||||
this.onMetadataListener = AbsAudioPlayer.addListener('onMetadata', this.onMetadata)
|
this.onMetadataListener = AbsAudioPlayer.addListener('onMetadata', this.onMetadata)
|
||||||
this.onProgressSyncFailing = AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
|
this.onProgressSyncFailing = AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
|
||||||
this.onProgressSyncSuccess = AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
|
this.onProgressSyncSuccess = AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
|
||||||
|
this.onPlaybackSpeedChangedListener = AbsAudioPlayer.addListener('onPlaybackSpeedChanged', this.onPlaybackSpeedChanged)
|
||||||
},
|
},
|
||||||
screenOrientationChange() {
|
screenOrientationChange() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -846,10 +820,10 @@ export default {
|
|||||||
document.documentElement.style.setProperty('--cover-image-height', coverHeight + 'px')
|
document.documentElement.style.setProperty('--cover-image-height', coverHeight + 'px')
|
||||||
document.documentElement.style.setProperty('--cover-image-width-collapsed', coverImageWidthCollapsed + 'px')
|
document.documentElement.style.setProperty('--cover-image-width-collapsed', coverImageWidthCollapsed + 'px')
|
||||||
document.documentElement.style.setProperty('--cover-image-height-collapsed', 46 + 'px')
|
document.documentElement.style.setProperty('--cover-image-height-collapsed', 46 + 'px')
|
||||||
document.documentElement.style.setProperty('--title-author-left-offset-collapsed', 24 + coverImageWidthCollapsed + 'px')
|
document.documentElement.style.setProperty('--title-author-left-offset-collapsed', 30 + coverImageWidthCollapsed + 'px')
|
||||||
},
|
},
|
||||||
minimizePlayerEvt() {
|
minimizePlayerEvt() {
|
||||||
this.showFullscreen = false
|
this.collapseFullscreen()
|
||||||
},
|
},
|
||||||
showProgressSyncIsFailing() {
|
showProgressSyncIsFailing() {
|
||||||
this.syncStatus = this.$constants.SyncStatus.FAILED
|
this.syncStatus = this.$constants.SyncStatus.FAILED
|
||||||
@@ -901,6 +875,7 @@ export default {
|
|||||||
if (this.onPlaybackFailedListener) this.onPlaybackFailedListener.remove()
|
if (this.onPlaybackFailedListener) this.onPlaybackFailedListener.remove()
|
||||||
if (this.onProgressSyncFailing) this.onProgressSyncFailing.remove()
|
if (this.onProgressSyncFailing) this.onProgressSyncFailing.remove()
|
||||||
if (this.onProgressSyncSuccess) this.onProgressSyncSuccess.remove()
|
if (this.onProgressSyncSuccess) this.onProgressSyncSuccess.remove()
|
||||||
|
if (this.onPlaybackSpeedChangedListener) this.onPlaybackSpeedChangedListener.remove()
|
||||||
clearInterval(this.playInterval)
|
clearInterval(this.playInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -912,11 +887,11 @@ export default {
|
|||||||
--cover-image-height: 0px;
|
--cover-image-height: 0px;
|
||||||
--cover-image-width-collapsed: 46px;
|
--cover-image-width-collapsed: 46px;
|
||||||
--cover-image-height-collapsed: 46px;
|
--cover-image-height-collapsed: 46px;
|
||||||
--title-author-left-offset-collapsed: 70px;
|
--title-author-left-offset-collapsed: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playerContainer {
|
.playerContainer {
|
||||||
height: 100px;
|
height: 120px;
|
||||||
}
|
}
|
||||||
.fullscreen .playerContainer {
|
.fullscreen .playerContainer {
|
||||||
height: 200px;
|
height: 200px;
|
||||||
@@ -931,21 +906,22 @@ export default {
|
|||||||
#playerTrack {
|
#playerTrack {
|
||||||
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
||||||
transition-property: margin;
|
transition-property: margin;
|
||||||
bottom: 20px;
|
bottom: 35px;
|
||||||
}
|
}
|
||||||
.fullscreen #playerTrack {
|
.fullscreen #playerTrack {
|
||||||
top: 20px;
|
|
||||||
bottom: unset;
|
bottom: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cover-wrapper {
|
.cover-wrapper {
|
||||||
bottom: 48px;
|
bottom: 68px;
|
||||||
left: 12px;
|
left: 24px;
|
||||||
height: var(--cover-image-height-collapsed);
|
height: var(--cover-image-height-collapsed);
|
||||||
width: var(--cover-image-width-collapsed);
|
width: var(--cover-image-width-collapsed);
|
||||||
transition: all 0.25s cubic-bezier(0.39, 0.575, 0.565, 1);
|
transition: all 0.25s cubic-bezier(0.39, 0.575, 0.565, 1);
|
||||||
transition-property: left, bottom, width, height;
|
transition-property: left, bottom, width, height;
|
||||||
transform-origin: left bottom;
|
transform-origin: left bottom;
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.total-track {
|
.total-track {
|
||||||
@@ -960,7 +936,7 @@ export default {
|
|||||||
transform-origin: left bottom;
|
transform-origin: left bottom;
|
||||||
|
|
||||||
width: 40%;
|
width: 40%;
|
||||||
bottom: 56px;
|
bottom: 76px;
|
||||||
left: var(--title-author-left-offset-collapsed);
|
left: var(--title-author-left-offset-collapsed);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
@@ -986,20 +962,18 @@ export default {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
.fullscreen .title-author-texts .title-text {
|
.fullscreen .title-author-texts .title-text {
|
||||||
font-size: clamp(0.8rem, calc(var(--cover-image-height) / 260 * 20), 1.5rem);
|
font-size: clamp(0.8rem, calc(var(--cover-image-height) / 260 * 20), 1.3rem);
|
||||||
}
|
}
|
||||||
.fullscreen .title-author-texts .author-text {
|
.fullscreen .title-author-texts .author-text {
|
||||||
font-size: clamp(0.6rem, calc(var(--cover-image-height) / 260 * 16), 1.1rem);
|
font-size: clamp(0.6rem, calc(var(--cover-image-height) / 260 * 16), 1rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
#playerControls {
|
#playerControls {
|
||||||
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
||||||
transition-property: width, bottom;
|
transition-property: width, bottom;
|
||||||
height: 48px;
|
width: 128px;
|
||||||
width: 140px;
|
padding-right: 24px;
|
||||||
padding-left: 12px;
|
bottom: 70px;
|
||||||
padding-right: 12px;
|
|
||||||
bottom: 50px;
|
|
||||||
}
|
}
|
||||||
#playerControls .jump-icon {
|
#playerControls .jump-icon {
|
||||||
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
||||||
@@ -1016,8 +990,7 @@ export default {
|
|||||||
width: 40px;
|
width: 40px;
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
margin: 0px 14px;
|
margin: 0px 7px;
|
||||||
/* padding: 8px; */
|
|
||||||
}
|
}
|
||||||
#playerControls .play-btn .material-icons {
|
#playerControls .play-btn .material-icons {
|
||||||
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
||||||
@@ -1032,27 +1005,28 @@ export default {
|
|||||||
width: var(--cover-image-width);
|
width: var(--cover-image-width);
|
||||||
left: calc(50% - (calc(var(--cover-image-width)) / 2));
|
left: calc(50% - (calc(var(--cover-image-width)) / 2));
|
||||||
bottom: calc(50% + 120px - (calc(var(--cover-image-height)) / 2));
|
bottom: calc(50% + 120px - (calc(var(--cover-image-height)) / 2));
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullscreen #playerControls {
|
.fullscreen #playerControls {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
bottom: 94px;
|
padding-left: 24px;
|
||||||
|
padding-right: 24px;
|
||||||
|
bottom: 78px;
|
||||||
|
left: 0;
|
||||||
}
|
}
|
||||||
.fullscreen #playerControls .jump-icon {
|
.fullscreen #playerControls .jump-icon {
|
||||||
margin: 0px 18px;
|
|
||||||
font-size: 2.4rem;
|
font-size: 2.4rem;
|
||||||
}
|
}
|
||||||
.fullscreen #playerControls .next-icon {
|
.fullscreen #playerControls .next-icon {
|
||||||
margin: 0px 20px;
|
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
.fullscreen #playerControls .play-btn {
|
.fullscreen #playerControls .play-btn {
|
||||||
/* padding: 16px; */
|
|
||||||
height: 65px;
|
height: 65px;
|
||||||
width: 65px;
|
width: 65px;
|
||||||
min-width: 65px;
|
min-width: 65px;
|
||||||
min-height: 65px;
|
min-height: 65px;
|
||||||
margin: 0px 26px;
|
|
||||||
}
|
}
|
||||||
.fullscreen #playerControls .play-btn .material-icons {
|
.fullscreen #playerControls .play-btn .material-icons {
|
||||||
font-size: 2.1rem;
|
font-size: 2.1rem;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<app-audio-player ref="audioPlayer" :bookmarks="bookmarks" :sleep-timer-running="isSleepTimerRunning" :sleep-time-remaining="sleepTimeRemaining" :is-server-item="!!serverLibraryItemId" @selectPlaybackSpeed="showPlaybackSpeedModal = true" @updateTime="(t) => (currentTime = t)" @showSleepTimer="showSleepTimer" @showBookmarks="showBookmarks" />
|
<app-audio-player ref="audioPlayer" :bookmarks="bookmarks" :sleep-timer-running="isSleepTimerRunning" :sleep-time-remaining="sleepTimeRemaining" :is-server-item="!!serverLibraryItemId" @selectPlaybackSpeed="showPlaybackSpeedModal = true" @updateTime="(t) => (currentTime = t)" @showSleepTimer="showSleepTimer" @showBookmarks="showBookmarks" />
|
||||||
|
|
||||||
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-rate.sync="playbackSpeed" @update:playbackRate="updatePlaybackSpeed" @change="changePlaybackSpeed" />
|
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-rate.sync="playbackSpeed" @update:playbackRate="updatePlaybackSpeed" @change="changePlaybackSpeed" />
|
||||||
<modals-sleep-timer-modal v-model="showSleepTimerModal" :current-time="sleepTimeRemaining" :sleep-timer-running="isSleepTimerRunning" :current-end-of-chapter-time="currentEndOfChapterTime" @change="selectSleepTimeout" @cancel="cancelSleepTimer" @increase="increaseSleepTimer" @decrease="decreaseSleepTimer" />
|
<modals-sleep-timer-modal v-model="showSleepTimerModal" :current-time="sleepTimeRemaining" :sleep-timer-running="isSleepTimerRunning" :current-end-of-chapter-time="currentEndOfChapterTime" :is-auto="isAutoSleepTimer" @change="selectSleepTimeout" @cancel="cancelSleepTimer" @increase="increaseSleepTimer" @decrease="decreaseSleepTimer" />
|
||||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="currentTime" :library-item-id="serverLibraryItemId" @select="selectBookmark" />
|
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="currentTime" :library-item-id="serverLibraryItemId" @select="selectBookmark" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -28,6 +28,7 @@ export default {
|
|||||||
isSleepTimerRunning: false,
|
isSleepTimerRunning: false,
|
||||||
sleepTimerEndTime: 0,
|
sleepTimerEndTime: 0,
|
||||||
sleepTimeRemaining: 0,
|
sleepTimeRemaining: 0,
|
||||||
|
isAutoSleepTimer: false,
|
||||||
onLocalMediaProgressUpdateListener: null,
|
onLocalMediaProgressUpdateListener: null,
|
||||||
onSleepTimerEndedListener: null,
|
onSleepTimerEndedListener: null,
|
||||||
onSleepTimerSetListener: null,
|
onSleepTimerSetListener: null,
|
||||||
@@ -42,6 +43,9 @@ export default {
|
|||||||
bookmarks() {
|
bookmarks() {
|
||||||
if (!this.serverLibraryItemId) return []
|
if (!this.serverLibraryItemId) return []
|
||||||
return this.$store.getters['user/getUserBookmarksForItem'](this.serverLibraryItemId)
|
return this.$store.getters['user/getUserBookmarksForItem'](this.serverLibraryItemId)
|
||||||
|
},
|
||||||
|
isIos() {
|
||||||
|
return this.$platform === 'ios'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -62,8 +66,9 @@ export default {
|
|||||||
console.log('Sleep Timer Ended Current Position: ' + currentPosition)
|
console.log('Sleep Timer Ended Current Position: ' + currentPosition)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSleepTimerSet({ value: sleepTimeRemaining }) {
|
onSleepTimerSet(payload) {
|
||||||
console.log('SLEEP TIMER SET', sleepTimeRemaining)
|
const { value: sleepTimeRemaining, isAuto } = payload
|
||||||
|
console.log('SLEEP TIMER SET', JSON.stringify(payload))
|
||||||
if (sleepTimeRemaining === 0) {
|
if (sleepTimeRemaining === 0) {
|
||||||
console.log('Sleep timer canceled')
|
console.log('Sleep timer canceled')
|
||||||
this.isSleepTimerRunning = false
|
this.isSleepTimerRunning = false
|
||||||
@@ -71,6 +76,7 @@ export default {
|
|||||||
this.isSleepTimerRunning = true
|
this.isSleepTimerRunning = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isAutoSleepTimer = !!isAuto
|
||||||
this.sleepTimeRemaining = sleepTimeRemaining
|
this.sleepTimeRemaining = sleepTimeRemaining
|
||||||
},
|
},
|
||||||
showSleepTimer() {
|
showSleepTimer() {
|
||||||
@@ -185,6 +191,7 @@ export default {
|
|||||||
const libraryItemId = payload.libraryItemId
|
const libraryItemId = payload.libraryItemId
|
||||||
const episodeId = payload.episodeId
|
const episodeId = payload.episodeId
|
||||||
const startTime = payload.startTime
|
const startTime = payload.startTime
|
||||||
|
const startWhenReady = !payload.paused
|
||||||
|
|
||||||
// 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
|
||||||
@@ -207,6 +214,8 @@ export default {
|
|||||||
if (startTime !== undefined && startTime !== null) {
|
if (startTime !== undefined && startTime !== null) {
|
||||||
// seek to start time
|
// seek to start time
|
||||||
AbsAudioPlayer.seek({ value: Math.floor(startTime) })
|
AbsAudioPlayer.seek({ value: Math.floor(startTime) })
|
||||||
|
} else if (this.$refs.audioPlayer) {
|
||||||
|
this.$refs.audioPlayer.play()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -220,7 +229,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('Called playLibraryItem', libraryItemId)
|
console.log('Called playLibraryItem', libraryItemId)
|
||||||
const preparePayload = { libraryItemId, episodeId, playWhenReady: true, playbackRate }
|
const preparePayload = { libraryItemId, episodeId, playWhenReady: startWhenReady, playbackRate }
|
||||||
if (startTime !== undefined && startTime !== null) preparePayload.startTime = startTime
|
if (startTime !== undefined && startTime !== null) preparePayload.startTime = startTime
|
||||||
AbsAudioPlayer.prepareLibraryItem(preparePayload)
|
AbsAudioPlayer.prepareLibraryItem(preparePayload)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@@ -265,9 +274,13 @@ export default {
|
|||||||
this.notifyOnReady()
|
this.notifyOnReady()
|
||||||
},
|
},
|
||||||
notifyOnReady() {
|
notifyOnReady() {
|
||||||
|
// TODO: iOS opens last active playback session on app launch. Should be consistent with Android
|
||||||
|
if (!this.isIos) return
|
||||||
|
|
||||||
// If settings aren't loaded yet, native player will receive incorrect settings
|
// If settings aren't loaded yet, native player will receive incorrect settings
|
||||||
console.log('Notify on ready... settingsLoaded:', this.settingsLoaded, 'isReady:', this.isReady)
|
console.log('Notify on ready... settingsLoaded:', this.settingsLoaded, 'isReady:', this.isReady)
|
||||||
if (this.settingsLoaded && this.isReady) {
|
if (this.settingsLoaded && this.isReady && this.$store.state.isFirstAudioLoad) {
|
||||||
|
this.$store.commit('setIsFirstAudioLoad', false) // Only run this once on app launch
|
||||||
AbsAudioPlayer.onReady()
|
AbsAudioPlayer.onReady()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
showBookshelfListView(newVal) {
|
showBookshelfListView(newVal) {
|
||||||
this.resetEntities()
|
this.resetEntities()
|
||||||
|
},
|
||||||
|
seriesId() {
|
||||||
|
this.resetEntities()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -85,6 +88,12 @@ export default {
|
|||||||
filterBy() {
|
filterBy() {
|
||||||
return this.$store.getters['user/getUserSetting']('mobileFilterBy')
|
return this.$store.getters['user/getUserSetting']('mobileFilterBy')
|
||||||
},
|
},
|
||||||
|
collapseSeries() {
|
||||||
|
return this.$store.getters['user/getUserSetting']('collapseSeries')
|
||||||
|
},
|
||||||
|
collapseBookSeries() {
|
||||||
|
return this.$store.getters['user/getUserSetting']('collapseBookSeries')
|
||||||
|
},
|
||||||
isCoverSquareAspectRatio() {
|
isCoverSquareAspectRatio() {
|
||||||
return this.bookCoverAspectRatio === 1
|
return this.bookCoverAspectRatio === 1
|
||||||
},
|
},
|
||||||
@@ -356,6 +365,9 @@ export default {
|
|||||||
let searchParams = new URLSearchParams()
|
let searchParams = new URLSearchParams()
|
||||||
if (this.page === 'series-books') {
|
if (this.page === 'series-books') {
|
||||||
searchParams.set('filter', `series.${this.$encode(this.seriesId)}`)
|
searchParams.set('filter', `series.${this.$encode(this.seriesId)}`)
|
||||||
|
if (this.collapseBookSeries) {
|
||||||
|
searchParams.set('collapseseries', 1)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.filterBy && this.filterBy !== 'all') {
|
if (this.filterBy && this.filterBy !== 'all') {
|
||||||
searchParams.set('filter', this.filterBy)
|
searchParams.set('filter', this.filterBy)
|
||||||
@@ -452,6 +464,12 @@ export default {
|
|||||||
this.libraryItemUpdated(ab)
|
this.libraryItemUpdated(ab)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
screenOrientationChange() {
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('LazyBookshelf Screen orientation change')
|
||||||
|
this.resetEntities()
|
||||||
|
}, 50)
|
||||||
|
},
|
||||||
initListeners() {
|
initListeners() {
|
||||||
const bookshelf = document.getElementById('bookshelf-wrapper')
|
const bookshelf = document.getElementById('bookshelf-wrapper')
|
||||||
if (bookshelf) {
|
if (bookshelf) {
|
||||||
@@ -466,6 +484,13 @@ export default {
|
|||||||
this.$socket.$on('item_removed', this.libraryItemRemoved)
|
this.$socket.$on('item_removed', this.libraryItemRemoved)
|
||||||
this.$socket.$on('items_updated', this.libraryItemsUpdated)
|
this.$socket.$on('items_updated', this.libraryItemsUpdated)
|
||||||
this.$socket.$on('items_added', this.libraryItemsAdded)
|
this.$socket.$on('items_added', this.libraryItemsAdded)
|
||||||
|
|
||||||
|
if (screen.orientation) {
|
||||||
|
// Not available on ios
|
||||||
|
screen.orientation.addEventListener('change', this.screenOrientationChange)
|
||||||
|
} else {
|
||||||
|
document.addEventListener('orientationchange', this.screenOrientationChange)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
removeListeners() {
|
removeListeners() {
|
||||||
const bookshelf = document.getElementById('bookshelf-wrapper')
|
const bookshelf = document.getElementById('bookshelf-wrapper')
|
||||||
@@ -481,6 +506,13 @@ export default {
|
|||||||
this.$socket.$off('item_removed', this.libraryItemRemoved)
|
this.$socket.$off('item_removed', this.libraryItemRemoved)
|
||||||
this.$socket.$off('items_updated', this.libraryItemsUpdated)
|
this.$socket.$off('items_updated', this.libraryItemsUpdated)
|
||||||
this.$socket.$off('items_added', this.libraryItemsAdded)
|
this.$socket.$off('items_added', this.libraryItemsAdded)
|
||||||
|
|
||||||
|
if (screen.orientation) {
|
||||||
|
// Not available on ios
|
||||||
|
screen.orientation.removeEventListener('change', this.screenOrientationChange)
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('orientationchange', this.screenOrientationChange)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updated() {
|
updated() {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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 v-if="!altViewEnabled" class="absolute text-center categoryPlacardtransform z-30 bottom-0.5 left-4 md:left-8 w-36 rounded-md" style="height: 18px">
|
||||||
<div class="w-full h-full flex items-center justify-center rounded-sm border shinyBlack">
|
<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>
|
||||||
|
|||||||
@@ -7,26 +7,6 @@
|
|||||||
<p class="truncate text-sm">{{ name }}</p>
|
<p class="truncate text-sm">{{ name }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="flex h-full px-1 overflow-hidden shadow-sm">
|
|
||||||
<div style="max-height: 48px; max-width: 48px" class="w-12 h-12 bg-primary overflow-hidden rounded">
|
|
||||||
<svg width="140%" height="140%" style="margin-left: -20%; margin-top: -20%; opacity: 0.6" viewBox="0 0 177 266" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill="white" d="M40.7156 165.47C10.2694 150.865 -31.5407 148.629 -38.0532 155.529L63.3191 204.159L76.9443 190.899C66.828 181.394 54.006 171.846 40.7156 165.47Z" stroke="white" stroke-width="4" transform="translate(-2 -1)" />
|
|
||||||
<path d="M-38.0532 155.529C-31.5407 148.629 10.2694 150.865 40.7156 165.47C54.006 171.846 66.828 181.394 76.9443 190.899L95.0391 173.37C80.6681 159.403 64.7526 149.155 51.5747 142.834C21.3549 128.337 -46.2471 114.563 -60.6897 144.67L-71.5489 167.307L44.5864 223.019L63.3191 204.159L-38.0532 155.529Z" fill="white" />
|
|
||||||
<path
|
|
||||||
d="M105.87 29.6508C80.857 17.6515 50.8784 28.1923 38.879 53.2056C26.8797 78.219 37.4205 108.198 62.4338 120.197C87.4472 132.196 117.426 121.656 129.425 96.6422C141.425 71.6288 130.884 41.6502 105.87 29.6508ZM106.789 85.783C112.761 73.3329 107.461 58.2599 95.0112 52.2874C82.5611 46.3148 67.4881 51.6147 61.5156 64.0648C55.543 76.5149 60.8429 91.5879 73.293 97.5604C85.7431 103.533 100.816 98.2331 106.789 85.783Z"
|
|
||||||
fill="white"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M151.336 159.01L159.048 166.762L82.7048 242.703L74.973 242.683L74.9934 234.951L151.336 159.01ZM181.725 108.497C179.624 108.491 177.436 109.326 175.835 110.918L160.415 126.257L191.848 157.856L207.268 142.517C210.554 139.248 210.568 133.954 207.299 130.667L187.685 110.95C186.009 109.264 183.91 108.502 181.725 108.497ZM151.399 135.226L58.2034 227.931L58.1203 259.447L89.6359 259.53L182.831 166.825L151.399 135.226Z"
|
|
||||||
fill="white"
|
|
||||||
/>
|
|
||||||
<path d="M151.336 159.01L159.048 166.762L82.7048 242.703L74.973 242.683L74.9934 234.951L151.336 159.01Z" fill="white" stroke="white" stroke-width="10px" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="flex-grow px-2 authorSearchCardContent h-full">
|
|
||||||
<p class="truncate text-sm">{{ author }}</p>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p>
|
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p>
|
||||||
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
|
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
|
||||||
|
|
||||||
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
|
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin' || matchKey === 'episode' || matchKey === 'narrators'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -78,11 +78,14 @@ export default {
|
|||||||
var lastPart = this.matchText.substr(totalLenSoFar)
|
var lastPart = this.matchText.substr(totalLenSoFar)
|
||||||
html += lastPart
|
html += lastPart
|
||||||
|
|
||||||
|
if (this.matchKey === 'episode') return `<p class="truncate">Episode: ${html}</p>`
|
||||||
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
|
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
|
||||||
|
if (this.matchKey === 'subtitle') return `<p class="truncate">${html}</p>`
|
||||||
if (this.matchKey === 'authors') return `by ${html}`
|
if (this.matchKey === 'authors') return `by ${html}`
|
||||||
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
|
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
|
||||||
if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
|
if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
|
||||||
if (this.matchKey === 'series') return `<p class="truncate">Series: ${html}</p>`
|
if (this.matchKey === 'series') return `<p class="truncate">Series: ${html}</p>`
|
||||||
|
if (this.matchKey === 'narrators') return `<p class="truncate">Narrator: ${html}</p>`
|
||||||
return `${html}`
|
return `${html}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,15 +10,20 @@
|
|||||||
<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' }">{{ displayLineTwo || ' ' }}</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="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }" style="background-color: #78350f">
|
||||||
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequenceList }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="booksInSeries" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }" style="background-color: #cd9d49dd">
|
||||||
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ booksInSeries }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
||||||
<div v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
|
<div v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="text-gray-300 text-center">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
<img v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
||||||
@@ -26,16 +31,16 @@
|
|||||||
<!-- Placeholder Cover Title & Author -->
|
<!-- Placeholder Cover Title & Author -->
|
||||||
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-center font-book" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p>
|
<p class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
||||||
<p class="text-center font-book" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
|
<p class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Play/pause button for podcast episode -->
|
<!-- Play/pause button for podcast episode -->
|
||||||
<div v-if="recentEpisode" class="absolute z-10 top-0 left-0 bottom-0 right-0 m-auto flex items-center justify-center w-12 h-12 rounded-full bg-white bg-opacity-70">
|
<div v-if="recentEpisode" class="absolute z-10 top-0 left-0 bottom-0 right-0 m-auto flex items-center justify-center w-12 h-12 rounded-full bg-white bg-opacity-70" @click.stop="playEpisode">
|
||||||
<span class="material-icons text-6xl text-black text-opacity-80">{{ streamIsPlaying ? 'pause_circle' : 'play_circle_filled' }}</span>
|
<span class="material-icons text-6xl text-black text-opacity-80">{{ streamIsPlaying ? 'pause_circle' : 'play_circle_filled' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -57,8 +62,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Podcast Episode # -->
|
<!-- Podcast Episode # -->
|
||||||
<div v-if="recentEpisodeNumber && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
<div v-if="recentEpisodeNumber !== null && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">Episode #{{ recentEpisodeNumber }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">
|
||||||
|
Episode<span v-if="recentEpisodeNumber"> #{{ recentEpisodeNumber }}</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Podcast Num Episodes -->
|
<!-- Podcast Num Episodes -->
|
||||||
@@ -216,7 +223,7 @@ export default {
|
|||||||
if (this.recentEpisode.episode) {
|
if (this.recentEpisode.episode) {
|
||||||
return this.recentEpisode.episode.replace(/^#/, '')
|
return this.recentEpisode.episode.replace(/^#/, '')
|
||||||
}
|
}
|
||||||
return this.recentEpisode.index
|
return ''
|
||||||
},
|
},
|
||||||
collapsedSeries() {
|
collapsedSeries() {
|
||||||
// Only added to item object when collapseSeries is enabled
|
// Only added to item object when collapseSeries is enabled
|
||||||
@@ -226,22 +233,36 @@ export default {
|
|||||||
// Only added to item object when collapseSeries is enabled
|
// Only added to item object when collapseSeries is enabled
|
||||||
return this.collapsedSeries ? this.collapsedSeries.numBooks : 0
|
return this.collapsedSeries ? this.collapsedSeries.numBooks : 0
|
||||||
},
|
},
|
||||||
displayTitle() {
|
seriesSequenceList() {
|
||||||
if (this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix && this.title.toLowerCase().startsWith('the ')) {
|
return this.collapsedSeries ? this.collapsedSeries.seriesSequenceList : null
|
||||||
return this.title.substr(4) + ', The'
|
|
||||||
}
|
|
||||||
return this.title
|
|
||||||
},
|
},
|
||||||
displayAuthor() {
|
libraryItemIdsInSeries() {
|
||||||
|
// Only added to item object when collapseSeries is enabled
|
||||||
|
return this.collapsedSeries ? this.collapsedSeries.libraryItemIds || [] : []
|
||||||
|
},
|
||||||
|
displayTitle() {
|
||||||
|
if (this.recentEpisode) return this.recentEpisode.title
|
||||||
|
|
||||||
|
const ignorePrefix = this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix
|
||||||
|
if (this.collapsedSeries) return ignorePrefix ? this.collapsedSeries.nameIgnorePrefix : this.collapsedSeries.name
|
||||||
|
return ignorePrefix ? this.mediaMetadata.titleIgnorePrefix : this.title
|
||||||
|
},
|
||||||
|
displayLineTwo() {
|
||||||
|
if (this.recentEpisode) return this.title
|
||||||
|
if (this.collapsedSeries) return ''
|
||||||
|
if (this.isPodcast) return this.author
|
||||||
|
|
||||||
if (this.orderBy === 'media.metadata.authorNameLF') return this.authorLF
|
if (this.orderBy === 'media.metadata.authorNameLF') return this.authorLF
|
||||||
return this.author
|
return this.author
|
||||||
},
|
},
|
||||||
displaySortLine() {
|
displaySortLine() {
|
||||||
|
if (this.collapsedSeries) return null
|
||||||
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 === 'media.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)
|
||||||
|
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
episodeProgress() {
|
episodeProgress() {
|
||||||
@@ -255,8 +276,13 @@ export default {
|
|||||||
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)
|
||||||
},
|
},
|
||||||
|
useEBookProgress() {
|
||||||
|
if (!this.userProgress || this.userProgress.progress) return false
|
||||||
|
return this.userProgress.ebookProgress > 0
|
||||||
|
},
|
||||||
userProgressPercent() {
|
userProgressPercent() {
|
||||||
return this.userProgress ? this.userProgress.progress || 0 : 0
|
if (this.useEBookProgress) return Math.max(Math.min(1, this.userProgress.ebookProgress), 0)
|
||||||
|
return this.userProgress ? Math.max(Math.min(1, this.userProgress.progress), 0) || 0 : 0
|
||||||
},
|
},
|
||||||
itemIsFinished() {
|
itemIsFinished() {
|
||||||
return this.userProgress ? !!this.userProgress.isFinished : false
|
return this.userProgress ? !!this.userProgress.isFinished : false
|
||||||
@@ -401,38 +427,40 @@ export default {
|
|||||||
// Server books may have a local library item
|
// Server books may have a local library item
|
||||||
this.localLibraryItem = localLibraryItem
|
this.localLibraryItem = localLibraryItem
|
||||||
},
|
},
|
||||||
|
async playEpisode() {
|
||||||
|
await this.$hapticsImpact()
|
||||||
|
const eventBus = this.$eventBus || this.$nuxt.$eventBus
|
||||||
|
if (this.streamIsPlaying) {
|
||||||
|
eventBus.$emit('pause-item')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.localLibraryItem) {
|
||||||
|
const localEpisode = this.localLibraryItem.media.episodes.find((ep) => ep.serverEpisodeId === this.recentEpisode.id)
|
||||||
|
if (localEpisode) {
|
||||||
|
// Play episode locally
|
||||||
|
eventBus.$emit('play-item', {
|
||||||
|
libraryItemId: this.localLibraryItemId,
|
||||||
|
episodeId: localEpisode.id,
|
||||||
|
serverLibraryItemId: this.libraryItemId,
|
||||||
|
serverEpisodeId: this.recentEpisode.id
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventBus.$emit('play-item', { libraryItemId: this.libraryItemId, episodeId: this.recentEpisode.id })
|
||||||
|
},
|
||||||
async clickCard(e) {
|
async clickCard(e) {
|
||||||
if (this.isSelectionMode) {
|
if (this.isSelectionMode) {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.selectBtnClick()
|
this.selectBtnClick()
|
||||||
} else if (this.recentEpisode) {
|
|
||||||
await this.$hapticsImpact()
|
|
||||||
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
|
||||||
if (this.streamIsPlaying) {
|
|
||||||
eventBus.$emit('pause-item')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.localLibraryItem) {
|
|
||||||
const localEpisode = this.localLibraryItem.media.episodes.find((ep) => ep.serverEpisodeId === this.recentEpisode.id)
|
|
||||||
if (localEpisode) {
|
|
||||||
// Play episode locally
|
|
||||||
eventBus.$emit('play-item', {
|
|
||||||
libraryItemId: this.localLibraryItemId,
|
|
||||||
episodeId: localEpisode.id,
|
|
||||||
serverLibraryItemId: this.libraryItemId,
|
|
||||||
serverEpisodeId: this.recentEpisode.id
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eventBus.$emit('play-item', { libraryItemId: this.libraryItemId, episodeId: this.recentEpisode.id })
|
|
||||||
} else {
|
} else {
|
||||||
var router = this.$router || this.$nuxt.$router
|
const router = this.$router || this.$nuxt.$router
|
||||||
if (router) {
|
if (router) {
|
||||||
if (this.collapsedSeries) router.push(`/library/${this.libraryId}/series/${this.collapsedSeries.id}`)
|
if (this.recentEpisode) router.push(`/item/${this.libraryItemId}/${this.recentEpisode.id}`)
|
||||||
|
else if (this.collapsedSeries) router.push(`/bookshelf/series/${this.collapsedSeries.id}`)
|
||||||
else router.push(`/item/${this.libraryItemId}`)
|
else router.push(`/item/${this.libraryItemId}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<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(240, width) + 'px' }">
|
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-mdtext-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` }">
|
<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>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||||
<covers-playlist-cover ref="cover" :items="items" :width="width" :height="height" />
|
<covers-playlist-cover ref="cover" :items="items" :width="width" :height="height" />
|
||||||
</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-mdtext-center" :style="{ width: Math.min(160, 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` }">
|
<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>
|
||||||
|
|||||||
@@ -5,10 +5,12 @@
|
|||||||
<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="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
|
||||||
|
|
||||||
<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 v-if="isAltViewEnabled && isCategorized" class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
</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(240, width) + 'px' }">
|
<div v-if="!isCategorized" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-mdtext-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` }">
|
<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>
|
||||||
@@ -53,6 +55,27 @@ export default {
|
|||||||
books() {
|
books() {
|
||||||
return this.series ? this.series.books || [] : []
|
return this.series ? this.series.books || [] : []
|
||||||
},
|
},
|
||||||
|
seriesBookProgress() {
|
||||||
|
return this.books
|
||||||
|
.map((libraryItem) => {
|
||||||
|
return this.store.getters['user/getUserMediaProgress'](libraryItem.id)
|
||||||
|
})
|
||||||
|
.filter((p) => !!p)
|
||||||
|
},
|
||||||
|
seriesBooksFinished() {
|
||||||
|
return this.seriesBookProgress.filter((p) => p.isFinished)
|
||||||
|
},
|
||||||
|
hasSeriesBookInProgress() {
|
||||||
|
return this.seriesBookProgress.some((p) => !p.isFinished && p.progress > 0)
|
||||||
|
},
|
||||||
|
seriesPercentInProgress() {
|
||||||
|
let totalFinishedAndInProgress = this.seriesBooksFinished.length
|
||||||
|
if (this.hasSeriesBookInProgress) totalFinishedAndInProgress += 1
|
||||||
|
return Math.min(1, Math.max(0, totalFinishedAndInProgress / this.books.length))
|
||||||
|
},
|
||||||
|
isSeriesFinished() {
|
||||||
|
return this.books.length === this.seriesBooksFinished.length
|
||||||
|
},
|
||||||
store() {
|
store() {
|
||||||
return this.$store || this.$nuxt.$store
|
return this.$store || this.$nuxt.$store
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex h-full px-1 overflow-hidden">
|
||||||
|
<div class="w-10 h-10 flex items-center justify-center">
|
||||||
|
<span class="material-icons text-2xl text-gray-200">record_voice_over</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow px-2 narratorSearchCardContent h-full">
|
||||||
|
<p class="truncate text-sm">{{ narrator }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
narrator: String
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.narratorSearchCardContent {
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative rounded-sm overflow-hidden" :style="{ height: height + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }">
|
<div class="relative rounded-sm overflow-hidden" :style="{ height: height + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }">
|
||||||
<div class="w-full h-full relative bg-bg">
|
<div class="w-full h-full relative" :class="{ 'bg-bg': !noBg }">
|
||||||
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
||||||
<div class="absolute cover-bg" ref="coverBg" />
|
<div class="absolute cover-bg" ref="coverBg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img v-if="fullCoverUrl" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? 1 : 0 }" :class="showCoverBg && hasCover ? 'object-contain' : 'object-fill'" />
|
<img v-if="fullCoverUrl" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? 1 : 0 }" :class="(showCoverBg && hasCover) || noBg ? 'object-contain' : 'object-fill'" />
|
||||||
|
|
||||||
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
||||||
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
<p class="text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||||
<div class="absolute top-2 right-2">
|
<div class="absolute top-2 right-2">
|
||||||
<widgets-loading-spinner />
|
<widgets-loading-spinner />
|
||||||
</div>
|
</div>
|
||||||
@@ -18,17 +18,17 @@
|
|||||||
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
|
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
|
||||||
<img src="/Logo.png" loading="lazy" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
|
<img src="/Logo.png" loading="lazy" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
|
||||||
<p class="text-center font-book text-error" :style="{ fontSize: titleFontSize + 'rem' }">Invalid Cover</p>
|
<p class="text-centertext-error" :style="{ fontSize: titleFontSize + 'rem' }">Invalid Cover</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center z-10" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center z-10" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-center font-book truncate leading-none origin-center" style="color: rgb(247 223 187); font-size: 0.8rem" :style="{ transform: `scale(${sizeMultiplier})` }">{{ titleCleaned }}</p>
|
<p class="text-centertruncate leading-none origin-center" style="color: rgb(247 223 187); font-size: 0.8rem" :style="{ transform: `scale(${sizeMultiplier})` }">{{ titleCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center z-10" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center z-10" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
||||||
<p class="text-center font-book truncate leading-none origin-center" style="color: rgb(247 223 187); opacity: 0.75; font-size: 0.6rem" :style="{ transform: `scale(${sizeMultiplier})` }">{{ authorCleaned }}</p>
|
<p class="text-centertruncate leading-none origin-center" style="color: rgb(247 223 187); opacity: 0.75; font-size: 0.6rem" :style="{ transform: `scale(${sizeMultiplier})` }">{{ authorCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -48,7 +48,8 @@ export default {
|
|||||||
},
|
},
|
||||||
bookCoverAspectRatio: Number,
|
bookCoverAspectRatio: Number,
|
||||||
downloadCover: String,
|
downloadCover: String,
|
||||||
raw: Boolean
|
raw: Boolean,
|
||||||
|
noBg: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -156,7 +157,7 @@ export default {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.imageReady = true
|
this.imageReady = true
|
||||||
})
|
})
|
||||||
if (this.$refs.cover && this.cover !== this.placeholderUrl) {
|
if (!this.noBg && this.$refs.cover && this.cover !== this.placeholderUrl) {
|
||||||
var { naturalWidth, naturalHeight } = this.$refs.cover
|
var { naturalWidth, naturalHeight } = this.$refs.cover
|
||||||
var aspectRatio = naturalHeight / naturalWidth
|
var aspectRatio = naturalHeight / naturalWidth
|
||||||
var arDiff = Math.abs(aspectRatio - this.bookCoverAspectRatio)
|
var arDiff = Math.abs(aspectRatio - this.bookCoverAspectRatio)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
||||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||||
|
|
||||||
<p class="font-book text-white text-opacity-60 text-center" :style="{ fontSize: Math.min(1, sizeMultiplier) + 'rem' }">Empty Collection</p>
|
<p class="text-white text-opacity-60 text-center" :style="{ fontSize: Math.min(1, sizeMultiplier) + 'rem' }">Empty Collection</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||