Compare commits

...
Author SHA1 Message Date
advplyr ff77dbf3f8 iOS update pods 2023-02-19 16:53:43 -06:00
advplyr 3f78fb7a14 iOS version bump 0.9.62 2023-02-19 15:40:24 -06:00
advplyr 152904b7c7 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-02-19 14:51:41 -06:00
advplyr 9214fecc86 Version bump 0.9.62-beta 2023-02-19 14:51:34 -06:00
advplyrandGitHub 049397f7ab Merge pull request #565 from jramer/master
fix: [iOS] seek pauses playback
2023-02-19 14:23:45 -06:00
advplyr 0f42a411a9 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-02-19 14:17:28 -06:00
advplyr 4575c61118 Update:Android foreground service updates 2023-02-19 14:17:22 -06:00
advplyr 7575a4f883 Merge branch 'master' into jramer/master 2023-02-19 11:47:20 -06:00
advplyr 8d4c322bce Fix:iOS fallback to transcoding #553 2023-02-19 11:47:05 -06:00
advplyr 28989f536a Set up for android widget 2023-02-19 11:14:30 -06:00
Joakim Ramer 3318f3501c fix: seek when changing track 2023-02-19 17:42:45 +01:00
Joakim Ramer c9ad0c7897 Merge branch 'master' of github.com:jramer/audiobookshelf-app 2023-02-19 15:04:46 +01:00
advplyr 018a988124 Merge branch 'master' into jramer/master 2023-02-18 10:20:01 -06:00
advplyr 750c370692 Fix:Android download file with capital M4B extension 2023-02-18 10:08:10 -06:00
advplyr 08ee08c698 Update:Show loading indicator when first connecting to server 2023-02-17 17:47:46 -06:00
advplyr d899fd4d89 Android & iOS only open most recent session on first load 2023-02-17 17:14:49 -06:00
advplyr aab44d8cee Fix:Alignment of recent sessions list #587 2023-02-17 16:51:11 -06:00
advplyr 2c2fac6e34 Update:Android download 3 audio files at a time #567 2023-02-17 16:24:08 -06:00
advplyr 47ccc54749 Fix:Reorder local audio files overlapping save button 2023-02-17 16:07:37 -06:00
advplyr 05d7d0d0d8 Fix:Android re-scan folder removing/unlinking downloads 2023-02-17 15:49:53 -06:00
advplyr 210fb7b88f Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-02-17 13:09:42 -06:00
advplyr 4eeb7f24d8 Fix:iOS downloads #537 2023-02-17 13:07:35 -06:00
Joakim Ramer 9496ee1d97 Merge branch 'master' of github.com:advplyr/audiobookshelf-app 2023-02-16 23:06:32 +01:00
Joakim Ramer f4b05a3236 Clarify state 2023-02-16 23:00:33 +01:00
Joakim Ramer b73763a9ec fix: Fixes streaming issues 2023-02-16 22:53:37 +01:00
advplyrandGitHub e3aad8c44f Merge pull request #582 from lkiesow/build-on-workflow-change
Build APK on Workflow Change
2023-02-14 06:27:51 -06:00
Lars Kiesow 1e7c298aa2 Build APK on Workflow Change
This patch removes a few restriction about when the Android build
workflows run. In particular, we want to run workflows…

- If the workflows themselves change because that could mean that the
  output changes.
- If the deployment page template changes because that would mean that
  the published page needs to be updated.

We could still exclude a few specific files if we wanted to, but they
should rarely change and it's probably not worth the effort of keeping
those lists updated.
2023-02-14 13:16:02 +01:00
advplyrandGitHub b9652c7cd8 Merge pull request #581 from lkiesow/test-page
Publish Android Test Page
2023-02-13 17:31:43 -06:00
advplyrandGitHub 04f00d2994 Update .github/workflows/deploy-apk.yml 2023-02-13 17:31:13 -06:00
advplyrandGitHub 3da6728f4c Add ignore paths to build-apk workflow 2023-02-13 17:27:57 -06:00
Lars Kiesow 5704ca2ae8 Publish Android Test Page
This page automatically builds the Android app from the latest commit on
the branch master, generates a test page and publishes it to GitHub
Pages as a nice way for non-developers to get the latest app version for
testing without having to build it themselves.

This page might need the pages source set to GitHub Actions at
Settings → Pages → Build and deployment → Source.
2023-02-13 21:42:54 +01:00
advplyr 6cd04e7098 Remove Gentium Book Basic font 2023-02-12 12:16:38 -06:00
advplyrandGitHub 290c3560d2 Merge pull request #574 from lkiesow/easy-continue-listening
Easy way to play current audiobook
2023-02-12 08:55:15 -06:00
advplyr 40bda3d48b Auto play podcast episode & check for local copy 2023-02-12 08:47:38 -06:00
advplyrandGitHub 63a838dc9d Merge pull request #577 from lkiesow/auto-build
Automatically test and build app
2023-02-12 08:03:43 -06:00
Lars Kiesow 127bf7d4e0 Automatic test and build app
This patch adds a GitHub Actions workflow which will automatically run
through all steps to build the Android version of the app. This is an
easy way of catching major build errors in pull requests or pushed
commits.

Additionally, this uploads the Android APK as build artifact. This
allows anyone to easily download any build for a while. That will make
it much easier for non-developers to participate in testing a new
version or a specific test since the no longer have to wait for a new
release.

The build artifacts are a bit tricky to find, but they are easy to point
out if anyone asks.
2023-02-12 01:21:16 +01:00
advplyrandGitHub 9af4947232 Merge pull request #576 from lkiesow/account-settings
Account Setting Information
2023-02-11 17:29:25 -06:00
advplyrandGitHub 3233a2a078 Merge pull request #575 from lkiesow/typeerror
Fix TypeError on Logout
2023-02-11 17:28:17 -06:00
Lars Kiesow 28ede56c7c Account Setting Information
The account setting list the connection name which is the server address
directly followed by the username. This is then followed by a field
repeating the username again.

This duplication of information is nor really helpful, which is why this
patch replaces this with two fields containing just the two information:
host address and username.
2023-02-11 23:53:01 +01:00
Lars Kiesow 5f6c1f57b0 Fix TypeError on Logout
Clicking on “logout”, you get the following TypeError:

```
TypeError: state.user is null
    getUserBookmarksForItem user.js:38
    bookmarks AudioPlayerContainer.vue:45
    VueJS 4
    render AudioPlayerContainer.vue:11
    VueJS 13
    logout user.js:115
    wrappedMutationHandler vuex.esm.js:844
    commitIterator vuex.esm.js:466
    commit vuex.esm.js:465
    _withCommit vuex.esm.js:624
    commit vuex.esm.js:464
    boundCommit vuex.esm.js:409
    _callee$ account.vue:67
    tryCatch pdf_viewer.js:1329
    invoke pdf_viewer.js:1502
    defineIteratorMethods pdf_viewer.js:1372
    Babel 10
    logout account.vue:56
    VueJS 4
    click Btn.vue:65
    VueJS 33
client.js:103
```

This patch fixes the problem by checking for `null` values.
2023-02-11 23:43:20 +01:00
Lars Kiesow ec704bf501 Easy way to play current audiobook
This patch implements a generic way of re-opening the player for the
book you last listened to. This is especially handy when first opening
the app and you can restart playback with a single click.

The player is opened only if no other book is already open in the
player. It will not overwrite or replace playbacks or player already in
progress.

The player is opened paused. No automatic playback since that could be
really annoying.

This closes #494
2023-02-11 23:14:41 +01:00
advplyrandGitHub 5e0c740f02 Merge pull request #572 from lkiesow/hyphen-auto
Improve word wrapping
2023-02-10 17:48:44 -06:00
Lars Kiesow a185c3abe9 Improve word wrapping
This patch improves word wrapping for the description text in book
details by justifying the text block and allowing for words to be
wrapped by automatically inserting hyphens if necessary.

This causes the description box to look far less ragged on the right
edge which I think helps the overall cleanliness of the look.

Unfortunately the app's/browser's aren't as good as e.g. the TeX
algorithm for hyphening and there are JavaScript libraries which seem to
reimplement that. But this is already a significant step up and far less
work for something which is not the main focus of this app.
2023-02-10 23:27:33 +01:00
advplyrandGitHub e8c6fc635f Merge pull request #569 from lkiesow/blurred-cover-bg
Blur Cover Image Background
2023-02-09 18:14:01 -06:00
advplyr f362db3855 Add prop to BookCover to not show a background 2023-02-09 18:13:30 -06:00
advplyrandGitHub 9ae584665e Merge pull request #568 from emmertex/master
Disable ATS for iOS
2023-02-09 17:51:20 -06:00
Lars Kiesow 1aa852c3dc Blur Cover Image Background
This patch adds a heavily blurred version of the cover image as
background to the cover image area in the book details.

This looks especially nice on larger devices like tablets.
2023-02-09 23:57:59 +01:00
Joakim Ramer ef46ad7a93 fix: removes some warnings 2023-02-09 02:40:21 +01:00
Joakim Ramer cbd74d08e1 fix: seek pauses playback 2023-02-09 02:38:56 +01:00
advplyrandGitHub 851d0357a0 Merge pull request #564 from lkiesow/lockfile-warning
Re-generate lock file
2023-02-08 17:09:48 -06:00
advplyrandGitHub fb5c23d190 Merge pull request #563 from lkiesow/menu-padding
Increase padding for menus
2023-02-08 16:57:29 -06:00
advplyrandGitHub 1743d0e13f Merge pull request #562 from lkiesow/book-details-menu-items
Icons for book details menu
2023-02-08 16:56:00 -06:00
advplyrandGitHub 25af0c4170 Merge pull request #561 from lkiesow/simplify-progress
Simplify progress (re)sets
2023-02-08 16:53:24 -06:00
Lars Kiesow 4bcb77250d Re-generate lock file
Running `npm ci` you always get a warning about an old lock-file format
being used:

```
npm WARN old lockfile
npm WARN old lockfile The package-lock.json file was created with an old version of npm,
npm WARN old lockfile so supplemental metadata must be fetched from the registry.
npm WARN old lockfile
npm WARN old lockfile This is a one-time fix-up, please be patient...
npm WARN old lockfile
```

This patch simply re-generates the file by removing the old one and
running`npm install`. That gets rid of the warning.
2023-02-08 23:22:54 +01:00
Lars Kiesow 6114e03cb7 Increase passing for menus
This patch slightly increases the padding for menu dialogs. No real
reason for this except that I like the look of this better ;-)
2023-02-08 23:19:37 +01:00
Lars Kiesow 9866a787bd Icons for book details menu
This patch adds icons similar to those used in other app menus to the
book details menu.
2023-02-08 23:09:52 +01:00
Andrew Frahn 20e64523b2 Disable ATS for iOS 2023-02-09 08:38:50 +11:00
Lars Kiesow 50a53ec0d1 Simplify progress (re)sets
This patch is a follow up to the discussion from #559 about potentially
simplifying progress interactions.

This patch essentially reduces the interactions to two simple options
which allow users to always marks books as finished or discard the
progress and thus mark them as not yet started with a single
interaction:

- If users have already listed to the book, regardless of the state of
  the progress (finished, half way through, …), they get an option to
  discard the progress. This replaces the additional “mark as not
  finished” option but it should still be clear to users what happens.

- If a book is not yet finished, regardless of the state of the progress
  (not yet started, half way through), users get a “mark as finished”
  option which sets the progress to 100%.

This is hopefully a bit simpler than what we have now but also doesn't
require a user to figure out that you need to first mark a book as
finished to be able to discard the progress.
2023-02-08 22:27:56 +01:00
advplyrandGitHub 40b2ff5a97 Merge pull request #560 from lkiesow/account-version-github
App info in account settings
2023-02-07 17:54:58 -06:00
Lars Kiesow 48cc000b95 App info in account settings
This patch updates the account settings view, trying to make it look a
bit cleaner by:

- Making the logout button look more like a button. The full width
  looked odd on large devices or in landscape mode.
- Removing the version number. It is already listed in the main menu
  anyway.
- Moving the hint about reporting bugs and the GitHub link to the
  bottom.

The hint is still a bit weird in this view. Maybe long-term, we want to
move all these information about the app (version, issues, …) to a
separate view. But that's something for another day.
2023-02-07 23:55:21 +01:00
advplyrandGitHub 75aaf4d784 Merge pull request #559 from lkiesow/discard-progress-menu-option
Move Discard Progress to Menu
2023-02-07 16:45:04 -06:00
advplyr 23220ecae6 Fix podcast page returning item progress 2023-02-07 16:44:23 -06:00
advplyrandGitHub 94b4dda94f Merge pull request #554 from KeyboardHammer/aaSpeed
Add playback speed adjustment for Android Auto
2023-02-07 16:38:13 -06:00
advplyr 86c5d1a3e9 Refresh media session connector actions when changing speed on app. Update rounding values for Android Auto speed icons 2023-02-07 16:10:02 -06:00
Lars Kiesow d7be01935f Move Discard Progress to Menu
This patch moves the discard progress functionality from the tiny icon
at the top right corner of the progress display to the options menu.

