Compare commits

..
Author SHA1 Message Date
advplyr 30efe6bd0a Update podfile 2025-01-18 11:35:06 -06:00
advplyrandISO-B 5d67c71791 Add epub reader setting to keep screen awake #1207
-----------

Co-authored-by: ISO-B <3048685+ISO-B@users.noreply.github.com>
2025-01-18 10:38:24 -06:00
advplyr c8d9887070 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2025-01-17 17:06:57 -06:00
advplyr 8268592e8e Update cordova-plugin-screen-orientation, update postcss in nuxt config 2025-01-17 17:06:50 -06:00
advplyrandGitHub 3b63c5c3f9 Merge pull request #1423 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-01-17 09:01:50 -06:00
ugyesandHosted Weblate d3918df4e3 Translated using Weblate (Hungarian)
Currently translated at 99.3% (330 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2025-01-16 23:16:32 +00:00
Milo IvirandHosted Weblate 7e586da521 Translated using Weblate (Croatian)
Currently translated at 100.0% (332 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-01-16 23:16:31 +00:00
Milo IvirandHosted Weblate 2f5407736c Translated using Weblate (Croatian)
Currently translated at 100.0% (332 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-01-16 23:16:31 +00:00
SunSpringandHosted Weblate b29a98e680 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (332 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-01-16 23:16:30 +00:00
Mathias FrancoandHosted Weblate 239a996846 Translated using Weblate (Dutch)
Currently translated at 100.0% (332 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2025-01-16 23:16:30 +00:00
Milo IvirandHosted Weblate 421ecf527d Translated using Weblate (Croatian)
Currently translated at 99.3% (330 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-01-16 23:16:29 +00:00
Vito0912andHosted Weblate c60e55832e Translated using Weblate (German)
Currently translated at 98.4% (327 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-01-16 23:16:28 +00:00
Rasmus EnevoldsenandHosted Weblate b1f9568f8a Translated using Weblate (Danish)
Currently translated at 100.0% (332 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-01-16 23:16:28 +00:00
thehijackerandHosted Weblate dd1adb433b Translated using Weblate (Slovenian)
Currently translated at 100.0% (332 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2025-01-16 23:16:27 +00:00
Bezruchenko SimonandHosted Weblate 5a4bcfbac5 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (332 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-01-16 23:16:27 +00:00
Mohamad DahhanandHosted Weblate 179642fa8f Translated using Weblate (Arabic)
Currently translated at 20.2% (66 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-01-16 23:16:26 +00:00
ugyesandHosted Weblate 33111c4f31 Translated using Weblate (Hungarian)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2025-01-16 23:16:26 +00:00
TrojaandHosted Weblate 03721e8090 Translated using Weblate (Belarusian)
Currently translated at 16.8% (55 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2025-01-16 23:16:25 +00:00
DavidandHosted Weblate 0c025e0497 Translated using Weblate (Catalan)
Currently translated at 58.8% (192 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-01-16 23:16:24 +00:00
Dawid KuźnickiandHosted Weblate 8cec40f672 Translated using Weblate (Polish)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-01-16 23:16:24 +00:00
SoaibuzzamanandHosted Weblate 4f7044bffa Translated using Weblate (Bengali)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bn/
2025-01-16 23:16:23 +00:00
advplyr 2c6c857e55 Update fullscreen ereader settings title to respect safe area 2025-01-16 16:00:53 -06:00
advplyr ede8c4ebad Update bookmark create button to sticky at the bottom of modal #1413 2025-01-16 15:57:14 -06:00
advplyr 88dc85c401 Fix bookmark item id 2025-01-15 17:27:55 -06:00
advplyr 7a94f78d78 Update bookmark timestamps to be relative to playback speed #1148 2025-01-13 17:08:22 -06:00
advplyr 6cacf6b2c5 Update create bookmark to auto focus and select input 2025-01-13 16:48:51 -06:00
advplyr 69d198117e Remove babel dev dependency 2025-01-13 16:31:27 -06:00
advplyr eb035c1023 Android gradle variables remove unused 2025-01-12 15:18:29 -06:00
advplyr 6dabc7a331 Update androidxCoreVersion to 1.12.0 and remove unnecessary gradle dependency 2025-01-12 15:03:29 -06:00
advplyr 58fe29e526 iOS Capacitor 6 updates 2025-01-12 14:58:49 -06:00
advplyr 20688d6395 Update to Capacitor 6 for package.json and android 2025-01-12 13:59:55 -06:00
advplyr 044dd7fea9 Update gradle, add jvmtoolchain, update kotlin version, address gradle warnings 2025-01-12 13:38:56 -06:00
advplyrandGitHub 2c1f5081f2 Merge pull request #1322 from ISO-B/feat_android_auto_browse
Added better library browsing for Android Auto
2025-01-11 16:59:29 -06:00
advplyr 9243e90e90 Android atuo Reset serverItemsInProgress 2025-01-11 16:49:20 -06:00
advplyr 0dc7813c40 Fix force reload when server changes 2025-01-11 16:33:38 -06:00
advplyr 847bedb65c Fix drawdown for paging with many of the same first letters, update series sequence ascending/descending string, universalize default drawdown grouping limit 2025-01-11 15:33:43 -06:00
advplyr 8e6e0cf673 Update loading library stats, filter non-audio items from search, prevent recent episodes from loading same podcast library item multiple times 2025-01-11 14:55:30 -06:00
advplyr 0da3045c73 Merge branch 'master' into feat_android_auto_browse 2025-01-11 11:10:24 -06:00
advplyrandGitHub d8accce9e7 Merge pull request #1410 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-12-27 17:13:30 -06:00
J. LavoieandHosted Weblate dbfd27301c Translated using Weblate (French)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-12-27 23:11:22 +00:00
pranelioandHosted Weblate 5ef5d8c343 Translated using Weblate (Lithuanian)
Currently translated at 55.5% (181 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/lt/
2024-12-27 23:11:21 +00:00
pranelioandHosted Weblate 6ff8282f9f Translated using Weblate (Lithuanian)
Currently translated at 52.7% (172 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/lt/
2024-12-27 23:11:21 +00:00
TamanegiiandHosted Weblate e3930e0588 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-12-27 23:11:20 +00:00
MODI NAVONandHosted Weblate 129db0ad21 Translated using Weblate (Hebrew)
Currently translated at 9.8% (32 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/he/
2024-12-27 23:11:20 +00:00
thehijackerandHosted Weblate 13a9ed902c Translated using Weblate (Slovenian)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2024-12-27 23:11:19 +00:00
PlazecandHosted Weblate 4e9435418e Translated using Weblate (Czech)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-12-27 23:11:18 +00:00
TrojaandHosted Weblate 9d990644dd Translated using Weblate (Belarusian)
Currently translated at 13.4% (44 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2024-12-27 23:11:18 +00:00
DmitryandHosted Weblate c733cab39e Translated using Weblate (Russian)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2024-12-27 23:11:17 +00:00
ugyesandHosted Weblate 5a004eeed4 Translated using Weblate (Hungarian)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2024-12-27 23:11:16 +00:00
TrojaandHosted Weblate 012ce933d3 Translated using Weblate (Belarusian)
Currently translated at 12.5% (41 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2024-12-27 23:11:16 +00:00
ugyesandHosted Weblate 9a2c6bc1ff Translated using Weblate (Hungarian)
Currently translated at 99.0% (323 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2024-12-27 23:11:15 +00:00
gallegonovatoandHosted Weblate f0877e192d Translated using Weblate (Spanish)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-12-27 23:11:15 +00:00
ABS translatorandHosted Weblate a04175a106 Translated using Weblate (Arabic)
Currently translated at 12.2% (40 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2024-12-27 23:11:14 +00:00
Bezruchenko SimonandHosted Weblate 0cdd4cd6ed Translated using Weblate (Ukrainian)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2024-12-27 23:11:13 +00:00
biuklijaandHosted Weblate c64d2a4fa4 Translated using Weblate (Croatian)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-12-27 23:11:12 +00:00
Vito0912andHosted Weblate 0b4b5a0b56 Translated using Weblate (German)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-12-27 23:11:12 +00:00
advplyrandHosted Weblate 42ff4e877a Added translation using Weblate (Belarusian) 2024-12-27 23:11:11 +00:00
TrojaandHosted Weblate 96f206e1a0 Translated using Weblate (Bulgarian)
Currently translated at 0.0% (0 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bg/
2024-12-27 23:11:10 +00:00
TrojaandHosted Weblate d2bce7a1e2 Translated using Weblate (Bulgarian)
Currently translated at 5.5% (18 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bg/
2024-12-27 23:11:10 +00:00
Hosted Weblate f1f64eaba1 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/
2024-12-27 23:11:09 +00:00
advplyrandGitHub 817eaf1bb2 Merge pull request #1417 from mikiher/subdirectory-support
Support servers with subdirectory
2024-12-27 17:11:02 -06:00
mikiher 16de3fdb97 Support servers with subdirectory 2024-12-23 20:26:23 +02:00
2452f09714 Turn page while playing (#1383) fixes #1365
* Add an EReader setting for turning when playing

This allows to allow the navigation with the Volume buttons even when
audiobooks are playing.

---------

Co-authored-by: advplyr <advplyr@protonmail.com>
2024-12-13 16:20:13 -06:00
advplyrandGitHub b02c1311ff Merge pull request #1407 from jaumet/Catalan_version
Catalan Translation
2024-12-12 08:16:08 -06:00
advplyr 21c35f7744 Add Catalan language option 2024-12-12 08:08:17 -06:00
advplyrandGitHub bd2f22e9d7 Merge pull request #1378 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-12-12 08:00:18 -06:00
Bezruchenko SimonandHosted Weblate d27776fd26 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2024-12-11 09:01:51 +01:00
ugyesandHosted Weblate 416ce15d1d Translated using Weblate (Hungarian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2024-12-11 09:01:50 +01:00
JaumeandGitHub 9f9abc6d3e Catalan Translation
This is the Calan translation , a file in
7stings/ca.json
2024-12-05 21:45:01 +01:00
Milo IvirandHosted Weblate 995f042fc8 Translated using Weblate (Croatian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-12-05 06:04:58 +01:00
ArturandHosted Weblate e905a50ed0 Translated using Weblate (Polish)
Currently translated at 91.9% (298 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2024-11-30 12:00:26 +01:00
shdwandHosted Weblate 5e036992b0 Translated using Weblate (French)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-11-28 13:00:32 +01:00
SoaibuzzamanandHosted Weblate cb81a87200 Translated using Weblate (Bengali)
Currently translated at 96.6% (313 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bn/
2024-11-26 22:00:22 +01:00
DmitryandHosted Weblate fad2ba23b7 Translated using Weblate (Russian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2024-11-26 22:00:22 +01:00
Bezruchenko SimonandHosted Weblate e093c0820d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2024-11-25 00:00:37 +00:00
biuklijaandHosted Weblate a65c729906 Translated using Weblate (Croatian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-11-25 00:00:36 +00:00
DRandHosted Weblate 1b7fc89d8b Translated using Weblate (Hebrew)
Currently translated at 7.4% (24 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/he/
2024-11-22 23:00:18 +01:00
burghy86andHosted Weblate 246b8512e4 Translated using Weblate (Italian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2024-11-22 23:00:18 +01:00
Mohamad DahhanandHosted Weblate 4209f0e86d Translated using Weblate (Arabic)
Currently translated at 12.0% (39 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2024-11-19 23:30:18 +01:00
DRandHosted Weblate 653f57ea5d Translated using Weblate (Hebrew)
Currently translated at 6.4% (21 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/he/
2024-11-19 23:30:18 +01:00
ISO-B 1766111e1d Android Auto: Added comments to code 2024-11-18 20:20:00 +02:00
ISO-B 8caa08843e Android Auto: Prevent crashing loop in case that app restarts while browsing content 2024-11-18 19:47:49 +02:00
ISO-B a08ae6f977 Android Auto: Ensure that podcast are listed from newest to oldest 2024-11-18 19:46:22 +02:00
Mohamad DahhanandHosted Weblate db246b38f2 Translated using Weblate (Arabic)
Currently translated at 10.1% (33 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2024-11-18 13:00:26 +01:00
thehijackerandHosted Weblate 1119d47a80 Translated using Weblate (Slovenian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2024-11-18 13:00:25 +01:00
Julio Cesar de jesusandHosted Weblate d4ddfad838 Translated using Weblate (Spanish)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-11-18 13:00:23 +01:00
Clara PapkeandHosted Weblate e2d0799d17 Translated using Weblate (German)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-11-18 13:00:23 +01:00
ISO-B f68f31c80f Android Auto: Streamlined browsing settings to single option 2024-11-17 21:16:39 +02:00
ISO-B b7c8e72ce2 Android Auto: Podcast episodes show publish date 2024-11-17 21:15:16 +02:00
Mohamad DahhanandHosted Weblate 9f5b286a45 Translated using Weblate (Arabic)
Currently translated at 4.0% (13 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2024-11-16 22:04:25 +01:00
DRandHosted Weblate 1de204ff02 Translated using Weblate (Hebrew)
Currently translated at 2.1% (7 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/he/
2024-11-16 22:04:25 +01:00
SunSpringandHosted Weblate cc4d364f75 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-11-16 22:04:25 +01:00
Paulo Henrique Dos Santos GarciaandHosted Weblate cd629065b8 Translated using Weblate (Portuguese (Brazil))
Currently translated at 92.5% (300 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2024-11-16 22:04:24 +01:00
isogashiimanandHosted Weblate 46dfd9c44e Translated using Weblate (French)
Currently translated at 96.9% (314 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-11-16 22:04:24 +01:00
gallegonovatoandHosted Weblate edc797035f Translated using Weblate (Spanish)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-11-16 22:04:24 +01:00
Bezruchenko SimonandHosted Weblate c1eb7d70b7 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (319 of 319 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2024-11-15 00:51:30 +01:00
biuklijaandHosted Weblate 921636ea69 Translated using Weblate (Croatian)
Currently translated at 100.0% (319 of 319 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-11-15 00:51:29 +01:00
gallegonovatoandHosted Weblate 40c2262f47 Translated using Weblate (Spanish)
Currently translated at 100.0% (319 of 319 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-11-15 00:51:28 +01:00
Hosted Weblate b91e8fd0a1 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/
2024-11-15 00:51:27 +01:00
advplyr ad7ca59532 Add more localization #1375 2024-11-14 17:51:16 -06:00
ISO-B 802c16c0df Android Auto improvements. Icons, Start up
Icons:
- Static browsable list items everywhere have hardcoded icons
- Libraries use icons that are defined on server

Start up / Initial loading:
- Initial loading first loads libraries from server. After that personalized shelves and items in progress are fetched simultaneously. Top menu items are update after every stage.
- If network connection is not available when android auto starts app tries to do initial loading again when network connection becomes available again
2024-11-14 19:23:55 +02:00
advplyr efbb0e1b1c Fix string order 2024-11-13 17:30:38 -06:00
advplyr 607f6e9b6c Fix string order 2024-11-13 17:28:32 -06:00
advplyr 72b775e179 Add more localization #1375 2024-11-13 17:25:26 -06:00
advplyrandGitHub 491f312036 Merge pull request #1368 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-11-13 17:09:25 -06:00
thehijackerandHosted Weblate 31221062dd Translated using Weblate (Slovenian)
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2024-11-13 19:05:41 +00:00
burghy86andHosted Weblate 73e6fa24a8 Translated using Weblate (Italian)
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2024-11-13 19:05:41 +00:00
ISO-B b335fd30d1 Android Auto improvements
General:
 - New top menu item Recent is added
 - Library caches are cleared when switching server

Search:
 - Is done using server API
 - Latest search is cache to prevent need to make new request when returning from browsable item.
 - Results are grouped by book, series, author and split by library
 - Only searches libraries with audio content

Library personalized shelves:
 - Recent books, series, authors, podcasts and episodes shelves are listed under Recent top menu
 - Discovery shelves can be found under library many from corresponding library
2024-11-13 09:20:03 +02:00
Nicholas WandHosted Weblate 37395003cb Deleted translation using Weblate (English (United States)) 2024-11-12 04:14:22 +01:00
TamanegiiandHosted Weblate 39b360b132 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hant/
2024-11-11 23:41:58 +01:00
John Joseph A. GatchalianandHosted Weblate 34b16c2da9 Translated using Weblate (English (United States))
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/en_US/
2024-11-11 23:41:57 +01:00
thehijackerandHosted Weblate 525108bf26 Translated using Weblate (Slovenian)
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2024-11-11 23:41:56 +01:00
TamanegiiandHosted Weblate 7563bdf900 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-11-11 23:41:56 +01:00
Pavel VachekandHosted Weblate 64785be947 Translated using Weblate (Czech)
Currently translated at 99.3% (313 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-11-11 23:41:55 +01:00
SunSpringandHosted Weblate a714e9b985 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-11-11 23:41:54 +01:00
Nicholas WandHosted Weblate bc69a8768c Added translation using Weblate (Arabic) 2024-11-11 23:41:54 +01:00
Bezruchenko SimonandHosted Weblate 7d67761ed8 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2024-11-11 23:41:53 +01:00
biuklijaandHosted Weblate 3d9b508335 Translated using Weblate (Croatian)
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-11-11 23:41:53 +01:00
advplyr 78f0c8823d Add Bengali and Slovenian to language setting dropdown 2024-11-11 16:41:21 -06:00
advplyr 145a3c44b7 Add localization chart to readme 2024-11-08 08:03:09 -06:00
advplyr 2063f51289 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2024-11-08 07:56:02 -06:00
advplyr da2bfd8fc9 Update version in gh templates 2024-11-08 07:55:58 -06:00
ISO-B eedcd188c3 Android Auto: Fixed and improved search
Search now queries data from server. Results are grouped by books, series and authors.
2024-11-08 12:49:47 +02:00
ISO-B 8134ec84c6 Enchancements for Android Auto library
- Hide libraries without audiobooks
- Sort books in series by sequence value
- Added option for selecting ASC or DESC sorting for series
- Order authors alphabetically
2024-10-27 21:55:17 +02:00
ISO-B e4a3cc5290 Added Android Auto browsing settings 2024-09-16 23:06:49 +03:00
ISO-B a3a58a25ef Added better library browsing for Android Auto
Each library has 3 options: Library, Series and Collection. Library is grouped by authors
2024-09-13 22:51:54 +03:00
119 changed files with 9211 additions and 5034 deletions
+2 -2
View File
@@ -62,10 +62,10 @@ body:
description: Please ensure your app is up to date. *If you are using a 3rd-party app, please reach out to them directly.* description: Please ensure your app is up to date. *If you are using a 3rd-party app, please reach out to them directly.*
multiple: true multiple: true
options: options:
- Android App - 0.9.77
- iOS App - 0.9.77
- Android App - 0.9.76 - Android App - 0.9.76
- iOS App - 0.9.76 - iOS App - 0.9.76
- Android App - 0.9.75
- iOS App - 0.9.75
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
+2 -2
View File
@@ -43,10 +43,10 @@ body:
description: Please ensure your app is up to date. *If you are using a 3rd-party app, please reach out to them directly.* description: Please ensure your app is up to date. *If you are using a 3rd-party app, please reach out to them directly.*
multiple: true multiple: true
options: options:
- Android App - 0.9.77
- iOS App - 0.9.77
- Android App - 0.9.76 - Android App - 0.9.76
- iOS App - 0.9.76 - iOS App - 0.9.76
- Android App - 0.9.75
- iOS App - 0.9.75
validations: validations:
required: true required: true
- type: textarea - type: textarea
+36 -34
View File
@@ -18,51 +18,54 @@ kotlin {
"--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", "--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED" "--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"
] ]
jvmToolchain(17)
} }
android { android {
namespace 'com.audiobookshelf.app' namespace 'com.audiobookshelf.app'
buildFeatures { buildFeatures {
viewBinding true viewBinding true
buildConfig true
} }
kotlinOptions { kotlinOptions {
freeCompilerArgs = ['-Xjvm-default=all'] freeCompilerArgs = ['-Xjvm-default=all']
} }
compileSdkVersion rootProject.ext.compileSdkVersion compileSdk rootProject.ext.compileSdkVersion
defaultConfig { defaultConfig {
applicationId "com.audiobookshelf.app" applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 108 versionCode 108
versionName "0.9.77-beta" versionName "0.9.77-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
manifestPlaceholders = [ manifestPlaceholders = [
"appAuthRedirectScheme": "com.audiobookshelf.app" "appAuthRedirectScheme": "com.audiobookshelf.app"
] ]
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
} }
buildTypes { }
debug { buildTypes {
applicationIdSuffix ".debug" debug {
versionNameSuffix "-debug" applicationIdSuffix ".debug"
minifyEnabled false versionNameSuffix "-debug"
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' minifyEnabled false
} proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
} }
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
} }
repositories { repositories {
flatDir{ flatDir {
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
} }
mavenCentral() mavenCentral()
} }
@@ -78,19 +81,18 @@ configurations.configureEach {
} }
dependencies { dependencies {
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation fileTree(include: ['*.jar'], dir: 'libs') implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation project(':capacitor-android') implementation project(':capacitor-android')
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation project(':capacitor-cordova-android-plugins') implementation project(':capacitor-cordova-android-plugins')
implementation "androidx.core:core-ktx:$androidx_core_ktx_version" implementation "androidx.core:core-ktx:$androidx_core_ktx_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
+1
View File
@@ -10,6 +10,7 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies { dependencies {
implementation project(':byteowls-capacitor-filesharer') implementation project(':byteowls-capacitor-filesharer')
implementation project(':capacitor-community-keep-awake')
implementation project(':capacitor-community-volume-buttons') implementation project(':capacitor-community-volume-buttons')
implementation project(':capacitor-app') implementation project(':capacitor-app')
implementation project(':capacitor-browser') implementation project(':capacitor-browser')
@@ -2,10 +2,12 @@
"appId": "com.audiobookshelf.app", "appId": "com.audiobookshelf.app",
"appName": "audiobookshelf-app", "appName": "audiobookshelf-app",
"webDir": "dist", "webDir": "dist",
"bundledWebRuntime": false,
"plugins": { "plugins": {
"CapacitorHttp": { "CapacitorHttp": {
"enabled": false "enabled": false
} }
},
"server": {
"androidScheme": "http"
} }
} }
@@ -3,6 +3,10 @@
"pkg": "@byteowls/capacitor-filesharer", "pkg": "@byteowls/capacitor-filesharer",
"classpath": "com.byteowls.capacitor.filesharer.FileSharerPlugin" "classpath": "com.byteowls.capacitor.filesharer.FileSharerPlugin"
}, },
{
"pkg": "@capacitor-community/keep-awake",
"classpath": "com.getcapacitor.community.keepawake.KeepAwakePlugin"
},
{ {
"pkg": "@capacitor-community/volume-buttons", "pkg": "@capacitor-community/volume-buttons",
"classpath": "com.ryltsov.alex.plugins.volume.buttons.VolumeButtonsPlugin" "classpath": "com.ryltsov.alex.plugins.volume.buttons.VolumeButtonsPlugin"
@@ -0,0 +1,36 @@
package com.audiobookshelf.app.data
import android.content.Context
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
class CollapsedSeries(
id:String,
var libraryId:String?,
var name:String,
//var nameIgnorePrefix:String,
var sequence:String?,
var libraryItemIds:MutableList<String>
) : LibraryItemWrapper(id) {
@get:JsonIgnore
val title get() = name
@get:JsonIgnore
val numBooks get() = libraryItemIds.size
@JsonIgnore
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
val extras = Bundle()
val mediaId = "__LIBRARY__${libraryId}__SERIE__${id}"
return MediaDescriptionCompat.Builder()
.setMediaId(mediaId)
.setTitle(title)
//.setIconUri(getCoverUri())
.setSubtitle("${numBooks} books")
.setExtras(extras)
.build()
}
}
@@ -1,12 +1,15 @@
package com.audiobookshelf.app.data package com.audiobookshelf.app.data
import android.content.Context import android.content.Context
import android.icu.text.DateFormat
import android.os.Bundle import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.MediaMetadataCompat
import androidx.media.utils.MediaConstants import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.media.MediaManager import com.audiobookshelf.app.media.MediaManager
import com.fasterxml.jackson.annotation.* import com.fasterxml.jackson.annotation.*
import com.audiobookshelf.app.media.getUriToAbsIconDrawable
import java.util.Date
// This auto-detects whether it is a Book or Podcast // This auto-detects whether it is a Book or Podcast
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION) @JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
@@ -25,7 +28,8 @@ open class MediaType(var metadata:MediaTypeMetadata, var coverPath:String?) {
open fun removeAudioTrack(localFileId:String) { } open fun removeAudioTrack(localFileId:String) { }
@JsonIgnore @JsonIgnore
open fun getLocalCopy():MediaType { return MediaType(MediaTypeMetadata("", false),null) } open fun getLocalCopy():MediaType { return MediaType(MediaTypeMetadata("", false),null) }
@JsonIgnore
open fun checkHasTracks():Boolean { return false }
} }
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@@ -93,6 +97,11 @@ class Podcast(
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes, 0) return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes, 0)
} }
@JsonIgnore
override fun checkHasTracks():Boolean {
return (episodes?.size ?: numEpisodes ?: 0) > 0
}
@JsonIgnore @JsonIgnore
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode):PodcastEpisode { fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode):PodcastEpisode {
val localEpisodeId = "local_ep_" + episode.id val localEpisodeId = "local_ep_" + episode.id
@@ -182,6 +191,11 @@ class Book(
override fun getLocalCopy(): Book { override fun getLocalCopy(): Book {
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(), ebookFile, null,null, 0) return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(), ebookFile, null,null, 0)
} }
@JsonIgnore
override fun checkHasTracks():Boolean {
return (tracks?.size ?: numTracks ?: 0) > 0
}
} }
// This auto-detects whether it is a BookMetadata or PodcastMetadata // This auto-detects whether it is a BookMetadata or PodcastMetadata
@@ -214,7 +228,9 @@ class BookMetadata(
var authorName:String?, var authorName:String?,
var authorNameLF:String?, var authorNameLF:String?,
var narratorName:String?, var narratorName:String?,
var seriesName:String? var seriesName:String?,
@JsonFormat(with=[JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY])
var series:List<SeriesType>?
) : MediaTypeMetadata(title, explicit) { ) : MediaTypeMetadata(title, explicit) {
@JsonIgnore @JsonIgnore
override fun getAuthorDisplayName():String { return authorName ?: "Unknown" } override fun getAuthorDisplayName():String { return authorName ?: "Unknown" }
@@ -299,11 +315,18 @@ data class PodcastEpisode(
val libraryItemDescription = libraryItem.getMediaDescription(null, ctx) val libraryItemDescription = libraryItem.getMediaDescription(null, ctx)
val mediaId = localEpisodeId ?: id val mediaId = localEpisodeId ?: id
var subtitle = libraryItemDescription.title
if (publishedAt !== null) {
val sdf = DateFormat.getDateInstance()
val publishedAtDT = Date(publishedAt!!)
subtitle = "${sdf.format(publishedAtDT)} / $subtitle"
}
val mediaDescriptionBuilder = MediaDescriptionCompat.Builder() val mediaDescriptionBuilder = MediaDescriptionCompat.Builder()
.setMediaId(mediaId) .setMediaId(mediaId)
.setTitle(title) .setTitle(title)
.setIconUri(coverUri) .setIconUri(coverUri)
.setSubtitle(libraryItemDescription.title) .setSubtitle(subtitle)
.setExtras(extras) .setExtras(extras)
libraryItemDescription.iconBitmap?.let { libraryItemDescription.iconBitmap?.let {
@@ -342,18 +365,39 @@ data class Library(
var name:String, var name:String,
var folders:MutableList<Folder>, var folders:MutableList<Folder>,
var icon:String, var icon:String,
var mediaType:String var mediaType:String,
var stats: LibraryStats?
) { ) {
@JsonIgnore @JsonIgnore
fun getMediaMetadata(): MediaMetadataCompat { fun getMediaMetadata(context: Context, targetType: String? = null): MediaMetadataCompat {
var mediaId = id
if (targetType !== null) {
mediaId = "__RECENTLY__$id"
}
return MediaMetadataCompat.Builder().apply { return MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, name) putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, name)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, name) putString(MediaMetadataCompat.METADATA_KEY_TITLE, name)
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToAbsIconDrawable(context, icon).toString())
}.build() }.build()
} }
} }
@JsonIgnoreProperties(ignoreUnknown = true)
data class LibraryStats(
var totalItems: Int,
var totalSize: Long,
var totalDuration: Double,
var numAudioFiles: Int
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class SeriesType(
var id: String,
var name: String,
var sequence: String?
)
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class Folder( data class Folder(
var id:String, var id:String,
@@ -387,3 +431,84 @@ data class LibraryItemWithEpisode(
var libraryItemWrapper:LibraryItemWrapper, var libraryItemWrapper:LibraryItemWrapper,
var episode:PodcastEpisode var episode:PodcastEpisode
) )
@JsonIgnoreProperties(ignoreUnknown = true)
data class LibraryItemSearchResultSeriesItemType(
var series: LibrarySeriesItem,
var books: List<LibraryItem>?
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class LibraryItemSearchResultLibraryItemType(
val libraryItem: LibraryItem
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class LibraryItemSearchResultType(
var book:List<LibraryItemSearchResultLibraryItemType>?,
var podcast:List<LibraryItemSearchResultLibraryItemType>?,
var series:List<LibraryItemSearchResultSeriesItemType>?,
var authors:List<LibraryAuthorItem>?
)
// For personalized shelves
@JsonTypeInfo(
use=JsonTypeInfo.Id.NAME,
property = "type",
include = JsonTypeInfo.As.PROPERTY,
visible = true
)
@JsonSubTypes(
JsonSubTypes.Type(LibraryShelfBookEntity::class, name = "book"),
JsonSubTypes.Type(LibraryShelfSeriesEntity::class, name = "series"),
JsonSubTypes.Type(LibraryShelfAuthorEntity::class, name = "authors"),
JsonSubTypes.Type(LibraryShelfEpisodeEntity::class, name = "episode"),
JsonSubTypes.Type(LibraryShelfPodcastEntity::class, name = "podcast")
)
@JsonIgnoreProperties(ignoreUnknown = true)
sealed class LibraryShelfType(
open val id: String,
open val label: String,
open val total: Int,
open val type: String,
)
data class LibraryShelfBookEntity(
override val id: String,
override val label: String,
override val total: Int,
override val type: String,
val entities: List<LibraryItem>?
) : LibraryShelfType(id, label, total, type)
data class LibraryShelfSeriesEntity(
override val id: String,
override val label: String,
override val total: Int,
override val type: String,
val entities: List<LibrarySeriesItem>?
) : LibraryShelfType(id, label, total, type)
data class LibraryShelfAuthorEntity(
override val id: String,
override val label: String,
override val total: Int,
override val type: String,
val entities: List<LibraryAuthorItem>?
) : LibraryShelfType(id, label, total, type)
data class LibraryShelfEpisodeEntity(
override val id: String,
override val label: String,
override val total: Int,
override val type: String,
val entities: List<LibraryItem>?
) : LibraryShelfType(id, label, total, type)
data class LibraryShelfPodcastEntity(
override val id: String,
override val label: String,
override val total: Int,
override val type: String,
val entities: List<LibraryItem>?
) : LibraryShelfType(id, label, total, type)
@@ -28,6 +28,10 @@ enum class StreamingUsingCellularSetting {
ASK, ALWAYS, NEVER ASK, ALWAYS, NEVER
} }
enum class AndroidAutoBrowseSeriesSequenceOrderSetting {
ASC, DESC
}
data class ServerConnectionConfig( data class ServerConnectionConfig(
var id:String, var id:String,
var index:Int, var index:Int,
@@ -133,7 +137,9 @@ data class DeviceSettings(
var disableSleepTimerResetFeedback: Boolean, var disableSleepTimerResetFeedback: Boolean,
var languageCode: String, var languageCode: String,
var downloadUsingCellular: DownloadUsingCellularSetting, var downloadUsingCellular: DownloadUsingCellularSetting,
var streamingUsingCellular: StreamingUsingCellularSetting var streamingUsingCellular: StreamingUsingCellularSetting,
var androidAutoBrowseLimitForGrouping: Int,
var androidAutoBrowseSeriesSequenceOrder: AndroidAutoBrowseSeriesSequenceOrderSetting
) { ) {
companion object { companion object {
// Static method to get default device settings // Static method to get default device settings
@@ -159,7 +165,9 @@ data class DeviceSettings(
disableSleepTimerResetFeedback = false, disableSleepTimerResetFeedback = false,
languageCode = "en-us", languageCode = "en-us",
downloadUsingCellular = DownloadUsingCellularSetting.ALWAYS, downloadUsingCellular = DownloadUsingCellularSetting.ALWAYS,
streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS,
androidAutoBrowseLimitForGrouping = 100,
androidAutoBrowseSeriesSequenceOrder = AndroidAutoBrowseSeriesSequenceOrderSetting.ASC
) )
} }
} }
@@ -0,0 +1,63 @@
package com.audiobookshelf.app.data
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.R
import com.audiobookshelf.app.device.DeviceManager
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
class LibraryAuthorItem(
id:String,
var libraryId:String,
var name:String,
var description:String?,
var imagePath:String?,
var addedAt:Long,
var updatedAt:Long,
var numBooks:Int?,
var libraryItems:MutableList<LibraryItem>?,
var series:MutableList<LibrarySeriesItem>?
) : LibraryItemWrapper(id) {
@get:JsonIgnore
val title get() = name
@get:JsonIgnore
val bookCount get() = if (numBooks != null) numBooks else libraryItems!!.size
@JsonIgnore
fun getPortraitUri(): Uri {
if (imagePath == null) {
return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.md_account_outline)
}
return Uri.parse("${DeviceManager.serverAddress}/api/authors/$id/image?token=${DeviceManager.token}")
}
@JsonIgnore
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, groupTitle: String?): MediaDescriptionCompat {
val extras = Bundle()
if (groupTitle !== null) {
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, groupTitle)
}
val mediaId = "__LIBRARY__${libraryId}__AUTHOR__${id}"
return MediaDescriptionCompat.Builder()
.setMediaId(mediaId)
.setTitle(title)
.setIconUri(getPortraitUri())
.setSubtitle("${bookCount} books")
.setExtras(extras)
.build()
}
@JsonIgnore
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
return getMediaDescription(progress, ctx, null)
}
}
@@ -0,0 +1,40 @@
package com.audiobookshelf.app.data
import android.content.Context
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
class LibraryCollection(
id:String,
var libraryId:String,
var name:String,
//var userId:String?,
var description:String?,
var books:MutableList<LibraryItem>?,
) : LibraryItemWrapper(id) {
@get:JsonIgnore
val title get() = name
@get:JsonIgnore
val bookCount get() = if (books != null) books!!.size else 0
@get:JsonIgnore
val audiobookCount get() = books?.filter { book -> (book.media as Book).getAudioTracks().isNotEmpty() }?.size ?: 0
@JsonIgnore
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
val extras = Bundle()
val mediaId = "__LIBRARY__${libraryId}__COLLECTION__${id}"
return MediaDescriptionCompat.Builder()
.setMediaId(mediaId)
.setTitle(title)
//.setIconUri(getCoverUri())
.setSubtitle("${bookCount} books")
.setExtras(extras)
.build()
}
}
@@ -32,10 +32,18 @@ class LibraryItem(
var media:MediaType, var media:MediaType,
var libraryFiles:MutableList<LibraryFile>?, var libraryFiles:MutableList<LibraryFile>?,
var userMediaProgress:MediaProgress?, // Only included when requesting library item with progress (for downloads) var userMediaProgress:MediaProgress?, // Only included when requesting library item with progress (for downloads)
var localLibraryItemId:String? // For Android Auto var collapsedSeries: CollapsedSeries?,
var localLibraryItemId:String?, // For Android Auto
val recentEpisode: PodcastEpisode? // Podcast episode shelf uses this
) : LibraryItemWrapper(id) { ) : LibraryItemWrapper(id) {
@get:JsonIgnore @get:JsonIgnore
val title get() = media.metadata.title val title: String
get() {
if (collapsedSeries != null) {
return collapsedSeries!!.title
}
return media.metadata.title
}
@get:JsonIgnore @get:JsonIgnore
val authorName get() = media.metadata.getAuthorDisplayName() val authorName get() = media.metadata.getAuthorDisplayName()
@@ -50,57 +58,116 @@ class LibraryItem(
@JsonIgnore @JsonIgnore
fun checkHasTracks():Boolean { fun checkHasTracks():Boolean {
return if (mediaType == "podcast") { return media.checkHasTracks()
((media as Podcast).numEpisodes ?: 0) > 0 }
} else {
((media as Book).numTracks ?: 0) > 0 @get:JsonIgnore
val seriesSequence: String
get() {
if (mediaType != "podcast") {
return ((media as Book).metadata as BookMetadata).series?.get(0)?.sequence.orEmpty()
} else {
return ""
}
} }
@get:JsonIgnore
val seriesSequenceParts: List<String>
get() {
if (seriesSequence.isEmpty()) {
return listOf("")
}
return seriesSequence.split(".", limit = 2)
}
@JsonIgnore
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, authorId: String?, showSeriesNumber: Boolean?, groupTitle: String?): MediaDescriptionCompat {
val extras = Bundle()
if (collapsedSeries == null) {
if (localLibraryItemId != null) {
extras.putLong(
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
MediaDescriptionCompat.STATUS_DOWNLOADED
)
}
if (progress != null) {
if (progress.isFinished) {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
)
} else {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
)
extras.putDouble(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress
)
}
} else if (mediaType != "podcast") {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
)
}
if (media.metadata.explicit) {
extras.putLong(
MediaConstants.METADATA_KEY_IS_EXPLICIT,
MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
)
}
}
if (groupTitle !== null) {
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, groupTitle)
}
val mediaId = if (localLibraryItemId != null) {
localLibraryItemId
} else if (collapsedSeries != null) {
if (authorId != null) {
"__LIBRARY__${libraryId}__AUTHOR_SERIES__${authorId}__${collapsedSeries!!.id}"
} else {
"__LIBRARY__${libraryId}__SERIES__${collapsedSeries!!.id}"
}
} else {
id
}
var subtitle = authorName
if (collapsedSeries != null) {
subtitle = "${collapsedSeries!!.numBooks} books"
}
var itemTitle = title
if (showSeriesNumber == true && seriesSequence != "") {
itemTitle = "$seriesSequence. $itemTitle"
}
return MediaDescriptionCompat.Builder()
.setMediaId(mediaId)
.setTitle(itemTitle)
.setIconUri(getCoverUri())
.setSubtitle(subtitle)
.setExtras(extras)
.build()
}
@JsonIgnore
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, authorId: String?, showSeriesNumber: Boolean?): MediaDescriptionCompat {
return getMediaDescription(progress, ctx, authorId, showSeriesNumber, null)
}
@JsonIgnore
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, authorId: String?): MediaDescriptionCompat {
return getMediaDescription(progress, ctx, authorId, null, null)
} }
@JsonIgnore @JsonIgnore
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat { override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
val extras = Bundle() /*
This is needed so Android auto library hierarchy for author series can be implemented
if (localLibraryItemId != null) { */
extras.putLong( return getMediaDescription(progress, ctx, null, null, null)
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
MediaDescriptionCompat.STATUS_DOWNLOADED
)
}
if (progress != null) {
if (progress.isFinished) {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
)
} else {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
)
extras.putDouble(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress
)
}
} else if (mediaType != "podcast") {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
)
}
if (media.metadata.explicit) {
extras.putLong(MediaConstants.METADATA_KEY_IS_EXPLICIT, MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT)
}
val mediaId = localLibraryItemId ?: id
return MediaDescriptionCompat.Builder()
.setMediaId(mediaId)
.setTitle(title)
.setIconUri(getCoverUri())
.setSubtitle(authorName)
.setExtras(extras)
.build()
} }
} }
@@ -0,0 +1,60 @@
package com.audiobookshelf.app.data
import android.content.Context
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import androidx.media.utils.MediaConstants
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
class LibrarySeriesItem(
id:String,
var libraryId:String,
var name:String,
var description:String?,
var addedAt:Long,
var updatedAt:Long,
var books:MutableList<LibraryItem>?,
var localLibraryItemId:String? // For Android Auto
) : LibraryItemWrapper(id) {
@get:JsonIgnore
val title get() = name
@get:JsonIgnore
val audiobookCount: Int
get() {
if (books == null) return 0
val booksWithAudio = books?.filter { b -> b.media.checkHasTracks() }
return booksWithAudio?.size ?: 0
}
@JsonIgnore
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, groupTitle: String?): MediaDescriptionCompat {
val extras = Bundle()
if (localLibraryItemId != null) {
extras.putLong(
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
MediaDescriptionCompat.STATUS_DOWNLOADED
)
}
if (groupTitle !== null) {
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, groupTitle)
}
val mediaId = "__LIBRARY__${libraryId}__SERIES__${id}"
return MediaDescriptionCompat.Builder()
.setMediaId(mediaId)
.setTitle(title)
//.setIconUri(getCoverUri())
.setSubtitle("$audiobookCount books")
.setExtras(extras)
.build()
}
@JsonIgnore
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
return getMediaDescription(progress, ctx, null)
}
}
@@ -41,7 +41,7 @@ data class LocalMediaItem(
@JsonIgnore @JsonIgnore
fun getMediaMetadata():MediaTypeMetadata { fun getMediaMetadata():MediaTypeMetadata {
return if (mediaType == "book") { return if (mediaType == "book") {
BookMetadata(name,null, mutableListOf(), mutableListOf(), mutableListOf(),null,null,null,null,null,null,null,false,null,null,null,null) BookMetadata(name,null, mutableListOf(), mutableListOf(), mutableListOf(),null,null,null,null,null,null,null,false,null,null,null, null, null)
} else { } else {
PodcastMetadata(name,null,null, mutableListOf(), false) PodcastMetadata(name,null,null, mutableListOf(), false)
} }
@@ -61,6 +61,12 @@ object DeviceManager {
if (deviceData.deviceSettings?.streamingUsingCellular == null) { if (deviceData.deviceSettings?.streamingUsingCellular == null) {
deviceData.deviceSettings?.streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS deviceData.deviceSettings?.streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS
} }
if (deviceData.deviceSettings?.androidAutoBrowseLimitForGrouping == null) {
deviceData.deviceSettings?.androidAutoBrowseLimitForGrouping = 100
}
if (deviceData.deviceSettings?.androidAutoBrowseSeriesSequenceOrder == null) {
deviceData.deviceSettings?.androidAutoBrowseSeriesSequenceOrder = AndroidAutoBrowseSeriesSequenceOrderSetting.ASC
}
} }
fun getBase64Id(id:String):String { fun getBase64Id(id:String):String {
@@ -20,13 +20,23 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
val tag = "MediaManager" val tag = "MediaManager"
private var serverLibraryItems = mutableListOf<LibraryItem>() // Store all items here private var serverLibraryItems = mutableListOf<LibraryItem>() // Store all items here
private var selectedLibraryItems = mutableListOf<LibraryItem>()
private var selectedLibraryId = "" private var cachedLibraryAuthors : MutableMap<String, MutableMap<String, LibraryAuthorItem>> = hashMapOf()
private var cachedLibraryAuthorItems : MutableMap<String, MutableMap<String, List<LibraryItem>>> = hashMapOf()
private var cachedLibraryAuthorSeriesItems : MutableMap<String, MutableMap<String, List<LibraryItem>>> = hashMapOf()
private var cachedLibrarySeries : MutableMap<String, List<LibrarySeriesItem>> = hashMapOf()
private var cachedLibrarySeriesItem : MutableMap<String, MutableMap<String, List<LibraryItem>>> = hashMapOf()
private var cachedLibraryCollections : MutableMap<String, MutableMap<String, LibraryCollection>> = hashMapOf()
private var cachedLibraryRecentShelves : MutableMap<String, MutableList<LibraryShelfType>> = hashMapOf()
private var cachedLibraryDiscovery : MutableMap<String, MutableList<LibraryItem>> = hashMapOf()
private var cachedLibraryPodcasts : MutableMap<String, MutableMap<String, LibraryItem>> = hashMapOf()
private var isLibraryPodcastsCached : MutableMap<String, Boolean> = hashMapOf()
var allLibraryPersonalizationsDone : Boolean = false
private var libraryPersonalizationsDone : Int = 0
private var selectedPodcast:Podcast? = null private var selectedPodcast:Podcast? = null
private var selectedLibraryItemId:String? = null private var selectedLibraryItemId:String? = null
private var podcastEpisodeLibraryItemMap = mutableMapOf<String, LibraryItemWithEpisode>() private var podcastEpisodeLibraryItemMap = mutableMapOf<String, LibraryItemWithEpisode>()
private var serverLibraryCategories = listOf<LibraryCategory>()
private var serverConfigIdUsed:String? = null private var serverConfigIdUsed:String? = null
private var serverConfigLastPing:Long = 0L private var serverConfigLastPing:Long = 0L
var serverUserMediaProgress:MutableList<MediaProgress> = mutableListOf() var serverUserMediaProgress:MutableList<MediaProgress> = mutableListOf()
@@ -39,6 +49,35 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
return serverLibraries.find { it.id == id } != null return serverLibraries.find { it.id == id } != null
} }
/**
* Check if there is discovery shelf for [libraryId]
* If personalized shelves are not yet populated for library then populate
*
*/
fun getHasDiscovery(libraryId: String) : Boolean {
if (cachedLibraryDiscovery.containsKey(libraryId)) {
if (cachedLibraryDiscovery[libraryId]!!.isNotEmpty()) {
return true
}
} else {
populatePersonalizedDataForLibrary(libraryId){}
}
return false
}
fun getLibrary(id:String) : Library? {
return serverLibraries.find { it.id == id }
}
/**
* Add [libraryItem] to [serverLibraryItems] if it is not already added
*/
private fun addServerLibrary(libraryItem: LibraryItem) {
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
fun getSavedPlaybackRate():Float { fun getSavedPlaybackRate():Float {
if (userSettingsPlaybackRate != null) { if (userSettingsPlaybackRate != null) {
return userSettingsPlaybackRate ?: 1f return userSettingsPlaybackRate ?: 1f
@@ -91,19 +130,32 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
} }
} }
fun checkResetServerItems() { fun checkResetServerItems():Boolean {
// When opening android auto need to check if still connected to server // When opening android auto need to check if still connected to server
// and reset any server data already set // and reset any server data already set
val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig() val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig()
if (!DeviceManager.isConnectedToServer || !DeviceManager.checkConnectivity(ctx) || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) { if (!DeviceManager.isConnectedToServer || !DeviceManager.checkConnectivity(ctx) || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) {
Log.d(tag, "Reset caches")
podcastEpisodeLibraryItemMap = mutableMapOf() podcastEpisodeLibraryItemMap = mutableMapOf()
serverLibraryCategories = listOf()
serverLibraries = listOf() serverLibraries = listOf()
serverLibraryItems = mutableListOf() serverLibraryItems = mutableListOf()
selectedLibraryItems = mutableListOf() cachedLibraryAuthors = hashMapOf()
selectedLibraryId = "" cachedLibraryAuthorItems = hashMapOf()
cachedLibraryAuthorSeriesItems = hashMapOf()
cachedLibrarySeries = hashMapOf()
cachedLibrarySeriesItem = hashMapOf()
cachedLibraryCollections = hashMapOf()
cachedLibraryRecentShelves = hashMapOf()
cachedLibraryDiscovery = hashMapOf()
cachedLibraryPodcasts = hashMapOf()
isLibraryPodcastsCached = hashMapOf()
serverItemsInProgress = listOf()
allLibraryPersonalizationsDone = false
libraryPersonalizationsDone = 0
return true
} }
return false
} }
private fun loadItemsInProgressForAllLibraries(cb: (List<ItemInProgress>) -> Unit) { private fun loadItemsInProgressForAllLibraries(cb: (List<ItemInProgress>) -> Unit) {
@@ -120,28 +172,458 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
} }
} }
fun loadLibraryItemsWithAudio(libraryId:String, cb: (List<LibraryItem>) -> Unit) { /**
if (selectedLibraryItems.isNotEmpty() && selectedLibraryId == libraryId) { * Load personalized shelves from server for all libraries.
cb(selectedLibraryItems) * [cb] resolves when all libraries are processed
*/
fun populatePersonalizedDataForAllLibraries(cb: () -> Unit ) {
serverLibraries.forEach {
libraryPersonalizationsDone++
Log.d(tag, "Loading personalization for library ${it.name} - ${it.id} - ${it.mediaType}")
populatePersonalizedDataForLibrary(it.id) {
Log.d(tag, "Loaded personalization for library ${it.name} - ${it.id} - ${it.mediaType}")
libraryPersonalizationsDone--
}
}
while (libraryPersonalizationsDone > 0) { }
Log.d(tag, "Finished loading all library personalization data")
allLibraryPersonalizationsDone = true
cb()
}
/**
* Get personalized shelves from server for selected [libraryId].
* Populates [cachedLibraryRecentShelves] and [cachedLibraryDiscovery].
*/
private fun populatePersonalizedDataForLibrary(libraryId: String, cb: () -> Unit) {
apiHandler.getLibraryPersonalized(libraryId) { shelves ->
Log.d(tag, "populatePersonalizedDataForLibrary $libraryId")
if (shelves === null) return@getLibraryPersonalized
shelves.map { shelf ->
Log.d(tag, "$shelf")
if (shelf.type == "book") {
if (shelf.id == "continue-listening") return@map
else if (shelf.id == "listen-again") return@map
else if (shelf.id == "recently-added") {
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
cachedLibraryRecentShelves[libraryId] = mutableListOf()
}
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
}
}
else if (shelf.id == "discover") {
if (!cachedLibraryDiscovery.containsKey(libraryId)) {
cachedLibraryDiscovery[libraryId] = mutableListOf()
}
(shelf as LibraryShelfBookEntity).entities?.map {
cachedLibraryDiscovery[libraryId]!!.add(it)
}
}
else if (shelf.id == "continue-reading") return@map
else if (shelf.id == "continue-series") return@map
shelf as LibraryShelfBookEntity
} else if (shelf.type == "series") {
if (shelf.id == "recent-series") {
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
cachedLibraryRecentShelves[libraryId] = mutableListOf()
}
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
}
}
} else if (shelf.type == "episode") {
if (shelf.id == "continue-listening") return@map
else if (shelf.id == "listen-again") return@map
else if (shelf.id == "newest-episodes") {
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
cachedLibraryRecentShelves[libraryId] = mutableListOf()
}
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
}
val podcastLibraryItemIds = mutableListOf<String>()
(shelf as LibraryShelfEpisodeEntity).entities?.forEach { libraryItem ->
if (!podcastLibraryItemIds.contains(libraryItem.id)) {
podcastLibraryItemIds.add(libraryItem.id)
loadPodcastItem(libraryItem.libraryId, libraryItem.id) {}
}
}
}
} else if (shelf.type == "podcast") {
if (shelf.id == "recently-added"){
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
cachedLibraryRecentShelves[libraryId] = mutableListOf()
}
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
}
}
else if (shelf.id == "discover"){
return@map
}
} else if (shelf.type =="authors") {
if (shelf.id == "newest-authors") {
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
cachedLibraryRecentShelves[libraryId] = mutableListOf()
}
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
}
}
}
}
Log.d(tag, "populatePersonalizedDataForLibrary $libraryId DONE")
cb()
}
}
/**
* Returns podcasts for selected library.
* If data is not found from local cache it is loaded from server
*/
fun loadLibraryPodcasts(libraryId:String, cb: (List<LibraryItem>?) -> Unit) {
// Without this there is possibility that only recent podcasts get loaded
// Loading recent podcasts will also create cachedLibraryPodcasts entry for library
if (!isLibraryPodcastsCached.containsKey(libraryId)) {
isLibraryPodcastsCached[libraryId] = false
}
// Ensure that there is map for library
if (!cachedLibraryPodcasts.containsKey(libraryId)) {
cachedLibraryPodcasts[libraryId] = mutableMapOf()
}
if (isLibraryPodcastsCached.getOrElse(libraryId) {false}) {
Log.d(tag, "loadLibraryPodcasts: Found from cache: $libraryId")
cb(cachedLibraryPodcasts[libraryId]?.values?.sortedBy { libraryItem -> (libraryItem.media as Podcast).metadata.title })
} else { } else {
apiHandler.getLibraryItems(libraryId) { libraryItems -> apiHandler.getLibraryItems(libraryId) { libraryItems ->
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() } val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
if (libraryItemsWithAudio.isNotEmpty()) {
selectedLibraryId = libraryId
}
selectedLibraryItems = mutableListOf()
libraryItemsWithAudio.forEach { libraryItem -> libraryItemsWithAudio.forEach { libraryItem ->
selectedLibraryItems.add(libraryItem) cachedLibraryPodcasts[libraryId]?.set(libraryItem.id, libraryItem)
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) { if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem) serverLibraryItems.add(libraryItem)
} }
} }
isLibraryPodcastsCached[libraryId] = true
Log.d(tag, "loadLibraryPodcasts: loaded from server: $libraryId")
cb(libraryItemsWithAudio.sortedBy { libraryItem -> (libraryItem.media as Podcast).metadata.title })
}
}
}
/**
* Returns series with audio books from selected library.
* If data is not found from local cache then it will be fetched from server
*/
fun loadLibrarySeriesWithAudio(libraryId:String, cb: (List<LibrarySeriesItem>) -> Unit) {
// Check "cache" first
if (cachedLibrarySeries.containsKey(libraryId)) {
Log.d(tag, "Series with audio found from cache | Library $libraryId ")
cb(cachedLibrarySeries[libraryId] as List<LibrarySeriesItem>)
} else {
apiHandler.getLibrarySeries(libraryId) { seriesItems ->
Log.d(tag, "Series with audio loaded from server | Library $libraryId")
val seriesItemsWithAudio = seriesItems.filter { si -> si.audiobookCount > 0 }
cachedLibrarySeries[libraryId] = seriesItemsWithAudio
cb(seriesItemsWithAudio)
}
}
}
/**
* Returns series with audiobooks from selected library using filter for paging.
* If data is not found from local cache then it will be fetched from server
*/
fun loadLibrarySeriesWithAudio(libraryId:String, seriesFilter:String, cb: (List<LibrarySeriesItem>) -> Unit) {
// Check "cache" first
if (!cachedLibrarySeries.containsKey(libraryId)) {
loadLibrarySeriesWithAudio(libraryId) {}
} else {
Log.d(tag, "Series with audio found from cache | Library $libraryId ")
}
val seriesWithBooks = cachedLibrarySeries[libraryId]!!.filter { ls -> ls.title.uppercase().startsWith(seriesFilter) }.toList()
cb(seriesWithBooks)
}
/**
* Sorts books in series. Assumes that sequence is main.minor
*/
private fun sortSeriesBooks(seriesBooks: List<LibraryItem>) : List<LibraryItem> {
val sortingLogic = compareBy<LibraryItem> { it.seriesSequenceParts[0].length }
.thenBy { it.seriesSequenceParts[0].ifEmpty { "" } }
.thenBy { it.seriesSequenceParts.getOrElse(1) { "" }.length }
.thenBy { it.seriesSequenceParts.getOrElse(1) { "" } }
return seriesBooks.sortedWith(sortingLogic)
}
/**
* Returns books for series from library.
* If data is not found from local cache then it will be fetched from server
*/
fun loadLibrarySeriesItemsWithAudio(libraryId:String, seriesId:String, cb: (List<LibraryItem>) -> Unit) {
// Check "cache" first
if (!cachedLibrarySeriesItem.containsKey(libraryId)) {
cachedLibrarySeriesItem[libraryId] = hashMapOf()
}
if (cachedLibrarySeriesItem[libraryId]!!.containsKey(seriesId)) {
Log.d(tag, "Items for series $seriesId found from cache | Library $libraryId")
cachedLibrarySeriesItem[libraryId]!![seriesId]?.let { cb(it) }
} else {
apiHandler.getLibrarySeriesItems(libraryId, seriesId) { libraryItems ->
Log.d(tag, "Items for series $seriesId loaded from server | Library $libraryId")
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
val sortedLibraryItemsWithAudio = sortSeriesBooks(libraryItemsWithAudio)
cachedLibrarySeriesItem[libraryId]!![seriesId] = sortedLibraryItemsWithAudio
sortedLibraryItemsWithAudio.forEach { libraryItem ->
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
cb(sortedLibraryItemsWithAudio)
}
}
}
/**
* Returns authors with books from library.
* If data is not found from local cache then it will be fetched from server
*/
fun loadAuthorsWithBooks(libraryId:String, cb: (List<LibraryAuthorItem>) -> Unit) {
// Check "cache" first
if (cachedLibraryAuthors.containsKey(libraryId)) {
Log.d(tag, "Authors with books found from cache | Library $libraryId ")
cb(cachedLibraryAuthors[libraryId]!!.values.toList())
} else {
// Fetch data from server and add it to local "cache"
apiHandler.getLibraryAuthors(libraryId) { authorItems ->
Log.d(tag, "Authors with books loaded from server | Library $libraryId ")
// TO-DO: This check won't ensure that there is audiobooks. Current API won't offer ability to do so
var authorItemsWithBooks = authorItems.filter { li -> li.bookCount != null && li.bookCount!! > 0 }
authorItemsWithBooks = authorItemsWithBooks.sortedBy { it.name }
// Ensure that there is map for library
cachedLibraryAuthors[libraryId] = mutableMapOf()
// Cache authors
authorItemsWithBooks.forEach {
if (!cachedLibraryAuthors[libraryId]!!.containsKey(it.id)) {
cachedLibraryAuthors[libraryId]!![it.id] = it
}
}
cb(authorItemsWithBooks)
}
}
}
/**
* Returns authors with books from selected library using filter for paging.
* If data is not found from local cache then it will be fetched from server
*/
fun loadAuthorsWithBooks(libraryId:String, authorFilter: String, cb: (List<LibraryAuthorItem>) -> Unit) {
// Check "cache" first
if (cachedLibraryAuthors.containsKey(libraryId)) {
Log.d(tag, "Authors with books found from cache | Library $libraryId ")
} else {
loadAuthorsWithBooks(libraryId) {}
}
val authorsWithBooks = cachedLibraryAuthors[libraryId]!!.values.filter { lai -> lai.name.uppercase().startsWith(authorFilter) }.toList()
cb(authorsWithBooks)
}
/**
* Returns audiobooks for author from library
* If data is not found from local cache then it will be fetched from server
*/
fun loadAuthorBooksWithAudio(libraryId:String, authorId:String, cb: (List<LibraryItem>) -> Unit) {
// Ensure that there is map for library
if (!cachedLibraryAuthorItems.containsKey(libraryId)) {
cachedLibraryAuthorItems[libraryId] = mutableMapOf()
}
// Check "cache" first
if (cachedLibraryAuthorItems[libraryId]!!.containsKey(authorId)) {
Log.d(tag, "Items for author $authorId found from cache | Library $libraryId")
cachedLibraryAuthorItems[libraryId]!![authorId]?.let { cb(it) }
} else {
apiHandler.getLibraryItemsFromAuthor(libraryId, authorId) { libraryItems ->
Log.d(tag, "Items for author $authorId loaded from server | Library $libraryId")
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
cachedLibraryAuthorItems[libraryId]!![authorId] = libraryItemsWithAudio
libraryItemsWithAudio.forEach { libraryItem ->
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
cb(libraryItemsWithAudio) cb(libraryItemsWithAudio)
} }
} }
} }
/**
* Returns audiobooks for author from specified series within library
* If data is not found from local cache then it will be fetched from server
*/
fun loadAuthorSeriesBooksWithAudio(libraryId:String, authorId:String, seriesId: String, cb: (List<LibraryItem>) -> Unit) {
val authorSeriesKey = "$authorId|$seriesId"
// Ensure that there is map for library
if (!cachedLibraryAuthorSeriesItems.containsKey(libraryId)) {
cachedLibraryAuthorSeriesItems[libraryId] = mutableMapOf()
}
// Check "cache" first
if (cachedLibraryAuthorSeriesItems[libraryId]!!.containsKey(authorSeriesKey)) {
Log.d(tag, "Items for series $seriesId with author $authorId found from cache | Library $libraryId")
cachedLibraryAuthorSeriesItems[libraryId]!![authorSeriesKey]?.let { cb(it) }
} else {
apiHandler.getLibrarySeriesItems(libraryId, seriesId) { libraryItems ->
Log.d(tag, "Items for series $seriesId with author $authorId loaded from server | Library $libraryId")
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
if (!cachedLibraryAuthors[libraryId]!!.containsKey(authorId)) {
Log.d(tag, "Author data is missing")
}
val authorName = cachedLibraryAuthors[libraryId]!![authorId]?.name ?: ""
Log.d(tag, "Using author name: $authorName")
val libraryItemsFromAuthorWithAudio = libraryItemsWithAudio.filter { li -> li.authorName.indexOf(authorName, ignoreCase = true) >= 0 }
val sortedLibraryItemsWithAudio = sortSeriesBooks(libraryItemsFromAuthorWithAudio)
cachedLibraryAuthorSeriesItems[libraryId]!![authorId] = sortedLibraryItemsWithAudio
sortedLibraryItemsWithAudio.forEach { libraryItem ->
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
cb(sortedLibraryItemsWithAudio)
}
}
}
/**
* Returns collections with audiobooks from library
* If data is not found from local cache then it will be fetched from server
*/
fun loadLibraryCollectionsWithAudio(libraryId:String, cb: (List<LibraryCollection>) -> Unit) {
if (cachedLibraryCollections.containsKey(libraryId)) {
Log.d(tag, "Collections with books found from cache | Library $libraryId ")
cb(cachedLibraryCollections[libraryId]!!.values.toList())
} else {
apiHandler.getLibraryCollections(libraryId) { libraryCollections ->
Log.d(tag, "Collections with books loaded from server | Library $libraryId ")
val libraryCollectionsWithAudio = libraryCollections.filter { lc -> lc.audiobookCount > 0 }
// Cache collections
cachedLibraryCollections[libraryId] = hashMapOf()
libraryCollectionsWithAudio.forEach {
if (!cachedLibraryCollections[libraryId]!!.containsKey(it.id)) {
cachedLibraryCollections[libraryId]!![it.id] = it
}
}
cb(libraryCollectionsWithAudio)
}
}
}
/**
* Returns audiobooks for collection from library
* If data is not found from local cache then it will be fetched from server
*/
fun loadLibraryCollectionBooksWithAudio(libraryId: String, collectionId: String, cb: (List<LibraryItem>) -> Unit) {
if (!cachedLibraryCollections.containsKey(libraryId)) {
loadLibraryCollectionsWithAudio(libraryId) {}
}
Log.d(tag, "Trying to find collection $collectionId items from from cache | Library $libraryId ")
if ( cachedLibraryCollections[libraryId]!!.containsKey(collectionId)) {
val libraryCollectionBookswithAudio = cachedLibraryCollections[libraryId]!![collectionId]?.books
libraryCollectionBookswithAudio?.forEach { libraryItem ->
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
cb(libraryCollectionBookswithAudio as List<LibraryItem>)
}
}
/**
* Returns audiobooks from discovery shelf for [libraryId]
* If data is not found from local cache then it will be fetched from server
*/
fun loadLibraryDiscoveryBooksWithAudio(libraryId: String, cb: (List<LibraryItem>) -> Unit) {
if (!cachedLibraryDiscovery.containsKey(libraryId)) {
cb(listOf())
}
val libraryItemsWithAudio = cachedLibraryDiscovery[libraryId]?.filter { li -> li.checkHasTracks() }
libraryItemsWithAudio?.forEach { libraryItem -> addServerLibrary(libraryItem) }
cb(libraryItemsWithAudio as List<LibraryItem>)
}
/**
* Returns recent shelves for [libraryId]
* If data is not shelves are found returns empty list
*/
fun getLibraryRecentShelfs(libraryId: String, cb: (List<LibraryShelfType>) -> Unit) {
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
Log.d(tag, "getLibraryRecentShelfs: No shelves $libraryId")
cb(listOf())
return
}
cb(cachedLibraryRecentShelves[libraryId] as List<LibraryShelfType>)
}
/**
* Returns recent shelf by [type] for [libraryId]
* If shelf is not found returns null
*/
fun getLibraryRecentShelfByType(libraryId: String, type:String, cb: (LibraryShelfType?) -> Unit) {
Log.d(tag, "getLibraryRecentShelfByType: $libraryId | $type")
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
cb(null)
return
}
for (shelf in cachedLibraryRecentShelves[libraryId]!!) {
if (shelf.type == type.lowercase()) {
cb(shelf)
return
}
}
cb(null)
}
/**
* Loads podcasts for newest episodes shelf
*/
private fun loadPodcastItem(libraryId: String, libraryItemId: String, cb: (LibraryItem?) -> Unit) {
// Ensure that there is map for library
if (!cachedLibraryPodcasts.containsKey(libraryId)) {
cachedLibraryPodcasts[libraryId] = mutableMapOf()
}
if (cachedLibraryPodcasts[libraryId]!!.containsKey(libraryItemId)) {
Log.d(tag, "loadPodcastItem: Podcast found from cache | Library $libraryItemId ")
cb(cachedLibraryPodcasts[libraryId]?.get(libraryItemId))
} else {
Log.d(tag, "loadPodcastItem: Calling getLibraryItem $libraryItemId")
apiHandler.getLibraryItem(libraryItemId) { libraryItem ->
if (libraryItem !== null) {
Log.d(tag, "loadPodcastItem: Got library item ${libraryItem.id} ${libraryItem.media.metadata.title}")
val podcast = libraryItem.media as Podcast
podcast.episodes?.forEach { podcastEpisode ->
podcastEpisodeLibraryItemMap[podcastEpisode.id] = LibraryItemWithEpisode(libraryItem, podcastEpisode)
}
cachedLibraryPodcasts[libraryId]?.set(libraryItemId, libraryItem)
cb(libraryItem)
}
}
}
}
private fun loadLibraryItem(libraryItemId:String, cb: (LibraryItemWrapper?) -> Unit) { private fun loadLibraryItem(libraryItemId:String, cb: (LibraryItemWrapper?) -> Unit) {
if (libraryItemId.startsWith("local")) { if (libraryItemId.startsWith("local")) {
cb(DeviceManager.dbManager.getLocalLibraryItem(libraryItemId)) cb(DeviceManager.dbManager.getLocalLibraryItem(libraryItemId))
@@ -187,8 +669,8 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
} }
selectedLibraryItemId = libraryItemWrapper.id selectedLibraryItemId = libraryItemWrapper.id
selectedPodcast = podcast selectedPodcast = podcast
val episodes = podcast.episodes?.sortedByDescending { it.publishedAt }
val children = podcast.episodes?.map { podcastEpisode -> val children = episodes?.map { podcastEpisode ->
val progress = serverUserMediaProgress.find { it.libraryItemId == libraryItemWrapper.id && it.episodeId == podcastEpisode.id } val progress = serverUserMediaProgress.find { it.libraryItemId == libraryItemWrapper.id && it.episodeId == podcastEpisode.id }
@@ -209,13 +691,16 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
} }
} }
/**
* Loads libraries for selected server with stats
*/
private fun loadLibraries(cb: (List<Library>) -> Unit) { private fun loadLibraries(cb: (List<Library>) -> Unit) {
if (serverLibraries.isNotEmpty()) { if (serverLibraries.isNotEmpty()) {
cb(serverLibraries) cb(serverLibraries)
} else { } else {
apiHandler.getLibraries { apiHandler.getLibraries { loadedLibraries ->
serverLibraries = it serverLibraries = loadedLibraries
cb(it) cb(serverLibraries)
} }
} }
} }
@@ -329,6 +814,25 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
} }
} }
fun initializeInProgressItems(cb: () -> Unit) {
Log.d(tag, "Initializing inprogress items")
loadItemsInProgressForAllLibraries { itemsInProgress ->
itemsInProgress.forEach {
val libraryItem = it.libraryItemWrapper as LibraryItem
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
if (it.episode != null) {
podcastEpisodeLibraryItemMap[it.episode.id] = LibraryItemWithEpisode(it.libraryItemWrapper, it.episode)
}
}
Log.d(tag, "Initializing inprogress items done")
cb()
}
}
fun loadAndroidAutoItems(cb: () -> Unit) { fun loadAndroidAutoItems(cb: () -> Unit) {
Log.d(tag, "Load android auto items") Log.d(tag, "Load android auto items")
@@ -343,23 +847,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
Log.w(tag, "No libraries returned from server request") Log.w(tag, "No libraries returned from server request")
cb() cb()
} else { } else {
val library = libraries[0] cb() // Fully loaded
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}")
loadItemsInProgressForAllLibraries { itemsInProgress ->
itemsInProgress.forEach {
val libraryItem = it.libraryItemWrapper as LibraryItem
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
if (it.episode != null) {
podcastEpisodeLibraryItemMap[it.episode.id] = LibraryItemWithEpisode(it.libraryItemWrapper, it.episode)
}
}
cb() // Fully loaded
}
} }
} }
} else { // Not connected to server } else { // Not connected to server
@@ -369,6 +857,65 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
} }
} }
/**
* Handles search requests.
* Searches from books, series and authors
*/
suspend fun doSearch(libraryId: String, queryString: String) : Map<String, List<MediaBrowserCompat.MediaItem>> {
return suspendCoroutine {
apiHandler.getSearchResults(libraryId, queryString) { searchResult ->
Log.d(tag, "searchLocalCache: $searchResult")
// Nothing found from server
if (searchResult === null) {
it.resume(mapOf())
return@getSearchResults
}
val foundItems: MutableMap<String, List<MediaBrowserCompat.MediaItem>> = mutableMapOf()
val serverLibrary = serverLibraries.find { sl -> sl.id == libraryId }
// Books
if (searchResult.book !== null && searchResult.book!!.isNotEmpty()) {
Log.d(tag, "searchLocalCache: found ${searchResult.book!!.size} books")
val children = searchResult.book!!.filter { it.libraryItem.checkHasTracks() }.map { bookResult ->
val libraryItem = bookResult.libraryItem
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
val progress = serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
libraryItem.localLibraryItemId = localLibraryItem?.id
val description = libraryItem.getMediaDescription(progress, ctx, null, null, "Books (${serverLibrary?.name})")
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
foundItems["book"] = children
}
if (searchResult.series !== null && searchResult.series!!.isNotEmpty()) {
Log.d(tag, "onSearch: found ${searchResult.series!!.size} series")
val children = searchResult.series!!.map { seriesResult ->
val seriesItem = seriesResult.series
seriesItem.books = seriesResult.books as MutableList<LibraryItem>
val description = seriesItem.getMediaDescription(null, ctx, "Series (${serverLibrary?.name})")
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
foundItems["series"] = children
}
if (searchResult.authors !== null && searchResult.authors!!.isNotEmpty()) {
Log.d(tag, "onSearch: found ${searchResult.authors!!.size} authors")
val children = searchResult.authors!!.map { authorItem ->
val description = authorItem.getMediaDescription(null, ctx, "Authors (${serverLibrary?.name})")
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
foundItems["authors"] = children
}
it.resume(foundItems)
}
}
}
fun getFirstItem() : LibraryItemWrapper? { fun getFirstItem() : LibraryItemWrapper? {
if (serverLibraryItems.isNotEmpty()) { if (serverLibraryItems.isNotEmpty()) {
return serverLibraryItems[0] return serverLibraryItems[0]
@@ -0,0 +1,55 @@
package com.audiobookshelf.app.media
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import androidx.annotation.AnyRes
import com.audiobookshelf.app.R
/**
* get uri to drawable or any other resource type if u wish
* @param drawableId - drawable res id
* @return - uri
*/
fun getUriToDrawable(context: Context, @AnyRes drawableId: Int): Uri {
return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
+ "://" + context.resources.getResourcePackageName(drawableId)
+ '/' + context.resources.getResourceTypeName(drawableId)
+ '/' + context.resources.getResourceEntryName(drawableId))
}
/**
* get uri to drawable or any other resource type if u wish
* @param drawableId - drawable res id
* @return - uri
*/
fun getUriToAbsIconDrawable(context: Context, absIconName: String): Uri {
val drawableId = when(absIconName) {
"audiobookshelf" -> R.drawable.abs_audiobookshelf
"authors" -> R.drawable.abs_authors
"book-1" -> R.drawable.abs_book_1
"books-1" -> R.drawable.abs_books_1
"books-2" -> R.drawable.abs_books_2
"columns" -> R.drawable.abs_columns
"database" -> R.drawable.abs_database
"file-picture" -> R.drawable.abs_file_picture
"headphones" -> R.drawable.abs_headphones
"heart" -> R.drawable.abs_heart
"microphone_1" -> R.drawable.abs_microphone_1
"microphone_2" -> R.drawable.abs_microphone_2
"microphone_3" -> R.drawable.abs_microphone_3
"music" -> R.drawable.abs_music
"podcast" -> R.drawable.abs_podcast
"radio" -> R.drawable.abs_radio
"rocket" -> R.drawable.abs_rocket
"rss" -> R.drawable.abs_rss
"star" -> R.drawable.abs_star
else -> R.drawable.icon_library_folder
}
return Uri.parse(
ContentResolver.SCHEME_ANDROID_RESOURCE
+ "://" + context.resources.getResourcePackageName(drawableId)
+ '/' + context.resources.getResourceTypeName(drawableId)
+ '/' + context.resources.getResourceEntryName(drawableId))
}
@@ -1,51 +1,45 @@
package com.audiobookshelf.app.player package com.audiobookshelf.app.player
import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.net.Uri
import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.MediaMetadataCompat
import androidx.annotation.AnyRes import android.util.Log
import com.audiobookshelf.app.R import com.audiobookshelf.app.R
import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.media.getUriToDrawable
class BrowseTree( class BrowseTree(
val context: Context, val context: Context,
itemsInProgress: List<ItemInProgress>, itemsInProgress: List<ItemInProgress>,
libraries: List<Library> libraries: List<Library>,
recentsLoaded: Boolean
) { ) {
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>() private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
/**
* get uri to drawable or any other resource type if u wish
* @param drawableId - drawable res id
* @return - uri
*/
private fun getUriToDrawable(@AnyRes drawableId: Int): Uri {
return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
+ "://" + context.resources.getResourcePackageName(drawableId)
+ '/' + context.resources.getResourceTypeName(drawableId)
+ '/' + context.resources.getResourceEntryName(drawableId))
}
init { init {
val rootList = mediaIdToChildren[AUTO_BROWSE_ROOT] ?: mutableListOf() val rootList = mediaIdToChildren[AUTO_BROWSE_ROOT] ?: mutableListOf()
val continueListeningMetadata = MediaMetadataCompat.Builder().apply { val continueListeningMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, CONTINUE_ROOT) putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, CONTINUE_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Listening") putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Continue")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.exo_icon_localaudio).toString()) putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString())
}.build()
val recentMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, RECENTLY_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Recent")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.md_clock_outline).toString())
}.build() }.build()
val downloadsMetadata = MediaMetadataCompat.Builder().apply { val downloadsMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT) putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads") putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.exo_icon_downloaddone).toString()) putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
}.build() }.build()
val librariesMetadata = MediaMetadataCompat.Builder().apply { val librariesMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LIBRARIES_ROOT) putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LIBRARIES_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Libraries") putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Libraries")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.icon_library_folder).toString()) putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.icon_library_folder).toString())
}.build() }.build()
if (itemsInProgress.isNotEmpty()) { if (itemsInProgress.isNotEmpty()) {
@@ -53,13 +47,28 @@ class BrowseTree(
} }
if (libraries.isNotEmpty()) { if (libraries.isNotEmpty()) {
if (recentsLoaded) {
rootList += recentMetadata
}
rootList += librariesMetadata rootList += librariesMetadata
libraries.forEach { library -> libraries.forEach { library ->
val libraryMediaMetadata = library.getMediaMetadata() // Skip libraries without audio content
if (library.stats?.numAudioFiles == 0) return@forEach
Log.d("BrowseTree", "Library $library | ${library.icon}")
// Generate library list items for Libraries menu
val libraryMediaMetadata = library.getMediaMetadata(context)
val children = mediaIdToChildren[LIBRARIES_ROOT] ?: mutableListOf() val children = mediaIdToChildren[LIBRARIES_ROOT] ?: mutableListOf()
children += libraryMediaMetadata children += libraryMediaMetadata
mediaIdToChildren[LIBRARIES_ROOT] = children mediaIdToChildren[LIBRARIES_ROOT] = children
if (recentsLoaded) {
// Generate library list items for Recent menu
val recentlyMediaMetadata = library.getMediaMetadata(context,"recently")
val childrenRecently = mediaIdToChildren[RECENTLY_ROOT] ?: mutableListOf()
childrenRecently += recentlyMediaMetadata
mediaIdToChildren[RECENTLY_ROOT] = childrenRecently
}
} }
} }
@@ -75,3 +84,4 @@ const val AUTO_BROWSE_ROOT = "/"
const val CONTINUE_ROOT = "__CONTINUE__" const val CONTINUE_ROOT = "__CONTINUE__"
const val DOWNLOADS_ROOT = "__DOWNLOADS__" const val DOWNLOADS_ROOT = "__DOWNLOADS__"
const val LIBRARIES_ROOT = "__LIBRARIES__" const val LIBRARIES_ROOT = "__LIBRARIES__"
const val RECENTLY_ROOT = "__RECENTLY__"
@@ -35,6 +35,8 @@ import com.audiobookshelf.app.media.MediaManager
import com.audiobookshelf.app.media.MediaProgressSyncer import com.audiobookshelf.app.media.MediaProgressSyncer
import com.audiobookshelf.app.server.ApiHandler import com.audiobookshelf.app.server.ApiHandler
import com.audiobookshelf.app.BuildConfig import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.media.getUriToAbsIconDrawable
import com.audiobookshelf.app.media.getUriToDrawable
import com.google.android.exoplayer2.* import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
@@ -47,6 +49,7 @@ import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.ui.PlayerNotificationManager import com.google.android.exoplayer2.ui.PlayerNotificationManager
import com.google.android.exoplayer2.upstream.* import com.google.android.exoplayer2.upstream.*
import kotlinx.coroutines.runBlocking
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
@@ -119,6 +122,18 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private var mShakeDetector: ShakeDetector? = null private var mShakeDetector: ShakeDetector? = null
private var shakeSensorUnregisterTask:TimerTask? = null private var shakeSensorUnregisterTask:TimerTask? = null
// These are used to trigger reloading if
private var forceReloadingAndroidAuto:Boolean = false
private var firstLoadDone:Boolean = false
fun isBrowseTreeInitialized() : Boolean {
return this::browseTree.isInitialized
}
// Cache latest search so it wont trigger again when returning from series for example
private var cachedSearch : String = ""
private var cachedSearchResults : MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
/* /*
Service related stuff Service related stuff
*/ */
@@ -977,6 +992,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private val AUTO_MEDIA_ROOT = "/" private val AUTO_MEDIA_ROOT = "/"
private val LIBRARIES_ROOT = "__LIBRARIES__" private val LIBRARIES_ROOT = "__LIBRARIES__"
private val RECENTLY_ROOT = "__RECENTLY__"
private val DOWNLOADS_ROOT = "__DOWNLOADS__" private val DOWNLOADS_ROOT = "__DOWNLOADS__"
private val CONTINUE_ROOT = "__CONTINUE__" private val CONTINUE_ROOT = "__CONTINUE__"
private lateinit var browseTree:BrowseTree private lateinit var browseTree:BrowseTree
@@ -999,8 +1015,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
// No further calls will be made to other media browsing methods. // No further calls will be made to other media browsing methods.
null null
} else { } else {
Log.d(tag, "Android Auto starting")
isStarted = true isStarted = true
mediaManager.checkResetServerItems() // Reset any server items if no longer connected to server
// Reset cache if no longer connected to server or server changed
if (mediaManager.checkResetServerItems()) {
forceReloadingAndroidAuto = true
}
isAndroidAuto = true isAndroidAuto = true
@@ -1026,33 +1047,45 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
result.detach() result.detach()
// Prevent crashing if app is restarted while browsing
if ((parentMediaId != DOWNLOADS_ROOT && parentMediaId != AUTO_MEDIA_ROOT) && !firstLoadDone){
result.sendResult(null)
return
}
if (parentMediaId == DOWNLOADS_ROOT) { // Load downloads if (parentMediaId == DOWNLOADS_ROOT) { // Load downloads
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book") val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast") val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf() val localBrowseItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
localBooks.forEach { localLibraryItem -> localBooks.forEach { localLibraryItem ->
if (localLibraryItem.media.getAudioTracks().isNotEmpty()) { if (localLibraryItem.media.getAudioTracks().isNotEmpty()) {
val progress = DeviceManager.dbManager.getLocalMediaProgress(localLibraryItem.id) val progress = DeviceManager.dbManager.getLocalMediaProgress(localLibraryItem.id)
val description = localLibraryItem.getMediaDescription(progress, ctx) val description = localLibraryItem.getMediaDescription(progress, ctx)
localBrowseItems += MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) localBrowseItems += MediaBrowserCompat.MediaItem(
description,
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
)
} }
} }
localPodcasts.forEach { localLibraryItem -> localPodcasts.forEach { localLibraryItem ->
val mediaDescription = localLibraryItem.getMediaDescription(null, ctx) val mediaDescription = localLibraryItem.getMediaDescription(null, ctx)
localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) localBrowseItems += MediaBrowserCompat.MediaItem(
mediaDescription,
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
} }
result.sendResult(localBrowseItems) result.sendResult(localBrowseItems)
} else if (parentMediaId == CONTINUE_ROOT) { } else if (parentMediaId == CONTINUE_ROOT) {
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf() val localBrowseItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
mediaManager.serverItemsInProgress.forEach { itemInProgress -> mediaManager.serverItemsInProgress.forEach { itemInProgress ->
val progress: MediaProgressWrapper? val progress: MediaProgressWrapper?
val mediaDescription:MediaDescriptionCompat val mediaDescription: MediaDescriptionCompat
if (itemInProgress.episode != null) { if (itemInProgress.episode != null) {
if (itemInProgress.isLocal) { if (itemInProgress.isLocal) {
progress = DeviceManager.dbManager.getLocalMediaProgress("${itemInProgress.libraryItemWrapper.id}-${itemInProgress.episode.id}") progress = DeviceManager.dbManager.getLocalMediaProgress("${itemInProgress.libraryItemWrapper.id}-${itemInProgress.episode.id}")
@@ -1082,32 +1115,459 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
} }
result.sendResult(localBrowseItems) result.sendResult(localBrowseItems)
} else if (parentMediaId == LIBRARIES_ROOT || parentMediaId == AUTO_MEDIA_ROOT) { } else if (parentMediaId == AUTO_MEDIA_ROOT) {
mediaManager.loadAndroidAutoItems { Log.d(tag, "Trying to initialize browseTree.")
browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries) if (!this::browseTree.isInitialized || forceReloadingAndroidAuto) {
forceReloadingAndroidAuto = false
mediaManager.loadAndroidAutoItems {
Log.d(tag, "android auto loaded. Starting browseTree initialize")
browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries, mediaManager.allLibraryPersonalizationsDone)
val children = browseTree[parentMediaId]?.map { item ->
Log.d(tag, "Found top menu item: ${item.description.title}")
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
Log.d(tag, "browseTree initialize and android auto loaded")
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
firstLoadDone = true
if (mediaManager.serverLibraries.isNotEmpty()) {
Log.d(tag, "Starting personalization fetch")
mediaManager.populatePersonalizedDataForAllLibraries {
notifyChildrenChanged("/")
}
Log.d(tag, "Initialize inprogress items")
mediaManager.initializeInProgressItems {
notifyChildrenChanged("/")
}
}
}
} else {
Log.d(tag, "Starting browseTree refresh")
browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries, mediaManager.allLibraryPersonalizationsDone)
val children = browseTree[parentMediaId]?.map { item -> val children = browseTree[parentMediaId]?.map { item ->
Log.d(tag, "Loading Browser Media Item ${item.description.title}") Log.d(tag, "Found top menu item: ${item.description.title}")
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
} }
Log.d(tag, "browseTree initialize and android auto loaded")
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
} else if (parentMediaId == LIBRARIES_ROOT || parentMediaId == RECENTLY_ROOT) {
Log.d(tag, "First load done: $firstLoadDone")
if (!firstLoadDone) {
result.sendResult(null)
return
}
// Wait until top-menu is initialized
while (!this::browseTree.isInitialized) {}
val children = browseTree[parentMediaId]?.map { item ->
Log.d(tag, "[MENU: $parentMediaId] Showing list item ${item.description.title}")
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} else if (mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library
Log.d(tag, "Loading items for library $parentMediaId")
val selectedLibrary = mediaManager.getLibrary(parentMediaId)
if (selectedLibrary?.mediaType == "podcast") { // Podcasts are browseable
mediaManager.loadLibraryPodcasts(parentMediaId) { libraryItems ->
val children = libraryItems?.map { libraryItem ->
val mediaDescription = libraryItem.getMediaDescription(null, ctx)
MediaBrowserCompat.MediaItem(
mediaDescription,
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
} else {
val children = mutableListOf(
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle("Authors")
.setMediaId("__LIBRARY__${parentMediaId}__AUTHORS")
.setIconUri(getUriToAbsIconDrawable(ctx, "authors"))
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
),
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle("Series")
.setMediaId("__LIBRARY__${parentMediaId}__SERIES_LIST")
.setIconUri(getUriToAbsIconDrawable(ctx, "columns"))
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
),
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle("Collections")
.setMediaId("__LIBRARY__${parentMediaId}__COLLECTIONS")
.setIconUri(getUriToDrawable(ctx, R.drawable.md_book_multiple_outline))
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
)
if (mediaManager.getHasDiscovery(parentMediaId)) {
children.add(
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle("Discovery")
.setMediaId("__LIBRARY__${parentMediaId}__DISCOVERY")
.setIconUri(getUriToDrawable(ctx, R.drawable.md_telescope))
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?) result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} }
} else if (mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library } else if (parentMediaId.startsWith(RECENTLY_ROOT)) {
Log.d(tag, "Loading items for library $parentMediaId") Log.d(tag, "Browsing recently $parentMediaId")
mediaManager.loadLibraryItemsWithAudio(parentMediaId) { libraryItems -> val mediaIdParts = parentMediaId.split("__")
val children = libraryItems.map { libraryItem -> if (!mediaManager.getIsLibrary(mediaIdParts[2])) {
if (libraryItem.mediaType == "podcast") { // Podcasts are browseable Log.d(tag, "${mediaIdParts[2]} is not library")
val mediaDescription = libraryItem.getMediaDescription(null, ctx) result.sendResult(null)
MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) return
}
Log.d(tag, "Mediaparts: ${mediaIdParts.size} | $mediaIdParts")
if (mediaIdParts.size == 3) {
mediaManager.getLibraryRecentShelfs(mediaIdParts[2]) { availableShelfs ->
Log.d(tag, "Found ${availableShelfs.size} shelfs")
val children : MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
for (shelf in availableShelfs) {
if (shelf.type == "book") {
children.add(
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle("Books")
.setMediaId("${parentMediaId}__BOOK")
.setIconUri(getUriToDrawable(ctx, R.drawable.md_book_open_blank_variant_outline))
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
)
} else if (shelf.type == "series") {
children.add(
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle("Series")
.setMediaId("${parentMediaId}__SERIES")
.setIconUri(getUriToAbsIconDrawable(ctx, "columns"))
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
)
} else if (shelf.type == "episode") {
children.add(
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle("Episodes")
.setMediaId("${parentMediaId}__EPISODE")
.setIconUri(getUriToAbsIconDrawable(ctx, "microphone_2"))
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
)
} else if (shelf.type == "podcast") {
children.add(
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle("Podcast")
.setMediaId("${parentMediaId}__PODCAST")
.setIconUri(getUriToAbsIconDrawable(ctx, "podcast"))
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
)
} else if (shelf.type == "authors") {
children.add(
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle("Authors")
.setMediaId("${parentMediaId}__AUTHORS")
.setIconUri(getUriToAbsIconDrawable(ctx, "authors"))
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
)
}
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
} else if (mediaIdParts.size == 4) {
mediaManager.getLibraryRecentShelfByType(mediaIdParts[2], mediaIdParts[3]) { shelf ->
if (shelf === null) {
result.sendResult(mutableListOf())
}else {
if (shelf.type == "book") {
val children = (shelf as LibraryShelfBookEntity).entities?.map { libraryItem ->
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
libraryItem.localLibraryItemId = localLibraryItem?.id
val description = libraryItem.getMediaDescription(progress, ctx, null, false)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} else if (shelf.type == "episode") {
val episodesWithRecentEpisode = (shelf as LibraryShelfEpisodeEntity).entities?.filter { libraryItem -> libraryItem.recentEpisode !== null }
val children = episodesWithRecentEpisode?.map { libraryItem ->
val podcast = libraryItem.media as Podcast
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.libraryId && it.episodeId == libraryItem.recentEpisode?.id }
// to show download icon
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.recentEpisode!!.id)
localLibraryItem?.let { lli ->
val localEpisode = (lli.media as Podcast).episodes?.find { it.serverEpisodeId == libraryItem.recentEpisode.id }
libraryItem.recentEpisode.localEpisodeId = localEpisode?.id
}
val description = libraryItem.recentEpisode.getMediaDescription(libraryItem, progress, ctx)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} else if (shelf.type == "podcast") {
val children = (shelf as LibraryShelfPodcastEntity).entities?.map { libraryItem ->
val mediaDescription = libraryItem.getMediaDescription(null, ctx)
MediaBrowserCompat.MediaItem(
mediaDescription,
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} else if (shelf.type == "series") {
val children = (shelf as LibraryShelfSeriesEntity).entities?.map { librarySeriesItem ->
val description = librarySeriesItem.getMediaDescription(null, ctx)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} else if (shelf.type == "authors") {
val children = (shelf as LibraryShelfAuthorEntity).entities?.map { authorItem ->
val description = authorItem.getMediaDescription(null, ctx)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} else {
result.sendResult(mutableListOf())
}
}
}
}
} else if (parentMediaId.startsWith("__LIBRARY__")) {
Log.d(tag, "Browsing library $parentMediaId")
val mediaIdParts = parentMediaId.split("__")
/*
MediaIdParts for Library
1: LIBRARY
2: mediaId for library
3: Browsing style (AUTHORS, AUTHOR, AUTHOR_SERIES, SERIES_LIST, SERIES, COLLECTION, COLLECTIONS, DISCOVERY)
4:
- Paging: SERIES_LIST, AUTHORS
- SeriesId: SERIES
- AuthorId: AUTHOR, AUTHOR_SERIES
- CollectionId: COLLECTIONS
5: SeriesId: AUTHOR_SERIES
*/
if (!mediaManager.getIsLibrary(mediaIdParts[2])) {
Log.d(tag, "${mediaIdParts[2]} is not library")
result.sendResult(null)
return
}
Log.d(tag, "$mediaIdParts")
if (mediaIdParts[3] == "SERIES_LIST" && mediaIdParts.size == 5) {
Log.d(tag, "Loading series from library ${mediaIdParts[2]} with paging ${mediaIdParts[4]}")
mediaManager.loadLibrarySeriesWithAudio(mediaIdParts[2], mediaIdParts[4]) { seriesItems ->
Log.d(tag, "Received ${seriesItems.size} series")
val seriesLetters = seriesItems.groupingBy { iwb -> iwb.title.substring(0, mediaIdParts[4].length + 1).uppercase() }.eachCount()
if (seriesItems.size > DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseLimitForGrouping && seriesItems.size > 1 && seriesLetters.size > 1) {
val children = seriesLetters.map { (seriesLetter, seriesCount) ->
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle(seriesLetter)
.setMediaId("${parentMediaId}${seriesLetter.last()}")
.setSubtitle("$seriesCount series")
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} else { } else {
val children = seriesItems.map { seriesItem ->
val description = seriesItem.getMediaDescription(null, ctx)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
}
} else if (mediaIdParts[3] == "SERIES_LIST") {
Log.d(tag, "Loading series from library ${mediaIdParts[2]}")
mediaManager.loadLibrarySeriesWithAudio(mediaIdParts[2]) { seriesItems ->
Log.d(tag, "Received ${seriesItems.size} series")
if (seriesItems.size > DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseLimitForGrouping && seriesItems.size > 1) {
val seriesLetters = seriesItems.groupingBy { iwb -> iwb.title.first().uppercaseChar() }.eachCount()
val children = seriesLetters.map { (seriesLetter, seriesCount) ->
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle(seriesLetter.toString())
.setSubtitle("$seriesCount series")
.setMediaId("${parentMediaId}__${seriesLetter}")
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} else {
val children = seriesItems.map { seriesItem ->
val description = seriesItem.getMediaDescription(null, ctx)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
}
} else if (mediaIdParts[3] == "SERIES") {
Log.d(tag, "Loading items for serie ${mediaIdParts[4]} from library ${mediaIdParts[2]}")
mediaManager.loadLibrarySeriesItemsWithAudio(
mediaIdParts[2],
mediaIdParts[4]
) { libraryItems ->
Log.d(tag, "Received ${libraryItems.size} library items")
var items = libraryItems
if (DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseSeriesSequenceOrder === AndroidAutoBrowseSeriesSequenceOrderSetting.DESC) {
items = libraryItems.reversed()
}
val children = items.map { libraryItem ->
val progress =
mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
libraryItem.localLibraryItemId = localLibraryItem?.id
val description = libraryItem.getMediaDescription(progress, ctx, null, true)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
} else if (mediaIdParts[3] == "AUTHORS" && mediaIdParts.size == 5) {
Log.d(tag, "Loading authors from library ${mediaIdParts[2]} with paging ${mediaIdParts[4]}")
mediaManager.loadAuthorsWithBooks(mediaIdParts[2], mediaIdParts[4]) { authorItems ->
Log.d(tag, "Received ${authorItems.size} authors")
val authorLetters = authorItems.groupingBy { iwb -> iwb.name.substring(0, mediaIdParts[4].length + 1).uppercase() }.eachCount()
if (authorItems.size > DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseLimitForGrouping && authorItems.size > 1 && authorLetters.size > 1) {
val children = authorLetters.map { (authorLetter, authorCount) ->
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle(authorLetter)
.setMediaId("${parentMediaId}${authorLetter.last()}")
.setSubtitle("$authorCount authors")
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} else {
val children = authorItems.map { authorItem ->
val description = authorItem.getMediaDescription(null, ctx)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
}
} else if (mediaIdParts[3] == "AUTHORS") {
Log.d(tag, "Loading authors from library ${mediaIdParts[2]}")
mediaManager.loadAuthorsWithBooks(mediaIdParts[2]) { authorItems ->
Log.d(tag, "Received ${authorItems.size} authors")
if (authorItems.size > DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseLimitForGrouping && authorItems.size > 1) {
val authorLetters = authorItems.groupingBy { iwb -> iwb.name.first().uppercaseChar() }.eachCount()
val children = authorLetters.map { (authorLetter, authorCount) ->
MediaBrowserCompat.MediaItem(
MediaDescriptionCompat.Builder()
.setTitle(authorLetter.toString())
.setSubtitle("$authorCount authors")
.setMediaId("${parentMediaId}__${authorLetter}")
.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} else {
val children = authorItems.map { authorItem ->
val description = authorItem.getMediaDescription(null, ctx)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
}
} else if (mediaIdParts[3] == "AUTHOR") {
mediaManager.loadAuthorBooksWithAudio(mediaIdParts[2], mediaIdParts[4]) { libraryItems ->
val children = libraryItems.map { libraryItem ->
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
libraryItem.localLibraryItemId = localLibraryItem?.id
if (libraryItem.collapsedSeries != null) {
val description = libraryItem.getMediaDescription(progress, ctx, mediaIdParts[4])
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
} else {
val description = libraryItem.getMediaDescription(progress, ctx)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
} else if (mediaIdParts[3] == "AUTHOR_SERIES") {
mediaManager.loadAuthorSeriesBooksWithAudio(mediaIdParts[2], mediaIdParts[4], mediaIdParts[5]) { libraryItems ->
var items = libraryItems
if (DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseSeriesSequenceOrder === AndroidAutoBrowseSeriesSequenceOrderSetting.DESC) {
items = libraryItems.reversed()
}
val children = items.map { libraryItem ->
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
libraryItem.localLibraryItemId = localLibraryItem?.id
val description = libraryItem.getMediaDescription(progress, ctx, null, true)
if (libraryItem.collapsedSeries != null) {
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
} else {
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
} else if (mediaIdParts[3] == "COLLECTIONS") {
Log.d(tag, "Loading collections from library ${mediaIdParts[2]}")
mediaManager.loadLibraryCollectionsWithAudio(mediaIdParts[2]) { collectionItems ->
Log.d(tag, "Received ${collectionItems.size} collections")
val children = collectionItems.map { collectionItem ->
val description = collectionItem.getMediaDescription(null, ctx)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
} else if (mediaIdParts[3] == "COLLECTION") {
Log.d(tag, "Loading collection ${mediaIdParts[4]} books from library ${mediaIdParts[2]}")
mediaManager.loadLibraryCollectionBooksWithAudio(mediaIdParts[2], mediaIdParts[4]) { libraryItems ->
Log.d(tag, "Received ${libraryItems.size} collections")
val children = libraryItems.map { libraryItem ->
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id } val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id) val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
libraryItem.localLibraryItemId = localLibraryItem?.id libraryItem.localLibraryItemId = localLibraryItem?.id
val description = libraryItem.getMediaDescription(progress, ctx) val description = libraryItem.getMediaDescription(progress, ctx)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
} }
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} }
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?) } else if (mediaIdParts[3] == "DISCOVERY") {
Log.d(tag, "Loading discovery from library ${mediaIdParts[2]}")
mediaManager.loadLibraryDiscoveryBooksWithAudio(mediaIdParts[2]) { libraryItems ->
Log.d(tag, "Received ${libraryItems.size} libraryItems for discovery")
val children = libraryItems.map { libraryItem ->
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
libraryItem.localLibraryItemId = localLibraryItem?.id
val description = libraryItem.getMediaDescription(progress, ctx)
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
} else {
result.sendResult(null)
} }
} else { } else {
Log.d(tag, "Loading podcast episodes for podcast $parentMediaId") Log.d(tag, "Loading podcast episodes for podcast $parentMediaId")
@@ -1119,13 +1579,35 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) { override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
result.detach() result.detach()
mediaManager.loadAndroidAutoItems { if (cachedSearch != query) {
browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries) Log.d(tag, "Search bundle: $extras")
val children = browseTree[LIBRARIES_ROOT]?.map { item -> var foundBooks: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) var foundPodcasts: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
var foundSeries: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
var foundAuthors: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
mediaManager.serverLibraries.forEach { serverLibrary ->
runBlocking {
// Skip searching library if it doesn't have any audio files
if (serverLibrary.stats?.numAudioFiles == 0) return@runBlocking
val searchResult = mediaManager.doSearch(serverLibrary.id, query)
for (resultData in searchResult.entries.iterator()) {
when (resultData.key) {
"book" -> foundBooks.addAll(resultData.value)
"series" -> foundSeries.addAll(resultData.value)
"authors" -> foundAuthors.addAll(resultData.value)
"podcast" -> foundPodcasts.addAll(resultData.value)
}
}
}
} }
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?) foundBooks.addAll(foundSeries)
foundBooks.addAll(foundAuthors)
cachedSearchResults = foundBooks
} }
result.sendResult(cachedSearchResults)
cachedSearch = query
Log.d(tag, "onSearch: Done")
} }
// //
@@ -1188,6 +1670,14 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
hasNetworkConnectivity = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) hasNetworkConnectivity = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
Log.i(tag, "Network capabilities changed. hasNetworkConnectivity=$hasNetworkConnectivity | isUnmeteredNetwork=$isUnmeteredNetwork") Log.i(tag, "Network capabilities changed. hasNetworkConnectivity=$hasNetworkConnectivity | isUnmeteredNetwork=$isUnmeteredNetwork")
clientEventEmitter?.onNetworkMeteredChanged(isUnmeteredNetwork) clientEventEmitter?.onNetworkMeteredChanged(isUnmeteredNetwork)
if (hasNetworkConnectivity) {
// Force android auto loading if libraries are empty.
// Lack of network connectivity is most likely reason for libraries being empty
if (isBrowseTreeInitialized() && firstLoadDone && mediaManager.serverLibraries.isEmpty()) {
forceReloadingAndroidAuto = true
notifyChildrenChanged("/")
}
}
} }
} }
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.provider.Settings import android.provider.Settings
import android.util.Base64
import android.util.Log import android.util.Log
import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
@@ -154,7 +155,7 @@ class ApiHandler(var ctx:Context) {
fun getLibraries(cb: (List<Library>) -> Unit) { fun getLibraries(cb: (List<Library>) -> Unit) {
val mapper = jacksonMapper val mapper = jacksonMapper
getRequest("/api/libraries", null,null) { getRequest("/api/libraries?include=stats", null,null) {
val libraries = mutableListOf<Library>() val libraries = mutableListOf<Library>()
var array = JSONArray() var array = JSONArray()
@@ -171,6 +172,23 @@ class ApiHandler(var ctx:Context) {
} }
} }
fun getLibraryPersonalized(libraryItemId:String, cb: (List<LibraryShelfType>?) -> Unit) {
getRequest("/api/libraries/$libraryItemId/personalized", null, null) {
if (it.has("error")) {
Log.e(tag, it.getString("error") ?: "getLibraryStats Failed")
cb(null)
} else {
val items = mutableListOf<LibraryShelfType>()
val array = it.getJSONArray("value")
for (i in 0 until array.length()) {
val item = jacksonMapper.readValue<LibraryShelfType>(array.get(i).toString())
items.add(item)
}
cb(items)
}
}
}
fun getLibraryItem(libraryItemId:String, cb: (LibraryItem?) -> Unit) { fun getLibraryItem(libraryItemId:String, cb: (LibraryItem?) -> Unit) {
getRequest("/api/items/$libraryItemId?expanded=1", null, null) { getRequest("/api/items/$libraryItemId?expanded=1", null, null) {
if (it.has("error")) { if (it.has("error")) {
@@ -211,6 +229,103 @@ class ApiHandler(var ctx:Context) {
} }
} }
fun getLibrarySeries(libraryId:String, cb: (List<LibrarySeriesItem>) -> Unit) {
Log.d(tag, "Getting series")
getRequest("/api/libraries/$libraryId/series?minified=1&sort=name&limit=10000", null, null) {
val items = mutableListOf<LibrarySeriesItem>()
if (it.has("results")) {
val array = it.getJSONArray("results")
for (i in 0 until array.length()) {
val item = jacksonMapper.readValue<LibrarySeriesItem>(array.get(i).toString())
items.add(item)
}
}
cb(items)
}
}
fun getLibrarySeriesItems(libraryId:String, seriesId:String, cb: (List<LibraryItem>) -> Unit) {
Log.d(tag, "Getting items for series")
val seriesIdBase64 = Base64.encodeToString(seriesId.toByteArray(), Base64.DEFAULT)
getRequest("/api/libraries/$libraryId/items?minified=1&sort=media.metadata.title&filter=series.${seriesIdBase64}&limit=1000", null, null) {
val items = mutableListOf<LibraryItem>()
if (it.has("results")) {
val array = it.getJSONArray("results")
for (i in 0 until array.length()) {
val item = jacksonMapper.readValue<LibraryItem>(array.get(i).toString())
items.add(item)
}
}
cb(items)
}
}
fun getLibraryAuthors(libraryId:String, cb: (List<LibraryAuthorItem>) -> Unit) {
Log.d(tag, "Getting series")
getRequest("/api/libraries/$libraryId/authors", null, null) {
val items = mutableListOf<LibraryAuthorItem>()
if (it.has("authors")) {
val array = it.getJSONArray("authors")
for (i in 0 until array.length()) {
val item = jacksonMapper.readValue<LibraryAuthorItem>(array.get(i).toString())
items.add(item)
}
}else{
Log.e(tag, "No results")
}
cb(items)
}
}
fun getLibraryItemsFromAuthor(libraryId:String, authorId:String, cb: (List<LibraryItem>) -> Unit) {
Log.d(tag, "Getting author items")
val authorIdBase64 = Base64.encodeToString(authorId.toByteArray(), Base64.DEFAULT)
getRequest("/api/libraries/$libraryId/items?limit=1000&minified=1&filter=authors.${authorIdBase64}&sort=media.metadata.title&collapseseries=1", null, null) {
val items = mutableListOf<LibraryItem>()
if (it.has("results")) {
val array = it.getJSONArray("results")
for (i in 0 until array.length()) {
val item = jacksonMapper.readValue<LibraryItem>(array.get(i).toString())
if (item.collapsedSeries != null) {
item.collapsedSeries?.libraryId = libraryId
}
items.add(item)
}
}else{
Log.e(tag, "No results")
}
cb(items)
}
}
fun getLibraryCollections(libraryId:String, cb: (List<LibraryCollection>) -> Unit) {
Log.d(tag, "Getting collections")
getRequest("/api/libraries/$libraryId/collections?minified=1&sort=name&limit=1000", null, null) {
val items = mutableListOf<LibraryCollection>()
if (it.has("results")) {
val array = it.getJSONArray("results")
for (i in 0 until array.length()) {
val item = jacksonMapper.readValue<LibraryCollection>(array.get(i).toString())
items.add(item)
}
}
cb(items)
}
}
fun getSearchResults(libraryId:String, queryString:String, cb: (LibraryItemSearchResultType?) -> Unit) {
Log.d(tag, "Doing search for library $libraryId")
getRequest("/api/libraries/$libraryId/search?q=$queryString", null, null) {
if (it.has("error")) {
Log.e(tag, it.getString("error") ?: "getSearchResults Failed")
cb(null)
} else {
val librarySearchResults = jacksonMapper.readValue<LibraryItemSearchResultType>(it.toString())
cb(librarySearchResults)
}
}
}
fun getAllItemsInProgress(cb: (List<ItemInProgress>) -> Unit) { fun getAllItemsInProgress(cb: (List<ItemInProgress>) -> Unit) {
getRequest("/api/me/items-in-progress", null, null) { getRequest("/api/me/items-in-progress", null, null) {
val items = mutableListOf<ItemInProgress>() val items = mutableListOf<ItemInProgress>()
@@ -0,0 +1 @@
<!-- drawable/clock_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z" /></vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="27dp"
android:height="32dp"
android:viewportWidth="27"
android:viewportHeight="32">
<path
android:pathData="M26.719,15.761c-0.165,-0.138 -0.422,-0.341 -0.77,-0.581v-2.703c0,-6.891 -5.586,-12.477 -12.478,-12.477v0c-6.891,0 -12.477,5.586 -12.477,12.477v2.703c-0.348,0.241 -0.605,0.443 -0.77,0.581 -0.137,0.114 -0.223,0.285 -0.223,0.476 0,0 0,0 0,0.001v-0,3.238c0,0 0,0.001 0,0.001 0,0.191 0.086,0.361 0.222,0.475l0.001,0.001c0.385,0.321 1.269,0.993 2.645,1.683v0.315c0,0.849 0.548,1.537 1.223,1.537v0c0.675,0 1.223,-0.688 1.223,-1.537v-7.767c0,-0.849 -0.548,-1.537 -1.223,-1.537v0c-0.647,0 -1.177,0.632 -1.22,1.431l-0.003,0.002v-1.601c0,-5.856 4.747,-10.602 10.603,-10.602v0c5.856,0 10.603,4.747 10.603,10.602v1.601l-0.003,-0.002c-0.043,-0.799 -0.573,-1.431 -1.22,-1.431v0c-0.675,0 -1.223,0.688 -1.223,1.537v7.766c0,0.849 0.548,1.537 1.223,1.537v0c0.676,0 1.223,-0.688 1.223,-1.537v-0.315c1.376,-0.69 2.26,-1.362 2.645,-1.683 0.137,-0.114 0.223,-0.285 0.223,-0.476 0,-0 0,-0.001 0,-0.001v0,-3.237c0,-0 0,-0 0,-0 0,-0.191 -0.086,-0.361 -0.222,-0.475l-0.001,-0.001zM9.12,29.262c0.816,0 1.477,-0.661 1.477,-1.477v0,-16.543c0,-0 0,-0 0,-0 0,-0.816 -0.661,-1.477 -1.477,-1.477h-1.526c-0.816,0 -1.478,0.662 -1.478,1.478v0,16.543c0,0.816 0.661,1.477 1.477,1.477 0,0 0,0 0,0v0zM6.673,13.731h3.368v0.352h-3.368zM14.234,29.262c0.816,0 1.477,-0.661 1.477,-1.477v0,-16.543c0,-0 0,-0 0,-0 0,-0.816 -0.661,-1.477 -1.477,-1.477h-1.526c-0.816,0 -1.478,0.662 -1.478,1.478v0,16.543c0,0.816 0.661,1.477 1.477,1.477 0,0 0,0 0,0v0zM11.787,13.731h3.367v0.352h-3.367zM19.348,29.262c0.816,0 1.477,-0.661 1.477,-1.477v0,-16.543c0,-0 0,-0 0,-0 0,-0.816 -0.661,-1.477 -1.477,-1.477h-1.526c-0.816,0 -1.478,0.662 -1.478,1.478v0,16.543c0,0.816 0.661,1.477 1.477,1.477 0,0 0,0 0,0v0zM16.901,13.731h3.367v0.352h-3.367zM3.566,29.773h19.81c0.615,0 1.113,0.498 1.113,1.113v0c0,0.615 -0.498,1.113 -1.113,1.113h-19.81c-0.615,0 -1.113,-0.498 -1.113,-1.113v-0c0,-0.615 0.498,-1.113 1.113,-1.113z"
android:fillColor="#fff"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M16,7.333c2.577,0 4.667,2.089 4.667,4.667v0c0,2.577 -2.089,4.667 -4.667,4.667v0c-2.577,0 -4.667,-2.089 -4.667,-4.667v0c0,-2.577 2.089,-4.667 4.667,-4.667v0zM6.667,10.667c0.747,0 1.44,0.2 2.04,0.56 -0.2,1.907 0.36,3.8 1.507,5.28 -0.667,1.28 -2,2.16 -3.547,2.16 -2.209,0 -4,-1.791 -4,-4v0c0,-2.209 1.791,-4 4,-4v0zM25.333,10.667c2.209,0 4,1.791 4,4v0c0,2.209 -1.791,4 -4,4v0c-1.547,0 -2.88,-0.88 -3.547,-2.16 1.147,-1.48 1.707,-3.373 1.507,-5.28 0.6,-0.36 1.293,-0.56 2.04,-0.56zM7.333,24.333c0,-2.76 3.88,-5 8.667,-5s8.667,2.24 8.667,5v2.333h-17.333v-2.333zM0,26.667v-2c0,-1.853 2.52,-3.413 5.933,-3.867 -0.787,0.907 -1.267,2.16 -1.267,3.533v2.333h-4.667zM32,26.667h-4.667v-2.333c0,-1.373 -0.48,-2.627 -1.267,-3.533 3.413,0.453 5.933,2.013 5.933,3.867v2z"
android:fillColor="#fff"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M28,4v26h-21c-1.657,0 -3,-1.343 -3,-3s1.343,-3 3,-3h19v-24h-20c-2.2,0 -4,1.8 -4,4v24c0,2.2 1.8,4 4,4h24v-28h-2zM7.002,26v0c-0.001,0 -0.001,0 -0.002,0 -0.552,0 -1,0.448 -1,1s0.448,1 1,1c0.001,0 0.001,-0 0.002,-0v0h18.997v-2h-18.997z"
android:fillColor="#fff"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M12,4v20h4v-20h-4zM16,6.667l5.333,17.333 4,-1.333 -5.333,-17.333 -4,1.333zM6.667,6.667v17.333h4v-17.333h-4zM4,25.333v2.667h24v-2.667h-24z"
android:fillColor="#fff"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="32dp"
android:viewportWidth="36"
android:viewportHeight="32">
<path
android:pathData="M7,4h-6c-0.55,0 -1,0.45 -1,1v22c0,0.55 0.45,1 1,1h6c0.55,0 1,-0.45 1,-1v-22c0,-0.55 -0.45,-1 -1,-1zM6,10h-4v-2h4v2zM17,4h-6c-0.55,0 -1,0.45 -1,1v22c0,0.55 0.45,1 1,1h6c0.55,0 1,-0.45 1,-1v-22c0,-0.55 -0.45,-1 -1,-1zM16,10h-4v-2h4v2zM23.909,5.546l-5.358,2.7c-0.491,0.247 -0.691,0.852 -0.443,1.343l8.999,17.861c0.247,0.491 0.852,0.691 1.343,0.443l5.358,-2.7c0.491,-0.247 0.691,-0.852 0.443,-1.343l-8.999,-17.861c-0.247,-0.491 -0.852,-0.691 -1.343,-0.443z"
android:fillColor="#fff"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M19.2,6h-6.4v19.2h6.4v-19.2zM22.4,6v19.2h6.4v-19.2h-6.4zM9.6,6h-6.4v19.2h6.4v-19.2zM0,2.8h32v25.6h-32v-25.6z"
android:fillColor="#fff"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="27dp"
android:height="32dp"
android:viewportWidth="27"
android:viewportHeight="32">
<path
android:pathData="M13.68,0c7.518,0 13.612,2.854 13.612,6.37 0,3.518 -6.096,6.37 -13.612,6.37s-13.612,-2.854 -13.612,-6.37c0,-3.516 6.096,-6.37 13.612,-6.37v0zM0.068,21.31v4.891c2.422,8.602 26.349,6.94 27.227,-0.44v-4.885c-1.195,8.102 -25.313,8.685 -27.227,0.435v0,0zM0,8.578v4.776c2.422,8.401 26.482,7.266 27.362,0.06v-4.773c-1.198,7.914 -25.448,7.995 -27.362,-0.063v0zM0,14.75v4.891c2.422,8.602 26.482,7.44 27.362,0.06v-4.885c-1.198,8.102 -25.448,8.185 -27.362,-0.065v0z"
android:fillColor="#fff"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M26,28h-20v-4l6,-10 8.219,10 5.781,-4v8zM26,15c0,1.657 -1.343,3 -3,3s-3,-1.343 -3,-3 1.343,-3 3,-3c1.657,0 3,1.343 3,3zM28.681,7.159c-0.694,-0.947 -1.662,-2.053 -2.724,-3.116s-2.169,-2.03 -3.116,-2.724c-1.612,-1.182 -2.393,-1.319 -2.841,-1.319h-15.5c-1.378,0 -2.5,1.121 -2.5,2.5v27c0,1.378 1.122,2.5 2.5,2.5h23c1.378,0 2.5,-1.122 2.5,-2.5v-19.5c0,-0.448 -0.137,-1.23 -1.319,-2.841zM24.543,5.457c0.959,0.959 1.712,1.825 2.268,2.543h-4.811v-4.811c0.718,0.556 1.584,1.309 2.543,2.268zM28,29.5c0,0.271 -0.229,0.5 -0.5,0.5h-23c-0.271,0 -0.5,-0.229 -0.5,-0.5v-27c0,-0.271 0.229,-0.5 0.5,-0.5 0,0 15.499,-0 15.5,0v7c0,0.552 0.448,1 1,1h7v19.5z"
android:fillColor="#fff"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M9,18h-2v14h2c0.55,0 1,-0.45 1,-1v-12c0,-0.55 -0.45,-1 -1,-1zM23,18c-0.55,0 -1,0.45 -1,1v12c0,0.55 0.45,1 1,1h2v-14h-2zM32,16c0,-8.837 -7.163,-16 -16,-16s-16,7.163 -16,16c0,1.919 0.338,3.759 0.958,5.464 -0.609,1.038 -0.958,2.246 -0.958,3.536 0,3.526 2.608,6.443 6,6.929v-13.857c-0.997,0.143 -1.927,0.495 -2.742,1.012 -0.168,-0.835 -0.258,-1.699 -0.258,-2.584 0,-7.18 5.82,-13 13,-13s13,5.82 13,13c0,0.885 -0.088,1.749 -0.257,2.584 -0.816,-0.517 -1.745,-0.87 -2.743,-1.013v13.858c3.392,-0.485 6,-3.402 6,-6.929 0,-1.29 -0.349,-2.498 -0.958,-3.536 0.62,-1.705 0.958,-3.545 0.958,-5.465z"
android:fillColor="#fff"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M23.6,2c-3.363,0 -6.258,2.736 -7.599,5.594 -1.342,-2.858 -4.237,-5.594 -7.601,-5.594 -4.637,0 -8.4,3.764 -8.4,8.401 0,9.433 9.516,11.906 16.001,21.232 6.13,-9.268 15.999,-12.1 15.999,-21.232 0,-4.637 -3.763,-8.401 -8.4,-8.401z"
android:fillColor="#fff"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="32dp"
android:viewportWidth="24"
android:viewportHeight="32">
<path
android:pathData="M6,6v10c0,3.313 2.688,6 6,6s6,-2.688 6,-6h-5c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5v-2h-5c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5v-2h-5c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5c0,-3.313 -2.688,-6 -6,-6s-6,2.688 -6,6zM20,15v1c0,4.419 -3.581,8 -8,8s-8,-3.581 -8,-8v-2.5c0,-0.831 -0.669,-1.5 -1.5,-1.5s-1.5,0.669 -1.5,1.5v2.5c0,5.569 4.138,10.169 9.5,10.9v2.1h-3c-0.831,0 -1.5,0.669 -1.5,1.5s0.669,1.5 1.5,1.5h9c0.831,0 1.5,-0.669 1.5,-1.5s-0.669,-1.5 -1.5,-1.5h-3v-2.1c5.363,-0.731 9.5,-5.331 9.5,-10.9v-2.5c0,-0.831 -0.669,-1.5 -1.5,-1.5s-1.5,0.669 -1.5,1.5v1.5z"
android:fillColor="#fff"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="32dp"
android:viewportWidth="22"
android:viewportHeight="32">
<path
android:pathData="M22.292,14.872c0,-0.836 -0.677,-1.51 -1.51,-1.51 -0.836,0 -1.51,0.677 -1.51,1.51 0,2.977 -0.714,5.435 -2.159,7.076 -1.31,1.484 -3.289,2.341 -5.964,2.341s-4.654,-0.854 -5.964,-2.339c-1.448,-1.641 -2.159,-4.099 -2.159,-7.078 0,-0.836 -0.677,-1.51 -1.51,-1.51 -0.836,0 -1.51,0.677 -1.51,1.51 0,3.711 0.961,6.857 2.914,9.073 1.438,1.63 3.375,2.734 5.815,3.164v2.479h-3.703c-0.661,0 -1.203,0.542 -1.203,1.203v1.206h14.646v-1.206c0,-0.661 -0.542,-1.203 -1.203,-1.203h-3.711v-2.479c2.44,-0.432 4.375,-1.534 5.815,-3.167 1.953,-2.214 2.917,-5.359 2.917,-9.07v0,0zM11.146,0c3.083,0 5.604,2.523 5.604,5.604v0.146h-3.013v3.57h3.016v1.818h-3.016v3.57h3.016v1.284c0,3.083 -2.523,5.604 -5.604,5.604 -3.083,0 -5.604,-2.523 -5.604,-5.604v-1.284h3.016v-3.57h-3.018v-1.818h3.016v-3.57h-3.016v-0.146c0,-3.081 2.521,-5.604 5.604,-5.604v0z"
android:fillColor="#fff"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M15,22c2.761,0 5,-2.239 5,-5v-12c0,-2.761 -2.239,-5 -5,-5s-5,2.239 -5,5v12c0,2.761 2.239,5 5,5zM22,14v3c0,3.866 -3.134,7 -7,7s-7,-3.134 -7,-7v-3h-2v3c0,4.632 3.5,8.447 8,8.944v4.056h-4v2h10v-2h-4v-4.056c4.5,-0.497 8,-4.312 8,-8.944v-3h-2z"
android:fillColor="#fff"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M30,0h2v23c0,2.761 -3.134,5 -7,5s-7,-2.239 -7,-5c0,-2.761 3.134,-5 7,-5 1.959,0 3.729,0.575 5,1.501v-11.501l-16,3.556v15.444c0,2.761 -3.134,5 -7,5s-7,-2.239 -7,-5c0,-2.761 3.134,-5 7,-5 1.959,0 3.729,0.575 5,1.501v-19.501l18,-4z"
android:fillColor="#fff"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="33dp"
android:height="32dp"
android:viewportWidth="33"
android:viewportHeight="32">
<path
android:pathData="M18.289,18.549v13.45h-3.973v-13.45c-1.305,-0.706 -2.191,-2.087 -2.191,-3.676 0,-2.307 1.871,-4.178 4.178,-4.178s4.178,1.871 4.178,4.178c0,1.589 -0.887,2.97 -2.192,3.676v0zM10.434,2.963c0.774,-0.382 1.091,-1.319 0.709,-2.091s-1.318,-1.091 -2.091,-0.71c-3.158,1.558 -5.613,4.134 -7.165,7.169 -1.146,2.241 -1.8,4.734 -1.879,7.251 -0.079,2.538 0.421,5.101 1.585,7.46 1.433,2.906 3.863,5.491 7.44,7.322 0.767,0.392 1.706,0.088 2.098,-0.679s0.088,-1.706 -0.679,-2.097c-2.929,-1.498 -4.905,-3.589 -6.059,-5.926 -0.93,-1.887 -1.33,-3.942 -1.267,-5.981 0.064,-2.058 0.599,-4.098 1.536,-5.93 1.256,-2.455 3.234,-4.536 5.771,-5.786v0zM23.554,0.161c-0.773,-0.382 -1.71,-0.064 -2.091,0.71s-0.064,1.71 0.71,2.091c2.537,1.251 4.515,3.332 5.771,5.787 0.937,1.832 1.472,3.871 1.536,5.93 0.064,2.038 -0.336,4.094 -1.267,5.981 -1.153,2.337 -3.13,4.428 -6.059,5.926 -0.767,0.392 -1.07,1.331 -0.679,2.097s1.331,1.071 2.098,0.679c3.577,-1.83 6.007,-4.415 7.44,-7.322 1.164,-2.359 1.664,-4.922 1.585,-7.46 -0.079,-2.516 -0.732,-5.01 -1.878,-7.251 -1.552,-3.034 -4.008,-5.611 -7.165,-7.169v0zM21.968,7.033c-0.582,-0.42 -1.395,-0.287 -1.814,0.295s-0.287,1.395 0.295,1.814c0.235,0.169 0.465,0.359 0.691,0.566 1.319,1.216 2.101,2.838 2.266,4.553 0.166,1.731 -0.295,3.566 -1.464,5.188 -0.198,0.276 -0.427,0.555 -0.686,0.836 -0.487,0.529 -0.453,1.353 0.076,1.839s1.353,0.452 1.84,-0.076c0.313,-0.339 0.607,-0.701 0.88,-1.081 1.555,-2.16 2.167,-4.617 1.943,-6.951 -0.225,-2.349 -1.293,-4.566 -3.091,-6.224 -0.283,-0.261 -0.595,-0.514 -0.935,-0.76v0zM12.156,9.143c0.582,-0.42 0.715,-1.232 0.295,-1.814s-1.232,-0.715 -1.814,-0.296c-0.341,0.246 -0.652,0.499 -0.935,0.76 -1.799,1.658 -2.866,3.875 -3.091,6.224 -0.224,2.334 0.387,4.792 1.943,6.952 0.274,0.379 0.567,0.741 0.88,1.08 0.487,0.529 1.311,0.563 1.839,0.076s0.563,-1.311 0.076,-1.839c-0.259,-0.281 -0.487,-0.56 -0.686,-0.836 -1.169,-1.623 -1.63,-3.457 -1.464,-5.188 0.164,-1.715 0.947,-3.337 2.266,-4.553 0.226,-0.207 0.456,-0.397 0.691,-0.566v0z"
android:fillColor="#fff"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M30.925,2.938c0.794,-0.231 1.25,-1.069 1.019,-1.863s-1.069,-1.25 -1.869,-1.012l-26.844,7.869c-0.587,0.169 -1.119,0.456 -1.569,0.825 -1.006,0.725 -1.663,1.906 -1.663,3.244v16c0,2.206 1.794,4 4,4h24c2.206,0 4,-1.794 4,-4v-16c0,-2.206 -1.794,-4 -4,-4h-14.344l17.269,-5.063zM23,25c-2.762,0 -5,-2.238 -5,-5s2.238,-5 5,-5 5,2.238 5,5 -2.238,5 -5,5zM5,16c0,-0.55 0.45,-1 1,-1h6c0.55,0 1,0.45 1,1s-0.45,1 -1,1h-6c-0.55,0 -1,-0.45 -1,-1zM4,20c0,-0.55 0.45,-1 1,-1h8c0.55,0 1,0.45 1,1s-0.45,1 -1,1h-8c-0.55,0 -1,-0.45 -1,-1zM5,24c0,-0.55 0.45,-1 1,-1h6c0.55,0 1,0.45 1,1s-0.45,1 -1,1h-6c-0.55,0 -1,-0.45 -1,-1z"
android:fillColor="#fff"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M22,2l-10,10h-6l-6,8c0,0 6.357,-1.77 10.065,-0.94l-10.065,12.94 13.184,-10.255c1.839,4.208 -1.184,10.255 -1.184,10.255l8,-6v-6l10,-10 2,-10 -10,2z"
android:fillColor="#fff"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M4.259,23.467c-2.35,0 -4.259,1.917 -4.259,4.252 0,2.349 1.909,4.244 4.259,4.244 2.358,0 4.265,-1.895 4.265,-4.244 -0,-2.336 -1.907,-4.252 -4.265,-4.252zM0.005,10.873v6.133c3.993,0 7.749,1.562 10.577,4.391 2.825,2.822 4.384,6.595 4.384,10.603h6.16c-0,-11.651 -9.478,-21.127 -21.121,-21.127zM0.012,0v6.136c14.243,0 25.836,11.604 25.836,25.864h6.152c0,-17.64 -14.352,-32 -31.988,-32z"
android:fillColor="#fff"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M32,12.408l-11.056,-1.607 -4.944,-10.018 -4.944,10.018 -11.056,1.607 8,7.798 -1.889,11.011 9.889,-5.199 9.889,5.199 -1.889,-11.011 8,-7.798z"
android:fillColor="#fff"/>
</vector>
@@ -0,0 +1 @@
<!-- drawable/account_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" /></vector>
@@ -0,0 +1 @@
<!-- drawable/book_multiple_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M19 2A2 2 0 0 1 21 4V16A2 2 0 0 1 19 18H9A2 2 0 0 1 7 16V4A2 2 0 0 1 9 2H19M19 4H16V10L13.5 7.75L11 10V4H9V16H19M3 20A2 2 0 0 0 5 22H17V20H5V6H3Z" /></vector>
@@ -0,0 +1 @@
<!-- drawable/book_open_blank_variant_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M12 21.5C10.65 20.65 8.2 20 6.5 20C4.85 20 3.15 20.3 1.75 21.05C1.65 21.1 1.6 21.1 1.5 21.1C1.25 21.1 1 20.85 1 20.6V6C1.6 5.55 2.25 5.25 3 5C4.11 4.65 5.33 4.5 6.5 4.5C8.45 4.5 10.55 4.9 12 6C13.45 4.9 15.55 4.5 17.5 4.5C18.67 4.5 19.89 4.65 21 5C21.75 5.25 22.4 5.55 23 6V20.6C23 20.85 22.75 21.1 22.5 21.1C22.4 21.1 22.35 21.1 22.25 21.05C20.85 20.3 19.15 20 17.5 20C15.8 20 13.35 20.65 12 21.5M11 7.5C9.64 6.9 7.84 6.5 6.5 6.5C5.3 6.5 4.1 6.65 3 7V18.5C4.1 18.15 5.3 18 6.5 18C7.84 18 9.64 18.4 11 19V7.5M13 19C14.36 18.4 16.16 18 17.5 18C18.7 18 19.9 18.15 21 18.5V7C19.9 6.65 18.7 6.5 17.5 6.5C16.16 6.5 14.36 6.9 13 7.5V19Z" /></vector>
@@ -0,0 +1 @@
<!-- drawable/clock_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z" /></vector>
@@ -0,0 +1 @@
<!-- drawable/telescope.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M21.9,8.9L20.2,9.9L16.2,3L17.9,2L21.9,8.9M9.8,7.9L12.8,13.1L18.9,9.6L15.9,4.4L9.8,7.9M11.4,12.7L9.4,9.2L5.1,11.7L7.1,15.2L11.4,12.7M2.1,14.6L3.1,16.3L5.7,14.8L4.7,13.1L2.1,14.6M12.1,14L11.8,13.6L7.5,16.1L7.8,16.5C8,16.8 8.3,17.1 8.6,17.3L7,22H9L10.4,17.7H10.5L12,22H14L12.1,16.4C12.6,15.7 12.6,14.8 12.1,14Z" /></vector>
+4 -4
View File
@@ -8,8 +8,8 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.google.gms:google-services:4.4.0' classpath 'com.google.gms:google-services:4.4.2'
classpath 'com.android.tools.build:gradle:8.1.1' classpath 'com.android.tools.build:gradle:8.8.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
@@ -26,7 +26,7 @@ allprojects {
} }
} }
task clean(type: Delete) { tasks.register('clean', Delete) {
delete rootProject.buildDir delete rootProject.layout.buildDirectory
} }
+3
View File
@@ -5,6 +5,9 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/
include ':byteowls-capacitor-filesharer' include ':byteowls-capacitor-filesharer'
project(':byteowls-capacitor-filesharer').projectDir = new File('../node_modules/@byteowls/capacitor-filesharer/android') project(':byteowls-capacitor-filesharer').projectDir = new File('../node_modules/@byteowls/capacitor-filesharer/android')
include ':capacitor-community-keep-awake'
project(':capacitor-community-keep-awake').projectDir = new File('../node_modules/@capacitor-community/keep-awake/android')
include ':capacitor-community-volume-buttons' include ':capacitor-community-volume-buttons'
project(':capacitor-community-volume-buttons').projectDir = new File('../node_modules/@capacitor-community/volume-buttons/android') project(':capacitor-community-volume-buttons').projectDir = new File('../node_modules/@capacitor-community/volume-buttons/android')
+3 -1
View File
@@ -31,4 +31,6 @@ android.useAndroidX=true
kapt.use.worker.api=false kapt.use.worker.api=false
android.defaults.buildfeatures.buildconfig=true android.buildfeatures.buildconfig=true
org.gradle.warning.mode=all
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
networkTimeout=10000 networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
+4
View File
@@ -1,3 +1,7 @@
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0"
}
include ':app' include ':app'
include ':capacitor-cordova-android-plugins' include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
+9 -24
View File
@@ -1,39 +1,24 @@
ext { ext {
minSdkVersion = 24 minSdkVersion = 24
compileSdkVersion = 34 compileSdkVersion = 35
targetSdkVersion = 34 targetSdkVersion = 34
androidxActivityVersion = '1.7.0' androidxActivityVersion = '1.8.0'
androidxAppCompatVersion = '1.7.0' androidxAppCompatVersion = '1.7.0'
androidxCoordinatorLayoutVersion = '1.2.0' androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.10.0' androidxCoreVersion = '1.12.0'
androidPlayCore = '1.9.0' androidxFragmentVersion = '1.6.2'
androidxFragmentVersion = '1.5.6'
junitVersion = '4.13.2' junitVersion = '4.13.2'
androidxJunitVersion = '1.1.5' androidxJunitVersion = '1.1.5'
androidxEspressoCoreVersion = '3.5.1' androidxEspressoCoreVersion = '3.5.1'
cordovaAndroidVersion = '10.1.1' cordovaAndroidVersion = '10.1.1'
androidx_car_version = '1.0.0-alpha7' androidx_core_ktx_version = '1.15.0'
androidx_core_ktx_version = '1.13.1'
androidx_media_version = '1.7.0' androidx_media_version = '1.7.0'
androidx_preference_version = '1.1.1'
androidx_test_runner_version = '1.3.0'
arch_lifecycle_version = '2.2.0'
constraint_layout_version = '2.0.1'
espresso_version = '3.3.0'
exoplayer_version = '2.18.7' exoplayer_version = '2.18.7'
fragment_version = '1.2.5'
glide_version = '4.11.0' glide_version = '4.11.0'
gms_strict_version_matcher_version = '1.0.3' junit_version = '4.13.2'
gradle_version = '3.1.4' kotlin_version = '2.1.0'
gson_version = '2.8.5' kotlin_coroutines_version = '1.10.1'
junit_version = '4.13'
kotlin_version = '2.0.0'
kotlin_coroutines_version = '1.6.4'
multidex_version = '1.0.3'
play_services_auth_version = '18.1.0'
recycler_view_version = '1.1.0'
robolectric_version = '4.2'
test_runner_version = '1.1.0' test_runner_version = '1.1.0'
coreSplashScreenVersion = '1.0.1' coreSplashScreenVersion = '1.0.1'
androidxWebkitVersion = '1.6.1' androidxWebkitVersion = '1.9.0'
} }
+3 -1
View File
@@ -2,10 +2,12 @@
"appId": "com.audiobookshelf.app", "appId": "com.audiobookshelf.app",
"appName": "audiobookshelf-app", "appName": "audiobookshelf-app",
"webDir": "dist", "webDir": "dist",
"bundledWebRuntime": false,
"plugins": { "plugins": {
"CapacitorHttp": { "CapacitorHttp": {
"enabled": false "enabled": false
} }
},
"server": {
"androidScheme": "http"
} }
} }
+1 -1
View File
@@ -4,7 +4,7 @@
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-rate.sync="playbackSpeed" @update:playbackRate="updatePlaybackSpeed" @change="changePlaybackSpeed" /> <modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-rate.sync="playbackSpeed" @update:playbackRate="updatePlaybackSpeed" @change="changePlaybackSpeed" />
<modals-sleep-timer-modal v-model="showSleepTimerModal" :current-time="sleepTimeRemaining" :sleep-timer-running="isSleepTimerRunning" :current-end-of-chapter-time="currentEndOfChapterTime" :is-auto="isAutoSleepTimer" @change="selectSleepTimeout" @cancel="cancelSleepTimer" @increase="increaseSleepTimer" @decrease="decreaseSleepTimer" /> <modals-sleep-timer-modal v-model="showSleepTimerModal" :current-time="sleepTimeRemaining" :sleep-timer-running="isSleepTimerRunning" :current-end-of-chapter-time="currentEndOfChapterTime" :is-auto="isAutoSleepTimer" @change="selectSleepTimeout" @cancel="cancelSleepTimer" @increase="increaseSleepTimer" @decrease="decreaseSleepTimer" />
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="currentTime" :library-item-id="serverLibraryItemId" @select="selectBookmark" /> <modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="currentTime" :library-item-id="serverLibraryItemId" :playback-rate="playbackSpeed" @select="selectBookmark" />
</div> </div>
</template> </template>
+2 -1
View File
@@ -8,7 +8,8 @@
</template> </template>
<div v-show="!entities.length && initialized" class="w-full py-16 text-center text-xl"> <div v-show="!entities.length && initialized" class="w-full py-16 text-center text-xl">
<div class="py-4 capitalize">No {{ entityName }}</div> <div v-if="page === 'collections'" class="py-4">{{ $strings.MessageNoCollections }}</div>
<div v-else class="py-4 capitalize">No {{ entityName }}</div>
<ui-btn v-if="hasFilter" @click="clearFilter">{{ $strings.ButtonClearFilter }}</ui-btn> <ui-btn v-if="hasFilter" @click="clearFilter">{{ $strings.ButtonClearFilter }}</ui-btn>
</div> </div>
</div> </div>
+1 -1
View File
@@ -284,7 +284,7 @@ export default {
if (this.collapsedSeries) return null if (this.collapsedSeries) return null
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs) if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs) if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt) if (this.orderBy === 'addedAt') return this.$getString('LabelAddedDate', [this.$formatDate(this._libraryItem.addedAt)])
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false) if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size) if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes` if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
+2 -6
View File
@@ -108,11 +108,7 @@ export default {
}, },
episodes() { episodes() {
if (this.isPodcast) { if (this.isPodcast) {
if (this.media.numEpisodes == 1) { return this.$getString('LabelNumEpisodes', [this.media.numEpisodes])
return '1 episode'
} else {
return this.media.numEpisodes + ' episodes'
}
} else { } else {
return null return null
} }
@@ -201,7 +197,7 @@ export default {
if (this.collapsedSeries) return null if (this.collapsedSeries) return null
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs) if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs) if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt) if (this.orderBy === 'addedAt') return this.$getString('LabelAddedDate', [this.$formatDate(this._libraryItem.addedAt)])
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size) if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
return null return null
}, },
+2 -3
View File
@@ -486,9 +486,8 @@ export default {
validateServerUrl(url, protocolOverride = null) { validateServerUrl(url, protocolOverride = null) {
try { try {
var urlObject = new URL(url) var urlObject = new URL(url)
var address = `${protocolOverride ? protocolOverride : urlObject.protocol}//${urlObject.hostname}` if (protocolOverride) urlObject.protocol = protocolOverride
if (urlObject.port) address += ':' + urlObject.port return urlObject.href
return address
} catch (error) { } catch (error) {
console.error('Invalid URL', error) console.error('Invalid URL', error)
return null return null
+29 -11
View File
@@ -6,34 +6,34 @@
</div> </div>
</template> </template>
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false"> <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 rounded-lg bg-primary border border-border overflow-y-auto overflow-x-hidden" style="max-height: 80vh" @click.stop.prevent> <div class="w-full rounded-lg bg-primary border border-border overflow-y-auto overflow-x-hidden relative mt-16" style="max-height: 80vh" @click.stop.prevent>
<div class="w-full h-full p-4" v-show="showBookmarkTitleInput"> <div class="w-full h-full p-4" v-if="showBookmarkTitleInput">
<div class="flex mb-4 items-center"> <div class="flex mb-4 items-center">
<div class="w-9 h-9 flex items-center justify-center rounded-full hover:bg-white hover:bg-opacity-10 cursor-pointer" @click.stop="showBookmarkTitleInput = false"> <div class="w-9 h-9 flex items-center justify-center rounded-full hover:bg-white hover:bg-opacity-10 cursor-pointer" @click.stop="showBookmarkTitleInput = false">
<span class="material-icons text-3xl">arrow_back</span> <span class="material-icons text-3xl">arrow_back</span>
</div> </div>
<p class="text-xl pl-2">{{ selectedBookmark ? 'Edit Bookmark' : 'New Bookmark' }}</p> <p class="text-xl pl-2">{{ selectedBookmark ? 'Edit Bookmark' : 'New Bookmark' }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="text-xl font-mono">{{ this.$secondsToTimestamp(currentTime) }}</p> <p class="text-xl font-mono">{{ this.$secondsToTimestamp(currentTime / _playbackRate) }}</p>
</div> </div>
<ui-text-input-with-label v-model="newBookmarkTitle" label="Note" /> <ui-text-input-with-label v-model="newBookmarkTitle" ref="noteInput" label="Note" />
<div class="flex justify-end mt-6"> <div class="flex justify-end mt-6">
<ui-btn color="success" class="w-full" @click.stop="submitBookmark">{{ selectedBookmark ? 'Update' : 'Create' }}</ui-btn> <ui-btn color="success" class="w-full" @click.stop="submitBookmark">{{ selectedBookmark ? 'Update' : 'Create' }}</ui-btn>
</div> </div>
</div> </div>
<div class="w-full h-full" v-show="!showBookmarkTitleInput"> <div class="w-full h-full" v-else>
<template v-for="bookmark in bookmarks"> <template v-for="bookmark in bookmarks">
<modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" @click="clickBookmark" @edit="editBookmark" @delete="deleteBookmark" /> <modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" :playback-rate="_playbackRate" @click="clickBookmark" @edit="editBookmark" @delete="deleteBookmark" />
</template> </template>
<div v-if="!bookmarks.length" class="flex h-32 items-center justify-center"> <div v-if="!bookmarks.length" class="flex h-32 items-center justify-center">
<p class="text-xl">{{ $strings.MessageNoBookmarks }}</p> <p class="text-xl">{{ $strings.MessageNoBookmarks }}</p>
</div> </div>
<div v-show="canCreateBookmark" class="flex px-4 py-2 items-center text-center justify-between border-b border-fg/10 bg-success cursor-pointer text-white text-opacity-80" @click.stop="createBookmark"> </div>
<span class="material-icons">add</span> <div v-if="canCreateBookmark && !showBookmarkTitleInput" class="flex px-4 py-2 items-center text-center justify-between border-b border-fg/10 bg-success cursor-pointer text-white text-opacity-80 sticky bottom-0 left-0 w-full" @click.stop="createBookmark">
<p class="text-base pl-2">{{ $strings.ButtonCreateBookmark }}</p> <span class="material-icons">add</span>
<p class="text-sm font-mono">{{ this.$secondsToTimestamp(currentTime) }}</p> <p class="text-base pl-2">{{ $strings.ButtonCreateBookmark }}</p>
</div> <p class="text-sm font-mono">{{ this.$secondsToTimestamp(currentTime / _playbackRate) }}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -54,6 +54,10 @@ export default {
type: Number, type: Number,
default: 0 default: 0
}, },
playbackRate: {
type: Number,
default: 1
},
libraryItemId: String libraryItemId: String
}, },
data() { data() {
@@ -82,6 +86,10 @@ export default {
}, },
canCreateBookmark() { canCreateBookmark() {
return !this.bookmarks.find((bm) => bm.time === this.currentTime) return !this.bookmarks.find((bm) => bm.time === this.currentTime)
},
_playbackRate() {
if (!this.playbackRate || isNaN(this.playbackRate)) return 1
return this.playbackRate
} }
}, },
methods: { methods: {
@@ -151,6 +159,16 @@ export default {
this.selectedBookmark = null this.selectedBookmark = null
this.newBookmarkTitle = this.$formatDate(Date.now(), 'MMM dd, yyyy HH:mm') this.newBookmarkTitle = this.$formatDate(Date.now(), 'MMM dd, yyyy HH:mm')
this.showBookmarkTitleInput = true this.showBookmarkTitleInput = true
// Auto focus the input and select the text
this.$nextTick(() => {
if (this.$refs.noteInput?.$refs.input?.$refs.input) {
this.$refs.noteInput.$refs.input.$refs.input.focus()
setTimeout(() => {
this.$refs.noteInput?.$refs.input?.$refs.input?.select()
}, 10)
}
})
}, },
async submitBookmark() { async submitBookmark() {
await this.$hapticsImpact() await this.$hapticsImpact()
@@ -17,7 +17,7 @@
<p v-if="episode.episode" class="font-semibold text-gray-200 text-xs">#{{ episode.episode }}</p> <p v-if="episode.episode" class="font-semibold text-gray-200 text-xs">#{{ episode.episode }}</p>
<p class="break-words mb-1 text-sm">{{ episode.title }}</p> <p class="break-words mb-1 text-sm">{{ episode.title }}</p>
<p v-if="episode.subtitle" class="break-words mb-1 text-xs text-gray-300 episode-subtitle">{{ episode.subtitle }}</p> <p v-if="episode.subtitle" class="break-words mb-1 text-xs text-gray-300 episode-subtitle">{{ episode.subtitle }}</p>
<p class="text-xxs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p> <p class="text-xxs text-gray-300">{{ $getString('LabelPublishedDate', [episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : $strings.LabelUnknown]) }}</p>
</div> </div>
</div> </div>
</template> </template>
+5 -5
View File
@@ -1,5 +1,5 @@
<template> <template>
<div :key="bookmark.id" :id="`bookmark-row-${bookmark.id}`" class="flex items-center px-1 py-4 justify-start relative" :class="highlight ? 'bg-bg bg-opacity-60' : ' bg-opacity-20'" @click="click"> <div :key="bookmark.time" :id="`bookmark-row-${bookmark.time}`" class="flex items-center px-1 py-4 justify-start relative" :class="highlight ? 'bg-bg bg-opacity-60' : ' bg-opacity-20'" @click="click">
<div class="flex-grow overflow-hidden px-2"> <div class="flex-grow overflow-hidden px-2">
<div class="flex items-center mb-0.5"> <div class="flex items-center mb-0.5">
<i class="material-icons text-lg pr-1 -mb-1" :class="highlight ? 'text-success' : 'text-fg-muted'">{{ highlight ? 'bookmark' : 'bookmark_border' }}</i> <i class="material-icons text-lg pr-1 -mb-1" :class="highlight ? 'text-success' : 'text-fg-muted'">{{ highlight ? 'bookmark' : 'bookmark_border' }}</i>
@@ -7,7 +7,7 @@
{{ bookmark.title }} {{ bookmark.title }}
</p> </p>
</div> </div>
<p class="text-sm font-mono text-fg-muted flex items-center"><span class="material-icons text-base pl-px pr-1">schedule</span>{{ $secondsToTimestamp(bookmark.time) }}</p> <p class="text-sm font-mono text-fg-muted flex items-center"><span class="material-icons text-base pl-px pr-1">schedule</span>{{ $secondsToTimestamp(bookmark.time / playbackRate) }}</p>
</div> </div>
<div class="h-full flex items-center justify-end transform w-16 pr-2" @click.stop> <div class="h-full flex items-center justify-end transform w-16 pr-2" @click.stop>
<span class="material-icons text-2xl mr-2 text-fg hover:text-yellow-400" @click.stop="editClick">edit</span> <span class="material-icons text-2xl mr-2 text-fg hover:text-yellow-400" @click.stop="editClick">edit</span>
@@ -23,7 +23,8 @@ export default {
type: Object, type: Object,
default: () => {} default: () => {}
}, },
highlight: Boolean highlight: Boolean,
playbackRate: Number
}, },
data() { data() {
return {} return {}
@@ -39,7 +40,6 @@ export default {
editClick() { editClick() {
this.$emit('edit', this.bookmark) this.$emit('edit', this.bookmark)
} }
}, }
mounted() {}
} }
</script> </script>
+60 -15
View File
@@ -54,9 +54,9 @@
</modals-fullscreen-modal> </modals-fullscreen-modal>
<!-- ereader settings modal --> <!-- ereader settings modal -->
<modals-fullscreen-modal v-model="showSettingsModal" :theme="ereaderTheme" half-screen> <modals-fullscreen-modal v-model="showSettingsModal" :theme="ereaderTheme">
<div style="box-shadow: 0px -8px 8px #11111155"> <div style="box-shadow: 0px -8px 8px #11111155">
<div class="flex items-end justify-between h-14 px-4 pb-2 mb-6"> <div class="flex items-end justify-between h-20 px-4 pb-2 mb-6">
<h1 class="text-lg">{{ $strings.HeaderEreaderSettings }}</h1> <h1 class="text-lg">{{ $strings.HeaderEreaderSettings }}</h1>
<button class="flex" @click="showSettingsModal = false"> <button class="flex" @click="showSettingsModal = false">
<span class="material-icons">close</span> <span class="material-icons">close</span>
@@ -66,39 +66,51 @@
<div class="w-full h-full px-4"> <div class="w-full h-full px-4">
<div class="flex items-center mb-6"> <div class="flex items-center mb-6">
<div class="w-32"> <div class="w-32">
<p class="text-base">{{ $strings.LabelTheme }}:</p> <p class="text-sm">{{ $strings.LabelTheme }}</p>
</div> </div>
<ui-toggle-btns v-model="ereaderSettings.theme" :items="themeItems" @input="settingsUpdated" /> <ui-toggle-btns v-model="ereaderSettings.theme" name="theme" :items="themeItems" @input="settingsUpdated" />
</div> </div>
<div class="flex items-center mb-6"> <div class="flex items-center mb-6">
<div class="w-32"> <div class="w-32">
<p class="text-base">{{ $strings.LabelFontScale }}:</p> <p class="text-sm">{{ $strings.LabelFontScale }}</p>
</div> </div>
<ui-range-input v-model="ereaderSettings.fontScale" :min="5" :max="300" :step="5" input-width="180px" @input="settingsUpdated" /> <ui-range-input v-model="ereaderSettings.fontScale" :min="5" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
</div> </div>
<div class="flex items-center mb-6"> <div class="flex items-center mb-6">
<div class="w-32"> <div class="w-32">
<p class="text-base">{{ $strings.LabelLineSpacing }}:</p> <p class="text-sm">{{ $strings.LabelLineSpacing }}</p>
</div> </div>
<ui-range-input v-model="ereaderSettings.lineSpacing" :min="100" :max="300" :step="5" input-width="180px" @input="settingsUpdated" /> <ui-range-input v-model="ereaderSettings.lineSpacing" :min="100" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
</div> </div>
<div class="flex items-center mb-6"> <div class="flex items-center mb-6">
<div class="w-32"> <div class="w-32">
<p class="text-base">{{ $strings.LabelFontBoldness }}:</p> <p class="text-sm">{{ $strings.LabelFontBoldness }}</p>
</div> </div>
<ui-range-input v-model="ereaderSettings.textStroke" :min="0" :max="300" :step="5" input-width="180px" @input="settingsUpdated" /> <ui-range-input v-model="ereaderSettings.textStroke" :min="0" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
</div> </div>
<div class="flex items-center mb-6"> <div class="flex items-center mb-6">
<div class="w-32"> <div class="w-32">
<p class="text-base">{{ $strings.LabelLayout }}:</p> <p class="text-sm">{{ $strings.LabelLayout }}</p>
</div> </div>
<ui-toggle-btns v-model="ereaderSettings.spread" :items="spreadItems" @input="settingsUpdated" /> <ui-toggle-btns v-model="ereaderSettings.spread" name="spread" :items="spreadItems" @input="settingsUpdated" />
</div>
<div class="flex items-center mb-6">
<div class="w-32">
<p class="text-sm">{{ $strings.LabelNavigateWithVolume }}</p>
</div>
<ui-toggle-btns v-model="ereaderSettings.navigateWithVolume" name="navigate-volume" :items="navigateWithVolumeItems" @input="settingsUpdated" />
</div>
<div class="flex items-center mb-6">
<div class="w-32">
<p class="text-sm">{{ $strings.LabelNavigateWithVolumeWhilePlaying }}</p>
</div>
<ui-toggle-btns v-model="ereaderSettings.navigateWithVolumeWhilePlaying" name="navigate-volume-playing" :items="onOffToggleButtonItems" @input="settingsUpdated" />
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<div class="w-32"> <div class="w-32">
<p class="text-base">{{ $strings.LabelNavigateWithVolume }}:</p> <p class="text-sm">{{ $strings.LabelKeepScreenAwake }}</p>
</div> </div>
<ui-toggle-btns v-model="ereaderSettings.navigateWithVolume" :items="navigateWithVolumeItems" @input="settingsUpdated" /> <ui-toggle-btns v-model="ereaderSettings.keepScreenAwake" name="keep-awake" :items="onOffToggleButtonItems" @input="settingsUpdated" />
</div> </div>
</div> </div>
</div> </div>
@@ -110,6 +122,7 @@
<script> <script>
import { Capacitor } from '@capacitor/core' import { Capacitor } from '@capacitor/core'
import { VolumeButtons } from '@capacitor-community/volume-buttons' import { VolumeButtons } from '@capacitor-community/volume-buttons'
import { KeepAwake } from '@capacitor-community/keep-awake'
export default { export default {
data() { data() {
@@ -132,7 +145,9 @@ export default {
lineSpacing: 115, lineSpacing: 115,
spread: 'auto', spread: 'auto',
textStroke: 0, textStroke: 0,
navigateWithVolume: 'enabled' navigateWithVolume: 'enabled',
navigateWithVolumeWhilePlaying: false,
keepScreenAwake: false
} }
} }
}, },
@@ -196,7 +211,7 @@ export default {
navigateWithVolumeItems() { navigateWithVolumeItems() {
return [ return [
{ {
text: this.$strings.LabelNavigateWithVolumeEnabled, text: this.$strings.LabelOn,
value: 'enabled' value: 'enabled'
}, },
{ {
@@ -204,11 +219,23 @@ export default {
value: 'mirrored' value: 'mirrored'
}, },
{ {
text: this.$strings.LabelNavigateWithVolumeDisabled, text: this.$strings.LabelOff,
value: 'none' value: 'none'
} }
] ]
}, },
onOffToggleButtonItems() {
return [
{
text: this.$strings.LabelOn,
value: true
},
{
text: this.$strings.LabelOff,
value: false
}
]
},
themeItems() { themeItems() {
return [ return [
{ {
@@ -295,6 +322,7 @@ export default {
localStorage.setItem('ereaderSettings', JSON.stringify(this.ereaderSettings)) localStorage.setItem('ereaderSettings', JSON.stringify(this.ereaderSettings))
this.initWatchVolume() this.initWatchVolume()
this.initKeepScreenAwake()
}, },
goToChapter(href) { goToChapter(href) {
this.showTOCModal = false this.showTOCModal = false
@@ -424,7 +452,7 @@ export default {
this.isInittingWatchVolume = true this.isInittingWatchVolume = true
const isWatching = await VolumeButtons.isWatching() const isWatching = await VolumeButtons.isWatching()
if (this.ereaderSettings.navigateWithVolume !== 'none' && !this.isPlayerOpen) { if (this.ereaderSettings.navigateWithVolume !== 'none' && (this.ereaderSettings.navigateWithVolumeWhilePlaying || !this.isPlayerOpen)) {
if (!isWatching.value) { if (!isWatching.value) {
const options = { const options = {
disableSystemVolumeHandler: true, disableSystemVolumeHandler: true,
@@ -440,11 +468,25 @@ export default {
this.isInittingWatchVolume = false this.isInittingWatchVolume = false
}, },
async initKeepScreenAwake() {
try {
if (this.ereaderSettings.keepScreenAwake) {
await KeepAwake.keepAwake()
console.log('Reader keep screen awake enabled')
} else {
await KeepAwake.allowSleep()
console.log('Reader keep screen awake disabled')
}
} catch (error) {
console.error('Failed to init keep screen awake', error)
}
},
registerListeners() { registerListeners() {
this.$eventBus.$on('close-ebook', this.closeEvt) this.$eventBus.$on('close-ebook', this.closeEvt)
document.body.addEventListener('touchstart', this.touchstart) document.body.addEventListener('touchstart', this.touchstart)
document.body.addEventListener('touchend', this.touchend) document.body.addEventListener('touchend', this.touchend)
this.initWatchVolume() this.initWatchVolume()
this.initKeepScreenAwake()
}, },
unregisterListeners() { unregisterListeners() {
this.$eventBus.$on('close-ebook', this.closeEvt) this.$eventBus.$on('close-ebook', this.closeEvt)
@@ -453,6 +495,9 @@ export default {
VolumeButtons.clearWatch().catch((error) => { VolumeButtons.clearWatch().catch((error) => {
console.error('Failed to clear volume watch', error) console.error('Failed to clear volume watch', error)
}) })
KeepAwake.allowSleep().catch((error) => {
console.error('Failed to allow sleep', error)
})
}, },
volumePressed(e) { volumePressed(e) {
if (this.ereaderSettings.navigateWithVolume == 'enabled') { if (this.ereaderSettings.navigateWithVolume == 'enabled') {
+1 -1
View File
@@ -9,7 +9,7 @@
<div class="flex items-center"> <div class="flex items-center">
<p class="hidden md:block text-xl font-semibold">{{ yearInReviewYear }} Year in Review</p> <p class="hidden md:block text-xl font-semibold">{{ yearInReviewYear }} Year in Review</p>
<div class="hidden md:block flex-grow" /> <div class="hidden md:block flex-grow" />
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? 'Hide Year in Review' : `See ${yearInReviewYear} Year in Review` }}</ui-btn> <ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide : $strings.LabelYearReviewShow }}</ui-btn>
</div> </div>
<!-- your year in review --> <!-- your year in review -->
+2 -2
View File
@@ -11,7 +11,7 @@
<p v-else>No Local Media Progress</p> <p v-else>No Local Media Progress</p>
</template>--> </template>-->
<p v-if="publishedAt" class="text-xs text-fg-muted mb-1">Published {{ $formatDate(publishedAt, 'MMM do, yyyy') }}</p> <p v-if="publishedAt" class="text-xs text-fg-muted mb-1">{{ $getString('LabelPublishedDate', [$formatDate(publishedAt, 'MMM do, yyyy')]) }}</p>
<p class="text-sm font-semibold">{{ title }}</p> <p class="text-sm font-semibold">{{ title }}</p>
@@ -40,7 +40,7 @@
<div v-if="userCanDownload"> <div v-if="userCanDownload">
<span v-if="isLocal" class="material-icons-outlined px-2 text-success text-lg">audio_file</span> <span v-if="isLocal" class="material-icons-outlined px-2 text-success text-lg">audio_file</span>
<span v-else-if="!localEpisode" class="material-icons mx-1.5 mt-2 text-xl" :class="(downloadItem || startingDownload) ? 'animate-bounce text-warning text-opacity-75' : ''" @click.stop="downloadClick">{{ (downloadItem || startingDownload) ? 'downloading' : 'download' }}</span> <span v-else-if="!localEpisode" class="material-icons mx-1.5 mt-2 text-xl" :class="downloadItem || startingDownload ? 'animate-bounce text-warning text-opacity-75' : ''" @click.stop="downloadClick">{{ downloadItem || startingDownload ? 'downloading' : 'download' }}</span>
<span v-else class="material-icons px-2 text-success text-xl">download_done</span> <span v-else class="material-icons px-2 text-success text-xl">download_done</span>
</div> </div>
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="w-full"> <div class="w-full">
<p class="pb-0.5 text-sm font-semibold">{{ label }}</p> <p class="pb-0.5 text-sm font-semibold">{{ label }}</p>
<ui-text-input v-model="inputValue" :disabled="disabled" :type="type" text-size="base" class="w-full" /> <ui-text-input v-model="inputValue" ref="input" :disabled="disabled" :type="type" text-size="base" class="w-full" />
</div> </div>
</template> </template>
+3 -2
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="inline-flex toggle-btn-wrapper shadow-md"> <div class="inline-flex toggle-btn-wrapper shadow-md">
<button v-for="item in items" :key="item.value" type="button" class="toggle-btn outline-none relative border border-border px-4 py-1" :class="{ selected: item.value === value }" @click.stop="clickBtn(item.value)"> <button v-for="(item, index) in items" :key="`${name}-${index}`" type="button" class="toggle-btn outline-none relative border border-border px-4 py-1" :class="{ selected: item.value === value }" @click.stop="clickBtn(item.value)">
{{ item.text }} {{ item.text }}
</button> </button>
</div> </div>
@@ -9,7 +9,8 @@
<script> <script>
export default { export default {
props: { props: {
value: String, name: String,
value: [String, Number, Boolean],
items: { items: {
type: Array, type: Array,
default: Object default: Object
+4
View File
@@ -30,6 +30,7 @@
4D91EEC62A40F28D004807ED /* EBookFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D91EEC52A40F28D004807ED /* EBookFile.swift */; }; 4D91EEC62A40F28D004807ED /* EBookFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D91EEC52A40F28D004807ED /* EBookFile.swift */; };
4DABC04F2B0139CA000F6264 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DABC04E2B0139CA000F6264 /* User.swift */; }; 4DABC04F2B0139CA000F6264 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DABC04E2B0139CA000F6264 /* User.swift */; };
4DF74912287105C600AC7814 /* DeviceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DF74911287105C600AC7814 /* DeviceSettings.swift */; }; 4DF74912287105C600AC7814 /* DeviceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DF74911287105C600AC7814 /* DeviceSettings.swift */; };
4DFE2DA32D345C390000B204 /* MyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DFE2DA22D345C390000B204 /* MyViewController.swift */; };
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; }; 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; }; 504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; }; 504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
@@ -105,6 +106,7 @@
4D91EEC52A40F28D004807ED /* EBookFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EBookFile.swift; sourceTree = "<group>"; }; 4D91EEC52A40F28D004807ED /* EBookFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EBookFile.swift; sourceTree = "<group>"; };
4DABC04E2B0139CA000F6264 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; }; 4DABC04E2B0139CA000F6264 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
4DF74911287105C600AC7814 /* DeviceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceSettings.swift; sourceTree = "<group>"; }; 4DF74911287105C600AC7814 /* DeviceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceSettings.swift; sourceTree = "<group>"; };
4DFE2DA22D345C390000B204 /* MyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyViewController.swift; sourceTree = "<group>"; };
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; }; 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
504EC3041FED79650016851F /* Audiobookshelf.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Audiobookshelf.app; sourceTree = BUILT_PRODUCTS_DIR; }; 504EC3041FED79650016851F /* Audiobookshelf.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Audiobookshelf.app; sourceTree = BUILT_PRODUCTS_DIR; };
504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -292,6 +294,7 @@
2FAD9762203C412B000D30F8 /* config.xml */, 2FAD9762203C412B000D30F8 /* config.xml */,
50B271D01FEDC1A000F3C39B /* public */, 50B271D01FEDC1A000F3C39B /* public */,
4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */, 4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */,
4DFE2DA22D345C390000B204 /* MyViewController.swift */,
); );
path = App; path = App;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -548,6 +551,7 @@
E9E985F828B02D9400957F23 /* PlayerProgress.swift in Sources */, E9E985F828B02D9400957F23 /* PlayerProgress.swift in Sources */,
E9D5505E28AC1C8500C746DD /* MediaProgress.swift in Sources */, E9D5505E28AC1C8500C746DD /* MediaProgress.swift in Sources */,
3A200C1527D64D7E00CBF02E /* AudioPlayer.swift in Sources */, 3A200C1527D64D7E00CBF02E /* AudioPlayer.swift in Sources */,
4DFE2DA32D345C390000B204 /* MyViewController.swift in Sources */,
E9D5507128AC1EC700C746DD /* DownloadItemPart.swift in Sources */, E9D5507128AC1EC700C746DD /* DownloadItemPart.swift in Sources */,
4D66B956282EE951008272D4 /* AbsFileSystem.m in Sources */, 4D66B956282EE951008272D4 /* AbsFileSystem.m in Sources */,
EACB38142BCCA1410060DA4A /* LegacyAudioPlayerRateManager.swift in Sources */, EACB38142BCCA1410060DA4A /* LegacyAudioPlayerRateManager.swift in Sources */,
+6 -7
View File
@@ -1,19 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14111" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait"> <device id="retina4_7" orientation="portrait" appearance="light"/>
<adaptation id="fullscreen"/>
</device>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
</dependencies> </dependencies>
<scenes> <scenes>
<!--Bridge View Controller--> <!--My View Controller-->
<scene sceneID="tne-QT-ifu"> <scene sceneID="tne-QT-ifu">
<objects> <objects>
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/> <viewController id="BYZ-38-t0r" customClass="MyViewController" customModule="Audiobookshelf" customModuleProvider="target" sceneMemberID="viewController"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="140" y="-1"/>
</scene> </scene>
</scenes> </scenes>
</document> </document>
+37
View File
@@ -0,0 +1,37 @@
//
// MyViewController.swift
// Audiobookshelf
//
// Created by advplyr on 1/12/25.
//
import UIKit
import Capacitor
class MyViewController: CAPBridgeViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override open func capacitorDidLoad() {
bridge?.registerPluginInstance(AbsDatabase())
bridge?.registerPluginInstance(AbsAudioPlayer())
bridge?.registerPluginInstance(AbsDownloader())
bridge?.registerPluginInstance(AbsFileSystem())
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
}
+18 -2
View File
@@ -2,10 +2,26 @@
"appId": "com.audiobookshelf.app", "appId": "com.audiobookshelf.app",
"appName": "audiobookshelf-app", "appName": "audiobookshelf-app",
"webDir": "dist", "webDir": "dist",
"bundledWebRuntime": false,
"plugins": { "plugins": {
"CapacitorHttp": { "CapacitorHttp": {
"enabled": false "enabled": false
} }
} },
"server": {
"androidScheme": "http"
},
"packageClassList": [
"FileSharerPlugin",
"KeepAwakePlugin",
"VolumeButtonsPlugin",
"AppPlugin",
"CAPBrowserPlugin",
"ClipboardPlugin",
"DialogPlugin",
"HapticsPlugin",
"CAPNetworkPlugin",
"PreferencesPlugin",
"StatusBarPlugin",
"CDVPlugin"
]
} }
+1
View File
@@ -12,6 +12,7 @@ def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
pod 'ByteowlsCapacitorFilesharer', :path => '../../node_modules/@byteowls/capacitor-filesharer' pod 'ByteowlsCapacitorFilesharer', :path => '../../node_modules/@byteowls/capacitor-filesharer'
pod 'CapacitorCommunityKeepAwake', :path => '../../node_modules/@capacitor-community/keep-awake'
pod 'CapacitorCommunityVolumeButtons', :path => '../../node_modules/@capacitor-community/volume-buttons' pod 'CapacitorCommunityVolumeButtons', :path => '../../node_modules/@capacitor-community/volume-buttons'
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser' pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser'
+33 -27
View File
@@ -1,29 +1,31 @@
PODS: PODS:
- Alamofire (5.8.1) - Alamofire (5.8.1)
- ByteowlsCapacitorFilesharer (5.0.0): - ByteowlsCapacitorFilesharer (6.0.0):
- Capacitor - Capacitor
- Capacitor (5.6.0): - Capacitor (6.2.0):
- CapacitorCordova - CapacitorCordova
- CapacitorApp (5.0.6): - CapacitorApp (6.0.2):
- Capacitor - Capacitor
- CapacitorBrowser (5.1.0): - CapacitorBrowser (6.0.4):
- Capacitor - Capacitor
- CapacitorClipboard (5.0.6): - CapacitorClipboard (6.0.2):
- Capacitor - Capacitor
- CapacitorCommunityVolumeButtons (1.0.2): - CapacitorCommunityKeepAwake (5.0.1):
- Capacitor - Capacitor
- CapacitorCordova (5.6.0) - CapacitorCommunityVolumeButtons (6.0.1):
- CapacitorDialog (5.0.6):
- Capacitor - Capacitor
- CapacitorHaptics (5.0.6): - CapacitorCordova (6.2.0)
- CapacitorDialog (6.0.2):
- Capacitor - Capacitor
- CapacitorNetwork (5.0.6): - CapacitorHaptics (6.0.2):
- Capacitor - Capacitor
- CapacitorPreferences (5.0.6): - CapacitorNetwork (6.0.3):
- Capacitor - Capacitor
- CapacitorStatusBar (5.0.6): - CapacitorPreferences (6.0.3):
- Capacitor - Capacitor
- CordovaPlugins (5.6.0): - CapacitorStatusBar (6.0.2):
- Capacitor
- CordovaPlugins (6.2.0):
- CapacitorCordova - CapacitorCordova
- Realm (10.47.0): - Realm (10.47.0):
- Realm/Headers (= 10.47.0) - Realm/Headers (= 10.47.0)
@@ -38,6 +40,7 @@ DEPENDENCIES:
- "CapacitorApp (from `../../node_modules/@capacitor/app`)" - "CapacitorApp (from `../../node_modules/@capacitor/app`)"
- "CapacitorBrowser (from `../../node_modules/@capacitor/browser`)" - "CapacitorBrowser (from `../../node_modules/@capacitor/browser`)"
- "CapacitorClipboard (from `../../node_modules/@capacitor/clipboard`)" - "CapacitorClipboard (from `../../node_modules/@capacitor/clipboard`)"
- "CapacitorCommunityKeepAwake (from `../../node_modules/@capacitor-community/keep-awake`)"
- "CapacitorCommunityVolumeButtons (from `../../node_modules/@capacitor-community/volume-buttons`)" - "CapacitorCommunityVolumeButtons (from `../../node_modules/@capacitor-community/volume-buttons`)"
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)" - "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
- "CapacitorDialog (from `../../node_modules/@capacitor/dialog`)" - "CapacitorDialog (from `../../node_modules/@capacitor/dialog`)"
@@ -65,6 +68,8 @@ EXTERNAL SOURCES:
:path: "../../node_modules/@capacitor/browser" :path: "../../node_modules/@capacitor/browser"
CapacitorClipboard: CapacitorClipboard:
:path: "../../node_modules/@capacitor/clipboard" :path: "../../node_modules/@capacitor/clipboard"
CapacitorCommunityKeepAwake:
:path: "../../node_modules/@capacitor-community/keep-awake"
CapacitorCommunityVolumeButtons: CapacitorCommunityVolumeButtons:
:path: "../../node_modules/@capacitor-community/volume-buttons" :path: "../../node_modules/@capacitor-community/volume-buttons"
CapacitorCordova: CapacitorCordova:
@@ -84,22 +89,23 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
Alamofire: 3ca42e259043ee0dc5c0cdd76c4bc568b8e42af7 Alamofire: 3ca42e259043ee0dc5c0cdd76c4bc568b8e42af7
ByteowlsCapacitorFilesharer: f6a773825632d65d5404a34764c4a3fd857bb176 ByteowlsCapacitorFilesharer: ea14537059851aed44df8c52f0984f61cd34f53b
Capacitor: ebfc16cdb8116d04c101686b080342872da42d43 Capacitor: 1f3c7b9802d958cd8c4eb63895fff85dff2e1eea
CapacitorApp: 024e1b1bea5f883d79f6330d309bc441c88ad04a CapacitorApp: 2a8c3a0b0814322e5e6e15fe595f02c3808f0f8b
CapacitorBrowser: 7a0fb6a1011abfaaf2dfedfd8248f942a8eda3d6 CapacitorBrowser: ef0529d16cd8839281050c350e7bbee4f5c6d65f
CapacitorClipboard: 77edf49827ea21da2a9c05c690a4a6a4d07199c4 CapacitorClipboard: 55e0a514f1e97b1409d533266c119dcbff3e78c3
CapacitorCommunityVolumeButtons: 132d27a8e482898e45480b7edc882f3d26a00cb6 CapacitorCommunityKeepAwake: e2ddd50812e3407f8dc3a2c28e97d66e9b59b2f5
CapacitorCordova: 931b48fcdbc9bc985fc2f16cec9f77c794a27729 CapacitorCommunityVolumeButtons: a7612c5996f1c66320ef7a567522346d84a57e22
CapacitorDialog: 0f3c15dfe9414b83bc64aef4078f1b92bcfead26 CapacitorCordova: b33e7f4aa4ed105dd43283acdd940964374a87d9
CapacitorHaptics: 1fffc1217c7e64a472d7845be50fb0c2f7d4204c CapacitorDialog: e966e2261e1c74a8a502e610dd66b4f97ec6fcca
CapacitorNetwork: d80b3e79bef6ec37640ee2806c19771f07ff2d0c CapacitorHaptics: b53409aaca1203f79c6d0eb3ed5de40556339518
CapacitorPreferences: f03954bcb0ff09c792909e46bff88e3183c16b10 CapacitorNetwork: da96b5fff8d05b67f4658503aabb22f65bda2c0f
CapacitorStatusBar: 565c0a1ebd79bb40d797606a8992b4a105885309 CapacitorPreferences: 9f9935bce493977183511362131a93f7b260191a
CordovaPlugins: dae7dac59b3cce58f5a7ce3894709d8f0f824656 CapacitorStatusBar: 3b9ac7d0684770522c532d1158a1434512ab1477
CordovaPlugins: 08731213c63ccf137c576da2926bf4b3a8c40f17
Realm: e43fb540ae947497e3ea8a662443256920602060 Realm: e43fb540ae947497e3ea8a662443256920602060
RealmSwift: 8b06ed06b5d16749ae0c4d91c0cba414a9e28189 RealmSwift: 8b06ed06b5d16749ae0c4d91c0cba414a9e28189
PODFILE CHECKSUM: d0bbe98fb520752929ffc76e7c901d54b2eed7f0 PODFILE CHECKSUM: 96d7bd74a37a613766883c65fc082490b5dbd7e9
COCOAPODS: 1.12.1 COCOAPODS: 1.12.1
+13 -36
View File
@@ -19,59 +19,36 @@ export default {
htmlAttrs: { htmlAttrs: {
lang: 'en' lang: 'en'
}, },
meta: [ meta: [{ charset: 'utf-8' }, { name: 'viewport', content: 'viewport-fit=cover, width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1' }, { hid: 'description', name: 'description', content: '' }, { name: 'format-detection', content: 'telephone=no' }],
{ charset: 'utf-8' },
{ name: 'viewport', content: 'viewport-fit=cover, width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1' },
{ hid: 'description', name: 'description', content: '' },
{ name: 'format-detection', content: 'telephone=no' }
],
script: [ script: [
{ {
src: '/libs/sortable.js' src: '/libs/sortable.js'
} }
], ],
link: [ link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
}, },
css: [ css: ['@/assets/tailwind.css', '@/assets/app.css'],
'@/assets/tailwind.css',
'@/assets/app.css'
],
plugins: [ plugins: ['@/plugins/server.js', '@/plugins/db.js', '@/plugins/localStore.js', '@/plugins/init.client.js', '@/plugins/axios.js', '@/plugins/nativeHttp.js', '@/plugins/capacitor/index.js', '@/plugins/capacitor/AbsAudioPlayer.js', '@/plugins/toast.js', '@/plugins/constants.js', '@/plugins/haptics.js', '@/plugins/i18n.js'],
'@/plugins/server.js',
'@/plugins/db.js',
'@/plugins/localStore.js',
'@/plugins/init.client.js',
'@/plugins/axios.js',
'@/plugins/nativeHttp.js',
'@/plugins/capacitor/index.js',
'@/plugins/capacitor/AbsAudioPlayer.js',
'@/plugins/toast.js',
'@/plugins/constants.js',
'@/plugins/haptics.js',
'@/plugins/i18n.js'
],
components: true, components: true,
modules: [ modules: ['@nuxtjs/axios'],
'@nuxtjs/axios'
],
axios: {}, axios: {},
build: { build: {
postcss: { postcss: {
plugins: { postcssOptions: {
tailwindcss: {}, plugins: {
autoprefixer: {}, tailwindcss: {},
}, autoprefixer: {}
}
}
}, },
babel: { babel: {
plugins: [['@babel/plugin-proposal-private-property-in-object', { loose: true }]], plugins: [['@babel/plugin-proposal-private-property-in-object', { loose: true }]]
}, }
} }
} }
+5755 -4589
View File
File diff suppressed because it is too large Load Diff
+19 -19
View File
@@ -5,31 +5,33 @@
"scripts": { "scripts": {
"dev": "nuxt --hostname 0.0.0.0 --port 1337", "dev": "nuxt --hostname 0.0.0.0 --port 1337",
"build": "nuxt build", "build": "nuxt build",
"start": "nuxt start", "start": "nuxt --hostname 0.0.0.0 --port 1337",
"generate": "nuxt generate", "generate": "nuxt generate",
"sync": "nuxt generate && npx cap sync", "sync": "nuxt generate && npx cap sync",
"icons-android": "cordova-res android --skip-config --copy", "icons-android": "cordova-res android --skip-config --copy",
"ionic:build": "npm run build", "ionic:build": "npm run build",
"ionic:serve": "npm run start" "ionic:serve": "npm run dev",
"devlive": "ionic cap run -l --external --port=1337"
}, },
"dependencies": { "dependencies": {
"@byteowls/capacitor-filesharer": "^5.0.0", "@byteowls/capacitor-filesharer": "^6.0.0",
"@capacitor-community/volume-buttons": "^1.0.2", "@capacitor-community/keep-awake": "^5.0.1",
"@capacitor/android": "^5.0.0", "@capacitor-community/volume-buttons": "^6.0.1",
"@capacitor/app": "^5.0.6", "@capacitor/android": "^6.0.0",
"@capacitor/browser": "^5.1.0", "@capacitor/app": "^6.0.0",
"@capacitor/clipboard": "^5.0.0", "@capacitor/browser": "^6.0.0",
"@capacitor/core": "^5.0.0", "@capacitor/clipboard": "^6.0.0",
"@capacitor/dialog": "^5.0.0", "@capacitor/core": "^6.0.0",
"@capacitor/haptics": "^5.0.0", "@capacitor/dialog": "^6.0.0",
"@capacitor/ios": "^5.0.0", "@capacitor/haptics": "^6.0.0",
"@capacitor/network": "^5.0.0", "@capacitor/ios": "^6.0.0",
"@capacitor/preferences": "^5.0.0", "@capacitor/network": "^6.0.0",
"@capacitor/status-bar": "^5.0.0", "@capacitor/preferences": "^6.0.0",
"@capacitor/status-bar": "^6.0.0",
"@nuxtjs/axios": "^5.13.6", "@nuxtjs/axios": "^5.13.6",
"@teckel/vue-pdf": "^4.3.5", "@teckel/vue-pdf": "^4.3.5",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"cordova-plugin-screen-orientation": "^3.0.2", "cordova-plugin-screen-orientation": "^3.0.4",
"core-js": "^3.15.1", "core-js": "^3.15.1",
"date-fns": "^2.25.0", "date-fns": "^2.25.0",
"epubjs": "0.3.88", "epubjs": "0.3.88",
@@ -42,9 +44,7 @@
"vuedraggable": "^2.24.3" "vuedraggable": "^2.24.3"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.13.15", "@capacitor/cli": "^6.0.0",
"@babel/preset-env": "7.13.15",
"@capacitor/cli": "^5.0.0",
"autoprefixer": "^10.4.17", "autoprefixer": "^10.4.17",
"postcss": "^8.4.33", "postcss": "^8.4.33",
"tailwindcss": "^3.4.1" "tailwindcss": "^3.4.1"
+1 -1
View File
@@ -3,7 +3,7 @@
<template v-if="!showSelectedFeed"> <template v-if="!showSelectedFeed">
<div class="w-full mx-auto h-20 flex items-center px-2"> <div class="w-full mx-auto h-20 flex items-center px-2">
<form class="w-full" @submit.prevent="submit"> <form class="w-full" @submit.prevent="submit">
<ui-text-input v-model="searchInput" :disabled="processing || !socketConnected" placeholder="Enter search term or RSS feed URL" text-size="sm" /> <ui-text-input v-model="searchInput" :disabled="processing || !socketConnected" :placeholder="$strings.MessagePodcastSearchField" text-size="sm" />
</form> </form>
</div> </div>
+2 -2
View File
@@ -138,7 +138,7 @@
<p ref="description" class="text-sm text-justify whitespace-pre-line font-light" :class="{ 'line-clamp-4': !showFullDescription }" style="hyphens: auto">{{ description }}</p> <p ref="description" class="text-sm text-justify whitespace-pre-line font-light" :class="{ 'line-clamp-4': !showFullDescription }" style="hyphens: auto">{{ description }}</p>
<div v-if="descriptionClamped" class="text-fg text-sm py-2" @click="showFullDescription = !showFullDescription"> <div v-if="descriptionClamped" class="text-fg text-sm py-2" @click="showFullDescription = !showFullDescription">
{{ showFullDescription ? 'Read less' : 'Read more' }} {{ showFullDescription ? $strings.ButtonReadLess : $strings.ButtonReadMore }}
<span class="material-icons align-middle text-base -mt-px">{{ showFullDescription ? 'expand_less' : 'expand_more' }}</span> <span class="material-icons align-middle text-base -mt-px">{{ showFullDescription ? 'expand_less' : 'expand_more' }}</span>
</div> </div>
</div> </div>
@@ -609,7 +609,7 @@ export default {
this.download(localFolder) this.download(localFolder)
}, },
async downloadClick() { async downloadClick() {
if (this.downloadItem || this.startingDownload) return if (this.downloadItem || this.startingDownload) return
const hasPermission = await this.checkCellularPermission('download') const hasPermission = await this.checkCellularPermission('download')
if (!hasPermission) return if (!hasPermission) return
+54 -1
View File
@@ -150,6 +150,22 @@
</div> </div>
</div> </div>
<!-- Android Auto settings -->
<template v-if="!isiOS">
<p class="uppercase text-xs font-semibold text-fg-muted mb-2 mt-10">{{ $strings.HeaderAndroidAutoSettings }}</p>
<div class="py-3 flex items-center">
<p class="pr-4 w-36">{{ $strings.LabelAndroidAutoBrowseLimitForGrouping }}</p>
<ui-text-input type="number" v-model="settings.androidAutoBrowseLimitForGrouping" style="width: 145px; max-width: 145px" @input="androidAutoBrowseLimitForGroupingUpdated" />
<span class="material-icons-outlined ml-2" @click.stop="showInfo('androidAutoBrowseLimitForGrouping')">info</span>
</div>
<div class="py-3 flex items-center">
<p class="pr-4 w-36">{{ $strings.LabelAndroidAutoBrowseSeriesSequenceOrder }}</p>
<div @click.stop="showAndroidAutoBrowseSeriesSequenceOrderOptions">
<ui-text-input :value="androidAutoBrowseSeriesSequenceOrderOption" readonly append-icon="expand_more" style="max-width: 200px" />
</div>
</div>
</template>
<div v-show="loading" class="w-full h-full absolute top-0 left-0 flex items-center justify-center z-10"> <div v-show="loading" class="w-full h-full absolute top-0 left-0 flex items-center justify-center z-10">
<ui-loading-indicator /> <ui-loading-indicator />
</div> </div>
@@ -193,7 +209,9 @@ export default {
autoSleepTimerAutoRewindTime: 300000, // 5 minutes autoSleepTimerAutoRewindTime: 300000, // 5 minutes
languageCode: 'en-us', languageCode: 'en-us',
downloadUsingCellular: 'ALWAYS', downloadUsingCellular: 'ALWAYS',
streamingUsingCellular: 'ALWAYS' streamingUsingCellular: 'ALWAYS',
androidAutoBrowseLimitForGrouping: 100,
androidAutoBrowseSeriesSequenceOrder: 'ASC'
}, },
theme: 'dark', theme: 'dark',
lockCurrentOrientation: false, lockCurrentOrientation: false,
@@ -221,6 +239,10 @@ export default {
enableMp3IndexSeeking: { enableMp3IndexSeeking: {
name: this.$strings.LabelEnableMp3IndexSeeking, name: this.$strings.LabelEnableMp3IndexSeeking,
message: this.$strings.LabelEnableMp3IndexSeekingHelp message: this.$strings.LabelEnableMp3IndexSeekingHelp
},
androidAutoBrowseLimitForGrouping: {
name: this.$strings.LabelAndroidAutoBrowseLimitForGrouping,
message: this.$strings.LabelAndroidAutoBrowseLimitForGroupingHelp
} }
}, },
hapticFeedbackItems: [ hapticFeedbackItems: [
@@ -290,6 +312,16 @@ export default {
text: this.$strings.LabelNever, text: this.$strings.LabelNever,
value: 'NEVER' value: 'NEVER'
} }
],
androidAutoBrowseSeriesSequenceOrderItems: [
{
text: this.$strings.LabelSequenceAscending,
value: 'ASC'
},
{
text: this.$strings.LabelSequenceDescending,
value: 'DESC'
}
] ]
} }
}, },
@@ -372,6 +404,10 @@ export default {
const item = this.streamingUsingCellularItems.find((i) => i.value === this.settings.streamingUsingCellular) const item = this.streamingUsingCellularItems.find((i) => i.value === this.settings.streamingUsingCellular)
return item?.text || 'Error' return item?.text || 'Error'
}, },
androidAutoBrowseSeriesSequenceOrderOption() {
const item = this.androidAutoBrowseSeriesSequenceOrderItems.find((i) => i.value === this.settings.androidAutoBrowseSeriesSequenceOrder)
return item?.text || 'Error'
},
moreMenuItems() { moreMenuItems() {
if (this.moreMenuSetting === 'shakeSensitivity') return this.shakeSensitivityItems if (this.moreMenuSetting === 'shakeSensitivity') return this.shakeSensitivityItems
else if (this.moreMenuSetting === 'hapticFeedback') return this.hapticFeedbackItems else if (this.moreMenuSetting === 'hapticFeedback') return this.hapticFeedbackItems
@@ -379,6 +415,7 @@ export default {
else if (this.moreMenuSetting === 'theme') return this.themeOptionItems else if (this.moreMenuSetting === 'theme') return this.themeOptionItems
else if (this.moreMenuSetting === 'downloadUsingCellular') return this.downloadUsingCellularItems else if (this.moreMenuSetting === 'downloadUsingCellular') return this.downloadUsingCellularItems
else if (this.moreMenuSetting === 'streamingUsingCellular') return this.streamingUsingCellularItems else if (this.moreMenuSetting === 'streamingUsingCellular') return this.streamingUsingCellularItems
else if (this.moreMenuSetting === 'androidAutoBrowseSeriesSequenceOrder') return this.androidAutoBrowseSeriesSequenceOrderItems
return [] return []
} }
}, },
@@ -421,6 +458,10 @@ export default {
this.moreMenuSetting = 'streamingUsingCellular' this.moreMenuSetting = 'streamingUsingCellular'
this.showMoreMenuDialog = true this.showMoreMenuDialog = true
}, },
showAndroidAutoBrowseSeriesSequenceOrderOptions() {
this.moreMenuSetting = 'androidAutoBrowseSeriesSequenceOrder'
this.showMoreMenuDialog = true
},
clickMenuAction(action) { clickMenuAction(action) {
this.showMoreMenuDialog = false this.showMoreMenuDialog = false
if (this.moreMenuSetting === 'shakeSensitivity') { if (this.moreMenuSetting === 'shakeSensitivity') {
@@ -441,6 +482,9 @@ export default {
} else if (this.moreMenuSetting === 'streamingUsingCellular') { } else if (this.moreMenuSetting === 'streamingUsingCellular') {
this.settings.streamingUsingCellular = action this.settings.streamingUsingCellular = action
this.saveSettings() this.saveSettings()
} else if (this.moreMenuSetting === 'androidAutoBrowseSeriesSequenceOrder') {
this.settings.androidAutoBrowseSeriesSequenceOrder = action
this.saveSettings()
} }
}, },
saveTheme(theme) { saveTheme(theme) {
@@ -451,6 +495,12 @@ export default {
if (!val) return // invalid times return falsy if (!val) return // invalid times return falsy
this.saveSettings() this.saveSettings()
}, },
androidAutoBrowseLimitForGroupingUpdated(val) {
if (!val) return // invalid times return falsy
if (val > 1000) val = 1000
if (val < 30) val = 30
this.saveSettings()
},
hapticFeedbackUpdated(val) { hapticFeedbackUpdated(val) {
this.$store.commit('globals/setHapticFeedback', val) this.$store.commit('globals/setHapticFeedback', val)
this.saveSettings() this.saveSettings()
@@ -576,6 +626,9 @@ export default {
this.settings.downloadUsingCellular = deviceSettings.downloadUsingCellular || 'ALWAYS' this.settings.downloadUsingCellular = deviceSettings.downloadUsingCellular || 'ALWAYS'
this.settings.streamingUsingCellular = deviceSettings.streamingUsingCellular || 'ALWAYS' this.settings.streamingUsingCellular = deviceSettings.streamingUsingCellular || 'ALWAYS'
this.settings.androidAutoBrowseLimitForGrouping = deviceSettings.androidAutoBrowseLimitForGrouping
this.settings.androidAutoBrowseSeriesSequenceOrder = deviceSettings.androidAutoBrowseSeriesSequenceOrder || 'ASC'
}, },
async init() { async init() {
this.loading = true this.loading = true
+11 -5
View File
@@ -21,7 +21,10 @@ class AbsAudioPlayerWeb extends WebPlugin {
// Use startTime to find current track index // Use startTime to find current track index
get currentTrackIndex() { get currentTrackIndex() {
return Math.max(0, this.audioTracks.findIndex(t => Math.floor(t.startOffset) <= this.startTime && Math.floor(t.startOffset + t.duration) > this.startTime)) return Math.max(
0,
this.audioTracks.findIndex((t) => Math.floor(t.startOffset) <= this.startTime && Math.floor(t.startOffset + t.duration) > this.startTime)
)
} }
get currentTrack() { get currentTrack() {
return this.audioTracks[this.currentTrackIndex] return this.audioTracks[this.currentTrackIndex]
@@ -37,7 +40,7 @@ class AbsAudioPlayerWeb extends WebPlugin {
} }
get totalDuration() { get totalDuration() {
var total = 0 var total = 0
this.audioTracks.forEach(at => total += at.duration) this.audioTracks.forEach((at) => (total += at.duration))
return total return total
} }
get playerPlaying() { get playerPlaying() {
@@ -194,7 +197,8 @@ class AbsAudioPlayerWeb extends WebPlugin {
// var lastBufferTime = this.getLastBufferedTime() // var lastBufferTime = this.getLastBufferedTime()
} }
evtEnded() { evtEnded() {
if (this.currentTrackIndex < this.audioTracks.length - 1) { // Has next track if (this.currentTrackIndex < this.audioTracks.length - 1) {
// Has next track
console.log(`[AbsAudioPlayer] Track ended - loading next track ${this.currentTrackIndex + 1}`) console.log(`[AbsAudioPlayer] Track ended - loading next track ${this.currentTrackIndex + 1}`)
var nextTrack = this.audioTracks[this.currentTrackIndex + 1] var nextTrack = this.audioTracks[this.currentTrackIndex + 1]
this.playWhenReady = true this.playWhenReady = true
@@ -221,7 +225,7 @@ class AbsAudioPlayerWeb extends WebPlugin {
this.player.play() this.player.play()
} }
} }
evtTimeupdate() { } evtTimeupdate() {}
sendPlaybackMetadata(playerState) { sendPlaybackMetadata(playerState) {
this.notifyListeners('onMetadata', { this.notifyListeners('onMetadata', {
@@ -235,7 +239,9 @@ class AbsAudioPlayerWeb extends WebPlugin {
if (!this.currentTrack) return if (!this.currentTrack) return
// When direct play track is loaded current time needs to be set // When direct play track is loaded current time needs to be set
this.trackStartTime = Math.max(0, this.startTime - (this.currentTrack.startOffset || 0)) this.trackStartTime = Math.max(0, this.startTime - (this.currentTrack.startOffset || 0))
this.player.src = `${vuexStore.getters['user/getServerAddress']}${this.currentTrack.contentUrl}?token=${vuexStore.getters['user/getToken']}` const serverAddressUrl = new URL(vuexStore.getters['user/getServerAddress'])
const serverHost = `${serverAddressUrl.protocol}//${serverAddressUrl.host}`
this.player.src = `${serverHost}${this.currentTrack.contentUrl}?token=${vuexStore.getters['user/getToken']}`
console.log(`[AbsAudioPlayer] Loading track src ${this.player.src}`) console.log(`[AbsAudioPlayer] Loading track src ${this.player.src}`)
this.player.load() this.player.load()
this.player.playbackRate = this.playbackRate this.player.playbackRate = this.playbackRate
+3
View File
@@ -5,6 +5,8 @@ const defaultCode = 'en-us'
let $localStore = null let $localStore = null
const languageCodeMap = { const languageCodeMap = {
bn: { label: 'বাংলা', dateFnsLocale: 'bn' },
ca: { label: 'Català', dateFnsLocale: 'ca' },
cs: { label: 'Čeština', dateFnsLocale: 'cs' }, cs: { label: 'Čeština', dateFnsLocale: 'cs' },
da: { label: 'Dansk', dateFnsLocale: 'da' }, da: { label: 'Dansk', dateFnsLocale: 'da' },
de: { label: 'Deutsch', dateFnsLocale: 'de' }, de: { label: 'Deutsch', dateFnsLocale: 'de' },
@@ -21,6 +23,7 @@ const languageCodeMap = {
pl: { label: 'Polski', dateFnsLocale: 'pl' }, pl: { label: 'Polski', dateFnsLocale: 'pl' },
'pt-br': { label: 'Português (Brasil)', dateFnsLocale: 'ptBR' }, 'pt-br': { label: 'Português (Brasil)', dateFnsLocale: 'ptBR' },
ru: { label: 'Русский', dateFnsLocale: 'ru' }, ru: { label: 'Русский', dateFnsLocale: 'ru' },
sl: { label: 'Slovenščina', dateFnsLocale: 'sl' },
sv: { label: 'Svenska', dateFnsLocale: 'sv' }, sv: { label: 'Svenska', dateFnsLocale: 'sv' },
uk: { label: 'Українська', dateFnsLocale: 'uk' }, uk: { label: 'Українська', dateFnsLocale: 'uk' },
'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' }, 'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' },
+7 -2
View File
@@ -26,14 +26,19 @@ class ServerSocket extends EventEmitter {
this.serverAddress = serverAddress this.serverAddress = serverAddress
this.token = token this.token = token
console.log('[SOCKET] Connect Socket', this.serverAddress) const serverUrl = new URL(serverAddress)
const serverHost = `${serverUrl.protocol}//${serverUrl.host}`
const serverPath = serverUrl.pathname === '/' ? '' : serverUrl.pathname
console.log(`[SOCKET] Connecting to ${serverHost} with path ${serverPath}/socket.io`)
const socketOptions = { const socketOptions = {
transports: ['websocket'], transports: ['websocket'],
upgrade: false, upgrade: false,
path: `${serverPath}/socket.io`
// reconnectionAttempts: 3 // reconnectionAttempts: 3
} }
this.socket = io(this.serverAddress, socketOptions) this.socket = io(serverHost, socketOptions)
this.setSocketListeners() this.setSocketListeners()
} }
+3 -1
View File
@@ -26,7 +26,9 @@ Join us on [discord](https://discord.gg/pJsjuNCKRq)
This application is built using [NuxtJS](https://nuxtjs.org/) and [Capacitor](https://capacitorjs.com/) in order to run on both iOS and Android on the same code base. This application is built using [NuxtJS](https://nuxtjs.org/) and [Capacitor](https://capacitorjs.com/) in order to run on both iOS and Android on the same code base.
Information on helping with translations of the apps [here](https://www.audiobookshelf.org/faq#how-do-i-help-with-translations). ### Localization
Thank you to [Weblate](https://hosted.weblate.org/engage/audiobookshelf/) for hosting our localization infrastructure pro-bono. If you want to see Audiobookshelf in your language, please help us localize. Additional information on helping with the translations [here](https://www.audiobookshelf.org/faq#how-do-i-help-with-translations). <a href="https://hosted.weblate.org/engage/audiobookshelf/"> <img src="https://hosted.weblate.org/widget/audiobookshelf/abs-mobile-app/horizontal-auto.svg" alt="Translation status" /> </a>
### Windows Environment Setup for Android ### Windows Environment Setup for Android
+2 -2
View File
@@ -69,7 +69,7 @@ export const getters = {
// return `http://localhost:3333/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}` // return `http://localhost:3333/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}`
} }
const url = new URL(`/api/items/${libraryItem.id}/cover`, serverAddress) const url = new URL(`${serverAddress}/api/items/${libraryItem.id}/cover`)
return `${url}?token=${userToken}&ts=${lastUpdate}${raw ? '&raw=1' : ''}` return `${url}?token=${userToken}&ts=${lastUpdate}${raw ? '&raw=1' : ''}`
}, },
getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, placeholder = null) => { getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, placeholder = null) => {
@@ -79,7 +79,7 @@ export const getters = {
const serverAddress = rootGetters['user/getServerAddress'] const serverAddress = rootGetters['user/getServerAddress']
if (!userToken || !serverAddress) return placeholder if (!userToken || !serverAddress) return placeholder
const url = new URL(`/api/items/${libraryItemId}/cover`, serverAddress) const url = new URL(`${serverAddress}/api/items/${libraryItemId}/cover`)
return `${url}?token=${userToken}` return `${url}?token=${userToken}`
}, },
getLocalMediaProgressById: (state) => (localLibraryItemId, episodeId = null) => { getLocalMediaProgressById: (state) => (localLibraryItemId, episodeId = null) => {
+68
View File
@@ -0,0 +1,68 @@
{
"ButtonAdd": "إضافة",
"ButtonAddNewServer": "إضافة خادم جديد",
"ButtonAuthors": "المؤلفون",
"ButtonBack": "الرجوع",
"ButtonCancel": "إلغاء",
"ButtonCancelTimer": "إلغاء المؤقت",
"ButtonClearFilter": "تصفية الفرز",
"ButtonCloseFeed": "إغلاق",
"ButtonCollections": "المجموعات",
"ButtonConnect": "اتصل",
"ButtonConnectToServer": "اتصال بالخادم",
"ButtonCreate": "إنشاء",
"ButtonCreateBookmark": "إنشاء إشارة مرجعية",
"ButtonCreateNewPlaylist": "إنشاء قائمة تشغيل جديدة",
"ButtonDelete": "حذف",
"ButtonDeleteLocalEpisode": "احذف الحلقة المحلية",
"ButtonDeleteLocalFile": "احذف الملف المحلي",
"ButtonDeleteLocalItem": "احذف العنصر المحلي",
"ButtonDisableAutoTimer": "تعطيل المؤقت التلقائي",
"ButtonDisconnect": "فصل",
"ButtonGoToWebClient": "اذهب إلى عميل الويب",
"ButtonHistory": "تاريخ",
"ButtonHome": "الرئيسية",
"ButtonIssues": "مشاكل",
"ButtonLatest": "أحدث",
"ButtonLibrary": "المكتبة",
"ButtonLocalMedia": "ملفات الوسائط المحلية",
"ButtonManageLocalFiles": "إدارة الملفات المحلية",
"ButtonNewFolder": "مجلد جديد",
"ButtonNextEpisode": "الحلقة التالية",
"ButtonOpenFeed": "فتح التغذية",
"ButtonOverride": "تجاوز",
"ButtonPause": "تَوَقَّف",
"ButtonPlay": "تشغيل",
"ButtonPlayEpisode": "شغل الحلقة",
"ButtonPlaying": "مشغل الآن",
"ButtonPlaylists": "قوائم التشغيل",
"ButtonRead": "اقرأ",
"ButtonReadLess": "قلص",
"ButtonReadMore": "المزيد",
"ButtonRemove": "إزالة",
"ButtonRemoveFromServer": "إزالة من الخادم",
"ButtonSave": "حفظ",
"ButtonSaveOrder": "حفظ الترتيب",
"ButtonSearch": "بحث",
"ButtonSendEbookToDevice": "أرسل الكتاب إلى الجهاز",
"ButtonSeries": "سلسلة",
"ButtonSetTimer": "اضبط مؤقت",
"ButtonStream": "بث",
"ButtonSubmit": "تقديم",
"ButtonSwitchServerUser": "تبديل الخادم/المستخدم",
"ButtonUserStats": "إحصائيات المستخدم",
"ButtonYes": "نعم",
"HeaderAccount": "الحساب",
"HeaderAdvanced": "متقدم",
"HeaderAudioTracks": "المسارات الصوتية",
"HeaderChapters": "الفصول",
"HeaderCollection": "مجموعة",
"HeaderCollectionItems": "عناصر المجموعة",
"HeaderDataSettings": "إعدادات البيانات",
"HeaderDetails": "التفاصيل",
"HeaderEbookFiles": "ملفات الكتب الإلكترونية",
"HeaderEpisodes": "الحلقات",
"HeaderEreaderSettings": "إعدادات القارئ الإلكتروني",
"HeaderLatestEpisodes": "أحدث الحلقات",
"HeaderLibraries": "المكتبات"
}
+50
View File
@@ -0,0 +1,50 @@
{
"ButtonAdd": "Дадаць",
"ButtonAddNewServer": "Дадаць новы сервер",
"ButtonAuthors": "Аўтары",
"ButtonBack": "Назад",
"ButtonCancel": "Адмяніць",
"ButtonClearFilter": "Ачысціць фільтр",
"ButtonCloseFeed": "Закрыць стужку",
"ButtonCollections": "Калекцыі",
"ButtonConnect": "Падлучыцца",
"ButtonConnectToServer": "Падлучыцца да сервера",
"ButtonCreate": "Ствараць",
"ButtonCreateBookmark": "Стварыць закладку",
"ButtonCreateNewPlaylist": "Стварыць новы плэйліст",
"ButtonDelete": "Выдаліць",
"ButtonDeleteLocalEpisode": "Выдаліць лакальны эпізод",
"ButtonDeleteLocalFile": "Выдаліць лакальны файл",
"ButtonDeleteLocalItem": "Выдаліць лакальны элемент",
"ButtonDisableAutoTimer": "Адключыць аўтаматычны таймер",
"ButtonDisconnect": "Адлучыцца",
"ButtonGoToWebClient": "Перайсці ў вэб-кліент",
"ButtonHistory": "Гісторыя",
"ButtonHome": "Галоўная",
"ButtonLatest": "Апошняе",
"ButtonLibrary": "Бібліятэка",
"ButtonLocalMedia": "Лакальныя медыя",
"ButtonManageLocalFiles": "Кіраванне лакальнымі файламі",
"ButtonNewFolder": "Новая тэчка",
"ButtonNextEpisode": "Наступны эпізод",
"ButtonOpenFeed": "Адкрыць стужку",
"ButtonPause": "Паўза",
"ButtonPlaying": "Прайграваецца",
"ButtonPlaylists": "Плэйлісты",
"ButtonRemoveFromServer": "Выдаліць з сервера",
"ButtonSave": "Захаваць",
"ButtonSaveOrder": "Захаваць парадак",
"ButtonSearch": "Пошук",
"ButtonSwitchServerUser": "Змяніць Сервер/Карыстальніка",
"ButtonUserStats": "Статыстыка карыстальніка",
"ButtonYes": "Так",
"HeaderAccount": "Уліковы запіс",
"HeaderCollection": "Калекцыя",
"HeaderCollectionItems": "Элементы калекцыі",
"HeaderConnectionStatus": "Стан злучэння",
"ToastPodcastCreateFailed": "Не ўдалося стварыць падкаст",
"ToastPodcastCreateSuccess": "Падкаст паспяхова створаны",
"ToastRSSFeedCloseFailed": "Не ўдалося закрыць RSS-стужку",
"ToastRSSFeedCloseSuccess": "RSS-стужка закрыта",
"ToastStreamingNotAllowedOnCellular": "Патокавая перадача праз мабільную сувязь забаронена"
}
+20 -1
View File
@@ -1 +1,20 @@
{} {
"ButtonAddNewServer": "",
"ButtonAuthors": "",
"ButtonBack": "",
"ButtonCancel": "",
"ButtonCancelTimer": "",
"ButtonClearFilter": "",
"ButtonCloseFeed": "",
"ButtonCollections": "",
"ButtonConnect": "",
"ButtonConnectToServer": "",
"ButtonCreate": "",
"ButtonCreateBookmark": "",
"ButtonCreateNewPlaylist": "",
"ButtonDelete": "",
"ButtonDeleteLocalEpisode": "",
"ButtonDeleteLocalFile": "",
"ButtonDeleteLocalItem": "",
"ButtonDisableAutoTimer": ""
}
+17 -3
View File
@@ -37,6 +37,8 @@
"ButtonPlaying": "বাজছে", "ButtonPlaying": "বাজছে",
"ButtonPlaylists": "প্লেলিস্ট", "ButtonPlaylists": "প্লেলিস্ট",
"ButtonRead": "পড়ুন", "ButtonRead": "পড়ুন",
"ButtonReadLess": "কম পড়ুন",
"ButtonReadMore": "আরও পড়ুন",
"ButtonRemove": "মুছে ফেলুন", "ButtonRemove": "মুছে ফেলুন",
"ButtonRemoveFromServer": "সার্ভার থেকে সরান", "ButtonRemoveFromServer": "সার্ভার থেকে সরান",
"ButtonSave": "সংরক্ষণ করুন", "ButtonSave": "সংরক্ষণ করুন",
@@ -85,8 +87,8 @@
"HeaderUserInterfaceSettings": "ব্যবহারকারী ইন্টারফেস সেটিংস", "HeaderUserInterfaceSettings": "ব্যবহারকারী ইন্টারফেস সেটিংস",
"HeaderYourStats": "আপনার পরিসংখ্যান", "HeaderYourStats": "আপনার পরিসংখ্যান",
"LabelAddToPlaylist": "প্লেলিস্টে যোগ করুন", "LabelAddToPlaylist": "প্লেলিস্টে যোগ করুন",
"LabelAdded": "যোগ করা হয়েছে",
"LabelAddedAt": "এতে যোগ করা হয়েছে", "LabelAddedAt": "এতে যোগ করা হয়েছে",
"LabelAddedDate": "যোগ করা হয়েছে {0}",
"LabelAll": "সব", "LabelAll": "সব",
"LabelAllowSeekingOnMediaControls": "মিডিয়া বিজ্ঞপ্তি নিয়ন্ত্রণে অবস্থান খোঁজার অনুমতি দিন", "LabelAllowSeekingOnMediaControls": "মিডিয়া বিজ্ঞপ্তি নিয়ন্ত্রণে অবস্থান খোঁজার অনুমতি দিন",
"LabelAlways": "সর্বদা", "LabelAlways": "সর্বদা",
@@ -177,16 +179,19 @@
"LabelNarrator": "কথক", "LabelNarrator": "কথক",
"LabelNarrators": "কথক", "LabelNarrators": "কথক",
"LabelNavigateWithVolume": "ভলিউম বাটন দিয়ে নেভিগেট করুন", "LabelNavigateWithVolume": "ভলিউম বাটন দিয়ে নেভিগেট করুন",
"LabelNavigateWithVolumeDisabled": "বন্ধ",
"LabelNavigateWithVolumeEnabled": "চালু",
"LabelNavigateWithVolumeMirrored": "মিরর করা হয়েছে", "LabelNavigateWithVolumeMirrored": "মিরর করা হয়েছে",
"LabelNavigateWithVolumeWhilePlaying": "চলার সময় ভলিউম কী দ্বারা নেভিগেট করার অনুমতি দিন",
"LabelNavigateWithVolumeWhilePlayingDisabled": "বন্ধ",
"LabelNavigateWithVolumeWhilePlayingEnabled": "চালু",
"LabelNever": "কখনও না", "LabelNever": "কখনও না",
"LabelNewestAuthors": "নতুন লেখক", "LabelNewestAuthors": "নতুন লেখক",
"LabelNewestEpisodes": "নতুনতম পর্ব", "LabelNewestEpisodes": "নতুনতম পর্ব",
"LabelNo": "না", "LabelNo": "না",
"LabelNotFinished": "সমাপ্ত হয়নি", "LabelNotFinished": "সমাপ্ত হয়নি",
"LabelNotStarted": "শুরু হয়নি", "LabelNotStarted": "শুরু হয়নি",
"LabelNumEpisodes": "{0}টি পর্ব",
"LabelOff": "বন্ধ", "LabelOff": "বন্ধ",
"LabelOn": "চালু",
"LabelPassword": "পাসওয়ার্ড", "LabelPassword": "পাসওয়ার্ড",
"LabelPath": "পথ", "LabelPath": "পথ",
"LabelPlaybackDirect": "সরাসরি", "LabelPlaybackDirect": "সরাসরি",
@@ -199,6 +204,7 @@
"LabelProgress": "প্রগতি", "LabelProgress": "প্রগতি",
"LabelPubDate": "প্রকাশের তারিখ", "LabelPubDate": "প্রকাশের তারিখ",
"LabelPublishYear": "প্রকাশের বছর", "LabelPublishYear": "প্রকাশের বছর",
"LabelPublishedDate": "প্রকাশিত {0}",
"LabelRSSFeedCustomOwnerEmail": "কাস্টম মালিকের ইমেইল", "LabelRSSFeedCustomOwnerEmail": "কাস্টম মালিকের ইমেইল",
"LabelRSSFeedCustomOwnerName": "কাস্টম মালিকের নাম", "LabelRSSFeedCustomOwnerName": "কাস্টম মালিকের নাম",
"LabelRSSFeedPreventIndexing": "সূচীকরণ প্রতিরোধ করুন", "LabelRSSFeedPreventIndexing": "সূচীকরণ প্রতিরোধ করুন",
@@ -243,12 +249,15 @@
"LabelTotalTrack": "মোট ট্র্যাক", "LabelTotalTrack": "মোট ট্র্যাক",
"LabelTracks": "ট্র্যাকস", "LabelTracks": "ট্র্যাকস",
"LabelType": "টাইপ", "LabelType": "টাইপ",
"LabelUnknown": "অজানা",
"LabelUnlockPlayer": "প্লেয়ার আনলক করুন", "LabelUnlockPlayer": "প্লেয়ার আনলক করুন",
"LabelUseBookshelfView": "বুকশেলফ ভিউ ব্যবহার করুন", "LabelUseBookshelfView": "বুকশেলফ ভিউ ব্যবহার করুন",
"LabelUser": "ব্যবহারকারী", "LabelUser": "ব্যবহারকারী",
"LabelUsername": "ব্যবহারকারীর নাম", "LabelUsername": "ব্যবহারকারীর নাম",
"LabelVeryHigh": "অত্যন্ত উচ্চ", "LabelVeryHigh": "অত্যন্ত উচ্চ",
"LabelVeryLow": "খুবই কম", "LabelVeryLow": "খুবই কম",
"LabelYearReviewHide": "পর্যালোচনায় বছর লুকান",
"LabelYearReviewShow": "বছর পর্যালোচনা দেখুন",
"LabelYourBookmarks": "আপনার বুকমার্কস", "LabelYourBookmarks": "আপনার বুকমার্কস",
"LabelYourProgress": "আপনার অগ্রগতি", "LabelYourProgress": "আপনার অগ্রগতি",
"MessageAndroid10Downloads": "অ্যান্ড্রয়েড ১০ এবং এর নীচের সংস্করণগুলি ডাউনলোডের জন্য অভ্যন্তরীণ অ্যাপ স্টোরেজ ব্যবহার করবে।", "MessageAndroid10Downloads": "অ্যান্ড্রয়েড ১০ এবং এর নীচের সংস্করণগুলি ডাউনলোডের জন্য অভ্যন্তরীণ অ্যাপ স্টোরেজ ব্যবহার করবে।",
@@ -282,6 +291,7 @@
"MessageMediaNotLinkedToServer": "মিডিয়া কোন অডিওবুকশেল্ফ সার্ভারের সাথে লিঙ্ক করা নেই৷ কোন অগ্রগতি সিঙ্ক করা হবে না।", "MessageMediaNotLinkedToServer": "মিডিয়া কোন অডিওবুকশেল্ফ সার্ভারের সাথে লিঙ্ক করা নেই৷ কোন অগ্রগতি সিঙ্ক করা হবে না।",
"MessageNoBookmarks": "কোন বুকমার্ক নেই", "MessageNoBookmarks": "কোন বুকমার্ক নেই",
"MessageNoChapters": "কোনও অধ্যায় নেই", "MessageNoChapters": "কোনও অধ্যায় নেই",
"MessageNoCollections": "কোনো সংগ্রহ নেই",
"MessageNoItems": "কোন আইটেম নেই", "MessageNoItems": "কোন আইটেম নেই",
"MessageNoItemsFound": "কোন আইটেম পাওয়া যায়নি", "MessageNoItemsFound": "কোন আইটেম পাওয়া যায়নি",
"MessageNoListeningSessions": "কোনও শোনার সেশন নেই", "MessageNoListeningSessions": "কোনও শোনার সেশন নেই",
@@ -291,7 +301,11 @@
"MessageNoSeries": "কোনো সিরিজ নেই", "MessageNoSeries": "কোনো সিরিজ নেই",
"MessageNoUpdatesWereNecessary": "কোন আপডেটের প্রয়োজন ছিল না", "MessageNoUpdatesWereNecessary": "কোন আপডেটের প্রয়োজন ছিল না",
"MessageNoUserPlaylists": "আপনার কোনো প্লেলিস্ট নেই", "MessageNoUserPlaylists": "আপনার কোনো প্লেলিস্ট নেই",
"MessagePodcastSearchField": "অনুসন্ধান শব্দ বা RSS ফিড URL লিখুন",
"MessageReportBugsAndContribute": "বাগ রিপোর্ট করুন, বৈশিষ্ট্যের অনুরোধ করুন এবং এতে অবদান রাখুন", "MessageReportBugsAndContribute": "বাগ রিপোর্ট করুন, বৈশিষ্ট্যের অনুরোধ করুন এবং এতে অবদান রাখুন",
"MessageSeriesAlreadyDownloaded": "আপনি ইতিমধ্যে এই সিরিজের সব বই ডাউনলোড করেছেন।",
"MessageSeriesDownloadConfirm": "{1} ফাইল(গুলো) সহ অনুপস্থিত {0} বই(গুলো) ডাউনলোড করুন, মোট {2}টি ফোল্ডার {3} এ?",
"MessageSeriesDownloadConfirmIos": "{1} ফাইল(গুলো) সহ অনুপস্থিত {0} বই(গুলো) ডাউনলোড করুন, মোট {2}টি?",
"MessageSocketConnectedOverMeteredCellular": "মিটারযুক্ত সেলুলারের দ্বারা সকেট সংযোগ করা হয়েছে", "MessageSocketConnectedOverMeteredCellular": "মিটারযুক্ত সেলুলারের দ্বারা সকেট সংযোগ করা হয়েছে",
"MessageSocketConnectedOverMeteredWifi": "মিটারযুক্ত ওয়াইফাইয়ের মাধ্যমে সকেট সংযুক্ত", "MessageSocketConnectedOverMeteredWifi": "মিটারযুক্ত ওয়াইফাইয়ের মাধ্যমে সকেট সংযুক্ত",
"MessageSocketConnectedOverUnmeteredCellular": "মিটারবিহীন সেলুলারের দ্বারা সকেট সংযোগ করা হয়েছে", "MessageSocketConnectedOverUnmeteredCellular": "মিটারবিহীন সেলুলারের দ্বারা সকেট সংযোগ করা হয়েছে",
+196
View File
@@ -0,0 +1,196 @@
{
"ButtonAdd": "Afegir",
"ButtonAddNewServer": "Afegir un nou servidor",
"ButtonAuthors": "Autors",
"ButtonBack": "Enrere",
"ButtonCancel": "Cancel·lar",
"ButtonCancelTimer": "Cancel·lar el temporitzador",
"ButtonClearFilter": "Eliminar filtres",
"ButtonCloseFeed": "Tancar font",
"ButtonCollections": "Col·leccions",
"ButtonConnect": "Connectar",
"ButtonConnectToServer": "Connectar al servidor",
"ButtonCreate": "Crear",
"ButtonCreateBookmark": "Crear marcador",
"ButtonCreateNewPlaylist": "Crear una nova llista de reproducció",
"ButtonDelete": "Eliminar",
"ButtonDeleteLocalEpisode": "Esborrar episodi local",
"ButtonDeleteLocalFile": "Esborrar fitxer local",
"ButtonDeleteLocalItem": "Esborrar element local",
"ButtonDisableAutoTimer": "Desactivar temporitzador automàtic",
"ButtonDisconnect": "Desconnectar",
"ButtonGoToWebClient": "Anar al client web",
"ButtonHistory": "Historial",
"ButtonHome": "Inici",
"ButtonIssues": "Problemes",
"ButtonLatest": "Últims",
"ButtonLibrary": "Biblioteca",
"ButtonLocalMedia": "Mitjans locals",
"ButtonManageLocalFiles": "Gestionar fitxers locals",
"ButtonNewFolder": "Nova carpeta",
"ButtonNextEpisode": "Pròxim episodi",
"ButtonOpenFeed": "Obrir font",
"ButtonOverride": "Substituir",
"ButtonPause": "Pausa",
"ButtonPlay": "Reproduir",
"ButtonPlayEpisode": "Reproduir episodi",
"ButtonPlaying": "Reproduint",
"ButtonPlaylists": "Llistes de reproducció",
"ButtonRead": "Llegir",
"ButtonReadLess": "Llig menys",
"ButtonReadMore": "Llig més",
"ButtonRemove": "Eliminar",
"ButtonRemoveFromServer": "Eliminar del servidor",
"ButtonSave": "Desar",
"ButtonSaveOrder": "Desar ordre",
"ButtonSearch": "Cercar",
"ButtonSendEbookToDevice": "Enviar llibre al dispositiu",
"ButtonSeries": "Sèries",
"ButtonSetTimer": "Ajustar el temporitzador",
"ButtonStream": "En directe",
"ButtonSubmit": "Enviar",
"ButtonSwitchServerUser": "Canviar servidor/usuari",
"ButtonUserStats": "Estadístiques de l'usuari",
"ButtonYes": "Sí",
"HeaderAccount": "Compte",
"HeaderAdvanced": "Avançat",
"HeaderAudioTracks": "Pistes d'àudio",
"HeaderChapters": "Capítols",
"HeaderCollection": "Col·lecció",
"HeaderCollectionItems": "Elements de la col·lecció",
"HeaderConnectionStatus": "Estat de la connexió",
"HeaderDataSettings": "Configuració de dades",
"HeaderDetails": "Detalls",
"HeaderDownloads": "Descàrregues",
"HeaderEbookFiles": "Fitxers de llibres electrònics",
"HeaderEpisodes": "Episodis",
"HeaderEreaderSettings": "Configuració del lector",
"HeaderLatestEpisodes": "Últims episodis",
"HeaderLibraries": "Biblioteques",
"HeaderLocalFolders": "Carpetes locals",
"HeaderLocalLibraryItems": "Elements de la biblioteca local",
"HeaderNewPlaylist": "Nova llista de reproducció",
"HeaderOpenRSSFeed": "Obrir font RSS",
"HeaderPlaybackSettings": "Configuració de reproducció",
"HeaderPlaylist": "Llista de reproducció",
"HeaderPlaylistItems": "Elements de la llista de reproducció",
"HeaderRSSFeed": "Font RSS",
"HeaderRSSFeedGeneral": "Detalls RSS",
"HeaderRSSFeedIsOpen": "Font RSS oberta",
"HeaderSelectDownloadLocation": "Seleccionar ubicació de descàrrega",
"HeaderSettings": "Configuració",
"HeaderSleepTimer": "Temporitzador de desconnexió",
"HeaderSleepTimerSettings": "Configuració del temporitzador de desconnexió",
"HeaderStatsMinutesListeningChart": "Minuts escoltant (Últims 7 dies)",
"HeaderStatsRecentSessions": "Sessions recents",
"HeaderTableOfContents": "Taula de continguts",
"HeaderUserInterfaceSettings": "Configuració de la interfície d'usuari",
"HeaderYourStats": "Les teves estadístiques",
"LabelAddToPlaylist": "Afegit a la llista de reproducció",
"LabelAddedAt": "Afegit a",
"LabelAddedDate": "Afegits {0}",
"LabelAll": "Tots",
"LabelAllowSeekingOnMediaControls": "Permetre la cerca en controls de mitjans",
"LabelAlways": "Sempre",
"LabelAskConfirmation": "Demanar confirmació",
"LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Nom Cognom)",
"LabelAuthorLastFirst": "Autor (Cognom, Nom)",
"LabelAuthors": "Autors",
"LabelAutoDownloadEpisodes": "Descarregar episodis automàticament",
"LabelAutoRewindTime": "Temps de rebobinatge automàtic",
"LabelAutoSleepTimer": "Temporitzador de desconnexió automàtic",
"LabelAutoSleepTimerAutoRewind": "Temporitzador automàtic amb rebobinatge",
"LabelAutoSleepTimerAutoRewindHelp": "Quan el temporitzador d'auto sleep acaba, reproduir l'element altra vegada farà que es rebobine automàticament a la teua posició.",
"LabelAutoSleepTimerHelp": "Quan es reprodueix entre hores especificades, el temporitzador s'activarà automàticament.",
"LabelBooks": "Llibres",
"LabelChapterTrack": "Seguiment de capítol",
"LabelChapters": "Capítols",
"LabelClosePlayer": "Tancar reproductor",
"LabelCollapseSeries": "Reduir sèrie",
"LabelComplete": "Complet",
"LabelContinueBooks": "Continuar llibres",
"LabelContinueEpisodes": "Continuar episodis",
"LabelContinueListening": "Continuar escoltant",
"LabelContinueReading": "Continuar llegint",
"LabelContinueSeries": "Continuar sèries",
"LabelCustomTime": "Temps personalitzat",
"LabelDescription": "Descripció",
"LabelDisableAudioFadeOut": "Desactivar desaparició d'àudio",
"LabelDisableAudioFadeOutHelp": "El volum de l'audio començara a reduïr-se quan quede menys d'1 minut del temporitzador d'anar a dormir. Habilita este ajustament per a no reduïr el volum.",
"LabelDisableAutoRewind": "Desactivar rebobinatge automàtic",
"LabelDisableShakeToReset": "Desactivar sacsejar per reiniciar",
"LabelDiscover": "Descobrir",
"LabelDownload": "Descarregar",
"LabelDownloaded": "Descarregat",
"LabelDuration": "Durada",
"LabelEbook": "Llibre electrònic",
"LabelEbooks": "Llibres electrònics",
"LabelEnable": "Activar",
"LabelEnd": "Final",
"LabelEndOfChapter": "Final del capítol",
"LabelEpisode": "Episodi",
"LabelFeedURL": "Font URL",
"LabelFile": "Fitxer",
"LabelFinished": "Acabat",
"LabelFolder": "Carpeta",
"LabelGenre": "Gènere",
"LabelGenres": "Gèneres",
"LabelHapticFeedback": "Retroalimentació tàctil",
"LabelHigh": "Alt",
"LabelInProgress": "En procés",
"LabelIncomplete": "Incomplet",
"LabelJumpBackwardsTime": "Saltar enrere",
"LabelLanguage": "Idioma",
"LabelLayout": "Disseny",
"LabelListenAgain": "Escoltar de nou",
"LabelMedium": "Mitjà",
"LabelMore": "Més",
"LabelName": "Nom",
"LabelNarrator": "Narrador",
"LabelNever": "Mai",
"LabelNewestAuthors": "Autors més recents",
"LabelNewestEpisodes": "Episodis més recents",
"LabelNotFinished": "No acabat",
"LabelNotStarted": "No iniciat",
"LabelOff": "Apagat",
"LabelPassword": "Contrasenya",
"LabelPath": "Ruta de carpeta",
"LabelPlaybackSpeed": "Velocitat de reproducció",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelProgress": "Progrés",
"LabelPubDate": "Data de publicació",
"LabelPublishYear": "Any de publicació",
"LabelRead": "Llegit",
"LabelReadAgain": "Tornar a llegir",
"LabelRemoveFromPlaylist": "Eliminar de la llista de reproducció",
"LabelSeries": "Sèries",
"LabelSize": "Mida",
"LabelSleepTimer": "Temporitzador de desconnexió",
"LabelStart": "Iniciar",
"LabelStatsBestDay": "Millor dia",
"LabelStatsDays": "Dies",
"LabelStatsDaysListened": "Dies escoltats",
"LabelStatsItemsFinished": "Elements acabats",
"LabelStatsMinutes": "Minuts",
"LabelStatsWeekListening": "Temps escoltant setmanal",
"LabelTags": "Etiquetes",
"LabelTheme": "Tema",
"LabelTimeRemaining": "{0} restant",
"LabelTitle": "Títol",
"LabelTracks": "Pistes",
"LabelType": "Tipus",
"LabelUnlockPlayer": "Desbloquejar reproductor",
"LabelUseBookshelfView": "Usar vista de prestatgeria",
"LabelUser": "Usuari",
"LabelVeryHigh": "Molt alt",
"LabelVeryLow": "Molt baix",
"LabelYourBookmarks": "Els teus marcadors",
"LabelYourProgress": "El teu progrés",
"MessageConfirmDeleteLocalEpisode": "Eliminar episodi local \"{0}\" del dispositiu?",
"MessageDownloadCompleteProcessing": "Descarrega completada. Processant...",
"MessageMarkAsFinished": "Marcar com acabat",
"MessageMediaLinkedToServer": "Enllaçat al servidor {0}",
"MessageNoItemsFound": "Cap element trobat"
}
+17 -3
View File
@@ -37,6 +37,8 @@
"ButtonPlaying": "Hraje", "ButtonPlaying": "Hraje",
"ButtonPlaylists": "Seznamy skladeb", "ButtonPlaylists": "Seznamy skladeb",
"ButtonRead": "Číst", "ButtonRead": "Číst",
"ButtonReadLess": "Číst méně",
"ButtonReadMore": "Číst další",
"ButtonRemove": "Odstranit", "ButtonRemove": "Odstranit",
"ButtonRemoveFromServer": "Odstranit ze serveru", "ButtonRemoveFromServer": "Odstranit ze serveru",
"ButtonSave": "Uložit", "ButtonSave": "Uložit",
@@ -85,8 +87,8 @@
"HeaderUserInterfaceSettings": "Nastavení uživatelského rozhraní", "HeaderUserInterfaceSettings": "Nastavení uživatelského rozhraní",
"HeaderYourStats": "Vaše statistiky", "HeaderYourStats": "Vaše statistiky",
"LabelAddToPlaylist": "Přidat do seznamu skladeb", "LabelAddToPlaylist": "Přidat do seznamu skladeb",
"LabelAdded": "Přidáno",
"LabelAddedAt": "Přidáno v", "LabelAddedAt": "Přidáno v",
"LabelAddedDate": "Přidáno {0}",
"LabelAll": "Vše", "LabelAll": "Vše",
"LabelAllowSeekingOnMediaControls": "Povolit vyhledávání polohy v ovládacích prvcích oznámení médií", "LabelAllowSeekingOnMediaControls": "Povolit vyhledávání polohy v ovládacích prvcích oznámení médií",
"LabelAlways": "Vždy", "LabelAlways": "Vždy",
@@ -177,16 +179,19 @@
"LabelNarrator": "Vypravěč", "LabelNarrator": "Vypravěč",
"LabelNarrators": "Vypravěči", "LabelNarrators": "Vypravěči",
"LabelNavigateWithVolume": "Navigace pomocí tlačítek hlasitosti", "LabelNavigateWithVolume": "Navigace pomocí tlačítek hlasitosti",
"LabelNavigateWithVolumeDisabled": "Vypnuto",
"LabelNavigateWithVolumeEnabled": "Zapnuto",
"LabelNavigateWithVolumeMirrored": "Zrcadleno", "LabelNavigateWithVolumeMirrored": "Zrcadleno",
"LabelNavigateWithVolumeWhilePlaying": "Umožnit navigaci pomocí tlačítek hlasitosti během přehrávání",
"LabelNavigateWithVolumeWhilePlayingDisabled": "Vypnuto",
"LabelNavigateWithVolumeWhilePlayingEnabled": "Zapnuto",
"LabelNever": "Nikdy", "LabelNever": "Nikdy",
"LabelNewestAuthors": "Nejnovější autoři", "LabelNewestAuthors": "Nejnovější autoři",
"LabelNewestEpisodes": "Nejnovější epizody", "LabelNewestEpisodes": "Nejnovější epizody",
"LabelNo": "Ne", "LabelNo": "Ne",
"LabelNotFinished": "Nedokončeno", "LabelNotFinished": "Nedokončeno",
"LabelNotStarted": "Nezahájeno", "LabelNotStarted": "Nezahájeno",
"LabelNumEpisodes": "{0} epizod",
"LabelOff": "Vypnout", "LabelOff": "Vypnout",
"LabelOn": "Zapnuto",
"LabelPassword": "Heslo", "LabelPassword": "Heslo",
"LabelPath": "Cesta", "LabelPath": "Cesta",
"LabelPlaybackDirect": "Přímé", "LabelPlaybackDirect": "Přímé",
@@ -199,6 +204,7 @@
"LabelProgress": "Průběh", "LabelProgress": "Průběh",
"LabelPubDate": "Datum vydání", "LabelPubDate": "Datum vydání",
"LabelPublishYear": "Rok vydání", "LabelPublishYear": "Rok vydání",
"LabelPublishedDate": "Vydáno {0}",
"LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka", "LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka",
"LabelRSSFeedCustomOwnerName": "Vlastní jméno vlastníka", "LabelRSSFeedCustomOwnerName": "Vlastní jméno vlastníka",
"LabelRSSFeedPreventIndexing": "Zabránit indexování", "LabelRSSFeedPreventIndexing": "Zabránit indexování",
@@ -243,12 +249,15 @@
"LabelTotalTrack": "Celková stopa", "LabelTotalTrack": "Celková stopa",
"LabelTracks": "Stopy", "LabelTracks": "Stopy",
"LabelType": "Typ", "LabelType": "Typ",
"LabelUnknown": "Neznámý",
"LabelUnlockPlayer": "Odemknout přehrávač", "LabelUnlockPlayer": "Odemknout přehrávač",
"LabelUseBookshelfView": "Použít vzhled knihovny", "LabelUseBookshelfView": "Použít vzhled knihovny",
"LabelUser": "Uživatel", "LabelUser": "Uživatel",
"LabelUsername": "Uživatelské jméno", "LabelUsername": "Uživatelské jméno",
"LabelVeryHigh": "Velmi vysoké", "LabelVeryHigh": "Velmi vysoké",
"LabelVeryLow": "Velmi nízké", "LabelVeryLow": "Velmi nízké",
"LabelYearReviewHide": "Skrýt rok v přehledu",
"LabelYearReviewShow": "Zobrazit rok v přehledu",
"LabelYourBookmarks": "Vaše záložky", "LabelYourBookmarks": "Vaše záložky",
"LabelYourProgress": "Váš pokrok", "LabelYourProgress": "Váš pokrok",
"MessageAndroid10Downloads": "Android 10 a starší budou používat interní úložiště aplikace pro stahování.", "MessageAndroid10Downloads": "Android 10 a starší budou používat interní úložiště aplikace pro stahování.",
@@ -282,6 +291,7 @@
"MessageMediaNotLinkedToServer": "Média nejsou propojena se serverem Audiobookshelf. Pokrok nebude synchronizován.", "MessageMediaNotLinkedToServer": "Média nejsou propojena se serverem Audiobookshelf. Pokrok nebude synchronizován.",
"MessageNoBookmarks": "Žádné záložky", "MessageNoBookmarks": "Žádné záložky",
"MessageNoChapters": "Žádné kapitoly", "MessageNoChapters": "Žádné kapitoly",
"MessageNoCollections": "Žádné kolekce",
"MessageNoItems": "Žádné položky", "MessageNoItems": "Žádné položky",
"MessageNoItemsFound": "Nebyly nalezeny žádné položky", "MessageNoItemsFound": "Nebyly nalezeny žádné položky",
"MessageNoListeningSessions": "Žádné poslechové relace", "MessageNoListeningSessions": "Žádné poslechové relace",
@@ -291,7 +301,11 @@
"MessageNoSeries": "Žádné série", "MessageNoSeries": "Žádné série",
"MessageNoUpdatesWereNecessary": "Nebyly nutné žádné aktualizace", "MessageNoUpdatesWereNecessary": "Nebyly nutné žádné aktualizace",
"MessageNoUserPlaylists": "Nemáte žádné seznamy skladeb", "MessageNoUserPlaylists": "Nemáte žádné seznamy skladeb",
"MessagePodcastSearchField": "Zadejte hledaný výraz nebo adresu URL kanálu RSS",
"MessageReportBugsAndContribute": "Nahlašte chyby, vyžádejte si funkce a přispěte na", "MessageReportBugsAndContribute": "Nahlašte chyby, vyžádejte si funkce a přispěte na",
"MessageSeriesAlreadyDownloaded": "Všechny knihy v této sérii jste již stáhli.",
"MessageSeriesDownloadConfirm": "Stáhnout chybějící {0} knihu(y) s {1} souborem(y), celkem {2} do složky {3}?",
"MessageSeriesDownloadConfirmIos": "Stáhnout chybějící {0} knihu(y) s {1} souborem(y), celkem {2}?",
"MessageSocketConnectedOverMeteredCellular": "Socket připojen přes měřenou mobilní síť", "MessageSocketConnectedOverMeteredCellular": "Socket připojen přes měřenou mobilní síť",
"MessageSocketConnectedOverMeteredWifi": "Socket připojen přes měřenou Wi-Fi", "MessageSocketConnectedOverMeteredWifi": "Socket připojen přes měřenou Wi-Fi",
"MessageSocketConnectedOverUnmeteredCellular": "Socket připojen přes neměřenou mobilní síť", "MessageSocketConnectedOverUnmeteredCellular": "Socket připojen přes neměřenou mobilní síť",
+87 -3
View File
@@ -37,6 +37,8 @@
"ButtonPlaying": "Afspiller", "ButtonPlaying": "Afspiller",
"ButtonPlaylists": "Afspilningslister", "ButtonPlaylists": "Afspilningslister",
"ButtonRead": "Læs", "ButtonRead": "Læs",
"ButtonReadLess": "Læs Mindre",
"ButtonReadMore": "Læs Mere",
"ButtonRemove": "Fjern", "ButtonRemove": "Fjern",
"ButtonRemoveFromServer": "Fjern fra Server", "ButtonRemoveFromServer": "Fjern fra Server",
"ButtonSave": "Gem", "ButtonSave": "Gem",
@@ -52,6 +54,7 @@
"ButtonYes": "Ja", "ButtonYes": "Ja",
"HeaderAccount": "Konto", "HeaderAccount": "Konto",
"HeaderAdvanced": "Avanceret", "HeaderAdvanced": "Avanceret",
"HeaderAndroidAutoSettings": "Android Auto Indstillinger",
"HeaderAudioTracks": "Lydspor", "HeaderAudioTracks": "Lydspor",
"HeaderChapters": "Kapitler", "HeaderChapters": "Kapitler",
"HeaderCollection": "Samling", "HeaderCollection": "Samling",
@@ -85,11 +88,14 @@
"HeaderUserInterfaceSettings": "Indstillinger for brugergrænseflade", "HeaderUserInterfaceSettings": "Indstillinger for brugergrænseflade",
"HeaderYourStats": "Dine Statistikker", "HeaderYourStats": "Dine Statistikker",
"LabelAddToPlaylist": "Tilføj til Afspilningsliste", "LabelAddToPlaylist": "Tilføj til Afspilningsliste",
"LabelAdded": "Tilføjet",
"LabelAddedAt": "Tilføjet Kl", "LabelAddedAt": "Tilføjet Kl",
"LabelAddedDate": "Tilføjet {0}",
"LabelAll": "Alle", "LabelAll": "Alle",
"LabelAllowSeekingOnMediaControls": "Tillad positionssøgning på mediemeddelelseskontroller", "LabelAllowSeekingOnMediaControls": "Tillad positionssøgning på mediemeddelelseskontroller",
"LabelAlways": "Altid", "LabelAlways": "Altid",
"LabelAndroidAutoBrowseLimitForGrouping": "Alfabetisk Rullemenu Begrænsning",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "Brug ikke alfabetisk rullemenu når der er færre end dette antal elementer at vise",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "Bogserie Rækkefølge",
"LabelAskConfirmation": "Bed om bekræftelse", "LabelAskConfirmation": "Bed om bekræftelse",
"LabelAuthor": "Forfatter", "LabelAuthor": "Forfatter",
"LabelAuthorFirstLast": "Forfatter (Fornavn Efternavn)", "LabelAuthorFirstLast": "Forfatter (Fornavn Efternavn)",
@@ -154,43 +160,79 @@
"LabelHost": "Vært", "LabelHost": "Vært",
"LabelInProgress": "I gang", "LabelInProgress": "I gang",
"LabelIncomplete": "Ufuldstændig", "LabelIncomplete": "Ufuldstændig",
"LabelInternalAppStorage": "Intern Lagerplads",
"LabelJumpBackwardsTime": "Spring tilbage tid", "LabelJumpBackwardsTime": "Spring tilbage tid",
"LabelJumpForwardsTime": "Spring frem tid", "LabelJumpForwardsTime": "Spring frem tid",
"LabelLanguage": "Sprog", "LabelLanguage": "Sprog",
"LabelLayout": "Layout", "LabelLayout": "Layout",
"LabelLayoutAuto": "Automatisk",
"LabelLayoutSinglePage": "Enkel Side",
"LabelLight": "Lys",
"LabelLineSpacing": "Linjeafstand", "LabelLineSpacing": "Linjeafstand",
"LabelListenAgain": "Lyt Igen",
"LabelLocalBooks": "Lokale Bøger",
"LabelLocalPodcasts": "Lokale Podcasts",
"LabelLockOrientation": "Lås Orientering",
"LabelLockPlayer": "Lås afspiller", "LabelLockPlayer": "Lås afspiller",
"LabelLow": "Lav",
"LabelMediaType": "Medietype", "LabelMediaType": "Medietype",
"LabelMedium": "Mellem",
"LabelMore": "Mere", "LabelMore": "Mere",
"LabelMoreInfo": "Mere info", "LabelMoreInfo": "Mere info",
"LabelName": "Navn", "LabelName": "Navn",
"LabelNarrator": "Fortæller", "LabelNarrator": "Fortæller",
"LabelNarrators": "Fortællere", "LabelNarrators": "Fortællere",
"LabelNavigateWithVolume": "Naviger med lydstyrketasterne",
"LabelNavigateWithVolumeMirrored": "Spejlet",
"LabelNavigateWithVolumeWhilePlaying": "Tillad navigering med lydstyrketasterne , mens du spiller",
"LabelNavigateWithVolumeWhilePlayingDisabled": "Slukket",
"LabelNavigateWithVolumeWhilePlayingEnabled": "Tændt",
"LabelNever": "Aldrig", "LabelNever": "Aldrig",
"LabelNewestAuthors": "Nyeste Forfattere",
"LabelNewestEpisodes": "Nyeste Episoder",
"LabelNo": "Nej",
"LabelNotFinished": "Ikke færdig", "LabelNotFinished": "Ikke færdig",
"LabelNotStarted": "Ikke påbegyndt", "LabelNotStarted": "Ikke påbegyndt",
"LabelNumEpisodes": "{0} episoder",
"LabelOff": "Slukket",
"LabelOn": "Tændt",
"LabelPassword": "Kodeord", "LabelPassword": "Kodeord",
"LabelPath": "Sti", "LabelPath": "Sti",
"LabelPlaybackDirect": "Direkte",
"LabelPlaybackLocal": "Lokal",
"LabelPlaybackSpeed": "Afspilningshastighed", "LabelPlaybackSpeed": "Afspilningshastighed",
"LabelPlaybackTranscode": "Transcode", "LabelPlaybackTranscode": "Transcode",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPreventIndexing": "Forhindrer, at dit feed bliver indekseret af iTunes og Google podcastkataloger", "LabelPreventIndexing": "Forhindrer, at dit feed bliver indekseret af iTunes og Google podcastkataloger",
"LabelProgress": "Fremskridt", "LabelProgress": "Fremskridt",
"LabelPubDate": "Udgivelsesdato", "LabelPubDate": "Udgivelsesdato",
"LabelPublishYear": "Udgivelsesår", "LabelPublishYear": "Udgivelsesår",
"LabelPublishedDate": "Udgivet {0}",
"LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail", "LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail",
"LabelRSSFeedCustomOwnerName": "Brugerdefineret ejerens navn", "LabelRSSFeedCustomOwnerName": "Brugerdefineret ejerens navn",
"LabelRSSFeedPreventIndexing": "Forhindrer indeksering", "LabelRSSFeedPreventIndexing": "Forhindrer indeksering",
"LabelRSSFeedSlug": "RSS-feed-slug", "LabelRSSFeedSlug": "RSS-feed-slug",
"LabelRandomly": "Tilfældigt", "LabelRandomly": "Tilfældigt",
"LabelRead": "Læst", "LabelRead": "Læst",
"LabelReadAgain": "Læs Igen",
"LabelRecentSeries": "Seneste serier", "LabelRecentSeries": "Seneste serier",
"LabelRecentlyAdded": "Nyligt Tilføjet",
"LabelRemoveFromPlaylist": "Fjern fra Playliste",
"LabelScaleElapsedTimeBySpeed": "Skalér Forløbet Tid med Hastighed",
"LabelSeason": "Sæson", "LabelSeason": "Sæson",
"LabelSelectADevice": "Vælg en Enhed",
"LabelSequenceAscending": "Sekvens Stigende",
"LabelSequenceDescending": "Sekvens Faldende",
"LabelSeries": "Serie", "LabelSeries": "Serie",
"LabelServerAddress": "Server Adresse",
"LabelSetEbookAsPrimary": "Indstil som primær", "LabelSetEbookAsPrimary": "Indstil som primær",
"LabelSetEbookAsSupplementary": "Indstil som supplerende", "LabelSetEbookAsSupplementary": "Indstil som supplerende",
"LabelShakeSensitivity": "Vibrations Sensitivity",
"LabelShowAll": "Vis alle", "LabelShowAll": "Vis alle",
"LabelSize": "Størrelse", "LabelSize": "Størrelse",
"LabelSleepTimer": "Søvntimer", "LabelSleepTimer": "Søvntimer",
"LabelStart": "Start",
"LabelStartTime": "Starttid", "LabelStartTime": "Starttid",
"LabelStatsBestDay": "Bedste dag", "LabelStatsBestDay": "Bedste dag",
"LabelStatsDailyAverage": "Daglig gennemsnit", "LabelStatsDailyAverage": "Daglig gennemsnit",
@@ -201,6 +243,7 @@
"LabelStatsMinutes": "minutter", "LabelStatsMinutes": "minutter",
"LabelStatsMinutesListening": "Minutter hørt", "LabelStatsMinutesListening": "Minutter hørt",
"LabelStatsWeekListening": "Ugens lytning", "LabelStatsWeekListening": "Ugens lytning",
"LabelStreamingUsingCellular": "Streaming ved brug af Mobildata",
"LabelTag": "Mærke", "LabelTag": "Mærke",
"LabelTags": "Mærker", "LabelTags": "Mærker",
"LabelTheme": "Tema", "LabelTheme": "Tema",
@@ -208,11 +251,33 @@
"LabelThemeLight": "Lys", "LabelThemeLight": "Lys",
"LabelTimeRemaining": "{0} tilbage", "LabelTimeRemaining": "{0} tilbage",
"LabelTitle": "Titel", "LabelTitle": "Titel",
"LabelTotalSize": "Total Størrelse",
"LabelTotalTrack": "Total Antal Spor",
"LabelTracks": "Spor", "LabelTracks": "Spor",
"LabelType": "Type",
"LabelUnknown": "Ukendt",
"LabelUnlockPlayer": "Lås Spiller Op",
"LabelUseBookshelfView": "Brug Bogreols Udseende",
"LabelUser": "Bruger", "LabelUser": "Bruger",
"LabelUsername": "Brugernavn", "LabelUsername": "Brugernavn",
"LabelVeryHigh": "Meget Højt",
"LabelVeryLow": "Meget Lavt",
"LabelYearReviewHide": "Skjul Gennemgang af Året",
"LabelYearReviewShow": "Se Gennnemgang af Året",
"LabelYourBookmarks": "Dine bogmærker", "LabelYourBookmarks": "Dine bogmærker",
"LabelYourProgress": "Din fremgang", "LabelYourProgress": "Din fremgang",
"MessageAndroid10Downloads": "Android 10 og mindre vil bruge intern lagerplads til downloads.",
"MessageAttemptingServerConnection": "Prøver at tilgå server...",
"MessageAudiobookshelfServerNotConnected": "Audiobookshel server er ikke tilsluttet",
"MessageAudiobookshelfServerRequired": "<strong>Vigtig</strong> Denne app er designet til at fungere med en Audiobookshelf server, som du eller en du kender hoster. Denne app levere ikke noget indhold.",
"MessageBookshelfEmpty": "Bogreol Tom",
"MessageConfirmDeleteLocalEpisode": "Slet lokal episode \"{0}\" fra din enhed? Filen på serveren vil ikke blive påvirket.",
"MessageConfirmDeleteLocalFiles": "Slet lokale filer af denne type fra din enhed? Filerne på serveren og din fremgang vil ikke blive påvirket.",
"MessageConfirmDiscardProgress": "Er du sikker på at du vil nulstille din fremgang?",
"MessageConfirmDownloadUsingCellular": "Du er ved at downloade via mobildata. Dette kan medføre ekstra omkostninger fra din teleoperatør. Vil du fortsætte?",
"MessageConfirmMarkAsFinished": "Er du sikker på at du vil markere dette element færdig?",
"MessageConfirmRemoveBookmark": "Er du sikker på at du vil fjerne bogmærke?",
"MessageConfirmStreamingUsingCellular": "Du er ved at streame via mobildata. Dette kan medføre ekstra omkostninger fra din teleoperatør. Vil du fortsætte?",
"MessageDiscardProgress": "Kassér fremgang", "MessageDiscardProgress": "Kassér fremgang",
"MessageDownloadCompleteProcessing": "Download fuldført. Behandler...", "MessageDownloadCompleteProcessing": "Download fuldført. Behandler...",
"MessageDownloading": "Downloader...", "MessageDownloading": "Downloader...",
@@ -220,31 +285,50 @@
"MessageEpisodesQueuedForDownload": "{0} episoder er sat i kø til download", "MessageEpisodesQueuedForDownload": "{0} episoder er sat i kø til download",
"MessageFeedURLWillBe": "Feed-URL vil være {0}", "MessageFeedURLWillBe": "Feed-URL vil være {0}",
"MessageFetching": "Henter...", "MessageFetching": "Henter...",
"MessageFollowTheProjectOnGithub": "Follow the project on GitHub", "MessageFollowTheProjectOnGithub": "Følg projektet på GitHub",
"MessageItemDownloadCompleteFailedToCreate": "Download af element er fuldført, men bibliotekselementet blev ikke oprettet",
"MessageLoading": "Indlæser...", "MessageLoading": "Indlæser...",
"MessageLoadingServerData": "Indlæser server information...",
"MessageMarkAsFinished": "Markér som afsluttet", "MessageMarkAsFinished": "Markér som afsluttet",
"MessageMediaLinkedToADifferentServer": "Mediet er tilsluttet en anden Audiobookshelf server på en anden adresse ({0}). Fremgang vil blive synkroniseret, når denne server adresse bliver tilsluttet.",
"MessageMediaLinkedToADifferentUser": "Mediet er tilsluttet til denne server, men var downloadet af en anden bruger. Fremgang vil blive synkroniseret, til den bruger der har downloadet dette.",
"MessageMediaLinkedToServer": "Tilsluttet server {0}",
"MessageMediaLinkedToThisServer": "Downloadet medier er tilsluttet til denne server",
"MessageMediaNotLinkedToServer": "Medie er ikke tilsluttet nogen Audiobookshelf server. Fremgang til ikke blive synkroniseret.",
"MessageNoBookmarks": "Ingen bogmærker", "MessageNoBookmarks": "Ingen bogmærker",
"MessageNoChapters": "Ingen kapitler", "MessageNoChapters": "Ingen kapitler",
"MessageNoCollections": "Ingen Samlinger",
"MessageNoItems": "Ingen elementer", "MessageNoItems": "Ingen elementer",
"MessageNoItemsFound": "Ingen elementer fundet", "MessageNoItemsFound": "Ingen elementer fundet",
"MessageNoListeningSessions": "Ingen lyttesessioner", "MessageNoListeningSessions": "Ingen lyttesessioner",
"MessageNoMediaFolders": "Ingen Mediemapper",
"MessageNoNetworkConnection": "Ingen Internetforbindelse",
"MessageNoPodcastsFound": "Ingen podcasts fundet", "MessageNoPodcastsFound": "Ingen podcasts fundet",
"MessageNoSeries": "Ingen serier", "MessageNoSeries": "Ingen serier",
"MessageNoUpdatesWereNecessary": "Ingen opdateringer var nødvendige", "MessageNoUpdatesWereNecessary": "Ingen opdateringer var nødvendige",
"MessageNoUserPlaylists": "Du har ingen afspilningslister", "MessageNoUserPlaylists": "Du har ingen afspilningslister",
"MessagePodcastSearchField": "Indtast søgeterm eller RSS-feed URL",
"MessageReportBugsAndContribute": "Rapporter fejl, anmod om funktioner og bidrag på", "MessageReportBugsAndContribute": "Rapporter fejl, anmod om funktioner og bidrag på",
"MessageSeriesAlreadyDownloaded": "Du har allerede downloadet alle bøgerne i denne serie.",
"MessageSeriesDownloadConfirm": "Download mangler {0} bog/bøger med {1} fil(er), i alt {2} til mappe {3}?",
"MessageSeriesDownloadConfirmIos": "Download mangler {0} bog/bøger med {1} fil(er), i alt {2}?",
"MessageSocketConnectedOverMeteredCellular": "Socket forbundet via mobildata",
"MessageSocketConnectedOverMeteredWifi": "Socket connected over metered Wi-Fi", "MessageSocketConnectedOverMeteredWifi": "Socket connected over metered Wi-Fi",
"MessageSocketConnectedOverUnmeteredCellular": "Socket forbundet via ubegrænset mobildata",
"MessageSocketConnectedOverUnmeteredWifi": "Socket connected over unmetered Wi-Fi", "MessageSocketConnectedOverUnmeteredWifi": "Socket connected over unmetered Wi-Fi",
"MessageSocketNotConnected": "Socket ikke tilsluttet",
"NoteRSSFeedPodcastAppsHttps": "Advarsel: De fleste podcast-apps kræver, at RSS-feedets URL bruger HTTPS", "NoteRSSFeedPodcastAppsHttps": "Advarsel: De fleste podcast-apps kræver, at RSS-feedets URL bruger HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Advarsel: En eller flere af dine episoder har ikke en Pub Date. Nogle podcast-apps kræver dette.", "NoteRSSFeedPodcastAppsPubDate": "Advarsel: En eller flere af dine episoder har ikke en Pub Date. Nogle podcast-apps kræver dette.",
"ToastBookmarkCreateFailed": "Mislykkedes oprettelse af bogmærke", "ToastBookmarkCreateFailed": "Mislykkedes oprettelse af bogmærke",
"ToastBookmarkRemoveFailed": "Mislykkedes fjernelse af bogmærke", "ToastBookmarkRemoveFailed": "Mislykkedes fjernelse af bogmærke",
"ToastBookmarkUpdateFailed": "Mislykkedes opdatering af bogmærke", "ToastBookmarkUpdateFailed": "Mislykkedes opdatering af bogmærke",
"ToastDownloadNotAllowedOnCellular": "Det er ikke tillad at downloade over mobildata",
"ToastItemMarkedAsFinishedFailed": "Mislykkedes markering som afsluttet", "ToastItemMarkedAsFinishedFailed": "Mislykkedes markering som afsluttet",
"ToastItemMarkedAsNotFinishedFailed": "Mislykkedes markering som ikke afsluttet", "ToastItemMarkedAsNotFinishedFailed": "Mislykkedes markering som ikke afsluttet",
"ToastPlaylistCreateFailed": "Mislykkedes oprettelse af afspilningsliste", "ToastPlaylistCreateFailed": "Mislykkedes oprettelse af afspilningsliste",
"ToastPodcastCreateFailed": "Mislykkedes oprettelse af podcast", "ToastPodcastCreateFailed": "Mislykkedes oprettelse af podcast",
"ToastPodcastCreateSuccess": "Podcast oprettet med succes", "ToastPodcastCreateSuccess": "Podcast oprettet med succes",
"ToastRSSFeedCloseFailed": "Mislykkedes lukning af RSS-feed", "ToastRSSFeedCloseFailed": "Mislykkedes lukning af RSS-feed",
"ToastRSSFeedCloseSuccess": "RSS-feed lukket" "ToastRSSFeedCloseSuccess": "RSS-feed lukket",
"ToastStreamingNotAllowedOnCellular": "Det er ikke tillad at streame over mobildata"
} }
+15 -3
View File
@@ -37,6 +37,8 @@
"ButtonPlaying": "Spielt", "ButtonPlaying": "Spielt",
"ButtonPlaylists": "Wiedergabelisten", "ButtonPlaylists": "Wiedergabelisten",
"ButtonRead": "Lesen", "ButtonRead": "Lesen",
"ButtonReadLess": "weniger Anzeigen",
"ButtonReadMore": "Mehr Anzeigen",
"ButtonRemove": "Entfernen", "ButtonRemove": "Entfernen",
"ButtonRemoveFromServer": "Vom Server entfernen", "ButtonRemoveFromServer": "Vom Server entfernen",
"ButtonSave": "Speichern", "ButtonSave": "Speichern",
@@ -52,6 +54,7 @@
"ButtonYes": "Ja", "ButtonYes": "Ja",
"HeaderAccount": "Konto", "HeaderAccount": "Konto",
"HeaderAdvanced": "Erweitert", "HeaderAdvanced": "Erweitert",
"HeaderAndroidAutoSettings": "Android Auto Einstellungen",
"HeaderAudioTracks": "Audiodateien", "HeaderAudioTracks": "Audiodateien",
"HeaderChapters": "Kapitel", "HeaderChapters": "Kapitel",
"HeaderCollection": "Sammlung", "HeaderCollection": "Sammlung",
@@ -85,8 +88,8 @@
"HeaderUserInterfaceSettings": "Einstellungen der Benutzeroberfläche", "HeaderUserInterfaceSettings": "Einstellungen der Benutzeroberfläche",
"HeaderYourStats": "Eigene Statistiken", "HeaderYourStats": "Eigene Statistiken",
"LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen", "LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen",
"LabelAdded": "Hinzugefügt",
"LabelAddedAt": "Hinzugefügt am", "LabelAddedAt": "Hinzugefügt am",
"LabelAddedDate": "{0} Hinzugefügt",
"LabelAll": "Alle", "LabelAll": "Alle",
"LabelAllowSeekingOnMediaControls": "Erlaube Vor- und Zurückspulen auf dem Medienkontrollelement bei den Benachrichtigungen", "LabelAllowSeekingOnMediaControls": "Erlaube Vor- und Zurückspulen auf dem Medienkontrollelement bei den Benachrichtigungen",
"LabelAlways": "Immer", "LabelAlways": "Immer",
@@ -177,16 +180,19 @@
"LabelNarrator": "Erzähler", "LabelNarrator": "Erzähler",
"LabelNarrators": "Erzähler", "LabelNarrators": "Erzähler",
"LabelNavigateWithVolume": "Mit Lautstärke blättern", "LabelNavigateWithVolume": "Mit Lautstärke blättern",
"LabelNavigateWithVolumeDisabled": "Aus",
"LabelNavigateWithVolumeEnabled": "An",
"LabelNavigateWithVolumeMirrored": "Umgekehrt", "LabelNavigateWithVolumeMirrored": "Umgekehrt",
"LabelNavigateWithVolumeWhilePlaying": "Auch bei Wiedergabe mit Lautstärketasten blättern",
"LabelNavigateWithVolumeWhilePlayingDisabled": "Aus",
"LabelNavigateWithVolumeWhilePlayingEnabled": "An",
"LabelNever": "Nie", "LabelNever": "Nie",
"LabelNewestAuthors": "Neueste Autoren", "LabelNewestAuthors": "Neueste Autoren",
"LabelNewestEpisodes": "Neueste Episoden", "LabelNewestEpisodes": "Neueste Episoden",
"LabelNo": "Nein", "LabelNo": "Nein",
"LabelNotFinished": "Nicht beendet", "LabelNotFinished": "Nicht beendet",
"LabelNotStarted": "Nicht begonnen", "LabelNotStarted": "Nicht begonnen",
"LabelNumEpisodes": "{0} Episoden",
"LabelOff": "Aus", "LabelOff": "Aus",
"LabelOn": "An",
"LabelPassword": "Passwort", "LabelPassword": "Passwort",
"LabelPath": "Pfad", "LabelPath": "Pfad",
"LabelPlaybackDirect": "Direkt", "LabelPlaybackDirect": "Direkt",
@@ -199,6 +205,7 @@
"LabelProgress": "Fortschritt", "LabelProgress": "Fortschritt",
"LabelPubDate": "Veröffentlichungsdatum", "LabelPubDate": "Veröffentlichungsdatum",
"LabelPublishYear": "Jahr", "LabelPublishYear": "Jahr",
"LabelPublishedDate": "Veröffentlicht {0}",
"LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail", "LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
"LabelRSSFeedCustomOwnerName": "Benutzerdefinierter Name des Eigentümers", "LabelRSSFeedCustomOwnerName": "Benutzerdefinierter Name des Eigentümers",
"LabelRSSFeedPreventIndexing": "Indizierung verhindern", "LabelRSSFeedPreventIndexing": "Indizierung verhindern",
@@ -243,12 +250,15 @@
"LabelTotalTrack": "Gesamtanzahl Titel", "LabelTotalTrack": "Gesamtanzahl Titel",
"LabelTracks": "Titel", "LabelTracks": "Titel",
"LabelType": "Typ", "LabelType": "Typ",
"LabelUnknown": "Unbekannt",
"LabelUnlockPlayer": "Mediaplayer entsperren", "LabelUnlockPlayer": "Mediaplayer entsperren",
"LabelUseBookshelfView": "Benutze Bücherregalansicht", "LabelUseBookshelfView": "Benutze Bücherregalansicht",
"LabelUser": "Benutzer", "LabelUser": "Benutzer",
"LabelUsername": "Benutzername", "LabelUsername": "Benutzername",
"LabelVeryHigh": "Sehr hoch", "LabelVeryHigh": "Sehr hoch",
"LabelVeryLow": "Sehr niedrig", "LabelVeryLow": "Sehr niedrig",
"LabelYearReviewHide": "Jahresrückblick verbergen",
"LabelYearReviewShow": "Jahresrückblick anzeigen",
"LabelYourBookmarks": "Lesezeichen", "LabelYourBookmarks": "Lesezeichen",
"LabelYourProgress": "Fortschritt", "LabelYourProgress": "Fortschritt",
"MessageAndroid10Downloads": "Bei Android 10 und darunter wird der interne App-Speicher für Downloads verwendet.", "MessageAndroid10Downloads": "Bei Android 10 und darunter wird der interne App-Speicher für Downloads verwendet.",
@@ -282,6 +292,7 @@
"MessageMediaNotLinkedToServer": "Die Medien sind nicht mit einem Audiobookshelf-Server verknüpft. Es wird kein Fortschritt synchronisiert.", "MessageMediaNotLinkedToServer": "Die Medien sind nicht mit einem Audiobookshelf-Server verknüpft. Es wird kein Fortschritt synchronisiert.",
"MessageNoBookmarks": "Keine Lesezeichen", "MessageNoBookmarks": "Keine Lesezeichen",
"MessageNoChapters": "Keine Kapitel", "MessageNoChapters": "Keine Kapitel",
"MessageNoCollections": "Keine Sammlungen",
"MessageNoItems": "Keine Medien", "MessageNoItems": "Keine Medien",
"MessageNoItemsFound": "Keine Medien gefunden", "MessageNoItemsFound": "Keine Medien gefunden",
"MessageNoListeningSessions": "Keine Hörsitzungen", "MessageNoListeningSessions": "Keine Hörsitzungen",
@@ -291,6 +302,7 @@
"MessageNoSeries": "Keine Serie", "MessageNoSeries": "Keine Serie",
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig", "MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
"MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden", "MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden",
"MessagePodcastSearchField": "Suchbegriff oder RSS-Feed URL eingeben",
"MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und mitwirken", "MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und mitwirken",
"MessageSeriesAlreadyDownloaded": "Du hast bereits alle Bücher in dieser Serie heruntergeladen.", "MessageSeriesAlreadyDownloaded": "Du hast bereits alle Bücher in dieser Serie heruntergeladen.",
"MessageSeriesDownloadConfirm": "Lade {0} Bücher mit {1} Daten, zusammen {2}, in den Ordner {3} herunter?", "MessageSeriesDownloadConfirm": "Lade {0} Bücher mit {1} Daten, zusammen {2}, in den Ordner {3} herunter?",
+21 -3
View File
@@ -37,6 +37,8 @@
"ButtonPlaying": "Playing", "ButtonPlaying": "Playing",
"ButtonPlaylists": "Playlists", "ButtonPlaylists": "Playlists",
"ButtonRead": "Read", "ButtonRead": "Read",
"ButtonReadLess": "Read less",
"ButtonReadMore": "Read more",
"ButtonRemove": "Remove", "ButtonRemove": "Remove",
"ButtonRemoveFromServer": "Remove from Server", "ButtonRemoveFromServer": "Remove from Server",
"ButtonSave": "Save", "ButtonSave": "Save",
@@ -52,6 +54,7 @@
"ButtonYes": "Yes", "ButtonYes": "Yes",
"HeaderAccount": "Account", "HeaderAccount": "Account",
"HeaderAdvanced": "Advanced", "HeaderAdvanced": "Advanced",
"HeaderAndroidAutoSettings": "Android Auto Settings",
"HeaderAudioTracks": "Audio Tracks", "HeaderAudioTracks": "Audio Tracks",
"HeaderChapters": "Chapters", "HeaderChapters": "Chapters",
"HeaderCollection": "Collection", "HeaderCollection": "Collection",
@@ -85,11 +88,14 @@
"HeaderUserInterfaceSettings": "User Interface Settings", "HeaderUserInterfaceSettings": "User Interface Settings",
"HeaderYourStats": "Your Stats", "HeaderYourStats": "Your Stats",
"LabelAddToPlaylist": "Add to Playlist", "LabelAddToPlaylist": "Add to Playlist",
"LabelAdded": "Added",
"LabelAddedAt": "Added At", "LabelAddedAt": "Added At",
"LabelAddedDate": "Added {0}",
"LabelAll": "All", "LabelAll": "All",
"LabelAllowSeekingOnMediaControls": "Allow position seeking on media notification controls", "LabelAllowSeekingOnMediaControls": "Allow position seeking on media notification controls",
"LabelAlways": "Always", "LabelAlways": "Always",
"LabelAndroidAutoBrowseLimitForGrouping": "Alphabetical drawdown limit",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "Don't use alphabetical drawdown when there is less than this amount of items to show",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "Series books order",
"LabelAskConfirmation": "Ask for confirmation", "LabelAskConfirmation": "Ask for confirmation",
"LabelAuthor": "Author", "LabelAuthor": "Author",
"LabelAuthorFirstLast": "Author (First Last)", "LabelAuthorFirstLast": "Author (First Last)",
@@ -157,6 +163,7 @@
"LabelInternalAppStorage": "Internal App Storage", "LabelInternalAppStorage": "Internal App Storage",
"LabelJumpBackwardsTime": "Jump backwards time", "LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time", "LabelJumpForwardsTime": "Jump forwards time",
"LabelKeepScreenAwake": "Keep screen awake",
"LabelLanguage": "Language", "LabelLanguage": "Language",
"LabelLayout": "Layout", "LabelLayout": "Layout",
"LabelLayoutAuto": "Auto", "LabelLayoutAuto": "Auto",
@@ -177,16 +184,19 @@
"LabelNarrator": "Narrator", "LabelNarrator": "Narrator",
"LabelNarrators": "Narrators", "LabelNarrators": "Narrators",
"LabelNavigateWithVolume": "Navigate with volume keys", "LabelNavigateWithVolume": "Navigate with volume keys",
"LabelNavigateWithVolumeDisabled": "Off",
"LabelNavigateWithVolumeEnabled": "On",
"LabelNavigateWithVolumeMirrored": "Mirrored", "LabelNavigateWithVolumeMirrored": "Mirrored",
"LabelNavigateWithVolumeWhilePlaying": "Allow volume keys to navigate while playing",
"LabelNavigateWithVolumeWhilePlayingDisabled": "Off",
"LabelNavigateWithVolumeWhilePlayingEnabled": "On",
"LabelNever": "Never", "LabelNever": "Never",
"LabelNewestAuthors": "Newest Authors", "LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes", "LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No", "LabelNo": "No",
"LabelNotFinished": "Not Finished", "LabelNotFinished": "Not Finished",
"LabelNotStarted": "Not Started", "LabelNotStarted": "Not Started",
"LabelNumEpisodes": "{0} episodes",
"LabelOff": "Off", "LabelOff": "Off",
"LabelOn": "On",
"LabelPassword": "Password", "LabelPassword": "Password",
"LabelPath": "Path", "LabelPath": "Path",
"LabelPlaybackDirect": "Direct", "LabelPlaybackDirect": "Direct",
@@ -199,6 +209,7 @@
"LabelProgress": "Progress", "LabelProgress": "Progress",
"LabelPubDate": "Pub Date", "LabelPubDate": "Pub Date",
"LabelPublishYear": "Publish Year", "LabelPublishYear": "Publish Year",
"LabelPublishedDate": "Published {0}",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email", "LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
"LabelRSSFeedCustomOwnerName": "Custom owner Name", "LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedPreventIndexing": "Prevent Indexing", "LabelRSSFeedPreventIndexing": "Prevent Indexing",
@@ -212,6 +223,8 @@
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed", "LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Season", "LabelSeason": "Season",
"LabelSelectADevice": "Select a device", "LabelSelectADevice": "Select a device",
"LabelSequenceAscending": "Sequence Ascending",
"LabelSequenceDescending": "Sequence Descending",
"LabelSeries": "Series", "LabelSeries": "Series",
"LabelServerAddress": "Server address", "LabelServerAddress": "Server address",
"LabelSetEbookAsPrimary": "Set as primary", "LabelSetEbookAsPrimary": "Set as primary",
@@ -243,12 +256,15 @@
"LabelTotalTrack": "Total Track", "LabelTotalTrack": "Total Track",
"LabelTracks": "Tracks", "LabelTracks": "Tracks",
"LabelType": "Type", "LabelType": "Type",
"LabelUnknown": "Unknown",
"LabelUnlockPlayer": "Unlock Player", "LabelUnlockPlayer": "Unlock Player",
"LabelUseBookshelfView": "Use bookshelf view", "LabelUseBookshelfView": "Use bookshelf view",
"LabelUser": "User", "LabelUser": "User",
"LabelUsername": "Username", "LabelUsername": "Username",
"LabelVeryHigh": "Very High", "LabelVeryHigh": "Very High",
"LabelVeryLow": "Very Low", "LabelVeryLow": "Very Low",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourBookmarks": "Your Bookmarks", "LabelYourBookmarks": "Your Bookmarks",
"LabelYourProgress": "Your Progress", "LabelYourProgress": "Your Progress",
"MessageAndroid10Downloads": "Android 10 and below will use internal app storage for downloads.", "MessageAndroid10Downloads": "Android 10 and below will use internal app storage for downloads.",
@@ -282,6 +298,7 @@
"MessageMediaNotLinkedToServer": "Media is not linked to an Audiobookshelf server. No progress will be synced.", "MessageMediaNotLinkedToServer": "Media is not linked to an Audiobookshelf server. No progress will be synced.",
"MessageNoBookmarks": "No Bookmarks", "MessageNoBookmarks": "No Bookmarks",
"MessageNoChapters": "No Chapters", "MessageNoChapters": "No Chapters",
"MessageNoCollections": "No Collections",
"MessageNoItems": "No Items", "MessageNoItems": "No Items",
"MessageNoItemsFound": "No items found", "MessageNoItemsFound": "No items found",
"MessageNoListeningSessions": "No Listening Sessions", "MessageNoListeningSessions": "No Listening Sessions",
@@ -291,6 +308,7 @@
"MessageNoSeries": "No series", "MessageNoSeries": "No series",
"MessageNoUpdatesWereNecessary": "No updates were necessary", "MessageNoUpdatesWereNecessary": "No updates were necessary",
"MessageNoUserPlaylists": "You have no playlists", "MessageNoUserPlaylists": "You have no playlists",
"MessagePodcastSearchField": "Enter search term or RSS feed URL",
"MessageReportBugsAndContribute": "Report bugs, request features, and contribute on", "MessageReportBugsAndContribute": "Report bugs, request features, and contribute on",
"MessageSeriesAlreadyDownloaded": "You have already downloaded all books in this series.", "MessageSeriesAlreadyDownloaded": "You have already downloaded all books in this series.",
"MessageSeriesDownloadConfirm": "Download missing {0} book(s) with {1} file(s), totaling {2} to folder {3}?", "MessageSeriesDownloadConfirm": "Download missing {0} book(s) with {1} file(s), totaling {2} to folder {3}?",
-1
View File
@@ -1 +0,0 @@
{}

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