The reasoning for this is that resetting progress is an option which is
unlikely to be used very often and therefor does not need to be on the
main screen. In addition to that, the menu already holds the related
options to mark the book as finished and to discard the progress of a
finished book.

Finally, this removes the tiny icon which does not really match the rest
of the user interface of the app.
2023-02-07 22:42:06 +01:00
Alex b2b647e2f7 fix float conversion, rounding for icons 2023-02-06 20:41:35 -06:00
advplyr 405cd21c32 Emit playback speed change event when Android Auto changes speed 2023-02-06 17:20:39 -06:00
advplyr 17a894f5ef Update Android Auto playback speed string 2023-02-06 16:34:12 -06:00
Alex 23c90f4118 Add playback spped adjustment for Android Auto 2023-02-05 19:38:12 -06:00
advplyr f215efdcd0 Update:Syncing local sessions rewrite to support offline sessions #381 2023-02-05 16:54:46 -06:00
advplyr 2f243787ce Add:Android sleep timer setting to disable vibrate on reset #552 2023-02-04 17:52:31 -06:00
advplyr 39909a398e Add:Android sleep timer setting to disable audio fade out #320 2023-02-04 17:39:41 -06:00
advplyr 5e98a4ff2f Update:Android cancelling sleep timer when it was auto-enabled will also disable the auto sleep timer setting with alert #545 2023-02-04 16:57:55 -06:00
advplyr 8710775872 Fix:Android download freezing UI when a lot of files are downloaded #550 2023-02-04 15:52:06 -06:00
advplyr 99d60eaf9a Update:Adjust current playback time with playback speed #549 2023-02-04 13:50:54 -06:00
advplyr 55b7f05685 Fix:Android media play/pause event #429 #427 2023-02-04 11:13:19 -06:00
advplyr 944aa5de66 Fix:Android download book play times in history 2023-02-04 10:32:36 -06:00
advplyr a12eb7efdf Fix:Android sync fails when restarting playback session 2023-02-04 10:11:31 -06:00
advplyr 62a5a2d069 Update:Increase height of minimized audio player to accomadate iOS safe area #531 2023-01-31 15:30:13 -06:00
advplyr ebd65b4fa7 Fix:Back button closing modals and fullscreen player #535 2023-01-31 14:50:26 -06:00
advplyrandGitHub 152cce2e9e Merge pull request #534 from lkiesow/player-progress-position
Player Progress Bar Position
2023-01-31 14:18:07 -06:00
advplyrandGitHub ea847a62ff Update components/app/AudioPlayer.vue 2023-01-31 14:17:45 -06:00
advplyrandGitHub 2a5ec618f0 Merge pull request #533 from lkiesow/metadata-separators
Fix Metadata List Separators
2023-01-31 14:10:35 -06:00
Lars Kiesow 3fc89377b4 Player Progress Bar Position
This patch moves the progress bar slightly up for a better separation
between playback controls and seeking.

The reasoning behind this is that I (and probably others as well) often
interact with this interface in a clumsy, half-asleep state. Due to the
closeness of the progress bar and the play/pause button, I have managed
it several times to accidentally seek instead of pause. That is really
annoying since you then have to manually find the previous playback
position again.

Long term, I would love to see more of the available space being used.
In particular in the player interface. There should be enough free space
available. Especially if you have just one of the progress bars active.
But for now, this is just a quick-fix to improve the current situation.
2023-01-31 15:31:06 +01:00
Lars Kiesow 93c16e6a62 Fix Metadata List Separators
This patch fixes the separators between metadata list entries in the book
detail view. Items were separated only by comma. A space was missing.

This just adds a space to both the genre as well as to the narrator
separators.
2023-01-31 14:43:49 +01:00
112 changed files with 20283 additions and 667 deletions
+81
View File
@@ -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>
+47
View File
@@ -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
+76
View File
@@ -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
+2 -2
View File
@@ -35,8 +35,8 @@ android {
applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 92
versionName "0.9.61-beta"
versionCode 93
versionName "0.9.62-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+5 -2
View File
@@ -22,6 +22,8 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true" >
<!-- TODO: Incomplete desktop widget -->
<!-- <receiver-->
<!-- android:name=".NewAppWidget"-->
<!-- android:exported="true" >-->
@@ -74,7 +76,7 @@
</intent-filter>
</activity>
<receiver android:name="androidx.media.session.MediaButtonReceiver" android:exported="false">
<receiver android:name="androidx.media.session.MediaButtonReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
@@ -83,7 +85,8 @@
<service
android:name=".player.PlayerNotificationService"
android:enabled="true"
android:exported="true" >
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
@@ -102,7 +102,9 @@ data class DeviceSettings(
var autoSleepTimer: Boolean,
var autoSleepTimerStartTime: String,
var autoSleepTimerEndTime: String,
var sleepTimerLength: Long // Time in milliseconds
var sleepTimerLength: Long, // Time in milliseconds
var disableSleepTimerFadeOut: Boolean,
var disableSleepTimerResetFeedback: Boolean
) {
companion object {
// Static method to get default device settings
@@ -119,7 +121,9 @@ data class DeviceSettings(
autoSleepTimer = false,
autoSleepTimerStartTime = "22:00",
autoSleepTimerEndTime = "06:00",
sleepTimerLength = 900000L // 15 minutes
sleepTimerLength = 900000L, // 15 minutes
disableSleepTimerFadeOut = false,
disableSleepTimerResetFeedback = false
)
}
}
@@ -154,8 +158,8 @@ data class DeviceSettings(
data class DeviceData(
var serverConnectionConfigs:MutableList<ServerConnectionConfig>,
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
fun getLastServerConnectionConfig(): ServerConnectionConfig? {
@@ -1,6 +1,5 @@
package com.audiobookshelf.app.data
import android.util.Log
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import kotlin.math.roundToInt
@@ -33,6 +32,11 @@ class LocalMediaProgress(
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
fun updateIsFinished(finished:Boolean) {
@@ -7,7 +7,7 @@ import android.os.Build
import android.provider.MediaStore
import android.support.v4.media.MediaMetadataCompat
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.JsonIgnoreProperties
import com.google.android.exoplayer2.MediaItem
@@ -254,7 +254,7 @@ class PlaybackSession(
}
@JsonIgnore
fun syncData(syncData:MediaProgressSyncData) {
fun syncData(syncData: MediaProgressSyncData) {
timeListening += syncData.timeListened
updatedAt = System.currentTimeMillis()
currentTime = syncData.currentTime
@@ -1,5 +1,8 @@
package com.audiobookshelf.app.device
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.util.Log
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.managers.DbManager
@@ -46,4 +49,27 @@ object DeviceManager {
if (id == null) return null
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
// 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
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}")
val existingItem = existingLocalLibraryItems.find { emi -> emi.id == getLocalLibraryItemId(itemFolder.id) }
when (scanLibraryItemFolder(itemFolder, localFolder, existingItem, forceAudioProbe)) {
ItemScanResult.REMOVED -> mediaItemsRemoved++
ItemScanResult.UPDATED -> mediaItemsUpdated++
ItemScanResult.ADDED -> mediaItemsAdded++
else -> mediaItemsUpToDate++
val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
// Do not scan folders that have no media items and not an existing item already
if (existingItem != null || filesInFolder.isNotEmpty()) {
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 itemId = getLocalLibraryItemId(itemFolder.id)
@@ -106,8 +111,6 @@ class FolderScanner(var ctx: Context) {
var coverContentUrl:String? = null
var coverAbsolutePath:String? = null
val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
val existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
}
@@ -22,7 +22,7 @@ class DbManager {
}
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) {
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) {
Paper.book("mediaItemHistory").write(mediaItemHistory.id,mediaItemHistory)
}
fun getMediaItemHistory(id:String): MediaItemHistory? {
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.delay
import kotlinx.coroutines.launch
import java.util.*
class DownloadItemManager(var downloadManager:DownloadManager, private var folderScanner: FolderScanner, var mainActivity: MainActivity, private var clientEventEmitter:DownloadEventEmitter) {
val tag = "DownloadItemManager"
private val maxSimultaneousDownloads = 1
private val maxSimultaneousDownloads = 3
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
enum class DownloadCheckStatus {
@@ -188,7 +189,8 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
// Rename to fix appended .mp3 on m4b/m4a files
// 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)
}
@@ -223,23 +225,27 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
if (downloadItem.isDownloadFinished) {
Log.i(tag, "Download Item finished ${downloadItem.media.metadata.title}")
folderScanner.scanDownloadItem(downloadItem) { downloadItemScanResult ->
Log.d(tag, "Item download complete ${downloadItem.itemTitle} | local library item id: ${downloadItemScanResult?.localLibraryItem?.id}")
GlobalScope.launch(Dispatchers.IO) {
folderScanner.scanDownloadItem(downloadItem) { downloadItemScanResult ->
Log.d(tag, "Item download complete ${downloadItem.itemTitle} | local library item id: ${downloadItemScanResult?.localLibraryItem?.id}")
val jsobj = JSObject()
jsobj.put("libraryItemId", downloadItem.id)
jsobj.put("localFolderId", downloadItem.localFolder.id)
val jsobj = JSObject()
jsobj.put("libraryItemId", downloadItem.id)
jsobj.put("localFolderId", downloadItem.localFolder.id)
downloadItemScanResult?.localLibraryItem?.let { localLibraryItem ->
jsobj.put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
downloadItemScanResult?.localLibraryItem?.let { 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)
}
}
}
@@ -20,6 +20,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
private var sleepTimerLength:Long = 0L
private var sleepTimerElapsed:Long = 0L
private var sleepTimerFinishedAt:Long = 0L
private var isAutoSleepTimer:Boolean = false // When timer was auto-set
private fun getCurrentTime():Long {
return playerNotificationService.getCurrentTime()
@@ -54,7 +55,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
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")
sleepTimerTask?.cancel()
sleepTimerRunning = true
@@ -85,7 +86,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
}
}
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds(), isAutoSleepTimer)
sleepTimerTask = Timer("SleepTimer", false).schedule(0L, 1000L) {
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")
if (sleepTimeSecondsRemaining > 0) {
playerNotificationService.clientEventEmitter?.onSleepTimerSet(sleepTimeSecondsRemaining)
playerNotificationService.clientEventEmitter?.onSleepTimerSet(sleepTimeSecondsRemaining, isAutoSleepTimer)
}
if (sleepTimeSecondsRemaining <= 0) {
@@ -107,10 +108,16 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
clearSleepTimer()
sleepTimerFinishedAt = System.currentTimeMillis()
} else if (sleepTimeSecondsRemaining <= 60) {
// Start fading out audio
val volume = sleepTimeSecondsRemaining / 60F
Log.d(tag, "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining")
setVolume(volume)
if (DeviceManager.deviceData.deviceSettings?.disableSleepTimerFadeOut == true) {
// Set volume to 1 in case setting was enabled while fading
setVolume(1f)
} else {
// Start fading out audio down to 10% volume
val percentToReduce = 1 - (sleepTimeSecondsRemaining / 60F)
val volume = 1f - (percentToReduce * 0.9f)
Log.d(tag, "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining")
setVolume(volume)
}
}
}
}
@@ -118,11 +125,17 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
return true
}
fun setManualSleepTimer(time: Long, isChapterTime:Boolean):Boolean {
isAutoSleepTimer = false
return setSleepTimer(time, isChapterTime)
}
private fun clearSleepTimer() {
sleepTimerTask?.cancel()
sleepTimerTask = null
sleepTimerEndTime = 0
sleepTimerRunning = false
isAutoSleepTimer = false
playerNotificationService.unregisterSensor()
}
@@ -132,12 +145,21 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
fun cancelSleepTimer() {
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()
playerNotificationService.clientEventEmitter?.onSleepTimerSet(0)
playerNotificationService.clientEventEmitter?.onSleepTimerSet(0, false)
}
// Vibrate when extending sleep timer by shaking
private fun vibrate() {
// Vibrate when resetting sleep timer
private fun vibrateFeedback() {
if (DeviceManager.deviceData.deviceSettings?.disableSleepTimerResetFeedback == true) return
val context = playerNotificationService.getContext()
val vibrator:Vibrator
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@@ -187,7 +209,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
private fun resetChapterTimer() {
this.getChapterEndTime()?.let { chapterEndTime ->
Log.d(tag, "Resetting stopped sleep timer to end of chapter $chapterEndTime")
vibrate()
vibrateFeedback()
setSleepTimer(chapterEndTime, true)
play()
}
@@ -212,7 +234,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
resetChapterTimer()
} else {
Log.d(tag, "Resetting stopped sleep timer to length $sleepTimerLength")
vibrate()
vibrateFeedback()
setSleepTimer(sleepTimerLength, false)
play()
}
@@ -222,14 +244,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
if (sleepTimerLength > 0L && sleepTimerElapsed > 3000L) {
Log.d(tag, "Resetting running sleep timer to length $sleepTimerLength")
vibrate()
vibrateFeedback()
setSleepTimer(sleepTimerLength, false)
} else if (sleepTimerLength == 0L) {
// When navigating to previous chapters make sure this is still the end of the current chapter
this.getChapterEndTime()?.let { chapterEndTime ->
if (chapterEndTime != sleepTimerEndTime) {
Log.d(tag, "Resetting chapter sleep timer to end of chapter $chapterEndTime from $sleepTimerEndTime")
vibrate()
vibrateFeedback()
setSleepTimer(chapterEndTime, true)
play()
}
@@ -264,7 +286,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
}
setVolume(1F)
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds(), isAutoSleepTimer)
}
fun decreaseSleepTime(time: Long) {
@@ -286,7 +308,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
}
setVolume(1F)
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds(), isAutoSleepTimer)
}
fun checkAutoSleepTimer() {
@@ -322,9 +344,11 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
if (chapterEndTimeMs == null) {
Log.e(tag, "Setting auto sleep timer to end of chapter/track but there is no current session")
} else {
isAutoSleepTimer = true
setSleepTimer(chapterEndTimeMs, true)
}
} else {
isAutoSleepTimer = true
setSleepTimer(deviceSettings.sleepTimerLength, false)
}
} else {
@@ -4,7 +4,6 @@ import android.util.Log
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.player.PlayerNotificationService
import com.audiobookshelf.app.player.SyncResult
object MediaEventManager {
const val tag = "MediaEventManager"
@@ -69,7 +68,7 @@ object MediaEventManager {
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 mediaItemEvent = MediaItemEvent(
@@ -103,8 +102,7 @@ object MediaEventManager {
libraryItemId,
episodeId,
isLocalOnly,
playbackSession.
serverConnectionConfigId,
playbackSession.serverConnectionConfigId,
playbackSession.serverAddress,
playbackSession.userId,
createdAt = System.currentTimeMillis(),
@@ -12,6 +12,7 @@ import java.util.*
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import org.json.JSONException
import org.json.JSONObject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@@ -32,7 +33,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
var serverItemsInProgress = listOf<ItemInProgress>()
var serverLibraries = listOf<Library>()
private var userSettingsPlaybackRate:Float? = null
var userSettingsPlaybackRate:Float? = null
fun getIsLibrary(id:String) : Boolean {
return serverLibraries.find { it.id == id } != null
@@ -61,12 +62,41 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
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() {
// When opening android auto need to check if still connected to server
// and reset any server data already set
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()
serverLibraryCategories = listOf()
serverLibraries = listOf()
@@ -217,7 +247,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
Log.d(tag, "checkSetValidServerConnectionConfig | $serverConfigIdUsed")
coroutineScope {
if (!apiHandler.isOnline()) {
if (!DeviceManager.checkConnectivity(ctx)) {
serverUserMediaProgress = mutableListOf()
cb(false)
} else {
@@ -1,4 +1,4 @@
package com.audiobookshelf.app.player
package com.audiobookshelf.app.media
import android.os.Handler
import android.os.Looper
@@ -6,9 +6,8 @@ import android.util.Log
import com.audiobookshelf.app.data.LocalMediaProgress
import com.audiobookshelf.app.data.MediaProgress
import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.data.Podcast
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 java.util.*
import kotlin.concurrent.schedule
@@ -25,7 +24,7 @@ data class SyncResult(
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 METERED_CONNECTION_SYNC_INTERVAL = 60000
@@ -40,7 +39,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
private val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset"
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
fun start(playbackSession:PlaybackSession) {
@@ -62,8 +61,8 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
listeningTimerRunning = true
lastSyncTime = System.currentTimeMillis()
Log.d(tag, "start: init last sync time $lastSyncTime")
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) {
Handler(Looper.getMainLooper()).post() {
@@ -199,8 +198,6 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
it.currentTime = mediaProgress.currentTime
MediaEventManager.syncEvent(mediaProgress, "Received from server get media progress request while playback session open")
DeviceManager.dbManager.saveLocalPlaybackSession(it)
saveLocalProgress(it)
}
}
@@ -226,16 +223,25 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
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) {
// Save local progress sync
currentPlaybackSession?.let {
DeviceManager.dbManager.saveLocalPlaybackSession(it)
saveLocalProgress(it)
lastSyncTime = System.currentTimeMillis()
// Local library item is linked to a server library item
// 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 ->
Log.d(
tag,
@@ -245,13 +251,14 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
if (syncSuccess) {
failedSyncs = 0
playerNotificationService.alertSyncSuccess()
DeviceManager.dbManager.removePlaybackSession(it.id) // Remove session from db
} else {
failedSyncs++
if (failedSyncs == 2) {
playerNotificationService.alertSyncFailing() // Show alert in client
failedSyncs = 0
}
Log.e(tag, "Local Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
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))
@@ -260,7 +267,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
cb(SyncResult(false, null, null))
}
}
} else if (shouldSyncServer) {
} else if (hasNetworkConnection && shouldSyncServer) {
Log.d(tag, "sync: currentSessionId=$currentSessionId")
apiHandler.sendProgressSync(currentSessionId, syncData) { syncSuccess, errorMsg ->
if (syncSuccess) {
@@ -268,13 +275,14 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
failedSyncs = 0
playerNotificationService.alertSyncSuccess()
lastSyncTime = System.currentTimeMillis()
DeviceManager.dbManager.removePlaybackSession(currentSessionId) // Remove session from db
} else {
failedSyncs++
if (failedSyncs == 2) {
playerNotificationService.alertSyncFailing() // Show alert in client
failedSyncs = 0
}
Log.e(tag, "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
Log.e(tag, "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime with session id=${currentSessionId}")
}
cb(SyncResult(true, syncSuccess, errorMsg))
}
@@ -307,6 +315,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
}
}
fun reset() {
currentPlaybackSession = null
currentLocalMediaProgress = null
@@ -32,7 +32,7 @@ data class DownloadItem(
for (it in downloadItemParts) {
if (!it.completed && it.downloadId == null) {
itemParts.add(it)
if (itemParts.size > limit) break
if (itemParts.size >= limit) break
}
}
@@ -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.util.Log
import android.view.KeyEvent
import com.audiobookshelf.app.data.LibraryItemWrapper
import com.audiobookshelf.app.data.PodcastEpisode
import com.audiobookshelf.app.device.DeviceManager
import java.util.*
import kotlin.concurrent.schedule
@@ -88,6 +89,25 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
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?) {
Log.d(tag, "ON PLAY FROM MEDIA ID $mediaId")
val libraryItemWrapper: LibraryItemWrapper?
@@ -138,15 +158,36 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
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) {
Log.d(tag, "handleCallMediaButton: key action_down for ${keyEvent.keyCode}")
when (keyEvent.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
Log.d(tag, "handleCallMediaButton: Media Play/Pause")
// 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) {
playerNotificationService.pause()
if (0 == mediaButtonClickCount) playerNotificationService.pause()
handleMediaButtonClickCount()
} else {
playerNotificationService.play()
if (0 == mediaButtonClickCount) {
playerNotificationService.play()
}
handleMediaButtonClickCount()
}
}
}
@@ -192,18 +233,6 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
KeyEvent.KEYCODE_MEDIA_STOP -> {
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 -> {
Log.d(tag, "KeyCode:${keyEvent.keyCode}")
return false
@@ -248,6 +277,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
CUSTOM_ACTION_JUMP_BACKWARD -> onRewind()
CUSTOM_ACTION_SKIP_FORWARD -> onSkipToNext()
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_SKIP_FORWARD = "com.audiobookshelf.customAction.skip_forward";
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_DIRECTSTREAM = 1
@@ -1,21 +1,37 @@
package com.audiobookshelf.app.player
import android.app.Notification
import android.content.pm.ServiceInfo
import android.os.Build
import android.util.Log
import com.google.android.exoplayer2.ui.PlayerNotificationManager
class PlayerNotificationListener(var playerNotificationService:PlayerNotificationService) : PlayerNotificationManager.NotificationListener {
var tag = "PlayerNotificationListener"
companion object {
var isForegroundService = false
}
override fun onNotificationPosted(
notificationId: Int,
notification: Notification,
onGoing: Boolean) {
// Start foreground service
Log.d(tag, "Notification Posted $notificationId - Start Foreground | $notification")
PlayerNotificationService.isClosed = false
playerNotificationService.startForeground(notificationId, notification)
if (onGoing && !isForegroundService) {
// Start foreground service
Log.d(tag, "Notification Posted $notificationId - Start Foreground | $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(
@@ -39,5 +55,6 @@ class PlayerNotificationListener(var playerNotificationService:PlayerNotificatio
PlayerNotificationService.isSwitchingPlayer = false
}
}
isForegroundService = false
}
}
@@ -34,6 +34,7 @@ import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.managers.DbManager
import com.audiobookshelf.app.managers.SleepTimerManager
import com.audiobookshelf.app.media.MediaManager
import com.audiobookshelf.app.media.MediaProgressSyncer
import com.audiobookshelf.app.server.ApiHandler
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.audio.AudioAttributes
@@ -59,6 +60,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
var isStarted = false
var isClosed = false
var isUnmeteredNetwork = false
var hasNetworkConnectivity = false // Not 100% reliable has internet
var isSwitchingPlayer = false // Used when switching between cast player and exoplayer
}
@@ -70,7 +72,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
fun onPlayingUpdate(isPlaying: Boolean)
fun onMetadata(metadata: PlaybackMetadata)
fun onSleepTimerEnded(currentPosition: Long)
fun onSleepTimerSet(sleepTimeRemaining: Int)
fun onSleepTimerSet(sleepTimeRemaining: Int, isAutoSleepTimer:Boolean)
fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress)
fun onPlaybackFailed(errorMessage:String)
fun onMediaPlayerChanged(mediaPlayer:String)
@@ -78,6 +80,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
fun onProgressSyncSuccess()
fun onNetworkMeteredChanged(isUnmetered:Boolean)
fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory)
fun onPlaybackSpeedChanged(playbackSpeed:Float)
}
private val binder = LocalBinder()
@@ -97,7 +100,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
var castPlayer:CastPlayer? = null
lateinit var sleepTimerManager:SleepTimerManager
lateinit var mediaProgressSyncer:MediaProgressSyncer
lateinit var mediaProgressSyncer: MediaProgressSyncer
private var notificationId = 10
private var channelId = "audiobookshelf_channel"
@@ -172,7 +175,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
castPlayer?.release()
mediaSession.release()
mediaProgressSyncer.reset()
Log.d(tag, "onDestroy")
isStarted = false
super.onDestroy()
@@ -193,8 +195,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
// To listen for network change from metered to unmetered
val networkRequest = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
val connectivityManager = getSystemService(ConnectivityManager::class.java) as ConnectivityManager
connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
@@ -378,23 +380,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
isClosed = false
val customActionProviders = mutableListOf(
JumpBackwardCustomActionProvider(),
JumpForwardCustomActionProvider(),
)
val metadata = playbackSession.getMediaMetadataCompat(ctx)
mediaSession.setMetadata(metadata)
val mediaItems = playbackSession.getMediaItems()
val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f
initialPlaybackRate = playbackRate
if (playbackSession.mediaPlayer != PLAYER_CAST && mediaItems.size > 1) {
customActionProviders.addAll(listOf(
SkipBackwardCustomActionProvider(),
SkipForwardCustomActionProvider(),
))
}
mediaSessionConnector.setCustomActionProviders(*customActionProviders.toTypedArray())
// Set actions on Android Auto like jump forward/backward
setMediaSessionConnectorCustomActions(playbackSession)
playbackSession.mediaPlayer = getMediaPlayer()
@@ -412,6 +406,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
currentPlaybackSession = playbackSession
DeviceManager.setLastPlaybackSession(playbackSession) // Save playback session to use when app is closed
Log.d(tag, "Set CurrentPlaybackSession MediaPlayer ${currentPlaybackSession?.mediaPlayer}")
clientEventEmitter?.onPlaybackSession(playbackSession)
@@ -472,6 +468,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) {
// On error and was attempting to direct play - fallback to transcode
currentPlaybackSession?.let { playbackSession ->
@@ -481,16 +493,19 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
val libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
val episodeId = playbackSession.episodeId
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
if (it == null) { // Play request failed
clientEventEmitter?.onPlaybackFailed(errorMessage)
closePlayback(true)
} else {
Handler(Looper.getMainLooper()).post {
preparePlayer(it, true, null)
mediaProgressSyncer.stop(false) {
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
if (it == null) { // Play request failed
clientEventEmitter?.onPlaybackFailed(errorMessage)
closePlayback(true)
} else {
Handler(Looper.getMainLooper()).post {
preparePlayer(it, true, null)
}
}
}
}
} else {
clientEventEmitter?.onPlaybackFailed(errorMessage)
closePlayback(true)
@@ -530,17 +545,22 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
fun startNewPlaybackSession() {
currentPlaybackSession?.let { playbackSession ->
Log.i(tag, "Starting new playback session for ${playbackSession.displayTitle}")
val forceTranscode = playbackSession.isHLS // If already HLS then force
val playItemRequestPayload = getPlayItemRequestPayload(forceTranscode)
val libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
val episodeId = playbackSession.episodeId
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
if (it == null) {
Log.e(tag, "Failed to start new playback session")
} else {
Handler(Looper.getMainLooper()).post {
preparePlayer(it, true, null)
mediaProgressSyncer.stop(false) {
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
if (it == null) {
Log.e(tag, "Failed to start new playback session")
} else {
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 +682,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
if (currentPlaybackSession == null) return true
mediaProgressSyncer.currentPlaybackSession?.let { playbackSession ->
if (!apiHandler.isOnline() || playbackSession.isLocalLibraryItemOnly) {
if (!DeviceManager.checkConnectivity(ctx) || playbackSession.isLocalLibraryItemOnly) {
return true // carry on
}
@@ -703,7 +723,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
// Should already be playing
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)
}
}
@@ -733,7 +755,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
mediaProgressSyncer.play(it)
mediaProgressSyncer.currentPlaybackSession?.let { playbackSession ->
mediaProgressSyncer.play(playbackSession)
}
clientEventEmitter?.onPlayingUpdate(true)
}
}
@@ -781,6 +806,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
currentPlaybackSession?.currentTime = timeToSeek / 1000.0
val newWindowIndex = currentPlaybackSession?.getCurrentTrackIndex() ?: 0
val newTimeOffset = currentPlaybackSession?.getCurrentTrackTimeMs() ?: 0
Log.d(tag, "seekPlayer seekTo $newWindowIndex | $newTimeOffset")
currentPlayer.seekTo(newWindowIndex, newTimeOffset)
} else {
currentPlayer.seekTo(timeToSeek)
@@ -812,7 +838,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
fun setPlaybackSpeed(speed: Float) {
mediaManager.userSettingsPlaybackRate = speed
currentPlayer.setPlaybackSpeed(speed)
// Refresh Android Auto actions
mediaProgressSyncer.currentPlaybackSession?.let {
setMediaSessionConnectorCustomActions(it)
}
}
fun closePlayback(calledOnError:Boolean? = false) {
@@ -849,7 +881,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
return if(currentPlayer == castPlayer) PLAYER_CAST else PLAYER_EXO
}
fun getDeviceInfo(): DeviceInfo {
private fun getDeviceInfo(): DeviceInfo {
/* EXAMPLE
manufacturer: Google
model: Pixel 6
@@ -881,7 +913,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
//
// 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 LIBRARIES_ROOT = "__LIBRARIES__"
@@ -1086,10 +1118,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
networkCapabilities: NetworkCapabilities
) {
super.onCapabilitiesChanged(network, networkCapabilities)
val unmetered = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
Log.i(tag, "Network capabilities changed is unmetered = $unmetered")
isUnmeteredNetwork = unmetered
clientEventEmitter?.onNetworkMeteredChanged(unmetered)
isUnmeteredNetwork = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
hasNetworkConnectivity = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
Log.i(tag, "Network capabilities changed. hasNetworkConnectivity=$hasNetworkConnectivity | isUnmeteredNetwork=$isUnmeteredNetwork")
clientEventEmitter?.onNetworkMeteredChanged(isUnmeteredNetwork)
}
}
@@ -1160,5 +1193,39 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
).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()
}
}
}
@@ -63,8 +63,11 @@ class AbsAudioPlayer : Plugin() {
emit("onSleepTimerEnded", currentPosition)
}
override fun onSleepTimerSet(sleepTimeRemaining: Int) {
emit("onSleepTimerSet", sleepTimeRemaining)
override fun onSleepTimerSet(sleepTimeRemaining: Int, isAutoSleepTimer:Boolean) {
val ret = JSObject()
ret.put("value", sleepTimeRemaining)
ret.put("isAuto", isAutoSleepTimer)
notifyListeners("onSleepTimerSet", ret)
}
override fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress) {
@@ -94,6 +97,10 @@ class AbsAudioPlayer : Plugin() {
override fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory) {
notifyListeners("onMediaItemHistoryUpdated", JSObject(jacksonMapper.writeValueAsString(mediaItemHistory)))
}
override fun onPlaybackSpeedChanged(playbackSpeed:Float) {
emit("onPlaybackSpeedChanged", playbackSpeed)
}
})
MediaEventManager.clientEventEmitter = playerNotificationService.clientEventEmitter
@@ -330,7 +337,7 @@ class AbsAudioPlayer : Plugin() {
val isChapterTime:Boolean = call.getBoolean("isChapterTime", false) == true
Handler(Looper.getMainLooper()).post {
val success:Boolean = playerNotificationService.sleepTimerManager.setSleepTimer(time, isChapterTime)
val success:Boolean = playerNotificationService.sleepTimerManager.setManualSleepTimer(time, isChapterTime)
val ret = JSObject()
ret.put("success", success)
call.resolve(ret)
@@ -216,13 +216,25 @@ class AbsDatabase : Plugin() {
}
@PluginMethod
fun syncLocalMediaProgressWithServer(call:PluginCall) {
fun syncLocalSessionsWithServer(call:PluginCall) {
if (DeviceManager.serverConnectionConfig == null) {
Log.e(tag, "syncLocalMediaProgressWithServer not connected to server")
Log.e(tag, "syncLocalSessionsWithServer not connected to server")
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()
}
}
}
}
}
@@ -51,7 +51,6 @@ class AbsDownloader : Plugin() {
folderScanner = FolderScanner(mainActivity)
apiHandler = ApiHandler(mainActivity)
downloadItemManager = DownloadItemManager(downloadManager, folderScanner, mainActivity, clientEventEmitter)
Log.d(tag, "Build SDK ${Build.VERSION.SDK_INT}")
}
@PluginMethod
@@ -1,13 +1,13 @@
package com.audiobookshelf.app.server
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.util.Log
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager
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.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
@@ -28,14 +28,14 @@ class ApiHandler(var ctx:Context) {
private var defaultClient = OkHttpClient()
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)
data class MediaProgressSyncResponsePayload(val numServerProgressUpdates:Int, val localProgressUpdates:List<LocalMediaProgress>, val serverProgressUpdates:List<MediaProgress>)
data class LocalMediaProgressSyncResultsPayload(var numLocalMediaProgressForServer:Int, var numServerProgressUpdates:Int, var numLocalProgressUpdates:Int, var serverProgressUpdates:List<MediaProgress>)
data class LocalSessionSyncResult(val id:String, val success:Boolean, val progressSynced:Boolean?, val error:String?)
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 token = config?.token ?: DeviceManager.token
@@ -45,18 +45,20 @@ class ApiHandler(var ctx:Context) {
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 token = config?.token ?: DeviceManager.token
val mediaType = "application/json; charset=utf-8".toMediaType()
val requestBody = payload.toString().toRequestBody(mediaType)
val requestUrl = "${address}$endpoint"
Log.d(tag, "postRequest to $requestUrl")
val request = Request.Builder().post(requestBody)
.url("${address}$endpoint").addHeader("Authorization", "Bearer ${token}")
.url(requestUrl).addHeader("Authorization", "Bearer ${token}")
.build()
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 requestBody = payload.toString().toRequestBody(mediaType)
val request = Request.Builder().patch(requestBody)
@@ -65,31 +67,7 @@ class ApiHandler(var ctx:Context) {
makeRequest(request, null, cb)
}
fun isOnline(): 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 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) {
private fun makeRequest(request:Request, httpClient:OkHttpClient?, cb: (JSObject) -> Unit) {
val client = httpClient ?: defaultClient
client.newCall(request).enqueue(object : Callback {
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) {
val mapper = jacksonMapper
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) {
Log.d(tag, "updateMediaProgress $libraryItemId $episodeId $updatePayload")
val endpoint = if(episodeId.isNullOrEmpty()) "/api/me/progress/$libraryItemId" else "/api/me/progress/$libraryItemId/$episodeId"
@@ -375,4 +312,55 @@ 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")
}
}
}
}
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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1013 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1011 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -10,4 +10,5 @@
<string name="action_jump_backward">Jump Backward</string>
<string name="action_skip_forward">Skip Forward</string>
<string name="action_skip_backward">Skip Backward</string>
<string name="action_change_speed">Change Playback Speed</string>
</resources>
Vendored Regular → Executable
View File
+1 -1
View File
@@ -14,7 +14,7 @@ ext {
cordovaAndroidVersion = '10.1.1'
androidx_car_version = '1.0.0-alpha7'
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_test_runner_version = '1.3.0'
arch_lifecycle_version = '2.2.0'
+3 -3
View File
@@ -20,9 +20,9 @@ body {
}
#content.playerOpen {
height: calc(100% - 164px);
min-height: calc(100% - 164px);
max-height: calc(100% - 164px);
height: calc(100% - 184px);
min-height: calc(100% - 184px);
max-height: calc(100% - 184px);
}
#bookshelf {
-19
View File
@@ -48,25 +48,6 @@
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 */
@font-face {
+1 -1
View File
@@ -10,7 +10,7 @@
<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">
<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>
+21 -19
View File
@@ -143,6 +143,7 @@ export default {
onMetadataListener: null,
onProgressSyncFailing: null,
onProgressSyncSuccess: null,
onPlaybackSpeedChangedListener: null,
touchStartY: 0,
touchStartTime: 0,
touchEndY: 0,
@@ -317,7 +318,7 @@ export default {
return this.$secondsToTimestamp(this.totalDuration)
},
currentTimePretty() {
return this.$secondsToTimestamp(this.currentTime)
return this.$secondsToTimestamp(this.currentTime / this.currentPlaybackRate)
},
timeRemaining() {
if (this.useChapterTrack && this.currentChapter) {
@@ -452,6 +453,7 @@ export default {
await this.$hapticsImpact()
console.log(`[AudioPlayer] Set Playback Rate: ${speed}`)
this.currentPlaybackRate = speed
this.updateTimestamp()
AbsAudioPlayer.setPlaybackSpeed({ value: speed })
},
restart() {
@@ -502,19 +504,17 @@ export default {
}
},
updateTimestamp() {
var ts = this.$refs.currentTimestamp
const ts = this.$refs.currentTimestamp
if (!ts) {
console.error('No timestamp el')
return
}
var currTimeStr = ''
let currentTime = this.currentTime / this.currentPlaybackRate
if (this.useChapterTrack && this.currentChapter) {
var currChapTime = Math.max(0, this.currentTime - this.currentChapter.start)
currTimeStr = this.$secondsToTimestamp(currChapTime)
} else {
currTimeStr = this.$secondsToTimestamp(this.currentTime)
const currChapTime = Math.max(0, this.currentTime - this.currentChapter.start)
currentTime = currChapTime / this.currentPlaybackRate
}
ts.innerText = currTimeStr
ts.innerText = this.$secondsToTimestamp(currentTime)
},
timeupdate() {
if (!this.$refs.playedTrack) {
@@ -783,8 +783,6 @@ export default {
console.log('received metadata update', data)
if (data.currentRate && data.currentRate > 0) this.playbackSpeed = data.currentRate
this.timeupdate()
},
// When a playback session is started the native android/ios will send the session
@@ -815,6 +813,11 @@ export default {
this.$toast.error(`Playback Failed: ${errorMessage}`)
this.endPlayback()
},
onPlaybackSpeedChanged(data) {
if (!data.value || isNaN(data.value)) return
this.currentPlaybackRate = Number(data.value)
this.updateTimestamp()
},
async init() {
this.useChapterTrack = await this.$localStore.getUseChapterTrack()
this.lockUi = await this.$localStore.getPlayerLock()
@@ -826,6 +829,7 @@ export default {
this.onMetadataListener = AbsAudioPlayer.addListener('onMetadata', this.onMetadata)
this.onProgressSyncFailing = AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
this.onProgressSyncSuccess = AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
this.onPlaybackSpeedChangedListener = AbsAudioPlayer.addListener('onPlaybackSpeedChanged', this.onPlaybackSpeedChanged)
},
screenOrientationChange() {
setTimeout(() => {
@@ -849,7 +853,7 @@ export default {
document.documentElement.style.setProperty('--title-author-left-offset-collapsed', 24 + coverImageWidthCollapsed + 'px')
},
minimizePlayerEvt() {
this.showFullscreen = false
this.collapseFullscreen()
},
showProgressSyncIsFailing() {
this.syncStatus = this.$constants.SyncStatus.FAILED
@@ -901,6 +905,7 @@ export default {
if (this.onPlaybackFailedListener) this.onPlaybackFailedListener.remove()
if (this.onProgressSyncFailing) this.onProgressSyncFailing.remove()
if (this.onProgressSyncSuccess) this.onProgressSyncSuccess.remove()
if (this.onPlaybackSpeedChangedListener) this.onPlaybackSpeedChangedListener.remove()
clearInterval(this.playInterval)
}
}
@@ -916,7 +921,7 @@ export default {
}
.playerContainer {
height: 100px;
height: 120px;
}
.fullscreen .playerContainer {
height: 200px;
@@ -931,15 +936,14 @@ export default {
#playerTrack {
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
transition-property: margin;
bottom: 20px;
bottom: 35px;
}
.fullscreen #playerTrack {
top: 20px;
bottom: unset;
}
.cover-wrapper {
bottom: 48px;
bottom: 68px;
left: 12px;
height: var(--cover-image-height-collapsed);
width: var(--cover-image-width-collapsed);
@@ -960,7 +964,7 @@ export default {
transform-origin: left bottom;
width: 40%;
bottom: 56px;
bottom: 76px;
left: var(--title-author-left-offset-collapsed);
text-align: left;
}
@@ -999,7 +1003,7 @@ export default {
width: 140px;
padding-left: 12px;
padding-right: 12px;
bottom: 50px;
bottom: 70px;
}
#playerControls .jump-icon {
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
@@ -1017,7 +1021,6 @@ export default {
min-width: 40px;
min-height: 40px;
margin: 0px 14px;
/* padding: 8px; */
}
#playerControls .play-btn .material-icons {
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
@@ -1047,7 +1050,6 @@ export default {
font-size: 2rem;
}
.fullscreen #playerControls .play-btn {
/* padding: 16px; */
height: 65px;
width: 65px;
min-width: 65px;
+16 -5
View File
@@ -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" />
<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" />
</div>
</template>
@@ -28,6 +28,7 @@ export default {
isSleepTimerRunning: false,
sleepTimerEndTime: 0,
sleepTimeRemaining: 0,
isAutoSleepTimer: false,
onLocalMediaProgressUpdateListener: null,
onSleepTimerEndedListener: null,
onSleepTimerSetListener: null,
@@ -42,6 +43,9 @@ export default {
bookmarks() {
if (!this.serverLibraryItemId) return []
return this.$store.getters['user/getUserBookmarksForItem'](this.serverLibraryItemId)
},
isIos() {
return this.$platform === 'ios'
}
},
methods: {
@@ -62,8 +66,9 @@ export default {
console.log('Sleep Timer Ended Current Position: ' + currentPosition)
}
},
onSleepTimerSet({ value: sleepTimeRemaining }) {
console.log('SLEEP TIMER SET', sleepTimeRemaining)
onSleepTimerSet(payload) {
const { value: sleepTimeRemaining, isAuto } = payload
console.log('SLEEP TIMER SET', JSON.stringify(payload))
if (sleepTimeRemaining === 0) {
console.log('Sleep timer canceled')
this.isSleepTimerRunning = false
@@ -71,6 +76,7 @@ export default {
this.isSleepTimerRunning = true
}
this.isAutoSleepTimer = !!isAuto
this.sleepTimeRemaining = sleepTimeRemaining
},
showSleepTimer() {
@@ -185,6 +191,7 @@ export default {
const libraryItemId = payload.libraryItemId
const episodeId = payload.episodeId
const startTime = payload.startTime
const startWhenReady = !payload.paused
// 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
@@ -220,7 +227,7 @@ export default {
}
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
AbsAudioPlayer.prepareLibraryItem(preparePayload)
.then((data) => {
@@ -265,9 +272,13 @@ export default {
this.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
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()
}
}
+1 -1
View File
@@ -13,7 +13,7 @@
</template>
</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">
<p class="transform text-xs">{{ label }}</p>
</div>
+3 -3
View File
@@ -18,7 +18,7 @@
<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' }">
<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>
<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,11 +26,11 @@
<!-- 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>
<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 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>
+1 -1
View File
@@ -5,7 +5,7 @@
<covers-collection-cover ref="cover" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(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` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
+1 -1
View File
@@ -4,7 +4,7 @@
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
<covers-playlist-cover ref="cover" :items="items" :width="width" :height="height" />
</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` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
+1 -1
View File
@@ -8,7 +8,7 @@
<div v-if="isAltViewEnabled && isCategorized" class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
<div v-if="!isCategorized" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(240, width) + 'px' }">
<div 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` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
+9 -8
View File
@@ -1,14 +1,14 @@
<template>
<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 class="absolute cover-bg" ref="coverBg" />
</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">
<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">
<widgets-loading-spinner />
</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 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' }" />
<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 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>
<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 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>
</template>
@@ -48,7 +48,8 @@ export default {
},
bookCoverAspectRatio: Number,
downloadCover: String,
raw: Boolean
raw: Boolean,
noBg: Boolean
},
data() {
return {
@@ -156,7 +157,7 @@ export default {
this.$nextTick(() => {
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 aspectRatio = naturalHeight / naturalWidth
var arDiff = Math.abs(aspectRatio - this.bookCoverAspectRatio)
+1 -1
View File
@@ -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 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>
</template>
+1 -1
View File
@@ -134,7 +134,7 @@ export default {
var innerP = document.createElement('p')
innerP.textContent = this.name
innerP.className = 'text-sm font-book text-white'
innerP.className = 'text-smtext-white'
imgdiv.appendChild(innerP)
return imgdiv
+1 -1
View File
@@ -14,7 +14,7 @@
<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">
<img src="/Logo.png" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
<p class="text-center font-book text-error" :style="{ fontSize: sizeMultiplier + 'rem' }">Invalid Cover</p>
<p class="text-centertext-error" :style="{ fontSize: sizeMultiplier + 'rem' }">Invalid Cover</p>
</div>
</div>
+2 -2
View File
@@ -5,8 +5,8 @@
<nuxt-link to="/bookshelf/series" v-if="selectedSeriesName" class="pt-1">
<span class="material-icons">arrow_back</span>
</nuxt-link>
<p v-show="!selectedSeriesName" class="font-book pt-1">{{ totalEntities }} {{ entityTitle }}</p>
<p v-show="selectedSeriesName" class="ml-2 font-book pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p>
<p v-show="!selectedSeriesName" class="pt-1">{{ totalEntities }} {{ entityTitle }}</p>
<p v-show="selectedSeriesName" class="ml-2pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p>
<div class="flex-grow" />
<span v-if="page == 'library' || seriesBookPage" class="material-icons px-2" @click="changeView">{{ !bookshelfListView ? 'view_list' : 'grid_view' }}</span>
<template v-if="page === 'library'">
+1 -1
View File
@@ -7,7 +7,7 @@
</template>
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20 p-2" style="max-height: 75%" @click.stop>
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in items">
<slot :name="item.value" :item="item" :selected="item.value === selected">
+13 -2
View File
@@ -51,7 +51,7 @@
<ui-btn @click="increaseSleepTime" class="w-9 h-9" :padding-x="0" small style="max-width: 36px"><span class="material-icons">add</span></ui-btn>
</div>
<ui-btn @click="cancelSleepTimer" class="w-full">Cancel Timer</ui-btn>
<ui-btn @click="cancelSleepTimer" class="w-full">{{ isAuto ? 'Disable Auto Timer' : 'Cancel Timer' }}</ui-btn>
</div>
</div>
</div>
@@ -59,12 +59,15 @@
</template>
<script>
import { Dialog } from '@capacitor/dialog'
export default {
props: {
value: Boolean,
currentTime: Number,
sleepTimerRunning: Boolean,
currentEndOfChapterTime: Number
currentEndOfChapterTime: Number,
isAuto: Boolean
},
data() {
return {
@@ -102,6 +105,14 @@ export default {
this.$nextTick(() => this.$emit('change', { time: timeout, isChapterTime: false }))
},
async cancelSleepTimer() {
if (this.isAuto) {
const { value } = await Dialog.confirm({
title: 'Confirm',
message: 'Are you sure you want to disable the auto sleep timer? You will need to enable this again in settings.'
})
if (!value) return
}
await this.$hapticsImpact()
this.$emit('cancel')
this.show = false
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<div id="epub-frame" class="w-full">
<div id="viewer" class="border border-gray-100 bg-white shadow-md h-full w-full"></div>
<div class="fixed left-0 h-8 w-full bg-bg px-2 flex items-center" :style="{ bottom: playerLibraryItemId ? '100px' : '0px' }">
<div class="fixed left-0 h-8 w-full bg-bg px-2 flex items-center" :style="{ bottom: playerLibraryItemId ? '120px' : '0px' }">
<p class="text-xs">epub</p>
<div class="flex-grow" />
+2 -2
View File
@@ -1,6 +1,6 @@
<template>
<div class="w-96 my-6 mx-auto">
<h1 class="text-2xl mb-4 font-book">Minutes Listening <span class="text-white text-opacity-60 text-lg">(Last 7 days)</span></h1>
<h1 class="text-2xl mb-4">Minutes Listening <span class="text-white text-opacity-60 text-lg">(Last 7 days)</span></h1>
<div class="relative w-96 h-72">
<div class="absolute top-0 left-0">
<template v-for="lbl in yAxisLabels">
@@ -25,7 +25,7 @@
<div class="absolute -bottom-2 left-0 flex ml-6">
<template v-for="dayObj in last7Days">
<div :key="dayObj.date" :style="{ width: daySpacing + daySpacing / 14 + 'px' }">
<p class="text-sm font-book">{{ dayObj.dayOfWeek.slice(0, 3) }}</p>
<p class="text-sm">{{ dayObj.dayOfWeek.slice(0, 3) }}</p>
</div>
</template>
</div>
@@ -44,13 +44,12 @@ export default {
},
methods: {
clickedIt() {
if (this.isIos) return // TODO: Implement on iOS
this.$router.push('/downloading')
},
onItemDownloadComplete(data) {
console.log('DownloadProgressIndicator onItemDownloadComplete', JSON.stringify(data))
if (!data || !data.libraryItemId) {
console.error('Invalid item downlaod complete payload')
console.error('Invalid item download complete payload')
return
}
+33 -32
View File
@@ -34,7 +34,7 @@
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
AC98F95F300E46A27FAE47A4 /* Pods_Audiobookshelf.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B775656E49492CFD6763B7B6 /* Pods_Audiobookshelf.framework */; };
65643F4DAA661FB0B247247E /* Pods_Audiobookshelf.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8F9A7CCC479333E44DC314BE /* Pods_Audiobookshelf.framework */; };
E9D5504628AC1A3900C746DD /* LibraryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504528AC1A3900C746DD /* LibraryItem.swift */; };
E9D5504828AC1A7A00C746DD /* MediaType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504728AC1A7A00C746DD /* MediaType.swift */; };
E9D5504A28AC1AA600C746DD /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504928AC1AA600C746DD /* Metadata.swift */; };
@@ -106,10 +106,9 @@
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
5F7EC3E03050241EA35154B3 /* Pods-Audiobookshelf.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Audiobookshelf.release.xcconfig"; path = "Pods/Target Support Files/Pods-Audiobookshelf/Pods-Audiobookshelf.release.xcconfig"; sourceTree = "<group>"; };
8FBD5FE4076DDFA0DA782256 /* Pods-Audiobookshelf.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Audiobookshelf.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Audiobookshelf/Pods-Audiobookshelf.debug.xcconfig"; sourceTree = "<group>"; };
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
B775656E49492CFD6763B7B6 /* Pods_Audiobookshelf.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Audiobookshelf.framework; sourceTree = BUILT_PRODUCTS_DIR; };
8F9A7CCC479333E44DC314BE /* Pods_Audiobookshelf.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Audiobookshelf.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BE96D57E131924D520D57057 /* Pods-Audiobookshelf.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Audiobookshelf.debug.xcconfig"; path = "Target Support Files/Pods-Audiobookshelf/Pods-Audiobookshelf.debug.xcconfig"; sourceTree = "<group>"; };
D2F7F575384A63F1C47DE984 /* Pods-Audiobookshelf.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Audiobookshelf.release.xcconfig"; path = "Target Support Files/Pods-Audiobookshelf/Pods-Audiobookshelf.release.xcconfig"; sourceTree = "<group>"; };
E9D5504528AC1A3900C746DD /* LibraryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryItem.swift; sourceTree = "<group>"; };
E9D5504728AC1A7A00C746DD /* MediaType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaType.swift; sourceTree = "<group>"; };
E9D5504928AC1AA600C746DD /* Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metadata.swift; sourceTree = "<group>"; };
@@ -139,7 +138,6 @@
E9E8814C28DA6B9000D750C1 /* PlayerTimeUtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerTimeUtilsTests.swift; sourceTree = "<group>"; };
E9E985F728B02D9400957F23 /* PlayerProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerProgress.swift; sourceTree = "<group>"; };
E9FA07E228C82848005520B0 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -147,7 +145,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
AC98F95F300E46A27FAE47A4 /* Pods_Audiobookshelf.framework in Frameworks */,
65643F4DAA661FB0B247247E /* Pods_Audiobookshelf.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -161,14 +159,24 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
2372704BEF15C94CF4EBDB08 /* Frameworks */ = {
isa = PBXGroup;
children = (
B775656E49492CFD6763B7B6 /* Pods_Audiobookshelf.framework */,
8F9A7CCC479333E44DC314BE /* Pods_Audiobookshelf.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
39E576BF5B2DD22C4195A799 /* Pods */ = {
isa = PBXGroup;
children = (
BE96D57E131924D520D57057 /* Pods-Audiobookshelf.debug.xcconfig */,
D2F7F575384A63F1C47DE984 /* Pods-Audiobookshelf.release.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
3ABF6190280432610070250E /* player */ = {
isa = PBXGroup;
children = (
@@ -245,8 +253,8 @@
504EC3061FED79650016851F /* App */,
E9E8813B28DA5DE500D750C1 /* AudiobookshelfUnitTests */,
504EC3051FED79650016851F /* Products */,
7F8756D8B27F46E3366F6CEA /* Pods */,
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
39E576BF5B2DD22C4195A799 /* Pods */,
2372704BEF15C94CF4EBDB08 /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -276,17 +284,6 @@
path = App;
sourceTree = "<group>";
};
7F8756D8B27F46E3366F6CEA /* Pods */ = {
isa = PBXGroup;
children = (
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
8FBD5FE4076DDFA0DA782256 /* Pods-Audiobookshelf.debug.xcconfig */,
5F7EC3E03050241EA35154B3 /* Pods-Audiobookshelf.release.xcconfig */,
);
name = Pods;
sourceTree = "<group>";
};
E9D5506328AC1D3F00C746DD /* server */ = {
isa = PBXGroup;
children = (
@@ -374,11 +371,11 @@
isa = PBXNativeTarget;
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "Audiobookshelf" */;
buildPhases = (
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
C5582D6256D39377D737E21E /* [CP] Check Pods Manifest.lock */,
504EC3001FED79650016851F /* Sources */,
504EC3011FED79650016851F /* Frameworks */,
504EC3021FED79650016851F /* Resources */,
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
CEB30CA5B985BAFB7ABA9E2F /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -471,16 +468,20 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
C5582D6256D39377D737E21E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Audiobookshelf-checkManifestLockResult.txt",
);
@@ -489,7 +490,7 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
CEB30CA5B985BAFB7ABA9E2F /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -715,17 +716,17 @@
};
504EC3171FED79650016851F /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 8FBD5FE4076DDFA0DA782256 /* Pods-Audiobookshelf.debug.xcconfig */;
baseConfigurationReference = BE96D57E131924D520D57057 /* Pods-Audiobookshelf.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 18;
CURRENT_PROJECT_VERSION = 19;
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.61;
MARKETING_VERSION = 0.9.62;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -739,17 +740,17 @@
};
504EC3181FED79650016851F /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 5F7EC3E03050241EA35154B3 /* Pods-Audiobookshelf.release.xcconfig */;
baseConfigurationReference = D2F7F575384A63F1C47DE984 /* Pods-Audiobookshelf.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 18;
CURRENT_PROJECT_VERSION = 19;
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.61;
MARKETING_VERSION = 0.9.62;
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
+1 -1
View File
@@ -14,7 +14,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Override point for customization after application launch.
let configuration = Realm.Configuration(
schemaVersion: 6,
schemaVersion: 7,
migrationBlock: { [weak self] migration, oldSchemaVersion in
if (oldSchemaVersion < 1) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)")
+5
View File
@@ -53,6 +53,11 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
</dict>
+6 -5
View File
@@ -159,7 +159,7 @@ public class AbsAudioPlayer: CAPPlugin {
@objc func sendMetadata() {
self.notifyListeners("onPlayingUpdate", data: [ "value": !PlayerHandler.paused ])
if let metadata = PlayerHandler.getMetdata() {
if let metadata = try? PlayerHandler.getMetdata()?.asDictionary() {
self.notifyListeners("onMetadata", data: metadata)
}
}
@@ -211,19 +211,19 @@ public class AbsAudioPlayer: CAPPlugin {
@objc func getSleepTimerTime(_ call: CAPPluginCall) {
call.resolve([
"value": PlayerHandler.getSleepTimeRemaining()
"value": PlayerHandler.getSleepTimeRemaining() ?? 0
])
}
@objc func sendSleepTimerEnded() {
self.notifyListeners("onSleepTimerEnded", data: [
"value": PlayerHandler.getCurrentTime()
"value": PlayerHandler.getCurrentTime() ?? 0
])
}
@objc func sendSleepTimerSet() {
self.notifyListeners("onSleepTimerSet", data: [
"value": PlayerHandler.getSleepTimeRemaining()
"value": PlayerHandler.getSleepTimeRemaining() ?? 0
])
}
@@ -238,6 +238,7 @@ public class AbsAudioPlayer: CAPPlugin {
@objc func onPlaybackFailed() {
if (PlayerHandler.getPlayMethod() == PlayMethod.directplay.rawValue) {
let session = PlayerHandler.getPlaybackSession()
let playWhenReady = PlayerHandler.getPlayWhenReady()
let libraryItemId = session?.libraryItemId ?? ""
let episodeId = session?.episodeId ?? nil
logger.log("Forcing Transcode")
@@ -247,7 +248,7 @@ public class AbsAudioPlayer: CAPPlugin {
do {
guard let self = self else { return }
try session.save()
PlayerHandler.startPlayback(sessionId: session.id, playWhenReady: self.initialPlayWhenReady, playbackRate: PlayerSettings.main().playbackRate)
PlayerHandler.startPlayback(sessionId: session.id, playWhenReady: playWhenReady, playbackRate: PlayerSettings.main().playbackRate)
self.sendPlaybackSession(session: try session.asDictionary())
self.sendMetadata()
} catch(let exception) {
+19 -8
View File
@@ -33,6 +33,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in
let realm = try Realm()
try realm.write {
downloadItemPart.bytesDownloaded = downloadItemPart.fileSize
downloadItemPart.progress = 100
downloadItemPart.completed = true
}
@@ -72,9 +73,11 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in
// Calculate the download percentage
let percentDownloaded = (Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)) * 100
// Only update the progress if we received accurate progress data
if percentDownloaded >= 0.0 && percentDownloaded <= 100.0 {
try Realm().write {
downloadItemPart.bytesDownloaded = Double(totalBytesWritten)
downloadItemPart.progress = percentDownloaded
}
}
@@ -109,6 +112,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
// Call the progress handler
do {
try progressHandler(downloadItem, part)
try? self.notifyListeners("onDownloadItemPartUpdate", data: part.asDictionary())
} catch {
logger.error("Error while processing progress")
debugPrint(error)
@@ -158,10 +162,9 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
}
}
// Emit status for active downloads
// Check for items done downloading
if let activeDownloads = fetchActiveDownloads() {
for item in activeDownloads.values {
try? self.notifyListeners("onItemDownloadUpdate", data: item.asDictionary())
if item.isDoneDownloading() { handleDoneDownloadItem(item) }
}
}
@@ -277,19 +280,22 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
let downloadItem = DownloadItem(libraryItem: item, episodeId: episodeId, server: Store.serverConfig!)
var tasks = [DownloadItemPartTask]()
for (i, track) in tracks.enumerated() {
let task = try startLibraryItemTrackDownload(item: item, position: i, track: track, episode: episode)
let task = try startLibraryItemTrackDownload(downloadItemId: downloadItem.id!, item: item, position: i, track: track, episode: episode)
downloadItem.downloadItemParts.append(task.part)
tasks.append(task)
}
// Also download the cover
if item.media?.coverPath != nil && !(item.media?.coverPath!.isEmpty ?? true) {
if let task = try? startLibraryItemCoverDownload(item: item) {
if let task = try? startLibraryItemCoverDownload(downloadItemId: downloadItem.id!, item: item) {
downloadItem.downloadItemParts.append(task.part)
tasks.append(task)
}
}
// Notify client of download item
try? self.notifyListeners("onDownloadItem", data: downloadItem.asDictionary())
// Persist in the database before status start coming in
try Database.shared.saveDownloadItem(downloadItem)
@@ -299,7 +305,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
}
}
private func startLibraryItemTrackDownload(item: LibraryItem, position: Int, track: AudioTrack, episode: PodcastEpisode?) throws -> DownloadItemPartTask {
private func startLibraryItemTrackDownload(downloadItemId: String, item: LibraryItem, position: Int, track: AudioTrack, episode: PodcastEpisode?) throws -> DownloadItemPartTask {
logger.log("TRACK \(track.contentUrl!)")
// If we don't name metadata, then we can't proceed
@@ -312,7 +318,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
let localUrl = "\(itemDirectory)/\(filename)"
let task = session.downloadTask(with: serverUrl)
let part = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: track.title ?? "Unknown", serverPath: Store.serverConfig!.address, audioTrack: track, episode: episode)
let part = DownloadItemPart(downloadItemId: downloadItemId, filename: filename, destination: localUrl, itemTitle: track.title ?? "Unknown", serverPath: Store.serverConfig!.address, audioTrack: track, episode: episode, size: track.metadata?.size ?? 0)
// Store the id on the task so the download item can be pulled from the database later
task.taskDescription = part.id
@@ -320,13 +326,18 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate {
return DownloadItemPartTask(part: part, task: task)
}
private func startLibraryItemCoverDownload(item: LibraryItem) throws -> DownloadItemPartTask {
private func startLibraryItemCoverDownload(downloadItemId: String, item: LibraryItem) throws -> DownloadItemPartTask {
let filename = "cover.jpg"
let serverPath = "/api/items/\(item.id)/cover"
let itemDirectory = try createLibraryItemFileDirectory(item: item)
let localUrl = "\(itemDirectory)/\(filename)"
let part = DownloadItemPart(filename: filename, destination: localUrl, itemTitle: "cover", serverPath: serverPath, audioTrack: nil, episode: nil)
// Find library file to get cover size
let coverLibraryFile = item.libraryFiles.first(where: {
$0.metadata?.path == item.media?.coverPath
})
let part = DownloadItemPart(downloadItemId: downloadItemId, filename: filename, destination: localUrl, itemTitle: "cover", serverPath: serverPath, audioTrack: nil, episode: nil, size: coverLibraryFile?.metadata?.size ?? 0)
let task = session.downloadTask(with: part.downloadURL!)
// Store the id on the task so the download item can be pulled from the database later
+4 -4
View File
@@ -7,8 +7,8 @@
import Foundation
class PlaybackMetadata: Codable {
var duration: Double = 0
var currentTime: Double = 0
var playerState: PlayerState = PlayerState.IDLE
struct PlaybackMetadata: Codable {
let duration: Double
let currentTime: Double
let playerState: PlayerState
}
+5 -5
View File
@@ -7,9 +7,9 @@
import Foundation
enum PlayerState: Codable {
case IDLE
case BUFFERING
case READY
case ENDED
enum PlayerState: String, Codable {
case idle = "IDLE"
case buffering = "BUFFERING"
case ready = "READY"
case ended = "ENDED"
}
@@ -9,7 +9,7 @@ import Foundation
import RealmSwift
class DownloadItem: Object, Codable {
@Persisted(primaryKey: true) var id: String?
@Persisted(primaryKey: true) var id:String?
@Persisted(indexed: true) var libraryItemId: String?
@Persisted var episodeId: String?
@Persisted var userMediaProgress: MediaProgress?
@@ -10,7 +10,9 @@ import RealmSwift
class DownloadItemPart: Object, Codable {
@Persisted(primaryKey: true) var id = ""
@Persisted var downloadItemId: String?
@Persisted var filename: String?
@Persisted var fileSize: Double = 0
@Persisted var itemTitle: String?
@Persisted var serverPath: String?
@Persisted var audioTrack: AudioTrack?
@@ -21,9 +23,10 @@ class DownloadItemPart: Object, Codable {
@Persisted var uri: String?
@Persisted var destinationUri: String?
@Persisted var progress: Double = 0
@Persisted var bytesDownloaded: Double = 0
private enum CodingKeys : String, CodingKey {
case id, filename, itemTitle, completed, moved, failed, progress
case id, downloadItemId, filename, fileSize, itemTitle, completed, moved, failed, progress, bytesDownloaded
}
override init() {
@@ -33,32 +36,40 @@ class DownloadItemPart: Object, Codable {
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(String.self, forKey: .id)
downloadItemId = try? values.decode(String.self, forKey: .downloadItemId)
filename = try? values.decode(String.self, forKey: .filename)
fileSize = try values.decode(Double.self, forKey: .fileSize)
itemTitle = try? values.decode(String.self, forKey: .itemTitle)
completed = try values.decode(Bool.self, forKey: .completed)
moved = try values.decode(Bool.self, forKey: .moved)
failed = try values.decode(Bool.self, forKey: .failed)
progress = try values.decode(Double.self, forKey: .progress)
bytesDownloaded = try values.decode(Double.self, forKey: .bytesDownloaded)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(downloadItemId, forKey: .downloadItemId)
try container.encode(filename, forKey: .filename)
try container.encode(fileSize, forKey: .fileSize)
try container.encode(itemTitle, forKey: .itemTitle)
try container.encode(completed, forKey: .completed)
try container.encode(moved, forKey: .moved)
try container.encode(failed, forKey: .failed)
try container.encode(progress, forKey: .progress)
try container.encode(bytesDownloaded, forKey: .bytesDownloaded)
}
}
extension DownloadItemPart {
convenience init(filename: String, destination: String, itemTitle: String, serverPath: String, audioTrack: AudioTrack?, episode: PodcastEpisode?) {
convenience init(downloadItemId: String, filename: String, destination: String, itemTitle: String, serverPath: String, audioTrack: AudioTrack?, episode: PodcastEpisode?, size: Double) {
self.init()
self.id = destination.toBase64()
self.downloadItemId = downloadItemId
self.filename = filename
self.fileSize = size
self.itemTitle = itemTitle
self.serverPath = serverPath
self.audioTrack = AudioTrack.detachCopy(of: audioTrack)
@@ -13,9 +13,10 @@ class FileMetadata: EmbeddedObject, Codable {
@Persisted var ext: String = ""
@Persisted var path: String = ""
@Persisted var relPath: String = ""
@Persisted var size:Double = 0
private enum CodingKeys : String, CodingKey {
case filename, ext, path, relPath
case filename, ext, path, relPath, size
}
override init() {
@@ -29,6 +30,7 @@ class FileMetadata: EmbeddedObject, Codable {
ext = try values.decode(String.self, forKey: .ext)
path = try values.decode(String.self, forKey: .path)
relPath = try values.decode(String.self, forKey: .relPath)
size = try values.decode(Double.self, forKey: .size)
}
func encode(to encoder: Encoder) throws {
@@ -37,5 +39,6 @@ class FileMetadata: EmbeddedObject, Codable {
try container.encode(ext, forKey: .ext)
try container.encode(path, forKey: .path)
try container.encode(relPath, forKey: .relPath)
try container.encode(size, forKey: .size)
}
}
+53 -40
View File
@@ -10,27 +10,32 @@ import AVFoundation
import UIKit
import MediaPlayer
enum PlayMethod:Int {
enum PlayMethod: Int {
case directplay = 0
case directstream = 1
case transcode = 2
case local = 3
}
enum PlayerStatus: Int {
case uninitialized = -1
case paused = 0
case playing = 1
}
class AudioPlayer: NSObject {
internal let queue = DispatchQueue(label: "ABSAudioPlayerQueue")
internal let logger = AppLogger(category: "AudioPlayer")
// enums and @objc are not compatible
@objc dynamic var status: Int
@objc dynamic var rate: Float
private var status: PlayerStatus
internal var rate: Float
private var tmpRate: Float = 1.0
private var playerContext = 0
private var playerItemContext = 0
private var playWhenReady: Bool
internal var playWhenReady: Bool
private var initialPlaybackRate: Float
internal var audioPlayer: AVQueuePlayer
@@ -57,9 +62,9 @@ class AudioPlayer: NSObject {
self.playWhenReady = playWhenReady
self.initialPlaybackRate = playbackRate
self.audioPlayer = AVQueuePlayer()
self.audioPlayer.automaticallyWaitsToMinimizeStalling = false
self.audioPlayer.automaticallyWaitsToMinimizeStalling = true
self.sessionId = sessionId
self.status = -1
self.status = .uninitialized
self.rate = 0.0
self.tmpRate = playbackRate
@@ -141,7 +146,7 @@ class AudioPlayer: NSObject {
}
public func isInitialized() -> Bool {
return self.status != -1
return self.status != .uninitialized
}
public func getPlaybackSession() -> PlaybackSession? {
@@ -156,6 +161,9 @@ class AudioPlayer: NSObject {
let trackEnd = startOffset + duration
if (time < trackEnd.rounded(.down)) {
return index
} else if (index == self.allPlayerItems.count - 1) {
// Seeking past end of last item
return index
}
}
return 0
@@ -254,7 +262,7 @@ class AudioPlayer: NSObject {
logger.log("queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)")
// Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session
let firstReady = self.status < 0
let firstReady = self.status == .uninitialized
if firstReady && !self.playWhenReady {
// Seek is async, and if we call this when also pressing play, we will get weird jumps in the scrub bar depending on timing
// Seeking to the correct position happens during play()
@@ -267,7 +275,7 @@ class AudioPlayer: NSObject {
self.play(allowSeekBack: false, isInitializing: true)
} else {
// Mark the player as ready
self.status = 0
self.status = .paused
}
} else if (playerItem.status == .failed) {
logger.error("queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")")
@@ -327,8 +335,8 @@ class AudioPlayer: NSObject {
self.audioPlayer.play()
self.audioPlayer.rate = self.tmpRate
}
self.status = 1
self.status = .playing
// Update the progress
self.updateNowPlaying()
@@ -351,27 +359,18 @@ class AudioPlayer: NSObject {
}
}
self.status = 0
self.status = .paused
updateNowPlaying()
self.startPausedTimer()
}
public func seek(_ to: Double, from: String) {
let continuePlaying = rate > 0.0
self.pause()
logger.log("SEEK: Seek to \(to) from \(from) and continuePlaying(\(continuePlaying)")
logger.log("SEEK: Seek to \(to) from \(from)")
guard let playbackSession = self.getPlaybackSession() else { return }
let currentTrack = playbackSession.audioTracks[self.currentTrackIndex]
let ctso = currentTrack.startOffset ?? 0.0
let trackEnd = ctso + currentTrack.duration
logger.log("SEEK: Seek current track END = \(trackEnd)")
let indexOfSeek = getItemIndexForTime(time: to)
logger.log("SEEK: Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)")
@@ -383,8 +382,6 @@ class AudioPlayer: NSObject {
playbackSession.currentTime = to
}
self.playWhenReady = continuePlaying // Only playWhenReady if already playing
self.status = -1
let playerItems = self.allPlayerItems[indexOfSeek..<self.allPlayerItems.count]
DispatchQueue.runOnMainQueue {
@@ -393,26 +390,33 @@ class AudioPlayer: NSObject {
self.audioPlayer.insert(item, after:self.audioPlayer.items().last)
}
}
seekInCurrentTrack(to: to, playbackSession: playbackSession)
setupQueueItemStatusObserver()
} else {
logger.log("SEEK: Seeking in current item \(to)")
let currentTrackStartOffset = playbackSession.audioTracks[self.currentTrackIndex].startOffset ?? 0.0
let seekTime = to - currentTrackStartOffset
seekInCurrentTrack(to: to, playbackSession: playbackSession)
}
// Only for use in here where we handle track selection
func seekInCurrentTrack(to: Double, playbackSession: PlaybackSession) {
let currentTrack = playbackSession.audioTracks[self.currentTrackIndex]
let ctso = currentTrack.startOffset ?? 0.0
let trackEnd = ctso + currentTrack.duration
logger.log("SEEK: Seeking in current item \(to) (track START = \(ctso) END = \(trackEnd))")
let boundedTime = min(max(to, ctso), trackEnd)
let seekTime = boundedTime - ctso
DispatchQueue.runOnMainQueue {
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { [weak self] completed in
self?.logger.log("SEEK: Completion handler called and continuePlaying(\(continuePlaying)")
self?.logger.log("SEEK: Completion handler called")
guard completed else {
self?.logger.log("SEEK: WARNING: seeking not completed (to \(seekTime)")
return
}
guard let self = self else { return }
if continuePlaying {
self.resumePlayback()
}
self.updateNowPlaying()
}
}
@@ -463,9 +467,18 @@ class AudioPlayer: NSObject {
}
public func isPlaying() -> Bool {
return self.status > 0
return self.status == .playing
}
public func getPlayerState() -> PlayerState {
switch status {
case .uninitialized:
return PlayerState.buffering
case .paused, .playing:
return PlayerState.ready
}
}
// MARK: - Private
private func createAsset(itemId:String, track:AudioTrack) -> AVAsset? {
guard let playbackSession = self.getPlaybackSession() else { return nil }
+13 -11
View File
@@ -54,6 +54,10 @@ class PlayerHandler {
self.player?.getCurrentTime()
}
public static func getPlayWhenReady() -> Bool {
self.player?.playWhenReady ?? false
}
public static func setPlaybackSpeed(speed: Float) {
self.player?.setPlaybackRate(speed)
}
@@ -88,9 +92,8 @@ class PlayerHandler {
public static func getPlaybackSession() -> PlaybackSession? {
guard let player = player else { return nil }
guard player.isInitialized() else { return nil }
return Database.shared.getPlaybackSession(id: player.getPlaybackSessionId())
return player.getPlaybackSession()
}
public static func seekForward(amount: Double) {
@@ -118,16 +121,15 @@ class PlayerHandler {
player.seek(amount, from: "handler")
}
public static func getMetdata() -> [String: Any]? {
public static func getMetdata() -> PlaybackMetadata? {
guard let player = player else { return nil }
guard player.isInitialized() else { return nil }
return [
"duration": player.getDuration(),
"currentTime": player.getCurrentTime(),
"playerState": !paused,
"currentRate": player.rate,
]
return PlaybackMetadata(
duration: player.getDuration() ?? 0,
currentTime: player.getCurrentTime() ?? 0,
playerState: player.getPlayerState()
)
}
// MARK: - Helper logic
+1 -1
View File
@@ -171,7 +171,7 @@ class ApiClient {
]
]
ApiClient.postResource(endpoint: endpoint, parameters: parameters, decodable: PlaybackSession.self) { obj in
var session = obj
let session = obj
session.serverConnectionConfigId = Store.serverConfig!.id
session.serverAddress = Store.serverConfig!.address
+24 -8
View File
@@ -3,6 +3,10 @@
<app-appbar />
<div id="content" class="overflow-hidden relative" :class="playerIsOpen ? 'playerOpen' : ''">
<Nuxt />
<div v-if="attemptingConnection" class="absolute top-0 left-0 z-50 w-full h-full flex items-center justify-center">
<ui-loading-indicator text="Connecting to server..." />
</div>
</div>
<app-audio-player-container ref="streamContainer" />
<modals-libraries-modal />
@@ -169,10 +173,23 @@ export default {
this.$eventBus.$emit('library-changed')
this.inittingLibraries = false
},
async syncLocalSessions() {
if (!this.user) {
console.log('[default] No need to sync local sessions - not connected to server')
return
}
console.log('[default] Calling syncLocalSessions')
const response = await this.$db.syncLocalSessionsWithServer()
if (response && response.error) {
console.error('[default] Failed to sync local sessions', response.error)
} else {
console.log('[default] Successfully synced local sessions')
}
},
async syncLocalMediaProgress() {
if (!this.user) {
console.log('[default] No need to sync local media progress - not connected to server')
this.$store.commit('setLastLocalMediaSyncResults', null)
return
}
@@ -180,15 +197,10 @@ export default {
const response = await this.$db.syncLocalMediaProgressWithServer()
if (!response) {
if (this.$platform != 'web') this.$toast.error('Failed to sync local media with server')
this.$store.commit('setLastLocalMediaSyncResults', null)
return
}
const { numLocalMediaProgressForServer, numServerProgressUpdates, numLocalProgressUpdates, serverProgressUpdates } = response
if (numLocalMediaProgressForServer > 0) {
response.syncedAt = Date.now()
response.serverConfigName = this.$store.getters['user/getServerConfigName']
this.$store.commit('setLastLocalMediaSyncResults', response)
if (serverProgressUpdates && serverProgressUpdates.length) {
serverProgressUpdates.forEach((progress) => {
console.log(`[default] Server progress was updated ${progress.id}`)
@@ -203,7 +215,6 @@ export default {
}
} else {
console.log('[default] syncLocalMediaProgress No local media progress to sync')
this.$store.commit('setLastLocalMediaSyncResults', null)
}
},
userUpdated(user) {
@@ -295,7 +306,12 @@ export default {
}
console.log(`[default] finished connection attempt or already connected ${!!this.user}`)
await this.syncLocalMediaProgress()
if (this.$platform === 'ios') {
// TODO: Update ios to not use this
await this.syncLocalMediaProgress()
} else {
await this.syncLocalSessions()
}
this.loadSavedSettings()
this.hasMounted = true
+18973 -80
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-app",
"version": "0.9.61-beta",
"version": "0.9.62-beta",
"author": "advplyr",
"scripts": {
"dev": "nuxt --hostname 0.0.0.0 --port 1337",
+4 -10
View File
@@ -1,14 +1,13 @@
<template>
<div class="w-full h-full p-4">
<ui-text-input-with-label :value="serverConnConfigName" label="Connection Config Name" disabled class="my-2" />
<ui-text-input-with-label :value="serverAddress" label="Host" disabled class="my-2" />
<ui-text-input-with-label :value="username" label="Username" disabled class="my-2" />
<ui-btn color="primary flex items-center justify-between text-base w-full mt-8" @click="logout">Logout<span class="material-icons" style="font-size: 1.1rem">logout</span></ui-btn>
<ui-btn color="primary flex items-center justify-between gap-2 ml-auto text-base mt-8" @click="logout">Logout<span class="material-icons" style="font-size: 1.1rem">logout</span></ui-btn>
<div class="flex items-center pt-8">
<div class="flex-grow" />
<p class="pr-2 text-sm font-book text-yellow-400">Report bugs, request features, provide feedback, and contribute on <a class="underline" href="https://github.com/advplyr/audiobookshelf-app" target="_blank">github</a>.</p>
<div class="flex justify-end items-center m-4 gap-3 right-0 bottom-0 absolute">
<p class="text-smtext-yellow-400 text-right">Report bugs, request features, provide feedback, and contribute on <a class="underline" href="https://github.com/advplyr/audiobookshelf-app" target="_blank">GitHub</a></p>
<a href="https://github.com/advplyr/audiobookshelf-app" target="_blank" class="text-white hover:text-gray-200 hover:scale-150 hover:rotate-6 transform duration-500">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" width="24" height="24" viewBox="0 0 24 24">
<path
@@ -17,8 +16,6 @@
</svg>
</a>
</div>
<p class="font-mono pt-1 pb-4">{{ $config.version }}</p>
</div>
</template>
@@ -44,9 +41,6 @@ export default {
serverConnectionConfig() {
return this.$store.state.user.serverConnectionConfig || {}
},
serverConnConfigName() {
return this.serverConnectionConfig.name
},
serverAddress() {
return this.serverConnectionConfig.address
}
+7 -2
View File
@@ -3,7 +3,10 @@
<home-bookshelf-nav-bar />
<home-bookshelf-toolbar v-show="!hideToolbar" />
<div id="bookshelf-wrapper" class="main-content overflow-y-auto overflow-x-hidden relative" :class="hideToolbar ? 'no-toolbar' : ''">
<nuxt-child />
<nuxt-child :loading.sync="loading" />
</div>
<div v-if="loading" class="absolute top-0 left-0 z-50 w-full h-full flex items-center justify-center">
<ui-loading-indicator text="Loading..." />
</div>
</div>
</template>
@@ -11,7 +14,9 @@
<script>
export default {
data() {
return {}
return {
loading: false
}
},
computed: {
hideToolbar() {
+56 -7
View File
@@ -26,18 +26,17 @@
</div>
</div>
</div>
<div v-if="loading" class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
<ui-loading-indicator text="Loading Library..." />
</div>
</div>
</template>
<script>
export default {
props: {
loading: Boolean
},
data() {
return {
shelves: [],
loading: false,
isFirstNetworkConnection: true,
lastServerFetch: 0,
lastServerFetchLibraryId: null,
@@ -77,6 +76,9 @@ export default {
networkConnected() {
return this.$store.state.networkConnected
},
isIos() {
return this.$platform === 'ios'
},
currentLibraryName() {
return this.$store.getters['libraries/getCurrentLibraryName']
},
@@ -94,6 +96,14 @@ export default {
},
localMediaProgress() {
return this.$store.state.globals.localMediaProgress
},
isLoading: {
get() {
return this.loading
},
set(val) {
this.$emit('update:loading', val)
}
}
},
methods: {
@@ -210,7 +220,7 @@ export default {
}
}
this.loading = true
this.isLoading = true
this.shelves = []
if (this.user && this.currentLibraryId && this.networkConnected) {
@@ -226,7 +236,7 @@ export default {
this.shelves = localCategories
this.lastServerFetch = 0
this.lastLocalFetch = Date.now()
this.loading = false
this.isLoading = false
console.log('[categories] Local shelves set from failure', this.shelves.length, this.lastLocalFetch)
return
}
@@ -247,6 +257,11 @@ export default {
return cat
})
// TODO: iOS has its own implementation of this. Android & iOS should be consistent here.
if (!this.isIos) {
this.openMediaPlayerWithMostRecentListening()
}
// Only add the local shelf with the same media type
const localShelves = localCategories.filter((cat) => cat.type === this.currentLibraryMediaType && !cat.localOnly)
this.shelves.push(...localShelves)
@@ -259,7 +274,41 @@ export default {
console.log('[categories] Local shelves set', this.shelves.length, this.lastLocalFetch)
}
this.loading = false
this.isLoading = false
},
openMediaPlayerWithMostRecentListening() {
// If we don't already have a player open
// Try opening the first book from continue-listening without playing it
if (this.$store.state.playerLibraryItemId || !this.$store.state.isFirstAudioLoad) return
this.$store.commit('setIsFirstAudioLoad', false) // Only run this once on app launch
const continueListeningShelf = this.shelves.find((cat) => cat.id === 'continue-listening')
const mostRecentEntity = continueListeningShelf?.entities?.[0]
if (mostRecentEntity) {
const playObject = {
libraryItemId: mostRecentEntity.id,
episodeId: mostRecentEntity.recentEpisode?.id || null,
paused: true
}
// Check if there is a local copy
if (mostRecentEntity.localLibraryItem) {
if (mostRecentEntity.recentEpisode) {
// Check if the podcast episode has a local copy
const localEpisode = mostRecentEntity.localLibraryItem.media.episodes.find((ep) => ep.serverEpisodeId === mostRecentEntity.recentEpisode.id)
if (localEpisode) {
playObject.libraryItemId = mostRecentEntity.localLibraryItem.id
playObject.episodeId = localEpisode.id
playObject.serverLibraryItemId = mostRecentEntity.id
playObject.serverEpisodeId = mostRecentEntity.recentEpisode.id
}
} else {
playObject.libraryItemId = mostRecentEntity.localLibraryItem.id
playObject.serverLibraryItemId = mostRecentEntity.id
}
}
this.$eventBus.$emit('play-item', playObject)
}
},
libraryChanged() {
if (this.currentLibraryId) {
+2 -2
View File
@@ -6,9 +6,9 @@
</nuxt-link>
<div class="absolute top-0 left-0 w-full p-6 flex items-center flex-col justify-center z-0 short:hidden">
<img src="/Logo.png" class="h-20 w-20 mb-2" />
<h1 class="text-2xl font-book">audiobookshelf</h1>
<h1 class="text-2xl">audiobookshelf</h1>
</div>
<p class="hidden absolute short:block top-1.5 left-12 p-2 font-book text-xl">audiobookshelf</p>
<p class="hidden absolute short:block top-1.5 left-12 p-2text-xl">audiobookshelf</p>
<!-- <p class="absolute bottom-16 left-0 right-0 px-2 text-center text-error"><strong>Important!</strong> This app requires that you are running <u>your own server</u> and does not provide any content.</p> -->
+50 -19
View File
@@ -1,5 +1,5 @@
<template>
<div class="w-full h-full px-3 py-4 overflow-y-auto relative bg-bg">
<div class="w-full h-full px-3 py-4 overflow-y-auto overflow-x-hidden relative bg-bg">
<div class="fixed top-0 left-0 w-full h-full pointer-events-none p-px z-10">
<div class="w-full h-full" :style="{ backgroundColor: coverRgb }" />
<div class="w-full h-full absolute top-0 left-0" style="background: linear-gradient(169deg, rgba(0, 0, 0, 0.4) 0%, rgba(55, 56, 56, 1) 80%)" />
@@ -7,8 +7,15 @@
<div class="z-10 relative">
<div class="w-full flex justify-center relative mb-4">
<div style="width: 0; transform: translateX(-50vw); overflow: visible">
<div style="width: 150vw; overflow: hidden">
<div id="coverBg" style="filter: blur(5vw)">
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" @imageLoaded="coverImageLoaded" />
</div>
</div>
</div>
<div class="relative" @click="showFullscreenCover = true">
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" @imageLoaded="coverImageLoaded" />
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" no-bg @imageLoaded="coverImageLoaded" />
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * progressPercent + 'px' }"></div>
</div>
</div>
@@ -19,7 +26,8 @@
<p v-if="seriesList && seriesList.length" class="text-sm text-gray-300 py-0.5">
<template v-for="(series, index) in seriesList">
<nuxt-link :key="series.id" :to="`/bookshelf/series/${series.id}`" class="underline">{{ series.text }}</nuxt-link><span :key="`${series.id}-comma`" v-if="index < seriesList.length - 1">,&nbsp;</span>
<nuxt-link :key="series.id" :to="`/bookshelf/series/${series.id}`" class="underline">{{ series.text }}</nuxt-link
><span :key="`${series.id}-comma`" v-if="index < seriesList.length - 1">,&nbsp;</span>
</template>
</p>
@@ -27,7 +35,8 @@
<p v-else-if="bookAuthors && bookAuthors.length" class="text-sm text-gray-300 py-0.5">
by
<template v-for="(author, index) in bookAuthors">
<nuxt-link :key="author.id" :to="`/bookshelf/library?filter=authors.${$encode(author.id)}`" class="underline">{{ author.name }}</nuxt-link><span :key="`${author.id}-comma`" v-if="index < bookAuthors.length - 1">,&nbsp;</span>
<nuxt-link :key="author.id" :to="`/bookshelf/library?filter=authors.${$encode(author.id)}`" class="underline">{{ author.name }}</nuxt-link
><span :key="`${author.id}-comma`" v-if="index < bookAuthors.length - 1">,&nbsp;</span>
</template>
</p>
@@ -41,9 +50,6 @@
<p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
<p v-if="progressPercent < 1" class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
<p v-else class="text-gray-400 text-xs">Finished {{ $formatDate(userProgressFinishedAt) }}</p>
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
<span class="material-icons text-sm">close</span>
</div>
</div>
<div v-if="isLocal" class="flex mt-4 -mx-1">
@@ -78,7 +84,8 @@
</div>
<div v-if="downloadItem" class="py-3">
<p class="text-center text-lg">Downloading! ({{ Math.round(downloadItem.itemProgress * 100) }}%)</p>
<p v-if="downloadItem.itemProgress == 1" class="text-center text-lg">Download complete. Processing...</p>
<p v-else class="text-center text-lg">Downloading! ({{ Math.round(downloadItem.itemProgress * 100) }}%)</p>
</div>
<!-- metadata -->
@@ -86,7 +93,8 @@
<div v-if="narrators && narrators.length" class="text-white text-opacity-60 uppercase text-sm">Narrators</div>
<div v-if="narrators && narrators.length" class="truncate text-sm">
<template v-for="(narrator, index) in narrators">
<nuxt-link :key="narrator" :to="`/bookshelf/library?filter=narrators.${$encode(narrator)}`" class="underline">{{ narrator }}</nuxt-link><span :key="index" v-if="index < narrators.length - 1">,</span>
<nuxt-link :key="narrator" :to="`/bookshelf/library?filter=narrators.${$encode(narrator)}`" class="underline">{{ narrator }}</nuxt-link
><span :key="index" v-if="index < narrators.length - 1">, </span>
</template>
</div>
@@ -96,7 +104,8 @@
<div v-if="genres.length" class="text-white text-opacity-60 uppercase text-sm">Genres</div>
<div v-if="genres.length" class="truncate text-sm">
<template v-for="(genre, index) in genres">
<nuxt-link :key="genre" :to="`/bookshelf/library?filter=genres.${$encode(genre)}`" class="underline">{{ genre }}</nuxt-link><span :key="index" v-if="index < genres.length - 1">,</span>
<nuxt-link :key="genre" :to="`/bookshelf/library?filter=genres.${$encode(genre)}`" class="underline">{{ genre }}</nuxt-link
><span :key="index" v-if="index < genres.length - 1">, </span>
</template>
</div>
</div>
@@ -118,7 +127,7 @@
</div>
<div class="w-full py-4">
<p class="text-sm">{{ description }}</p>
<p class="text-sm text-justify" style="hyphens: auto;">{{ description }}</p>
</div>
<tables-podcast-episodes-table v-if="isPodcast" :library-item="libraryItem" :local-library-item-id="localLibraryItemId" :episodes="episodes" :local-episodes="localLibraryItemEpisodes" :is-local="isLocal" />
@@ -290,6 +299,7 @@ export default {
return this.$store.getters['user/getToken']
},
userItemProgress() {
if (this.isPodcast) return null
if (this.isLocal) return this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId)
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
},
@@ -365,33 +375,48 @@ export default {
if (!this.isIos) {
items.push({
text: 'History',
value: 'history'
value: 'history',
icon: 'history'
})
}
items.push({
text: this.userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished',
value: 'markFinished'
})
if (!this.userIsFinished) {
items.push({
text: 'Mark as Finished',
value: 'markFinished',
icon: 'beenhere'
})
}
if (this.progressPercent > 0) {
items.push({
text: 'Discard Progress',
value: 'discardProgress',
icon: 'backspace'
})
}
}
if (this.localLibraryItemId) {
items.push({
text: 'Manage Local Files',
value: 'manageLocal'
value: 'manageLocal',
icon: 'folder'
})
}
if (!this.isPodcast && this.serverLibraryItemId) {
items.push({
text: 'Add to Playlist',
value: 'playlist'
value: 'playlist',
icon: 'playlist_add'
})
}
items.push({
text: 'More Info',
value: 'details'
value: 'details',
icon: 'info'
})
return items
@@ -440,6 +465,8 @@ export default {
this.toggleFinished()
} else if (action === 'history') {
this.$router.push(`/media/${this.mediaId}/history?title=${this.title}`)
} else if (action === 'discardProgress') {
this.clearProgressClick()
}
},
moreButtonPress() {
@@ -710,4 +737,8 @@ export default {
width: calc(100% - 64px);
max-width: calc(100% - 64px);
}
#coverBg > div {
width: 150vw !important;
max-width: 150vw !important;
}
</style>

Some files were not shown because too many files have changed in this diff Show More