Compare commits

...
Author SHA1 Message Date
advplyr 8bc5932889 Version bump v0.9.76-beta 2024-08-13 10:26:08 -05:00
advplyrandGitHub 3cd597d71e Merge pull request #1286 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-08-13 10:19:04 -05:00
Fredrik LindqvistandHosted Weblate 48d5ad462e Translated using Weblate (Swedish)
Currently translated at 81.1% (249 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2024-08-13 17:18:39 +02:00
advplyr 40cab87640 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2024-08-13 10:18:28 -05:00
advplyr f405947ad8 Add:Random library sort #1285 2024-08-13 10:18:23 -05:00
advplyr fd550c15a5 iOS version bump 0.9.75 2024-08-12 17:38:44 -05:00
advplyr d18537cf04 Version bump v0.9.75-beta & targetSdkVersion bump to 34 2024-08-12 17:24:05 -05:00
advplyrandGitHub f01f0229a8 Merge pull request #1278 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-08-12 16:40:47 -05:00
CharlieandHosted Weblate b369eb2d40 Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-08-12 23:34:46 +02:00
CharlieandHosted Weblate f08c65c5bf Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-08-12 23:34:46 +02:00
tonttulaandHosted Weblate 1043f4ecd7 Translated using Weblate (Finnish)
Currently translated at 72.3% (222 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-08-12 23:34:46 +02:00
SunSpringandHosted Weblate 4f17a32373 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-08-12 23:34:46 +02:00
CharlieandHosted Weblate 2203aae6e6 Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-08-12 23:34:46 +02:00
advplyrandGitHub 7b60c7b6c5 Merge pull request #1279 from nichwall/release_close_issue
Add: workflow to automatically close issues on release
2024-08-12 16:34:41 -05:00
Nicholas Wallace be98e06efc Add: workflow to automatically close issues on release 2024-08-06 21:02:27 -07:00
advplyrandGitHub 45239bca34 Merge pull request #1258 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-08-04 10:09:21 -05:00
burghy86andHosted Weblate 2493ce7ed7 Translated using Weblate (Italian)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2024-07-26 04:45:02 +00:00
advplyr 2b1e53b371 Remove references to matrix server 2024-07-23 16:22:24 -05:00
weblate.user.1274andHosted Weblate d643a1769f Translated using Weblate (Norwegian Bokmål)
Currently translated at 84.0% (258 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2024-07-23 23:19:53 +02:00
MarioandHosted Weblate a63b7a188c Translated using Weblate (German)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-07-23 23:19:53 +02:00
gallegonovatoandHosted Weblate f9c0162b8a Translated using Weblate (Spanish)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-07-23 23:19:52 +02:00
Petteri HjortandHosted Weblate cda3cb3512 Translated using Weblate (Finnish)
Currently translated at 49.8% (153 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-07-23 23:19:52 +02:00
Marcin MartelaandHosted Weblate 766f6938a7 Translated using Weblate (Polish)
Currently translated at 96.0% (295 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2024-07-23 23:19:51 +02:00
tonttulaandHosted Weblate 8e1ccb517b Translated using Weblate (Finnish)
Currently translated at 48.2% (148 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-07-23 23:19:51 +02:00
MarioandHosted Weblate 540aab6f83 Translated using Weblate (German)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-07-23 23:19:50 +02:00
CharlieandHosted Weblate d1969d7897 Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-07-23 23:19:50 +02:00
AhetekandHosted Weblate b69e8fd649 Translated using Weblate (Polish)
Currently translated at 93.4% (287 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2024-07-23 23:19:49 +02:00
Lode SmetsandHosted Weblate 2e95cf908d Translated using Weblate (Dutch)
Currently translated at 64.8% (199 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2024-07-23 23:19:49 +02:00
MarioandHosted Weblate b589bae2b0 Translated using Weblate (German)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-07-23 23:19:48 +02:00
AhetekandHosted Weblate e7e4891d82 Translated using Weblate (Polish)
Currently translated at 93.4% (287 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2024-07-23 23:19:48 +02:00
advplyr 03a46decba Fix:Server connect form press enter to submit #1158 2024-07-08 17:21:31 -05:00
advplyrandGitHub d80482f443 Merge pull request #1250 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-06-28 15:50:19 -05:00
J. LavoieandHosted Weblate 735cce7614 Translated using Weblate (Finnish)
Currently translated at 39.0% (120 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-06-27 04:45:17 +00:00
J. LavoieandHosted Weblate 8ea35a0b36 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.3% (305 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2024-06-27 04:45:16 +00:00
J. LavoieandHosted Weblate ad90bd3d05 Translated using Weblate (Norwegian Bokmål)
Currently translated at 81.1% (249 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2024-06-27 04:45:15 +00:00
J. LavoieandHosted Weblate fa3856e845 Translated using Weblate (Italian)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2024-06-27 04:45:15 +00:00
dex girlandHosted Weblate 752d4ddc8c Translated using Weblate (Croatian)
Currently translated at 69.0% (212 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-06-27 04:45:05 +00:00
J. LavoieandHosted Weblate fd2d7a05d1 Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-06-27 04:45:04 +00:00
J. LavoieandHosted Weblate ccf4675867 Translated using Weblate (Spanish)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-06-27 04:45:03 +00:00
J. LavoieandHosted Weblate bc043c73ca Translated using Weblate (German)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-27 04:45:02 +00:00
Allan NordhøyandHosted Weblate 291ab247bc Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-06-25 16:34:50 +02:00
Allan NordhøyandHosted Weblate d901f470a1 Translated using Weblate (Vietnamese)
Currently translated at 85.6% (263 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/vi/
2024-06-25 16:34:50 +02:00
Allan NordhøyandHosted Weblate 0ebb01e39d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2024-06-25 16:34:50 +02:00
Allan NordhøyandHosted Weblate 20ac99bf8d Translated using Weblate (Swedish)
Currently translated at 59.2% (182 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2024-06-25 16:34:50 +02:00
Allan NordhøyandHosted Weblate 90e86e1575 Translated using Weblate (Russian)
Currently translated at 99.3% (305 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2024-06-25 16:34:50 +02:00
Allan NordhøyandHosted Weblate 28652031e7 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.3% (305 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2024-06-25 16:34:50 +02:00
Allan NordhøyandHosted Weblate cd8be40cc8 Translated using Weblate (Polish)
Currently translated at 90.2% (277 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2024-06-25 16:34:50 +02:00
Allan NordhøyandHosted Weblate 52fc937f96 Translated using Weblate (Norwegian Bokmål)
Currently translated at 81.1% (249 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate 68e4722387 Translated using Weblate (Dutch)
Currently translated at 57.0% (175 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate 5f5b38b2f9 Translated using Weblate (Lithuanian)
Currently translated at 59.2% (182 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/lt/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate 65f8d3ce81 Translated using Weblate (Italian)
Currently translated at 96.4% (296 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate ac571fd63d Translated using Weblate (Hungarian)
Currently translated at 91.5% (281 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate ea2d6e9f31 Translated using Weblate (Croatian)
Currently translated at 46.5% (143 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate 80b999d648 Translated using Weblate (Hindi)
Currently translated at 24.1% (74 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hi/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate 54720039e4 Translated using Weblate (Gujarati)
Currently translated at 23.4% (72 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/gu/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate 8c6717c201 Translated using Weblate (French)
Currently translated at 99.6% (306 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate 6610754e25 Translated using Weblate (Spanish)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate d38622d8a2 Translated using Weblate (German)
Currently translated at 99.6% (306 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-25 16:34:49 +02:00
Allan NordhøyandHosted Weblate cd2750a489 Translated using Weblate (Danish)
Currently translated at 64.1% (197 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2024-06-25 16:34:48 +02:00
Allan NordhøyandHosted Weblate 30140629ae Translated using Weblate (Czech)
Currently translated at 96.0% (295 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-06-25 16:34:48 +02:00
Allan NordhøyandHosted Weblate 0ce29337ef Translated using Weblate (Norwegian Bokmål)
Currently translated at 69.7% (214 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2024-06-25 15:36:41 +02:00
Allan NordhøyandHosted Weblate 6715f29ebb Translated using Weblate (Norwegian Bokmål)
Currently translated at 68.7% (211 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2024-06-25 13:32:12 +00:00
phewiandHosted Weblate b4a8dc9e26 Translated using Weblate (Finnish)
Currently translated at 36.4% (112 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-06-25 13:32:12 +00:00
advplyrandGitHub c349a0bf00 Merge pull request #1248 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-06-25 08:32:05 -05:00
SunSpringandHosted Weblate 0dc6c142be Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-06-21 04:45:03 +00:00
PlazecandHosted Weblate e46c1d9120 Translated using Weblate (Czech)
Currently translated at 96.4% (296 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-06-20 04:45:02 +00:00
advplyrandGitHub 27ad554ea1 Merge pull request #1245 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-06-19 15:18:59 -05:00
CharlieandHosted Weblate 5660772b46 Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-06-19 06:45:03 +02:00
kuci-JKandHosted Weblate dab80642ab Translated using Weblate (Czech)
Currently translated at 95.7% (294 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-06-19 06:45:02 +02:00
Nicholas WandHosted Weblate da46191208 Translated using Weblate (Bengali)
Currently translated at 57.6% (177 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bn/
2024-06-18 22:01:23 +02:00
JLandHosted Weblate 6484c4de79 Translated using Weblate (Danish)
Currently translated at 64.1% (197 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2024-06-18 22:01:23 +02:00
kuci-JKandHosted Weblate 6716b6042a Translated using Weblate (Czech)
Currently translated at 94.1% (289 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-06-18 22:01:23 +02:00
PlazecandHosted Weblate 0686859fa7 Translated using Weblate (Czech)
Currently translated at 94.1% (289 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-06-18 22:01:23 +02:00
advplyr 4180862c3a Fix: Sleep Timer modal defaults to Custom Time during first access #1224 2024-06-17 16:44:06 -05:00
PlazecandHosted Weblate 7aa20d3211 Translated using Weblate (Czech)
Currently translated at 92.8% (285 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-06-17 23:42:10 +02:00
pmangroandHosted Weblate b694b55710 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2024-06-17 23:42:10 +02:00
pmangroandHosted Weblate 024b613063 Translated using Weblate (Portuguese (Brazil))
Currently translated at 93.8% (288 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2024-06-17 23:42:10 +02:00
SmoukusandHosted Weblate 8768d11a20 Translated using Weblate (Croatian)
Currently translated at 46.5% (143 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-06-17 23:42:10 +02:00
ValentinandHosted Weblate 5898303ba4 Translated using Weblate (German)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-17 23:42:10 +02:00
advplyr e79d09941c Update:Editor config/vscode settings/prettierrc 2024-06-16 09:49:05 -05:00
advplyr c52a60fe77 Add:Finnish language option 2024-06-16 09:44:09 -05:00
advplyrandGitHub e64f9aa427 Merge pull request #1239 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-06-16 09:40:59 -05:00
Petteri HjortandHosted Weblate ee343f78ef Translated using Weblate (Finnish)
Currently translated at 27.3% (84 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-06-16 06:45:04 +02:00
Vito0912andHosted Weblate 1f20d5821f Translated using Weblate (German)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-16 06:45:03 +02:00
Nicholas WandHosted Weblate b0c512e946 Added translation using Weblate (Finnish) 2024-06-15 16:32:35 +02:00
CharlieandHosted Weblate 0d13b7abd1 Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-06-15 14:31:50 +00:00
SunSpringandHosted Weblate 43fd8285c9 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-06-15 06:45:09 +02:00
Illia PyshniakandHosted Weblate 2f7e8d618a Translated using Weblate (Ukrainian)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2024-06-15 06:45:08 +02:00
DmitryandHosted Weblate a0b910deeb Translated using Weblate (Russian)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2024-06-15 06:45:08 +02:00
Petras ŠukysandHosted Weblate e9dc45613c Translated using Weblate (Lithuanian)
Currently translated at 59.2% (182 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/lt/
2024-06-15 06:45:07 +02:00
Pierrick GuillaumeandHosted Weblate b5e90ec3d2 Translated using Weblate (French)
Currently translated at 87.9% (270 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-06-15 06:45:06 +02:00
DiamondtipDRandHosted Weblate 3d9f42c1e1 Translated using Weblate (Spanish)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-06-15 06:45:06 +02:00
SunSpringandHosted Weblate e62f430f37 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-06-14 11:19:29 +02:00
burghy86andHosted Weblate 9c53564c46 Translated using Weblate (Italian)
Currently translated at 96.7% (297 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2024-06-14 11:19:29 +02:00
Daniel SchosserandHosted Weblate f7add6f6cd Translated using Weblate (German)
Currently translated at 99.6% (306 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-14 11:19:29 +02:00
MarioandHosted Weblate 7786e6a827 Translated using Weblate (German)
Currently translated at 99.6% (306 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-14 11:19:29 +02:00
apineiro97andHosted Weblate b213e2aabf Translated using Weblate (Spanish)
Currently translated at 99.6% (306 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-06-14 06:45:03 +02:00
Vito0912andHosted Weblate ebca0fafca Translated using Weblate (German)
Currently translated at 95.1% (292 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-14 06:45:02 +02:00
advplyrandGitHub ac8a2ca985 Merge pull request #1235 from mfcar/mf/downloadTweaks
Tweak the download page (add library size and play episode)
2024-06-12 18:14:35 -05:00
advplyr 6380d7cc02 Fix local episode play-item event, set PodcastEpisode size and AudioTrack FileMetadata for downloads 2024-06-12 18:01:44 -05:00
advplyrandGitHub 9056ba6b8d Merge pull request #1234 from nichwall/localization_workflow_update
Update i18n workflow to 1.3.0
2024-06-11 17:03:21 -05:00
advplyrandGitHub 23801363c4 Merge pull request #1233 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-06-11 17:03:01 -05:00
Marcos Carvalho 1cdcd7327f Add play downloaded episode and library size 2024-06-11 19:31:10 +01:00
Nicholas Wallace 50ec50003e Update i18n workflow to 1.3.0 2024-06-10 19:25:34 -07:00
gallegonovatoandHosted Weblate 5ff4db1a3f Translated using Weblate (Spanish)
Currently translated at 100.0% (304 of 304 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-06-10 21:56:03 +00:00
advplyrandGitHub 394b44c343 Merge pull request #1222 from BimBimSalaBim/master
Update font settings in EpubReader and Reader components
2024-06-10 16:55:55 -05:00
advplyr eb2aa6f773 Map locally stored ebook settings onto defaults 2024-06-10 16:38:29 -05:00
advplyr 81130e04e5 Merge master 2024-06-10 16:32:09 -05:00
advplyrandGitHub c0ec0758f4 Merge pull request #1228 from weblate/weblate-audiobookshelf-test-abs-mobile-app
Translations update from Hosted Weblate
2024-06-10 16:23:07 -05:00
Nicholas WandHosted Weblate e21f7c545e Translated using Weblate (Chinese (Simplified))
Currently translated at 96.0% (292 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/zh_Hans/
2024-06-09 21:51:48 +02:00
Nicholas WandHosted Weblate c16dd2fcf6 Translated using Weblate (Vietnamese)
Currently translated at 86.5% (263 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/vi/
2024-06-09 21:51:48 +02:00
Nicholas WandHosted Weblate 90974e6d73 Translated using Weblate (Ukrainian)
Currently translated at 95.7% (291 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/uk/
2024-06-09 21:51:48 +02:00
Nicholas WandHosted Weblate 8e4f75d85b Translated using Weblate (Swedish)
Currently translated at 59.5% (181 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/sv/
2024-06-09 21:51:48 +02:00
Nicholas WandHosted Weblate 792ee2adb8 Translated using Weblate (Russian)
Currently translated at 94.0% (286 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/ru/
2024-06-09 21:51:47 +02:00
Nicholas WandHosted Weblate 2c57489c07 Translated using Weblate (Portuguese (Brazil))
Currently translated at 94.0% (286 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/pt_BR/
2024-06-09 21:51:47 +02:00
Nicholas WandHosted Weblate e3321770fd Translated using Weblate (Polish)
Currently translated at 91.4% (278 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/pl/
2024-06-09 21:51:47 +02:00
Nicholas WandHosted Weblate 848673f028 Translated using Weblate (Norwegian Bokmål)
Currently translated at 57.8% (176 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/nb_NO/
2024-06-09 21:51:47 +02:00
Nicholas WandHosted Weblate e6d3003ecf Translated using Weblate (Dutch)
Currently translated at 57.5% (175 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/nl/
2024-06-09 21:51:47 +02:00
Nicholas WandHosted Weblate 90551b7ce7 Translated using Weblate (Lithuanian)
Currently translated at 59.5% (181 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/lt/
2024-06-09 21:51:46 +02:00
Nicholas WandHosted Weblate aabac5da13 Translated using Weblate (Italian)
Currently translated at 92.4% (281 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/it/
2024-06-09 21:51:46 +02:00
Nicholas WandHosted Weblate 4fad77f9c1 Translated using Weblate (Hungarian)
Currently translated at 92.7% (282 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/hu/
2024-06-09 21:51:46 +02:00
Nicholas WandHosted Weblate 0fb6d746eb Translated using Weblate (Croatian)
Currently translated at 45.7% (139 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/hr/
2024-06-09 21:51:46 +02:00
Nicholas WandHosted Weblate 7fc712d19b Translated using Weblate (Hindi)
Currently translated at 24.3% (74 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/hi/
2024-06-09 21:51:46 +02:00
Nicholas WandHosted Weblate d6018cfe10 Translated using Weblate (Gujarati)
Currently translated at 23.3% (71 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/gu/
2024-06-09 21:51:45 +02:00
Nicholas WandHosted Weblate 5b8f231a98 Translated using Weblate (French)
Currently translated at 86.8% (264 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/fr/
2024-06-09 21:51:45 +02:00
Nicholas WandHosted Weblate cb503f84e1 Translated using Weblate (Spanish)
Currently translated at 93.0% (283 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/es/
2024-06-09 21:51:45 +02:00
Nicholas WandHosted Weblate a34f9b80ac Translated using Weblate (Danish)
Currently translated at 58.5% (178 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/da/
2024-06-09 21:51:44 +02:00
Nicholas WandHosted Weblate 3e6696b516 Translated using Weblate (Czech)
Currently translated at 93.0% (283 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/cs/
2024-06-09 21:51:44 +02:00
Nicholas WandHosted Weblate 9cd8bd439a Translated using Weblate (English)
Currently translated at 20.3% (62 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/en/
2024-06-09 21:51:44 +02:00
Nicholas WandHosted Weblate ca5b99e23d Added translation using Weblate (Bengali) 2024-06-09 21:44:05 +02:00
Nicholas WandHosted Weblate c44fd74f5b Translated using Weblate (German)
Currently translated at 99.0% (301 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/de/
2024-06-09 21:40:09 +02:00
advplyrandGitHub f82c4834c3 Merge pull request #1225 from lukasitaly/patch-1
Update de.json
2024-06-08 16:03:45 -05:00
advplyrandGitHub dda95bd69c Merge pull request #1217 from mfcar/mf/networkStatusSwift
Implement network monitoring on iOS
2024-06-08 16:02:53 -05:00
LukasandGitHub a623cb57a7 Update de.json
Changed "Mobilfunknetz" to "mobile Daten", but I wouldn't really change the remaining strings with "Mobilfunknetz" just for the sake of uniformity, since "Mobilfunknetz" definitely fits better than "mobile Daten".
2024-06-08 17:01:49 +00:00
LukasandGitHub e798d930fb Update de.json
Translation of not yet translated strings
2024-06-07 08:42:05 +02:00
advplyrandGitHub f91b0c9e2f Merge pull request #1210 from mfcar/mf/limitCellularData
Add settings to limit Cellular Data (Download/Streaming)
2024-06-04 16:56:01 -05:00
advplyr b6824cf242 Fix:Latest podcast episodes to play local 2024-06-04 16:50:55 -05:00
advplyr 41477fdac1 Merge translation strings 2024-06-04 16:28:35 -05:00
advplyr d4e8afc2f1 Map translation strings 2024-06-04 16:23:23 -05:00
advplyr d42cbfe3e4 Revert podcast episode local playback event 2024-06-04 16:21:28 -05:00
advplyr c7c3ef52e6 Update issue templates to 0.9.73 and 0.9.74 2024-06-04 10:25:01 -05:00
advplyr 92344325b6 Add 0.9.64 to issue templates 2024-06-04 10:23:21 -05:00
Faizan Zafar a8172fb3f9 Update font settings in EpubReader and Reader components to include a "Font Boldness" slider which applies a stroke to the text #3020 on main repo 2024-06-03 20:25:11 -07:00
advplyrandGitHub a73ec783d5 Merge pull request #1221 from c-franco/update-spanish-translation
Update Spanish translation
2024-06-03 17:20:00 -05:00
Christian Franco 5c9fc4c9e4 Update Spanish translation 2024-06-03 16:31:40 +01:00
advplyr 494ff38ef3 Fix:Epub rendition resize on screen orientation change #1213 2024-06-02 16:20:41 -05:00
advplyr b7e6cbf1ab Fix:PDF ereader flickering 2024-06-02 15:54:50 -05:00
advplyrandGitHub b33623b74f Merge pull request #1215 from Dalabad/patch-1
Update de.json
2024-05-28 17:24:57 -05:00
Marcos Carvalho 4f939ce6fc Implement network monitoring on iOS 2024-05-27 22:16:49 +01:00
Daniel SchosserandGitHub 4f45f360df Update de.json 2024-05-27 22:50:11 +02:00
Marcos Carvalho 4ad54f34fe Improvements and fixes on the cellular permissions 2024-05-26 22:08:53 +01:00
Marcos Carvalho 574dda5286 Fix android build 2024-05-24 08:10:25 +01:00
Marcos Carvalho d788623509 Initial work 2024-05-23 22:33:06 +01:00
advplyrandGitHub f047f6a23b Merge pull request #1208 from mfcar/mf/detectableTimestamp
feat: Add Time Marker Detection in Episode Descriptions
2024-05-21 17:11:49 -05:00
advplyr 48b30f6b03 iOS version bump 0.9.74 2024-05-20 09:42:43 -05:00
Marcos Carvalho 67d32164d9 Update the parse description to identify time marker in anchor links 2024-05-20 08:33:02 +01:00
advplyr 6672a21049 Version bump v0.9.74-beta 2024-05-19 16:58:20 -05:00
Marcos Carvalho b833019a14 Change href link to span, to avoid change the page and improve code 2024-05-19 14:14:47 +01:00
Marcos Carvalho 00eeeb2a5a Add detectable timestamp 2024-05-19 10:40:04 +01:00
advplyrandGitHub 06a0a68143 Merge pull request #1205 from indiefan/command-target-duplication-fix
Fix bug iOS AudioPlayer where multiple calls to setupRemoteTransportC…
2024-05-17 17:05:14 -05:00
advplyrandGitHub 1a7ae468bd Merge pull request #1206 from cor-bee/patch-1
Update uk.json
2024-05-17 16:50:49 -05:00
Illia PyshniakandGitHub e8f74d7358 Update uk.json 2024-05-17 14:00:58 +03:00
Marke Hallowell 984f01da23 Fix bug iOS AudioPlayer where multiple calls to setupRemoteTransportControls resulted in duplicate MPRemoteCommand targets being applied for the same action. 2024-05-16 19:11:38 -07:00
advplyrandGitHub d6e75410a5 Merge pull request #1201 from nichwall/bug_report
App bug report format
2024-05-12 12:41:45 -05:00
Nicholas Wallace 7627e1b58c App bug report format 2024-05-11 18:24:38 -07:00
advplyrandGitHub 5fb31b18ae Merge pull request #1199 from nichwall/feature_request
Add: feature request
2024-05-10 17:55:03 -05:00
Nicholas WandGitHub 20903e8217 App selection allows for multiple 2024-05-09 07:12:02 -07:00
Nicholas WandGitHub 8fe348d4eb Update feature_request.yaml 2024-05-09 06:40:06 -07:00
Nicholas Wallace 24cda8a179 Add: feature request 2024-05-08 23:30:13 -07:00
advplyr fc954a968b Remove old missing parts references 2024-04-28 16:38:40 -05:00
advplyr aae53a54cd Update:UI/UX: Add explicit indicator next to titles #1181 2024-04-28 16:34:31 -05:00
advplyr cfedcffc98 Add:No series filter option for book libraries #1184 2024-04-28 16:00:57 -05:00
advplyrandGitHub 0e74c74a6c Merge pull request #1179 from indiefan/rate-manager
Refactor AudioPlayer to use a Rate Manager protocol.
2024-04-27 16:49:27 -05:00
advplyr fb86c45e78 iOS add defaultRate fixing merge 2024-04-27 16:41:29 -05:00
advplyrandGitHub 124300b215 Merge branch 'master' into rate-manager 2024-04-27 16:06:20 -05:00
advplyrandGitHub 6746c0627b Merge pull request #1178 from indiefan/master
Update Now Playing info with correct default rate.
2024-04-27 16:01:06 -05:00
Marke Hallowell 0f2cd3b132 Update Now Playing info with correct default rate so that third party apps don't mistake current status as scrubbing. 2024-04-21 15:39:37 -07:00
advplyr e94d6fdc1e Update:Android notification image to not resize #657 2024-04-19 16:32:16 -05:00
advplyr e301b3071c Update:Add 1.7x playback speed to modal #1180 2024-04-19 14:39:00 -05:00
Marke Hallowell 3d3dad5e4a Adding new sources to project file. 2024-04-14 16:47:38 -07:00
Marke Hallowell f8a9033738 Add an AudioPlayerRateManager implementation for iOS 16+. 2024-04-14 16:12:55 -07:00
Marke Hallowell edb25f5bcd Refactor AudioPlayer to use an AudioPlayerRateManager protocol for managing rate/defaultRate state management. Implements a LegacyAudioPlayerRateManager implementation with existing (pre-iOS 16) behavior. 2024-04-14 11:45:03 -07:00
Marke Hallowell bcb239efd1 Update Now Playing info with correct default rate so that third party apps don't mistake current status as scrubbing. 2024-04-13 23:10:03 -07:00
advplyrandGitHub d208211c6f Merge pull request #1176 from Fymyte/patch-4
Update French Translation
2024-04-12 15:22:06 -05:00
Pierrick GuillaumeandGitHub 0b20b72241 Update fr.json 2024-04-12 20:32:47 +02:00
Pierrick GuillaumeandGitHub 5c29459af2 Update fr.json 2024-04-12 20:31:15 +02:00
advplyrandGitHub e54fd38784 Merge pull request #1174 from nichwall/i18n-update
Update i18n workflow to use 1.2.0
2024-04-11 17:17:23 -05:00
Nicholas Wallace 94774f13df Update i18n workflow to use 1.2.0 2024-04-10 18:38:11 -07:00
advplyr 43ec35fe67 Fix:Android device info client version 2024-04-05 16:39:24 -05:00
advplyrandGitHub b2645dc444 Merge pull request #1162 from springsunx/patch-1
Update zh-cn.json
2024-04-03 18:16:14 -05:00
SunXandGitHub c79624a82c Update zh-cn.json 2024-04-03 09:06:13 +08:00
advplyr 8201f3c9a6 Fix:Close sleep timer custom timer sub modal on sleep timer close #1112 2024-04-01 17:35:22 -05:00
advplyrandGitHub e0ac6a1c38 Merge pull request #1160 from pmangro/master
[PR-BR] Go to Web Client
2024-04-01 17:18:33 -05:00
pmangroandGitHub 400da5566d [PR-BR] Go to Web Client 2024-03-31 19:50:45 -03:00
advplyr 9b67037774 Update:Support for comic files with webp images https://github.com/advplyr/audiobookshelf/issues/2792 2024-03-27 16:13:13 -05:00
advplyr 48596542c7 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2024-03-25 16:33:36 -05:00
advplyr 9715d65d88 Update:Go to web client buttons visible to all users #1079 2024-03-25 16:33:30 -05:00
advplyrandGitHub 2e14b31a69 Merge pull request #1153 from burghy86/patch-4
Update it.json
2024-03-25 14:51:30 -05:00
burghy86andGitHub 1c8ce6c3db Update it.json
monthly translation of new strings
2024-03-25 16:29:07 +01:00
advplyrandGitHub dc74a6213d Merge pull request #1150 from nichwall/translation_faq_link
Translation guide link added
2024-03-24 05:06:47 -05:00
Nicholas Wallace e9a66bb7a7 Translation guide link added 2024-03-23 20:08:41 -07:00
advplyr 3c6d4b5f19 Add:Nav button in side drawer and item more menu to go to web client #1079 2024-03-22 17:54:56 -05:00
advplyrandGitHub f04eebe0d8 Merge pull request #1142 from nichwall/translation_workflow_check
Add: i18n workflow check
2024-03-20 16:43:26 -05:00
advplyrandGitHub 63b5227ccc Merge pull request #1140 from cor-bee/master
Update Ukrainian translation
2024-03-20 16:31:00 -05:00
Nicholas Wallace 934d9b1df4 Fixed release number of action 2024-03-19 21:19:38 -07:00
Nicholas Wallace 8994cb3a6e Added newline to force check 2024-03-19 21:12:52 -07:00
Nicholas Wallace 4a32de482b Example: German keys alphabetized 2024-03-19 21:09:49 -07:00
Nicholas Wallace 95f47793d2 Example: German strings out of order 2024-03-19 21:08:43 -07:00
Nicholas Wallace f27048618d Add: newline to end of file 2024-03-19 21:05:53 -07:00
Nicholas Wallace c90ad419e8 Add: i18n workflow check 2024-03-19 21:05:09 -07:00
Illia PyshniakandGitHub 2cdc3ecb9d Update uk.json 2024-03-19 23:09:35 +02:00
advplyr 9f89fcd968 Fix:Podcast library playlists page incorrect label in toolbar #1135 2024-03-19 12:52:14 -05:00
advplyrandGitHub 7c2d51f811 Merge pull request #1136 from cor-bee/master
Ukrainian translation
2024-03-18 17:44:49 -05:00
advplyr 41fe7a29cd Merge 2024-03-18 17:41:13 -05:00
advplyrandGitHub 54f3eec303 Merge pull request #1127 from nichwall/vietnamese_translations
Vietnamese translations
2024-03-18 17:36:58 -05:00
advplyr cd91013f69 Update Vietnamese datefnslocale code 2024-03-18 17:37:04 -05:00
advplyrandGitHub 0af16ef05f Merge pull request #1139 from pmangro/master
[PT-BR] Remove from Playlist
2024-03-18 11:57:25 -05:00
pmangroandGitHub 5ba830d56e [PT-BR] Remove from Playlist 2024-03-18 09:45:46 -03:00
Illia PyshniakandGitHub 81919e963a Add Ukrainian language to i18n.js 2024-03-15 22:28:00 +02:00
Illia PyshniakandGitHub 8a1d4bb958 Ukrainian app translation 2024-03-15 22:26:52 +02:00
Nicholas Wallace eabac0e709 Add: Vietnamese to language code map 2024-03-09 16:23:25 -07:00
Nicholas Wallace d90783d7c8 Fixed whitespace 2024-03-09 16:20:24 -07:00
Nicholas Wallace faf8211e5c Add: Vietnamese app translations 2024-03-09 16:20:00 -07:00
advplyr 65d90947c5 Fix:Remove from playlist button #987 2024-03-04 14:56:58 -06:00
advplyr 752e918d26 iOS version bump 0.9.73 & update dependencies 2024-03-03 16:28:55 -06:00
advplyr ad08cc69f4 Version bump 0.9.73-beta 2024-03-03 14:15:32 -06:00
advplyr da3882fd5e Fix:Playlist tab not shown after adding first playlist #1092 2024-03-03 13:29:58 -06:00
advplyr c04c7a55d9 Fix:iOS progress bar overflowing edge of screen 2024-03-03 12:52:17 -06:00
advplyr 3318846b43 Add:Remove from playlist option in more menu for items on playlist page #987 2024-03-02 15:25:18 -06:00
advplyrandGitHub e0860feeb9 Merge pull request #1109 from megamegax/hu
feat(i18n): add Hungarian localization
2024-03-01 15:39:15 -06:00
Hunyady Mihály c0253a69b9 feat(i18n): replace NoItemsFound translation to a more general one 2024-03-01 22:09:33 +01:00
Hunyady Mihály 9242c7e6e0 feat(i18n): small grammar adjustments 2024-03-01 22:04:09 +01:00
Hunyady Mihály 0bfc579ae4 feat(i18n): adjust translation 2024-03-01 21:39:06 +01:00
megamegax 5fad06756b feat(i18n): add Hungarian localization 2024-03-01 20:03:21 +01:00
advplyrandGitHub 5be6afa6c1 Merge pull request #1106 from pmangro/0.9.72.a-PT-BR
[PT-BR] Translated LabelAllowSeekingOnMediaControls
2024-02-27 15:58:36 -06:00
pmangroandGitHub 5ba3b8be7a [PT-BR] Translated LabelAllowSeekingOnMediaControls 2024-02-27 14:39:21 -03:00
advplyr 0eb62f541d Fix:Local media item page long titles overflowing screen pushing play & more menu button offscreen #1103 2024-02-25 15:35:23 -06:00
advplyrandGitHub 6baff881c7 Merge pull request #1094 from fidoriel/disable-widget-ios-scrubing
Disable ios/android scrubbing in MPRemoteCommandCenter/MediaBrowserServiceCompat
2024-02-25 15:29:52 -06:00
fidoriel 8c23c1e795 german translation for LabelAllowSeekingOnMediaControls 2024-02-25 22:25:17 +01:00
advplyr 9706c66a27 iOS Update Realm schema version for new setting, update remote transport controls on device settings updated 2024-02-25 15:04:54 -06:00
advplyr f4f7252537 Update wording for enable seeking on media controls settings, remove restart required help text, map translations 2024-02-25 14:44:40 -06:00
advplyr 430d200151 Update android disable media control seek setting to update in realtime 2024-02-25 14:11:28 -06:00
advplyr 7be82d03f3 Merge branch 'master' into disable-widget-ios-scrubing 2024-02-25 12:51:58 -06:00
advplyr 15aaef742d Fix:Ignore swiping for audio player when modal is open #1099 2024-02-24 15:00:29 -06:00
advplyrandGitHub e443ba09da Merge pull request #1095 from pmangro/0.9.72.a-PT-BR
Added PT-BR Strings
2024-02-24 14:49:35 -06:00
luca 8235d1f1f7 reorder setting and add info label 2024-02-22 20:27:33 +01:00
pmangroandGitHub 9a39dcceee Update i18n.js to add pt-br 2024-02-21 08:55:45 -03:00
pmangroandGitHub a707b7fc86 Typos corrected 2024-02-21 08:53:34 -03:00
fidoriel b6fea85ef4 fix compile android 2024-02-21 10:16:47 +01:00
advplyr 7e3b3f55ae Fix:Android auto not loading server items #1081 2024-02-20 17:05:07 -06:00
fidoriel d2d3bbe8a1 fix default false 2024-02-20 22:08:47 +01:00
fidoriel 41a65c0f9d add setting for widget scrubbing 2024-02-20 21:58:31 +01:00
pmangroandGitHub 165ddd58a6 Update pt-br.json - initial file 2024-02-20 17:29:03 -03:00
pmangroandGitHub 09dc04c0de Created pt-br.json 2024-02-20 15:40:29 -03:00
advplyrandGitHub 57d5dbc0d0 Merge pull request #1082 from arcao/patch-1
Translate cs.json
2024-02-14 18:42:25 -06:00
Martin SloupandGitHub 11af75e127 Translate cs.json
Translate untranslated texts to Czech and fix already translated texts.
2024-02-14 02:10:33 +01:00
advplyr f8d82a3020 iOS version bump 0.9.72 2024-02-11 08:54:54 -06:00
advplyr 81d7b1cfd7 Version bump 0.9.72-beta 2024-02-10 16:35:01 -06:00
advplyr e15c1bcb32 Fix:Continue listening shelves show on initial failed connection #1038 2024-02-09 17:52:24 -06:00
advplyr 826d414d9f Update:iOS do not resume playback after app suspended. Attempted fix for #973 2024-02-07 18:02:25 -06:00
advplyr 0da6fca727 Fix:iOS 0 second listening sessions #970 and include device info with offline sessions 2024-02-05 19:04:11 -06:00
advplyr 2327bc1705 Update playMethod to fallback to 0 2024-02-04 18:14:24 -06:00
advplyr 3bb76cecac Fix:Android respect offline mode on app restart 2024-02-04 17:46:56 -06:00
advplyr e3f82bb237 Fix:Include device info with sync local-all request 2024-02-04 17:27:57 -06:00
advplyr fcd6270f2d Fix:Hide library list button from app bar when no network connection 2024-02-04 15:53:05 -06:00
advplyr d5411703ca Update:Add 10 second timeout when loading home page shelves 2024-02-04 15:48:37 -06:00
advplyr 8fc2058db8 Update:Handle navigating to library item pages when server connection goes down
- Added loading indicator to library item page on initial request to server
- Redirect to downloaded item page if server item request fails
2024-02-04 15:46:26 -06:00
advplyr d03949f5d1 Update:Audio player title scrolls when overflow #740
- increase width of the title on minimized player to scale with screen width
- mini player shows media title and chapter title
2024-02-03 17:31:53 -06:00
advplyr ff01786063 Remove unused fastSort.js 2024-02-03 13:28:41 -06:00
advplyr bb117ab4da Update:Authors page increase size to fill width with 2 per row #1026 2024-02-02 18:33:23 -06:00
advplyr f5ec247b47 Update:Minimized audio player allow swipe up to fullscreen #1046 2024-02-01 18:20:10 -06:00
advplyr 6e6dd00523 Update:Allow swiping down anywhere on fullscreen audio player to minimize #1046 2024-02-01 17:51:10 -06:00
advplyr c9fb98f033 Fix:Android crash on startup due to SimpleStorage #1040
- Update SimpleStorage to 1.5.5-SNAPSHOT
2024-01-29 16:46:39 -06:00
advplyr 75ecff5e6d Remove @nuxtjs/tailwindcss dependency and replace with standalone tailwindcss 2024-01-25 17:39:22 -06:00
advplyrandGitHub 91a9f918fb Merge pull request #1052 from bloodscript/issue-1051-german-localization-improvement
[issue-1051] German Localization
2024-01-24 14:18:17 -06:00
Steffen Meiselbach 03305f90ca corrected ereader based on current Duden entry 2024-01-23 12:38:31 +01:00
Steffen Meiselbach 9428bc07c5 reverted to uniformly uncapitalized pronouns 2024-01-23 12:37:03 +01:00
Steffen Meiselbach f38f9c3502 ebook written as used in Duden and uniformly capitalized version of Du 2024-01-23 12:33:01 +01:00
Steffen Meiselbach 64a796aece standardized addressing (du), missing translations and uniformly starting with capital character 2024-01-23 07:24:40 +01:00
advplyr 63039fe8c9 Fix pl.json formatting 2024-01-22 17:34:59 -06:00
Steffen Meiselbach 3f2b723d0f (sleep) timer is typically used in german parlance 2024-01-22 22:55:28 +01:00
advplyr ffd7fb1cf9 Update workflows to use node 20 2024-01-21 15:56:24 -06:00
advplyrandGitHub b049b206cf Merge pull request #1049 from Bogumio/patch-2
Update pl.json
2024-01-21 15:31:27 -06:00
advplyr 35f56dc8a9 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2024-01-21 15:32:13 -06:00
advplyr a71cd326cc Update readme node version to 20 2024-01-21 15:32:06 -06:00
BogumioandGitHub cc841d1c7c Update pl.json
The rest of the translations have been completed. Minor corrections and unifications.
2024-01-21 12:04:06 +01:00
advplyrandGitHub 16ecdcf96e Merge pull request #1042 from Bogumio/patch-1
Update pl.json
2024-01-14 11:15:48 -06:00
BogumioandGitHub 96ec56d1ad Update pl.json
Most of the missing phrases have been completed. I will try to translate the rest soon.
2024-01-14 18:07:55 +01:00
advplyr 9b15bafc9f Fix:Settings page button/toggle touch area #1021 2024-01-11 17:27:41 -06:00
advplyr aa02f2e465 Update readme for iOS beta full 2024-01-06 14:15:19 -06:00
advplyrandGitHub 15464617ef Merge pull request #1032 from burghy86/patch-3
Update it.json
2024-01-05 10:21:43 -06:00
burghy86andGitHub 60ecceb59d Update it.json
all string translate
2024-01-05 10:18:48 +01:00
advplyr 3fe943e989 Add:Player setting to scale elapsed time by playback speed #1028
- Moved player settings to new playerSettings key in storage preferences
2024-01-04 15:28:25 -06:00
111 changed files with 7385 additions and 19998 deletions
+1 -6
View File
@@ -1,13 +1,8 @@
# editorconfig.org
root = true root = true
[*] [*]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
-37
View File
@@ -1,37 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
labels: bug
---
### Steps to reproduce
1.
2.
3.
### Expected behaviour
- Tell us what should happen
### Actual behaviour
- Tell us what happens
### Environment data
Audiobookshelf Version:
- [ ] Android App?
- [ ] iOS App?
#### Android Issue
Android version:
Device model:
Stock or customized system:
#### iOS Issue
iOS Version:
iPhone model:
+88
View File
@@ -0,0 +1,88 @@
name: 🐞 ABS App Bug Report
description: File a bug/issue and help us improve the Audiobookshelf mobile apps.
title: "[Bug]: "
labels: ["bug", "triage"]
body:
- type: markdown
attributes:
value: "## App Bug Description"
- type: markdown
attributes:
value: "Thank you for filing a bug report! 🐛"
- type: markdown
attributes:
value: "Join the [discord server](https://discord.gg/HQgCbd6E75) for questions or if you are not sure about a bug."
- type: textarea
id: what-happened
attributes:
label: What was the Problem?
description: Please help us understand the problem.
placeholder: Describe the problem you saw or experienced in detail! Include a screenshot or video, if applicable.
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to Reproduce the Issue
description: Please help us understand how we can reliably reproduce the issue.
placeholder: "1. Go to the library page of a Podcast library and..."
validations:
required: true
- type: textarea
id: what-was-expected
attributes:
label: What was Expected?
description: Please help us understand what should have happened.
placeholder: Please help us visualize by including a doodle or screenshot.
validations:
required: true
- type: markdown
attributes:
value: "## Mobile Environment"
- type: input
id: phone-model
attributes:
label: Phone Model
description: What kind of phone are you using?
placeholder: e.g. Pixel 6, iPhone 14, Samusung Galaxy s23, etc
validations:
required: true
- type: input
id: phone-os
attributes:
label: Phone OS
description: What operating system? If Android, is it stock or a custom system?
placeholder: e.g. Android 14, iOS 17.4, etc
validations:
required: true
- type: dropdown
id: app-version
attributes:
label: Audiobookshelf App Version
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
options:
- Android App - 0.9.74
- iOS App - 0.9.74
- Android App - 0.9.73
- iOS App - 0.9.73
validations:
required: true
- type: dropdown
id: install-source
attributes:
label: Installation Source
multiple: true
options:
- Google Play Store
- Testflight
- SideStore
- Other (List in "Additional Notes")
validations:
required: true
- type: textarea
id: additional-notes
attributes:
label: Additional Notes
description: Anything else you want to add?
placeholder: "e.g. I have tried X, Y, and Z."
-18
View File
@@ -1,18 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
labels: enhancement
---
### Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
### Describe the solution you'd like
A clear and concise description of what you want to happen.
### Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
### Additional context
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,57 @@
name: 🚀 App Feature Request
description: Request a feature/enhancement
title: "[Enhancement]: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: "## App Feature Request Description"
- type: markdown
attributes:
value: "Please first search in both issues & discussions for your enhancement and make sure your app is up to date."
- type: textarea
id: describe
attributes:
label: Describe the Feature/Enhancement
description: Please help us understand what you want.
placeholder: What is your vision?
validations:
required: true
- type: textarea
id: the-why
attributes:
label: Why would this be helpful?
description: Please help us understand why this would enhance your experience.
placeholder: Explain the "why" or "use case".
validations:
required: true
- type: textarea
id: image
attributes:
label: Future Implementation (Screenshot)
description: Please help us visualize by including a doodle or screenshot.
placeholder: How could this look or where could it go?
validations:
required: true
- type: markdown
attributes:
value: "## App Current Implementation"
- type: dropdown
id: version
attributes:
label: Audiobookshelf App Version
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
options:
- Android App - 0.9.74
- iOS App - 0.9.74
- Android App - 0.9.73
- iOS App - 0.9.73
validations:
required: true
- type: textarea
id: current-image
attributes:
label: Current Implementation (Screenshot)
description: What screen were you looking at when you thought of this enhancement?
placeholder: If an image is not applicable, please explain why.
+2 -2
View File
@@ -16,10 +16,10 @@ jobs:
- name: checkout sources - name: checkout sources
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: use Node.js 16.x - name: use Node.js 20.x
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 20
- name: Set up Java - name: Set up Java
uses: actions/setup-java@v2 uses: actions/setup-java@v2
@@ -0,0 +1,20 @@
name: Close fixed issues on release.
on:
release:
types: [published]
permissions:
contents: read
issues: write
jobs:
comment:
runs-on: ubuntu-latest
steps:
- name: Close issues marked as fixed upon a release.
uses: gcampbell-msft/fixed-pending-release@7fa1b75a0c04bcd4b375110522878e5f6100cff5
with:
label: 'awaiting release'
removeLabel: true
applyToAll: true
message: Fixed in [${releaseTag}](${releaseUrl}).
+2 -2
View File
@@ -15,10 +15,10 @@ jobs:
- name: checkout sources - name: checkout sources
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: use Node.js 16.x - name: use Node.js 20.x
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 20
- name: Set up Java - name: Set up Java
uses: actions/setup-java@v2 uses: actions/setup-java@v2
+28
View File
@@ -0,0 +1,28 @@
name: Verify all i18n files are alphabetized
on:
push:
pull_request:
paths:
- strings/** # Should only check if any strings changed
jobs:
update_translations:
runs-on: ubuntu-latest
steps:
# Check out the repository
- name: Checkout repository
uses: actions/checkout@v4
# Set up node to run the javascript
- name: Set up node
uses: actions/setup-node@v4
with:
node-version: '20'
# The only argument is the `directory`, which is where the i18n files are
# stored.
- name: Run Update JSON Files action
uses: audiobookshelf/audiobookshelf-i18n-updater@v1.3.0
with:
directory: 'strings/' # Adjust the directory path as needed
+17
View File
@@ -0,0 +1,17 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 400,
"proseWrap": "never",
"trailingComma": "none",
"overrides": [
{
"files": ["*.html"],
"options": {
"singleQuote": false,
"wrapAttributes": false,
"sortAttributes": false
}
}
]
}
+7
View File
@@ -0,0 +1,7 @@
{
"recommendations": [
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode",
"octref.vetur"
]
}
+27
View File
@@ -0,0 +1,27 @@
{
"vetur.format.defaultFormatterOptions": {
"prettier": {
"semi": false,
"singleQuote": true,
"printWidth": 400,
"proseWrap": "never",
"trailingComma": "none"
},
"prettyhtml": {
"printWidth": 400,
"singleQuote": false,
"wrapAttributes": false,
"sortAttributes": false
}
},
"editor.formatOnSave": true,
"editor.detectIndentation": true,
"editor.tabSize": 2,
"javascript.format.semicolons": "remove",
"[javascript][json][jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "octref.vetur"
}
}
+5 -3
View File
@@ -33,8 +33,8 @@ android {
applicationId "com.audiobookshelf.app" applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 102 versionCode 107
versionName "0.9.71-beta" versionName "0.9.76-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
manifestPlaceholders = [ manifestPlaceholders = [
"appAuthRedirectScheme": "com.audiobookshelf.app" "appAuthRedirectScheme": "com.audiobookshelf.app"
@@ -64,6 +64,8 @@ repositories {
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
} }
mavenCentral() mavenCentral()
// TODO: Temporarily using SNAPSHOT version of Simple Storage that resolves https://github.com/anggrayudi/SimpleStorage/issues/133
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
} }
configurations.configureEach { configurations.configureEach {
@@ -118,7 +120,7 @@ dependencies {
implementation 'io.github.pilgr:paperdb:2.7.2' implementation 'io.github.pilgr:paperdb:2.7.2'
// Simple Storage // Simple Storage
implementation "com.anggrayudi:storage:1.5.4" implementation "com.anggrayudi:storage:1.5.5-SNAPSHOT"
// OK HTTP // OK HTTP
implementation 'com.squareup.okhttp3:okhttp:4.9.2' implementation 'com.squareup.okhttp3:okhttp:4.9.2'
+1
View File
@@ -6,6 +6,7 @@
<!-- Permissions --> <!-- Permissions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
@@ -103,6 +103,9 @@ class MainActivity : BridgeActivity() {
} }
} }
fun isPlayerNotificationServiceInitialized():Boolean {
return ::foregroundService.isInitialized
}
fun stopMyService() { fun stopMyService() {
if (mBounded) { if (mBounded) {
@@ -96,7 +96,7 @@ class Podcast(
@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
val newEpisode = PodcastEpisode(localEpisodeId,(episodes?.size ?: 0) + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,null,null,audioTrack,episode.chapters,audioTrack.duration,0, episode.id, localEpisodeId) val newEpisode = PodcastEpisode(localEpisodeId,(episodes?.size ?: 0) + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,null,null,audioTrack,episode.chapters,audioTrack.duration,episode.size, episode.id, localEpisodeId)
episodes?.add(newEpisode) episodes?.add(newEpisode)
var index = 1 var index = 1
@@ -20,6 +20,14 @@ enum class ShakeSensitivitySetting {
VERY_LOW, LOW, MEDIUM, HIGH, VERY_HIGH VERY_LOW, LOW, MEDIUM, HIGH, VERY_HIGH
} }
enum class DownloadUsingCellularSetting {
ASK, ALWAYS, NEVER
}
enum class StreamingUsingCellularSetting {
ASK, ALWAYS, NEVER
}
data class ServerConnectionConfig( data class ServerConnectionConfig(
var id:String, var id:String,
var index:Int, var index:Int,
@@ -107,6 +115,7 @@ data class PlayItemRequestPayload(
data class DeviceSettings( data class DeviceSettings(
var disableAutoRewind:Boolean, var disableAutoRewind:Boolean,
var enableAltView:Boolean, var enableAltView:Boolean,
var allowSeekingOnMediaControls:Boolean,
var jumpBackwardsTime:Int, var jumpBackwardsTime:Int,
var jumpForwardTime:Int, var jumpForwardTime:Int,
var enableMp3IndexSeeking:Boolean, var enableMp3IndexSeeking:Boolean,
@@ -122,7 +131,9 @@ data class DeviceSettings(
var sleepTimerLength: Long, // Time in milliseconds var sleepTimerLength: Long, // Time in milliseconds
var disableSleepTimerFadeOut: Boolean, var disableSleepTimerFadeOut: Boolean,
var disableSleepTimerResetFeedback: Boolean, var disableSleepTimerResetFeedback: Boolean,
var languageCode: String var languageCode: String,
var downloadUsingCellular: DownloadUsingCellularSetting,
var streamingUsingCellular: StreamingUsingCellularSetting
) { ) {
companion object { companion object {
// Static method to get default device settings // Static method to get default device settings
@@ -130,6 +141,7 @@ data class DeviceSettings(
return DeviceSettings( return DeviceSettings(
disableAutoRewind = false, disableAutoRewind = false,
enableAltView = true, enableAltView = true,
allowSeekingOnMediaControls = false,
jumpBackwardsTime = 10, jumpBackwardsTime = 10,
jumpForwardTime = 10, jumpForwardTime = 10,
enableMp3IndexSeeking = false, enableMp3IndexSeeking = false,
@@ -145,7 +157,9 @@ data class DeviceSettings(
autoSleepTimerAutoRewindTime = 300000L, // 5 minutes autoSleepTimerAutoRewindTime = 300000L, // 5 minutes
disableSleepTimerFadeOut = false, disableSleepTimerFadeOut = false,
disableSleepTimerResetFeedback = false, disableSleepTimerResetFeedback = false,
languageCode = "en-us" languageCode = "en-us",
downloadUsingCellular = DownloadUsingCellularSetting.ALWAYS,
streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS
) )
} }
} }
@@ -53,6 +53,14 @@ object DeviceManager {
if (deviceData.deviceSettings?.languageCode == null) { if (deviceData.deviceSettings?.languageCode == null) {
deviceData.deviceSettings?.languageCode = "en-us" deviceData.deviceSettings?.languageCode = "en-us"
} }
if (deviceData.deviceSettings?.downloadUsingCellular == null) {
deviceData.deviceSettings?.downloadUsingCellular = DownloadUsingCellularSetting.ALWAYS
}
if (deviceData.deviceSettings?.streamingUsingCellular == null) {
deviceData.deviceSettings?.streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS
}
} }
fun getBase64Id(id:String):String { fun getBase64Id(id:String):String {
@@ -70,6 +70,7 @@ class FolderScanner(var ctx: Context) {
) )
localLibraryItem.localFiles.add(localFile) localLibraryItem.localFiles.add(localFile)
val trackFileMetadata = FileMetadata(file.name, file.extension, file.absolutePath, file.getBasePath(ctx), file.length())
// Create new audio track // Create new audio track
val track = AudioTrack( val track = AudioTrack(
audioTrackFromServer.index, audioTrackFromServer.index,
@@ -78,7 +79,7 @@ class FolderScanner(var ctx: Context) {
localFile.filename ?: "", localFile.filename ?: "",
localFile.contentUrl, localFile.contentUrl,
localFile.mimeType ?: "", localFile.mimeType ?: "",
null, trackFileMetadata,
true, true,
localFileId, localFileId,
null, null,
@@ -274,7 +275,8 @@ class FolderScanner(var ctx: Context) {
localLibraryItem.localFiles.add(localFile) localLibraryItem.localFiles.add(localFile)
// Create new audio track // Create new audio track
val track = AudioTrack(audioTrackFromServer.index, audioTrackFromServer.startOffset, audioTrackFromServer.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, null, audioTrackFromServer.index) val trackFileMetadata = FileMetadata(docFile.name ?: "", docFile.extension ?: "", docFile.getAbsolutePath(ctx), docFile.getBasePath(ctx), docFile.length())
val track = AudioTrack(audioTrackFromServer.index, audioTrackFromServer.startOffset, audioTrackFromServer.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", trackFileMetadata, true, localFileId, null, audioTrackFromServer.index)
audioTracks.add(track) audioTracks.add(track)
Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}") Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}")
@@ -247,14 +247,18 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
} }
private fun checkSetValidServerConnectionConfig(cb: (Boolean) -> Unit) = runBlocking { private fun checkSetValidServerConnectionConfig(cb: (Boolean) -> Unit) = runBlocking {
Log.d(tag, "checkSetValidServerConnectionConfig | $serverConfigIdUsed") Log.d(tag, "checkSetValidServerConnectionConfig | serverConfigIdUsed=$serverConfigIdUsed | lastServerConnectionConfigId=${DeviceManager.deviceData.lastServerConnectionConfigId}")
coroutineScope { coroutineScope {
if (!DeviceManager.checkConnectivity(ctx)) { if (!DeviceManager.checkConnectivity(ctx)) {
serverUserMediaProgress = mutableListOf() serverUserMediaProgress = mutableListOf()
Log.d(tag, "checkSetValidServerConnectionConfig: No connectivity")
cb(false)
} else if (DeviceManager.deviceData.lastServerConnectionConfigId.isNullOrBlank()) { // If in offline mode last server connection config is unset
serverUserMediaProgress = mutableListOf()
Log.d(tag, "checkSetValidServerConnectionConfig: No last server connection config")
cb(false) cb(false)
} else { } else {
var hasValidConn = false var hasValidConn = false
var lookupMediaProgress = true var lookupMediaProgress = true
@@ -332,6 +336,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
checkSetValidServerConnectionConfig { isConnected -> checkSetValidServerConnectionConfig { isConnected ->
if (isConnected) { if (isConnected) {
serverConfigIdUsed = DeviceManager.serverConnectionConfigId serverConfigIdUsed = DeviceManager.serverConnectionConfigId
Log.d(tag, "loadAndroidAutoItems: Connected to server config id=$serverConfigIdUsed")
loadLibraries { libraries -> loadLibraries { libraries ->
if (libraries.isEmpty()) { if (libraries.isEmpty()) {
@@ -358,6 +363,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
} }
} }
} else { // Not connected to server } else { // Not connected to server
Log.d(tag, "loadAndroidAutoItems: Not connected to server")
cb() cb()
} }
} }
@@ -238,6 +238,8 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
saveLocalProgress(it) saveLocalProgress(it)
lastSyncTime = System.currentTimeMillis() lastSyncTime = System.currentTimeMillis()
Log.d(tag, "Sync local device current serverConnectionConfigId=${DeviceManager.serverConnectionConfig?.id}")
// Local library item is linked to a server library item // Local library item is linked to a server library item
// Send sync to server also if connected to this server and local item belongs to this server // Send sync to server also if connected to this server and local item belongs to this server
if (hasNetworkConnection && shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) { if (hasNetworkConnection && shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
@@ -15,8 +15,6 @@ import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.PlayerNotificationManager import com.google.android.exoplayer2.ui.PlayerNotificationManager
import kotlinx.coroutines.* import kotlinx.coroutines.*
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
class AbMediaDescriptionAdapter constructor(private val controller: MediaControllerCompat, private val playerNotificationService: PlayerNotificationService) : PlayerNotificationManager.MediaDescriptionAdapter { class AbMediaDescriptionAdapter constructor(private val controller: MediaControllerCompat, private val playerNotificationService: PlayerNotificationService) : PlayerNotificationManager.MediaDescriptionAdapter {
private val tag = "MediaDescriptionAdapter" private val tag = "MediaDescriptionAdapter"
@@ -77,7 +75,7 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
.load(uri) .load(uri)
.placeholder(R.drawable.icon) .placeholder(R.drawable.icon)
.error(R.drawable.icon) .error(R.drawable.icon)
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE) .submit()
.get() .get()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -85,7 +83,7 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
Glide.with(playerNotificationService) Glide.with(playerNotificationService)
.asBitmap() .asBitmap()
.load(Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)) .load(Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon))
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE) .submit()
.get() .get()
} }
} }
@@ -34,6 +34,7 @@ import com.audiobookshelf.app.managers.SleepTimerManager
import com.audiobookshelf.app.media.MediaManager import com.audiobookshelf.app.media.MediaManager
import com.audiobookshelf.app.media.MediaProgressSyncer import com.audiobookshelf.app.media.MediaProgressSyncer
import com.audiobookshelf.app.server.ApiHandler import com.audiobookshelf.app.server.ApiHandler
import com.audiobookshelf.app.BuildConfig
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
@@ -329,15 +330,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} }
} }
mediaSessionConnector.setEnabledPlaybackActions( setMediaSessionConnectorPlaybackActions()
PlaybackStateCompat.ACTION_PLAY_PAUSE
or PlaybackStateCompat.ACTION_PLAY
or PlaybackStateCompat.ACTION_PAUSE
or PlaybackStateCompat.ACTION_SEEK_TO
or PlaybackStateCompat.ACTION_FAST_FORWARD
or PlaybackStateCompat.ACTION_REWIND
or PlaybackStateCompat.ACTION_STOP
)
mediaSessionConnector.setQueueNavigator(queueNavigator) mediaSessionConnector.setQueueNavigator(queueNavigator)
mediaSessionConnector.setPlaybackPreparer(MediaSessionPlaybackPreparer(this)) mediaSessionConnector.setPlaybackPreparer(MediaSessionPlaybackPreparer(this))
@@ -512,6 +505,20 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
mediaSessionConnector.setCustomActionProviders(*customActionProviders.toTypedArray()) mediaSessionConnector.setCustomActionProviders(*customActionProviders.toTypedArray())
} }
fun setMediaSessionConnectorPlaybackActions() {
var playbackActions = PlaybackStateCompat.ACTION_PLAY_PAUSE or
PlaybackStateCompat.ACTION_PLAY or
PlaybackStateCompat.ACTION_PAUSE or
PlaybackStateCompat.ACTION_FAST_FORWARD or
PlaybackStateCompat.ACTION_REWIND or
PlaybackStateCompat.ACTION_STOP
if (deviceSettings.allowSeekingOnMediaControls) {
playbackActions = playbackActions or PlaybackStateCompat.ACTION_SEEK_TO
}
mediaSessionConnector.setEnabledPlaybackActions(playbackActions)
}
fun handlePlayerPlaybackError(errorMessage:String) { fun handlePlayerPlaybackError(errorMessage:String) {
// On error and was attempting to direct play - fallback to transcode // On error and was attempting to direct play - fallback to transcode
currentPlaybackSession?.let { playbackSession -> currentPlaybackSession?.let { playbackSession ->
@@ -941,7 +948,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
appVersion: 0.9.46-beta appVersion: 0.9.46-beta
*/ */
val deviceId = Settings.Secure.getString(ctx.contentResolver, Settings.Secure.ANDROID_ID) val deviceId = Settings.Secure.getString(ctx.contentResolver, Settings.Secure.ANDROID_ID)
return DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, Build.VERSION.RELEASE) return DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
} }
private val deviceSettings get() = DeviceManager.deviceData.deviceSettings ?: DeviceSettings.default() private val deviceSettings get() = DeviceManager.deviceData.deviceSettings ?: DeviceSettings.default()
@@ -1,5 +1,7 @@
package com.audiobookshelf.app.plugins package com.audiobookshelf.app.plugins
import android.os.Handler
import android.os.Looper
import android.util.Log import android.util.Log
import com.audiobookshelf.app.MainActivity import com.audiobookshelf.app.MainActivity
import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.*
@@ -493,9 +495,16 @@ class AbsDatabase : Plugin() {
fun updateDeviceSettings(call:PluginCall) { // Returns device data fun updateDeviceSettings(call:PluginCall) { // Returns device data
Log.d(tag, "updateDeviceSettings ${call.data}") Log.d(tag, "updateDeviceSettings ${call.data}")
val newDeviceSettings = jacksonMapper.readValue<DeviceSettings>(call.data.toString()) val newDeviceSettings = jacksonMapper.readValue<DeviceSettings>(call.data.toString())
GlobalScope.launch(Dispatchers.IO) {
Handler(Looper.getMainLooper()).post {
DeviceManager.deviceData.deviceSettings = newDeviceSettings DeviceManager.deviceData.deviceSettings = newDeviceSettings
DeviceManager.dbManager.saveDeviceData(DeviceManager.deviceData) DeviceManager.dbManager.saveDeviceData(DeviceManager.deviceData)
// Updates playback actions for media notification (handles media control seek locking setting)
if (mainActivity.isPlayerNotificationServiceInitialized()) {
mainActivity.foregroundService.setMediaSessionConnectorPlaybackActions()
}
call.resolve(JSObject(jacksonMapper.writeValueAsString(DeviceManager.deviceData))) call.resolve(JSObject(jacksonMapper.writeValueAsString(DeviceManager.deviceData)))
} }
} }
@@ -1,6 +1,9 @@
package com.audiobookshelf.app.server package com.audiobookshelf.app.server
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Build
import android.provider.Settings
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
@@ -8,6 +11,7 @@ import com.audiobookshelf.app.media.MediaEventManager
import com.audiobookshelf.app.media.MediaProgressSyncData import com.audiobookshelf.app.media.MediaProgressSyncData
import com.audiobookshelf.app.media.SyncResult import com.audiobookshelf.app.media.SyncResult
import com.audiobookshelf.app.models.User import com.audiobookshelf.app.models.User
import com.audiobookshelf.app.BuildConfig
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.core.json.JsonReadFeature import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
@@ -31,7 +35,7 @@ class ApiHandler(var ctx:Context) {
private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build() private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build()
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
data class LocalSessionsSyncRequestPayload(val sessions:List<PlaybackSession>) data class LocalSessionsSyncRequestPayload(val sessions:List<PlaybackSession>, val deviceInfo:DeviceInfo)
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class LocalSessionSyncResult(val id:String, val success:Boolean, val progressSynced:Boolean?, val error:String?) data class LocalSessionSyncResult(val id:String, val success:Boolean, val progressSynced:Boolean?, val error:String?)
data class LocalSessionsSyncResponsePayload(val results:List<LocalSessionSyncResult>) data class LocalSessionsSyncResponsePayload(val results:List<LocalSessionSyncResult>)
@@ -322,7 +326,11 @@ class ApiHandler(var ctx:Context) {
} }
fun sendSyncLocalSessions(playbackSessions:List<PlaybackSession>, cb: (Boolean, String?) -> Unit) { fun sendSyncLocalSessions(playbackSessions:List<PlaybackSession>, cb: (Boolean, String?) -> Unit) {
val payload = JSObject(jacksonMapper.writeValueAsString(LocalSessionsSyncRequestPayload(playbackSessions))) @SuppressLint("HardwareIds")
val deviceId = Settings.Secure.getString(ctx.contentResolver, Settings.Secure.ANDROID_ID)
val deviceInfo = DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
val payload = JSObject(jacksonMapper.writeValueAsString(LocalSessionsSyncRequestPayload(playbackSessions, deviceInfo)))
Log.d(tag, "Sending ${playbackSessions.size} saved local playback sessions to server") Log.d(tag, "Sending ${playbackSessions.size} saved local playback sessions to server")
postRequest("/api/session/local-all", payload, null) { postRequest("/api/session/local-all", payload, null) {
if (!it.getString("error").isNullOrEmpty()) { if (!it.getString("error").isNullOrEmpty()) {
+1 -1
View File
@@ -1,7 +1,7 @@
ext { ext {
minSdkVersion = 24 minSdkVersion = 24
compileSdkVersion = 34 compileSdkVersion = 34
targetSdkVersion = 33 targetSdkVersion = 34
androidxActivityVersion = '1.7.0' androidxActivityVersion = '1.7.0'
androidxAppCompatVersion = '1.6.1' androidxAppCompatVersion = '1.6.1'
androidxCoordinatorLayoutVersion = '1.2.0' androidxCoordinatorLayoutVersion = '1.2.0'
+117
View File
@@ -0,0 +1,117 @@
class WrappingMarquee {
#scrollDelay = 2000
#scrollSpeed = 30
/**
* @param {HTMLElement} el
*/
constructor(el) {
this.el = el
/** @type {HTMLElement} */
this.pEl = el?.firstElementChild
this.innerText = ''
this.isScrolling = false
/** @type {NodeJS.Timeout} */
this.timer = null
/** @type {number} */
this.animationId = null
}
/**
* Transparent gradient mask shown when text is scrolling left and overflowing right
*
* @param {boolean} showLeft
*/
setMask(showLeft) {
if (!this.el) return
this.el.style.maskImage = showLeft ? 'linear-gradient(90deg, transparent 0%, #fff 10%, #000 90%, transparent)' : 'linear-gradient(90deg, #000 90%, transparent)'
}
startScroll() {
if (this.isScrolling) {
console.warn('Already scrolling')
return
}
this.isScrolling = true
this.setMask(true)
let textScrollAmount = this.el.scrollWidth
this.pEl.innerHTML = this.innerText + '&nbsp;'.repeat(15)
let totalScrollAmount = this.el.scrollWidth
let scrollDuration = totalScrollAmount * this.#scrollSpeed
this.pEl.innerHTML = this.pEl.innerHTML + this.innerText
let done = false
let start, previousTimeStamp
const step = (timeStamp) => {
if (start === undefined) {
start = timeStamp
}
const elapsed = timeStamp - start
if (this.isScrolling && previousTimeStamp !== timeStamp) {
const amountToMove = Math.min(elapsed / scrollDuration * totalScrollAmount, totalScrollAmount)
this.pEl.style.transform = `translateX(-${amountToMove}px)`
if (amountToMove === totalScrollAmount) done = true
if (amountToMove > textScrollAmount) this.setMask(false)
}
if (!this.isScrolling || done) { // canceled or done
this.isScrolling = false
this.pEl.style.transform = 'translateX(0px)'
this.pEl.innerText = this.innerText
this.setMask(false)
if (done) {
this.startTimer()
}
} else if (elapsed < scrollDuration) { // step
previousTimeStamp = timeStamp
this.animationId = window.requestAnimationFrame(step)
}
}
this.animationId = window.requestAnimationFrame(step)
}
startTimer() {
clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.startScroll()
}, this.#scrollDelay)
}
reset() {
clearTimeout(this.timer)
this.timer = null
this.isScrolling = false
window.cancelAnimationFrame(this.animationId)
}
/**
* Initialize and start marquee if text overflows container
* resets the marquee if already active
*
* @param {string} innerText
*/
init(innerText) {
if (!this.el || !this.pEl) return
this.reset()
this.innerText = innerText
this.pEl.innerText = innerText
this.pEl.style.transform = 'translateX(0px)'
if (this.el.scrollWidth > this.el.clientWidth) {
this.setMask(false)
this.startTimer()
} else {
this.el.style.maskImage = ''
}
}
}
export default WrappingMarquee
-158
View File
@@ -1,158 +0,0 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global['fast-sort'] = {}));
}(this, (function (exports) {
'use strict';
// >>> INTERFACES <<<
// >>> HELPERS <<<
var castComparer = function (comparer) { return function (a, b, order) { return comparer(a, b, order) * order; }; };
var throwInvalidConfigErrorIfTrue = function (condition, context) {
if (condition)
throw Error("Invalid sort config: " + context);
};
var unpackObjectSorter = function (sortByObj) {
var _a = sortByObj || {}, asc = _a.asc, desc = _a.desc;
var order = asc ? 1 : -1;
var sortBy = (asc || desc);
// Validate object config
throwInvalidConfigErrorIfTrue(!sortBy, 'Expected `asc` or `desc` property');
throwInvalidConfigErrorIfTrue(asc && desc, 'Ambiguous object with `asc` and `desc` config properties');
var comparer = sortByObj.comparer && castComparer(sortByObj.comparer);
return { order: order, sortBy: sortBy, comparer: comparer };
};
// >>> SORTERS <<<
var multiPropertySorterProvider = function (defaultComparer) {
return function multiPropertySorter(sortBy, sortByArr, depth, order, comparer, a, b) {
var valA;
var valB;
if (typeof sortBy === 'string') {
valA = a[sortBy];
valB = b[sortBy];
}
else if (typeof sortBy === 'function') {
valA = sortBy(a);
valB = sortBy(b);
}
else {
var objectSorterConfig = unpackObjectSorter(sortBy);
return multiPropertySorter(objectSorterConfig.sortBy, sortByArr, depth, objectSorterConfig.order, objectSorterConfig.comparer || defaultComparer, a, b);
}
var equality = comparer(valA, valB, order);
if ((equality === 0 || (valA == null && valB == null)) &&
sortByArr.length > depth) {
return multiPropertySorter(sortByArr[depth], sortByArr, depth + 1, order, comparer, a, b);
}
return equality;
};
};
function getSortStrategy(sortBy, comparer, order) {
// Flat array sorter
if (sortBy === undefined || sortBy === true) {
return function (a, b) { return comparer(a, b, order); };
}
// Sort list of objects by single object key
if (typeof sortBy === 'string') {
throwInvalidConfigErrorIfTrue(sortBy.includes('.'), 'String syntax not allowed for nested properties.');
return function (a, b) { return comparer(a[sortBy], b[sortBy], order); };
}
// Sort list of objects by single function sorter
if (typeof sortBy === 'function') {
return function (a, b) { return comparer(sortBy(a), sortBy(b), order); };
}
// Sort by multiple properties
if (Array.isArray(sortBy)) {
var multiPropSorter_1 = multiPropertySorterProvider(comparer);
return function (a, b) { return multiPropSorter_1(sortBy[0], sortBy, 1, order, comparer, a, b); };
}
// Unpack object config to get actual sorter strategy
var objectSorterConfig = unpackObjectSorter(sortBy);
return getSortStrategy(objectSorterConfig.sortBy, objectSorterConfig.comparer || comparer, objectSorterConfig.order);
}
var sortArray = function (order, ctx, sortBy, comparer) {
var _a;
if (!Array.isArray(ctx)) {
return ctx;
}
// Unwrap sortBy if array with only 1 value to get faster sort strategy
if (Array.isArray(sortBy) && sortBy.length < 2) {
_a = sortBy, sortBy = _a[0];
}
return ctx.sort(getSortStrategy(sortBy, comparer, order));
};
// >>> Public <<<
var createNewSortInstance = function (opts) {
var comparer = castComparer(opts.comparer);
return function (_ctx) {
var ctx = Array.isArray(_ctx) && !opts.inPlaceSorting
? _ctx.slice()
: _ctx;
return {
/**
* Sort array in ascending order.
* @example
* sort([3, 1, 4]).asc();
* sort(users).asc(u => u.firstName);
* sort(users).asc([
* U => u.firstName
* u => u.lastName,
* ]);
*/
asc: function (sortBy) {
return sortArray(1, ctx, sortBy, comparer);
},
/**
* Sort array in descending order.
* @example
* sort([3, 1, 4]).desc();
* sort(users).desc(u => u.firstName);
* sort(users).desc([
* U => u.firstName
* u => u.lastName,
* ]);
*/
desc: function (sortBy) {
return sortArray(-1, ctx, sortBy, comparer);
},
/**
* Sort array in ascending or descending order. It allows sorting on multiple props
* in different order for each of them.
* @example
* sort(users).by([
* { asc: u => u.score }
* { desc: u => u.age }
* ]);
*/
by: function (sortBy) {
return sortArray(1, ctx, sortBy, comparer);
},
};
};
};
var defaultComparer = function (a, b, order) {
if (a == null)
return order;
if (b == null)
return -order;
if (a < b)
return -1;
if (a === b)
return 0;
return 1;
};
var sort = createNewSortInstance({
comparer: defaultComparer,
});
var inPlaceSort = createNewSortInstance({
comparer: defaultComparer,
inPlaceSorting: true,
});
exports.createNewSortInstance = createNewSortInstance;
exports.inPlaceSort = inPlaceSort;
exports.sort = sort;
Object.defineProperty(exports, '__esModule', { value: true });
})));
+7 -4
View File
@@ -7,7 +7,7 @@
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center mr-2 cursor-pointer"> <a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center mr-2 cursor-pointer">
<span class="material-icons text-3xl text-fg">arrow_back</span> <span class="material-icons text-3xl text-fg">arrow_back</span>
</a> </a>
<div v-if="user && currentLibrary"> <div v-if="user && currentLibrary && networkConnected">
<div class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal"> <div class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
<ui-library-icon :icon="currentLibraryIcon" :size="4" font-size="base" /> <ui-library-icon :icon="currentLibraryIcon" :size="4" font-size="base" />
<p class="text-sm leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p> <p class="text-sm leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
@@ -54,14 +54,17 @@ export default {
this.$store.commit('setCastAvailable', val) this.$store.commit('setCastAvailable', val)
} }
}, },
networkConnected() {
return this.$store.state.networkConnected
},
currentLibrary() { currentLibrary() {
return this.$store.getters['libraries/getCurrentLibrary'] return this.$store.getters['libraries/getCurrentLibrary']
}, },
currentLibraryName() { currentLibraryName() {
return this.currentLibrary ? this.currentLibrary.name : '' return this.currentLibrary?.name || ''
}, },
currentLibraryIcon() { currentLibraryIcon() {
return this.currentLibrary ? this.currentLibrary.icon : 'database' return this.currentLibrary?.icon || 'database'
}, },
showBack() { showBack() {
if (!this.$route.name) return true if (!this.$route.name) return true
@@ -71,7 +74,7 @@ export default {
return this.$store.state.user.user return this.$store.state.user.user
}, },
username() { username() {
return this.user ? this.user.username : 'err' return this.user?.username || 'err'
}, },
isCasting() { isCasting() {
return this.$store.state.isCasting return this.$store.state.isCasting
+172 -83
View File
@@ -15,7 +15,7 @@
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" :class="{ 'text-black text-opacity-75': coverBgIsLight }" style="font-size: 10px">{{ isDirectPlayMethod ? $strings.LabelPlaybackDirect : isLocalPlayMethod ? $strings.LabelPlaybackLocal : $strings.LabelPlaybackTranscode }}</p> <p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" :class="{ 'text-black text-opacity-75': coverBgIsLight }" style="font-size: 10px">{{ isDirectPlayMethod ? $strings.LabelPlaybackDirect : isLocalPlayMethod ? $strings.LabelPlaybackLocal : $strings.LabelPlaybackTranscode }}</p>
</div> </div>
<div v-if="useChapterTrack && useTotalTrack && showFullscreen" class="absolute total-track w-full z-30 px-6"> <div v-if="playerSettings.useChapterTrack && playerSettings.useTotalTrack && showFullscreen" class="absolute total-track w-full z-30 px-6">
<div class="flex"> <div class="flex">
<p class="font-mono text-fg" style="font-size: 0.8rem">{{ currentTimePretty }}</p> <p class="font-mono text-fg" style="font-size: 0.8rem">{{ currentTimePretty }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
@@ -41,7 +41,9 @@
</div> </div>
<div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor"> <div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor">
<p class="title-text truncate">{{ title }}</p> <div ref="titlewrapper" class="overflow-hidden relative">
<p class="title-text whitespace-nowrap"></p>
</div>
<p class="author-text text-fg text-opacity-75 truncate">{{ authorName }}</p> <p class="author-text text-fg text-opacity-75 truncate">{{ authorName }}</p>
</div> </div>
@@ -66,17 +68,17 @@
<div v-else class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: var(--gradient-minimized-audio-player)" /> <div v-else class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: var(--gradient-minimized-audio-player)" />
<div id="playerControls" class="absolute right-0 bottom-0 mx-auto" style="max-width: 414px"> <div id="playerControls" class="absolute right-0 bottom-0 mx-auto" style="max-width: 414px">
<div class="flex items-center max-w-full" :class="lockUi ? 'justify-center' : 'justify-between'"> <div class="flex items-center max-w-full" :class="playerSettings.lockUi ? 'justify-center' : 'justify-between'">
<span v-show="showFullscreen && !lockUi" class="material-icons next-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span> <span v-show="showFullscreen && !playerSettings.lockUi" class="material-icons next-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
<span v-show="!lockUi" class="material-icons jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">{{ jumpBackwardsIcon }}</span> <span v-show="!playerSettings.lockUi" class="material-icons jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">{{ jumpBackwardsIcon }}</span>
<div class="play-btn cursor-pointer shadow-sm flex items-center justify-center rounded-full text-primary mx-4 relative overflow-hidden" :style="{ backgroundColor: coverRgb }" :class="{ 'animate-spin': seekLoading }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick"> <div class="play-btn cursor-pointer shadow-sm flex items-center justify-center rounded-full text-primary mx-4 relative overflow-hidden" :style="{ backgroundColor: coverRgb }" :class="{ 'animate-spin': seekLoading }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
<div v-if="!coverBgIsLight" class="absolute top-0 left-0 w-full h-full bg-white bg-opacity-20 pointer-events-none" /> <div v-if="!coverBgIsLight" class="absolute top-0 left-0 w-full h-full bg-white bg-opacity-20 pointer-events-none" />
<span v-if="!isLoading" class="material-icons" :class="{ 'text-white': coverRgb && !coverBgIsLight }">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span> <span v-if="!isLoading" class="material-icons" :class="{ 'text-white': coverRgb && !coverBgIsLight }">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span>
<widgets-spinner-icon v-else class="h-8 w-8" /> <widgets-spinner-icon v-else class="h-8 w-8" />
</div> </div>
<span v-show="!lockUi" class="material-icons jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">{{ jumpForwardIcon }}</span> <span v-show="!playerSettings.lockUi" class="material-icons jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">{{ jumpForwardIcon }}</span>
<span v-show="showFullscreen && !lockUi" class="material-icons next-icon text-fg cursor-pointer" :class="nextChapter && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span> <span v-show="showFullscreen && !playerSettings.lockUi" class="material-icons next-icon text-fg cursor-pointer" :class="nextChapter && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
</div> </div>
</div> </div>
@@ -90,7 +92,7 @@
<div ref="readyTrack" class="h-full bg-track-buffered absolute top-0 left-0 rounded-full pointer-events-none" /> <div ref="readyTrack" class="h-full bg-track-buffered absolute top-0 left-0 rounded-full pointer-events-none" />
<div ref="bufferedTrack" class="h-full bg-track absolute top-0 left-0 rounded-full pointer-events-none" /> <div ref="bufferedTrack" class="h-full bg-track absolute top-0 left-0 rounded-full pointer-events-none" />
<div ref="playedTrack" class="h-full bg-track-cursor absolute top-0 left-0 rounded-full pointer-events-none" /> <div ref="playedTrack" class="h-full bg-track-cursor absolute top-0 left-0 rounded-full pointer-events-none" />
<div ref="trackCursor" class="h-7 w-7 rounded-full absolute pointer-events-auto flex items-center justify-center" :style="{ top: '-11px' }" :class="{ 'opacity-0': lockUi || !showFullscreen }" @touchstart="touchstartCursor"> <div ref="trackCursor" class="h-7 w-7 rounded-full absolute pointer-events-auto flex items-center justify-center" :style="{ top: '-11px' }" :class="{ 'opacity-0': playerSettings.lockUi || !showFullscreen }" @touchstart="touchstartCursor">
<div class="bg-track-cursor rounded-full w-3.5 h-3.5 pointer-events-none" /> <div class="bg-track-cursor rounded-full w-3.5 h-3.5 pointer-events-none" />
</div> </div>
</div> </div>
@@ -98,7 +100,7 @@
</div> </div>
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" :playback-rate="currentPlaybackRate" @select="selectChapter" /> <modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" :playback-rate="currentPlaybackRate" @select="selectChapter" />
<modals-dialog v-model="showMoreMenuDialog" :items="menuItems" @action="clickMenuAction" /> <modals-dialog v-model="showMoreMenuDialog" :items="menuItems" width="80vw" @action="clickMenuAction" />
</div> </div>
</template> </template>
@@ -106,6 +108,7 @@
import { Capacitor } from '@capacitor/core' import { Capacitor } from '@capacitor/core'
import { AbsAudioPlayer } from '@/plugins/capacitor' import { AbsAudioPlayer } from '@/plugins/capacitor'
import { FastAverageColor } from 'fast-average-color' import { FastAverageColor } from 'fast-average-color'
import WrappingMarquee from '@/assets/WrappingMarquee.js'
export default { export default {
props: { props: {
@@ -145,10 +148,12 @@ export default {
onPlaybackSpeedChangedListener: null, onPlaybackSpeedChangedListener: null,
touchStartY: 0, touchStartY: 0,
touchStartTime: 0, touchStartTime: 0,
touchEndY: 0, playerSettings: {
useChapterTrack: false, useChapterTrack: false,
useTotalTrack: true, useTotalTrack: true,
lockUi: false, scaleElapsedTimeBySpeed: true,
lockUi: false
},
isLoading: false, isLoading: false,
isDraggingCursor: false, isDraggingCursor: false,
draggingTouchStartX: 0, draggingTouchStartX: 0,
@@ -157,17 +162,22 @@ export default {
syncStatus: 0, syncStatus: 0,
showMoreMenuDialog: false, showMoreMenuDialog: false,
coverRgb: 'rgb(55, 56, 56)', coverRgb: 'rgb(55, 56, 56)',
coverBgIsLight: false coverBgIsLight: false,
titleMarquee: null,
isRefreshingUI: false
} }
}, },
watch: { watch: {
showFullscreen(val) { showFullscreen(val) {
this.updateScreenSize() this.updateScreenSize()
this.$store.commit('setPlayerFullscreen', !!val) this.$store.commit('setPlayerFullscreen', !!val)
document.querySelector('body').style.backgroundColor = this.showFullscreen ? this.coverRgb : "" document.querySelector('body').style.backgroundColor = this.showFullscreen ? this.coverRgb : ''
}, },
bookCoverAspectRatio() { bookCoverAspectRatio() {
this.updateScreenSize() this.updateScreenSize()
},
title(val) {
if (this.titleMarquee) this.titleMarquee.init(val)
} }
}, },
computed: { computed: {
@@ -187,17 +197,22 @@ export default {
{ {
text: this.$strings.LabelTotalTrack, text: this.$strings.LabelTotalTrack,
value: 'total_track', value: 'total_track',
icon: this.useTotalTrack ? 'check_box' : 'check_box_outline_blank' icon: this.playerSettings.useTotalTrack ? 'check_box' : 'check_box_outline_blank'
}, },
{ {
text: this.$strings.LabelChapterTrack, text: this.$strings.LabelChapterTrack,
value: 'chapter_track', value: 'chapter_track',
icon: this.useChapterTrack ? 'check_box' : 'check_box_outline_blank' icon: this.playerSettings.useChapterTrack ? 'check_box' : 'check_box_outline_blank'
}, },
{ {
text: this.lockUi ? this.$strings.LabelUnlockPlayer : this.$strings.LabelLockPlayer, text: this.$strings.LabelScaleElapsedTimeBySpeed,
value: 'scale_elapsed_time',
icon: this.playerSettings.scaleElapsedTimeBySpeed ? 'check_box' : 'check_box_outline_blank'
},
{
text: this.playerSettings.lockUi ? this.$strings.LabelUnlockPlayer : this.$strings.LabelLockPlayer,
value: 'lock', value: 'lock',
icon: this.lockUi ? 'lock' : 'lock_open' icon: this.playerSettings.lockUi ? 'lock' : 'lock_open'
}, },
{ {
text: this.$strings.LabelClosePlayer, text: this.$strings.LabelClosePlayer,
@@ -257,22 +272,22 @@ export default {
return this.mediaPlayer === 'cast-player' return this.mediaPlayer === 'cast-player'
}, },
mediaPlayer() { mediaPlayer() {
return this.playbackSession ? this.playbackSession.mediaPlayer : null return this.playbackSession?.mediaPlayer || null
}, },
mediaType() { mediaType() {
return this.playbackSession ? this.playbackSession.mediaType : null return this.playbackSession?.mediaType || null
}, },
isPodcast() { isPodcast() {
return this.mediaType === 'podcast' return this.mediaType === 'podcast'
}, },
mediaMetadata() { mediaMetadata() {
return this.playbackSession ? this.playbackSession.mediaMetadata : null return this.playbackSession?.mediaMetadata || null
}, },
libraryItem() { libraryItem() {
return this.playbackSession ? this.playbackSession.libraryItem || null : null return this.playbackSession?.libraryItem || null
}, },
localLibraryItem() { localLibraryItem() {
return this.playbackSession ? this.playbackSession.localLibraryItem || null : null return this.playbackSession?.localLibraryItem || null
}, },
localLibraryItemCoverSrc() { localLibraryItemCoverSrc() {
var localItemCover = this.localLibraryItem?.coverContentUrl || null var localItemCover = this.localLibraryItem?.coverContentUrl || null
@@ -280,7 +295,7 @@ export default {
return null return null
}, },
playMethod() { playMethod() {
return this.playbackSession ? this.playbackSession.playMethod : null return this.playbackSession?.playMethod || 0
}, },
isLocalPlayMethod() { isLocalPlayMethod() {
return this.playMethod == this.$constants.PlayMethod.LOCAL return this.playMethod == this.$constants.PlayMethod.LOCAL
@@ -289,19 +304,19 @@ export default {
return this.playMethod == this.$constants.PlayMethod.DIRECTPLAY return this.playMethod == this.$constants.PlayMethod.DIRECTPLAY
}, },
title() { title() {
if (this.currentChapterTitle && this.showFullscreen) return this.currentChapterTitle const mediaItemTitle = this.playbackSession?.displayTitle || this.mediaMetadata?.title || 'Title'
if (this.playbackSession) return this.playbackSession.displayTitle if (this.currentChapterTitle) {
return this.mediaMetadata ? this.mediaMetadata.title : 'Title' if (this.showFullscreen) return this.currentChapterTitle
return `${mediaItemTitle} | ${this.currentChapterTitle}`
}
return mediaItemTitle
}, },
authorName() { authorName() {
if (this.playbackSession) return this.playbackSession.displayAuthor if (this.playbackSession) return this.playbackSession.displayAuthor
return this.mediaMetadata ? this.mediaMetadata.authorName : 'Author' return this.mediaMetadata?.authorName || 'Author'
}, },
chapters() { chapters() {
if (this.playbackSession && this.playbackSession.chapters) { return this.playbackSession?.chapters || []
return this.playbackSession.chapters
}
return []
}, },
currentChapter() { currentChapter() {
if (!this.chapters.length) return null if (!this.chapters.length) return null
@@ -312,7 +327,7 @@ export default {
return this.chapters.find((c) => Number(Number(c.start).toFixed(2)) > this.currentTime) return this.chapters.find((c) => Number(Number(c.start).toFixed(2)) > this.currentTime)
}, },
currentChapterTitle() { currentChapterTitle() {
return this.currentChapter ? this.currentChapter.title : '' return this.currentChapter?.title || ''
}, },
currentChapterDuration() { currentChapterDuration() {
return this.currentChapter ? this.currentChapter.end - this.currentChapter.start : this.totalDuration return this.currentChapter ? this.currentChapter.end - this.currentChapter.start : this.totalDuration
@@ -322,11 +337,14 @@ export default {
}, },
currentTimePretty() { currentTimePretty() {
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
return this.$secondsToTimestamp(currentTimeToUse / this.currentPlaybackRate) if (this.playerSettings.scaleElapsedTimeBySpeed) {
currentTimeToUse = currentTimeToUse / this.currentPlaybackRate
}
return this.$secondsToTimestamp(currentTimeToUse)
}, },
timeRemaining() { timeRemaining() {
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
if (this.useChapterTrack && this.currentChapter) { if (this.playerSettings.useChapterTrack && this.currentChapter) {
var currChapTime = currentTimeToUse - this.currentChapter.start var currChapTime = currentTimeToUse - this.currentChapter.start
return (this.currentChapterDuration - currChapTime) / this.currentPlaybackRate return (this.currentChapterDuration - currChapTime) / this.currentPlaybackRate
} }
@@ -412,7 +430,11 @@ export default {
AbsAudioPlayer.requestSession() AbsAudioPlayer.requestSession()
}, },
clickContainer() { clickContainer() {
this.expandToFullscreen()
},
expandToFullscreen() {
this.showFullscreen = true this.showFullscreen = true
if (this.titleMarquee) this.titleMarquee.reset()
// Update track for total time bar if useChapterTrack is set // Update track for total time bar if useChapterTrack is set
this.$nextTick(() => { this.$nextTick(() => {
@@ -421,6 +443,8 @@ export default {
}, },
collapseFullscreen() { collapseFullscreen() {
this.showFullscreen = false this.showFullscreen = false
if (this.titleMarquee) this.titleMarquee.reset()
this.forceCloseDropdownMenu() this.forceCloseDropdownMenu()
}, },
async jumpNextChapter() { async jumpNextChapter() {
@@ -494,7 +518,7 @@ export default {
this.updateReadyTrack() this.updateReadyTrack()
}, },
updateReadyTrack() { updateReadyTrack() {
if (this.useChapterTrack) { if (this.playerSettings.useChapterTrack) {
if (this.$refs.totalReadyTrack) { if (this.$refs.totalReadyTrack) {
this.$refs.totalReadyTrack.style.width = this.readyTrackWidth + 'px' this.$refs.totalReadyTrack.style.width = this.readyTrackWidth + 'px'
} }
@@ -511,11 +535,14 @@ export default {
} }
let currentTime = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime let currentTime = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
if (this.useChapterTrack && this.currentChapter) { if (this.playerSettings.useChapterTrack && this.currentChapter) {
currentTime = Math.max(0, currentTime - this.currentChapter.start) currentTime = Math.max(0, currentTime - this.currentChapter.start)
} }
if (this.playerSettings.scaleElapsedTimeBySpeed) {
currentTime = currentTime / this.currentPlaybackRate
}
ts.innerText = this.$secondsToTimestamp(currentTime / this.currentPlaybackRate) ts.innerText = this.$secondsToTimestamp(currentTime)
}, },
timeupdate() { timeupdate() {
if (!this.$refs.playedTrack) { if (!this.$refs.playedTrack) {
@@ -543,7 +570,7 @@ export default {
let bufferedPercent = this.bufferedTime / this.totalDuration let bufferedPercent = this.bufferedTime / this.totalDuration
const totalBufferedPercent = bufferedPercent const totalBufferedPercent = bufferedPercent
if (this.useChapterTrack && this.currentChapter) { if (this.playerSettings.useChapterTrack && this.currentChapter) {
const currChapTime = currentTimeToUse - this.currentChapter.start const currChapTime = currentTimeToUse - this.currentChapter.start
percentDone = currChapTime / this.currentChapterDuration percentDone = currChapTime / this.currentChapterDuration
bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration)) bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration))
@@ -557,7 +584,7 @@ export default {
this.$refs.trackCursor.style.left = ptWidth - 14 + 'px' this.$refs.trackCursor.style.left = ptWidth - 14 + 'px'
} }
if (this.useChapterTrack) { if (this.playerSettings.useChapterTrack) {
if (this.$refs.totalPlayedTrack) this.$refs.totalPlayedTrack.style.width = Math.round(totalPercentDone * this.trackWidth) + 'px' if (this.$refs.totalPlayedTrack) this.$refs.totalPlayedTrack.style.width = Math.round(totalPercentDone * this.trackWidth) + 'px'
if (this.$refs.totalBufferedTrack) this.$refs.totalBufferedTrack.style.width = Math.round(totalBufferedPercent * this.trackWidth) + 'px' if (this.$refs.totalBufferedTrack) this.$refs.totalBufferedTrack.style.width = Math.round(totalBufferedPercent * this.trackWidth) + 'px'
} }
@@ -584,7 +611,7 @@ export default {
} }
}, },
async touchstartCursor(e) { async touchstartCursor(e) {
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.lockUi) return if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.playerSettings.lockUi) return
await this.$hapticsImpact() await this.$hapticsImpact()
this.isDraggingCursor = true this.isDraggingCursor = true
@@ -625,42 +652,49 @@ export default {
resetStream(startTime) { resetStream(startTime) {
this.closePlayback() this.closePlayback()
}, },
handleGesture() {
var touchDistance = this.touchEndY - this.touchStartY
if (touchDistance > 100) {
this.collapseFullscreen()
}
},
touchstart(e) { touchstart(e) {
if (!this.showFullscreen || !e.changedTouches) return if (!e.changedTouches || this.$store.state.globals.isModalOpen) return
if (e.pageX < 20) { const touchPosY = e.changedTouches[0].pageY
// when minimized only listen to touchstart on the player
if (!this.showFullscreen && touchPosY < window.innerHeight - 120) return
// for ios
if (!this.showFullscreen && e.pageX < 20) {
e.preventDefault() e.preventDefault()
e.stopImmediatePropagation() e.stopImmediatePropagation()
} }
this.touchStartY = e.changedTouches[0].screenY this.touchStartY = touchPosY
if (this.touchStartY > window.innerHeight / 3) {
// console.log('touch too low')
return
}
this.touchStartTime = Date.now() this.touchStartTime = Date.now()
}, },
touchend(e) { touchend(e) {
if (!e.changedTouches) return if (!e.changedTouches) return
const touchDuration = Date.now() - this.touchStartTime
const touchEndY = e.changedTouches[0].pageY
const touchDistanceY = touchEndY - this.touchStartY
// reset touch start data
this.touchStartTime = 0
this.touchStartY = 0
if (this.isDraggingCursor) { if (this.isDraggingCursor) {
if (this.draggingCurrentTime !== this.currentTime) { if (this.draggingCurrentTime !== this.currentTime) {
this.seek(this.draggingCurrentTime) this.seek(this.draggingCurrentTime)
} }
this.isDraggingCursor = false this.isDraggingCursor = false
} else if (this.showFullscreen) { } else {
this.touchEndY = e.changedTouches[0].screenY
var touchDuration = Date.now() - this.touchStartTime
if (touchDuration > 1200) { if (touchDuration > 1200) {
// console.log('touch too long', touchDuration) // console.log('touch too long', touchDuration)
return return
} }
this.handleGesture() if (this.showFullscreen) {
// Touch start higher than touchend
if (touchDistanceY > 100) {
this.collapseFullscreen()
}
} else if (touchDistanceY < -100) {
this.expandToFullscreen()
}
} }
}, },
touchmove(e) { touchmove(e) {
@@ -670,7 +704,7 @@ export default {
let duration = this.totalDuration let duration = this.totalDuration
let minTime = 0 let minTime = 0
let maxTime = duration let maxTime = duration
if (this.useChapterTrack && this.currentChapter) { if (this.playerSettings.useChapterTrack && this.currentChapter) {
duration = this.currentChapterDuration duration = this.currentChapterDuration
minTime = this.currentChapter.start minTime = this.currentChapter.start
maxTime = minTime + duration maxTime = minTime + duration
@@ -690,37 +724,40 @@ export default {
if (action === 'history') { if (action === 'history') {
this.$router.push(`/media/${this.mediaId}/history?title=${this.title}`) this.$router.push(`/media/${this.mediaId}/history?title=${this.title}`)
this.showFullscreen = false this.showFullscreen = false
} else if (action === 'scale_elapsed_time') {
this.playerSettings.scaleElapsedTimeBySpeed = !this.playerSettings.scaleElapsedTimeBySpeed
this.updateTimestamp()
this.savePlayerSettings()
} else if (action === 'lock') { } else if (action === 'lock') {
this.lockUi = !this.lockUi this.playerSettings.lockUi = !this.playerSettings.lockUi
this.$localStore.setPlayerLock(this.lockUi) this.savePlayerSettings()
} else if (action === 'chapter_track') { } else if (action === 'chapter_track') {
this.useChapterTrack = !this.useChapterTrack this.playerSettings.useChapterTrack = !this.playerSettings.useChapterTrack
this.useTotalTrack = !this.useChapterTrack || this.useTotalTrack this.playerSettings.useTotalTrack = !this.playerSettings.useChapterTrack || this.playerSettings.useTotalTrack
this.updateTimestamp() this.updateTimestamp()
this.updateTrack() this.updateTrack()
this.updateReadyTrack() this.updateReadyTrack()
this.updateUseChapterTrack() this.updateUseChapterTrack()
this.$localStore.setUseTotalTrack(this.useTotalTrack) this.savePlayerSettings()
} else if (action === 'total_track') { } else if (action === 'total_track') {
this.useTotalTrack = !this.useTotalTrack this.playerSettings.useTotalTrack = !this.playerSettings.useTotalTrack
this.useChapterTrack = !this.useTotalTrack || this.useChapterTrack this.playerSettings.useChapterTrack = !this.playerSettings.useTotalTrack || this.playerSettings.useChapterTrack
this.updateTimestamp() this.updateTimestamp()
this.updateTrack() this.updateTrack()
this.updateReadyTrack() this.updateReadyTrack()
this.updateUseChapterTrack() this.updateUseChapterTrack()
this.$localStore.setUseTotalTrack(this.useTotalTrack) this.savePlayerSettings()
} else if (action === 'close') { } else if (action === 'close') {
this.closePlayback() this.closePlayback()
} }
}) })
}, },
updateUseChapterTrack() { updateUseChapterTrack() {
this.$localStore.setUseChapterTrack(this.useChapterTrack)
// Chapter track in NowPlaying only supported on iOS for now // Chapter track in NowPlaying only supported on iOS for now
if (this.$platform === 'ios') { if (this.$platform === 'ios') {
AbsAudioPlayer.setChapterTrack({ enabled: this.useChapterTrack }) AbsAudioPlayer.setChapterTrack({ enabled: this.playerSettings.useChapterTrack })
} }
}, },
forceCloseDropdownMenu() { forceCloseDropdownMenu() {
@@ -739,6 +776,30 @@ export default {
this.isLoading = false this.isLoading = false
this.playbackSession = null this.playbackSession = null
}, },
async loadPlayerSettings() {
const savedPlayerSettings = await this.$localStore.getPlayerSettings()
if (!savedPlayerSettings) {
// In 0.9.72-beta 'useChapterTrack', 'useTotalTrack' and 'playerLock' was replaced with 'playerSettings' JSON object
// Check if this old key was set and if so migrate them over to 'playerSettings'
const chapterTrackPref = await this.$localStore.getPreferenceByKey('useChapterTrack')
if (chapterTrackPref) {
this.playerSettings.useChapterTrack = chapterTrackPref === '1'
const totalTrackPref = await this.$localStore.getPreferenceByKey('useTotalTrack')
this.playerSettings.useTotalTrack = totalTrackPref === '1'
const playerLockPref = await this.$localStore.getPreferenceByKey('playerLock')
this.playerSettings.lockUi = playerLockPref === '1'
}
this.savePlayerSettings()
} else {
this.playerSettings.useChapterTrack = !!savedPlayerSettings.useChapterTrack
this.playerSettings.useTotalTrack = !!savedPlayerSettings.useTotalTrack
this.playerSettings.lockUi = !!savedPlayerSettings.lockUi
this.playerSettings.scaleElapsedTimeBySpeed = !!savedPlayerSettings.scaleElapsedTimeBySpeed
}
},
savePlayerSettings() {
return this.$localStore.setPlayerSettings({ ...this.playerSettings })
},
// //
// Listeners from audio AbsAudioPlayer // Listeners from audio AbsAudioPlayer
// //
@@ -783,6 +844,10 @@ export default {
// Set track width // Set track width
this.$nextTick(() => { this.$nextTick(() => {
if (this.titleMarquee) this.titleMarquee.reset()
this.titleMarquee = new WrappingMarquee(this.$refs.titlewrapper)
this.titleMarquee.init(this.title)
if (this.$refs.track) { if (this.$refs.track) {
this.trackWidth = this.$refs.track.clientWidth this.trackWidth = this.$refs.track.clientWidth
} else { } else {
@@ -805,9 +870,7 @@ export default {
this.updateTimestamp() this.updateTimestamp()
}, },
async init() { async init() {
this.useChapterTrack = await this.$localStore.getUseChapterTrack() await this.loadPlayerSettings()
this.useTotalTrack = await this.$localStore.getUseTotalTrack()
this.lockUi = await this.$localStore.getPlayerLock()
this.onPlaybackSessionListener = AbsAudioPlayer.addListener('onPlaybackSession', this.onPlaybackSession) this.onPlaybackSessionListener = AbsAudioPlayer.addListener('onPlaybackSession', this.onPlaybackSession)
this.onPlaybackClosedListener = AbsAudioPlayer.addListener('onPlaybackClosed', this.onPlaybackClosed) this.onPlaybackClosedListener = AbsAudioPlayer.addListener('onPlaybackClosed', this.onPlaybackClosed)
@@ -818,26 +881,51 @@ export default {
this.onProgressSyncSuccess = AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess) this.onProgressSyncSuccess = AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
this.onPlaybackSpeedChangedListener = AbsAudioPlayer.addListener('onPlaybackSpeedChanged', this.onPlaybackSpeedChanged) this.onPlaybackSpeedChangedListener = AbsAudioPlayer.addListener('onPlaybackSpeedChanged', this.onPlaybackSpeedChanged)
}, },
screenOrientationChange() { async screenOrientationChange() {
setTimeout(() => { if (this.isRefreshingUI) return
this.updateScreenSize() this.isRefreshingUI = true
if (this.$refs.track) { const windowWidth = window.innerWidth
this.trackWidth = this.$refs.track.clientWidth this.refreshUI()
this.updateTrack()
this.updateReadyTrack() // Window width does not always change right away. Wait up to 250ms for a change.
// iPhone 10 on iOS 16 took between 100 - 200ms to update when going from portrait to landscape
// but landscape to portrait was immediate
for (let i = 0; i < 5; i++) {
await new Promise((resolve) => setTimeout(resolve, 50))
if (window.innerWidth !== windowWidth) {
this.refreshUI()
break
} }
}, 50) }
this.isRefreshingUI = false
},
refreshUI() {
this.updateScreenSize()
if (this.$refs.track) {
this.trackWidth = this.$refs.track.clientWidth
this.updateTrack()
this.updateReadyTrack()
}
}, },
updateScreenSize() { updateScreenSize() {
setTimeout(() => {
if (this.titleMarquee) this.titleMarquee.init(this.title)
}, 500)
this.windowHeight = window.innerHeight this.windowHeight = window.innerHeight
this.windowWidth = window.innerWidth this.windowWidth = window.innerWidth
const coverHeight = this.fullscreenBookCoverWidth * this.bookCoverAspectRatio const coverHeight = this.fullscreenBookCoverWidth * this.bookCoverAspectRatio
const coverImageWidthCollapsed = 46 / this.bookCoverAspectRatio const coverImageWidthCollapsed = 46 / this.bookCoverAspectRatio
const titleAuthorLeftOffsetCollapsed = 30 + coverImageWidthCollapsed
const titleAuthorWidthCollapsed = this.windowWidth - 128 - titleAuthorLeftOffsetCollapsed - 10
document.documentElement.style.setProperty('--cover-image-width', this.fullscreenBookCoverWidth + 'px') document.documentElement.style.setProperty('--cover-image-width', this.fullscreenBookCoverWidth + 'px')
document.documentElement.style.setProperty('--cover-image-height', coverHeight + 'px') document.documentElement.style.setProperty('--cover-image-height', coverHeight + 'px')
document.documentElement.style.setProperty('--cover-image-width-collapsed', coverImageWidthCollapsed + 'px') document.documentElement.style.setProperty('--cover-image-width-collapsed', coverImageWidthCollapsed + 'px')
document.documentElement.style.setProperty('--cover-image-height-collapsed', 46 + 'px') document.documentElement.style.setProperty('--cover-image-height-collapsed', 46 + 'px')
document.documentElement.style.setProperty('--title-author-left-offset-collapsed', 30 + coverImageWidthCollapsed + 'px') document.documentElement.style.setProperty('--title-author-left-offset-collapsed', titleAuthorLeftOffsetCollapsed + 'px')
document.documentElement.style.setProperty('--title-author-width-collapsed', titleAuthorWidthCollapsed + 'px')
}, },
minimizePlayerEvt() { minimizePlayerEvt() {
this.collapseFullscreen() this.collapseFullscreen()
@@ -905,6 +993,7 @@ export default {
--cover-image-width-collapsed: 46px; --cover-image-width-collapsed: 46px;
--cover-image-height-collapsed: 46px; --cover-image-height-collapsed: 46px;
--title-author-left-offset-collapsed: 80px; --title-author-left-offset-collapsed: 80px;
--title-author-width-collapsed: 40%;
} }
.playerContainer { .playerContainer {
@@ -952,7 +1041,7 @@ export default {
transition-property: left, bottom, width, height; transition-property: left, bottom, width, height;
transform-origin: left bottom; transform-origin: left bottom;
width: 40%; width: var(--title-author-width-collapsed);
bottom: 76px; bottom: 76px;
left: var(--title-author-left-offset-collapsed); left: var(--title-author-left-offset-collapsed);
text-align: left; text-align: left;
+13 -2
View File
@@ -11,6 +11,7 @@
<script> <script>
import { AbsAudioPlayer } from '@/plugins/capacitor' import { AbsAudioPlayer } from '@/plugins/capacitor'
import { Dialog } from '@capacitor/dialog' import { Dialog } from '@capacitor/dialog'
import CellularPermissionHelpers from '@/mixins/cellularPermissionHelpers'
export default { export default {
data() { data() {
@@ -39,6 +40,7 @@ export default {
serverEpisodeId: null serverEpisodeId: null
} }
}, },
mixins: [CellularPermissionHelpers],
computed: { computed: {
bookmarks() { bookmarks() {
if (!this.serverLibraryItemId) return [] if (!this.serverLibraryItemId) return []
@@ -193,12 +195,21 @@ export default {
const startTime = payload.startTime const startTime = payload.startTime
const startWhenReady = !payload.paused const startWhenReady = !payload.paused
const isLocal = libraryItemId.startsWith('local')
if (!isLocal) {
const hasPermission = await this.checkCellularPermission('streaming')
if (!hasPermission) {
this.$store.commit('setPlayerDoneStartingPlayback')
return
}
}
// When playing local library item and can also play this item from the server // When playing local library item and can also play this item from the server
// then store the server library item id so it can be used if a cast is made // then store the server library item id so it can be used if a cast is made
const serverLibraryItemId = payload.serverLibraryItemId || null const serverLibraryItemId = payload.serverLibraryItemId || null
const serverEpisodeId = payload.serverEpisodeId || null const serverEpisodeId = payload.serverEpisodeId || null
if (libraryItemId.startsWith('local') && this.$store.state.isCasting) { if (isLocal && this.$store.state.isCasting) {
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Warning', title: 'Warning',
message: `Cannot cast downloaded media items. Confirm to close cast and play on your device.` message: `Cannot cast downloaded media items. Confirm to close cast and play on your device.`
@@ -373,4 +384,4 @@ export default {
this.$eventBus.$off('device-focus-update', this.deviceFocused) this.$eventBus.$off('device-focus-update', this.deviceFocused)
} }
} }
</script> </script>
+14 -1
View File
@@ -79,7 +79,10 @@ export default {
return this.$store.state.serverSettings || {} return this.$store.state.serverSettings || {}
}, },
username() { username() {
return this.user ? this.user.username : '' return this.user?.username || ''
},
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
}, },
navItems() { navItems() {
var items = [ var items = [
@@ -132,6 +135,12 @@ export default {
}) })
if (this.serverConnectionConfig) { if (this.serverConnectionConfig) {
items.push({
icon: 'language',
text: this.$strings.ButtonGoToWebClient,
action: 'openWebClient'
})
items.push({ items.push({
icon: 'login', icon: 'login',
text: this.$strings.ButtonSwitchServerUser, text: this.$strings.ButtonSwitchServerUser,
@@ -151,6 +160,10 @@ export default {
if (action === 'logout') { if (action === 'logout') {
await this.logout() await this.logout()
this.$router.push('/connect') this.$router.push('/connect')
} else if (action === 'openWebClient') {
this.show = false
let path = `/library/${this.$store.state.libraries.currentLibraryId}`
await this.$store.dispatch('user/openWebClient', path)
} }
}, },
clickBackground() { clickBackground() {
+1 -1
View File
@@ -79,7 +79,7 @@ export default {
return this.page return this.page
}, },
hasFilter() { hasFilter() {
if (this.page === 'series' || this.page === 'collections') return false if (this.page === 'series' || this.page === 'collections' || this.page === 'playlists') return false
return this.filterBy !== 'all' return this.filterBy !== 'all'
}, },
orderBy() { orderBy() {
+15 -11
View File
@@ -7,9 +7,12 @@
<!-- Alternative bookshelf title/author/sort --> <!-- Alternative bookshelf title/author/sort -->
<div v-if="isAltViewEnabled" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }"> <div v-if="isAltViewEnabled" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }"> <div :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }" class="flex items-center">
{{ displayTitle }} <p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
</p> {{ displayTitle }}
</p>
<widgets-explicit-indicator v-if="isExplicit" />
</div>
<p class="truncate text-fg-muted" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || '&nbsp;' }}</p> <p class="truncate text-fg-muted" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || '&nbsp;' }}</p>
<p v-if="displaySortLine" class="truncate text-fg-muted" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p> <p v-if="displaySortLine" class="truncate text-fg-muted" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
</div> </div>
@@ -310,7 +313,7 @@ export default {
return !!this.userProgress?.isFinished return !!this.userProgress?.isFinished
}, },
showError() { showError() {
return this.numMissingParts || this.isMissing || this.isInvalid return this.isMissing || this.isInvalid
}, },
localLibraryItemId() { localLibraryItemId() {
if (this.isLocal) return this.libraryItemId if (this.isLocal) return this.libraryItemId
@@ -341,9 +344,8 @@ export default {
isInvalid() { isInvalid() {
return this._libraryItem.isInvalid return this._libraryItem.isInvalid
}, },
numMissingParts() { isExplicit() {
if (this.isPodcast) return 0 return !!this.mediaMetadata.explicit
return this.media.numMissingParts
}, },
overlayWrapperClasslist() { overlayWrapperClasslist() {
var classes = [] var classes = []
@@ -366,9 +368,6 @@ export default {
userCanDownload() { userCanDownload() {
return this.store.getters['user/getUserCanDownload'] return this.store.getters['user/getUserCanDownload']
}, },
userIsRoot() {
return this.store.getters['user/getIsRoot']
},
titleFontSize() { titleFontSize() {
return 0.75 * this.sizeMultiplier return 0.75 * this.sizeMultiplier
}, },
@@ -493,7 +492,12 @@ export default {
if (router) { if (router) {
if (this.recentEpisode) router.push(`/item/${this.libraryItemId}/${this.recentEpisode.id}`) if (this.recentEpisode) router.push(`/item/${this.libraryItemId}/${this.recentEpisode.id}`)
else if (this.collapsedSeries) router.push(`/bookshelf/series/${this.collapsedSeries.id}`) else if (this.collapsedSeries) router.push(`/bookshelf/series/${this.collapsedSeries.id}`)
else router.push(`/item/${this.libraryItemId}`) else if (this.localLibraryItem) {
// Pass local library item id to server page to allow falling back to offline page
router.push(`/item/${this.libraryItemId}?localLibraryItemId=${this.localLibraryItemId}`)
} else {
router.push(`/item/${this.libraryItemId}`)
}
} }
} }
}, },
+1 -5
View File
@@ -215,7 +215,7 @@ export default {
return !!this.userProgress?.isFinished return !!this.userProgress?.isFinished
}, },
showError() { showError() {
return this.numMissingParts || this.isMissing || this.isInvalid return this.isMissing || this.isInvalid
}, },
isStreaming() { isStreaming() {
return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId
@@ -235,10 +235,6 @@ export default {
isInvalid() { isInvalid() {
return this._libraryItem.isInvalid return this._libraryItem.isInvalid
}, },
numMissingParts() {
if (this.isPodcast) return 0
return this.media.numMissingParts
},
store() { store() {
return this.$store || this.$nuxt.$store return this.$store || this.$nuxt.$store
}, },
+1 -1
View File
@@ -45,7 +45,7 @@
<ui-text-input v-model="password" type="password" :disabled="processing" :placeholder="$strings.LabelPassword" class="w-full mb-2 text-lg" /> <ui-text-input v-model="password" type="password" :disabled="processing" :placeholder="$strings.LabelPassword" class="w-full mb-2 text-lg" />
<div class="flex items-center pt-2"> <div class="flex items-center pt-2">
<ui-icon-btn v-if="serverConfig.id" small bg-color="error" icon="delete" @click="removeServerConfigClick" /> <ui-icon-btn v-if="serverConfig.id" small bg-color="error" icon="delete" type="button" @click="removeServerConfigClick" />
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn :disabled="processing || !networkConnected" type="submit" class="mt-1 h-10">{{ networkConnected ? $strings.ButtonSubmit : $strings.MessageNoNetworkConnection }}</ui-btn> <ui-btn :disabled="processing || !networkConnected" type="submit" class="mt-1 h-10">{{ networkConnected ? $strings.ButtonSubmit : $strings.MessageNoNetworkConnection }}</ui-btn>
</div> </div>
+5 -5
View File
@@ -64,14 +64,14 @@ export default {
return this.$route.query || {} return this.$route.query || {}
}, },
entityTitle() { entityTitle() {
if (this.isPodcast) return this.$strings.LabelPodcasts if (this.page === 'library') {
if (this.page === 'library') return this.$strings.LabelBooks return this.isPodcast ? this.$strings.LabelPodcasts : this.$strings.LabelBooks
else if (this.page === 'series') { } else if (this.page === 'playlists') {
return this.$strings.ButtonPlaylists
} else if (this.page === 'series') {
return this.$strings.LabelSeries return this.$strings.LabelSeries
} else if (this.page === 'collections') { } else if (this.page === 'collections') {
return this.$strings.ButtonCollections return this.$strings.ButtonCollections
} else if (this.page === 'playlists') {
return this.$strings.ButtonPlaylists
} else if (this.page === 'authors') { } else if (this.page === 'authors') {
return this.$strings.LabelAuthors return this.$strings.LabelAuthors
} }
+1 -1
View File
@@ -40,7 +40,7 @@ export default {
default: '16px' default: '16px'
}, },
width: { width: {
type: Number, type: [String, Number],
default: 300 default: 300
} }
}, },
+8 -1
View File
@@ -210,7 +210,7 @@ export default {
] ]
}, },
sublistItems() { sublistItems() {
return (this[this.sublist] || []).map((item) => { const sublistItems = (this[this.sublist] || []).map((item) => {
if (typeof item === 'string') { if (typeof item === 'string') {
return { return {
text: item, text: item,
@@ -223,6 +223,13 @@ export default {
} }
} }
}) })
if (this.sublist === 'series') {
sublistItems.unshift({
text: this.$strings.MessageNoSeries,
value: this.$encode('no-series')
})
}
return sublistItems
}, },
filterData() { filterData() {
return this.$store.state.libraries.filterData || {} return this.$store.state.libraries.filterData || {}
+48
View File
@@ -26,6 +26,10 @@ export default {
type: Object, type: Object,
default: () => null default: () => null
}, },
playlist: {
type: Object,
default: () => null
},
hideRssFeedOption: Boolean hideRssFeedOption: Boolean
}, },
data() { data() {
@@ -92,6 +96,15 @@ export default {
} }
} }
// If on playlist page show remove from playlist option
if (this.playlist) {
items.push({
text: this.$strings.LabelRemoveFromPlaylist,
value: 'removeFromPlaylist',
icon: 'playlist_remove'
})
}
if (this.showRSSFeedOption) { if (this.showRSSFeedOption) {
items.push({ items.push({
text: this.rssFeed ? this.$strings.HeaderRSSFeed : this.$strings.HeaderOpenRSSFeed, text: this.rssFeed ? this.$strings.HeaderRSSFeed : this.$strings.HeaderOpenRSSFeed,
@@ -122,6 +135,14 @@ export default {
} }
} }
if (this.isConnectedToServer) {
items.push({
text: this.$strings.ButtonGoToWebClient,
value: 'openWebClient',
icon: 'language'
})
}
if (!this.episode) { if (!this.episode) {
items.push({ items.push({
text: this.$strings.LabelMoreInfo, text: this.$strings.LabelMoreInfo,
@@ -255,6 +276,8 @@ export default {
} else if (action === 'playlist') { } else if (action === 'playlist') {
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode: this.episode }]) this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode: this.episode }])
this.$store.commit('globals/setShowPlaylistsAddCreateModal', true) this.$store.commit('globals/setShowPlaylistsAddCreateModal', true)
} else if (action === 'removeFromPlaylist') {
this.removeFromPlaylistClick()
} else if (action === 'markFinished') { } else if (action === 'markFinished') {
if (this.episode) this.toggleEpisodeFinished() if (this.episode) this.toggleEpisodeFinished()
else this.toggleFinished() else this.toggleFinished()
@@ -270,6 +293,8 @@ export default {
this.clickRSSFeed() this.clickRSSFeed()
} else if (action === 'sendEbook') { } else if (action === 'sendEbook') {
this.showSendEbookDevicesModal = true this.showSendEbookDevicesModal = true
} else if (action === 'openWebClient') {
this.$store.dispatch('user/openWebClient', `/item/${this.serverLibraryItemId}`)
} }
}, },
async toggleFinished() { async toggleFinished() {
@@ -456,6 +481,29 @@ export default {
.finally(() => { .finally(() => {
this.$emit('update:processing', false) this.$emit('update:processing', false)
}) })
},
removeFromPlaylistClick() {
if (!this.playlist) {
this.$toast.error('Invalid: No Playlist')
return
}
this.$emit('update:processing', true)
let url = `/api/playlists/${this.playlist.id}/item/${this.serverLibraryItemId}`
if (this.serverEpisodeId) url += `/${this.serverEpisodeId}`
this.$nativeHttp
.delete(url)
.then(() => {
this.$toast.success('Item removed from playlist')
})
.catch((error) => {
const errorMsg = error.response?.data || 'Unknown error'
console.error('Failed to remove item from playlist', error)
this.$toast.error('Failed to remove from playlist: ' + errorMsg)
})
.finally(() => {
this.$emit('update:processing', false)
})
} }
}, },
mounted() {} mounted() {}
+8
View File
@@ -63,6 +63,10 @@ export default {
{ {
text: this.$strings.LabelFileModified, text: this.$strings.LabelFileModified,
value: 'mtimeMs' value: 'mtimeMs'
},
{
text: this.$strings.LabelRandomly,
value: 'random'
} }
], ],
podcastItems: [ podcastItems: [
@@ -89,6 +93,10 @@ export default {
{ {
text: this.$strings.LabelFileModified, text: this.$strings.LabelFileModified,
value: 'mtimeMs' value: 'mtimeMs'
},
{
text: this.$strings.LabelRandomly,
value: 'random'
} }
], ],
episodeItems: [ episodeItems: [
+1 -1
View File
@@ -71,7 +71,7 @@ export default {
} }
}, },
rates() { rates() {
return [0.5, 1, 1.2, 1.5, 2, 3] return [0.5, 1, 1.2, 1.5, 1.7, 2, 3]
}, },
canIncrement() { canIncrement() {
return this.playbackRate + 0.1 <= this.MAX_SPEED return this.playbackRate + 0.1 <= this.MAX_SPEED
+5 -8
View File
@@ -6,13 +6,7 @@
</div> </div>
</template> </template>
<div <div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center"
@click="
show = false
manualTimerModal = false
"
>
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-border" style="max-height: 75%" @click.stop> <div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-border" style="max-height: 75%" @click.stop>
<div v-if="manualTimerModal" class="p-4"> <div v-if="manualTimerModal" class="p-4">
<div class="flex mb-4" @click="manualTimerModal = false"> <div class="flex mb-4" @click="manualTimerModal = false">
@@ -71,7 +65,7 @@ export default {
}, },
data() { data() {
return { return {
manualTimerModal: null, manualTimerModal: false,
manualTimeoutMin: 1 manualTimeoutMin: 1
} }
}, },
@@ -81,6 +75,9 @@ export default {
return this.value return this.value
}, },
set(val) { set(val) {
if (!val) {
this.manualTimerModal = false
}
this.$emit('input', val) this.$emit('input', val)
} }
}, },
+1 -1
View File
@@ -349,7 +349,7 @@ export default {
} }
}, },
parseFilenames(filenames) { parseFilenames(filenames) {
const acceptableImages = ['.jpeg', '.jpg', '.png'] const acceptableImages = ['.jpeg', '.jpg', '.png', '.webp']
var imageFiles = filenames.filter((f) => { var imageFiles = filenames.filter((f) => {
return acceptableImages.includes((Path.extname(f) || '').toLowerCase()) return acceptableImages.includes((Path.extname(f) || '').toLowerCase())
}) })
+51 -13
View File
@@ -34,16 +34,18 @@ export default {
currentLocationNum: 0, currentLocationNum: 0,
currentLocationCfi: null, currentLocationCfi: null,
inittingDisplay: true, inittingDisplay: true,
isRefreshingUI: false,
ereaderSettings: { ereaderSettings: {
theme: 'dark', theme: 'dark',
fontScale: 100, fontScale: 100,
lineSpacing: 115 lineSpacing: 115,
textStroke: 0
} }
} }
}, },
watch: { watch: {
isPlayerOpen() { isPlayerOpen() {
this.updateHeight() this.refreshUI()
} }
}, },
computed: { computed: {
@@ -112,7 +114,8 @@ export default {
'*': { '*': {
color: `${fontColor}!important`, color: `${fontColor}!important`,
'background-color': `${backgroundColor}!important`, 'background-color': `${backgroundColor}!important`,
'line-height': this.ereaderSettings.lineSpacing + '%!important' 'line-height': this.ereaderSettings.lineSpacing + '%!important',
'-webkit-text-stroke': this.ereaderSettings.textStroke/100 + 'px ' + fontColor + '!important'
}, },
a: { a: {
color: `${fontColor}!important` color: `${fontColor}!important`
@@ -135,11 +138,6 @@ export default {
goToChapter(href) { goToChapter(href) {
return this.rendition?.display(href) return this.rendition?.display(href)
}, },
updateHeight() {
if (this.rendition && this.rendition.resize) {
this.rendition.resize(window.innerWidth, window.innerHeight - this.readerHeightOffset)
}
},
prev() { prev() {
if (this.rendition) { if (this.rendition) {
this.rendition.prev() this.rendition.prev()
@@ -382,14 +380,54 @@ export default {
this.rendition.getContents().forEach((c) => { this.rendition.getContents().forEach((c) => {
c.addStylesheetRules(this.themeRules) c.addStylesheetRules(this.themeRules)
}) })
},
async screenOrientationChange() {
if (this.isRefreshingUI) return
this.isRefreshingUI = true
const windowWidth = window.innerWidth
this.refreshUI()
// Window width does not always change right away. Wait up to 250ms for a change.
// iPhone 10 on iOS 16 took between 100 - 200ms to update when going from portrait to landscape
// but landscape to portrait was immediate
for (let i = 0; i < 5; i++) {
await new Promise((resolve) => setTimeout(resolve, 50))
if (window.innerWidth !== windowWidth) {
this.refreshUI()
break
}
}
this.isRefreshingUI = false
},
refreshUI() {
if (this.rendition?.resize) {
this.rendition.resize(window.innerWidth, window.innerHeight - this.readerHeightOffset)
}
} }
}, },
beforeDestroy() {
this.book?.destroy()
},
mounted() { mounted() {
this.initEpub() this.initEpub()
}
if (screen.orientation) {
// Not available on ios
screen.orientation.addEventListener('change', this.screenOrientationChange)
} else {
document.addEventListener('orientationchange', this.screenOrientationChange)
}
window.addEventListener('resize', this.screenOrientationChange)
},
beforeDestroy() {
this.book?.destroy()
if (screen.orientation) {
// Not available on ios
screen.orientation.removeEventListener('change', this.screenOrientationChange)
} else {
document.removeEventListener('orientationchange', this.screenOrientationChange)
}
window.removeEventListener('resize', this.screenOrientationChange)
},
} }
</script> </script>
@@ -404,4 +442,4 @@ export default {
max-height: calc(100% - 132px); max-height: calc(100% - 132px);
overflow: hidden; overflow: hidden;
} }
</style> </style>
+16 -11
View File
@@ -14,7 +14,7 @@
<div class="h-full flex items-center justify-center"> <div class="h-full flex items-center justify-center">
<div :style="{ width: pdfWidth + 'px', height: pdfHeight + 'px' }" class="w-full h-full overflow-auto"> <div :style="{ width: pdfWidth + 'px', height: pdfHeight + 'px' }" class="w-full h-full overflow-auto">
<div v-if="loadedRatio > 0 && loadedRatio < 1" style="background-color: green; color: white; text-align: center" :style="{ width: loadedRatio * 100 + '%' }">{{ Math.floor(loadedRatio * 100) }}%</div> <div v-if="loadedRatio > 0 && loadedRatio < 1" style="background-color: green; color: white; text-align: center" :style="{ width: loadedRatio * 100 + '%' }">{{ Math.floor(loadedRatio * 100) }}%</div>
<pdf ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md bg-white" :src="pdfDocInitParams" :page="page" :rotate="rotate" @progress="loadedRatio = $event" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event" @loaded="loadedEvt"></pdf> <pdf v-if="pdfDocInitParams" ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md bg-white" :src="pdfDocInitParams" :page="page" :rotate="rotate" @progress="loadedRatio = $event" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event" @loaded="loadedEvt"></pdf>
</div> </div>
</div> </div>
@@ -48,7 +48,8 @@ export default {
page: 1, page: 1,
numPages: 0, numPages: 0,
windowWidth: 0, windowWidth: 0,
windowHeight: 0 windowHeight: 0,
pdfDocInitParams: null
} }
}, },
computed: { computed: {
@@ -106,14 +107,6 @@ export default {
if (!this.userItemProgress?.ebookLocation || isNaN(this.userItemProgress.ebookLocation)) return 0 if (!this.userItemProgress?.ebookLocation || isNaN(this.userItemProgress.ebookLocation)) return 0
return Number(this.userItemProgress.ebookLocation) return Number(this.userItemProgress.ebookLocation)
}, },
pdfDocInitParams() {
return {
url: this.url,
httpHeaders: {
Authorization: `Bearer ${this.userToken}`
}
}
},
isPlayerOpen() { isPlayerOpen() {
return this.$store.getters['getIsPlayerOpen'] return this.$store.getters['getIsPlayerOpen']
} }
@@ -157,6 +150,8 @@ export default {
} }
}, },
numPagesLoaded(e) { numPagesLoaded(e) {
if (!e) return
this.numPages = e this.numPages = e
}, },
prev() { prev() {
@@ -175,6 +170,14 @@ export default {
screenOrientationChange() { screenOrientationChange() {
this.windowWidth = window.innerWidth this.windowWidth = window.innerWidth
this.windowHeight = window.innerHeight this.windowHeight = window.innerHeight
},
init() {
this.pdfDocInitParams = {
url: this.url,
httpHeaders: {
Authorization: `Bearer ${this.userToken}`
}
}
} }
}, },
mounted() { mounted() {
@@ -188,6 +191,8 @@ export default {
document.addEventListener('orientationchange', this.screenOrientationChange) document.addEventListener('orientationchange', this.screenOrientationChange)
} }
window.addEventListener('resize', this.screenOrientationChange) window.addEventListener('resize', this.screenOrientationChange)
this.init()
}, },
beforeDestroy() { beforeDestroy() {
if (screen.orientation) { if (screen.orientation) {
@@ -199,4 +204,4 @@ export default {
window.removeEventListener('resize', this.screenOrientationChange) window.removeEventListener('resize', this.screenOrientationChange)
} }
} }
</script> </script>
+14 -7
View File
@@ -82,6 +82,12 @@
</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="w-32">
<p class="text-base">{{ $strings.LabelFontBoldness }}:</p>
</div>
<ui-range-input v-model="ereaderSettings.textStroke" :min="0" :max="300" :step="5" input-width="180px" @input="settingsUpdated" />
</div>
<div class="flex items-center"> <div class="flex items-center">
<div class="w-32"> <div class="w-32">
<p class="text-base">{{ $strings.LabelLayout }}:</p> <p class="text-base">{{ $strings.LabelLayout }}:</p>
@@ -116,7 +122,8 @@ export default {
theme: 'dark', theme: 'dark',
fontScale: 100, fontScale: 100,
lineSpacing: 115, lineSpacing: 115,
spread: 'auto' spread: 'auto',
textStroke: 0
} }
} }
}, },
@@ -369,12 +376,12 @@ export default {
try { try {
const settings = localStorage.getItem('ereaderSettings') const settings = localStorage.getItem('ereaderSettings')
if (settings) { if (settings) {
const ereaderSettings = JSON.parse(settings) const _ereaderSettings = JSON.parse(settings)
if (!ereaderSettings.spread) { for (const key in this.ereaderSettings) {
// Added in 0.9.71 if (_ereaderSettings[key] !== undefined) {
ereaderSettings.spread = 'auto' this.ereaderSettings[key] = _ereaderSettings[key]
}
} }
this.ereaderSettings = ereaderSettings
this.settingsUpdated() this.settingsUpdated()
} }
} catch (error) { } catch (error) {
@@ -396,4 +403,4 @@ export default {
this.unregisterListeners() this.unregisterListeners()
} }
} }
</script> </script>
+5
View File
@@ -58,6 +58,7 @@
<script> <script>
import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor' import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor'
import cellularPermissionHelpers from '@/mixins/cellularPermissionHelpers'
export default { export default {
props: { props: {
@@ -73,6 +74,7 @@ export default {
}, },
isLocal: Boolean isLocal: Boolean
}, },
mixins: [cellularPermissionHelpers],
data() { data() {
return { return {
isProcessingReadUpdate: false, isProcessingReadUpdate: false,
@@ -180,6 +182,9 @@ export default {
async downloadClick() { async downloadClick() {
if (this.downloadItem || this.startingDownload) return if (this.downloadItem || this.startingDownload) return
const hasPermission = await this.checkCellularPermission('download')
if (!hasPermission) return
this.startingDownload = true this.startingDownload = true
setTimeout(() => { setTimeout(() => {
this.startingDownload = false this.startingDownload = false
+16 -4
View File
@@ -6,8 +6,11 @@
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId)" :width="40" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="md:hidden" /> <covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId)" :width="40" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="md:hidden" />
</div> </div>
<div class="flex-grow px-2"> <div class="flex-grow px-2">
<div class="-mt-0.5 mb-0.5" @click.stop> <div class="flex items-center">
<nuxt-link :to="`/item/${libraryItemId}`" class="text-sm text-fg underline">{{ podcast.metadata.title }}</nuxt-link> <div class="-mt-0.5 mb-0.5" @click.stop>
<nuxt-link :to="`/item/${libraryItemId}`" class="text-sm text-fg underline">{{ podcast.metadata.title }}</nuxt-link>
</div>
<widgets-explicit-indicator v-if="podcast.metadata.explicit" />
</div> </div>
<p v-if="publishedAt" class="text-xs text-fg-muted">{{ $dateDistanceFromNow(publishedAt) }}</p> <p v-if="publishedAt" class="text-xs text-fg-muted">{{ $dateDistanceFromNow(publishedAt) }}</p>
</div> </div>
@@ -58,6 +61,7 @@
<script> <script>
import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor' import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor'
import CellularPermissionHelpers from '@/mixins/cellularPermissionHelpers'
export default { export default {
props: { props: {
@@ -80,6 +84,7 @@ export default {
processing: false processing: false
} }
}, },
mixins: [CellularPermissionHelpers],
computed: { computed: {
bookCoverAspectRatio() { bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio'] return this.$store.getters['libraries/getBookCoverAspectRatio']
@@ -184,6 +189,10 @@ export default {
}, },
async downloadClick() { async downloadClick() {
if (this.downloadItem || this.pendingDownload) return if (this.downloadItem || this.pendingDownload) return
const hasPermission = await this.checkCellularPermission('download')
if (!hasPermission) return
this.pendingDownload = true this.pendingDownload = true
await this.$hapticsImpact() await this.$hapticsImpact()
if (this.isIos) { if (this.isIos) {
@@ -259,7 +268,6 @@ export default {
if (this.localEpisode && this.localLibraryItemId) { if (this.localEpisode && this.localLibraryItemId) {
console.log('Play local episode', this.localEpisode.id, this.localLibraryItemId) console.log('Play local episode', this.localEpisode.id, this.localLibraryItemId)
this.$eventBus.$emit('play-item', { this.$eventBus.$emit('play-item', {
libraryItemId: this.localLibraryItemId, libraryItemId: this.localLibraryItemId,
episodeId: this.localEpisode.id, episodeId: this.localEpisode.id,
@@ -282,7 +290,11 @@ export default {
const isFinished = !this.userIsFinished const isFinished = !this.userIsFinished
const localLibraryItemId = this.isLocal ? this.libraryItemId : this.localLibraryItemId const localLibraryItemId = this.isLocal ? this.libraryItemId : this.localLibraryItemId
const localEpisodeId = this.isLocal ? this.episode.id : this.localEpisode.id const localEpisodeId = this.isLocal ? this.episode.id : this.localEpisode.id
const payload = await this.$db.updateLocalMediaProgressFinished({ localLibraryItemId, localEpisodeId, isFinished }) const payload = await this.$db.updateLocalMediaProgressFinished({
localLibraryItemId,
localEpisodeId,
isFinished
})
console.log('toggleFinished payload', JSON.stringify(payload)) console.log('toggleFinished payload', JSON.stringify(payload))
if (payload?.error) { if (payload?.error) {
this.$toast.error(payload?.error || 'Unknown error') this.$toast.error(payload?.error || 'Unknown error')
+6 -2
View File
@@ -1,5 +1,5 @@
<template> <template>
<button class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" @mousedown.prevent :disabled="disabled || loading" :class="className" @click="clickBtn"> <button class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" :disabled="disabled || loading" :class="className" :type="type" @mousedown.prevent @click="clickBtn">
<div v-if="loading" class="text-fg absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100"> <div v-if="loading" class="text-fg absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24"> <svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" /> <path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
@@ -13,6 +13,10 @@
export default { export default {
props: { props: {
icon: String, icon: String,
type: {
type: String,
default: 'button'
},
disabled: Boolean, disabled: Boolean,
bgColor: { bgColor: {
type: String, type: String,
@@ -77,4 +81,4 @@ button.icon-btn:disabled::before {
button.icon-btn:disabled span { button.icon-btn:disabled span {
color: #777; color: #777;
} }
</style> </style>
+50
View File
@@ -0,0 +1,50 @@
<template>
<div>
<svg xmlns="http://www.w3.org/2000/svg" width="12px" height="12px" viewBox="0 0 512 512" class="ml-1">
<path
fill="white"
d="M 89.00,40.12
C 89.00,40.12 127.00,40.12 127.00,40.12
127.00,40.12 198.00,40.12 198.00,40.12
198.00,40.12 416.00,40.12 416.00,40.12
446.58,40.05 472.95,66.42 473.00,97.00
473.00,97.00 473.00,303.00 473.00,303.00
473.00,303.00 473.00,418.00 473.00,418.00
472.65,447.55 445.06,472.95 416.00,473.00
416.00,473.00 210.00,473.00 210.00,473.00
210.00,473.00 95.00,473.00 95.00,473.00
65.45,472.65 40.05,445.06 40.00,416.00
40.00,416.00 40.00,136.00 40.00,136.00
40.00,136.00 40.00,109.00 40.00,109.00
40.00,109.00 40.00,96.00 40.00,96.00
40.07,81.58 46.89,67.14 57.01,57.01
61.17,52.86 64.86,50.13 70.00,47.31
77.25,43.33 81.02,42.18 89.00,40.12 Z
M 337.00,121.00
C 337.00,121.00 175.00,121.00 175.00,121.00
175.00,121.00 175.00,392.00 175.00,392.00
175.00,392.00 337.00,392.00 337.00,392.00
337.00,392.00 337.00,349.00 337.00,349.00
337.00,349.00 226.00,349.00 226.00,349.00
226.00,349.00 226.00,274.00 226.00,274.00
226.00,274.00 332.00,274.00 332.00,274.00
332.00,274.00 332.00,232.00 332.00,232.00
332.00,232.00 226.00,232.00 226.00,232.00
226.00,232.00 226.00,164.00 226.00,164.00
226.00,164.00 337.00,164.00 337.00,164.00
337.00,164.00 337.00,121.00 337.00,121.00 Z"
/>
</svg>
</div>
</template>
<script>
export default {
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
+16 -4
View File
@@ -65,6 +65,9 @@
E9E8814D28DA6B9000D750C1 /* PlayerTimeUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E8814C28DA6B9000D750C1 /* PlayerTimeUtilsTests.swift */; }; E9E8814D28DA6B9000D750C1 /* PlayerTimeUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E8814C28DA6B9000D750C1 /* PlayerTimeUtilsTests.swift */; };
E9E985F828B02D9400957F23 /* PlayerProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E985F728B02D9400957F23 /* PlayerProgress.swift */; }; E9E985F828B02D9400957F23 /* PlayerProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E985F728B02D9400957F23 /* PlayerProgress.swift */; };
E9FA07E328C82848005520B0 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FA07E228C82848005520B0 /* Logger.swift */; }; E9FA07E328C82848005520B0 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FA07E228C82848005520B0 /* Logger.swift */; };
EACB38122BCCA1330060DA4A /* AudioPlayerRateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EACB38112BCCA1330060DA4A /* AudioPlayerRateManager.swift */; };
EACB38142BCCA1410060DA4A /* LegacyAudioPlayerRateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EACB38132BCCA1410060DA4A /* LegacyAudioPlayerRateManager.swift */; };
EACB38162BCCA1500060DA4A /* DefaultedAudioPlayerRateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EACB38152BCCA1500060DA4A /* DefaultedAudioPlayerRateManager.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -142,6 +145,9 @@
E9E8814C28DA6B9000D750C1 /* PlayerTimeUtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerTimeUtilsTests.swift; sourceTree = "<group>"; }; E9E8814C28DA6B9000D750C1 /* PlayerTimeUtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerTimeUtilsTests.swift; sourceTree = "<group>"; };
E9E985F728B02D9400957F23 /* PlayerProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerProgress.swift; sourceTree = "<group>"; }; E9E985F728B02D9400957F23 /* PlayerProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerProgress.swift; sourceTree = "<group>"; };
E9FA07E228C82848005520B0 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; }; E9FA07E228C82848005520B0 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
EACB38112BCCA1330060DA4A /* AudioPlayerRateManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioPlayerRateManager.swift; sourceTree = "<group>"; };
EACB38132BCCA1410060DA4A /* LegacyAudioPlayerRateManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyAudioPlayerRateManager.swift; sourceTree = "<group>"; };
EACB38152BCCA1500060DA4A /* DefaultedAudioPlayerRateManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultedAudioPlayerRateManager.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -183,6 +189,9 @@
3ABF6190280432610070250E /* player */ = { 3ABF6190280432610070250E /* player */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
EACB38152BCCA1500060DA4A /* DefaultedAudioPlayerRateManager.swift */,
EACB38132BCCA1410060DA4A /* LegacyAudioPlayerRateManager.swift */,
EACB38112BCCA1330060DA4A /* AudioPlayerRateManager.swift */,
E9E8814828DA641B00D750C1 /* util */, E9E8814828DA641B00D750C1 /* util */,
3A200C1427D64D7E00CBF02E /* AudioPlayer.swift */, 3A200C1427D64D7E00CBF02E /* AudioPlayer.swift */,
E9DFCBFA28C28F4A00B36356 /* AudioPlayerSleepTimer.swift */, E9DFCBFA28C28F4A00B36356 /* AudioPlayerSleepTimer.swift */,
@@ -521,6 +530,7 @@
E9D5506228AC1CC900C746DD /* PlayerState.swift in Sources */, E9D5506228AC1CC900C746DD /* PlayerState.swift in Sources */,
3AD4FCE728043E72006DB301 /* AbsDatabase.m in Sources */, 3AD4FCE728043E72006DB301 /* AbsDatabase.m in Sources */,
504EC3081FED79650016851F /* AppDelegate.swift in Sources */, 504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
EACB38122BCCA1330060DA4A /* AudioPlayerRateManager.swift in Sources */,
E9FA07E328C82848005520B0 /* Logger.swift in Sources */, E9FA07E328C82848005520B0 /* Logger.swift in Sources */,
3A90295F280968E700E1D427 /* PlaybackReport.swift in Sources */, 3A90295F280968E700E1D427 /* PlaybackReport.swift in Sources */,
E9D5505A28AC1C4500C746DD /* Folder.swift in Sources */, E9D5505A28AC1C4500C746DD /* Folder.swift in Sources */,
@@ -540,6 +550,7 @@
3A200C1527D64D7E00CBF02E /* AudioPlayer.swift in Sources */, 3A200C1527D64D7E00CBF02E /* AudioPlayer.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 */,
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */, 3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */,
3AB34053280829BF0039308B /* Extensions.swift in Sources */, 3AB34053280829BF0039308B /* Extensions.swift in Sources */,
E9D5505828AC1C1A00C746DD /* Library.swift in Sources */, E9D5505828AC1C1A00C746DD /* Library.swift in Sources */,
@@ -548,6 +559,7 @@
4DABC04F2B0139CA000F6264 /* User.swift in Sources */, 4DABC04F2B0139CA000F6264 /* User.swift in Sources */,
4D66B952282EE822008272D4 /* AbsDownloader.m in Sources */, 4D66B952282EE822008272D4 /* AbsDownloader.m in Sources */,
E9D5506828AC1DC300C746DD /* LocalPodcastEpisode.swift in Sources */, E9D5506828AC1DC300C746DD /* LocalPodcastEpisode.swift in Sources */,
EACB38162BCCA1500060DA4A /* DefaultedAudioPlayerRateManager.swift in Sources */,
E9D5505228AC1B5D00C746DD /* Chapter.swift in Sources */, E9D5505228AC1B5D00C746DD /* Chapter.swift in Sources */,
E9D5506028AC1CA900C746DD /* PlaybackMetadata.swift in Sources */, E9D5506028AC1CA900C746DD /* PlaybackMetadata.swift in Sources */,
E9D5504828AC1A7A00C746DD /* MediaType.swift in Sources */, E9D5504828AC1A7A00C746DD /* MediaType.swift in Sources */,
@@ -728,12 +740,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons; ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 28; CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_TEAM = 7UFJ7D8V6A; DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.71; MARKETING_VERSION = 0.9.75;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev; PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -752,12 +764,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons; ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 28; CURRENT_PROJECT_VERSION = 32;
DEVELOPMENT_TEAM = 7UFJ7D8V6A; DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.71; MARKETING_VERSION = 0.9.75;
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app; PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
+8 -1
View File
@@ -14,7 +14,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Override point for customization after application launch. // Override point for customization after application launch.
let configuration = Realm.Configuration( let configuration = Realm.Configuration(
schemaVersion: 16, schemaVersion: 18,
migrationBlock: { [weak self] migration, oldSchemaVersion in migrationBlock: { [weak self] migration, oldSchemaVersion in
if (oldSchemaVersion < 1) { if (oldSchemaVersion < 1) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)") self?.logger.log("Realm schema version was \(oldSchemaVersion)")
@@ -54,6 +54,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
newObject?["chapterTrack"] = false newObject?["chapterTrack"] = false
} }
} }
if (oldSchemaVersion < 17) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding downloadUsingCellular and streamingUsingCellular settings")
migration.enumerateObjects(ofType: PlayerSettings.className()) { oldObject, newObject in
newObject?["downloadUsingCellular"] = "ALWAYS"
newObject?["streamingUsingCellular"] = "ALWAYS"
}
}
} }
) )
+61 -34
View File
@@ -8,13 +8,16 @@
import Foundation import Foundation
import Capacitor import Capacitor
import RealmSwift import RealmSwift
import Network
@objc(AbsAudioPlayer) @objc(AbsAudioPlayer)
public class AbsAudioPlayer: CAPPlugin { public class AbsAudioPlayer: CAPPlugin {
private let logger = AppLogger(category: "AbsAudioPlayer") private let logger = AppLogger(category: "AbsAudioPlayer")
private var initialPlayWhenReady = false private var initialPlayWhenReady = false
private var monitor: NWPathMonitor?
private let queue = DispatchQueue.global(qos: .background)
override public func load() { override public func load() {
NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(sendPlaybackClosedEvent), name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(sendPlaybackClosedEvent), name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil)
@@ -24,20 +27,25 @@ public class AbsAudioPlayer: CAPPlugin {
NotificationCenter.default.addObserver(self, selector: #selector(sendSleepTimerEnded), name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(sendSleepTimerEnded), name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(onPlaybackFailed), name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(onPlaybackFailed), name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(onLocalMediaProgressUpdate), name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(onLocalMediaProgressUpdate), name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil)
self.bridge?.webView?.allowsBackForwardNavigationGestures = true; self.bridge?.webView?.allowsBackForwardNavigationGestures = true;
self.bridge?.webView?.scrollView.alwaysBounceVertical = false; self.bridge?.webView?.scrollView.alwaysBounceVertical = false;
setupNetworkMonitor()
} }
deinit {
monitor?.cancel()
}
@objc func onReady(_ call: CAPPluginCall) { @objc func onReady(_ call: CAPPluginCall) {
// TODO: Was used to notify when Abs UI was ready so that last played media could be opened - this was buggy and removed // TODO: Was used to notify when Abs UI was ready so that last played media could be opened - this was buggy and removed
call.resolve() call.resolve()
} }
@objc func startPlaybackSession(_ session: PlaybackSession, playWhenReady: Bool, playbackRate: Float) throws { @objc func startPlaybackSession(_ session: PlaybackSession, playWhenReady: Bool, playbackRate: Float) throws {
guard let libraryItemId = session.libraryItemId else { throw PlayerError.libraryItemIdNotSpecified } guard let libraryItemId = session.libraryItemId else { throw PlayerError.libraryItemIdNotSpecified }
self.sendPrepareMetadataEvent(itemId: libraryItemId, playWhenReady: playWhenReady) self.sendPrepareMetadataEvent(itemId: libraryItemId, playWhenReady: playWhenReady)
self.sendPlaybackSession(session: try session.asDictionary()) self.sendPlaybackSession(session: try session.asDictionary())
PlayerHandler.startPlayback(sessionId: session.id, playWhenReady: playWhenReady, playbackRate: playbackRate) PlayerHandler.startPlayback(sessionId: session.id, playWhenReady: playWhenReady, playbackRate: playbackRate)
@@ -50,14 +58,14 @@ public class AbsAudioPlayer: CAPPlugin {
let playWhenReady = call.getBool("playWhenReady", true) let playWhenReady = call.getBool("playWhenReady", true)
let playbackRate = call.getFloat("playbackRate", 1) let playbackRate = call.getFloat("playbackRate", 1)
let startTimeOverride = call.getDouble("startTime") let startTimeOverride = call.getDouble("startTime")
if libraryItemId == nil { if libraryItemId == nil {
logger.error("provide library item id") logger.error("provide library item id")
return call.resolve() return call.resolve()
} }
PlayerHandler.stopPlayback() PlayerHandler.stopPlayback()
let isLocalItem = libraryItemId?.starts(with: "local_") ?? false let isLocalItem = libraryItemId?.starts(with: "local_") ?? false
if (isLocalItem) { if (isLocalItem) {
let item = Database.shared.getLocalLibraryItem(localLibraryItemId: libraryItemId!) let item = Database.shared.getLocalLibraryItem(localLibraryItemId: libraryItemId!)
@@ -66,7 +74,7 @@ public class AbsAudioPlayer: CAPPlugin {
logger.error("Failed to get local playback session") logger.error("Failed to get local playback session")
return call.resolve([:]) return call.resolve([:])
} }
do { do {
if (startTimeOverride != nil) { if (startTimeOverride != nil) {
playbackSession.currentTime = startTimeOverride! playbackSession.currentTime = startTimeOverride!
@@ -96,14 +104,14 @@ public class AbsAudioPlayer: CAPPlugin {
} }
} }
} }
@objc func closePlayback(_ call: CAPPluginCall) { @objc func closePlayback(_ call: CAPPluginCall) {
logger.log("Close playback") logger.log("Close playback")
PlayerHandler.stopPlayback() PlayerHandler.stopPlayback()
call.resolve() call.resolve()
} }
@objc func getCurrentTime(_ call: CAPPluginCall) { @objc func getCurrentTime(_ call: CAPPluginCall) {
call.resolve([ call.resolve([
"value": PlayerHandler.getCurrentTime() ?? 0, "value": PlayerHandler.getCurrentTime() ?? 0,
@@ -130,7 +138,7 @@ public class AbsAudioPlayer: CAPPlugin {
PlayerHandler.setChapterTrack() PlayerHandler.setChapterTrack()
call.resolve() call.resolve()
} }
@objc func playPlayer(_ call: CAPPluginCall) { @objc func playPlayer(_ call: CAPPluginCall) {
PlayerHandler.paused = false PlayerHandler.paused = false
call.resolve() call.resolve()
@@ -144,7 +152,7 @@ public class AbsAudioPlayer: CAPPlugin {
PlayerHandler.paused = !PlayerHandler.paused PlayerHandler.paused = !PlayerHandler.paused
call.resolve([ "playing": !PlayerHandler.paused ]) call.resolve([ "playing": !PlayerHandler.paused ])
} }
@objc func seek(_ call: CAPPluginCall) { @objc func seek(_ call: CAPPluginCall) {
PlayerHandler.seek(amount: call.getDouble("value", 0.0)) PlayerHandler.seek(amount: call.getDouble("value", 0.0))
call.resolve() call.resolve()
@@ -157,7 +165,7 @@ public class AbsAudioPlayer: CAPPlugin {
PlayerHandler.seekBackward(amount: call.getDouble("value", 0.0)) PlayerHandler.seekBackward(amount: call.getDouble("value", 0.0))
call.resolve() call.resolve()
} }
@objc func sendMetadata() { @objc func sendMetadata() {
self.notifyListeners("onPlayingUpdate", data: [ "value": !PlayerHandler.paused ]) self.notifyListeners("onPlayingUpdate", data: [ "value": !PlayerHandler.paused ])
if let metadata = try? PlayerHandler.getMetdata()?.asDictionary() { if let metadata = try? PlayerHandler.getMetdata()?.asDictionary() {
@@ -167,34 +175,34 @@ public class AbsAudioPlayer: CAPPlugin {
@objc func sendPlaybackClosedEvent() { @objc func sendPlaybackClosedEvent() {
self.notifyListeners("onPlaybackClosed", data: [ "value": true ]) self.notifyListeners("onPlaybackClosed", data: [ "value": true ])
} }
@objc func decreaseSleepTime(_ call: CAPPluginCall) { @objc func decreaseSleepTime(_ call: CAPPluginCall) {
guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) } guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) }
guard let time = Double(timeString) else { return call.resolve([ "success": false ]) } guard let time = Double(timeString) else { return call.resolve([ "success": false ]) }
guard let _ = PlayerHandler.getSleepTimeRemaining() else { return call.resolve([ "success": false ]) } guard let _ = PlayerHandler.getSleepTimeRemaining() else { return call.resolve([ "success": false ]) }
let seconds = time/1000 let seconds = time/1000
PlayerHandler.decreaseSleepTime(decreaseSeconds: seconds) PlayerHandler.decreaseSleepTime(decreaseSeconds: seconds)
call.resolve() call.resolve()
} }
@objc func increaseSleepTime(_ call: CAPPluginCall) { @objc func increaseSleepTime(_ call: CAPPluginCall) {
guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) } guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) }
guard let time = Double(timeString) else { return call.resolve([ "success": false ]) } guard let time = Double(timeString) else { return call.resolve([ "success": false ]) }
guard let _ = PlayerHandler.getSleepTimeRemaining() else { return call.resolve([ "success": false ]) } guard let _ = PlayerHandler.getSleepTimeRemaining() else { return call.resolve([ "success": false ]) }
let seconds = time/1000 let seconds = time/1000
PlayerHandler.increaseSleepTime(increaseSeconds: seconds) PlayerHandler.increaseSleepTime(increaseSeconds: seconds)
call.resolve() call.resolve()
} }
@objc func setSleepTimer(_ call: CAPPluginCall) { @objc func setSleepTimer(_ call: CAPPluginCall) {
guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) } guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) }
guard let time = Double(timeString) else { return call.resolve([ "success": false ]) } guard let time = Double(timeString) else { return call.resolve([ "success": false ]) }
let isChapterTime = call.getBool("isChapterTime", false) let isChapterTime = call.getBool("isChapterTime", false)
let seconds = time / 1000 let seconds = time / 1000
logger.log("chapter time: \(isChapterTime)") logger.log("chapter time: \(isChapterTime)")
if isChapterTime { if isChapterTime {
PlayerHandler.setChapterSleepTime(stopAt: seconds) PlayerHandler.setChapterSleepTime(stopAt: seconds)
@@ -204,30 +212,30 @@ public class AbsAudioPlayer: CAPPlugin {
call.resolve([ "success": true ]) call.resolve([ "success": true ])
} }
} }
@objc func cancelSleepTimer(_ call: CAPPluginCall) { @objc func cancelSleepTimer(_ call: CAPPluginCall) {
PlayerHandler.cancelSleepTime() PlayerHandler.cancelSleepTime()
call.resolve() call.resolve()
} }
@objc func getSleepTimerTime(_ call: CAPPluginCall) { @objc func getSleepTimerTime(_ call: CAPPluginCall) {
call.resolve([ call.resolve([
"value": PlayerHandler.getSleepTimeRemaining() ?? 0 "value": PlayerHandler.getSleepTimeRemaining() ?? 0
]) ])
} }
@objc func sendSleepTimerEnded() { @objc func sendSleepTimerEnded() {
self.notifyListeners("onSleepTimerEnded", data: [ self.notifyListeners("onSleepTimerEnded", data: [
"value": PlayerHandler.getCurrentTime() ?? 0 "value": PlayerHandler.getCurrentTime() ?? 0
]) ])
} }
@objc func sendSleepTimerSet() { @objc func sendSleepTimerSet() {
self.notifyListeners("onSleepTimerSet", data: [ self.notifyListeners("onSleepTimerSet", data: [
"value": PlayerHandler.getSleepTimeRemaining() ?? 0 "value": PlayerHandler.getSleepTimeRemaining() ?? 0
]) ])
} }
@objc func onLocalMediaProgressUpdate() { @objc func onLocalMediaProgressUpdate() {
guard let localMediaProgressId = PlayerHandler.getPlaybackSession()?.localMediaProgressId else { return } guard let localMediaProgressId = PlayerHandler.getPlaybackSession()?.localMediaProgressId else { return }
guard let localMediaProgress = Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId) else { return } guard let localMediaProgress = Database.shared.getLocalMediaProgress(localMediaProgressId: localMediaProgressId) else { return }
@@ -235,7 +243,7 @@ public class AbsAudioPlayer: CAPPlugin {
logger.log("Sending local progress back to the UI") logger.log("Sending local progress back to the UI")
self.notifyListeners("onLocalMediaProgressUpdate", data: progressUpdate) self.notifyListeners("onLocalMediaProgressUpdate", data: progressUpdate)
} }
@objc func onPlaybackFailed() { @objc func onPlaybackFailed() {
if (PlayerHandler.getPlayMethod() == PlayMethod.directplay.rawValue) { if (PlayerHandler.getPlayMethod() == PlayMethod.directplay.rawValue) {
let session = PlayerHandler.getPlaybackSession() let session = PlayerHandler.getPlaybackSession()
@@ -243,7 +251,7 @@ public class AbsAudioPlayer: CAPPlugin {
let libraryItemId = session?.libraryItemId ?? "" let libraryItemId = session?.libraryItemId ?? ""
let episodeId = session?.episodeId ?? nil let episodeId = session?.episodeId ?? nil
logger.log("Forcing Transcode") logger.log("Forcing Transcode")
// If direct playing then fallback to transcode // If direct playing then fallback to transcode
ApiClient.startPlaybackSession(libraryItemId: libraryItemId, episodeId: episodeId, forceTranscode: true) { [weak self] session in ApiClient.startPlaybackSession(libraryItemId: libraryItemId, episodeId: episodeId, forceTranscode: true) { [weak self] session in
do { do {
@@ -262,19 +270,38 @@ public class AbsAudioPlayer: CAPPlugin {
"value": "Playback Error" "value": "Playback Error"
]) ])
} }
} }
@objc func sendPrepareMetadataEvent(itemId: String, playWhenReady: Bool) { @objc func sendPrepareMetadataEvent(itemId: String, playWhenReady: Bool) {
self.notifyListeners("onPrepareMedia", data: [ self.notifyListeners("onPrepareMedia", data: [
"audiobookId": itemId, "audiobookId": itemId,
"playWhenReady": playWhenReady, "playWhenReady": playWhenReady,
]) ])
} }
@objc func sendPlaybackSession(session: [String: Any]) { @objc func sendPlaybackSession(session: [String: Any]) {
self.notifyListeners("onPlaybackSession", data: session) self.notifyListeners("onPlaybackSession", data: session)
} }
private func setupNetworkMonitor() {
monitor = NWPathMonitor()
monitor?.pathUpdateHandler = { [weak self] path in
guard let self = self else { return }
let isUnmetered = !path.isExpensive && !path.isConstrained
DispatchQueue.main.async {
self.notifyNetworkMeteredChanged(isUnmetered: isUnmetered)
}
}
monitor?.start(queue: queue)
}
private func notifyNetworkMeteredChanged(isUnmetered: Bool) {
let data: [String: Any] = ["value": isUnmetered]
self.notifyListeners("onNetworkMeteredChanged", data: data)
}
} }
enum PlayerError: String, Error { enum PlayerError: String, Error {
+46 -37
View File
@@ -29,20 +29,20 @@ extension String {
@objc(AbsDatabase) @objc(AbsDatabase)
public class AbsDatabase: CAPPlugin { public class AbsDatabase: CAPPlugin {
private let logger = AppLogger(category: "AbsDatabase") private let logger = AppLogger(category: "AbsDatabase")
@objc func setCurrentServerConnectionConfig(_ call: CAPPluginCall) { @objc func setCurrentServerConnectionConfig(_ call: CAPPluginCall) {
var id = call.getString("id") var id = call.getString("id")
let address = call.getString("address", "") let address = call.getString("address", "")
let userId = call.getString("userId", "") let userId = call.getString("userId", "")
let username = call.getString("username", "") let username = call.getString("username", "")
let token = call.getString("token", "") let token = call.getString("token", "")
let name = "\(address) (\(username))" let name = "\(address) (\(username))"
if id == nil { if id == nil {
id = "\(address)@\(username)".toBase64() id = "\(address)@\(username)".toBase64()
} }
let config = ServerConnectionConfig() let config = ServerConnectionConfig()
config.id = id ?? "" config.id = id ?? ""
config.index = 0 config.index = 0
@@ -51,7 +51,7 @@ public class AbsDatabase: CAPPlugin {
config.userId = userId config.userId = userId
config.username = username config.username = username
config.token = token config.token = token
Store.serverConfig = config Store.serverConfig = config
let savedConfig = Store.serverConfig // Fetch the latest value let savedConfig = Store.serverConfig // Fetch the latest value
call.resolve(convertServerConnectionConfigToJSON(config: savedConfig!)) call.resolve(convertServerConnectionConfigToJSON(config: savedConfig!))
@@ -59,26 +59,26 @@ public class AbsDatabase: CAPPlugin {
@objc func removeServerConnectionConfig(_ call: CAPPluginCall) { @objc func removeServerConnectionConfig(_ call: CAPPluginCall) {
let id = call.getString("serverConnectionConfigId", "") let id = call.getString("serverConnectionConfigId", "")
Database.shared.deleteServerConnectionConfig(id: id) Database.shared.deleteServerConnectionConfig(id: id)
call.resolve() call.resolve()
} }
@objc func logout(_ call: CAPPluginCall) { @objc func logout(_ call: CAPPluginCall) {
Store.serverConfig = nil Store.serverConfig = nil
call.resolve() call.resolve()
} }
@objc func getDeviceData(_ call: CAPPluginCall) { @objc func getDeviceData(_ call: CAPPluginCall) {
let configs = Database.shared.getServerConnectionConfigs() let configs = Database.shared.getServerConnectionConfigs()
let index = Database.shared.getLastActiveConfigIndex() let index = Database.shared.getLastActiveConfigIndex()
let settings = Database.shared.getDeviceSettings() let settings = Database.shared.getDeviceSettings()
call.resolve([ call.resolve([
"serverConnectionConfigs": configs.map { config in convertServerConnectionConfigToJSON(config: config) }, "serverConnectionConfigs": configs.map { config in convertServerConnectionConfigToJSON(config: config) },
"lastServerConnectionConfigId": configs.first { config in config.index == index }?.id as Any, "lastServerConnectionConfigId": configs.first { config in config.index == index }?.id as Any,
"deviceSettings": deviceSettingsToJSON(settings: settings) "deviceSettings": deviceSettingsToJSON(settings: settings)
]) ])
} }
@objc func getLocalLibraryItems(_ call: CAPPluginCall) { @objc func getLocalLibraryItems(_ call: CAPPluginCall) {
do { do {
let items = Database.shared.getLocalLibraryItems() let items = Database.shared.getLocalLibraryItems()
@@ -89,7 +89,7 @@ public class AbsDatabase: CAPPlugin {
call.resolve() call.resolve()
} }
} }
@objc func getLocalLibraryItem(_ call: CAPPluginCall) { @objc func getLocalLibraryItem(_ call: CAPPluginCall) {
do { do {
let item = Database.shared.getLocalLibraryItem(localLibraryItemId: call.getString("id") ?? "") let item = Database.shared.getLocalLibraryItem(localLibraryItemId: call.getString("id") ?? "")
@@ -105,7 +105,7 @@ public class AbsDatabase: CAPPlugin {
call.resolve() call.resolve()
} }
} }
@objc func getLocalLibraryItemByLId(_ call: CAPPluginCall) { @objc func getLocalLibraryItemByLId(_ call: CAPPluginCall) {
do { do {
let item = Database.shared.getLocalLibraryItem(byServerLibraryItemId: call.getString("libraryItemId") ?? "") let item = Database.shared.getLocalLibraryItem(byServerLibraryItemId: call.getString("libraryItemId") ?? "")
@@ -121,11 +121,11 @@ public class AbsDatabase: CAPPlugin {
call.resolve() call.resolve()
} }
} }
@objc func getLocalLibraryItemsInFolder(_ call: CAPPluginCall) { @objc func getLocalLibraryItemsInFolder(_ call: CAPPluginCall) {
call.resolve([ "value": [] ]) call.resolve([ "value": [] ])
} }
@objc func getAllLocalMediaProgress(_ call: CAPPluginCall) { @objc func getAllLocalMediaProgress(_ call: CAPPluginCall) {
do { do {
call.resolve([ "value": try Database.shared.getAllLocalMediaProgress().asDictionaryArray() ]) call.resolve([ "value": try Database.shared.getAllLocalMediaProgress().asDictionaryArray() ])
@@ -135,7 +135,7 @@ public class AbsDatabase: CAPPlugin {
call.resolve(["value": []]) call.resolve(["value": []])
} }
} }
@objc func removeLocalMediaProgress(_ call: CAPPluginCall) { @objc func removeLocalMediaProgress(_ call: CAPPluginCall) {
let localMediaProgressId = call.getString("localMediaProgressId") let localMediaProgressId = call.getString("localMediaProgressId")
guard let localMediaProgressId = localMediaProgressId else { guard let localMediaProgressId = localMediaProgressId else {
@@ -145,26 +145,27 @@ public class AbsDatabase: CAPPlugin {
try? Database.shared.removeLocalMediaProgress(localMediaProgressId: localMediaProgressId) try? Database.shared.removeLocalMediaProgress(localMediaProgressId: localMediaProgressId)
call.resolve() call.resolve()
} }
@objc func syncLocalSessionsWithServer(_ call: CAPPluginCall) { @objc func syncLocalSessionsWithServer(_ call: CAPPluginCall) {
logger.log("syncLocalSessionsWithServer: Starting") let isFirstSync = call.getBool("isFirstSync", false)
logger.log("syncLocalSessionsWithServer: Starting (First sync: \(isFirstSync))")
guard Store.serverConfig != nil else { guard Store.serverConfig != nil else {
call.reject("syncLocalSessionsWithServer not connected to server") call.reject("syncLocalSessionsWithServer not connected to server")
return call.resolve() return call.resolve()
} }
Task { Task {
await ApiClient.syncLocalSessionsWithServer() await ApiClient.syncLocalSessionsWithServer(isFirstSync: isFirstSync)
call.resolve() call.resolve()
} }
} }
@objc func syncServerMediaProgressWithLocalMediaProgress(_ call: CAPPluginCall) { @objc func syncServerMediaProgressWithLocalMediaProgress(_ call: CAPPluginCall) {
let serverMediaProgress = call.getJson("mediaProgress", type: MediaProgress.self) let serverMediaProgress = call.getJson("mediaProgress", type: MediaProgress.self)
let localLibraryItemId = call.getString("localLibraryItemId") let localLibraryItemId = call.getString("localLibraryItemId")
let localEpisodeId = call.getString("localEpisodeId") let localEpisodeId = call.getString("localEpisodeId")
let localMediaProgressId = call.getString("localMediaProgressId") let localMediaProgressId = call.getString("localMediaProgressId")
do { do {
guard let serverMediaProgress = serverMediaProgress else { guard let serverMediaProgress = serverMediaProgress else {
return call.reject("serverMediaProgress not specified") return call.reject("serverMediaProgress not specified")
@@ -172,35 +173,35 @@ public class AbsDatabase: CAPPlugin {
guard localLibraryItemId != nil || localMediaProgressId != nil else { guard localLibraryItemId != nil || localMediaProgressId != nil else {
return call.reject("localLibraryItemId or localMediaProgressId must be specified") return call.reject("localLibraryItemId or localMediaProgressId must be specified")
} }
let localMediaProgress = try LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId) let localMediaProgress = try LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId)
guard let localMediaProgress = localMediaProgress else { guard let localMediaProgress = localMediaProgress else {
call.reject("Local media progress not found or created") call.reject("Local media progress not found or created")
return return
} }
logger.log("syncServerMediaProgressWithLocalMediaProgress: Saving local media progress") logger.log("syncServerMediaProgressWithLocalMediaProgress: Saving local media progress")
try localMediaProgress.updateFromServerMediaProgress(serverMediaProgress) try localMediaProgress.updateFromServerMediaProgress(serverMediaProgress)
call.resolve(try localMediaProgress.asDictionary()) call.resolve(try localMediaProgress.asDictionary())
} catch { } catch {
call.reject("Failed to sync media progress") call.reject("Failed to sync media progress")
debugPrint(error) debugPrint(error)
} }
} }
@objc func updateLocalMediaProgressFinished(_ call: CAPPluginCall) { @objc func updateLocalMediaProgressFinished(_ call: CAPPluginCall) {
let localLibraryItemId = call.getString("localLibraryItemId") let localLibraryItemId = call.getString("localLibraryItemId")
let localEpisodeId = call.getString("localEpisodeId") let localEpisodeId = call.getString("localEpisodeId")
let isFinished = call.getBool("isFinished", false) let isFinished = call.getBool("isFinished", false)
var localMediaProgressId = localLibraryItemId ?? "" var localMediaProgressId = localLibraryItemId ?? ""
if localEpisodeId != nil { if localEpisodeId != nil {
localMediaProgressId += "-\(localEpisodeId ?? "")" localMediaProgressId += "-\(localEpisodeId ?? "")"
} }
logger.log("updateLocalMediaProgressFinished \(localMediaProgressId) | Is Finished: \(isFinished)") logger.log("updateLocalMediaProgressFinished \(localMediaProgressId) | Is Finished: \(isFinished)")
do { do {
let localMediaProgress = try LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId) let localMediaProgress = try LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localMediaProgressId, localLibraryItemId: localLibraryItemId, localEpisodeId: localEpisodeId)
guard let localMediaProgress = localMediaProgress else { guard let localMediaProgress = localMediaProgress else {
@@ -210,11 +211,11 @@ public class AbsDatabase: CAPPlugin {
// Update finished status // Update finished status
try localMediaProgress.updateIsFinished(isFinished) try localMediaProgress.updateIsFinished(isFinished)
// Build API response // Build API response
let progressDictionary = try? localMediaProgress.asDictionary() let progressDictionary = try? localMediaProgress.asDictionary()
var response: [String: Any] = ["local": true, "server": false, "localMediaProgress": progressDictionary ?? ""] var response: [String: Any] = ["local": true, "server": false, "localMediaProgress": progressDictionary ?? ""]
// Send update to the server if logged in // Send update to the server if logged in
let hasLinkedServer = localMediaProgress.serverConnectionConfigId != nil let hasLinkedServer = localMediaProgress.serverConnectionConfigId != nil
let loggedIntoServer = Store.serverConfig?.id == localMediaProgress.serverConnectionConfigId let loggedIntoServer = Store.serverConfig?.id == localMediaProgress.serverConnectionConfigId
@@ -233,37 +234,45 @@ public class AbsDatabase: CAPPlugin {
return return
} }
} }
@objc func updateDeviceSettings(_ call: CAPPluginCall) { @objc func updateDeviceSettings(_ call: CAPPluginCall) {
let disableAutoRewind = call.getBool("disableAutoRewind") ?? false let disableAutoRewind = call.getBool("disableAutoRewind") ?? false
let enableAltView = call.getBool("enableAltView") ?? false let enableAltView = call.getBool("enableAltView") ?? false
let allowSeekingOnMediaControls = call.getBool("allowSeekingOnMediaControls") ?? false
let jumpBackwardsTime = call.getInt("jumpBackwardsTime") ?? 10 let jumpBackwardsTime = call.getInt("jumpBackwardsTime") ?? 10
let jumpForwardTime = call.getInt("jumpForwardTime") ?? 10 let jumpForwardTime = call.getInt("jumpForwardTime") ?? 10
let lockOrientation = call.getString("lockOrientation") ?? "NONE" let lockOrientation = call.getString("lockOrientation") ?? "NONE"
let hapticFeedback = call.getString("hapticFeedback") ?? "LIGHT" let hapticFeedback = call.getString("hapticFeedback") ?? "LIGHT"
let languageCode = call.getString("languageCode") ?? "en-us" let languageCode = call.getString("languageCode") ?? "en-us"
let downloadUsingCellular = call.getString("downloadUsingCellular") ?? "ALWAYS"
let streamingUsingCellular = call.getString("streamingUsingCellular") ?? "ALWAYS"
let settings = DeviceSettings() let settings = DeviceSettings()
settings.disableAutoRewind = disableAutoRewind settings.disableAutoRewind = disableAutoRewind
settings.enableAltView = enableAltView settings.enableAltView = enableAltView
settings.allowSeekingOnMediaControls = allowSeekingOnMediaControls
settings.jumpBackwardsTime = jumpBackwardsTime settings.jumpBackwardsTime = jumpBackwardsTime
settings.jumpForwardTime = jumpForwardTime settings.jumpForwardTime = jumpForwardTime
settings.lockOrientation = lockOrientation settings.lockOrientation = lockOrientation
settings.hapticFeedback = hapticFeedback settings.hapticFeedback = hapticFeedback
settings.languageCode = languageCode settings.languageCode = languageCode
settings.downloadUsingCellular = downloadUsingCellular
settings.streamingUsingCellular = streamingUsingCellular
Database.shared.setDeviceSettings(deviceSettings: settings) Database.shared.setDeviceSettings(deviceSettings: settings)
// call.resolve([ "value": [] ]) // Updates the media notification controls (for allowSeekingOnMediaControls setting)
PlayerHandler.updateRemoteTransportControls()
getDeviceData(call) getDeviceData(call)
} }
@objc func updateLocalEbookProgress(_ call: CAPPluginCall) { @objc func updateLocalEbookProgress(_ call: CAPPluginCall) {
let localLibraryItemId = call.getString("localLibraryItemId") let localLibraryItemId = call.getString("localLibraryItemId")
let ebookLocation = call.getString("ebookLocation", "") let ebookLocation = call.getString("ebookLocation", "")
let ebookProgress = call.getDouble("ebookProgress", 0.0) let ebookProgress = call.getDouble("ebookProgress", 0.0)
logger.log("updateLocalEbookProgress \(localLibraryItemId ?? "Unknown") | ebookLocation: \(ebookLocation) | ebookProgress: \(ebookProgress)") logger.log("updateLocalEbookProgress \(localLibraryItemId ?? "Unknown") | ebookLocation: \(ebookLocation) | ebookProgress: \(ebookProgress)")
do { do {
let localMediaProgress = try LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localLibraryItemId, localLibraryItemId: localLibraryItemId, localEpisodeId: nil) let localMediaProgress = try LocalMediaProgress.fetchOrCreateLocalMediaProgress(localMediaProgressId: localLibraryItemId, localLibraryItemId: localLibraryItemId, localEpisodeId: nil)
guard let localMediaProgress = localMediaProgress else { guard let localMediaProgress = localMediaProgress else {
@@ -273,7 +282,7 @@ public class AbsDatabase: CAPPlugin {
// Update finished status // Update finished status
try localMediaProgress.updateEbookProgress(ebookLocation: ebookLocation, ebookProgress: ebookProgress) try localMediaProgress.updateEbookProgress(ebookLocation: ebookLocation, ebookProgress: ebookProgress)
// Build API response // Build API response
let progressDictionary = try? localMediaProgress.asDictionary() let progressDictionary = try? localMediaProgress.asDictionary()
let response: [String: Any] = ["localMediaProgress": progressDictionary ?? ""] let response: [String: Any] = ["localMediaProgress": progressDictionary ?? ""]
+9 -9
View File
@@ -1,5 +1,5 @@
PODS: PODS:
- Alamofire (5.6.4) - Alamofire (5.8.1)
- ByteowlsCapacitorFilesharer (5.0.0): - ByteowlsCapacitorFilesharer (5.0.0):
- Capacitor - Capacitor
- Capacitor (5.4.0): - Capacitor (5.4.0):
@@ -23,11 +23,11 @@ PODS:
- Capacitor - Capacitor
- CordovaPlugins (5.4.0): - CordovaPlugins (5.4.0):
- CapacitorCordova - CapacitorCordova
- Realm (10.36.0): - Realm (10.47.0):
- Realm/Headers (= 10.36.0) - Realm/Headers (= 10.47.0)
- Realm/Headers (10.36.0) - Realm/Headers (10.47.0)
- RealmSwift (10.36.0): - RealmSwift (10.47.0):
- Realm (= 10.36.0) - Realm (= 10.47.0)
DEPENDENCIES: DEPENDENCIES:
- Alamofire (~> 5.5) - Alamofire (~> 5.5)
@@ -78,7 +78,7 @@ EXTERNAL SOURCES:
:path: "../capacitor-cordova-ios-plugins" :path: "../capacitor-cordova-ios-plugins"
SPEC CHECKSUMS: SPEC CHECKSUMS:
Alamofire: 4e95d97098eacb88856099c4fc79b526a299e48c Alamofire: 3ca42e259043ee0dc5c0cdd76c4bc568b8e42af7
ByteowlsCapacitorFilesharer: f6a773825632d65d5404a34764c4a3fd857bb176 ByteowlsCapacitorFilesharer: f6a773825632d65d5404a34764c4a3fd857bb176
Capacitor: a5cd803e02b471591c81165f400ace01f40b11d3 Capacitor: a5cd803e02b471591c81165f400ace01f40b11d3
CapacitorApp: 024e1b1bea5f883d79f6330d309bc441c88ad04a CapacitorApp: 024e1b1bea5f883d79f6330d309bc441c88ad04a
@@ -91,8 +91,8 @@ SPEC CHECKSUMS:
CapacitorPreferences: f03954bcb0ff09c792909e46bff88e3183c16b10 CapacitorPreferences: f03954bcb0ff09c792909e46bff88e3183c16b10
CapacitorStatusBar: 565c0a1ebd79bb40d797606a8992b4a105885309 CapacitorStatusBar: 565c0a1ebd79bb40d797606a8992b4a105885309
CordovaPlugins: a5db67e5ac1061b9869a0efd754f2c2f776aeccc CordovaPlugins: a5db67e5ac1061b9869a0efd754f2c2f776aeccc
Realm: 3fd136cb4c83a927482a7f1612496d37beed3cf5 Realm: e43fb540ae947497e3ea8a662443256920602060
RealmSwift: 513d4dcbf5bfc4d573454088b592685fc48dd716 RealmSwift: 8b06ed06b5d16749ae0c4d91c0cba414a9e28189
PODFILE CHECKSUM: 02e6ffe2f51a453ce222ee9af0e55e9448d8514c PODFILE CHECKSUM: 02e6ffe2f51a453ce222ee9af0e55e9448d8514c
+7 -1
View File
@@ -11,11 +11,14 @@ import RealmSwift
class DeviceSettings: Object { class DeviceSettings: Object {
@Persisted var disableAutoRewind: Bool = false @Persisted var disableAutoRewind: Bool = false
@Persisted var enableAltView: Bool = true @Persisted var enableAltView: Bool = true
@Persisted var allowSeekingOnMediaControls: Bool = false
@Persisted var jumpBackwardsTime: Int = 10 @Persisted var jumpBackwardsTime: Int = 10
@Persisted var jumpForwardTime: Int = 10 @Persisted var jumpForwardTime: Int = 10
@Persisted var lockOrientation: String = "NONE" @Persisted var lockOrientation: String = "NONE"
@Persisted var hapticFeedback: String = "LIGHT" @Persisted var hapticFeedback: String = "LIGHT"
@Persisted var languageCode: String = "en-us" @Persisted var languageCode: String = "en-us"
@Persisted var downloadUsingCellular: String = "ALWAYS"
@Persisted var streamingUsingCellular: String = "ALWAYS"
} }
func getDefaultDeviceSettings() -> DeviceSettings { func getDefaultDeviceSettings() -> DeviceSettings {
@@ -26,10 +29,13 @@ func deviceSettingsToJSON(settings: DeviceSettings) -> Dictionary<String, Any> {
return [ return [
"disableAutoRewind": settings.disableAutoRewind, "disableAutoRewind": settings.disableAutoRewind,
"enableAltView": settings.enableAltView, "enableAltView": settings.enableAltView,
"allowSeekingOnMediaControls": settings.allowSeekingOnMediaControls,
"jumpBackwardsTime": settings.jumpBackwardsTime, "jumpBackwardsTime": settings.jumpBackwardsTime,
"jumpForwardTime": settings.jumpForwardTime, "jumpForwardTime": settings.jumpForwardTime,
"lockOrientation": settings.lockOrientation, "lockOrientation": settings.lockOrientation,
"hapticFeedback": settings.hapticFeedback, "hapticFeedback": settings.hapticFeedback,
"languageCode": settings.languageCode "languageCode": settings.languageCode,
"downloadUsingCellular": settings.downloadUsingCellular,
"streamingUsingCellular": settings.streamingUsingCellular
] ]
} }
+45 -42
View File
@@ -29,9 +29,7 @@ class AudioPlayer: NSObject {
internal let logger = AppLogger(category: "AudioPlayer") internal let logger = AppLogger(category: "AudioPlayer")
private var status: PlayerStatus private var status: PlayerStatus
internal var rate: Float internal var rateManager: AudioPlayerRateManager
private var tmpRate: Float = 1.0
private var playerContext = 0 private var playerContext = 0
private var playerItemContext = 0 private var playerItemContext = 0
@@ -64,11 +62,18 @@ class AudioPlayer: NSObject {
self.audioPlayer.automaticallyWaitsToMinimizeStalling = true self.audioPlayer.automaticallyWaitsToMinimizeStalling = true
self.sessionId = sessionId self.sessionId = sessionId
self.status = .uninitialized self.status = .uninitialized
self.rate = 0.0
self.tmpRate = playbackRate if #available(iOS 16.0, *) {
self.rateManager = DefaultedAudioPlayerRateManager(audioPlayer: self.audioPlayer, defaultRate: playbackRate)
} else {
self.rateManager = LegacyAudioPlayerRateManager(audioPlayer: self.audioPlayer, defaultRate: playbackRate)
}
super.init() super.init()
self.rateManager.rateChangedCompletion = self.updateNowPlaying
self.rateManager.defaultRateChangedCompletion = self.setupTimeObservers
initAudioSession() initAudioSession()
setupRemoteTransportControls() setupRemoteTransportControls()
@@ -81,7 +86,6 @@ class AudioPlayer: NSObject {
// Listen to player events // Listen to player events
self.setupAudioSessionNotifications() self.setupAudioSessionNotifications()
self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: .new, context: &playerContext)
self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), options: .new, context: &playerContext) self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), options: .new, context: &playerContext)
for track in playbackSession.audioTracks { for track in playbackSession.audioTracks {
@@ -150,7 +154,6 @@ class AudioPlayer: NSObject {
} }
// Remove observers // Remove observers
self.audioPlayer.removeObserver(self, forKeyPath: #keyPath(AVPlayer.rate), context: &playerContext)
self.audioPlayer.removeObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), context: &playerContext) self.audioPlayer.removeObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), context: &playerContext)
self.removeTimeObservers() self.removeTimeObservers()
@@ -332,7 +335,7 @@ class AudioPlayer: NSObject {
self.markAudioSessionAs(active: true) self.markAudioSessionAs(active: true)
DispatchQueue.runOnMainQueue { DispatchQueue.runOnMainQueue {
self.audioPlayer.play() self.audioPlayer.play()
self.audioPlayer.rate = self.tmpRate self.rateManager.handlePlayEvent()
} }
self.status = .playing self.status = .playing
@@ -424,25 +427,8 @@ class AudioPlayer: NSObject {
} }
} }
public func setPlaybackRate(_ rate: Float, observed: Bool = false) { public func setPlaybackRate(_ rate: Float) {
let playbackSpeedChanged = rate > 0.0 && rate != self.tmpRate && !(observed && rate == 1) self.rateManager.setPlaybackRate(rate)
if self.audioPlayer.rate != rate {
logger.log("setPlaybakRate rate changed from \(self.audioPlayer.rate) to \(rate)")
DispatchQueue.runOnMainQueue {
self.audioPlayer.rate = rate
}
}
self.rate = rate
self.updateNowPlaying()
if playbackSpeedChanged {
self.tmpRate = rate
// Setup the time observer again at the new rate
self.setupTimeObservers()
}
} }
public func setChapterTrack() { public func setChapterTrack() {
@@ -534,14 +520,24 @@ class AudioPlayer: NSObject {
return return
} }
switch type { // When interruption is from the app suspending then don't resume playback
case .ended: if #available(iOS 14.5, *) {
guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return } let reasonValue = userInfo[AVAudioSessionInterruptionReasonKey] as? UInt ?? 0
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) let reason = AVAudioSession.InterruptionReason(rawValue: reasonValue)
if options.contains(.shouldResume) { if (reason == .appWasSuspended) {
self.play(allowSeekBack: true) logger.log("AVAudioSession was suspended")
return
} }
default: () }
switch type {
case .ended:
guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
self.play(allowSeekBack: true)
}
default: ()
} }
} }
@@ -578,7 +574,7 @@ class AudioPlayer: NSObject {
} }
// MARK: - Now playing // MARK: - Now playing
private func setupRemoteTransportControls() { func setupRemoteTransportControls() {
DispatchQueue.runOnMainQueue { DispatchQueue.runOnMainQueue {
UIApplication.shared.beginReceivingRemoteControlEvents() UIApplication.shared.beginReceivingRemoteControlEvents()
} }
@@ -588,6 +584,7 @@ class AudioPlayer: NSObject {
let jumpBackwardsTime = deviceSettings.jumpBackwardsTime let jumpBackwardsTime = deviceSettings.jumpBackwardsTime
commandCenter.playCommand.isEnabled = true commandCenter.playCommand.isEnabled = true
commandCenter.playCommand.removeTarget(nil)
commandCenter.playCommand.addTarget { [weak self] event in commandCenter.playCommand.addTarget { [weak self] event in
guard let strongSelf = self else { return .commandFailed } guard let strongSelf = self else { return .commandFailed }
if strongSelf.isPlaying() { if strongSelf.isPlaying() {
@@ -599,6 +596,7 @@ class AudioPlayer: NSObject {
} }
commandCenter.pauseCommand.isEnabled = true commandCenter.pauseCommand.isEnabled = true
commandCenter.pauseCommand.removeTarget(nil)
commandCenter.pauseCommand.addTarget { [weak self] event in commandCenter.pauseCommand.addTarget { [weak self] event in
guard let strongSelf = self else { return .commandFailed } guard let strongSelf = self else { return .commandFailed }
if strongSelf.isPlaying() { if strongSelf.isPlaying() {
@@ -610,6 +608,7 @@ class AudioPlayer: NSObject {
} }
commandCenter.togglePlayPauseCommand.isEnabled = true commandCenter.togglePlayPauseCommand.isEnabled = true
commandCenter.togglePlayPauseCommand.removeTarget(nil)
commandCenter.togglePlayPauseCommand.addTarget { [weak self] event in commandCenter.togglePlayPauseCommand.addTarget { [weak self] event in
guard let strongSelf = self else { return .commandFailed } guard let strongSelf = self else { return .commandFailed }
if strongSelf.isPlaying() { if strongSelf.isPlaying() {
@@ -621,6 +620,7 @@ class AudioPlayer: NSObject {
} }
commandCenter.skipForwardCommand.isEnabled = true commandCenter.skipForwardCommand.isEnabled = true
commandCenter.skipForwardCommand.removeTarget(nil)
commandCenter.skipForwardCommand.preferredIntervals = [NSNumber(value: jumpForwardTime)] commandCenter.skipForwardCommand.preferredIntervals = [NSNumber(value: jumpForwardTime)]
commandCenter.skipForwardCommand.addTarget { [weak self] event in commandCenter.skipForwardCommand.addTarget { [weak self] event in
guard let command = event.command as? MPSkipIntervalCommand else { guard let command = event.command as? MPSkipIntervalCommand else {
@@ -633,6 +633,7 @@ class AudioPlayer: NSObject {
return .success return .success
} }
commandCenter.skipBackwardCommand.isEnabled = true commandCenter.skipBackwardCommand.isEnabled = true
commandCenter.skipBackwardCommand.removeTarget(nil)
commandCenter.skipBackwardCommand.preferredIntervals = [NSNumber(value: jumpBackwardsTime)] commandCenter.skipBackwardCommand.preferredIntervals = [NSNumber(value: jumpBackwardsTime)]
commandCenter.skipBackwardCommand.addTarget { [weak self] event in commandCenter.skipBackwardCommand.addTarget { [weak self] event in
guard let command = event.command as? MPSkipIntervalCommand else { guard let command = event.command as? MPSkipIntervalCommand else {
@@ -646,6 +647,7 @@ class AudioPlayer: NSObject {
} }
commandCenter.nextTrackCommand.isEnabled = true commandCenter.nextTrackCommand.isEnabled = true
commandCenter.nextTrackCommand.removeTarget(nil)
commandCenter.nextTrackCommand.addTarget { [weak self] _ in commandCenter.nextTrackCommand.addTarget { [weak self] _ in
guard let currentTime = self?.getCurrentTime() else { guard let currentTime = self?.getCurrentTime() else {
return .commandFailed return .commandFailed
@@ -654,6 +656,7 @@ class AudioPlayer: NSObject {
return .success return .success
} }
commandCenter.previousTrackCommand.isEnabled = true commandCenter.previousTrackCommand.isEnabled = true
commandCenter.previousTrackCommand.removeTarget(nil)
commandCenter.previousTrackCommand.addTarget { [weak self] _ in commandCenter.previousTrackCommand.addTarget { [weak self] _ in
guard let currentTime = self?.getCurrentTime() else { guard let currentTime = self?.getCurrentTime() else {
return .commandFailed return .commandFailed
@@ -662,7 +665,8 @@ class AudioPlayer: NSObject {
return .success return .success
} }
commandCenter.changePlaybackPositionCommand.isEnabled = true commandCenter.changePlaybackPositionCommand.isEnabled = deviceSettings.allowSeekingOnMediaControls
commandCenter.changePlaybackPositionCommand.removeTarget(nil)
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
guard let event = event as? MPChangePlaybackPositionCommandEvent else { guard let event = event as? MPChangePlaybackPositionCommandEvent else {
return .noSuchContent return .noSuchContent
@@ -680,6 +684,7 @@ class AudioPlayer: NSObject {
} }
commandCenter.changePlaybackRateCommand.isEnabled = true commandCenter.changePlaybackRateCommand.isEnabled = true
commandCenter.changePlaybackRateCommand.removeTarget(nil)
commandCenter.changePlaybackRateCommand.supportedPlaybackRates = [0.5, 0.75, 1.0, 1.25, 1.5, 2] commandCenter.changePlaybackRateCommand.supportedPlaybackRates = [0.5, 0.75, 1.0, 1.25, 1.5, 2]
commandCenter.changePlaybackRateCommand.addTarget { [weak self] event in commandCenter.changePlaybackRateCommand.addTarget { [weak self] event in
guard let event = event as? MPChangePlaybackRateCommandEvent else { guard let event = event as? MPChangePlaybackRateCommandEvent else {
@@ -696,23 +701,21 @@ class AudioPlayer: NSObject {
NowPlayingInfo.shared.update( NowPlayingInfo.shared.update(
duration: currentChapter.getRelativeChapterEndTime(), duration: currentChapter.getRelativeChapterEndTime(),
currentTime: currentChapter.getRelativeChapterCurrentTime(sessionCurrentTime: session.currentTime), currentTime: currentChapter.getRelativeChapterCurrentTime(sessionCurrentTime: session.currentTime),
rate: rate, rate: self.rateManager.rate,
defaultRate: self.rateManager.defaultRate,
chapterName: currentChapter.title, chapterName: currentChapter.title,
chapterNumber: (session.chapters.firstIndex(of: currentChapter) ?? 0) + 1, chapterNumber: (session.chapters.firstIndex(of: currentChapter) ?? 0) + 1,
chapterCount: session.chapters.count chapterCount: session.chapters.count
) )
} else if let duration = self.getDuration(), let currentTime = self.getCurrentTime() { } else if let duration = self.getDuration(), let currentTime = self.getCurrentTime() {
NowPlayingInfo.shared.update(duration: duration, currentTime: currentTime, rate: rate) NowPlayingInfo.shared.update(duration: duration, currentTime: currentTime, rate: self.rateManager.rate, defaultRate: self.rateManager.defaultRate)
} }
} }
// MARK: - Observer // MARK: - Observer
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &playerContext { if context == &playerContext {
if keyPath == #keyPath(AVPlayer.rate) { if keyPath == #keyPath(AVPlayer.currentItem) {
logger.log("playerContext observer player rate")
self.setPlaybackRate(change?[.newKey] as? Float ?? 1.0, observed: true)
} else if keyPath == #keyPath(AVPlayer.currentItem) {
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil) NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
logger.log("WARNING: Item ended") logger.log("WARNING: Item ended")
} }
@@ -0,0 +1,23 @@
//
// AudioPlayerRateManager.swift
// Audiobookshelf
//
// Created by Marke Hallowell on 4/14/24.
//
import Foundation
import AVFoundation
protocol AudioPlayerRateManager {
var rate: Float { get }
var defaultRate: Float { get }
var rateChangedCompletion: () -> Void { get set }
var defaultRateChangedCompletion: () -> Void { get set }
init(audioPlayer: AVPlayer, defaultRate: Float)
func setPlaybackRate(_ rate: Float)
// Callback for play events (e.g. LegacyAudioPlayerRateManager uses this set rate immediately after playback resumes)
func handlePlayEvent() -> Void
}
@@ -22,7 +22,7 @@ extension AudioPlayer {
// Return the player time until sleep // Return the player time until sleep
var sleepTimeRemaining: Double? = nil var sleepTimeRemaining: Double? = nil
if let chapterStopAt = self.sleepTimeChapterStopAt { if let chapterStopAt = self.sleepTimeChapterStopAt {
sleepTimeRemaining = (chapterStopAt - currentTime) / Double(self.rate > 0 ? self.rate : 1.0) sleepTimeRemaining = (chapterStopAt - currentTime) / Double(self.rateManager.rate > 0 ? self.rateManager.rate : 1.0)
} else if self.isCountdownSleepTimerSet() { } else if self.isCountdownSleepTimerSet() {
sleepTimeRemaining = self.sleepTimeRemaining sleepTimeRemaining = self.sleepTimeRemaining
} }
@@ -0,0 +1,94 @@
//
// DefaultedAudioPlayerRateManager.swift
// Audiobookshelf
//
// Created by Marke Hallowell on 4/14/24.
//
import Foundation
import AVFoundation
@available(iOS 16.0, *)
class DefaultedAudioPlayerRateManager: NSObject, AudioPlayerRateManager {
internal let logger = AppLogger(category: "DefaultedAudioPlayerRateManager")
internal var audioPlayer: AVPlayer
// MARK: - AudioPlayerRateManager
public private(set) var defaultRate: Float
public private(set) var rate: Float
public var rateChangedCompletion: () -> Void
public var defaultRateChangedCompletion: () -> Void
required init(audioPlayer: AVPlayer, defaultRate: Float) {
self.audioPlayer = audioPlayer
self.rateChangedCompletion = {}
self.defaultRateChangedCompletion = {}
self.rate = self.audioPlayer.rate
self.defaultRate = defaultRate
self.audioPlayer.defaultRate = defaultRate
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(handleObservedRateChange), name: AVPlayer.rateDidChangeNotification, object: self.audioPlayer)
}
public func setPlaybackRate(_ rate: Float) {
self.handlePlaybackRateChange(rate, observed: false)
}
// No-op (player automatically resumes at last-known defaultRate)
public func handlePlayEvent() { }
// MARK: - Destructor
public func destroy() {
NotificationCenter.default.removeObserver(self, name: AVPlayer.rateDidChangeNotification, object: self.audioPlayer)
}
// MARK: - Internal
internal func handlePlaybackRateChange(_ rate: Float, observed: Bool = false) {
let playbackSpeedChanged = rate > 0.0 && rate != self.defaultRate
if playbackSpeedChanged {
self.defaultRate = rate
self.audioPlayer.defaultRate = rate
self.defaultRateChangedCompletion()
// Check to see if we also need to make a temporary rate change to player
if self.audioPlayer.rate > 0.0 {
if self.audioPlayer.rate != rate {
self.rate = rate
self.audioPlayer.rate = rate
self.rateChangedCompletion()
}
}
} else {
self.rate = rate
self.rateChangedCompletion()
}
}
// MARK: - iOS rate change notification handler
@objc internal func handleObservedRateChange(notification: Notification) {
// TODO: Consider handling cases individually (e.g. overall session impact?)
/*
guard let reason = notification.userInfo?[AVPlayer.rateDidChangeReasonKey] as? AVPlayer.RateDidChangeReason else {
return
}
switch reason {
case .appBackgrounded:
// App transitioned to the background.
case .audioSessionInterrupted:
// The system interrupts the app's audio session.
case .setRateCalled:
// The app set the player's rate.
case .setRateFailed:
// An attempt to change the player's rate failed.
default:
break
}
*/
self.handlePlaybackRateChange(self.audioPlayer.rate, observed: true)
}
}
@@ -0,0 +1,84 @@
//
// LegacyAudioPlayerRateManager.swift
// Audiobookshelf
//
// Created by Marke Hallowell on 4/14/24.
//
import Foundation
import AVFoundation
class LegacyAudioPlayerRateManager: NSObject, AudioPlayerRateManager {
internal let logger = AppLogger(category: "AudioPlayer")
internal var audioPlayer: AVPlayer
internal var managerContext = 0
// MARK: - AudioPlayerRateManager
public private(set) var defaultRate: Float
public private(set) var rate: Float
public var rateChangedCompletion: () -> Void
public var defaultRateChangedCompletion: () -> Void
required init(audioPlayer: AVPlayer, defaultRate: Float) {
self.rate = 0.0
self.defaultRate = defaultRate
self.audioPlayer = audioPlayer
self.rateChangedCompletion = {}
self.defaultRateChangedCompletion = {}
super.init()
self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: .new, context: &managerContext)
}
public func setPlaybackRate(_ rate: Float) {
self.handlePlaybackRateChange(rate, observed: false)
}
public func handlePlayEvent() {
DispatchQueue.runOnMainQueue {
self.audioPlayer.rate = self.defaultRate
}
}
// MARK: - Destructor
public func destroy() {
// Remove Observer
self.audioPlayer.removeObserver(self, forKeyPath: #keyPath(AVPlayer.rate), context: &managerContext)
}
// MARK: - Internal
internal func handlePlaybackRateChange(_ rate: Float, observed: Bool = false) {
let playbackSpeedChanged = rate > 0.0 && rate != self.defaultRate && !(observed && rate == 1)
if self.audioPlayer.rate != rate {
logger.log("setPlaybakRate rate changed from \(self.audioPlayer.rate) to \(rate)")
DispatchQueue.runOnMainQueue {
self.audioPlayer.rate = rate
}
}
self.rate = rate
self.rateChangedCompletion()
if playbackSpeedChanged {
self.defaultRate = rate
self.defaultRateChangedCompletion()
}
}
// MARK: - Observer
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &managerContext {
if keyPath == #keyPath(AVPlayer.rate) {
logger.log("playerContext observer player rate")
self.handlePlaybackRateChange(change?[.newKey] as? Float ?? 1.0, observed: true)
}
} else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
}
}
+5 -1
View File
@@ -38,7 +38,7 @@ class PlayerHandler {
public static var paused: Bool { public static var paused: Bool {
get { get {
guard let player = player else { return true } guard let player = player else { return true }
return player.rate == 0.0 return player.rateManager.rate == 0.0
} }
set(paused) { set(paused) {
if paused { if paused {
@@ -135,6 +135,10 @@ class PlayerHandler {
) )
} }
public static func updateRemoteTransportControls() {
self.player?.setupRemoteTransportControls()
}
// MARK: - Helper logic // MARK: - Helper logic
private static func cleanupOldSessions(currentSessionId: String?) { private static func cleanupOldSessions(currentSessionId: String?) {
+7 -4
View File
@@ -190,10 +190,10 @@ class ApiClient {
} }
public static func reportAllLocalPlaybackSessions(_ sessions: [PlaybackSession]) async -> Bool { public static func reportAllLocalPlaybackSessions(_ sessions: [PlaybackSession]) async -> Bool {
return await postResource(endpoint: "api/session/local-all", parameters: LocalPlaybackSessionSyncAllPayload(sessions: sessions)) return await postResource(endpoint: "api/session/local-all", parameters: LocalPlaybackSessionSyncAllPayload(sessions: sessions, deviceInfo: sessions.first?.deviceInfo))
} }
public static func syncLocalSessionsWithServer() async { public static func syncLocalSessionsWithServer(isFirstSync: Bool) async {
do { do {
// Sync server progress with local media progress // Sync server progress with local media progress
let localMediaProgressList = Database.shared.getAllLocalMediaProgress().filter { let localMediaProgressList = Database.shared.getAllLocalMediaProgress().filter {
@@ -232,13 +232,15 @@ class ApiClient {
let playbackSessions = Database.shared.getAllPlaybackSessions().filter { let playbackSessions = Database.shared.getAllPlaybackSessions().filter {
$0.serverConnectionConfigId == Store.serverConfig?.id $0.serverConnectionConfigId == Store.serverConfig?.id
}.map { $0.freeze() } }.map { $0.freeze() }
logger.log("syncLocalSessionsWithServer: Found \(playbackSessions.count) playback sessions for server") logger.log("syncLocalSessionsWithServer: Found \(playbackSessions.count) playback sessions for server (first sync: \(isFirstSync))")
if (!playbackSessions.isEmpty) { if (!playbackSessions.isEmpty) {
let success = await ApiClient.reportAllLocalPlaybackSessions(playbackSessions) let success = await ApiClient.reportAllLocalPlaybackSessions(playbackSessions)
if (success) { if (success) {
// Remove sessions from db // Remove sessions from db
try playbackSessions.forEach { session in try playbackSessions.forEach { session in
if (!session.isActiveSession) { logger.log("syncLocalSessionsWithServer: Handling \(session.displayTitle ?? "") (\(session.id)) \(session.isActiveSession)")
// On first sync then remove all sessions
if (!session.isActiveSession || isFirstSync) {
if let session = session.thaw() { if let session = session.thaw() {
try session.delete() try session.delete()
} }
@@ -327,6 +329,7 @@ struct LocalMediaProgressSyncResultsPayload: Codable {
struct LocalPlaybackSessionSyncAllPayload: Codable { struct LocalPlaybackSessionSyncAllPayload: Codable {
var sessions: [PlaybackSession] var sessions: [PlaybackSession]
var deviceInfo: [String: String?]?
} }
struct Connectivity { struct Connectivity {
+2 -2
View File
@@ -53,14 +53,14 @@ class NowPlayingInfo {
self.setMetadata(artwork: artwork, metadata: metadata) self.setMetadata(artwork: artwork, metadata: metadata)
} }
} }
public func update(duration: Double, currentTime: Double, rate: Float, chapterName: String? = nil, chapterNumber: Int? = nil, chapterCount: Int? = nil) { public func update(duration: Double, currentTime: Double, rate: Float, defaultRate: Float, chapterName: String? = nil, chapterNumber: Int? = nil, chapterCount: Int? = nil) {
// Update on the main to prevent access collisions // Update on the main to prevent access collisions
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
if let self = self { if let self = self {
self.nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration self.nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration
self.nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime self.nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime
self.nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate self.nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate
self.nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = 1.0 self.nowPlayingInfo[MPNowPlayingInfoPropertyDefaultPlaybackRate] = defaultRate
if let chapterName = chapterName, let chapterNumber = chapterNumber, let chapterCount = chapterCount { if let chapterName = chapterName, let chapterNumber = chapterNumber, let chapterCount = chapterCount {
+6 -9
View File
@@ -45,7 +45,7 @@ export default {
if (timeSinceDisconnect > 5000) { if (timeSinceDisconnect > 5000) {
console.log('Time since disconnect was', timeSinceDisconnect, 'sync with server') console.log('Time since disconnect was', timeSinceDisconnect, 'sync with server')
setTimeout(() => { setTimeout(() => {
this.syncLocalSessions() this.syncLocalSessions(false)
}, 4000) }, 4000)
} }
} }
@@ -139,7 +139,7 @@ export default {
console.log(`[default] Got server config, attempt authorize ${serverConfig.address}`) console.log(`[default] Got server config, attempt authorize ${serverConfig.address}`)
const authRes = await this.postRequest(`${serverConfig.address}/api/authorize`, null, { Authorization: `Bearer ${serverConfig.token}` }, 10000).catch((error) => { const authRes = await this.postRequest(`${serverConfig.address}/api/authorize`, null, { Authorization: `Bearer ${serverConfig.token}` }, 6000).catch((error) => {
console.error('[default] Server auth failed', error) console.error('[default] Server auth failed', error)
return false return false
}) })
@@ -195,14 +195,14 @@ export default {
this.$eventBus.$emit('library-changed') this.$eventBus.$emit('library-changed')
this.inittingLibraries = false this.inittingLibraries = false
}, },
async syncLocalSessions() { async syncLocalSessions(isFirstSync) {
if (!this.user) { if (!this.user) {
console.log('[default] No need to sync local sessions - not connected to server') console.log('[default] No need to sync local sessions - not connected to server')
return return
} }
console.log('[default] Calling syncLocalSessions') console.log('[default] Calling syncLocalSessions')
const response = await this.$db.syncLocalSessionsWithServer() const response = await this.$db.syncLocalSessionsWithServer(isFirstSync)
if (response?.error) { if (response?.error) {
console.error('[default] Failed to sync local sessions', response.error) console.error('[default] Failed to sync local sessions', response.error)
} else { } else {
@@ -212,8 +212,7 @@ export default {
} }
}, },
userUpdated(user) { userUpdated(user) {
// console.log('[default] userUpdated:', JSON.stringify(user)) if (this.user?.id == user.id) {
if (this.user && this.user.id == user.id) {
this.$store.commit('user/setUser', user) this.$store.commit('user/setUser', user)
} }
}, },
@@ -326,8 +325,6 @@ export default {
await this.$store.dispatch('setupNetworkListener') await this.$store.dispatch('setupNetworkListener')
await this.$store.dispatch('globals/loadLocalMediaProgress')
if (this.$store.state.user.serverConnectionConfig) { if (this.$store.state.user.serverConnectionConfig) {
await this.initLibraries() await this.initLibraries()
} else { } else {
@@ -335,7 +332,7 @@ export default {
} }
console.log(`[default] finished connection attempt or already connected ${!!this.user}`) console.log(`[default] finished connection attempt or already connected ${!!this.user}`)
await this.syncLocalSessions() await this.syncLocalSessions(true)
this.hasMounted = true this.hasMounted = true
+42
View File
@@ -0,0 +1,42 @@
import { Dialog } from '@capacitor/dialog';
export default {
methods: {
async checkCellularPermission(actionType) {
if (this.$store.state.networkConnectionType !== 'cellular') return true
let permission;
if (actionType === 'download') {
permission = this.$store.getters['getCanDownloadUsingCellular']
if (permission === 'NEVER') {
this.$toast.error(this.$strings.ToastDownloadNotAllowedOnCellular)
return false
}
} else if (actionType === 'streaming') {
permission = this.$store.getters['getCanStreamingUsingCellular']
if (permission === 'NEVER') {
this.$toast.error(this.$strings.ToastStreamingNotAllowedOnCellular)
return false
}
}
if (permission === 'ASK') {
const confirmed = await this.confirmAction(actionType)
return confirmed
}
return true
},
async confirmAction(actionType) {
const message = actionType === 'download' ?
this.$strings.MessageConfirmDownloadUsingCellular :
this.$strings.MessageConfirmStreamingUsingCellular
const { value } = await Dialog.confirm({
title: 'Confirm',
message
})
return value
}
}
}
+8 -5
View File
@@ -3,7 +3,7 @@ const pkg = require('./package.json')
export default { export default {
ssr: false, ssr: false,
target: 'static', target: 'static',
telemetry: false,
env: { env: {
PROD: '1', PROD: '1',
ANDROID_APP_URL: 'https://play.google.com/store/apps/details?id=com.audiobookshelf.app', ANDROID_APP_URL: 'https://play.google.com/store/apps/details?id=com.audiobookshelf.app',
@@ -36,6 +36,7 @@ export default {
}, },
css: [ css: [
'@/assets/tailwind.css',
'@/assets/app.css' '@/assets/app.css'
], ],
@@ -56,10 +57,6 @@ export default {
components: true, components: true,
buildModules: [
'@nuxtjs/tailwindcss',
],
modules: [ modules: [
'@nuxtjs/axios' '@nuxtjs/axios'
], ],
@@ -67,6 +64,12 @@ export default {
axios: {}, axios: {},
build: { build: {
postcss: {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
},
babel: { babel: {
plugins: [['@babel/plugin-proposal-private-property-in-object', { loose: true }]], plugins: [['@babel/plugin-proposal-private-property-in-object', { loose: true }]],
}, },
+2203 -17840
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-app", "name": "audiobookshelf-app",
"version": "0.9.71-beta", "version": "0.9.76-beta",
"author": "advplyr", "author": "advplyr",
"scripts": { "scripts": {
"dev": "nuxt --hostname 0.0.0.0 --port 1337", "dev": "nuxt --hostname 0.0.0.0 --port 1337",
@@ -44,7 +44,8 @@
"@babel/core": "7.13.15", "@babel/core": "7.13.15",
"@babel/preset-env": "7.13.15", "@babel/preset-env": "7.13.15",
"@capacitor/cli": "^5.0.0", "@capacitor/cli": "^5.0.0",
"@nuxtjs/tailwindcss": "^6.10.3", "autoprefixer": "^10.4.17",
"postcss": "^8.3.5" "postcss": "^8.4.33",
"tailwindcss": "^3.4.1"
} }
} }
+7 -2
View File
@@ -3,7 +3,7 @@
<div id="bookshelf" class="w-full h-full p-4 overflow-y-auto"> <div id="bookshelf" class="w-full h-full p-4 overflow-y-auto">
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
<template v-for="author in authors"> <template v-for="author in authors">
<cards-author-card :key="author.id" :author="author" :width="96" :height="120" class="p-2" /> <cards-author-card :key="author.id" :author="author" :width="cardWidth" :height="cardHeight" class="p-2" />
</template> </template>
</div> </div>
</div> </div>
@@ -16,16 +16,21 @@ export default {
return { return {
loading: true, loading: true,
authors: [], authors: [],
loadedLibraryId: null loadedLibraryId: null,
cardWidth: 200
} }
}, },
computed: { computed: {
currentLibraryId() { currentLibraryId() {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
},
cardHeight() {
return this.cardWidth * 1.25
} }
}, },
methods: { methods: {
async init() { async init() {
this.cardWidth = (window.innerWidth - 64) / 2
if (!this.currentLibraryId) { if (!this.currentLibraryId) {
return return
} }
+7 -2
View File
@@ -236,7 +236,7 @@ export default {
console.log('[categories] Local shelves set', this.shelves.length, this.lastLocalFetch) console.log('[categories] Local shelves set', this.shelves.length, this.lastLocalFetch)
if (isConnectedToServerWithInternet) { if (isConnectedToServerWithInternet) {
const categories = await this.$nativeHttp.get(`/api/libraries/${this.currentLibraryId}/personalized?minified=1&include=rssfeed,numEpisodesIncomplete`).catch((error) => { const categories = await this.$nativeHttp.get(`/api/libraries/${this.currentLibraryId}/personalized?minified=1&include=rssfeed,numEpisodesIncomplete`, { connectTimeout: 10000 }).catch((error) => {
console.error('[categories] Failed to fetch categories', error) console.error('[categories] Failed to fetch categories', error)
return [] return []
}) })
@@ -327,8 +327,13 @@ export default {
this.$eventBus.$off('library-changed', this.libraryChanged) this.$eventBus.$off('library-changed', this.libraryChanged)
} }
}, },
mounted() { async mounted() {
if (this.$route.query.error) {
this.$toast.error(this.$route.query.error)
}
this.initListeners() this.initListeners()
await this.$store.dispatch('globals/loadLocalMediaProgress')
console.log(`[categories] mounted so fetching categories`) console.log(`[categories] mounted so fetching categories`)
this.fetchCategories() this.fetchCategories()
}, },
+6 -4
View File
@@ -3,7 +3,7 @@
<h1 class="text-xl mb-2 font-semibold">{{ $strings.HeaderLatestEpisodes }}</h1> <h1 class="text-xl mb-2 font-semibold">{{ $strings.HeaderLatestEpisodes }}</h1>
<template v-for="episode in recentEpisodes"> <template v-for="episode in recentEpisodes">
<tables-podcast-latest-episode-row :episode="episode" :local-episode="localEpisodeMap[episode.id]" :library-item-id="episode.libraryItemId" :local-library-item-id="null" :is-local="isLocal" :key="episode.id" @addToPlaylist="addEpisodeToPlaylist" /> <tables-podcast-latest-episode-row :episode="episode" :local-episode="localEpisodeMap[episode.id]" :library-item-id="episode.libraryItemId" :local-library-item-id="localEpisodeMap[episode.id]?.localLibraryItemId" :key="episode.id" @addToPlaylist="addEpisodeToPlaylist" />
</template> </template>
</div> </div>
</template> </template>
@@ -17,7 +17,6 @@ export default {
totalEpisodes: 0, totalEpisodes: 0,
currentPage: 0, currentPage: 0,
localLibraryItems: [], localLibraryItems: [],
isLocal: false,
loadedLibraryId: null loadedLibraryId: null
} }
}, },
@@ -30,7 +29,10 @@ export default {
const episodes = [] const episodes = []
this.localLibraryItems.forEach((li) => { this.localLibraryItems.forEach((li) => {
if (li.media.episodes?.length) { if (li.media.episodes?.length) {
episodes.push(...li.media.episodes) li.media.episodes.map((ep) => {
ep.localLibraryItemId = li.id
episodes.push(ep)
})
} }
}) })
return episodes return episodes
@@ -106,4 +108,4 @@ export default {
this.$eventBus.$off('new-local-library-item', this.newLocalLibraryItem) this.$eventBus.$off('new-local-library-item', this.newLocalLibraryItem)
} }
} }
</script> </script>
+2 -6
View File
@@ -34,11 +34,7 @@ export default {
deviceData: null deviceData: null
} }
}, },
computed: { computed: {},
networkConnected() {
return this.$store.state.networkConnected
}
},
methods: { methods: {
async init() { async init() {
this.deviceData = await this.$db.getDeviceData() this.deviceData = await this.$db.getDeviceData()
@@ -53,4 +49,4 @@ export default {
this.init() this.init()
} }
} }
</script> </script>
+3
View File
@@ -23,6 +23,9 @@
</div> </div>
</template> </template>
</div> </div>
<div v-if="localLibraryItems.length" class="mt-4 text-sm text-fg-muted">
{{ $strings.LabelTotalSize }}: {{ $bytesPretty(localLibraryItems.reduce((acc, item) => acc + item.size, 0)) }}
</div>
</div> </div>
</template> </template>
+73 -18
View File
@@ -41,7 +41,7 @@
</ui-btn> </ui-btn>
</div> </div>
<p class="text-sm text-fg mt-1.5 mb-0.5 default-style" v-html="description" /> <p class="text-sm text-fg mt-1.5 mb-0.5 default-style description-container" v-html="transformedDescription"></p>
<!-- loading overlay --> <!-- loading overlay -->
<div v-if="processing" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-30 flex items-center justify-center"> <div v-if="processing" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-30 flex items-center justify-center">
@@ -56,6 +56,7 @@
import { Capacitor } from '@capacitor/core' import { Capacitor } from '@capacitor/core'
import { Dialog } from '@capacitor/dialog' import { Dialog } from '@capacitor/dialog'
import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor' import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor'
import cellularPermissionHelpers from '@/mixins/cellularPermissionHelpers'
export default { export default {
async asyncData({ store, params, redirect, app }) { async asyncData({ store, params, redirect, app }) {
@@ -115,7 +116,11 @@ export default {
startingDownload: false startingDownload: false
} }
}, },
mixins: [cellularPermissionHelpers],
computed: { computed: {
transformedDescription() {
return this.parseDescription(this.description)
},
bookCoverAspectRatio() { bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio'] return this.$store.getters['libraries/getBookCoverAspectRatio']
}, },
@@ -310,6 +315,24 @@ export default {
} }
}, },
methods: { methods: {
parseDescription(description) {
const timeMarkerLinkRegex = /<a href="#([^"]*?\b\d{1,2}:\d{1,2}(?::\d{1,2})?)">(.*?)<\/a>/g
const timeMarkerRegex = /\b\d{1,2}:\d{1,2}(?::\d{1,2})?\b/g
function convertToSeconds(time) {
const timeParts = time.split(':').map(Number)
return timeParts.reduce((acc, part, index) => acc * 60 + part, 0)
}
return description.replace(timeMarkerLinkRegex, (match, href, displayTime) => {
const time = displayTime.match(timeMarkerRegex)[0]
const seekTimeInSeconds = convertToSeconds(time)
return `<span class="time-marker cursor-pointer text-blue-400 hover:text-blue-300" data-time="${seekTimeInSeconds}">${displayTime}</span>`
}).replace(timeMarkerRegex, (match) => {
const seekTimeInSeconds = convertToSeconds(match)
return `<span class="time-marker cursor-pointer text-blue-400 hover:text-blue-300" data-time="${seekTimeInSeconds}">${match}</span>`
})
},
async deleteLocalEpisode() { async deleteLocalEpisode() {
await this.$hapticsImpact() await this.$hapticsImpact()
@@ -351,28 +374,56 @@ export default {
} else { } else {
this.$store.commit('setPlayerIsStartingPlayback', this.episode.id) this.$store.commit('setPlayerIsStartingPlayback', this.episode.id)
if (this.localEpisodeId && this.localLibraryItemId && !this.isLocal) { const playbackData = this.generatePlaybackData()
console.log('Play local episode', this.localEpisodeId, this.localLibraryItemId) this.emitPlayItemEvent(playbackData)
}
},
async clickPlaybackTime(event) {
const startTime = event.target.getAttribute('data-time')
if (this.playerIsStartingPlayback) return
this.$eventBus.$emit('play-item', { await this.$hapticsImpact()
libraryItemId: this.localLibraryItemId,
episodeId: this.localEpisodeId, this.$store.commit('setPlayerIsStartingPlayback', this.episode.id)
serverLibraryItemId: this.serverLibraryItemId,
serverEpisodeId: this.serverEpisodeId const playbackData = this.generatePlaybackData(startTime)
}) this.emitPlayItemEvent(playbackData)
} else { },
this.$eventBus.$emit('play-item', { generatePlaybackData(startTime) {
libraryItemId: this.libraryItemId, const playbackData = {
episodeId: this.episode.id, libraryItemId: this.libraryItemId,
serverLibraryItemId: this.serverLibraryItemId, episodeId: this.episode.id,
serverEpisodeId: this.serverEpisodeId serverLibraryItemId: this.serverLibraryItemId,
}) serverEpisodeId: this.serverEpisodeId,
} startTime
}
if (this.localEpisodeId && this.localLibraryItemId && !this.isLocal) {
playbackData.libraryItemId = this.localLibraryItemId
playbackData.episodeId = this.localEpisodeId
}
return playbackData
},
emitPlayItemEvent(playbackData) {
this.$eventBus.$emit('play-item', playbackData)
},
bindTimeMarkerEvents() {
const container = document.querySelector('.description-container')
if (container) {
container.addEventListener('click', (event) => {
if (event.target.classList.contains('time-marker')) {
this.clickPlaybackTime(event)
}
})
} }
}, },
async downloadClick() { async downloadClick() {
if (this.downloadItem || this.startingDownload) return if (this.downloadItem || this.startingDownload) return
const hasPermission = await this.checkCellularPermission('download')
if (!hasPermission) return
this.startingDownload = true this.startingDownload = true
setTimeout(() => { setTimeout(() => {
this.startingDownload = false this.startingDownload = false
@@ -557,9 +608,13 @@ export default {
}, },
mounted() { mounted() {
this.$eventBus.$on('new-local-library-item', this.newLocalLibraryItem) this.$eventBus.$on('new-local-library-item', this.newLocalLibraryItem)
this.bindTimeMarkerEvents()
}, },
beforeDestroy() { beforeDestroy() {
this.$eventBus.$off('new-local-library-item', this.newLocalLibraryItem) this.$eventBus.$off('new-local-library-item', this.newLocalLibraryItem)
document.querySelectorAll('.time-marker').forEach(marker => {
marker.removeEventListener('click', this.clickPlaybackTime)
})
} }
} }
</script> </script>
+87 -52
View File
@@ -1,5 +1,8 @@
<template> <template>
<div id="item-page" class="w-full h-full overflow-y-auto overflow-x-hidden relative bg-bg"> <div v-if="!libraryItem" class="w-full h-full relative flex items-center justify-center bg-bg">
<ui-loading-indicator />
</div>
<div v-else id="item-page" class="w-full h-full overflow-y-auto overflow-x-hidden relative bg-bg">
<!-- cover --> <!-- cover -->
<div class="w-full flex justify-center relative"> <div class="w-full flex justify-center relative">
<div style="width: 0; transform: translateX(-50vw); overflow: visible"> <div style="width: 0; transform: translateX(-50vw); overflow: visible">
@@ -25,7 +28,10 @@
<div class="relative z-10 px-3 py-4"> <div class="relative z-10 px-3 py-4">
<!-- title --> <!-- title -->
<div class="text-center mb-2"> <div class="text-center mb-2">
<h1 class="text-xl font-semibold">{{ title }}</h1> <div class="flex items-center justify-center">
<h1 class="text-xl font-semibold">{{ title }}</h1>
<widgets-explicit-indicator v-if="isExplicit" />
</div>
<p v-if="subtitle" class="text-fg text-base">{{ subtitle }}</p> <p v-if="subtitle" class="text-fg text-base">{{ subtitle }}</p>
</div> </div>
@@ -165,45 +171,40 @@
import { Dialog } from '@capacitor/dialog' import { Dialog } from '@capacitor/dialog'
import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor' import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor'
import { FastAverageColor } from 'fast-average-color' import { FastAverageColor } from 'fast-average-color'
import cellularPermissionHelpers from '@/mixins/cellularPermissionHelpers'
export default { export default {
async asyncData({ store, params, redirect, app }) { async asyncData({ store, params, redirect, app, query }) {
const libraryItemId = params.id const libraryItemId = params.id
let libraryItem = null let libraryItem = null
if (libraryItemId.startsWith('local')) { if (libraryItemId.startsWith('local')) {
libraryItem = await app.$db.getLocalLibraryItem(libraryItemId) libraryItem = await app.$db.getLocalLibraryItem(libraryItemId)
console.log('Got lli', libraryItemId) if (!libraryItem) {
return redirect('/?error=Failed to get downloaded library item')
}
// If library item is linked to the currently connected server then redirect to the page using the server library item id // If library item is linked to the currently connected server then redirect to the page using the server library item id
if (libraryItem?.libraryItemId?.startsWith('li_')) { if (libraryItem?.libraryItemId?.startsWith('li_')) {
// Detect old library item id // Detect old library item id
console.error('Local library item has old server library item id', libraryItem.libraryItemId) console.error('Local library item has old server library item id', libraryItem.libraryItemId)
} else if (libraryItem?.libraryItemId && libraryItem?.serverAddress === store.getters['user/getServerAddress'] && store.state.networkConnected) { } else if (query.noredirect !== '1' && libraryItem?.libraryItemId && libraryItem?.serverAddress === store.getters['user/getServerAddress'] && store.state.networkConnected) {
let query = '' const queryParams = new URLSearchParams()
if (libraryItem.mediaType === 'podcast') query = '?episodefilter=downloaded' // Filter by downloaded when redirecting from the local copy queryParams.set('localLibraryItemId', libraryItemId)
return redirect(`/item/${libraryItem.libraryItemId}${query}`) if (libraryItem.mediaType === 'podcast') {
} // Filter by downloaded when redirecting from the local copy
} else if (store.state.user.serverConnectionConfig) { queryParams.set('episodefilter', 'downloaded')
libraryItem = await app.$nativeHttp.get(`/api/items/${libraryItemId}?expanded=1&include=rssfeed`).catch((error) => {
console.error('Failed', error)
return false
})
if (libraryItem) {
const localLibraryItem = await app.$db.getLocalLibraryItemByLId(libraryItemId)
if (localLibraryItem) {
console.log('Library item has local library item also', localLibraryItem.id)
libraryItem.localLibraryItem = localLibraryItem
} }
return redirect(`/item/${libraryItem.libraryItemId}?${queryParams.toString()}`)
} }
} else if (!store.state.user.serverConnectionConfig) {
// Not connected to server
return redirect('/?error=No server connection to get library item')
} }
if (!libraryItem) {
console.error('No item...', params.id)
return redirect('/')
}
return { return {
libraryItem, libraryItem,
rssFeed: libraryItem.rssFeed || null libraryItemId
} }
}, },
data() { data() {
@@ -221,6 +222,7 @@ export default {
startingDownload: false startingDownload: false
} }
}, },
mixins: [cellularPermissionHelpers],
computed: { computed: {
isIos() { isIos() {
return this.$platform === 'ios' return this.$platform === 'ios'
@@ -296,8 +298,8 @@ export default {
bookCoverAspectRatio() { bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio'] return this.$store.getters['libraries/getBookCoverAspectRatio']
}, },
libraryItemId() { rssFeed() {
return this.libraryItem.id return this.libraryItem?.rssFeed
}, },
mediaType() { mediaType() {
return this.libraryItem.mediaType return this.libraryItem.mediaType
@@ -439,6 +441,9 @@ export default {
isInvalid() { isInvalid() {
return this.libraryItem.isInvalid return this.libraryItem.isInvalid
}, },
isExplicit() {
return !!this.mediaMetadata.explicit
},
showPlay() { showPlay() {
return !this.isMissing && !this.isInvalid && (this.numTracks || this.episodes.length) return !this.isMissing && !this.isInvalid && (this.numTracks || this.episodes.length)
}, },
@@ -604,9 +609,11 @@ export default {
this.download(localFolder) this.download(localFolder)
}, },
async downloadClick() { async downloadClick() {
if (this.downloadItem || this.startingDownload) { if (this.downloadItem || this.startingDownload) return
return
} const hasPermission = await this.checkCellularPermission('download')
if (!hasPermission) return
this.startingDownload = true this.startingDownload = true
setTimeout(() => { setTimeout(() => {
this.startingDownload = false this.startingDownload = false
@@ -722,31 +729,59 @@ export default {
if (!this.libraryItem.libraryId) return if (!this.libraryItem.libraryId) return
await this.$store.dispatch('libraries/fetch', this.libraryItem.libraryId) await this.$store.dispatch('libraries/fetch', this.libraryItem.libraryId)
this.$localStore.setLastLibraryId(this.libraryItem.libraryId) this.$localStore.setLastLibraryId(this.libraryItem.libraryId)
},
init() {
// If library of this item is different from current library then switch libraries
if (this.$store.state.libraries.currentLibraryId !== this.libraryItem.libraryId) {
this.setLibrary()
}
this.windowWidth = window.innerWidth
window.addEventListener('resize', this.windowResized)
this.$eventBus.$on('library-changed', this.libraryChanged)
this.$eventBus.$on('new-local-library-item', this.newLocalLibraryItem)
this.$socket.$on('item_updated', this.itemUpdated)
this.$socket.$on('rss_feed_open', this.rssFeedOpen)
this.$socket.$on('rss_feed_closed', this.rssFeedClosed)
this.checkDescriptionClamped()
// Set height of page below cover image
const itemPageBgGradientHeight = window.outerHeight - 64 - this.coverHeight
document.documentElement.style.setProperty('--item-page-bg-gradient-height', itemPageBgGradientHeight + 'px')
// Set last scroll position if was set for this item
if (this.$store.state.lastItemScrollData.id === this.libraryItemId && window['item-page']) {
window['item-page'].scrollTop = this.$store.state.lastItemScrollData.scrollTop || 0
}
},
async loadServerLibraryItem() {
console.log(`Fetching library item "${this.libraryItemId}" from server`)
const libraryItem = await this.$nativeHttp.get(`/api/items/${this.libraryItemId}?expanded=1&include=rssfeed`, { connectTimeout: 5000 }).catch((error) => {
console.error('Failed', error)
return null
})
if (libraryItem) {
const localLibraryItem = await this.$db.getLocalLibraryItemByLId(this.libraryItemId)
if (localLibraryItem) {
console.log('Library item has local library item also', localLibraryItem.id)
libraryItem.localLibraryItem = localLibraryItem
}
this.libraryItem = libraryItem
} else if (this.$route.query.localLibraryItemId) {
// Failed to get server library item but is local library item so redirect
return this.$router.replace(`/item/${this.$route.query.localLibraryItemId}?noredirect=1`)
} else {
this.$toast.error('Failed to get library item from server')
return this.$router.replace('/bookshelf')
}
} }
}, },
mounted() { async mounted() {
// If library of this item is different from current library then switch libraries if (!this.libraryItem) {
if (this.$store.state.libraries.currentLibraryId !== this.libraryItem.libraryId) { await this.loadServerLibraryItem()
this.setLibrary()
}
this.windowWidth = window.innerWidth
window.addEventListener('resize', this.windowResized)
this.$eventBus.$on('library-changed', this.libraryChanged)
this.$eventBus.$on('new-local-library-item', this.newLocalLibraryItem)
this.$socket.$on('item_updated', this.itemUpdated)
this.$socket.$on('rss_feed_open', this.rssFeedOpen)
this.$socket.$on('rss_feed_closed', this.rssFeedClosed)
this.checkDescriptionClamped()
// Set height of page below cover image
const itemPageBgGradientHeight = window.outerHeight - 64 - this.coverHeight
document.documentElement.style.setProperty('--item-page-bg-gradient-height', itemPageBgGradientHeight + 'px')
// Set last scroll position if was set for this item
if (this.$store.state.lastItemScrollData.id === this.libraryItemId && window['item-page']) {
window['item-page'].scrollTop = this.$store.state.lastItemScrollData.scrollTop || 0
} }
this.init()
}, },
beforeDestroy() { beforeDestroy() {
window.removeEventListener('resize', this.windowResized) window.removeEventListener('resize', this.windowResized)
+74 -22
View File
@@ -2,7 +2,7 @@
<div class="w-full h-full py-6 px-2"> <div class="w-full h-full py-6 px-2">
<div v-if="localLibraryItem" class="w-full h-full"> <div v-if="localLibraryItem" class="w-full h-full">
<div class="px-2 flex items-center mb-2"> <div class="px-2 flex items-center mb-2">
<p class="text-base font-semibold">{{ mediaMetadata.title }}</p> <p class="text-base font-semibold truncate">{{ mediaMetadata.title }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
<button v-if="audioTracks.length && !isPodcast" class="shadow-sm text-success flex items-center justify-center rounded-full mx-2" @click.stop="play"> <button v-if="audioTracks.length && !isPodcast" class="shadow-sm text-success flex items-center justify-center rounded-full mx-2" @click.stop="play">
@@ -17,7 +17,10 @@
<div class="w-full max-w-full media-item-container overflow-y-auto overflow-x-hidden relative pb-4" :class="{ 'media-order-changed': orderChanged }"> <div class="w-full max-w-full media-item-container overflow-y-auto overflow-x-hidden relative pb-4" :class="{ 'media-order-changed': orderChanged }">
<div v-if="!isPodcast && audioTracksCopy.length" class="w-full py-2"> <div v-if="!isPodcast && audioTracksCopy.length" class="w-full py-2">
<p class="text-base mb-2">Audio Tracks ({{ audioTracks.length }})</p> <div class="flex justify-between items-center mb-2">
<p class="text-base">Audio Tracks ({{ audioTracks.length }})</p>
<p class="text-xs text-fg-muted px-2">{{ $strings.LabelTotalSize }}: {{ $bytesPretty(totalAudioSize) }}</p>
</div>
<draggable v-model="audioTracksCopy" v-bind="dragOptions" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate" :disabled="isIos"> <draggable v-model="audioTracksCopy" v-bind="dragOptions" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate" :disabled="isIos">
<transition-group type="transition" :name="!drag ? 'dragtrack' : null"> <transition-group type="transition" :name="!drag ? 'dragtrack' : null">
@@ -46,7 +49,10 @@
</div> </div>
<div v-if="isPodcast" class="w-full py-2"> <div v-if="isPodcast" class="w-full py-2">
<p class="text-base mb-2">Episodes ({{ episodes.length }})</p> <div class="flex justify-between items-center mb-2">
<p class="text-base">Episodes ({{ episodes.length }})</p>
<p class="text-xs text-fg-muted px-2">{{ $strings.LabelTotalSize }}: {{ $bytesPretty(totalEpisodesSize) }}</p>
</div>
<template v-for="episode in episodes"> <template v-for="episode in episodes">
<div :key="episode.id" class="flex items-center my-1"> <div :key="episode.id" class="flex items-center my-1">
<div class="w-10 h-12 flex items-center justify-center" style="min-width: 48px"> <div class="w-10 h-12 flex items-center justify-center" style="min-width: 48px">
@@ -83,22 +89,31 @@
</div> </div>
</div> </div>
<p v-if="otherFiles.length" class="text-lg py-2">Other Files</p> <div v-if="otherFiles.length">
<template v-for="file in otherFiles"> <div class="flex justify-between items-center py-2">
<div :key="file.id" class="flex items-center my-1"> <p class="text-lg">Other Files</p>
<div class="w-12 h-12 flex items-center justify-center"> <p class="text-xs text-fg-muted px-2">{{ $strings.LabelTotalSize }}: {{ $bytesPretty(totalOtherFilesSize) }}</p>
<img v-if="(file.mimeType || '').startsWith('image')" :src="getCapImageSrc(file.contentUrl)" class="w-full h-full object-contain" />
<span v-else class="material-icons">music_note</span>
</div>
<div class="flex-grow px-2">
<p class="text-sm">{{ file.filename }}</p>
</div>
<div class="w-24 text-center text-fg-muted" style="min-width: 96px">
<p class="text-xs">{{ file.mimeType }}</p>
<p class="text-sm">{{ $bytesPretty(file.size) }}</p>
</div>
</div> </div>
</template> <template v-for="file in otherFiles">
<div :key="file.id" class="flex items-center my-1">
<div class="w-12 h-12 flex items-center justify-center">
<img v-if="(file.mimeType || '').startsWith('image')" :src="getCapImageSrc(file.contentUrl)" class="w-full h-full object-contain" />
<span v-else class="material-icons">music_note</span>
</div>
<div class="flex-grow px-2">
<p class="text-sm">{{ file.filename }}</p>
</div>
<div class="w-24 text-center text-fg-muted" style="min-width: 96px">
<p class="text-xs">{{ file.mimeType }}</p>
<p class="text-sm">{{ $bytesPretty(file.size) }}</p>
</div>
</div>
</template>
</div>
<div class="mt-4 text-sm text-fg-muted">
{{ $strings.LabelTotalSize }}: {{ $bytesPretty(totalLibraryItemSize) }}
</div>
</div> </div>
</div> </div>
<div v-else class="px-2 w-full h-full"> <div v-else class="px-2 w-full h-full">
@@ -218,17 +233,27 @@ export default {
}, },
dialogItems() { dialogItems() {
if (this.selectedAudioTrack || this.selectedEpisode) { if (this.selectedAudioTrack || this.selectedEpisode) {
return [ const items = [
{ {
text: this.$strings.ButtonDeleteLocalFile, text: this.$strings.ButtonDeleteLocalFile,
value: 'track-delete' value: 'track-delete',
icon: 'delete'
} }
] ]
if (this.isPodcast && this.selectedEpisode) {
items.unshift({
text: this.$strings.ButtonPlayEpisode,
value: 'play-episode',
icon: 'play_arrow'
})
}
return items
} else { } else {
return [ return [
{ {
text: this.$strings.ButtonDeleteLocalItem, text: this.$strings.ButtonDeleteLocalItem,
value: 'delete' value: 'delete',
icon: 'delete'
} }
] ]
} }
@@ -236,6 +261,18 @@ export default {
playerIsStartingPlayback() { playerIsStartingPlayback() {
// Play has been pressed and waiting for native play response // Play has been pressed and waiting for native play response
return this.$store.state.playerIsStartingPlayback return this.$store.state.playerIsStartingPlayback
},
totalAudioSize() {
return this.audioTracks.reduce((acc, item) => item.metadata ? acc + item.metadata.size : acc, 0)
},
totalEpisodesSize() {
return this.episodes.reduce((acc, item) => acc + item.size, 0)
},
totalOtherFilesSize() {
return this.otherFiles.reduce((acc, item) => acc + item.size, 0)
},
totalLibraryItemSize() {
return this.localFiles.reduce((acc, item) => acc + item.size, 0)
} }
}, },
methods: { methods: {
@@ -291,6 +328,19 @@ export default {
getCapImageSrc(contentUrl) { getCapImageSrc(contentUrl) {
return Capacitor.convertFileSrc(contentUrl) return Capacitor.convertFileSrc(contentUrl)
}, },
async playEpisode() {
if (!this.selectedEpisode) return
if (this.playerIsStartingPlayback) return
await this.$hapticsImpact()
this.$store.commit('setPlayerIsStartingPlayback', this.selectedEpisode.serverEpisodeId)
this.$eventBus.$emit('play-item', {
libraryItemId: this.localLibraryItemId,
episodeId: this.selectedEpisode.id,
serverLibraryItemId: this.libraryItemId,
serverEpisodeId: this.selectedEpisode.serverEpisodeId
})
},
async dialogAction(action) { async dialogAction(action) {
console.log('Dialog action', action) console.log('Dialog action', action)
await this.$hapticsImpact() await this.$hapticsImpact()
@@ -300,8 +350,10 @@ export default {
} else if (action == 'track-delete') { } else if (action == 'track-delete') {
if (this.isPodcast) this.deleteEpisode() if (this.isPodcast) this.deleteEpisode()
else this.deleteTrack() else this.deleteTrack()
} else if (action == 'play-episode') {
this.playEpisode()
} }
this.showDialog = false this.showDialog = false;
}, },
getLocalFileForTrack(localFileId) { getLocalFileForTrack(localFileId) {
return this.localFiles.find((lf) => lf.id == localFileId) return this.localFiles.find((lf) => lf.id == localFileId)
+18 -2
View File
@@ -24,7 +24,7 @@
</div> </div>
</div> </div>
<modals-item-more-menu-modal v-model="showMoreMenu" :library-item="selectedLibraryItem" :episode="selectedEpisode" hide-rss-feed-option :processing.sync="processing" /> <modals-item-more-menu-modal v-model="showMoreMenu" :library-item="selectedLibraryItem" :episode="selectedEpisode" :playlist="playlist" hide-rss-feed-option :processing.sync="processing" />
<div v-show="processing" class="fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-black/50 z-50"> <div v-show="processing" class="fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-black/50 z-50">
<ui-loading-indicator /> <ui-loading-indicator />
</div> </div>
@@ -140,8 +140,24 @@ export default {
this.$eventBus.$emit('play-item', { libraryItemId: nextItem.libraryItemId, episodeId: nextItem.episodeId }) this.$eventBus.$emit('play-item', { libraryItemId: nextItem.libraryItemId, episodeId: nextItem.episodeId })
} }
} }
},
playlistUpdated(playlist) {
if (this.playlist.id !== playlist.id) return
this.playlist = playlist
},
playlistRemoved(playlist) {
if (this.playlist.id === playlist.id) {
this.$router.replace('/bookshelf/playlists')
}
} }
}, },
mounted() {} mounted() {
this.$socket.$on('playlist_updated', this.playlistUpdated)
this.$socket.$on('playlist_removed', this.playlistRemoved)
},
beforeDestroy() {
this.$socket.$off('playlist_updated', this.playlistUpdated)
this.$socket.$off('playlist_removed', this.playlistRemoved)
}
} }
</script> </script>
+108 -24
View File
@@ -2,16 +2,16 @@
<div class="w-full h-full px-4 py-8 overflow-y-auto"> <div class="w-full h-full px-4 py-8 overflow-y-auto">
<!-- Display settings --> <!-- Display settings -->
<p class="uppercase text-xs font-semibold text-fg-muted mb-2">{{ $strings.HeaderUserInterfaceSettings }}</p> <p class="uppercase text-xs font-semibold text-fg-muted mb-2">{{ $strings.HeaderUserInterfaceSettings }}</p>
<div class="flex items-center py-3" @click="toggleEnableAltView"> <div class="flex items-center py-3">
<div class="w-10 flex justify-center"> <div class="w-10 flex justify-center" @click="toggleEnableAltView">
<ui-toggle-switch v-model="enableBookshelfView" @input="saveSettings" /> <ui-toggle-switch v-model="enableBookshelfView" @input="saveSettings" />
</div> </div>
<p class="pl-4">{{ $strings.LabelUseBookshelfView }}</p> <p class="pl-4">{{ $strings.LabelUseBookshelfView }}</p>
</div> </div>
<!-- screen.orientation.lock not supported on iOS webview --> <!-- screen.orientation.lock not supported on iOS webview -->
<div v-if="!isiOS" class="flex items-center py-3" @click.stop="toggleLockOrientation"> <div v-if="!isiOS" class="flex items-center py-3">
<div class="w-10 flex justify-center pointer-events-none"> <div class="w-10 flex justify-center" @click.stop="toggleLockOrientation">
<ui-toggle-switch v-model="lockCurrentOrientation" /> <ui-toggle-switch v-model="lockCurrentOrientation" class="pointer-events-none" />
</div> </div>
<p class="pl-4">{{ $strings.LabelLockOrientation }}</p> <p class="pl-4">{{ $strings.LabelLockOrientation }}</p>
</div> </div>
@@ -36,37 +36,43 @@
<!-- Playback settings --> <!-- Playback settings -->
<p class="uppercase text-xs font-semibold text-fg-muted mb-2 mt-10">{{ $strings.HeaderPlaybackSettings }}</p> <p class="uppercase text-xs font-semibold text-fg-muted mb-2 mt-10">{{ $strings.HeaderPlaybackSettings }}</p>
<div v-if="!isiOS" class="flex items-center py-3" @click="toggleDisableAutoRewind"> <div v-if="!isiOS" class="flex items-center py-3">
<div class="w-10 flex justify-center"> <div class="w-10 flex justify-center" @click="toggleDisableAutoRewind">
<ui-toggle-switch v-model="settings.disableAutoRewind" @input="saveSettings" /> <ui-toggle-switch v-model="settings.disableAutoRewind" @input="saveSettings" />
</div> </div>
<p class="pl-4">{{ $strings.LabelDisableAutoRewind }}</p> <p class="pl-4">{{ $strings.LabelDisableAutoRewind }}</p>
</div> </div>
<div class="flex items-center py-3" @click="toggleJumpBackwards"> <div class="flex items-center py-3">
<div class="w-10 flex justify-center"> <div class="w-10 flex justify-center" @click="toggleJumpBackwards">
<span class="material-icons text-4xl">{{ currentJumpBackwardsTimeIcon }}</span> <span class="material-icons text-4xl">{{ currentJumpBackwardsTimeIcon }}</span>
</div> </div>
<p class="pl-4">{{ $strings.LabelJumpBackwardsTime }}</p> <p class="pl-4">{{ $strings.LabelJumpBackwardsTime }}</p>
</div> </div>
<div class="flex items-center py-3" @click="toggleJumpForward"> <div class="flex items-center py-3">
<div class="w-10 flex justify-center"> <div class="w-10 flex justify-center" @click="toggleJumpForward">
<span class="material-icons text-4xl">{{ currentJumpForwardTimeIcon }}</span> <span class="material-icons text-4xl">{{ currentJumpForwardTimeIcon }}</span>
</div> </div>
<p class="pl-4">{{ $strings.LabelJumpForwardsTime }}</p> <p class="pl-4">{{ $strings.LabelJumpForwardsTime }}</p>
</div> </div>
<div v-if="!isiOS" class="flex items-center py-3" @click="toggleEnableMp3IndexSeeking"> <div v-if="!isiOS" class="flex items-center py-3">
<div class="w-10 flex justify-center"> <div class="w-10 flex justify-center" @click="toggleEnableMp3IndexSeeking">
<ui-toggle-switch v-model="settings.enableMp3IndexSeeking" @input="saveSettings" /> <ui-toggle-switch v-model="settings.enableMp3IndexSeeking" @input="saveSettings" />
</div> </div>
<p class="pl-4">{{ $strings.LabelEnableMp3IndexSeeking }}</p> <p class="pl-4">{{ $strings.LabelEnableMp3IndexSeeking }}</p>
<span class="material-icons-outlined ml-2" @click.stop="showConfirmMp3IndexSeeking">info</span> <span class="material-icons-outlined ml-2" @click.stop="showConfirmMp3IndexSeeking">info</span>
</div> </div>
<div class="flex items-center py-3">
<div class="w-10 flex justify-center" @click="toggleAllowSeekingOnMediaControls">
<ui-toggle-switch v-model="settings.allowSeekingOnMediaControls" @input="saveSettings" />
</div>
<p class="pl-4">{{ $strings.LabelAllowSeekingOnMediaControls }}</p>
</div>
<!-- Sleep timer settings --> <!-- Sleep timer settings -->
<template v-if="!isiOS"> <template v-if="!isiOS">
<p class="uppercase text-xs font-semibold text-fg-muted mb-2 mt-10">{{ $strings.HeaderSleepTimerSettings }}</p> <p class="uppercase text-xs font-semibold text-fg-muted mb-2 mt-10">{{ $strings.HeaderSleepTimerSettings }}</p>
<div class="flex items-center py-3" @click="toggleDisableShakeToResetSleepTimer"> <div class="flex items-center py-3">
<div class="w-10 flex justify-center"> <div class="w-10 flex justify-center" @click="toggleDisableShakeToResetSleepTimer">
<ui-toggle-switch v-model="settings.disableShakeToResetSleepTimer" @input="saveSettings" /> <ui-toggle-switch v-model="settings.disableShakeToResetSleepTimer" @input="saveSettings" />
</div> </div>
<p class="pl-4">{{ $strings.LabelDisableShakeToReset }}</p> <p class="pl-4">{{ $strings.LabelDisableShakeToReset }}</p>
@@ -78,22 +84,22 @@
<ui-text-input :value="shakeSensitivityOption" readonly append-icon="expand_more" style="width: 145px; max-width: 145px" /> <ui-text-input :value="shakeSensitivityOption" readonly append-icon="expand_more" style="width: 145px; max-width: 145px" />
</div> </div>
</div> </div>
<div class="flex items-center py-3" @click="toggleDisableSleepTimerFadeOut"> <div class="flex items-center py-3">
<div class="w-10 flex justify-center"> <div class="w-10 flex justify-center" @click="toggleDisableSleepTimerFadeOut">
<ui-toggle-switch v-model="settings.disableSleepTimerFadeOut" @input="saveSettings" /> <ui-toggle-switch v-model="settings.disableSleepTimerFadeOut" @input="saveSettings" />
</div> </div>
<p class="pl-4">{{ $strings.LabelDisableAudioFadeOut }}</p> <p class="pl-4">{{ $strings.LabelDisableAudioFadeOut }}</p>
<span class="material-icons-outlined ml-2" @click.stop="showInfo('disableSleepTimerFadeOut')">info</span> <span class="material-icons-outlined ml-2" @click.stop="showInfo('disableSleepTimerFadeOut')">info</span>
</div> </div>
<div class="flex items-center py-3" @click="toggleDisableSleepTimerResetFeedback"> <div class="flex items-center py-3">
<div class="w-10 flex justify-center"> <div class="w-10 flex justify-center" @click="toggleDisableSleepTimerResetFeedback">
<ui-toggle-switch v-model="settings.disableSleepTimerResetFeedback" @input="saveSettings" /> <ui-toggle-switch v-model="settings.disableSleepTimerResetFeedback" @input="saveSettings" />
</div> </div>
<p class="pl-4">{{ $strings.LabelDisableVibrateOnReset }}</p> <p class="pl-4">{{ $strings.LabelDisableVibrateOnReset }}</p>
<span class="material-icons-outlined ml-2" @click.stop="showInfo('disableSleepTimerResetFeedback')">info</span> <span class="material-icons-outlined ml-2" @click.stop="showInfo('disableSleepTimerResetFeedback')">info</span>
</div> </div>
<div class="flex items-center py-3" @click="toggleAutoSleepTimer"> <div class="flex items-center py-3">
<div class="w-10 flex justify-center"> <div class="w-10 flex justify-center" @click="toggleAutoSleepTimer">
<ui-toggle-switch v-model="settings.autoSleepTimer" @input="saveSettings" /> <ui-toggle-switch v-model="settings.autoSleepTimer" @input="saveSettings" />
</div> </div>
<p class="pl-4">{{ $strings.LabelAutoSleepTimer }}</p> <p class="pl-4">{{ $strings.LabelAutoSleepTimer }}</p>
@@ -115,8 +121,8 @@
<ui-text-input :value="sleepTimerLengthOption" readonly append-icon="expand_more" style="width: 145px; max-width: 145px" /> <ui-text-input :value="sleepTimerLengthOption" readonly append-icon="expand_more" style="width: 145px; max-width: 145px" />
</div> </div>
</div> </div>
<div v-if="settings.autoSleepTimer" class="flex items-center py-3" @click="toggleAutoSleepTimerAutoRewind"> <div v-if="settings.autoSleepTimer" class="flex items-center py-3">
<div class="w-10 flex justify-center"> <div class="w-10 flex justify-center" @click="toggleAutoSleepTimerAutoRewind">
<ui-toggle-switch v-model="settings.autoSleepTimerAutoRewind" @input="saveSettings" /> <ui-toggle-switch v-model="settings.autoSleepTimerAutoRewind" @input="saveSettings" />
</div> </div>
<p class="pl-4">{{ $strings.LabelAutoSleepTimerAutoRewind }}</p> <p class="pl-4">{{ $strings.LabelAutoSleepTimerAutoRewind }}</p>
@@ -129,6 +135,21 @@
</div> </div>
</div> </div>
<!-- Data settings -->
<p class="uppercase text-xs font-semibold text-fg-muted mb-2 mt-10">{{ $strings.HeaderDataSettings }}</p>
<div class="py-3 flex items-center">
<p class="pr-4 w-36">{{ $strings.LabelDownloadUsingCellular }}</p>
<div @click.stop="showDownloadUsingCellularOptions">
<ui-text-input :value="downloadUsingCellularOption" readonly append-icon="expand_more" style="max-width: 200px" />
</div>
</div>
<div class="py-3 flex items-center">
<p class="pr-4 w-36">{{ $strings.LabelStreamingUsingCellular }}</p>
<div @click.stop="showStreamingUsingCellularOptions">
<ui-text-input :value="streamingUsingCellularOption" readonly append-icon="expand_more" style="max-width: 200px" />
</div>
</div>
<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>
@@ -154,6 +175,7 @@ export default {
settings: { settings: {
disableAutoRewind: false, disableAutoRewind: false,
enableAltView: true, enableAltView: true,
allowSeekingOnMediaControls: false,
jumpForwardTime: 10, jumpForwardTime: 10,
jumpBackwardsTime: 10, jumpBackwardsTime: 10,
enableMp3IndexSeeking: false, enableMp3IndexSeeking: false,
@@ -169,7 +191,9 @@ export default {
disableSleepTimerResetFeedback: false, disableSleepTimerResetFeedback: false,
autoSleepTimerAutoRewind: false, autoSleepTimerAutoRewind: false,
autoSleepTimerAutoRewindTime: 300000, // 5 minutes autoSleepTimerAutoRewindTime: 300000, // 5 minutes
languageCode: 'en-us' languageCode: 'en-us',
downloadUsingCellular: 'ALWAYS',
streamingUsingCellular: 'ALWAYS'
}, },
theme: 'dark', theme: 'dark',
lockCurrentOrientation: false, lockCurrentOrientation: false,
@@ -238,6 +262,34 @@ export default {
text: this.$strings.LabelVeryHigh, text: this.$strings.LabelVeryHigh,
value: 'VERY_HIGH' value: 'VERY_HIGH'
} }
],
downloadUsingCellularItems: [
{
text: this.$strings.LabelAskConfirmation,
value: 'ASK'
},
{
text: this.$strings.LabelAlways,
value: 'ALWAYS'
},
{
text: this.$strings.LabelNever,
value: 'NEVER'
}
],
streamingUsingCellularItems: [
{
text: this.$strings.LabelAskConfirmation,
value: 'ASK'
},
{
text: this.$strings.LabelAlways,
value: 'ALWAYS'
},
{
text: this.$strings.LabelNever,
value: 'NEVER'
}
] ]
} }
}, },
@@ -312,11 +364,21 @@ export default {
const minutes = Number(this.settings.autoSleepTimerAutoRewindTime) / 1000 / 60 const minutes = Number(this.settings.autoSleepTimerAutoRewindTime) / 1000 / 60
return `${minutes} min` return `${minutes} min`
}, },
downloadUsingCellularOption() {
const item = this.downloadUsingCellularItems.find((i) => i.value === this.settings.downloadUsingCellular)
return item?.text || 'Error'
},
streamingUsingCellularOption() {
const item = this.streamingUsingCellularItems.find((i) => i.value === this.settings.streamingUsingCellular)
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
else if (this.moreMenuSetting === 'language') return this.languageOptionItems else if (this.moreMenuSetting === 'language') return this.languageOptionItems
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 === 'streamingUsingCellular') return this.streamingUsingCellularItems
return [] return []
} }
}, },
@@ -351,6 +413,14 @@ export default {
this.moreMenuSetting = 'theme' this.moreMenuSetting = 'theme'
this.showMoreMenuDialog = true this.showMoreMenuDialog = true
}, },
showDownloadUsingCellularOptions() {
this.moreMenuSetting = 'downloadUsingCellular'
this.showMoreMenuDialog = true
},
showStreamingUsingCellularOptions() {
this.moreMenuSetting = 'streamingUsingCellular'
this.showMoreMenuDialog = true
},
clickMenuAction(action) { clickMenuAction(action) {
this.showMoreMenuDialog = false this.showMoreMenuDialog = false
if (this.moreMenuSetting === 'shakeSensitivity') { if (this.moreMenuSetting === 'shakeSensitivity') {
@@ -365,6 +435,12 @@ export default {
} else if (this.moreMenuSetting === 'theme') { } else if (this.moreMenuSetting === 'theme') {
this.theme = action this.theme = action
this.saveTheme(action) this.saveTheme(action)
} else if (this.moreMenuSetting === 'downloadUsingCellular') {
this.settings.downloadUsingCellular = action
this.saveSettings()
} else if (this.moreMenuSetting === 'streamingUsingCellular') {
this.settings.streamingUsingCellular = action
this.saveSettings()
} }
}, },
saveTheme(theme) { saveTheme(theme) {
@@ -429,6 +505,10 @@ export default {
this.settings.enableAltView = !this.settings.enableAltView this.settings.enableAltView = !this.settings.enableAltView
this.saveSettings() this.saveSettings()
}, },
toggleAllowSeekingOnMediaControls() {
this.settings.allowSeekingOnMediaControls = !this.settings.allowSeekingOnMediaControls
this.saveSettings()
},
getCurrentOrientation() { getCurrentOrientation() {
const orientation = window.screen?.orientation || {} const orientation = window.screen?.orientation || {}
const type = orientation.type || '' const type = orientation.type || ''
@@ -471,6 +551,7 @@ export default {
const deviceSettings = this.deviceData.deviceSettings || {} const deviceSettings = this.deviceData.deviceSettings || {}
this.settings.disableAutoRewind = !!deviceSettings.disableAutoRewind this.settings.disableAutoRewind = !!deviceSettings.disableAutoRewind
this.settings.enableAltView = !!deviceSettings.enableAltView this.settings.enableAltView = !!deviceSettings.enableAltView
this.settings.allowSeekingOnMediaControls = !!deviceSettings.allowSeekingOnMediaControls
this.settings.jumpForwardTime = deviceSettings.jumpForwardTime || 10 this.settings.jumpForwardTime = deviceSettings.jumpForwardTime || 10
this.settings.jumpBackwardsTime = deviceSettings.jumpBackwardsTime || 10 this.settings.jumpBackwardsTime = deviceSettings.jumpBackwardsTime || 10
this.settings.enableMp3IndexSeeking = !!deviceSettings.enableMp3IndexSeeking this.settings.enableMp3IndexSeeking = !!deviceSettings.enableMp3IndexSeeking
@@ -492,6 +573,9 @@ export default {
this.settings.autoSleepTimerAutoRewindTime = !isNaN(deviceSettings.autoSleepTimerAutoRewindTime) ? deviceSettings.autoSleepTimerAutoRewindTime : 300000 // 5 minutes this.settings.autoSleepTimerAutoRewindTime = !isNaN(deviceSettings.autoSleepTimerAutoRewindTime) ? deviceSettings.autoSleepTimerAutoRewindTime : 300000 // 5 minutes
this.settings.languageCode = deviceSettings.languageCode || 'en-us' this.settings.languageCode = deviceSettings.languageCode || 'en-us'
this.settings.downloadUsingCellular = deviceSettings.downloadUsingCellular || 'ALWAYS'
this.settings.streamingUsingCellular = deviceSettings.streamingUsingCellular || 'ALWAYS'
}, },
async init() { async init() {
this.loading = true this.loading = true
+1 -1
View File
@@ -196,7 +196,7 @@ class AbsDatabaseWeb extends WebPlugin {
return null return null
} }
async syncLocalSessionsWithServer() { async syncLocalSessionsWithServer({ isFirstSync }) {
return null return null
} }
+2 -2
View File
@@ -70,8 +70,8 @@ class DbService {
return AbsDatabase.removeLocalMediaProgress({ localMediaProgressId }) return AbsDatabase.removeLocalMediaProgress({ localMediaProgressId })
} }
syncLocalSessionsWithServer() { syncLocalSessionsWithServer(isFirstSync) {
return AbsDatabase.syncLocalSessionsWithServer() return AbsDatabase.syncLocalSessionsWithServer({ isFirstSync })
} }
syncServerMediaProgressWithLocalMediaProgress(payload) { syncServerMediaProgressWithLocalMediaProgress(payload) {
+35 -30
View File
@@ -1,38 +1,41 @@
import Vue from "vue" import Vue from 'vue'
import enUsStrings from '../strings/en-us.json' import enUsStrings from '../strings/en-us.json'
const defaultCode = 'en-us' const defaultCode = 'en-us'
let $localStore = null let $localStore = null
const languageCodeMap = { const languageCodeMap = {
'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' },
'en-us': { label: 'English', dateFnsLocale: 'enUS' }, 'en-us': { label: 'English', dateFnsLocale: 'enUS' },
'es': { label: 'Español', dateFnsLocale: 'es' }, es: { label: 'Español', dateFnsLocale: 'es' },
'fr': { label: 'Français', dateFnsLocale: 'fr' }, fi: { label: 'Suomi', dateFnsLocale: 'fi' },
'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' }, fr: { label: 'Français', dateFnsLocale: 'fr' },
'it': { label: 'Italiano', dateFnsLocale: 'it' }, hr: { label: 'Hrvatski', dateFnsLocale: 'hr' },
'lt': { label: 'Lietuvių', dateFnsLocale: 'lt' }, it: { label: 'Italiano', dateFnsLocale: 'it' },
'nl': { label: 'Nederlands', dateFnsLocale: 'nl' }, lt: { label: 'Lietuvių', dateFnsLocale: 'lt' },
'no': { label: 'Norsk', dateFnsLocale: 'no' }, hu: { label: 'Magyar', dateFnsLocale: 'hu' },
'pl': { label: 'Polski', dateFnsLocale: 'pl' }, nl: { label: 'Nederlands', dateFnsLocale: 'nl' },
'ru': { label: 'Русский', dateFnsLocale: 'ru' }, no: { label: 'Norsk', dateFnsLocale: 'no' },
'sv': { label: 'Svenska', dateFnsLocale: 'sv' }, pl: { label: 'Polski', dateFnsLocale: 'pl' },
'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }, 'pt-br': { label: 'Português (Brasil)', dateFnsLocale: 'ptBR' },
ru: { label: 'Русский', dateFnsLocale: 'ru' },
sv: { label: 'Svenska', dateFnsLocale: 'sv' },
uk: { label: 'Українська', dateFnsLocale: 'uk' },
'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' },
'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }
} }
function supplant(str, subs) { function supplant(str, subs) {
// source: http://crockford.com/javascript/remedial.html // source: http://crockford.com/javascript/remedial.html
return str.replace(/{([^{}]*)}/g, return str.replace(/{([^{}]*)}/g, function (a, b) {
function (a, b) { var r = subs[b]
var r = subs[b] return typeof r === 'string' || typeof r === 'number' ? r : a
return typeof r === 'string' || typeof r === 'number' ? r : a })
}
)
} }
Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => { Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map((code) => {
return { return {
text: languageCodeMap[code].label, text: languageCodeMap[code].label,
value: code value: code
@@ -62,12 +65,14 @@ var translations = {
function loadTranslationStrings(code) { function loadTranslationStrings(code) {
return new Promise((resolve) => { return new Promise((resolve) => {
import(`../strings/${code}`).then((fileContents) => { import(`../strings/${code}`)
resolve(fileContents.default) .then((fileContents) => {
}).catch((error) => { resolve(fileContents.default)
console.error('Failed to load i18n strings', code, error) })
resolve(null) .catch((error) => {
}) console.error('Failed to load i18n strings', code, error)
resolve(null)
})
}) })
} }
@@ -78,7 +83,7 @@ async function loadi18n(code) {
return false return false
} }
const strings = translations[code] || await loadTranslationStrings(code) const strings = translations[code] || (await loadTranslationStrings(code))
if (!strings) { if (!strings) {
console.warn(`Invalid lang code ${code}`) console.warn(`Invalid lang code ${code}`)
return false return false
@@ -131,4 +136,4 @@ async function initialize() {
export default ({ app, store }, inject) => { export default ({ app, store }, inject) => {
$localStore = app.$localStore $localStore = app.$localStore
initialize() initialize()
} }
+23 -43
View File
@@ -42,56 +42,20 @@ class LocalStorage {
} }
} }
async setUseChapterTrack(useChapterTrack) { async setPlayerSettings(playerSettings) {
try { try {
await Preferences.set({ key: 'useChapterTrack', value: useChapterTrack ? '1' : '0' }) await Preferences.set({ key: 'playerSettings', value: JSON.stringify(playerSettings) })
} catch (error) { } catch (error) {
console.error('[LocalStorage] Failed to set use chapter track', error) console.error('[LocalStorage] Failed to set player settings', error)
} }
} }
async getUseChapterTrack() { async getPlayerSettings() {
try { try {
var obj = await Preferences.get({ key: 'useChapterTrack' }) || {} const playerSettingsObj = await Preferences.get({ key: 'playerSettings' }) || {}
return obj.value === '1' return playerSettingsObj.value ? JSON.parse(playerSettingsObj.value) : null
} catch (error) { } catch (error) {
console.error('[LocalStorage] Failed to get use chapter track', error) console.error('[LocalStorage] Failed to get player settings', error)
return false
}
}
async setUseTotalTrack(useTotalTrack) {
try {
await Preferences.set({ key: 'useTotalTrack', value: useTotalTrack ? '1' : '0' })
} catch (error) {
console.error('[LocalStorage] Failed to set use total track', error)
}
}
async getUseTotalTrack() {
try {
var obj = await Preferences.get({ key: 'useTotalTrack' }) || {}
return obj.value === '1'
} catch (error) {
console.error('[LocalStorage] Failed to get use total track', error)
return false
}
}
async setPlayerLock(lock) {
try {
await Preferences.set({ key: 'playerLock', value: lock ? '1' : '0' })
} catch (error) {
console.error('[LocalStorage] Failed to set player lock', error)
}
}
async getPlayerLock() {
try {
var obj = await Preferences.get({ key: 'playerLock' }) || {}
return obj.value === '1'
} catch (error) {
console.error('[LocalStorage] Failed to get player lock', error)
return false return false
} }
} }
@@ -179,6 +143,22 @@ class LocalStorage {
return false return false
} }
} }
/**
* Get preference value by key
*
* @param {string} key
* @returns {Promise<string>}
*/
async getPreferenceByKey(key) {
try {
const obj = await Preferences.get({ key }) || {}
return obj.value || null
} catch (error) {
console.error(`[LocalStorage] Failed to get preference "${key}"`, error)
return null
}
}
} }
+8 -5
View File
@@ -48,11 +48,7 @@ class ServerSocket extends EventEmitter {
this.socket.on('init', this.onInit.bind(this)) this.socket.on('init', this.onInit.bind(this))
this.socket.on('user_updated', this.onUserUpdated.bind(this)) this.socket.on('user_updated', this.onUserUpdated.bind(this))
this.socket.on('user_item_progress_updated', this.onUserItemProgressUpdated.bind(this)) this.socket.on('user_item_progress_updated', this.onUserItemProgressUpdated.bind(this))
this.socket.on('playlist_added', this.onPlaylistAdded.bind(this))
// Good for testing socket requests
// this.socket.onAny((evt, args) => {
// console.log(`[SOCKET] onAny: ${this.socket.id}: ${evt} ${JSON.stringify(args)}`)
// })
} }
removeListeners() { removeListeners() {
@@ -93,6 +89,13 @@ class ServerSocket extends EventEmitter {
this.$store.commit('user/updateUserMediaProgress', payload.data) this.$store.commit('user/updateUserMediaProgress', payload.data)
this.emit('user_media_progress_updated', payload) this.emit('user_media_progress_updated', payload)
} }
onPlaylistAdded() {
// Currently numUserPlaylists is only used for showing the playlist tab or not. Precise number is not necessary
if (!this.$store.state.libraries.numUserPlaylists) {
this.$store.commit('libraries/setNumUserPlaylists', 1)
}
}
} }
export default ({ app, store }, inject) => { export default ({ app, store }, inject) => {
+33 -17
View File
@@ -3,38 +3,40 @@
Audiobookshelf is a self-hosted audiobook and podcast server. Audiobookshelf is a self-hosted audiobook and podcast server.
### Android (beta) ### Android (beta)
Get the Android app on the [Google Play Store](https://play.google.com/store/apps/details?id=com.audiobookshelf.app) Get the Android app on the [Google Play Store](https://play.google.com/store/apps/details?id=com.audiobookshelf.app)
### iOS (early beta) ### iOS (early beta)
Available to beta testers through Test Flight **Beta is currently full. Apple has a hard limit of 10k beta testers. Updates will be posted in Discord.**
Join the beta testers and install the iOS app: https://testflight.apple.com/join/wiic7QIW Using Test Flight: https://testflight.apple.com/join/wiic7QIW **_(beta is full)_**
--- ---
[Go to the main project repo github.com/advplyr/audiobookshelf](https://github.com/advplyr/audiobookshelf) or the project site [audiobookshelf.org](https://audiobookshelf.org) [Go to the main project repo github.com/advplyr/audiobookshelf](https://github.com/advplyr/audiobookshelf) or the project site [audiobookshelf.org](https://audiobookshelf.org)
Join us on [discord](https://discord.gg/pJsjuNCKRq) or [Matrix](https://matrix.to/#/#audiobookshelf:matrix.org) Join us on [discord](https://discord.gg/pJsjuNCKRq)
**Requires an Audiobookshelf server to connect with** **Requires an Audiobookshelf server to connect with**
<img alt="Screenshot" src="https://github.com/advplyr/audiobookshelf-app/raw/master/screenshots/DeviceDemoScreens.png" /> <img alt="Screenshot" src="https://github.com/advplyr/audiobookshelf-app/raw/master/screenshots/DeviceDemoScreens.png" />
## Contributing ## Contributing
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).
### Windows Environment Setup for Android ### Windows Environment Setup for Android
Required Software: Required Software:
* [Git](https://git-scm.com/downloads) - [Git](https://git-scm.com/downloads)
* [Node.js](https://nodejs.org/en/) - [Node.js](https://nodejs.org/en/) (version 20)
* Code editor of choice([VSCode](https://code.visualstudio.com/download), etc) - Code editor of choice([VSCode](https://code.visualstudio.com/download), etc)
* [Android Studio](https://developer.android.com/studio) - [Android Studio](https://developer.android.com/studio)
* [Android SDK](https://developer.android.com/studio) - [Android SDK](https://developer.android.com/studio)
<details> <details>
<summary>Install the required software with <a href=(https://docs.microsoft.com/en-us/windows/package-manager/winget/#production-recommended)>winget</a></summary> <summary>Install the required software with <a href=(https://docs.microsoft.com/en-us/windows/package-manager/winget/#production-recommended)>winget</a></summary>
@@ -46,7 +48,7 @@ Note: This requires a PowerShell prompt with winget installed. You should be ab
winget install -e --id Git.Git; ` winget install -e --id Git.Git; `
winget install -e --id Microsoft.VisualStudioCode; ` winget install -e --id Microsoft.VisualStudioCode; `
winget install -e --id Google.AndroidStudio; ` winget install -e --id Google.AndroidStudio; `
winget install -e --id OpenJS.NodeJS --version 16.12.0; #v17 has issues with openssl winget install -e --id OpenJS.NodeJS --version 20.11.0;
``` ```
![](/screenshots/dev_setup_windows_winget.png) ![](/screenshots/dev_setup_windows_winget.png)
@@ -61,10 +63,10 @@ Your Windows environment should now be set up and ready to proceed!
Required Software: Required Software:
* [Android Studio](https://developer.android.com/studio) - [Android Studio](https://developer.android.com/studio)
* [Node.js](https://nodejs.org/en/) - [Node.js](https://nodejs.org/en/) (version 20)
* [Cocoapods](https://guides.cocoapods.org/using/getting-started.html#installation) - [Cocoapods](https://guides.cocoapods.org/using/getting-started.html#installation)
* [Android SDK](https://developer.android.com/studio) - [Android SDK](https://developer.android.com/studio)
<details> <details>
<summary>Install the required software with <a href=(https://brew.sh/)>homebrew</a></summary> <summary>Install the required software with <a href=(https://brew.sh/)>homebrew</a></summary>
@@ -83,6 +85,7 @@ brew install android-studio node cocoapods
Clone or fork the project from terminal or powershell and `cd` into the project directory. Clone or fork the project from terminal or powershell and `cd` into the project directory.
Install the required node packages: Install the required node packages:
```shell ```shell
npm install npm install
``` ```
@@ -91,10 +94,12 @@ npm install
<summary>Expand for screenshot</summary> <summary>Expand for screenshot</summary>
![](/screenshots/dev_setup_android_npm_install.png) ![](/screenshots/dev_setup_android_npm_install.png)
</details> </details>
<br> <br>
Generate static web app: Generate static web app:
```shell ```shell
npm run generate npm run generate
``` ```
@@ -103,10 +108,12 @@ npm run generate
<summary>Expand for screenshot</summary> <summary>Expand for screenshot</summary>
![](/screenshots/dev_setup_android_npm_run.png) ![](/screenshots/dev_setup_android_npm_run.png)
</details> </details>
<br> <br>
Copy web app into native android/ios folders: Copy web app into native android/ios folders:
```shell ```shell
npx cap sync npx cap sync
``` ```
@@ -115,6 +122,7 @@ npx cap sync
<summary>Expand for screenshot</summary> <summary>Expand for screenshot</summary>
![](/screenshots/dev_setup_android_cap_sync.png) ![](/screenshots/dev_setup_android_cap_sync.png)
</details> </details>
<br> <br>
@@ -128,6 +136,7 @@ npx cap open android
<summary>Expand for screenshot</summary> <summary>Expand for screenshot</summary>
![](/screenshots/dev_setup_cap_android.png) ![](/screenshots/dev_setup_cap_android.png)
</details> </details>
<br> <br>
@@ -137,15 +146,16 @@ Start coding!
Required Software: Required Software:
* [Xcode](https://developer.apple.com/xcode/) - [Xcode](https://developer.apple.com/xcode/)
* [Node.js](https://nodejs.org/en/) - [Node.js](https://nodejs.org/en/)
* [Cocoapods](https://guides.cocoapods.org/using/getting-started.html#installation) - [Cocoapods](https://guides.cocoapods.org/using/getting-started.html#installation)
### Start working on the iOS app ### Start working on the iOS app
Clone or fork the project in the terminal and `cd` into the project directory. Clone or fork the project in the terminal and `cd` into the project directory.
Install the required node packages: Install the required node packages:
```shell ```shell
npm install npm install
``` ```
@@ -154,10 +164,12 @@ npm install
<summary>Expand for screenshot</summary> <summary>Expand for screenshot</summary>
![](/screenshots/dev_setup_ios_npm_install.png) ![](/screenshots/dev_setup_ios_npm_install.png)
</details> </details>
<br> <br>
Generate static web app: Generate static web app:
```shell ```shell
npm run generate npm run generate
``` ```
@@ -166,10 +178,12 @@ npm run generate
<summary>Expand for screenshot</summary> <summary>Expand for screenshot</summary>
![](/screenshots/dev_setup_ios_npm_generate.png) ![](/screenshots/dev_setup_ios_npm_generate.png)
</details> </details>
<br> <br>
Copy web app into native android/ios folders: Copy web app into native android/ios folders:
```shell ```shell
npx cap sync npx cap sync
``` ```
@@ -178,6 +192,7 @@ npx cap sync
<summary>Expand for screenshot</summary> <summary>Expand for screenshot</summary>
![](/screenshots/dev_setup_ios_cap_sync.png) ![](/screenshots/dev_setup_ios_cap_sync.png)
</details> </details>
<br> <br>
@@ -191,6 +206,7 @@ npx cap open ios
<summary>Expand for screenshot</summary> <summary>Expand for screenshot</summary>
![](/screenshots/dev_setup_ios_cap_open.png) ![](/screenshots/dev_setup_ios_cap_open.png)
</details> </details>
<br> <br>
+9 -1
View File
@@ -77,6 +77,14 @@ export const getters = {
}, },
getOrientationLockSetting: state => { getOrientationLockSetting: state => {
return state.deviceData?.deviceSettings?.lockOrientation return state.deviceData?.deviceSettings?.lockOrientation
},
getCanDownloadUsingCellular: state => {
if (!state.deviceData?.deviceSettings?.downloadUsingCellular) return 'ALWAYS'
return state.deviceData.deviceSettings.downloadUsingCellular || 'ALWAYS'
},
getCanStreamingUsingCellular: state => {
if (!state.deviceData?.deviceSettings?.streamingUsingCellular) return 'ALWAYS'
return state.deviceData.deviceSettings.streamingUsingCellular || 'ALWAYS'
} }
} }
@@ -180,4 +188,4 @@ export const mutations = {
state.serverSettings = val state.serverSettings = val
this.$localStore.setServerSettings(state.serverSettings) this.$localStore.setServerSettings(state.serverSettings)
} }
} }
+17
View File
@@ -1,3 +1,5 @@
import { Browser } from '@capacitor/browser'
export const state = () => ({ export const state = () => ({
user: null, user: null,
serverConnectionConfig: null, serverConnectionConfig: null,
@@ -115,6 +117,21 @@ export const actions = {
commit('setSettings', userSettings) commit('setSettings', userSettings)
this.$eventBus.$emit('user-settings', state.settings) this.$eventBus.$emit('user-settings', state.settings)
} }
},
async openWebClient({ getters }, path = null) {
const serverAddress = getters.getServerAddress
if (!serverAddress) {
console.error('openWebClient: No server address')
return
}
try {
let url = serverAddress.replace(/\/$/, '') // Remove trailing slash
if (path?.startsWith('/')) url += path
await Browser.open({ url })
} catch (error) {
console.error('Error opening browser', error)
}
} }
} }
+179
View File
@@ -0,0 +1,179 @@
{
"ButtonAdd": "যোগ করুন",
"ButtonAuthors": "লেখকগণ",
"ButtonCancel": "বাতিল করুন",
"ButtonClearFilter": "ফিল্টার পরিষ্কার করুন",
"ButtonCloseFeed": "ফিড বন্ধ করুন",
"ButtonCollections": "সংগ্রহ",
"ButtonCreate": "তৈরি করুন",
"ButtonDelete": "মুছুন",
"ButtonHome": "নীড়",
"ButtonIssues": "ইস্যু",
"ButtonLatest": "সর্বশেষ",
"ButtonLibrary": "লাইব্রেরি",
"ButtonOpenFeed": "ফিড খুলুন",
"ButtonPause": "বিরতি",
"ButtonPlay": "বাজান",
"ButtonPlaying": "বাজছে",
"ButtonPlaylists": "প্লেলিস্ট",
"ButtonRead": "পড়ুন",
"ButtonRemove": "মুছে ফেলুন",
"ButtonSave": "সংরক্ষণ করুন",
"ButtonSearch": "অনুসন্ধান",
"ButtonSeries": "সিরিজ",
"ButtonSubmit": "জমা দিন",
"ButtonYes": "হ্যাঁ",
"HeaderAccount": "অ্যাকাউন্ট",
"HeaderAdvanced": "অ্যাডভান্সড",
"HeaderAudioTracks": "অডিও ট্র্যাক",
"HeaderChapters": "অধ্যায়",
"HeaderCollection": "সংগ্রহ",
"HeaderCollectionItems": "সংগ্রহ আইটেম",
"HeaderDetails": "বিস্তারিত",
"HeaderEbookFiles": "ই-বই ফাইল",
"HeaderEpisodes": "পর্ব",
"HeaderEreaderSettings": "ই-রিডার সেটিংস",
"HeaderLatestEpisodes": "সর্বশেষ পর্ব",
"HeaderLibraries": "লাইব্রেরি",
"HeaderOpenRSSFeed": "আরএসএস ফিড খুলুন",
"HeaderPlaylist": "প্লেলিস্ট",
"HeaderPlaylistItems": "প্লেলিস্ট আইটেম",
"HeaderRSSFeedGeneral": "আরএসএস বিবরণ",
"HeaderRSSFeedIsOpen": "আরএসএস ফিড খোলা আছে",
"HeaderSettings": "সেটিংস",
"HeaderSleepTimer": "স্লিপ টাইমার",
"HeaderStatsMinutesListeningChart": "মিনিট শ্রবণ (গত ৭ দিন)",
"HeaderStatsRecentSessions": "সাম্প্রতিক সেশন",
"HeaderTableOfContents": "বিষয়বস্তুর সারণী",
"HeaderYourStats": "আপনার পরিসংখ্যান",
"LabelAddToPlaylist": "প্লেলিস্টে যোগ করুন",
"LabelAdded": "যোগ করা হয়েছে",
"LabelAddedAt": "এতে যোগ করা হয়েছে",
"LabelAll": "সব",
"LabelAuthor": "লেখক",
"LabelAuthorFirstLast": "লেখক (প্রথম শেষ)",
"LabelAuthorLastFirst": "লেখক (শেষ, প্রথম)",
"LabelAuthors": "লেখকগণ",
"LabelAutoDownloadEpisodes": "স্বয়ংক্রিয় ডাউনলোড পর্ব",
"LabelBooks": "বইগুলো",
"LabelChapters": "অধ্যায়",
"LabelClosePlayer": "প্লেয়ার বন্ধ করুন",
"LabelCollapseSeries": "সিরিজ সঙ্কুচিত করুন",
"LabelComplete": "সম্পূর্ণ",
"LabelContinueListening": "শোনা চালিয়ে যান",
"LabelContinueReading": "পড়া চালিয়ে যান",
"LabelContinueSeries": "সিরিজ চালিয়ে যান",
"LabelDescription": "বিবরণ",
"LabelDiscover": "আবিষ্কার",
"LabelDownload": "ডাউনলোড করুন",
"LabelDuration": "সময়কাল",
"LabelEbook": "ই-বই",
"LabelEbooks": "ই-বইগুলো",
"LabelEnable": "সক্ষম করুন",
"LabelEnd": "সমাপ্ত",
"LabelEpisode": "পর্ব",
"LabelFeedURL": "ফিড ইউআরএল",
"LabelFile": "ফাইল",
"LabelFileBirthtime": "ফাইল জন্মের সময়",
"LabelFileModified": "ফাইল পরিবর্তিত",
"LabelFilename": "ফাইলের নাম",
"LabelFinished": "সমাপ্ত",
"LabelFolder": "ফোল্ডার",
"LabelFontScale": "ফন্ট স্কেল",
"LabelGenre": "ঘরানা",
"LabelGenres": "ঘরানাগুলো",
"LabelHasEbook": "ই-বই আছে",
"LabelHasSupplementaryEbook": "পরিপূরক ই-বই আছে",
"LabelHost": "নিমন্ত্রণকর্তা",
"LabelInProgress": "প্রগতিতে আছে",
"LabelIncomplete": "অসম্পূর্ণ",
"LabelLanguage": "ভাষা",
"LabelLayout": "লেআউট",
"LabelLayoutSinglePage": "একক পৃষ্ঠা",
"LabelLight": "আলো",
"LabelLineSpacing": "লাইন স্পেসিং",
"LabelListenAgain": "আবার শুনুন",
"LabelMediaType": "মিডিয়ার ধরন",
"LabelMore": "আরো",
"LabelMoreInfo": "আরো তথ্য",
"LabelName": "নাম",
"LabelNarrator": "কথক",
"LabelNarrators": "কথক",
"LabelNewestAuthors": "নতুন লেখক",
"LabelNewestEpisodes": "নতুনতম পর্ব",
"LabelNotFinished": "সমাপ্ত হয়নি",
"LabelNotStarted": "শুরু হয়নি",
"LabelPassword": "পাসওয়ার্ড",
"LabelPath": "পথ",
"LabelPodcast": "পডকাস্ট",
"LabelPodcasts": "পডকাস্টগুলো",
"LabelPreventIndexing": "আইটিউনস এবং গুগল পডকাস্ট ডিরেক্টরি দ্বারা আপনার ফিডকে ইন্ডেক্স করা থেকে বিরত রাখুন",
"LabelProgress": "প্রগতি",
"LabelPubDate": "প্রকাশের তারিখ",
"LabelPublishYear": "প্রকাশের বছর",
"LabelRSSFeedCustomOwnerEmail": "কাস্টম মালিকের ইমেইল",
"LabelRSSFeedCustomOwnerName": "কাস্টম মালিকের নাম",
"LabelRSSFeedPreventIndexing": "সূচীকরণ প্রতিরোধ করুন",
"LabelRSSFeedSlug": "আরএসএস ফিড স্লাগ",
"LabelRead": "পড়ুন",
"LabelReadAgain": "আবার পড়ুন",
"LabelRecentSeries": "সাম্প্রতিক সিরিজ",
"LabelRecentlyAdded": "সম্প্রতি যোগ করা হয়েছে",
"LabelSeason": "সেশন",
"LabelSeries": "সিরিজ",
"LabelSetEbookAsPrimary": "প্রাথমিক হিসাবে সেট করুন",
"LabelSetEbookAsSupplementary": "পরিপূরক হিসেবে সেট করুন",
"LabelShowAll": "সব দেখান",
"LabelSize": "আকার",
"LabelSleepTimer": "স্লিপ টাইমার",
"LabelStart": "শুরু",
"LabelStatsBestDay": "সেরা দিন",
"LabelStatsDailyAverage": "দৈনিক গড়",
"LabelStatsDays": "দিন",
"LabelStatsDaysListened": "যেদিন শোনা হয়েছে",
"LabelStatsInARow": "এক সারিতে",
"LabelStatsItemsFinished": "আইটেম সমাপ্ত",
"LabelStatsMinutes": "মিনিট",
"LabelStatsMinutesListening": "মিনিট শুনছেন",
"LabelStatsWeekListening": "সপ্তাহ শোনা",
"LabelTag": "ট্যাগ",
"LabelTags": "ট্যাগগুলো",
"LabelTheme": "থিম",
"LabelThemeDark": "অন্ধকার",
"LabelThemeLight": "আলো",
"LabelTimeRemaining": "{0}টি অবশিষ্ট",
"LabelTitle": "শিরোনাম",
"LabelTracks": "ট্র্যাকস",
"LabelType": "টাইপ",
"LabelUser": "ব্যবহারকারী",
"LabelUsername": "ব্যবহারকারীর নাম",
"LabelYourBookmarks": "আপনার বুকমার্কস",
"LabelYourProgress": "আপনার অগ্রগতি",
"MessageDownloadingEpisode": "ডাউনলোডিং পর্ব",
"MessageEpisodesQueuedForDownload": "{0} পর্ব(গুলি) ডাউনলোডের জন্য সারিবদ্ধ",
"MessageFeedURLWillBe": "ফিড URL হবে {0}",
"MessageFetching": "আনয় হচ্ছে..।",
"MessageLoading": "লোড হচ্ছে..।",
"MessageMarkAsFinished": "সমাপ্ত হিসাবে চিহ্নিত করুন",
"MessageNoBookmarks": "কোন বুকমার্ক নেই",
"MessageNoChapters": "কোনও অধ্যায় নেই",
"MessageNoItems": "কোন আইটেম নেই",
"MessageNoItemsFound": "কোন আইটেম পাওয়া যায়নি",
"MessageNoListeningSessions": "কোনও শোনার সেশন নেই",
"MessageNoPodcastsFound": "কোন পডকাস্ট পাওয়া যায়নি",
"MessageNoUpdatesWereNecessary": "কোন আপডেটের প্রয়োজন ছিল না",
"MessageNoUserPlaylists": "আপনার কোনো প্লেলিস্ট নেই",
"MessageReportBugsAndContribute": "বাগ রিপোর্ট করুন, বৈশিষ্ট্যের অনুরোধ করুন এবং এতে অবদান রাখুন",
"NoteRSSFeedPodcastAppsHttps": "সতর্কতা: বেশিরভাগ পডকাস্ট অ্যাপের জন্য প্রয়োজন হবে RSS ফিড URL যেটি HTTPS ব্যবহার করছে",
"NoteRSSFeedPodcastAppsPubDate": "সতর্কতা: আপনার 1 বা তার বেশি পর্বের একটি পাব তারিখ নেই। কিছু পডকাস্ট অ্যাপের এটি প্রয়োজন।",
"ToastBookmarkCreateFailed": "বুকমার্ক তৈরি করতে ব্যর্থ",
"ToastBookmarkRemoveFailed": "বুকমার্ক সরাতে ব্যর্থ",
"ToastBookmarkUpdateFailed": "বুকমার্ক আপডেট করতে ব্যর্থ",
"ToastItemMarkedAsFinishedFailed": "সমাপ্ত হিসাবে চিহ্নিত করতে ব্যর্থ",
"ToastItemMarkedAsNotFinishedFailed": "সমাপ্ত হয়নি হিসাবে চিহ্নিত করতে ব্যর্থ",
"ToastPlaylistCreateFailed": "প্লেলিস্ট তৈরি করতে ব্যর্থ",
"ToastPodcastCreateFailed": "পডকাস্ট তৈরি করতে ব্যর্থ",
"ToastPodcastCreateSuccess": "পডকাস্ট সফলভাবে তৈরি করা হয়েছে",
"ToastRSSFeedCloseFailed": "RSS ফিড বন্ধ করতে ব্যর্থ",
"ToastRSSFeedCloseSuccess": "RSS ফিড বন্ধ"
}
+175 -157
View File
@@ -1,52 +1,54 @@
{ {
"ButtonAdd": "Přidat", "ButtonAdd": "Přidat",
"ButtonAddNewServer": "Add New Server", "ButtonAddNewServer": "Přidat nový server",
"ButtonAuthors": "Autoři", "ButtonAuthors": "Autoři",
"ButtonBack": "Back", "ButtonBack": "Zpět",
"ButtonCancel": "Zrušit", "ButtonCancel": "Zrušit",
"ButtonCancelTimer": "Cancel Timer", "ButtonCancelTimer": "Zrušit časovač",
"ButtonClearFilter": "Vymazat filtr", "ButtonClearFilter": "Vymazat filtr",
"ButtonCloseFeed": "Zavřít kanál", "ButtonCloseFeed": "Zavřít kanál",
"ButtonCollections": "Kolekce", "ButtonCollections": "Kolekce",
"ButtonConnect": "Connect", "ButtonConnect": "Připojit",
"ButtonConnectToServer": "Connect to Server", "ButtonConnectToServer": "Připojit k serveru",
"ButtonCreate": "Vytvořit", "ButtonCreate": "Vytvořit",
"ButtonCreateBookmark": "Create Bookmark", "ButtonCreateBookmark": "Vytvořit záložku",
"ButtonCreateNewPlaylist": "Create New Playlist", "ButtonCreateNewPlaylist": "Vytvořit nový seznam skladeb",
"ButtonDelete": "Smazat", "ButtonDelete": "Smazat",
"ButtonDeleteLocalEpisode": "Delete local episode", "ButtonDeleteLocalEpisode": "Smazat místní epizodu",
"ButtonDeleteLocalFile": "Delete local file", "ButtonDeleteLocalFile": "Smazat místní soubor",
"ButtonDeleteLocalItem": "Delete local item", "ButtonDeleteLocalItem": "Smazat místní položku",
"ButtonDisableAutoTimer": "Disable Auto Timer", "ButtonDisableAutoTimer": "Zakázat automatický časovač",
"ButtonDisconnect": "Disconnect", "ButtonDisconnect": "Odpojit",
"ButtonHistory": "History", "ButtonGoToWebClient": "Go to Web Client",
"ButtonHistory": "Historie",
"ButtonHome": "Domů", "ButtonHome": "Domů",
"ButtonIssues": "Problémy", "ButtonIssues": "Problémy",
"ButtonLatest": "Nejnovější", "ButtonLatest": "Nejnovější",
"ButtonLibrary": "Knihovna", "ButtonLibrary": "Knihovna",
"ButtonLocalMedia": "Local Media", "ButtonLocalMedia": "Místní média",
"ButtonManageLocalFiles": "Manage Local Files", "ButtonManageLocalFiles": "Spravovat místní soubory",
"ButtonNewFolder": "New Folder", "ButtonNewFolder": "Nová složka",
"ButtonNextEpisode": "Next Episode", "ButtonNextEpisode": "Další epizoda",
"ButtonOpenFeed": "Otevřít kanál", "ButtonOpenFeed": "Otevřít kanál",
"ButtonOverride": "Override", "ButtonOverride": "Přepsat",
"ButtonPause": "Pause", "ButtonPause": "Pozastavit",
"ButtonPlay": "Přehrát", "ButtonPlay": "Přehrát",
"ButtonPlayEpisode": "Přehrát epizodu",
"ButtonPlaying": "Hraje", "ButtonPlaying": "Hraje",
"ButtonPlaylists": "Seznamy skladeb", "ButtonPlaylists": "Seznamy skladeb",
"ButtonRead": "Číst", "ButtonRead": "Číst",
"ButtonRemove": "Odstranit", "ButtonRemove": "Odstranit",
"ButtonRemoveFromServer": "Remove from Server", "ButtonRemoveFromServer": "Odstranit ze serveru",
"ButtonSave": "Uložit", "ButtonSave": "Uložit",
"ButtonSaveOrder": "Save Order", "ButtonSaveOrder": "Uložit pořadí",
"ButtonSearch": "Hledat", "ButtonSearch": "Hledat",
"ButtonSendEbookToDevice": "Send Ebook to Device", "ButtonSendEbookToDevice": "Odeslat e-knihu na zařízení",
"ButtonSeries": "Série", "ButtonSeries": "Série",
"ButtonSetTimer": "Set Timer", "ButtonSetTimer": "Nastavit časovač",
"ButtonStream": "Stream", "ButtonStream": "Streamovat",
"ButtonSubmit": "Odeslat", "ButtonSubmit": "Odeslat",
"ButtonSwitchServerUser": "Switch Server/User", "ButtonSwitchServerUser": "Přepnout server/uživatele",
"ButtonUserStats": "User Stats", "ButtonUserStats": "Statistiky uživatele",
"ButtonYes": "Ano", "ButtonYes": "Ano",
"HeaderAccount": "Účet", "HeaderAccount": "Účet",
"HeaderAdvanced": "Pokročilé", "HeaderAdvanced": "Pokročilé",
@@ -54,238 +56,254 @@
"HeaderChapters": "Kapitoly", "HeaderChapters": "Kapitoly",
"HeaderCollection": "Kolekce", "HeaderCollection": "Kolekce",
"HeaderCollectionItems": "Položky kolekce", "HeaderCollectionItems": "Položky kolekce",
"HeaderConnectionStatus": "Connection Status", "HeaderConnectionStatus": "Stav připojení",
"HeaderDataSettings": "Nastavení dat",
"HeaderDetails": "Podrobnosti", "HeaderDetails": "Podrobnosti",
"HeaderDownloads": "Downloads", "HeaderDownloads": "Stahování",
"HeaderEbookFiles": "Soubory elektronických knih", "HeaderEbookFiles": "Soubory e-knih",
"HeaderEpisodes": "Epizody", "HeaderEpisodes": "Epizody",
"HeaderEreaderSettings": "Nastavení čtečky elektronických knih", "HeaderEreaderSettings": "Nastavení čtečky e-knih",
"HeaderLatestEpisodes": "Poslední epizody", "HeaderLatestEpisodes": "Nejnovější epizody",
"HeaderLibraries": "Knihovny", "HeaderLibraries": "Knihovny",
"HeaderLocalFolders": "Local Folders", "HeaderLocalFolders": "Místní složky",
"HeaderLocalLibraryItems": "Local Library Items", "HeaderLocalLibraryItems": "Místní položky knihovny",
"HeaderNewPlaylist": "New Playlist", "HeaderNewPlaylist": "Nový seznam skladeb",
"HeaderOpenRSSFeed": "Otevřít RSS kanál", "HeaderOpenRSSFeed": "Otevřít RSS kanál",
"HeaderPlaybackSettings": "Playback Settings", "HeaderPlaybackSettings": "Nastavení přehrávání",
"HeaderPlaylist": "Seznam skladeb", "HeaderPlaylist": "Seznam skladeb",
"HeaderPlaylistItems": "Položky seznamu přehrávání", "HeaderPlaylistItems": "Položky seznamu přehrávání",
"HeaderRSSFeed": "RSS Feed", "HeaderRSSFeed": "RSS kanál",
"HeaderRSSFeedGeneral": "Podrobnosti o RSS", "HeaderRSSFeedGeneral": "Detaily RSS",
"HeaderRSSFeedIsOpen": "Informační kanál RSS je otevřený", "HeaderRSSFeedIsOpen": "Kanál RSS je otevřen",
"HeaderSelectDownloadLocation": "Select Download Location", "HeaderSelectDownloadLocation": "Vyberte umístění pro stažení",
"HeaderSettings": "Nastavení", "HeaderSettings": "Nastavení",
"HeaderSleepTimer": "Časovač vypnutí", "HeaderSleepTimer": "Časovač spánku",
"HeaderSleepTimerSettings": "Sleep Timer Settings", "HeaderSleepTimerSettings": "Nastavení časovače spánku",
"HeaderStatsMinutesListeningChart": "Počet minut poslechu (posledních 7 dní)", "HeaderStatsMinutesListeningChart": "Poslech za posledních 7 dní (min)",
"HeaderStatsRecentSessions": "Poslední relace", "HeaderStatsRecentSessions": "Poslední sezení",
"HeaderTableOfContents": "Obsah", "HeaderTableOfContents": "Obsah",
"HeaderUserInterfaceSettings": "User Interface Settings", "HeaderUserInterfaceSettings": "Nastavení uživatelského rozhraní",
"HeaderYourStats": "Vaše statistiky", "HeaderYourStats": "Vaše statistiky",
"LabelAddToPlaylist": "Přidat do seznamu skladeb",
"LabelAdded": "Přidáno", "LabelAdded": "Přidáno",
"LabelAddedAt": "Přidáno v", "LabelAddedAt": "Přidáno v",
"LabelAddToPlaylist": "Přidat do seznamu přehrávání",
"LabelAll": "Vše", "LabelAll": "Vše",
"LabelAllowSeekingOnMediaControls": "Povolit vyhledávání polohy v ovládacích prvcích oznámení médií",
"LabelAlways": "Vždy",
"LabelAskConfirmation": "Požádat o potvrzení",
"LabelAuthor": "Autor", "LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (jméno a příjmení)", "LabelAuthorFirstLast": "Autor (jméno a příjmení)",
"LabelAuthorLastFirst": "Autor (příjmení a jméno)", "LabelAuthorLastFirst": "Autor (příjmení a jméno)",
"LabelAuthors": "Autoři", "LabelAuthors": "Autoři",
"LabelAutoDownloadEpisodes": "Automaticky stahovat epizody", "LabelAutoDownloadEpisodes": "Automaticky stahovat epizody",
"LabelAutoRewindTime": "Auto rewind time", "LabelAutoRewindTime": "Přetočit zpět o",
"LabelAutoSleepTimer": "Auto sleep timer", "LabelAutoSleepTimer": "Automatický časovač spánku",
"LabelAutoSleepTimerAutoRewind": "Auto sleep timer auto rewind", "LabelAutoSleepTimerAutoRewind": "Přetočit zpět po uspání",
"LabelAutoSleepTimerAutoRewindHelp": "When the auto sleep timer finishes, playing the item again will automatically rewind your position.", "LabelAutoSleepTimerAutoRewindHelp": "Když dojde k uspání automatickým časovačem spánku, pozice přehrávání je posunuta zpět o vybraný čas.",
"LabelAutoSleepTimerHelp": "When playing media between the specified start and end times a sleep timer will automatically start.", "LabelAutoSleepTimerHelp": "hem přehrávání média v časovém rozmezí \"Od\" a \"Do\" se automaticky spustí časovač spánku.",
"LabelBooks": "Knihy", "LabelBooks": "Knihy",
"LabelChapterTrack": "Stopa kapitoly",
"LabelChapters": "Kapitoly", "LabelChapters": "Kapitoly",
"LabelChapterTrack": "Chapter Track",
"LabelClosePlayer": "Zavřít přehrávač", "LabelClosePlayer": "Zavřít přehrávač",
"LabelCollapseSeries": "Sbalit sérii", "LabelCollapseSeries": "Sbalit sérii",
"LabelComplete": "Dokončeno", "LabelComplete": "Dokončeno",
"LabelContinueBooks": "Continue Books", "LabelContinueBooks": "Pokračovat v knihách",
"LabelContinueEpisodes": "Continue Episodes", "LabelContinueEpisodes": "Pokračovat v epizodách",
"LabelContinueListening": "Continue Listening", "LabelContinueListening": "Pokračovat v poslechu",
"LabelContinueReading": "Continue Reading", "LabelContinueReading": "Pokračovat ve čtení",
"LabelContinueSeries": "Continue Series", "LabelContinueSeries": "Pokračovat v sériích",
"LabelCustomTime": "Custom time", "LabelCustomTime": "Vlastní čas",
"LabelDescription": "Popis", "LabelDescription": "Popis",
"LabelDisableAudioFadeOut": "Disable audio fade out", "LabelDisableAudioFadeOut": "Vypnout postupné snižování zvuku",
"LabelDisableAudioFadeOutHelp": "Audio volume will start decreasing when there is less than 1 minute remaining on the sleep timer. Enable this setting to not fade out.", "LabelDisableAudioFadeOutHelp": "Hlasitost zvuku začne klesat, pokud zbývá méně než 1 minuta do vypršení časovače spánku. Tuto možnost povolte, pokud nechcete postupné snižování hlasitosti.",
"LabelDisableAutoRewind": "Disable auto rewind", "LabelDisableAutoRewind": "Zakázat automatické přetočení",
"LabelDisableShakeToReset": "Disable shake to reset", "LabelDisableShakeToReset": "Zakázat resetování otřesem",
"LabelDisableShakeToResetHelp": "Shaking your device while the timer is running OR within 2 minutes of the timer expiring will reset the sleep timer. Enable this setting to disable shake to reset.", "LabelDisableShakeToResetHelp": "Když je časovač spánku spuštěn nebo zbýva 2 minuty do jeho vypršení, zatřesení zařízením se resetuje časovač spánku. Povolte toto nastavení, pokud chcete resetování otřesem zakázat.",
"LabelDisableVibrateOnReset": "Disable vibrate on reset", "LabelDisableVibrateOnReset": "Zakázat vibraci při resetování",
"LabelDisableVibrateOnResetHelp": "When the sleep timer gets reset your device will vibrate. Enable this setting to not vibrate when the sleep timer resets.", "LabelDisableVibrateOnResetHelp": "Když je časovač spánku resetován, zařízení zavibruje. Tuto možnost povolte, pokud nechcete, aby zařízení vibrace provádělo při resetování časovače spánku.",
"LabelDiscover": "Discover", "LabelDiscover": "Objevit",
"LabelDownload": "Stáhnout", "LabelDownload": "Stáhnout",
"LabelDownloaded": "Downloaded", "LabelDownloadUsingCellular": "Stáhnout přes mobilní data",
"LabelDuration": "Doba trvání", "LabelDownloaded": "Staženo",
"LabelEbook": "Elektronická kniha", "LabelDuration": "Délka trvání",
"LabelEbooks": "Ebooks", "LabelEbook": "E-kniha",
"LabelEbooks": "E-knihy",
"LabelEnable": "Povolit", "LabelEnable": "Povolit",
"LabelEnableMp3IndexSeeking": "Enable mp3 index seeking", "LabelEnableMp3IndexSeeking": "Povolit indexové vyhledávání mp3",
"LabelEnableMp3IndexSeekingHelp": "This setting should only be enabled if you have mp3 files that are not seeking correctly. Inaccurate seeking is most likely due to Variable birate (VBR) MP3 files. This setting will force index seeking, in which a time-to-byte mapping is built as the file is read. In some cases with large MP3 files there will be a delay when seeking towards the end of the file.", "LabelEnableMp3IndexSeekingHelp": "Toto nastavení by mělo být povoleno pouze v případě, že soubory MP3 nejsou správně vyhledávány. Nepřesné vyhledávání je s největší pravděpodobností způsobeno soubory MP3 s proměnlivým datovým tokem (VBR). Toto nastavení vynutí indexové vyhledávání, při kterém se při čtení souboru vytváří mapování času na bajty. V některých případech u velkých souborů MP3 dochází ke zpoždění při vyhledávání ke konci souboru.",
"LabelEnd": "Konec", "LabelEnd": "Konec",
"LabelEndOfChapter": "End of Chapter", "LabelEndOfChapter": "Konec kapitoly",
"LabelEndTime": "End time", "LabelEndTime": "Do",
"LabelEpisode": "Epizoda", "LabelEpisode": "Epizoda",
"LabelFeedURL": "URL zdroje", "LabelFeedURL": "URL kanálu",
"LabelFile": "Soubor", "LabelFile": "Soubor",
"LabelFileBirthtime": "Čas vzniku souboru", "LabelFileBirthtime": "Čas vytvoření souboru",
"LabelFileModified": "Soubor změněn", "LabelFileModified": "Soubor upraven",
"LabelFilename": "Název souboru", "LabelFilename": "Název souboru",
"LabelFinished": "Dokončeno", "LabelFinished": "Dokončeno",
"LabelFolder": "Složka", "LabelFolder": "Složka",
"LabelFontBoldness": "Výraznost písma",
"LabelFontScale": "Měřítko písma", "LabelFontScale": "Měřítko písma",
"LabelGenre": "Žánr", "LabelGenre": "Žánr",
"LabelGenres": "Žánry", "LabelGenres": "Žánry",
"LabelHapticFeedback": "Haptic feedback", "LabelHapticFeedback": "Haptická odezva",
"LabelHasEbook": "Has ebook", "LabelHasEbook": "Má e-knihu",
"LabelHasSupplementaryEbook": "Has supplementary ebook", "LabelHasSupplementaryEbook": "Má doplňkovou e-knihu",
"LabelHeavy": "Heavy", "LabelHeavy": "Těžké",
"LabelHigh": "High", "LabelHigh": "Vysoké",
"LabelHost": "Hostitel", "LabelHost": "Hostitel",
"LabelIncomplete": "Neúplné",
"LabelInProgress": "Probíhá", "LabelInProgress": "Probíhá",
"LabelInternalAppStorage": "Internal App Storage", "LabelIncomplete": "Neúplné",
"LabelJumpBackwardsTime": "Jump backwards time", "LabelInternalAppStorage": "Interní úložiště aplikace",
"LabelJumpForwardsTime": "Jump forwards time", "LabelJumpBackwardsTime": "Délka skoku zpět v čase",
"LabelJumpForwardsTime": "Délka skoku vpřed v čase",
"LabelLanguage": "Jazyk", "LabelLanguage": "Jazyk",
"LabelLayout": "Layout", "LabelLayout": "Rozvržení",
"LabelLayoutAuto": "Auto", "LabelLayoutAuto": "Automatické",
"LabelLayoutSinglePage": "Single page", "LabelLayoutSinglePage": "Jedna stránka",
"LabelLight": "Light", "LabelLight": "Světlé",
"LabelLineSpacing": "Řádkování", "LabelLineSpacing": "Řádkování",
"LabelListenAgain": "Listen Again", "LabelListenAgain": "Poslouchat znovu",
"LabelLocalBooks": "Local Books", "LabelLocalBooks": "Místní knihy",
"LabelLocalPodcasts": "Local Podcasts", "LabelLocalPodcasts": "Místní podcasty",
"LabelLockOrientation": "Lock orientation", "LabelLockOrientation": "Zamknout orientaci",
"LabelLockPlayer": "Lock Player", "LabelLockPlayer": "Zamknout přehrávač",
"LabelLow": "Low", "LabelLow": "Nízké",
"LabelMediaType": "Typ média", "LabelMediaType": "Typ média",
"LabelMedium": "Medium", "LabelMedium": "Střední",
"LabelMore": "Více", "LabelMore": "Více",
"LabelMoreInfo": "Více informací", "LabelMoreInfo": "Více informací",
"LabelName": "Jméno", "LabelName": "Jméno",
"LabelNarrator": "Interpret", "LabelNarrator": "Vypravěč",
"LabelNarrators": "Interpreti", "LabelNarrators": "Vypravěči",
"LabelNewestAuthors": "Newest Authors", "LabelNever": "Nikdy",
"LabelNewestEpisodes": "Newest Episodes", "LabelNewestAuthors": "Nejnovější autoři",
"LabelNo": "No", "LabelNewestEpisodes": "Nejnovější epizody",
"LabelNo": "Ne",
"LabelNotFinished": "Nedokončeno", "LabelNotFinished": "Nedokončeno",
"LabelNotStarted": "Nezahájeno", "LabelNotStarted": "Nezahájeno",
"LabelOff": "Off", "LabelOff": "Vypnout",
"LabelPassword": "Heslo", "LabelPassword": "Heslo",
"LabelPath": "Cesta", "LabelPath": "Cesta",
"LabelPlaybackDirect": "Direct", "LabelPlaybackDirect": "Přímé",
"LabelPlaybackLocal": "Local", "LabelPlaybackLocal": "Místní",
"LabelPlaybackSpeed": "Playback Speed", "LabelPlaybackSpeed": "Rychlost přehrávání",
"LabelPlaybackTranscode": "Transcode", "LabelPlaybackTranscode": "Překódovává se",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasty", "LabelPodcasts": "Podcasty",
"LabelPreventIndexing": "Zabránit indexování vašeho kanálu v adresářích podcastů iTunes a Google", "LabelPreventIndexing": "Zabránit indexování vašeho kanálu v adresářích podcastů iTunes a Google",
"LabelProgress": "Průběh", "LabelProgress": "Průběh",
"LabelPubDate": "Datum vydání", "LabelPubDate": "Datum vydání",
"LabelPublishYear": "Rok vydání", "LabelPublishYear": "Rok vydání",
"LabelRead": "Číst",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"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í",
"LabelRSSFeedSlug": "RSS kanál Slug", "LabelRSSFeedSlug": "Klíčové slovo kanálu RSS",
"LabelRead": "Číst",
"LabelReadAgain": "Číst znovu",
"LabelRecentSeries": "Nedávné série",
"LabelRecentlyAdded": "Nedávno přidáno",
"LabelRemoveFromPlaylist": "Odebrat z Playlistu",
"LabelScaleElapsedTimeBySpeed": "Škálovat uplynulý čas podle rychlosti",
"LabelSeason": "Sezóna", "LabelSeason": "Sezóna",
"LabelSelectADevice": "Select a device", "LabelSelectADevice": "Vyberte zařízení",
"LabelSeries": "Série", "LabelSeries": "Série",
"LabelServerAddress": "Server address", "LabelServerAddress": "Adresa serveru",
"LabelSetEbookAsPrimary": "Nastavit jako primární", "LabelSetEbookAsPrimary": "Nastavit jako primární",
"LabelSetEbookAsSupplementary": "Nastavit jako doplňkové", "LabelSetEbookAsSupplementary": "Nastavit jako doplňkový",
"LabelShakeSensitivity": "Shake sensitivity", "LabelShakeSensitivity": "Citlivost na otřes",
"LabelShowAll": "Zobrazit vše", "LabelShowAll": "Zobrazit vše",
"LabelSize": "Velikost", "LabelSize": "Velikost",
"LabelSleepTimer": "Časovač vypnutí", "LabelSleepTimer": "Spánek po",
"LabelStart": "Spustit", "LabelStart": "Spustit",
"LabelStartTime": "Čas Spuštění", "LabelStartTime": "Od",
"LabelStatsBestDay": "Nejlepší den", "LabelStatsBestDay": "Nejlepší den",
"LabelStatsDailyAverage": "Denní průměr", "LabelStatsDailyAverage": "Denní průměr",
"LabelStatsDays": "Dny", "LabelStatsDays": "Dny",
"LabelStatsDaysListened": "Dny poslechu", "LabelStatsDaysListened": "Dny poslechu",
"LabelStatsInARow": "v řadě", "LabelStatsInARow": "v řadě",
"LabelStatsItemsFinished": "Dokončené Položky", "LabelStatsItemsFinished": "Dokončené položky",
"LabelStatsMinutes": "minut", "LabelStatsMinutes": "minut",
"LabelStatsMinutesListening": "Minuty poslechu", "LabelStatsMinutesListening": "Minuty poslechu",
"LabelStatsWeekListening": "Týdenní poslech", "LabelStatsWeekListening": "Za týden",
"LabelTag": "Značka", "LabelStreamingUsingCellular": "Přehrávání přes mobilní data",
"LabelTags": "Značky", "LabelTag": "Štítek",
"LabelTags": "Štítky",
"LabelTheme": "Téma", "LabelTheme": "Téma",
"LabelThemeDark": "Tmavé", "LabelThemeDark": "Tmavé",
"LabelThemeLight": "Světlé", "LabelThemeLight": "Světlé",
"LabelTimeRemaining": "{0} zbývá", "LabelTimeRemaining": "{0} zbývá",
"LabelTitle": "Název", "LabelTitle": "Název",
"LabelTotalTrack": "Total Track", "LabelTotalSize": "Celková velikost",
"LabelTotalTrack": "Celková stopa",
"LabelTracks": "Stopy", "LabelTracks": "Stopy",
"LabelType": "Typ", "LabelType": "Typ",
"LabelUnlockPlayer": "Unlock Player", "LabelUnlockPlayer": "Odemknout přehrávač",
"LabelUseBookshelfView": "Use bookshelf view", "LabelUseBookshelfView": "Použít vzhled knihovny",
"LabelUser": "Uživatel", "LabelUser": "Uživatel",
"LabelUsername": "Uživatelské jméno", "LabelUsername": "Uživatelské jméno",
"LabelVeryHigh": "Very High", "LabelVeryHigh": "Velmi vysoké",
"LabelVeryLow": "Very Low", "LabelVeryLow": "Velmi nízké",
"LabelYourBookmarks": "Vaše záložky", "LabelYourBookmarks": "Vaše záložky",
"LabelYourProgress": "Váš pokrok", "LabelYourProgress": "Váš pokrok",
"MessageAndroid10Downloads": "Android 10 and below will use internal app storage for downloads.", "MessageAndroid10Downloads": "Android 10 a starší budou používat interní úložiště aplikace pro stahování.",
"MessageAttemptingServerConnection": "Attempting server connection...", "MessageAttemptingServerConnection": "Pokouším se o připojit k serveru...",
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf server not connected", "MessageAudiobookshelfServerNotConnected": "Server Audiobookshelf není připojen",
"MessageAudiobookshelfServerRequired": "<strong>Important!</strong> This app is designed to work with an Audiobookshelf server that you or someone you know is hosting. This app does not provide any content.", "MessageAudiobookshelfServerRequired": "<strong>Důležité!</strong> Tato aplikace je navržena, aby pracovala se serverem Audiobookshelf, který hostuje buď vy, nebo někdo, koho znáte. Tato aplikace neposkytuje žádný obsah.",
"MessageBookshelfEmpty": "Bookshelf empty", "MessageBookshelfEmpty": "Knihovna je prázdná",
"MessageConfirmDeleteLocalEpisode": "Remove local episode \"{0}\" from your device? The file on the server will be unaffected.", "MessageConfirmDeleteLocalEpisode": "Odebrat místní epizodu „{0}“ ze zařízení? Soubor na serveru zůstane nezměněný.",
"MessageConfirmDeleteLocalFiles": "Remove local files of this item from your device? The files on the server and your progress will be unaffected.", "MessageConfirmDeleteLocalFiles": "Odebrat místní soubory této položky ze zařízení? Soubory na serveru a váš pokrok nebudou ovlivněny.",
"MessageConfirmDiscardProgress": "Are you sure you want to reset your progress?", "MessageConfirmDiscardProgress": "Opravdu chcete zahodit svůj pokrok?",
"MessageConfirmMarkAsFinished": "Are you sure you want to mark this item as finished?", "MessageConfirmDownloadUsingCellular": "Chystáte se stahovat přes mobilní data. Toto může zahrnovat poplatky za mobilní data. Chcete pokračovat?",
"MessageConfirmRemoveBookmark": "Are you sure you want to remove bookmark?", "MessageConfirmMarkAsFinished": "Opravdu chcete tuto položku označit jako dokončenou?",
"MessageDiscardProgress": "Discard Progress", "MessageConfirmRemoveBookmark": "Opravdu chcete odebrat záložku?",
"MessageDownloadCompleteProcessing": "Download complete. Processing...", "MessageConfirmStreamingUsingCellular": "Chystáte se přehrávat přes mobilní data. Toto může zahrnovat poplatky za mobilní data. Chcete pokračovat?",
"MessageDownloading": "Downloading...", "MessageDiscardProgress": "Zahodit pokrok",
"MessageDownloadCompleteProcessing": "Stahování dokončeno. Zpracovává se...",
"MessageDownloading": "Stahuje se...",
"MessageDownloadingEpisode": "Stahuji epizodu", "MessageDownloadingEpisode": "Stahuji epizodu",
"MessageEpisodesQueuedForDownload": "{0} epizody zařazené do fronty ke stažení", "MessageEpisodesQueuedForDownload": "{0} epizody zařazené do fronty ke stažení",
"MessageFeedURLWillBe": "URL zdroje bude {0}", "MessageFeedURLWillBe": "URL zdroje bude {0}",
"MessageFetching": "Stahování...", "MessageFetching": "Stahování...",
"MessageFollowTheProjectOnGithub": "Follow the project on Github", "MessageFollowTheProjectOnGithub": "Sledujte projekt na GitHubu",
"MessageItemDownloadCompleteFailedToCreate": "Item download complete but failed to create library item", "MessageItemDownloadCompleteFailedToCreate": "Stahování položky bylo dokončeno, ale nepodařilo se vytvořit položku v knihovně",
"MessageLoading": "Načítá se...", "MessageLoading": "Načítá se...",
"MessageLoadingServerData": "Loading server data...", "MessageLoadingServerData": "Načítání dat ze serveru...",
"MessageMarkAsFinished": "Označit jako dokončené", "MessageMarkAsFinished": "Označit jako dokončené",
"MessageMediaLinkedToADifferentServer": "Media is linked to an Audiobookshelf server on a different address ({0}). Progress will be synced when connected to this server address.", "MessageMediaLinkedToADifferentServer": "Média jsou propojena se serverem Audiobookshelf na jiné adrese ({0}). Pokrok bude synchronizován, když budete připojeni k této adrese serveru.",
"MessageMediaLinkedToADifferentUser": "Media is linked to this server but was downloaded by a different user. Progress will only be synced to the user that downloaded it.", "MessageMediaLinkedToADifferentUser": "Média jsou propojena se serverem, ale byla stažena jiným uživatelem. Pokrok bude synchronizován pouze s uživatelem, který je stáhl.",
"MessageMediaLinkedToServer": "Linked to server {0}", "MessageMediaLinkedToServer": "Propojeno se serverem {0}",
"MessageMediaLinkedToThisServer": "Downloaded media is linked to this server", "MessageMediaLinkedToThisServer": "Stažená média jsou propojena s tímto serverem",
"MessageMediaNotLinkedToServer": "Media is not linked to an Audiobookshelf server. No progress will be synced.", "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",
"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",
"MessageNoMediaFolders": "No Media Folders", "MessageNoMediaFolders": "Žádné složky médií",
"MessageNoNetworkConnection": "No network connection", "MessageNoNetworkConnection": "Žádné připojení k síti",
"MessageNoPodcastsFound": "Nebyly nalezeny žádné podcasty", "MessageNoPodcastsFound": "Nebyly nalezeny žádné podcasty",
"MessageNoSeries": "No series",
"MessageNoUpdatesWereNecessary": "Nebyly nutné žádné aktualizace", "MessageNoUpdatesWereNecessary": "Nebyly nutné žádné aktualizace",
"MessageNoUserPlaylists": "Nemáte žádné seznamy skladeb", "MessageNoUserPlaylists": "Nemáte žádné seznamy skladeb",
"MessageReportBugsAndContribute": "Hlásit chyby, žádat o funkce a přispívat", "MessageReportBugsAndContribute": "Nahlašte chyby, vyžádejte si funkce a přispěte na",
"MessageSocketConnectedOverMeteredCellular": "Socket connected over metered cellular", "MessageSocketConnectedOverMeteredCellular": "Socket připojen přes měřenou mobilní síť",
"MessageSocketConnectedOverMeteredWifi": "Socket connected over metered wifi", "MessageSocketConnectedOverMeteredWifi": "Socket připojen přes měřenou Wi-Fi",
"MessageSocketConnectedOverUnmeteredCellular": "Socket connected over unmetered cellular", "MessageSocketConnectedOverUnmeteredCellular": "Socket připojen přes neměřenou mobilní síť",
"MessageSocketConnectedOverUnmeteredWifi": "Socket connected over unmetered wifi", "MessageSocketConnectedOverUnmeteredWifi": "Socket připojen přes neměřenou Wi-Fi",
"MessageSocketNotConnected": "Socket not connected", "MessageSocketNotConnected": "Socket není připojen",
"NoteRSSFeedPodcastAppsHttps": "Upozornění: Většina aplikací pro podcasty bude vyžadovat, aby adresa URL kanálu RSS používala protokol HTTPS", "NoteRSSFeedPodcastAppsHttps": "Upozornění: Většina aplikací pro podcasty bude vyžadovat, aby adresa URL kanálu RSS používala protokol HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Upozornění: 1 nebo více epizod nemá datum vydání. Některé podcastové aplikace to vyžadují.", "NoteRSSFeedPodcastAppsPubDate": "Upozornění: 1 nebo více epizod nemá datum vydání. Některé podcastové aplikace to vyžadují.",
"ToastBookmarkCreateFailed": "Vytvoření záložky se nezdařilo", "ToastBookmarkCreateFailed": "Vytvoření záložky se nezdařilo",
"ToastBookmarkRemoveFailed": "Nepodařilo se odstranit záložku", "ToastBookmarkRemoveFailed": "Nepodařilo se odstranit záložku",
"ToastBookmarkUpdateFailed": "Aktualizace záložky se nezdařila", "ToastBookmarkUpdateFailed": "Aktualizace záložky se nezdařila",
"ToastDownloadNotAllowedOnCellular": "Stahování přes mobilní data není povoleno",
"ToastItemMarkedAsFinishedFailed": "Nepodařilo se označit jako dokončené", "ToastItemMarkedAsFinishedFailed": "Nepodařilo se označit jako dokončené",
"ToastItemMarkedAsNotFinishedFailed": "Nepodařilo se označit jako nedokončené", "ToastItemMarkedAsNotFinishedFailed": "Nepodařilo se označit jako nedokončené",
"ToastPlaylistCreateFailed": "Vytvoření seznamu přehrávání se nezdařilo", "ToastPlaylistCreateFailed": "Vytvoření seznamu přehrávání se nezdařilo",
"ToastPodcastCreateFailed": "Vytvoření podcastu se nezdařilo", "ToastPodcastCreateFailed": "Vytvoření podcastu se nezdařilo",
"ToastPodcastCreateSuccess": "Podcast byl úspěšně vytvořen", "ToastPodcastCreateSuccess": "Podcast byl úspěšně vytvořen",
"ToastRSSFeedCloseFailed": "Nepodařilo se zavřít RSS kanál", "ToastRSSFeedCloseFailed": "Nepodařilo se zavřít RSS kanál",
"ToastRSSFeedCloseSuccess": "RSS kanál uzavřen" "ToastRSSFeedCloseSuccess": "RSS kanál uzavřen",
} "ToastStreamingNotAllowedOnCellular": "Přehrávání přes mobilní data není povoleno"
}
+47 -31
View File
@@ -1,51 +1,53 @@
{ {
"ButtonAdd": "Tilføj", "ButtonAdd": "Tilføj",
"ButtonAddNewServer": "Add New Server", "ButtonAddNewServer": "Tilføj ny server",
"ButtonAuthors": "Forfattere", "ButtonAuthors": "Forfattere",
"ButtonBack": "Back", "ButtonBack": "Tilbage",
"ButtonCancel": "Annuller", "ButtonCancel": "Annuller",
"ButtonCancelTimer": "Cancel Timer", "ButtonCancelTimer": "Stop timer",
"ButtonClearFilter": "Ryd filter", "ButtonClearFilter": "Ryd filter",
"ButtonCloseFeed": "Luk feed", "ButtonCloseFeed": "Luk feed",
"ButtonCollections": "Samlinger", "ButtonCollections": "Samlinger",
"ButtonConnect": "Connect", "ButtonConnect": "Forbind",
"ButtonConnectToServer": "Connect to Server", "ButtonConnectToServer": "Forbind til Server",
"ButtonCreate": "Opret", "ButtonCreate": "Opret",
"ButtonCreateBookmark": "Create Bookmark", "ButtonCreateBookmark": "Opret Bogmærker",
"ButtonCreateNewPlaylist": "Create New Playlist", "ButtonCreateNewPlaylist": "Opret Ny Playlist",
"ButtonDelete": "Slet", "ButtonDelete": "Slet",
"ButtonDeleteLocalEpisode": "Delete local episode", "ButtonDeleteLocalEpisode": "Slet lokalt afsnit",
"ButtonDeleteLocalFile": "Delete local file", "ButtonDeleteLocalFile": "Delete local file",
"ButtonDeleteLocalItem": "Delete local item", "ButtonDeleteLocalItem": "Delete local item",
"ButtonDisableAutoTimer": "Disable Auto Timer", "ButtonDisableAutoTimer": "Afbryd Auto Timer",
"ButtonDisconnect": "Disconnect", "ButtonDisconnect": "Luk forbindelse",
"ButtonHistory": "History", "ButtonGoToWebClient": "Go to Web Client",
"ButtonHistory": "Historik",
"ButtonHome": "Hjem", "ButtonHome": "Hjem",
"ButtonIssues": "Problemer", "ButtonIssues": "Problemer",
"ButtonLatest": "Seneste", "ButtonLatest": "Seneste",
"ButtonLibrary": "Bibliotek", "ButtonLibrary": "Bibliotek",
"ButtonLocalMedia": "Local Media", "ButtonLocalMedia": "Local Media",
"ButtonManageLocalFiles": "Manage Local Files", "ButtonManageLocalFiles": "Administrér Lokale Filer",
"ButtonNewFolder": "New Folder", "ButtonNewFolder": "Ny Folder",
"ButtonNextEpisode": "Next Episode", "ButtonNextEpisode": "Næste Afsnit",
"ButtonOpenFeed": "Åbn feed", "ButtonOpenFeed": "Åbn feed",
"ButtonOverride": "Override", "ButtonOverride": "Override",
"ButtonPause": "Pause", "ButtonPause": "Pause",
"ButtonPlay": "Afspil", "ButtonPlay": "Afspil",
"ButtonPlayEpisode": "Afspil Afsnit",
"ButtonPlaying": "Afspiller", "ButtonPlaying": "Afspiller",
"ButtonPlaylists": "Afspilningslister", "ButtonPlaylists": "Afspilningslister",
"ButtonRead": "Læs", "ButtonRead": "Læs",
"ButtonRemove": "Fjern", "ButtonRemove": "Fjern",
"ButtonRemoveFromServer": "Remove from Server", "ButtonRemoveFromServer": "Fjern fra Server",
"ButtonSave": "Gem", "ButtonSave": "Gem",
"ButtonSaveOrder": "Save Order", "ButtonSaveOrder": "Gem Rækkefølge",
"ButtonSearch": "Søg", "ButtonSearch": "Søg",
"ButtonSendEbookToDevice": "Send Ebook to Device", "ButtonSendEbookToDevice": "Send E-bog til Enhed",
"ButtonSeries": "Serie", "ButtonSeries": "Serie",
"ButtonSetTimer": "Set Timer", "ButtonSetTimer": "Set Timer",
"ButtonStream": "Stream", "ButtonStream": "Stream",
"ButtonSubmit": "Send", "ButtonSubmit": "Send",
"ButtonSwitchServerUser": "Switch Server/User", "ButtonSwitchServerUser": "Skift Server/Bruger",
"ButtonUserStats": "User Stats", "ButtonUserStats": "User Stats",
"ButtonYes": "Ja", "ButtonYes": "Ja",
"HeaderAccount": "Konto", "HeaderAccount": "Konto",
@@ -54,7 +56,8 @@
"HeaderChapters": "Kapitler", "HeaderChapters": "Kapitler",
"HeaderCollection": "Samling", "HeaderCollection": "Samling",
"HeaderCollectionItems": "Samlingselementer", "HeaderCollectionItems": "Samlingselementer",
"HeaderConnectionStatus": "Connection Status", "HeaderConnectionStatus": "Forbindelsesstatus",
"HeaderDataSettings": "Data Settings",
"HeaderDetails": "Detaljer", "HeaderDetails": "Detaljer",
"HeaderDownloads": "Downloads", "HeaderDownloads": "Downloads",
"HeaderEbookFiles": "E-bogsfiler", "HeaderEbookFiles": "E-bogsfiler",
@@ -81,10 +84,13 @@
"HeaderTableOfContents": "Indholdsfortegnelse", "HeaderTableOfContents": "Indholdsfortegnelse",
"HeaderUserInterfaceSettings": "User Interface Settings", "HeaderUserInterfaceSettings": "User Interface Settings",
"HeaderYourStats": "Dine Statistikker", "HeaderYourStats": "Dine Statistikker",
"LabelAddToPlaylist": "Tilføj til Afspilningsliste",
"LabelAdded": "Tilføjet", "LabelAdded": "Tilføjet",
"LabelAddedAt": "Tilføjet Kl.", "LabelAddedAt": "Tilføjet Kl.",
"LabelAddToPlaylist": "Tilføj til Afspilningsliste",
"LabelAll": "Alle", "LabelAll": "Alle",
"LabelAllowSeekingOnMediaControls": "Allow position seeking on media notification controls",
"LabelAlways": "Always",
"LabelAskConfirmation": "Ask for confirmation",
"LabelAuthor": "Forfatter", "LabelAuthor": "Forfatter",
"LabelAuthorFirstLast": "Forfatter (Fornavn Efternavn)", "LabelAuthorFirstLast": "Forfatter (Fornavn Efternavn)",
"LabelAuthorLastFirst": "Forfatter (Efternavn, Fornavn)", "LabelAuthorLastFirst": "Forfatter (Efternavn, Fornavn)",
@@ -96,8 +102,8 @@
"LabelAutoSleepTimerAutoRewindHelp": "When the auto sleep timer finishes, playing the item again will automatically rewind your position.", "LabelAutoSleepTimerAutoRewindHelp": "When the auto sleep timer finishes, playing the item again will automatically rewind your position.",
"LabelAutoSleepTimerHelp": "When playing media between the specified start and end times a sleep timer will automatically start.", "LabelAutoSleepTimerHelp": "When playing media between the specified start and end times a sleep timer will automatically start.",
"LabelBooks": "Bøger", "LabelBooks": "Bøger",
"LabelChapters": "Kapitler",
"LabelChapterTrack": "Chapter Track", "LabelChapterTrack": "Chapter Track",
"LabelChapters": "Kapitler",
"LabelClosePlayer": "Luk afspiller", "LabelClosePlayer": "Luk afspiller",
"LabelCollapseSeries": "Fold Serie Sammen", "LabelCollapseSeries": "Fold Serie Sammen",
"LabelComplete": "Fuldfør", "LabelComplete": "Fuldfør",
@@ -117,6 +123,7 @@
"LabelDisableVibrateOnResetHelp": "When the sleep timer gets reset your device will vibrate. Enable this setting to not vibrate when the sleep timer resets.", "LabelDisableVibrateOnResetHelp": "When the sleep timer gets reset your device will vibrate. Enable this setting to not vibrate when the sleep timer resets.",
"LabelDiscover": "Discover", "LabelDiscover": "Discover",
"LabelDownload": "Download", "LabelDownload": "Download",
"LabelDownloadUsingCellular": "Download using Cellular",
"LabelDownloaded": "Downloaded", "LabelDownloaded": "Downloaded",
"LabelDuration": "Varighed", "LabelDuration": "Varighed",
"LabelEbook": "E-bog", "LabelEbook": "E-bog",
@@ -144,8 +151,8 @@
"LabelHeavy": "Heavy", "LabelHeavy": "Heavy",
"LabelHigh": "High", "LabelHigh": "High",
"LabelHost": "Vært", "LabelHost": "Vært",
"LabelIncomplete": "Ufuldstændig",
"LabelInProgress": "I gang", "LabelInProgress": "I gang",
"LabelIncomplete": "Ufuldstændig",
"LabelInternalAppStorage": "Internal App Storage", "LabelInternalAppStorage": "Internal App Storage",
"LabelJumpBackwardsTime": "Jump backwards time", "LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time", "LabelJumpForwardsTime": "Jump forwards time",
@@ -168,6 +175,7 @@
"LabelName": "Navn", "LabelName": "Navn",
"LabelNarrator": "Fortæller", "LabelNarrator": "Fortæller",
"LabelNarrators": "Fortællere", "LabelNarrators": "Fortællere",
"LabelNever": "Never",
"LabelNewestAuthors": "Newest Authors", "LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes", "LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No", "LabelNo": "No",
@@ -186,14 +194,16 @@
"LabelProgress": "Fremskridt", "LabelProgress": "Fremskridt",
"LabelPubDate": "Udgivelsesdato", "LabelPubDate": "Udgivelsesdato",
"LabelPublishYear": "Udgivelsesår", "LabelPublishYear": "Udgivelsesår",
"LabelRead": "Læst",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"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",
"LabelRead": "Læst",
"LabelReadAgain": "Read Again",
"LabelRecentSeries": "Recent Series",
"LabelRecentlyAdded": "Recently Added",
"LabelRemoveFromPlaylist": "Remove from Playlist",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Sæson", "LabelSeason": "Sæson",
"LabelSelectADevice": "Select a device", "LabelSelectADevice": "Select a device",
"LabelSeries": "Serie", "LabelSeries": "Serie",
@@ -215,6 +225,7 @@
"LabelStatsMinutes": "minutter", "LabelStatsMinutes": "minutter",
"LabelStatsMinutesListening": "Minutter hørt", "LabelStatsMinutesListening": "Minutter hørt",
"LabelStatsWeekListening": "Ugens lytning", "LabelStatsWeekListening": "Ugens lytning",
"LabelStreamingUsingCellular": "Streaming using Cellular",
"LabelTag": "Mærke", "LabelTag": "Mærke",
"LabelTags": "Mærker", "LabelTags": "Mærker",
"LabelTheme": "Tema", "LabelTheme": "Tema",
@@ -241,8 +252,10 @@
"MessageConfirmDeleteLocalEpisode": "Remove local episode \"{0}\" from your device? The file on the server will be unaffected.", "MessageConfirmDeleteLocalEpisode": "Remove local episode \"{0}\" from your device? The file on the server will be unaffected.",
"MessageConfirmDeleteLocalFiles": "Remove local files of this item from your device? The files on the server and your progress will be unaffected.", "MessageConfirmDeleteLocalFiles": "Remove local files of this item from your device? The files on the server and your progress will be unaffected.",
"MessageConfirmDiscardProgress": "Are you sure you want to reset your progress?", "MessageConfirmDiscardProgress": "Are you sure you want to reset your progress?",
"MessageConfirmDownloadUsingCellular": "You are about to download using cellular data. This may include carrier data charges. Do you wish to continue?",
"MessageConfirmMarkAsFinished": "Are you sure you want to mark this item as finished?", "MessageConfirmMarkAsFinished": "Are you sure you want to mark this item as finished?",
"MessageConfirmRemoveBookmark": "Are you sure you want to remove bookmark?", "MessageConfirmRemoveBookmark": "Are you sure you want to remove bookmark?",
"MessageConfirmStreamingUsingCellular": "You are about to stream using cellular data. This may include carrier data charges. Do you wish to continue?",
"MessageDiscardProgress": "Discard Progress", "MessageDiscardProgress": "Discard Progress",
"MessageDownloadCompleteProcessing": "Download complete. Processing...", "MessageDownloadCompleteProcessing": "Download complete. Processing...",
"MessageDownloading": "Downloading...", "MessageDownloading": "Downloading...",
@@ -250,7 +263,7 @@
"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": "Follow the project on GitHub",
"MessageItemDownloadCompleteFailedToCreate": "Item download complete but failed to create library item", "MessageItemDownloadCompleteFailedToCreate": "Item download complete but failed to create library item",
"MessageLoading": "Indlæser...", "MessageLoading": "Indlæser...",
"MessageLoadingServerData": "Loading server data...", "MessageLoadingServerData": "Loading server data...",
@@ -268,24 +281,27 @@
"MessageNoMediaFolders": "No Media Folders", "MessageNoMediaFolders": "No Media Folders",
"MessageNoNetworkConnection": "No network connection", "MessageNoNetworkConnection": "No network connection",
"MessageNoPodcastsFound": "Ingen podcasts fundet", "MessageNoPodcastsFound": "Ingen podcasts fundet",
"MessageNoSeries": "No series",
"MessageNoUpdatesWereNecessary": "Ingen opdateringer var nødvendige", "MessageNoUpdatesWereNecessary": "Ingen opdateringer var nødvendige",
"MessageNoUserPlaylists": "Du har ingen afspilningslister", "MessageNoUserPlaylists": "Du har ingen afspilningslister",
"MessageReportBugsAndContribute": "Rapporter fejl, anmod om funktioner og bidrag på", "MessageReportBugsAndContribute": "Rapporter fejl, anmod om funktioner og bidrag på",
"MessageSocketConnectedOverMeteredCellular": "Socket connected over metered cellular", "MessageSocketConnectedOverMeteredCellular": "Socket connected over metered cellular",
"MessageSocketConnectedOverMeteredWifi": "Socket connected over metered wifi", "MessageSocketConnectedOverMeteredWifi": "Socket connected over metered Wi-Fi",
"MessageSocketConnectedOverUnmeteredCellular": "Socket connected over unmetered cellular", "MessageSocketConnectedOverUnmeteredCellular": "Socket connected over unmetered cellular",
"MessageSocketConnectedOverUnmeteredWifi": "Socket connected over unmetered wifi", "MessageSocketConnectedOverUnmeteredWifi": "Socket connected over unmetered Wi-Fi",
"MessageSocketNotConnected": "Socket not connected", "MessageSocketNotConnected": "Socket not connected",
"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": "Downloading is not allowed on cellular data",
"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": "Streaming is not allowed on cellular data"
}
+92 -74
View File
@@ -17,56 +17,59 @@
"ButtonDeleteLocalEpisode": "Lösche lokale Episode", "ButtonDeleteLocalEpisode": "Lösche lokale Episode",
"ButtonDeleteLocalFile": "Lösche lokale Datei", "ButtonDeleteLocalFile": "Lösche lokale Datei",
"ButtonDeleteLocalItem": "Lösche lokales Element", "ButtonDeleteLocalItem": "Lösche lokales Element",
"ButtonDisableAutoTimer": "Deaktiviere Automatischer-Timer", "ButtonDisableAutoTimer": "Deaktiviere automatischen Timer",
"ButtonDisconnect": "Trennen", "ButtonDisconnect": "Trennen",
"ButtonGoToWebClient": "Gehe zum Web Client",
"ButtonHistory": "Historie", "ButtonHistory": "Historie",
"ButtonHome": "Startseite", "ButtonHome": "Startseite",
"ButtonIssues": "Probleme", "ButtonIssues": "Probleme",
"ButtonLatest": "Neuste", "ButtonLatest": "Neueste",
"ButtonLibrary": "Bibliothek", "ButtonLibrary": "Bibliothek",
"ButtonLocalMedia": "Lokale Medien", "ButtonLocalMedia": "Lokale Medien",
"ButtonManageLocalFiles": "Verwalte lokale Dateien", "ButtonManageLocalFiles": "Verwalte lokale Dateien",
"ButtonNewFolder": "Neuer Ordner", "ButtonNewFolder": "Neuer Ordner",
"ButtonNextEpisode": "Nächste Episode", "ButtonNextEpisode": "Nächste Episode",
"ButtonOpenFeed": "Feed öffnen", "ButtonOpenFeed": "Feed öffnen",
"ButtonOverride": "Überschreieben", "ButtonOverride": "Überschreiben",
"ButtonPause": "Pause", "ButtonPause": "Pause",
"ButtonPlay": "Abspielen", "ButtonPlay": "Abspielen",
"ButtonPlayEpisode": "Episode abspielen",
"ButtonPlaying": "Spielt", "ButtonPlaying": "Spielt",
"ButtonPlaylists": "Wiedergabelisten", "ButtonPlaylists": "Wiedergabelisten",
"ButtonRead": "Lese", "ButtonRead": "Lesen",
"ButtonRemove": "Löschen", "ButtonRemove": "Entfernen",
"ButtonRemoveFromServer": "Vom Server entfernen", "ButtonRemoveFromServer": "Vom Server entfernen",
"ButtonSave": "Speichern", "ButtonSave": "Speichern",
"ButtonSaveOrder": "Speicher Reihenfolge", "ButtonSaveOrder": "Reihenfolge speichern",
"ButtonSearch": "Suchen", "ButtonSearch": "Suchen",
"ButtonSendEbookToDevice": "Sende Ebook an Gerät", "ButtonSendEbookToDevice": "E-Buch an Gerät senden",
"ButtonSeries": "Serien", "ButtonSeries": "Serien",
"ButtonSetTimer": "Setze Timer", "ButtonSetTimer": "Setze Timer",
"ButtonStream": "Stream", "ButtonStream": "Stream",
"ButtonSubmit": "Ok", "ButtonSubmit": "Ok",
"ButtonSwitchServerUser": "Wechsle Server/Benutzer", "ButtonSwitchServerUser": "Wechsle Server/Benutzer",
"ButtonUserStats": "Nutzer Statistiken", "ButtonUserStats": "Nutzerstatistiken",
"ButtonYes": "Ja", "ButtonYes": "Ja",
"HeaderAccount": "Konto", "HeaderAccount": "Konto",
"HeaderAdvanced": "Erweitert", "HeaderAdvanced": "Erweitert",
"HeaderAudioTracks": "Audiodateien", "HeaderAudioTracks": "Audiodateien",
"HeaderChapters": "Kapitel", "HeaderChapters": "Kapitel",
"HeaderCollection": "Sammlungen", "HeaderCollection": "Sammlung",
"HeaderCollectionItems": "Sammlungseinträge", "HeaderCollectionItems": "Sammlungseinträge",
"HeaderConnectionStatus": "Verbindungs Status", "HeaderConnectionStatus": "Verbindungsstatus",
"HeaderDetails": "Einzelheiten", "HeaderDataSettings": "Daten-Einstellungen",
"HeaderDetails": "Details",
"HeaderDownloads": "Downloads", "HeaderDownloads": "Downloads",
"HeaderEbookFiles": "E-Book Dateien", "HeaderEbookFiles": "E-Buch-Dateien",
"HeaderEpisodes": "Episoden", "HeaderEpisodes": "Episoden",
"HeaderEreaderSettings": "Ereader Einstellungen", "HeaderEreaderSettings": "Einstellungen zum Lesen",
"HeaderLatestEpisodes": "Letzte Episoden", "HeaderLatestEpisodes": "Neueste Episoden",
"HeaderLibraries": "Bibliotheken", "HeaderLibraries": "Bibliotheken",
"HeaderLocalFolders": "Lokale Ordner", "HeaderLocalFolders": "Lokale Ordner",
"HeaderLocalLibraryItems": "Lokale Bibliothek Elemente", "HeaderLocalLibraryItems": "Elemente der lokalen Bibliothek",
"HeaderNewPlaylist": "Neue Wiedergabeliste", "HeaderNewPlaylist": "Neue Wiedergabeliste",
"HeaderOpenRSSFeed": "RSS-Feed öffnen", "HeaderOpenRSSFeed": "RSS-Feed öffnen",
"HeaderPlaybackSettings": "Wiedergabe Einstellungen", "HeaderPlaybackSettings": "Wiedergabe-Einstellungen",
"HeaderPlaylist": "Wiedergabeliste", "HeaderPlaylist": "Wiedergabeliste",
"HeaderPlaylistItems": "Einträge in der Wiedergabeliste", "HeaderPlaylistItems": "Einträge in der Wiedergabeliste",
"HeaderRSSFeed": "RSS-Feed", "HeaderRSSFeed": "RSS-Feed",
@@ -74,58 +77,62 @@
"HeaderRSSFeedIsOpen": "RSS-Feed ist geöffnet", "HeaderRSSFeedIsOpen": "RSS-Feed ist geöffnet",
"HeaderSelectDownloadLocation": "Wähle Download-Speicherort", "HeaderSelectDownloadLocation": "Wähle Download-Speicherort",
"HeaderSettings": "Einstellungen", "HeaderSettings": "Einstellungen",
"HeaderSleepTimer": "Einschlaf-Timer", "HeaderSleepTimer": "Schlummerfunktion",
"HeaderSleepTimerSettings": "Einschlaf-Timer Einstellungen", "HeaderSleepTimerSettings": "Schlummerfunktion Einstellungen",
"HeaderStatsMinutesListeningChart": "Hörminuten (letzte 7 Tage)", "HeaderStatsMinutesListeningChart": "Hörminuten (letzte 7 Tage)",
"HeaderStatsRecentSessions": "Neueste Ereignisse", "HeaderStatsRecentSessions": "Neueste Ereignisse",
"HeaderTableOfContents": "Inhaltsverzeichnis", "HeaderTableOfContents": "Inhaltsverzeichnis",
"HeaderUserInterfaceSettings": "Einstellungen der Benutzeroberfläche", "HeaderUserInterfaceSettings": "Einstellungen der Benutzeroberfläche",
"HeaderYourStats": "Eigene Statistiken", "HeaderYourStats": "Eigene Statistiken",
"LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen",
"LabelAdded": "Hinzugefügt", "LabelAdded": "Hinzugefügt",
"LabelAddedAt": "Hinzugefügt am", "LabelAddedAt": "Hinzugefügt am",
"LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen",
"LabelAll": "Alle", "LabelAll": "Alle",
"LabelAllowSeekingOnMediaControls": "Erlaube Vor- und Zurückspulen auf dem Medienkontrollelement bei den Benachrichtigungen",
"LabelAlways": "Immer",
"LabelAskConfirmation": "Bestätigung anfordern",
"LabelAuthor": "Autor", "LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Vorname Nachname)", "LabelAuthorFirstLast": "Autor (Vorname Nachname)",
"LabelAuthorLastFirst": "Autor (Nachname, Vorname)", "LabelAuthorLastFirst": "Autor (Nachname, Vorname)",
"LabelAuthors": "Autoren", "LabelAuthors": "Autoren",
"LabelAutoDownloadEpisodes": "Episoden automatisch herunterladen", "LabelAutoDownloadEpisodes": "Episoden automatisch herunterladen",
"LabelAutoRewindTime": "Automatische Rückspulzeit", "LabelAutoRewindTime": "Automatische Rückspulzeit",
"LabelAutoSleepTimer": "Automatischer Einschlaf-Timer", "LabelAutoSleepTimer": "Automatische Schlummerfunktion",
"LabelAutoSleepTimerAutoRewind": "Automatischer Einschlaf-Timer automatische Rückspulzeit", "LabelAutoSleepTimerAutoRewind": "Automatische Schlummerfunktion automatische Rückspulzeit",
"LabelAutoSleepTimerAutoRewindHelp": "Wenn die Einschlaf-Timer abgelaufen ist, wird bei der erneuten Wiedergabe des Titels die Position automatisch zurückgespult.", "LabelAutoSleepTimerAutoRewindHelp": "Wenn die Schlummerfunktion abgelaufen ist, wird bei der erneuten Wiedergabe des Titels die Position automatisch zurückgespult.",
"LabelAutoSleepTimerHelp": "Bei der Wiedergabe von Medien zwischen der angegebenen Start- und Endzeit wird automatisch ein Einschlaf-Timer gestartet.", "LabelAutoSleepTimerHelp": "Bei der Wiedergabe von Medien zwischen der angegebenen Start- und Endzeit wird automatisch eine Schlummerfunktion gestartet.",
"LabelBooks": "Bücher", "LabelBooks": "Bücher",
"LabelChapterTrack": "Kapitelspur",
"LabelChapters": "Kapitel", "LabelChapters": "Kapitel",
"LabelChapterTrack": "Kapitel Spur",
"LabelClosePlayer": "Player schließen", "LabelClosePlayer": "Player schließen",
"LabelCollapseSeries": "Serien zusammenfassen", "LabelCollapseSeries": "Serien zusammenfassen",
"LabelComplete": "Vollständig", "LabelComplete": "Vollständig",
"LabelContinueBooks": "Bücher fortfahren", "LabelContinueBooks": "Bücher fortfahren",
"LabelContinueEpisodes": "Episoden fortfahren", "LabelContinueEpisodes": "Episoden fortfahren",
"LabelContinueListening": "Weiterhören", "LabelContinueListening": "Weiterhören",
"LabelContinueReading": "Continue Reading", "LabelContinueReading": "Weiterlesen",
"LabelContinueSeries": "Serie fortsetzen", "LabelContinueSeries": "Serie fortsetzen",
"LabelCustomTime": "Benutzerdefinierte Zeit", "LabelCustomTime": "Benutzerdefinierte Zeit",
"LabelDescription": "Beschreibung", "LabelDescription": "Beschreibung",
"LabelDisableAudioFadeOut": "Deaktiviere Audio abklingen", "LabelDisableAudioFadeOut": "Deaktiviere Audio abklingen",
"LabelDisableAudioFadeOutHelp": "Die Audiolautstärke wird verringert, wenn weniger als 1 Minute für den Einschlaf-Timer verbleibt. Aktivieren Sie diese Einstellung, um nicht abzuklingen.", "LabelDisableAudioFadeOutHelp": "Die Audiolautstärke wird verringert, wenn weniger als 1 Minute für den Sleep-Timer verbleibt. Aktiviere diese Einstellung, um nicht abzuklingen.",
"LabelDisableAutoRewind": "Deaktiviere automatisches Rückspulen", "LabelDisableAutoRewind": "Deaktiviere automatisches Zurückspulen",
"LabelDisableShakeToReset": "Deaktiviere Schütteln zum Zurücksetzen", "LabelDisableShakeToReset": "Deaktiviere Schütteln zum Zurücksetzen",
"LabelDisableShakeToResetHelp": "Wenn Sie Ihr Gerät schütteln, während der Timer läuft ODER innerhalb von 2 Minuten nach Ablauf des Timers, wird der Sleep-Timer zurückgesetzt. Aktivieren Sie diese Einstellung, um das Schütteln zum Zurücksetzen zu deaktivieren.", "LabelDisableShakeToResetHelp": "Wenn du dein Gerät schüttelst, während der Timer läuft ODER innerhalb von 2 Minuten nach Ablauf des Timers, wird der Sleep-Timer zurückgesetzt. Aktiviere diese Einstellung, um das Schütteln zum Zurücksetzen zu deaktivieren.",
"LabelDisableVibrateOnReset": "Deaktiviere Vibrieren beim Zurücksetzen", "LabelDisableVibrateOnReset": "Deaktiviere Vibrieren beim Zurücksetzen",
"LabelDisableVibrateOnResetHelp": "Wenn der Einschlaf-Timer zurückgesetzt wird, vibriert Ihr Gerät. Aktivieren Sie diese Einstellung, um nicht zu vibrieren, wenn der Einschlaf-Timer zurückgesetzt wird.", "LabelDisableVibrateOnResetHelp": "Wenn der Sleep-Timer zurückgesetzt wird, vibriert dein Gerät. Aktiviere diese Einstellung, um nicht zu vibrieren, wenn der Sleep-Timer zurückgesetzt wird.",
"LabelDiscover": "Entdecken", "LabelDiscover": "Entdecken",
"LabelDownload": "Herunterladen", "LabelDownload": "Herunterladen",
"LabelDownloadUsingCellular": "Über mobile Daten herunterladen",
"LabelDownloaded": "Heruntergeladen", "LabelDownloaded": "Heruntergeladen",
"LabelDuration": "Laufzeit", "LabelDuration": "Laufzeit",
"LabelEbook": "E-Book", "LabelEbook": "E-Buch",
"LabelEbooks": "Ebooks", "LabelEbooks": "E-Bücher",
"LabelEnable": "Aktivieren", "LabelEnable": "Aktivieren",
"LabelEnableMp3IndexSeeking": "MP3-Indexsuche", "LabelEnableMp3IndexSeeking": "MP3-Indexsuche",
"LabelEnableMp3IndexSeekingHelp": "Diese Einstellung sollte nur aktiviert werden, wenn Sie MP3-Dateien haben, bei denen das Navigieren (Seeking) nicht korrekt funktioniert. Ungenaues Navigieren ist höchstwahrscheinlich auf MP3-Dateien mit variabler Bitrate (VBR) zurückzuführen. Diese Einstellung erzwingt das Index-Navigieren (Index Seeking), bei dem eine Zeit-zu-Byte-Zuordnung erstellt wird, während die Datei gelesen wird. In einigen Fällen kann es bei großen MP3-Dateien zu einer Verzögerung kommen, wenn gegen Ende der Datei navigiert wird.", "LabelEnableMp3IndexSeekingHelp": "Diese Einstellung sollte nur aktiviert werden, wenn du MP3-Dateien hast, bei denen das Navigieren (Seeking) nicht korrekt funktioniert. Ungenaues Navigieren ist höchstwahrscheinlich auf MP3-Dateien mit variabler Bitrate (VBR) zurückzuführen. Diese Einstellung erzwingt das Index-Navigieren (Index Seeking), bei dem eine Zeit-zu-Byte-Zuordnung erstellt wird, während die Datei gelesen wird. In einigen Fällen kann es bei großen MP3-Dateien zu einer Verzögerung kommen, wenn gegen Ende der Datei navigiert wird.",
"LabelEnd": "Ende", "LabelEnd": "Ende",
"LabelEndOfChapter": "Kapitelende", "LabelEndOfChapter": "Ende des Kapitels",
"LabelEndTime": "Endzeit", "LabelEndTime": "Endzeit",
"LabelEpisode": "Episode", "LabelEpisode": "Episode",
"LabelFeedURL": "Feed URL", "LabelFeedURL": "Feed URL",
@@ -133,45 +140,47 @@
"LabelFileBirthtime": "Datei erstellt", "LabelFileBirthtime": "Datei erstellt",
"LabelFileModified": "Datei geändert", "LabelFileModified": "Datei geändert",
"LabelFilename": "Dateiname", "LabelFilename": "Dateiname",
"LabelFinished": "beendet", "LabelFinished": "Beendet",
"LabelFolder": "Ordner", "LabelFolder": "Ordner",
"LabelFontBoldness": "Schriftstärke",
"LabelFontScale": "Schriftgröße", "LabelFontScale": "Schriftgröße",
"LabelGenre": "Kategorie", "LabelGenre": "Kategorie",
"LabelGenres": "Kategorien", "LabelGenres": "Kategorien",
"LabelHapticFeedback": "Haptische Rückmeldung", "LabelHapticFeedback": "Haptische Rückmeldung",
"LabelHasEbook": "Hat Ebook", "LabelHasEbook": "E-Book verfügbar",
"LabelHasSupplementaryEbook": "Hat zusätzliches Ebook", "LabelHasSupplementaryEbook": "Ergänzendes E-Book verfügbar",
"LabelHeavy": "Stark", "LabelHeavy": "Stark",
"LabelHigh": "Hoch", "LabelHigh": "Hoch",
"LabelHost": "Host", "LabelHost": "Host",
"LabelIncomplete": "Unvollständig",
"LabelInProgress": "In Bearbeitung", "LabelInProgress": "In Bearbeitung",
"LabelIncomplete": "Unvollständig",
"LabelInternalAppStorage": "Interner App Speicher", "LabelInternalAppStorage": "Interner App Speicher",
"LabelJumpBackwardsTime": "Rückspulzeit", "LabelJumpBackwardsTime": "Rückspulzeit",
"LabelJumpForwardsTime": "Vorwärtsspulzeit", "LabelJumpForwardsTime": "Vorwärtsspulzeit",
"LabelLanguage": "Sprache", "LabelLanguage": "Sprache",
"LabelLayout": "Layout", "LabelLayout": "Layout",
"LabelLayoutAuto": "Auto", "LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page", "LabelLayoutSinglePage": "Eine Seite",
"LabelLight": "Leicht", "LabelLight": "Leicht",
"LabelLineSpacing": "Zeilenabstand", "LabelLineSpacing": "Zeilenabstand",
"LabelListenAgain": "Erneut anhören", "LabelListenAgain": "Erneut anhören",
"LabelLocalBooks": "Lokale Bücher", "LabelLocalBooks": "Lokale Bücher",
"LabelLocalPodcasts": "Lokale Podcasts", "LabelLocalPodcasts": "Lokale Podcasts",
"LabelLockOrientation": "automatische Bildschirmausrichtung sperren", "LabelLockOrientation": "Automatische Bildschirmausrichtung sperren",
"LabelLockPlayer": "Mediaplayer sperren", "LabelLockPlayer": "Mediaplayer sperren",
"LabelLow": "Wenig", "LabelLow": "Wenig",
"LabelMediaType": "Medientyp", "LabelMediaType": "Medientyp",
"LabelMedium": "Mittel", "LabelMedium": "Mittel",
"LabelMore": "Mehr", "LabelMore": "Mehr",
"LabelMoreInfo": "Mehr Info", "LabelMoreInfo": "Mehr Infos",
"LabelName": "Name", "LabelName": "Name",
"LabelNarrator": "Erzähler", "LabelNarrator": "Erzähler",
"LabelNarrators": "Erzähler", "LabelNarrators": "Erzähler",
"LabelNever": "Nie",
"LabelNewestAuthors": "Neueste Autoren", "LabelNewestAuthors": "Neueste Autoren",
"LabelNewestEpisodes": "Newest Episodes", "LabelNewestEpisodes": "Neueste Episoden",
"LabelNo": "Nein", "LabelNo": "Nein",
"LabelNotFinished": "nicht beendet", "LabelNotFinished": "Nicht beendet",
"LabelNotStarted": "Nicht begonnen", "LabelNotStarted": "Nicht begonnen",
"LabelOff": "Aus", "LabelOff": "Aus",
"LabelPassword": "Passwort", "LabelPassword": "Passwort",
@@ -186,44 +195,48 @@
"LabelProgress": "Fortschritt", "LabelProgress": "Fortschritt",
"LabelPubDate": "Veröffentlichungsdatum", "LabelPubDate": "Veröffentlichungsdatum",
"LabelPublishYear": "Jahr", "LabelPublishYear": "Jahr",
"LabelRead": "Lesen",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
"LabelRecentSeries": "Aktuelle Serien",
"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",
"LabelRSSFeedSlug": "RSS Feed Schlagwort", "LabelRSSFeedSlug": "RSS-Feed-Schlagwort",
"LabelRead": "Lesen",
"LabelReadAgain": "Noch einmal Lesen",
"LabelRecentSeries": "Aktuelle Serien",
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
"LabelRemoveFromPlaylist": "Von Wiedergabeliste entfernen",
"LabelScaleElapsedTimeBySpeed": "Vergangene Zeit anhand der Geschwindigkeit skalieren",
"LabelSeason": "Staffel", "LabelSeason": "Staffel",
"LabelSelectADevice": "Wähle ein Gerät", "LabelSelectADevice": "Wähle ein Gerät",
"LabelSeries": "Serien", "LabelSeries": "Serien",
"LabelServerAddress": "Server Adresse", "LabelServerAddress": "Serveradresse",
"LabelSetEbookAsPrimary": "Setzen als Hauptbuch", "LabelSetEbookAsPrimary": "Als Hauptbuch setzen",
"LabelSetEbookAsSupplementary": "Setzen als Ergänzung", "LabelSetEbookAsSupplementary": "Als Ergänzung setzen",
"LabelShakeSensitivity": "Schüttel-Empfindlichkeit", "LabelShakeSensitivity": "Schüttel-Empfindlichkeit",
"LabelShowAll": "Alles anzeigen", "LabelShowAll": "Alles anzeigen",
"LabelSize": "Größe", "LabelSize": "Größe",
"LabelSleepTimer": "Einschlaf-Timer", "LabelSleepTimer": "Schlummerfunktion",
"LabelStart": "Start", "LabelStart": "Start",
"LabelStartTime": "Startzeit", "LabelStartTime": "Startzeit",
"LabelStatsBestDay": "Bester Tag", "LabelStatsBestDay": "Bester Tag",
"LabelStatsDailyAverage": "Tagesdurchschnitt", "LabelStatsDailyAverage": "Tagesdurchschnitt",
"LabelStatsDays": "Tage", "LabelStatsDays": "Tage",
"LabelStatsDaysListened": "Gehörte Tage", "LabelStatsDaysListened": "Gehörte Tage",
"LabelStatsInARow": "nacheinander", "LabelStatsInARow": "Nacheinander",
"LabelStatsItemsFinished": "Gehörte Medien", "LabelStatsItemsFinished": "Gehörte Medien",
"LabelStatsMinutes": "Minuten", "LabelStatsMinutes": "Minuten",
"LabelStatsMinutesListening": "Gehörte Minuten", "LabelStatsMinutesListening": "Gehörte Minuten",
"LabelStatsWeekListening": "Gehörte Wochen", "LabelStatsWeekListening": "Gehörte Wochen",
"LabelStreamingUsingCellular": "Über mobile Daten streamen",
"LabelTag": "Schlagwort", "LabelTag": "Schlagwort",
"LabelTags": "Schlagwörter", "LabelTags": "Schlagwörter",
"LabelTheme": "Theme", "LabelTheme": "Farbschema",
"LabelThemeDark": "Dunkel", "LabelThemeDark": "Dunkel",
"LabelThemeLight": "Licht", "LabelThemeLight": "Hell",
"LabelTimeRemaining": "{0} verbleibend", "LabelTimeRemaining": "{0} verbleibend",
"LabelTitle": "Titel", "LabelTitle": "Titel",
"LabelTotalTrack": "Total Track", "LabelTotalSize": "Gesamtegröße",
"LabelTracks": "Dateien", "LabelTotalTrack": "Gesamtanzahl Titel",
"LabelTracks": "Titel",
"LabelType": "Typ", "LabelType": "Typ",
"LabelUnlockPlayer": "Mediaplayer entsperren", "LabelUnlockPlayer": "Mediaplayer entsperren",
"LabelUseBookshelfView": "Benutze Bücherregalansicht", "LabelUseBookshelfView": "Benutze Bücherregalansicht",
@@ -236,23 +249,25 @@
"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.",
"MessageAttemptingServerConnection": "Versuch einer Serververbindung...", "MessageAttemptingServerConnection": "Versuch einer Serververbindung...",
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf Server nicht verbunden", "MessageAudiobookshelfServerNotConnected": "Audiobookshelf Server nicht verbunden",
"MessageAudiobookshelfServerRequired": "<strong>Wichtig!</strong> Diese App wurde entwickelt, um mit einem Audiobookshelf-Server zu arbeiten, den Sie oder jemand, den Sie kennen, hostet. Diese App stellt keine Inhalte zur Verfügung.", "MessageAudiobookshelfServerRequired": "<strong>Wichtig!</strong> Diese App wurde entwickelt, um mit einem Audiobookshelf-Server zu arbeiten, den du oder jemand, den du kennst, hostet. Diese App stellt keine Inhalte zur Verfügung.",
"MessageBookshelfEmpty": "Bücherregal leer", "MessageBookshelfEmpty": "Bücherregal leer",
"MessageConfirmDeleteLocalEpisode": "Entferne lokale Episode \"{0}\" von Ihrem Gerät? Die Datei auf dem Server bleibt davon unberührt.", "MessageConfirmDeleteLocalEpisode": "Soll die lokale Episode \"{0}\" von deinem Gerät entfernt werden? Die Datei auf dem Server bleibt davon unberührt.",
"MessageConfirmDeleteLocalFiles": "Entfernen Sie lokale Dateien dieses Elements von Ihrem Gerät? Die Dateien auf dem Server und Ihr Fortschritt bleiben davon unberührt.", "MessageConfirmDeleteLocalFiles": "Sollen lokale Dateien dieses Elements von deinem Gerät entfernt werden? Die Dateien auf dem Server und Ihr Fortschritt bleiben davon unberührt.",
"MessageConfirmDiscardProgress": "Sind Sie sicher, dass Sie Ihren Fortschritt zurücksetzen wollen?", "MessageConfirmDiscardProgress": "Bist du sicher, dass du deinen Fortschritt zurücksetzen willst?",
"MessageConfirmMarkAsFinished": "Sind Sie sicher, dass Sie diesen Artikel als beendet markieren wollen?", "MessageConfirmDownloadUsingCellular": "Du bist dabei, über mobile Daten herunterzuladen. Dies kann zu Gebühren deines Mobilfunkanbieters führen. Möchtest du fortfahren?",
"MessageConfirmRemoveBookmark": "Sind Sie sicher, dass Sie das Lesezeichen entfernen möchten?", "MessageConfirmMarkAsFinished": "Bist du sicher, dass du diesen Artikel als beendet markieren willst?",
"MessageDiscardProgress": "Verwerfe Fortschritt", "MessageConfirmRemoveBookmark": "Bist du sicher, dass du das Lesezeichen entfernen willst?",
"MessageDownloadCompleteProcessing": "Download abgeschlossen. Verarbeite...", "MessageConfirmStreamingUsingCellular": "Du bist dabei, über mobile Daten zu streamen. Dies kann zu Gebühren deines Mobilfunkanbieters führen. Möchtest du fortfahren?",
"MessageDownloading": "Downloaded...", "MessageDiscardProgress": "Fortschritt verwerfen",
"MessageDownloadCompleteProcessing": "Download abgeschlossen. Wird verarbeitet …",
"MessageDownloading": "Wird heruntergeladen …",
"MessageDownloadingEpisode": "Episode herunterladen", "MessageDownloadingEpisode": "Episode herunterladen",
"MessageEpisodesQueuedForDownload": "{0} Episode(n) in der Warteschlange zum Herunterladen", "MessageEpisodesQueuedForDownload": "{0} Episode(n) in der Warteschlange zum Herunterladen",
"MessageFeedURLWillBe": "Feed-URL wird {0} sein", "MessageFeedURLWillBe": "Feed-URL wird {0} sein",
"MessageFetching": "Abrufen...", "MessageFetching": "Wird abgerufen …",
"MessageFollowTheProjectOnGithub": "Folge dem Projekt auf Github", "MessageFollowTheProjectOnGithub": "Folge dem Projekt auf GitHub",
"MessageItemDownloadCompleteFailedToCreate": "Element-Download abgeschlossen, aber Bibliothekselement kann nicht erstellt werden", "MessageItemDownloadCompleteFailedToCreate": "Element-Download abgeschlossen, aber Bibliothekselement kann nicht erstellt werden",
"MessageLoading": "Laden...", "MessageLoading": "Wird geladen …",
"MessageLoadingServerData": "Lade Server Daten...", "MessageLoadingServerData": "Lade Server Daten...",
"MessageMarkAsFinished": "Als beendet markieren", "MessageMarkAsFinished": "Als beendet markieren",
"MessageMediaLinkedToADifferentServer": "Die Medien sind mit einem Audiobookshelf-Server mit einer anderen Adresse ({0}) verknüpft. Der Fortschritt wird synchronisiert, wenn eine Verbindung zu dieser Serveradresse besteht.", "MessageMediaLinkedToADifferentServer": "Die Medien sind mit einem Audiobookshelf-Server mit einer anderen Adresse ({0}) verknüpft. Der Fortschritt wird synchronisiert, wenn eine Verbindung zu dieser Serveradresse besteht.",
@@ -268,24 +283,27 @@
"MessageNoMediaFolders": "Keine Medien Ordner", "MessageNoMediaFolders": "Keine Medien Ordner",
"MessageNoNetworkConnection": "Keine Netzwerkverbindung", "MessageNoNetworkConnection": "Keine Netzwerkverbindung",
"MessageNoPodcastsFound": "Keine Podcasts gefunden", "MessageNoPodcastsFound": "Keine Podcasts gefunden",
"MessageNoSeries": "Keine Serie",
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig", "MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
"MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden", "MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden",
"MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und Beiträge leisten auf", "MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und mitwirken",
"MessageSocketConnectedOverMeteredCellular": "Socket verbunden über getaktetes Mobilfunknetz", "MessageSocketConnectedOverMeteredCellular": "Socket verbunden über getaktetes Mobilfunknetz",
"MessageSocketConnectedOverMeteredWifi": "Socket verbunden über getaktetes WLAN", "MessageSocketConnectedOverMeteredWifi": "Socket verbunden über getaktetes WLAN",
"MessageSocketConnectedOverUnmeteredCellular": "Socket verbunden über ungetaktetes Mobilfunknetz", "MessageSocketConnectedOverUnmeteredCellular": "Socket verbunden über ungetaktetes Mobilfunknetz",
"MessageSocketConnectedOverUnmeteredWifi": "Socket verbunden über ungetaktetes Mobilfunknetz", "MessageSocketConnectedOverUnmeteredWifi": "Socket verbunden über ungetaktetes Mobilfunknetz",
"MessageSocketNotConnected": "Socket nicht verbunden", "MessageSocketNotConnected": "Socket nicht verbunden",
"NoteRSSFeedPodcastAppsHttps": "Warnung: Die meisten Podcast-Apps verlangen, dass die URL des RSS-Feeds HTTPS verwendet.", "NoteRSSFeedPodcastAppsHttps": "Warnung: Die meisten Podcast-Apps verlangen, dass die URL des RSS-Feeds HTTPS verwendet",
"NoteRSSFeedPodcastAppsPubDate": "Warnung: 1 oder mehrere Ihrer Episoden haben kein Veröffentlichungsdatum. Einige Podcast-Apps verlangen dies.", "NoteRSSFeedPodcastAppsPubDate": "Warnung: 1 oder mehrere deiner Episoden haben kein Veröffentlichungsdatum. Einige Podcast-Apps verlangen dies.",
"ToastBookmarkCreateFailed": "Lesezeichen konnte nicht erstellt werden", "ToastBookmarkCreateFailed": "Lesezeichen konnte nicht erstellt werden",
"ToastBookmarkRemoveFailed": "Lesezeichen konnte nicht gelöscht werden", "ToastBookmarkRemoveFailed": "Lesezeichen konnte nicht entfernt werden",
"ToastBookmarkUpdateFailed": "Lesezeichenaktualisierung fehlgeschlagen", "ToastBookmarkUpdateFailed": "Lesezeichenaktualisierung fehlgeschlagen",
"ToastDownloadNotAllowedOnCellular": "Das Herunterladen über mobile Daten ist nicht erlaubt",
"ToastItemMarkedAsFinishedFailed": "Fehler bei der Markierung des Mediums als \"Beendet\"", "ToastItemMarkedAsFinishedFailed": "Fehler bei der Markierung des Mediums als \"Beendet\"",
"ToastItemMarkedAsNotFinishedFailed": "Fehler bei der Markierung des Mediums als \"Nicht Beendet\"", "ToastItemMarkedAsNotFinishedFailed": "Fehler bei der Markierung des Mediums als \"Nicht Beendet\"",
"ToastPlaylistCreateFailed": "Erstellen der Wiedergabeliste fehlgeschlagen", "ToastPlaylistCreateFailed": "Erstellen der Wiedergabeliste fehlgeschlagen",
"ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden", "ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden",
"ToastPodcastCreateSuccess": "Podcast erstellt", "ToastPodcastCreateSuccess": "Podcast erstellt",
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden", "ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen" "ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
} "ToastStreamingNotAllowedOnCellular": "Das Streamen über mobile Daten ist nicht erlaubt"
}
+28 -9
View File
@@ -19,6 +19,7 @@
"ButtonDeleteLocalItem": "Delete local item", "ButtonDeleteLocalItem": "Delete local item",
"ButtonDisableAutoTimer": "Disable Auto Timer", "ButtonDisableAutoTimer": "Disable Auto Timer",
"ButtonDisconnect": "Disconnect", "ButtonDisconnect": "Disconnect",
"ButtonGoToWebClient": "Go to Web Client",
"ButtonHistory": "History", "ButtonHistory": "History",
"ButtonHome": "Home", "ButtonHome": "Home",
"ButtonIssues": "Issues", "ButtonIssues": "Issues",
@@ -32,6 +33,7 @@
"ButtonOverride": "Override", "ButtonOverride": "Override",
"ButtonPause": "Pause", "ButtonPause": "Pause",
"ButtonPlay": "Play", "ButtonPlay": "Play",
"ButtonPlayEpisode": "Play Episode",
"ButtonPlaying": "Playing", "ButtonPlaying": "Playing",
"ButtonPlaylists": "Playlists", "ButtonPlaylists": "Playlists",
"ButtonRead": "Read", "ButtonRead": "Read",
@@ -55,6 +57,7 @@
"HeaderCollection": "Collection", "HeaderCollection": "Collection",
"HeaderCollectionItems": "Collection Items", "HeaderCollectionItems": "Collection Items",
"HeaderConnectionStatus": "Connection Status", "HeaderConnectionStatus": "Connection Status",
"HeaderDataSettings": "Data Settings",
"HeaderDetails": "Details", "HeaderDetails": "Details",
"HeaderDownloads": "Downloads", "HeaderDownloads": "Downloads",
"HeaderEbookFiles": "Ebook Files", "HeaderEbookFiles": "Ebook Files",
@@ -81,10 +84,13 @@
"HeaderTableOfContents": "Table of Contents", "HeaderTableOfContents": "Table of Contents",
"HeaderUserInterfaceSettings": "User Interface Settings", "HeaderUserInterfaceSettings": "User Interface Settings",
"HeaderYourStats": "Your Stats", "HeaderYourStats": "Your Stats",
"LabelAddToPlaylist": "Add to Playlist",
"LabelAdded": "Added", "LabelAdded": "Added",
"LabelAddedAt": "Added At", "LabelAddedAt": "Added At",
"LabelAddToPlaylist": "Add to Playlist",
"LabelAll": "All", "LabelAll": "All",
"LabelAllowSeekingOnMediaControls": "Allow position seeking on media notification controls",
"LabelAlways": "Always",
"LabelAskConfirmation": "Ask for confirmation",
"LabelAuthor": "Author", "LabelAuthor": "Author",
"LabelAuthorFirstLast": "Author (First Last)", "LabelAuthorFirstLast": "Author (First Last)",
"LabelAuthorLastFirst": "Author (Last, First)", "LabelAuthorLastFirst": "Author (Last, First)",
@@ -96,8 +102,8 @@
"LabelAutoSleepTimerAutoRewindHelp": "When the auto sleep timer finishes, playing the item again will automatically rewind your position.", "LabelAutoSleepTimerAutoRewindHelp": "When the auto sleep timer finishes, playing the item again will automatically rewind your position.",
"LabelAutoSleepTimerHelp": "When playing media between the specified start and end times a sleep timer will automatically start.", "LabelAutoSleepTimerHelp": "When playing media between the specified start and end times a sleep timer will automatically start.",
"LabelBooks": "Books", "LabelBooks": "Books",
"LabelChapters": "Chapters",
"LabelChapterTrack": "Chapter Track", "LabelChapterTrack": "Chapter Track",
"LabelChapters": "Chapters",
"LabelClosePlayer": "Close player", "LabelClosePlayer": "Close player",
"LabelCollapseSeries": "Collapse Series", "LabelCollapseSeries": "Collapse Series",
"LabelComplete": "Complete", "LabelComplete": "Complete",
@@ -117,6 +123,7 @@
"LabelDisableVibrateOnResetHelp": "When the sleep timer gets reset your device will vibrate. Enable this setting to not vibrate when the sleep timer resets.", "LabelDisableVibrateOnResetHelp": "When the sleep timer gets reset your device will vibrate. Enable this setting to not vibrate when the sleep timer resets.",
"LabelDiscover": "Discover", "LabelDiscover": "Discover",
"LabelDownload": "Download", "LabelDownload": "Download",
"LabelDownloadUsingCellular": "Download using Cellular",
"LabelDownloaded": "Downloaded", "LabelDownloaded": "Downloaded",
"LabelDuration": "Duration", "LabelDuration": "Duration",
"LabelEbook": "Ebook", "LabelEbook": "Ebook",
@@ -135,6 +142,7 @@
"LabelFilename": "Filename", "LabelFilename": "Filename",
"LabelFinished": "Finished", "LabelFinished": "Finished",
"LabelFolder": "Folder", "LabelFolder": "Folder",
"LabelFontBoldness": "Font Boldness",
"LabelFontScale": "Font scale", "LabelFontScale": "Font scale",
"LabelGenre": "Genre", "LabelGenre": "Genre",
"LabelGenres": "Genres", "LabelGenres": "Genres",
@@ -144,8 +152,8 @@
"LabelHeavy": "Heavy", "LabelHeavy": "Heavy",
"LabelHigh": "High", "LabelHigh": "High",
"LabelHost": "Host", "LabelHost": "Host",
"LabelIncomplete": "Incomplete",
"LabelInProgress": "In Progress", "LabelInProgress": "In Progress",
"LabelIncomplete": "Incomplete",
"LabelInternalAppStorage": "Internal App Storage", "LabelInternalAppStorage": "Internal App Storage",
"LabelJumpBackwardsTime": "Jump backwards time", "LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time", "LabelJumpForwardsTime": "Jump forwards time",
@@ -168,6 +176,7 @@
"LabelName": "Name", "LabelName": "Name",
"LabelNarrator": "Narrator", "LabelNarrator": "Narrator",
"LabelNarrators": "Narrators", "LabelNarrators": "Narrators",
"LabelNever": "Never",
"LabelNewestAuthors": "Newest Authors", "LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes", "LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No", "LabelNo": "No",
@@ -186,14 +195,17 @@
"LabelProgress": "Progress", "LabelProgress": "Progress",
"LabelPubDate": "Pub Date", "LabelPubDate": "Pub Date",
"LabelPublishYear": "Publish Year", "LabelPublishYear": "Publish Year",
"LabelRead": "Read",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email", "LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
"LabelRSSFeedCustomOwnerName": "Custom owner Name", "LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedPreventIndexing": "Prevent Indexing", "LabelRSSFeedPreventIndexing": "Prevent Indexing",
"LabelRSSFeedSlug": "RSS Feed Slug", "LabelRSSFeedSlug": "RSS Feed Slug",
"LabelRandomly": "Randomly",
"LabelRead": "Read",
"LabelReadAgain": "Read Again",
"LabelRecentSeries": "Recent Series",
"LabelRecentlyAdded": "Recently Added",
"LabelRemoveFromPlaylist": "Remove from Playlist",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Season", "LabelSeason": "Season",
"LabelSelectADevice": "Select a device", "LabelSelectADevice": "Select a device",
"LabelSeries": "Series", "LabelSeries": "Series",
@@ -215,6 +227,7 @@
"LabelStatsMinutes": "minutes", "LabelStatsMinutes": "minutes",
"LabelStatsMinutesListening": "Minutes Listening", "LabelStatsMinutesListening": "Minutes Listening",
"LabelStatsWeekListening": "Week Listening", "LabelStatsWeekListening": "Week Listening",
"LabelStreamingUsingCellular": "Streaming using Cellular",
"LabelTag": "Tag", "LabelTag": "Tag",
"LabelTags": "Tags", "LabelTags": "Tags",
"LabelTheme": "Theme", "LabelTheme": "Theme",
@@ -222,6 +235,7 @@
"LabelThemeLight": "Light", "LabelThemeLight": "Light",
"LabelTimeRemaining": "{0} remaining", "LabelTimeRemaining": "{0} remaining",
"LabelTitle": "Title", "LabelTitle": "Title",
"LabelTotalSize": "Total Size",
"LabelTotalTrack": "Total Track", "LabelTotalTrack": "Total Track",
"LabelTracks": "Tracks", "LabelTracks": "Tracks",
"LabelType": "Type", "LabelType": "Type",
@@ -241,8 +255,10 @@
"MessageConfirmDeleteLocalEpisode": "Remove local episode \"{0}\" from your device? The file on the server will be unaffected.", "MessageConfirmDeleteLocalEpisode": "Remove local episode \"{0}\" from your device? The file on the server will be unaffected.",
"MessageConfirmDeleteLocalFiles": "Remove local files of this item from your device? The files on the server and your progress will be unaffected.", "MessageConfirmDeleteLocalFiles": "Remove local files of this item from your device? The files on the server and your progress will be unaffected.",
"MessageConfirmDiscardProgress": "Are you sure you want to reset your progress?", "MessageConfirmDiscardProgress": "Are you sure you want to reset your progress?",
"MessageConfirmDownloadUsingCellular": "You are about to download using cellular data. This may include carrier data charges. Do you wish to continue?",
"MessageConfirmMarkAsFinished": "Are you sure you want to mark this item as finished?", "MessageConfirmMarkAsFinished": "Are you sure you want to mark this item as finished?",
"MessageConfirmRemoveBookmark": "Are you sure you want to remove bookmark?", "MessageConfirmRemoveBookmark": "Are you sure you want to remove bookmark?",
"MessageConfirmStreamingUsingCellular": "You are about to stream using cellular data. This may include carrier data charges. Do you wish to continue?",
"MessageDiscardProgress": "Discard Progress", "MessageDiscardProgress": "Discard Progress",
"MessageDownloadCompleteProcessing": "Download complete. Processing...", "MessageDownloadCompleteProcessing": "Download complete. Processing...",
"MessageDownloading": "Downloading...", "MessageDownloading": "Downloading...",
@@ -268,6 +284,7 @@
"MessageNoMediaFolders": "No Media Folders", "MessageNoMediaFolders": "No Media Folders",
"MessageNoNetworkConnection": "No network connection", "MessageNoNetworkConnection": "No network connection",
"MessageNoPodcastsFound": "No podcasts found", "MessageNoPodcastsFound": "No podcasts found",
"MessageNoSeries": "No series",
"MessageNoUpdatesWereNecessary": "No updates were necessary", "MessageNoUpdatesWereNecessary": "No updates were necessary",
"MessageNoUserPlaylists": "You have no playlists", "MessageNoUserPlaylists": "You have no playlists",
"MessageReportBugsAndContribute": "Report bugs, request features, and contribute on", "MessageReportBugsAndContribute": "Report bugs, request features, and contribute on",
@@ -281,11 +298,13 @@
"ToastBookmarkCreateFailed": "Failed to create bookmark", "ToastBookmarkCreateFailed": "Failed to create bookmark",
"ToastBookmarkRemoveFailed": "Failed to remove bookmark", "ToastBookmarkRemoveFailed": "Failed to remove bookmark",
"ToastBookmarkUpdateFailed": "Failed to update bookmark", "ToastBookmarkUpdateFailed": "Failed to update bookmark",
"ToastDownloadNotAllowedOnCellular": "Downloading is not allowed on cellular data",
"ToastItemMarkedAsFinishedFailed": "Failed to mark as Finished", "ToastItemMarkedAsFinishedFailed": "Failed to mark as Finished",
"ToastItemMarkedAsNotFinishedFailed": "Failed to mark as Not Finished", "ToastItemMarkedAsNotFinishedFailed": "Failed to mark as Not Finished",
"ToastPlaylistCreateFailed": "Failed to create playlist", "ToastPlaylistCreateFailed": "Failed to create playlist",
"ToastPodcastCreateFailed": "Failed to create podcast", "ToastPodcastCreateFailed": "Failed to create podcast",
"ToastPodcastCreateSuccess": "Podcast created successfully", "ToastPodcastCreateSuccess": "Podcast created successfully",
"ToastRSSFeedCloseFailed": "Failed to close RSS feed", "ToastRSSFeedCloseFailed": "Failed to close RSS feed",
"ToastRSSFeedCloseSuccess": "RSS feed closed" "ToastRSSFeedCloseSuccess": "RSS feed closed",
} "ToastStreamingNotAllowedOnCellular": "Streaming is not allowed on cellular data"
}
+195 -177
View File
@@ -1,209 +1,220 @@
{ {
"ButtonAdd": "Agregar", "ButtonAdd": "Agregar",
"ButtonAddNewServer": "Add New Server", "ButtonAddNewServer": "Agregar nuevo servidor",
"ButtonAuthors": "Autores", "ButtonAuthors": "Autores",
"ButtonBack": "Back", "ButtonBack": "Atrás",
"ButtonCancel": "Cancelar", "ButtonCancel": "Cancelar",
"ButtonCancelTimer": "Cancel Timer", "ButtonCancelTimer": "Cancelar el temporizador",
"ButtonClearFilter": "Quitar Filtros", "ButtonClearFilter": "Quitar filtros",
"ButtonCloseFeed": "Cerrar Fuente", "ButtonCloseFeed": "Cerrar fuente",
"ButtonCollections": "Colecciones", "ButtonCollections": "Colecciones",
"ButtonConnect": "Connect", "ButtonConnect": "Conectar",
"ButtonConnectToServer": "Connect to Server", "ButtonConnectToServer": "Conectar al servidor",
"ButtonCreate": "Crear", "ButtonCreate": "Crear",
"ButtonCreateBookmark": "Create Bookmark", "ButtonCreateBookmark": "Crear marcador",
"ButtonCreateNewPlaylist": "Create New Playlist", "ButtonCreateNewPlaylist": "Crear nueva lista de reproducción",
"ButtonDelete": "Eliminar", "ButtonDelete": "Eliminar",
"ButtonDeleteLocalEpisode": "Delete local episode", "ButtonDeleteLocalEpisode": "Borrar episodio local",
"ButtonDeleteLocalFile": "Delete local file", "ButtonDeleteLocalFile": "Borrar archivo local",
"ButtonDeleteLocalItem": "Delete local item", "ButtonDeleteLocalItem": "Borrar elemento local",
"ButtonDisableAutoTimer": "Disable Auto Timer", "ButtonDisableAutoTimer": "Desactivar temporizador automático",
"ButtonDisconnect": "Disconnect", "ButtonDisconnect": "Desconectar",
"ButtonHistory": "History", "ButtonGoToWebClient": "Ir al cliente web",
"ButtonHistory": "Historial",
"ButtonHome": "Inicio", "ButtonHome": "Inicio",
"ButtonIssues": "Problemas", "ButtonIssues": "Problemas",
"ButtonLatest": "Últimos", "ButtonLatest": "Últimos",
"ButtonLibrary": "Biblioteca", "ButtonLibrary": "Biblioteca",
"ButtonLocalMedia": "Local Media", "ButtonLocalMedia": "Medios locales",
"ButtonManageLocalFiles": "Manage Local Files", "ButtonManageLocalFiles": "Gestionar archivos locales",
"ButtonNewFolder": "New Folder", "ButtonNewFolder": "Nueva carpeta",
"ButtonNextEpisode": "Next Episode", "ButtonNextEpisode": "Próximo episodio",
"ButtonOpenFeed": "Abrir Fuente", "ButtonOpenFeed": "Abrir fuente",
"ButtonOverride": "Override", "ButtonOverride": "Sustituir",
"ButtonPause": "Pause", "ButtonPause": "Pausar",
"ButtonPlay": "Reproducir", "ButtonPlay": "Reproducir",
"ButtonPlayEpisode": "Reproducir episodio",
"ButtonPlaying": "Reproduciendo", "ButtonPlaying": "Reproduciendo",
"ButtonPlaylists": "Listas de Reproducción", "ButtonPlaylists": "Listas de reproducción",
"ButtonRead": "Leer", "ButtonRead": "Leer",
"ButtonRemove": "Remover", "ButtonRemove": "Eliminar",
"ButtonRemoveFromServer": "Remove from Server", "ButtonRemoveFromServer": "Eliminar del Servidor",
"ButtonSave": "Guardar", "ButtonSave": "Guardar",
"ButtonSaveOrder": "Save Order", "ButtonSaveOrder": "Guardar pedido",
"ButtonSearch": "Buscar", "ButtonSearch": "Buscar",
"ButtonSendEbookToDevice": "Send Ebook to Device", "ButtonSendEbookToDevice": "Enviar libro al dispositivo",
"ButtonSeries": "Series", "ButtonSeries": "Series",
"ButtonSetTimer": "Set Timer", "ButtonSetTimer": "Ajustar el temporizador",
"ButtonStream": "Stream", "ButtonStream": "Stream",
"ButtonSubmit": "Enviar", "ButtonSubmit": "Enviar",
"ButtonSwitchServerUser": "Switch Server/User", "ButtonSwitchServerUser": "Cambiar servidor/usuario",
"ButtonUserStats": "User Stats", "ButtonUserStats": "Estadísticas de usuario",
"ButtonYes": "Aceptar", "ButtonYes": "",
"HeaderAccount": "Cuenta", "HeaderAccount": "Cuenta",
"HeaderAdvanced": "Avanzado", "HeaderAdvanced": "Avanzado",
"HeaderAudioTracks": "Pistas de Audio", "HeaderAudioTracks": "Pistas de audio",
"HeaderChapters": "Capítulos", "HeaderChapters": "Capítulos",
"HeaderCollection": "Colección", "HeaderCollection": "Colección",
"HeaderCollectionItems": "Elementos en la Colección", "HeaderCollectionItems": "Elementos en la colección",
"HeaderConnectionStatus": "Connection Status", "HeaderConnectionStatus": "Estado de la conexión",
"HeaderDataSettings": "Ajustes de los datos",
"HeaderDetails": "Detalles", "HeaderDetails": "Detalles",
"HeaderDownloads": "Downloads", "HeaderDownloads": "Descargas",
"HeaderEbookFiles": "Archivos de Ebook", "HeaderEbookFiles": "Archivos de libros digitales",
"HeaderEpisodes": "Episodios", "HeaderEpisodes": "Episodios",
"HeaderEreaderSettings": "Opciones de Ereader", "HeaderEreaderSettings": "Ajustes del lector",
"HeaderLatestEpisodes": "Últimos Episodios", "HeaderLatestEpisodes": "Últimos episodios",
"HeaderLibraries": "Bibliotecas", "HeaderLibraries": "Bibliotecas",
"HeaderLocalFolders": "Local Folders", "HeaderLocalFolders": "Carpetas locales",
"HeaderLocalLibraryItems": "Local Library Items", "HeaderLocalLibraryItems": "Elementos de la biblioteca local",
"HeaderNewPlaylist": "New Playlist", "HeaderNewPlaylist": "Nueva lista de reproducción",
"HeaderOpenRSSFeed": "Abrir fuente RSS", "HeaderOpenRSSFeed": "Abrir fuente RSS",
"HeaderPlaybackSettings": "Playback Settings", "HeaderPlaybackSettings": "Ajustes de reproducción",
"HeaderPlaylist": "Lista de Reproducción", "HeaderPlaylist": "Lista de reproducción",
"HeaderPlaylistItems": "Elementos de Lista de Reproducción", "HeaderPlaylistItems": "Elementos de lista de reproducción",
"HeaderRSSFeed": "RSS Feed", "HeaderRSSFeed": "Fuente RSS",
"HeaderRSSFeedGeneral": "Detalles RSS", "HeaderRSSFeedGeneral": "Detalles RSS",
"HeaderRSSFeedIsOpen": "Fuente RSS esta abierta", "HeaderRSSFeedIsOpen": "Fuente RSS está abierta",
"HeaderSelectDownloadLocation": "Select Download Location", "HeaderSelectDownloadLocation": "Seleccionar ubicación de descarga",
"HeaderSettings": "Configuraciones", "HeaderSettings": "Configuraciones",
"HeaderSleepTimer": "Temporizador para Dormir", "HeaderSleepTimer": "Temporizador de apagado",
"HeaderSleepTimerSettings": "Sleep Timer Settings", "HeaderSleepTimerSettings": "Ajustes del temporizador para dormir",
"HeaderStatsMinutesListeningChart": "Minutos Escuchando (Últimos 7 días)", "HeaderStatsMinutesListeningChart": "Minutos escuchando (Últimos 7 días)",
"HeaderStatsRecentSessions": "Sesiones Recientes", "HeaderStatsRecentSessions": "Sesiones recientes",
"HeaderTableOfContents": "Tabla de Contenidos", "HeaderTableOfContents": "Tabla de contenidos",
"HeaderUserInterfaceSettings": "User Interface Settings", "HeaderUserInterfaceSettings": "Ajustes de la interfaz de usuario",
"HeaderYourStats": "Tus Estadísticas", "HeaderYourStats": "Tus estadísticas",
"LabelAddToPlaylist": "Añadido a la lista de reproducción",
"LabelAdded": "Añadido", "LabelAdded": "Añadido",
"LabelAddedAt": "Añadido", "LabelAddedAt": "Añadido",
"LabelAddToPlaylist": "Añadido a la Lista de Reproducción",
"LabelAll": "Todos", "LabelAll": "Todos",
"LabelAllowSeekingOnMediaControls": "Permitir la búsqueda de posición en los controles de notificación de medios",
"LabelAlways": "Siempre",
"LabelAskConfirmation": "Pedir confirmación",
"LabelAuthor": "Autor", "LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Nombre Apellido)", "LabelAuthorFirstLast": "Autor (Nombre Apellido)",
"LabelAuthorLastFirst": "Autor (Apellido, Nombre)", "LabelAuthorLastFirst": "Autor (Apellido, Nombre)",
"LabelAuthors": "Autores", "LabelAuthors": "Autores",
"LabelAutoDownloadEpisodes": "Descargar Episodios Automáticamente", "LabelAutoDownloadEpisodes": "Descargar episodios automáticamente",
"LabelAutoRewindTime": "Auto rewind time", "LabelAutoRewindTime": "Tiempo de rebobinado automático",
"LabelAutoSleepTimer": "Auto sleep timer", "LabelAutoSleepTimer": "Temporizador de apagado automático",
"LabelAutoSleepTimerAutoRewind": "Auto sleep timer auto rewind", "LabelAutoSleepTimerAutoRewind": "Temporizador de apagado automático con rebobinado automático",
"LabelAutoSleepTimerAutoRewindHelp": "When the auto sleep timer finishes, playing the item again will automatically rewind your position.", "LabelAutoSleepTimerAutoRewindHelp": "Cuando el temporizador de auto apagado finaliza, reproducir el elemento nuevamente rebobinará automáticamente tu posición.",
"LabelAutoSleepTimerHelp": "When playing media between the specified start and end times a sleep timer will automatically start.", "LabelAutoSleepTimerHelp": "Cuando se reproduce contenido multimedia entre las horas de inicio y finalización especificadas, se activará automáticamente un temporizador de apagado.",
"LabelBooks": "Libros", "LabelBooks": "Libros",
"LabelChapterTrack": "Seguimiento de Capítulo",
"LabelChapters": "Capítulos", "LabelChapters": "Capítulos",
"LabelChapterTrack": "Chapter Track", "LabelClosePlayer": "Cerrar reproductor",
"LabelClosePlayer": "Cerrar Reproductor", "LabelCollapseSeries": "Colapsar serie",
"LabelCollapseSeries": "Colapsar Serie",
"LabelComplete": "Completo", "LabelComplete": "Completo",
"LabelContinueBooks": "Continue Books", "LabelContinueBooks": "Continuar libros",
"LabelContinueEpisodes": "Continue Episodes", "LabelContinueEpisodes": "Continuar episodios",
"LabelContinueListening": "Continue Listening", "LabelContinueListening": "Seguir escuchando",
"LabelContinueReading": "Continue Reading", "LabelContinueReading": "Continuar leyendo",
"LabelContinueSeries": "Continue Series", "LabelContinueSeries": "Continuar series",
"LabelCustomTime": "Custom time", "LabelCustomTime": "Tiempo personalizado",
"LabelDescription": "Descripción", "LabelDescription": "Descripción",
"LabelDisableAudioFadeOut": "Disable audio fade out", "LabelDisableAudioFadeOut": "Desactivar el fundido de audio",
"LabelDisableAudioFadeOutHelp": "Audio volume will start decreasing when there is less than 1 minute remaining on the sleep timer. Enable this setting to not fade out.", "LabelDisableAudioFadeOutHelp": "El volumen de audio comenzará a disminuir cuando quede menos de 1 minuto en el temporizador de apagado. Habilite este ajuste para que no se desvanezca.",
"LabelDisableAutoRewind": "Disable auto rewind", "LabelDisableAutoRewind": "Desactivar rebobinado automático",
"LabelDisableShakeToReset": "Disable shake to reset", "LabelDisableShakeToReset": "Desactivar agitar para reiniciar",
"LabelDisableShakeToResetHelp": "Shaking your device while the timer is running OR within 2 minutes of the timer expiring will reset the sleep timer. Enable this setting to disable shake to reset.", "LabelDisableShakeToResetHelp": "Si agitas el dispositivo mientras el temporizador está en marcha o en los 2 minutos siguientes a la expiración del temporizador, éste se reiniciará. Habilita esta configuración para desactivar el restablecimiento al agitar.",
"LabelDisableVibrateOnReset": "Disable vibrate on reset", "LabelDisableVibrateOnReset": "Desactivar vibración al reiniciar",
"LabelDisableVibrateOnResetHelp": "When the sleep timer gets reset your device will vibrate. Enable this setting to not vibrate when the sleep timer resets.", "LabelDisableVibrateOnResetHelp": "Cuando el temporizador de apagado se reinicia, el dispositivo vibra. Activa esta opción para que no vibre cuando se reinicie el temporizador.",
"LabelDiscover": "Discover", "LabelDiscover": "Descubrir",
"LabelDownload": "Descargar", "LabelDownload": "Descargar",
"LabelDownloaded": "Downloaded", "LabelDownloadUsingCellular": "Descargar usando el móvil",
"LabelDownloaded": "Descargado",
"LabelDuration": "Duración", "LabelDuration": "Duración",
"LabelEbook": "Ebook", "LabelEbook": "Libro electrónico",
"LabelEbooks": "Ebooks", "LabelEbooks": "Libros electrónicos",
"LabelEnable": "Habilitar", "LabelEnable": "Habilitar",
"LabelEnableMp3IndexSeeking": "Enable mp3 index seeking", "LabelEnableMp3IndexSeeking": "Activar la búsqueda de índices mp3",
"LabelEnableMp3IndexSeekingHelp": "This setting should only be enabled if you have mp3 files that are not seeking correctly. Inaccurate seeking is most likely due to Variable birate (VBR) MP3 files. This setting will force index seeking, in which a time-to-byte mapping is built as the file is read. In some cases with large MP3 files there will be a delay when seeking towards the end of the file.", "LabelEnableMp3IndexSeekingHelp": "Esta configuración solo debe habilitarse si tienes archivos mp3 que no se están buscando correctamente. La búsqueda inexacta probablemente se deba a archivos MP3 de tasa de bits variable (VBR). Esta configuración forzará la búsqueda de índice, en la que se construye un mapeo de tiempo a bytes mientras se lee el archivo. En algunos casos, con archivos MP3 grandes, puede haber un retraso al buscar hacia el final del archivo.",
"LabelEnd": "Fin", "LabelEnd": "Fin",
"LabelEndOfChapter": "End of Chapter", "LabelEndOfChapter": "Fin del capítulo",
"LabelEndTime": "End time", "LabelEndTime": "Hora de finalización",
"LabelEpisode": "Episodio", "LabelEpisode": "Episodio",
"LabelFeedURL": "Fuente de URL", "LabelFeedURL": "Fuente de URL",
"LabelFile": "Archivo", "LabelFile": "Archivo",
"LabelFileBirthtime": "Archivo Creado en", "LabelFileBirthtime": "Archivo creado en",
"LabelFileModified": "Archivo modificado", "LabelFileModified": "Archivo modificado",
"LabelFilename": "Nombre del Archivo", "LabelFilename": "Nombre del archivo",
"LabelFinished": "Terminado", "LabelFinished": "Terminado",
"LabelFolder": "Carpeta", "LabelFolder": "Carpeta",
"LabelFontScale": "Tamaño de Fuente", "LabelFontBoldness": "Nivel de negrilla en fuente",
"LabelGenre": "Genero", "LabelFontScale": "Tamaño de fuente",
"LabelGenre": "Género",
"LabelGenres": "Géneros", "LabelGenres": "Géneros",
"LabelHapticFeedback": "Haptic feedback", "LabelHapticFeedback": "Respuesta táctil",
"LabelHasEbook": "Has ebook", "LabelHasEbook": "Tiene un libro",
"LabelHasSupplementaryEbook": "Has supplementary ebook", "LabelHasSupplementaryEbook": "Tiene un libro complementario",
"LabelHeavy": "Heavy", "LabelHeavy": "Pesado",
"LabelHigh": "High", "LabelHigh": "Alto",
"LabelHost": "Host", "LabelHost": "Host",
"LabelInProgress": "En proceso",
"LabelIncomplete": "Incompleto", "LabelIncomplete": "Incompleto",
"LabelInProgress": "En Proceso", "LabelInternalAppStorage": "Almacenamiento interno de aplicaciones",
"LabelInternalAppStorage": "Internal App Storage", "LabelJumpBackwardsTime": "Saltar atrás en el tiempo",
"LabelJumpBackwardsTime": "Jump backwards time", "LabelJumpForwardsTime": "Salto adelante en el tiempo",
"LabelJumpForwardsTime": "Jump forwards time", "LabelLanguage": "Idioma",
"LabelLanguage": "Lenguaje", "LabelLayout": "Diseño",
"LabelLayout": "Layout",
"LabelLayoutAuto": "Auto", "LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page", "LabelLayoutSinglePage": "Página única",
"LabelLight": "Light", "LabelLight": "Claro",
"LabelLineSpacing": "Interlineado", "LabelLineSpacing": "Interlineado",
"LabelListenAgain": "Listen Again", "LabelListenAgain": "Volver a escuchar",
"LabelLocalBooks": "Local Books", "LabelLocalBooks": "Libros Locales",
"LabelLocalPodcasts": "Local Podcasts", "LabelLocalPodcasts": "Podcasts Locales",
"LabelLockOrientation": "Lock orientation", "LabelLockOrientation": "Bloquear orientación",
"LabelLockPlayer": "Lock Player", "LabelLockPlayer": "Bloquear el reproductor",
"LabelLow": "Low", "LabelLow": "Bajo",
"LabelMediaType": "Tipo de Multimedia", "LabelMediaType": "Tipo de multimedia",
"LabelMedium": "Medium", "LabelMedium": "Medio",
"LabelMore": "Más", "LabelMore": "Más",
"LabelMoreInfo": "Más Información", "LabelMoreInfo": "Más información",
"LabelName": "Nombre", "LabelName": "Nombre",
"LabelNarrator": "Narrador", "LabelNarrator": "Narrador",
"LabelNarrators": "Narradores", "LabelNarrators": "Narradores",
"LabelNewestAuthors": "Newest Authors", "LabelNever": "Nunca",
"LabelNewestEpisodes": "Newest Episodes", "LabelNewestAuthors": "Autores más recientes",
"LabelNewestEpisodes": "Episodios más recientes",
"LabelNo": "No", "LabelNo": "No",
"LabelNotFinished": "No Terminado", "LabelNotFinished": "No terminado",
"LabelNotStarted": "Sin Iniciar", "LabelNotStarted": "Sin iniciar",
"LabelOff": "Off", "LabelOff": "Apagado",
"LabelPassword": "Contraseña", "LabelPassword": "Contraseña",
"LabelPath": "Ruta de Carpeta", "LabelPath": "Ruta de carpeta",
"LabelPlaybackDirect": "Direct", "LabelPlaybackDirect": "Directo",
"LabelPlaybackLocal": "Local", "LabelPlaybackLocal": "Local",
"LabelPlaybackSpeed": "Playback Speed", "LabelPlaybackSpeed": "Velocidad de reproducción",
"LabelPlaybackTranscode": "Transcode", "LabelPlaybackTranscode": "Transcodificar",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPreventIndexing": "Evite que su fuente sea indexada por los directorios de podcasts de iTunes y Google", "LabelPreventIndexing": "Evite que su fuente sea indexada por los directorios de podcasts de iTunes y Google",
"LabelProgress": "Progreso", "LabelProgress": "Progreso",
"LabelPubDate": "Fecha de Publicación", "LabelPubDate": "Fecha de publicación",
"LabelPublishYear": "Año de Publicación", "LabelPublishYear": "Año de publicación",
"LabelRead": "Leído", "LabelRSSFeedCustomOwnerEmail": "Correo electrónico de dueño personalizado",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Email de dueño personalizado",
"LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado", "LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado",
"LabelRSSFeedPreventIndexing": "Prevenir Indexado", "LabelRSSFeedPreventIndexing": "Prevenir indexado",
"LabelRSSFeedSlug": "Fuente RSS Slug", "LabelRSSFeedSlug": "Fuente RSS Slug",
"LabelRead": "Leído",
"LabelReadAgain": "Volver a leer",
"LabelRecentSeries": "Series Recientes",
"LabelRecentlyAdded": "Añadido Recientemente",
"LabelRemoveFromPlaylist": "Eliminar de la Lista de Reproducción",
"LabelScaleElapsedTimeBySpeed": "Escala el tiempo transcurrido según la velocidad",
"LabelSeason": "Temporada", "LabelSeason": "Temporada",
"LabelSelectADevice": "Select a device", "LabelSelectADevice": "Selecciona un dispositivo",
"LabelSeries": "Series", "LabelSeries": "Series",
"LabelServerAddress": "Server address", "LabelServerAddress": "Dirección del servidor",
"LabelSetEbookAsPrimary": "Establecer como primario", "LabelSetEbookAsPrimary": "Establecer como primario",
"LabelSetEbookAsSupplementary": "Establecer como suplementario", "LabelSetEbookAsSupplementary": "Establecer como suplementario",
"LabelShakeSensitivity": "Shake sensitivity", "LabelShakeSensitivity": "Sensibilidad a la sacudida",
"LabelShowAll": "Mostrar Todos", "LabelShowAll": "Mostrar Todos",
"LabelSize": "Tamaño", "LabelSize": "Tamaño",
"LabelSleepTimer": "Temporizador para Dormir", "LabelSleepTimer": "Temporizador de apagado",
"LabelStart": "Iniciar", "LabelStart": "Iniciar",
"LabelStartTime": "Tiempo de Inicio", "LabelStartTime": "Tiempo de Inicio",
"LabelStatsBestDay": "Mejor Día", "LabelStatsBestDay": "Mejor Día",
@@ -215,6 +226,7 @@
"LabelStatsMinutes": "minutos", "LabelStatsMinutes": "minutos",
"LabelStatsMinutesListening": "Minutos Escuchando", "LabelStatsMinutesListening": "Minutos Escuchando",
"LabelStatsWeekListening": "Tiempo escuchando en la Semana", "LabelStatsWeekListening": "Tiempo escuchando en la Semana",
"LabelStreamingUsingCellular": "Streaming por móvil",
"LabelTag": "Etiqueta", "LabelTag": "Etiqueta",
"LabelTags": "Etiquetas", "LabelTags": "Etiquetas",
"LabelTheme": "Tema", "LabelTheme": "Tema",
@@ -222,70 +234,76 @@
"LabelThemeLight": "Claro", "LabelThemeLight": "Claro",
"LabelTimeRemaining": "{0} restante", "LabelTimeRemaining": "{0} restante",
"LabelTitle": "Título", "LabelTitle": "Título",
"LabelTotalTrack": "Total Track", "LabelTotalSize": "Tamaño Total",
"LabelTotalTrack": "Pista Total",
"LabelTracks": "Pistas", "LabelTracks": "Pistas",
"LabelType": "Tipo", "LabelType": "Tipo",
"LabelUnlockPlayer": "Unlock Player", "LabelUnlockPlayer": "Desbloquear Reproductor",
"LabelUseBookshelfView": "Use bookshelf view", "LabelUseBookshelfView": "Usar la Vista de Estantería",
"LabelUser": "Usuario", "LabelUser": "Usuario",
"LabelUsername": "Nombre de Usuario", "LabelUsername": "Nombre de Usuario",
"LabelVeryHigh": "Very High", "LabelVeryHigh": "Muy Alto",
"LabelVeryLow": "Very Low", "LabelVeryLow": "Muy Bajo",
"LabelYourBookmarks": "Tus Marcadores", "LabelYourBookmarks": "Tus Marcadores",
"LabelYourProgress": "Tu Progreso", "LabelYourProgress": "Tu Progreso",
"MessageAndroid10Downloads": "Android 10 and below will use internal app storage for downloads.", "MessageAndroid10Downloads": "Android 10 e inferiores utilizarán el almacenamiento interno de aplicaciones para las descargas.",
"MessageAttemptingServerConnection": "Attempting server connection...", "MessageAttemptingServerConnection": "Intentando conectar con el servidor...",
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf server not connected", "MessageAudiobookshelfServerNotConnected": "Servidor de Audiobookshelf no conectado",
"MessageAudiobookshelfServerRequired": "<strong>Important!</strong> This app is designed to work with an Audiobookshelf server that you or someone you know is hosting. This app does not provide any content.", "MessageAudiobookshelfServerRequired": "<strong>¡Importante!</strong> Esta aplicación está diseñada para trabajar con un servidor Audiobookshelf que usted o alguien que usted conoce es el anfitrión. Esta aplicación no proporciona ningún contenido.",
"MessageBookshelfEmpty": "Bookshelf empty", "MessageBookshelfEmpty": "Estantería vacía",
"MessageConfirmDeleteLocalEpisode": "Remove local episode \"{0}\" from your device? The file on the server will be unaffected.", "MessageConfirmDeleteLocalEpisode": "¿Eliminar episodio local \"{0}\" de su dispositivo? El archivo en el servidor no se verá afectado.",
"MessageConfirmDeleteLocalFiles": "Remove local files of this item from your device? The files on the server and your progress will be unaffected.", "MessageConfirmDeleteLocalFiles": "¿Eliminar los archivos locales de este elemento de tu dispositivo? Los archivos del servidor y tu progreso no se verán afectados.",
"MessageConfirmDiscardProgress": "Are you sure you want to reset your progress?", "MessageConfirmDiscardProgress": "¿Estás seguro de que quieres reiniciar tu progreso?",
"MessageConfirmMarkAsFinished": "Are you sure you want to mark this item as finished?", "MessageConfirmDownloadUsingCellular": "Estas a punto de realizar una descarga utilizando datos móviles. Esto puede incluir cargos de datos del operador. ¿Deseas continuar?",
"MessageConfirmRemoveBookmark": "Are you sure you want to remove bookmark?", "MessageConfirmMarkAsFinished": "¿Está seguro de que desea marcar este artículo como terminado?",
"MessageDiscardProgress": "Discard Progress", "MessageConfirmRemoveBookmark": "¿Estás seguro de que quieres eliminar el marcador?",
"MessageDownloadCompleteProcessing": "Download complete. Processing...", "MessageConfirmStreamingUsingCellular": "Estás a punto de hacer streaming utilizando datos móviles. Esto puede incluir cargos de datos del operador. ¿Deseas continuar?",
"MessageDownloading": "Downloading...", "MessageDiscardProgress": "Descartar Progreso",
"MessageDownloadCompleteProcessing": "Descarga Completada. Procesando...",
"MessageDownloading": "Descargando...",
"MessageDownloadingEpisode": "Descargando Capitulo", "MessageDownloadingEpisode": "Descargando Capitulo",
"MessageEpisodesQueuedForDownload": "{0} Episodio(s) en cola para descargar", "MessageEpisodesQueuedForDownload": "{0} Episodio(s) en cola para descargar",
"MessageFeedURLWillBe": "URL de la fuente será {0}", "MessageFeedURLWillBe": "URL de la fuente será {0}",
"MessageFetching": "Buscando...", "MessageFetching": "Buscando...",
"MessageFollowTheProjectOnGithub": "Follow the project on Github", "MessageFollowTheProjectOnGithub": "Sigue el proyecto en GitHub",
"MessageItemDownloadCompleteFailedToCreate": "Item download complete but failed to create library item", "MessageItemDownloadCompleteFailedToCreate": "Se ha completado la descarga del elemento, pero no se ha podido crear el elemento de la biblioteca",
"MessageLoading": "Cargando...", "MessageLoading": "Cargando...",
"MessageLoadingServerData": "Loading server data...", "MessageLoadingServerData": "Cargando datos del servidor...",
"MessageMarkAsFinished": "Marcar como Terminado", "MessageMarkAsFinished": "Marcar como terminado",
"MessageMediaLinkedToADifferentServer": "Media is linked to an Audiobookshelf server on a different address ({0}). Progress will be synced when connected to this server address.", "MessageMediaLinkedToADifferentServer": "El contenido está vinculado a un servidor de Audiobookshelf en una dirección diferente ({0}). El progreso se sincronizará cuando se conecte a esta dirección de servidor.",
"MessageMediaLinkedToADifferentUser": "Media is linked to this server but was downloaded by a different user. Progress will only be synced to the user that downloaded it.", "MessageMediaLinkedToADifferentUser": "El contenido está vinculado a este servidor pero fue descargado por otro usuario. El progreso solo se sincronizará con el usuario que lo descargó.",
"MessageMediaLinkedToServer": "Linked to server {0}", "MessageMediaLinkedToServer": "Vinculado al servidor {0}",
"MessageMediaLinkedToThisServer": "Downloaded media is linked to this server", "MessageMediaLinkedToThisServer": "El contenido descargado está vinculado a este servidor",
"MessageMediaNotLinkedToServer": "Media is not linked to an Audiobookshelf server. No progress will be synced.", "MessageMediaNotLinkedToServer": "El contenido multimedia no está vinculado a un servidor de Audiobookshelf. No se sincronizará ningún progreso.",
"MessageNoBookmarks": "Sin Marcadores", "MessageNoBookmarks": "Sin marcadores",
"MessageNoChapters": "Sin Capítulos", "MessageNoChapters": "Sin capítulos",
"MessageNoItems": "Sin Elementos", "MessageNoItems": "Sin elementos",
"MessageNoItemsFound": "Ningún Elemento Encontrado", "MessageNoItemsFound": "Ningún elemento encontrado",
"MessageNoListeningSessions": "Ninguna Session Escuchada", "MessageNoListeningSessions": "Ninguna sesión escuchada",
"MessageNoMediaFolders": "No Media Folders", "MessageNoMediaFolders": "Sin carpetas multimedia",
"MessageNoNetworkConnection": "No network connection", "MessageNoNetworkConnection": "Sin conexión de red",
"MessageNoPodcastsFound": "Ningún podcast encontrado", "MessageNoPodcastsFound": "Ningún podcast encontrado",
"MessageNoSeries": "Ninguna serie",
"MessageNoUpdatesWereNecessary": "No fue necesario actualizar", "MessageNoUpdatesWereNecessary": "No fue necesario actualizar",
"MessageNoUserPlaylists": "No tienes lista de reproducciones", "MessageNoUserPlaylists": "No tienes ninguna lista de reproducción",
"MessageReportBugsAndContribute": "Reporte erres, solicite funciones y contribuya en", "MessageReportBugsAndContribute": "Reporte erres, solicite funciones y contribuya en",
"MessageSocketConnectedOverMeteredCellular": "Socket connected over metered cellular", "MessageSocketConnectedOverMeteredCellular": "Conexión de socket a través de red celular de tarifa por consumo",
"MessageSocketConnectedOverMeteredWifi": "Socket connected over metered wifi", "MessageSocketConnectedOverMeteredWifi": "Socket conectado a través de una red Wi-Fi con tarificación por consumo",
"MessageSocketConnectedOverUnmeteredCellular": "Socket connected over unmetered cellular", "MessageSocketConnectedOverUnmeteredCellular": "Socket conectado a través de red celular sin tarificación por consumo",
"MessageSocketConnectedOverUnmeteredWifi": "Socket connected over unmetered wifi", "MessageSocketConnectedOverUnmeteredWifi": "Socket conectado a través de una red Wi-Fi sin tarificación por consumo",
"MessageSocketNotConnected": "Socket not connected", "MessageSocketNotConnected": "Socket no conectado",
"NoteRSSFeedPodcastAppsHttps": "Advertencia: La mayoría de las aplicaciones de podcast requieren que la URL de la fuente RSS use HTTPS", "NoteRSSFeedPodcastAppsHttps": "Advertencia: La mayoría de las aplicaciones de podcast requieren que la URL de la fuente RSS use HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Advertencia: 1 o más de sus episodios no tienen fecha de publicación. Algunas aplicaciones de podcast lo requieren.", "NoteRSSFeedPodcastAppsPubDate": "Advertencia: 1 o más de sus episodios no tienen fecha de publicación. Algunas aplicaciones de podcast lo requieren.",
"ToastBookmarkCreateFailed": "Error al crear marcador", "ToastBookmarkCreateFailed": "Error al crear marcador",
"ToastBookmarkRemoveFailed": "Error al eliminar marcador", "ToastBookmarkRemoveFailed": "Error al eliminar marcador",
"ToastBookmarkUpdateFailed": "Error al actualizar el marcador", "ToastBookmarkUpdateFailed": "Error al actualizar el marcador",
"ToastItemMarkedAsFinishedFailed": "Error al marcar como Terminado", "ToastDownloadNotAllowedOnCellular": "Las descargas no están permitidas con datos móviles",
"ToastItemMarkedAsNotFinishedFailed": "Error al marcar como No Terminado", "ToastItemMarkedAsFinishedFailed": "Error al marcar como terminado",
"ToastPlaylistCreateFailed": "Error al crear la lista de reproducción.", "ToastItemMarkedAsNotFinishedFailed": "No se ha podido marcar como no finalizado",
"ToastPlaylistCreateFailed": "Error al crear la lista de reproducción",
"ToastPodcastCreateFailed": "Error al crear podcast", "ToastPodcastCreateFailed": "Error al crear podcast",
"ToastPodcastCreateSuccess": "Podcast creado", "ToastPodcastCreateSuccess": "Podcast creado",
"ToastRSSFeedCloseFailed": "Error al cerrar fuente RSS", "ToastRSSFeedCloseFailed": "Error al cerrar fuente RSS",
"ToastRSSFeedCloseSuccess": "Fuente RSS cerrada" "ToastRSSFeedCloseSuccess": "Fuente RSS cerrada",
} "ToastStreamingNotAllowedOnCellular": "El streaming no está permitido con datos móviles"
}
+226
View File
@@ -0,0 +1,226 @@
{
"ButtonAdd": "Lisää",
"ButtonAddNewServer": "Lisää uusi palvelin",
"ButtonAuthors": "Tekijät",
"ButtonBack": "Takaisin",
"ButtonCancel": "Peruuta",
"ButtonCancelTimer": "Peruuta ajastin",
"ButtonClearFilter": "Poista suodatus",
"ButtonCloseFeed": "Sulje syöte",
"ButtonCollections": "Kokoelmat",
"ButtonConnect": "Yhdistä",
"ButtonConnectToServer": "Yhdistä palvelimelle",
"ButtonCreate": "Luo",
"ButtonCreateBookmark": "Luo kirjanmerkki",
"ButtonCreateNewPlaylist": "Luo uusi soittolista",
"ButtonDelete": "Poista",
"ButtonDeleteLocalEpisode": "Poista paikallinen jakso",
"ButtonDeleteLocalFile": "Poista paikallinen tiedosto",
"ButtonDeleteLocalItem": "Poista paikallinen sisältö",
"ButtonDisableAutoTimer": "Sammuta automaattiajastin",
"ButtonDisconnect": "Katkaise yhteys",
"ButtonGoToWebClient": "Siirry Web-käyttöliittymään",
"ButtonHistory": "Historia",
"ButtonHome": "Koti",
"ButtonIssues": "Ongelmat",
"ButtonLatest": "Viimeisimmät",
"ButtonLibrary": "Kirjasto",
"ButtonLocalMedia": "Paikallinen media",
"ButtonManageLocalFiles": "Hallitse paikallsiia tiedostoja",
"ButtonNewFolder": "Uusi kansio",
"ButtonNextEpisode": "Seuraava jakso",
"ButtonOpenFeed": "Avaa syöte",
"ButtonOverride": "Ohita",
"ButtonPause": "Pysäytä",
"ButtonPlay": "Toista",
"ButtonPlayEpisode": "Toista jakso",
"ButtonPlaying": "Toistetaan",
"ButtonPlaylists": "Soittolistat",
"ButtonRead": "Lue",
"ButtonRemove": "Poista",
"ButtonRemoveFromServer": "Poista palvelimelta",
"ButtonSave": "Tallenna",
"ButtonSaveOrder": "Tallenna järjestys",
"ButtonSearch": "Etsi",
"ButtonSendEbookToDevice": "Lähetä kirja lukulaitteeseen",
"ButtonSeries": "Sarjat",
"ButtonSetTimer": "Aseta ajastin",
"ButtonStream": "Suoratoisto",
"ButtonSubmit": "Lähetä",
"ButtonSwitchServerUser": "Vaihda palvelinta/käyttäjää",
"ButtonUserStats": "Käyttäjän tilastot",
"ButtonYes": "Kyllä",
"HeaderAccount": "Tili",
"HeaderAdvanced": "Edistynyt",
"HeaderAudioTracks": "Ääniraidat",
"HeaderChapters": "Luvut",
"HeaderCollection": "Kokoelma",
"HeaderCollectionItems": "Kokoelman osat",
"HeaderConnectionStatus": "Kokoelman tila",
"HeaderDataSettings": "Tietojen asetukset",
"HeaderDetails": "Yksityiskohdat",
"HeaderDownloads": "Lataukset",
"HeaderEbookFiles": "E-kirjatiedostot",
"HeaderEpisodes": "Jaksot",
"HeaderEreaderSettings": "E-lukijan asetukset",
"HeaderLatestEpisodes": "Viimeisimmät jaksot",
"HeaderLibraries": "Kirjastot",
"HeaderLocalFolders": "Paikalliset kansiot",
"HeaderLocalLibraryItems": "Paikallisen kirjaston niteet",
"HeaderNewPlaylist": "Uusi soittolista",
"HeaderOpenRSSFeed": "Avaa RSS-syöte",
"HeaderPlaybackSettings": "Toistoasetukset",
"HeaderPlaylist": "Soittolista",
"HeaderPlaylistItems": "Soittolistan kohteet",
"HeaderRSSFeed": "RSS syöte",
"HeaderRSSFeedGeneral": "RSS yksityiskohdat",
"HeaderRSSFeedIsOpen": "RSS syöte on avoinna",
"HeaderSelectDownloadLocation": "Valitse tallennussijainti",
"HeaderSettings": "Asetukset",
"HeaderSleepTimer": "Uniajastin",
"HeaderSleepTimerSettings": "Uniajastimen asetukset",
"HeaderStatsMinutesListeningChart": "Kuunteluminuutit (viim. 7 pv)",
"HeaderStatsRecentSessions": "Viimeaikaiset istunnot",
"HeaderTableOfContents": "Sisällysluettelo",
"HeaderUserInterfaceSettings": "Käyttöliittymän asetukset",
"HeaderYourStats": "Tilastosi",
"LabelAddToPlaylist": "Lisää soittolistaan",
"LabelAdded": "Lisätty",
"LabelAddedAt": "Lisätty listalle",
"LabelAll": "Kaikki",
"LabelAllowSeekingOnMediaControls": "Salli kelaaminen ilmoituksen säätimillä",
"LabelAlways": "Aina",
"LabelAskConfirmation": "Kysy varmistusta",
"LabelAuthor": "Tekijä",
"LabelAuthorFirstLast": "Tekijä (Etunimi Sukunimi)",
"LabelAuthorLastFirst": "Tekijä (Sukunimi, Etunimi)",
"LabelAuthors": "Tekijät",
"LabelAutoDownloadEpisodes": "Lataa jaksot automaattisesti",
"LabelAutoRewindTime": "Automaattisen takaisinkelauksen kesto",
"LabelAutoSleepTimer": "Automaattinen uniajastin",
"LabelAutoSleepTimerAutoRewind": "Automaattisen uniajastimen automaattinen takaisinkelaus",
"LabelAutoSleepTimerAutoRewindHelp": "Kun automaattinen uniajastin päättyy, ensi kerralla toistetaan osa kuunenltua uudelleen.",
"LabelAutoSleepTimerHelp": "Toiston alkaessa tällä aikävälillä unikytkin kytketään automaattisesti käyttöön.",
"LabelBooks": "Kirjat",
"LabelChapterTrack": "Luvun raita",
"LabelChapters": "Luvut",
"LabelClosePlayer": "Sulje soitin",
"LabelCollapseSeries": "Pienennä sarja",
"LabelComplete": "Valmis",
"LabelContinueBooks": "Jatka kirjoja",
"LabelContinueEpisodes": "Jatka jaksoja",
"LabelContinueListening": "Jatka kuuntelua",
"LabelContinueReading": "Jatka lukemista",
"LabelContinueSeries": "Jatka sarjoja",
"LabelCustomTime": "Mukautettu aika",
"LabelDescription": "Kuvaus",
"LabelDisableAudioFadeOut": "Poista äänen häivytys käytöstä",
"LabelDisableAudioFadeOutHelp": "Äänenvoimakkuus alkaa laskea kun uniajastimessa on jäljellä alle minuutti. Laita tämä asetus pois päältä estääksesi sen.",
"LabelDisableAutoRewind": "Poista käytöstä automaattinen takaisinkelaus",
"LabelDisableShakeToReset": "Poista uniajastimnen nollaus ravistamalla käytöstä",
"LabelDisableShakeToResetHelp": "Ravistamalla laitettasi uniajastimen kuluessa tai enintään kaksi minuuttia sen loppumisen jälkeen uudistaa ajastimen.",
"LabelDownload": "Lataa",
"LabelDownloadUsingCellular": "Lataa käyttäen mobiilidataa",
"LabelDownloaded": "Ladatut",
"LabelDuration": "Kesto",
"LabelEbook": "E-kirja",
"LabelEbooks": "E-kirjat",
"LabelEnable": "Ota käyttöön",
"LabelEndOfChapter": "Luvun loppu",
"LabelEpisode": "Jakso",
"LabelFile": "Tiedosto",
"LabelFileBirthtime": "Tiedoston syntymäaika",
"LabelFileModified": "Muutettu tiedosto",
"LabelFilename": "Tiedostonimi",
"LabelFolder": "Kansio",
"LabelInProgress": "Kesken",
"LabelIncomplete": "Keskeneräinen",
"LabelJumpBackwardsTime": "Hyppää taaksepäin ajassa",
"LabelJumpForwardsTime": "Hyppää eteenpäin ajassa",
"LabelLanguage": "Kieli",
"LabelListenAgain": "Kuuntele uudelleen",
"LabelLockPlayer": "Lukitse soitin",
"LabelMediaType": "Mediatyyppi",
"LabelMore": "Lisää",
"LabelMoreInfo": "Lisätietoja",
"LabelName": "Nimi",
"LabelNarrator": "Lukija",
"LabelNarrators": "Lukijat",
"LabelNever": "Ei koskaan",
"LabelNewestAuthors": "Uusimmat kirjailijat",
"LabelNewestEpisodes": "Uusimmat jaksot",
"LabelNo": "Ei",
"LabelPassword": "Salasana",
"LabelPath": "Polku",
"LabelPlaybackSpeed": "Toistonopeus",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcastit",
"LabelPublishYear": "Julkaisuvuosi",
"LabelRSSFeedPreventIndexing": "Estä indeksointi",
"LabelRead": "Lue",
"LabelReadAgain": "Lue uudelleen",
"LabelRecentSeries": "Viimeisimmät sarjat",
"LabelRecentlyAdded": "Viimeeksi lisätyt",
"LabelRemoveFromPlaylist": "Poista soittolistalta",
"LabelSeason": "Kausi",
"LabelSelectADevice": "Valitse laite",
"LabelServerAddress": "Palvelimen osoite",
"LabelSetEbookAsPrimary": "Aseta ensisijaiseksi",
"LabelSetEbookAsSupplementary": "Aseta täydentäväksi",
"LabelShakeSensitivity": "Ravistusherkkyys",
"LabelShowAll": "Näytä kaikki",
"LabelSize": "Koko",
"LabelSleepTimer": "Uniajastin",
"LabelStatsDailyAverage": "Päivittäinen keskiarvo",
"LabelStatsInARow": "peräjälkeen",
"LabelStatsMinutes": "minuuttia",
"LabelTheme": "Teema",
"LabelThemeDark": "Tumma",
"LabelThemeLight": "Kirkas",
"LabelTimeRemaining": "{0} jäljellä",
"LabelTotalSize": "Kokonaiskoko",
"LabelType": "Tyyppi",
"LabelUseBookshelfView": "Käytä kirjahyllynäkymää",
"LabelUser": "Käyttäjä",
"LabelUsername": "Käyttäjätunnus",
"LabelYourBookmarks": "Kirjanmerkkisi",
"LabelYourProgress": "Edistymisesi",
"MessageAndroid10Downloads": "Android 10 ja sitä vanhemmat käyttävät sovelluksen sisäistä tallennustilaa latauksiin.",
"MessageAttemptingServerConnection": "Yritetään palvelinyhteyttä...",
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf palvelinta ei ole yhdistetty",
"MessageBookshelfEmpty": "Kirjahylly on tyhjä",
"MessageConfirmDiscardProgress": "Haluatko varmasti nollata edistymisesi?",
"MessageConfirmDownloadUsingCellular": "Olet lataamassa mobiilidatan avulla. Tämä voi aiheuttaa operaattorin datamaksuja. Haluatko jatkaa?",
"MessageConfirmMarkAsFinished": "Haluatko varmasti merkitä tämän kohteen valmiiksi?",
"MessageConfirmRemoveBookmark": "Haluatko varmasti poistaa kirjanmerkin?",
"MessageConfirmStreamingUsingCellular": "Olet suoratoistamassa matkapuhelindatan avulla. Tämä voi aiheuttaa operaattorin datamaksuja. Haluatko jatkaa?",
"MessageDiscardProgress": "Hylkää edistyminen",
"MessageDownloadCompleteProcessing": "Lataus valmis. Käsitellään...",
"MessageDownloading": "Ladataan...",
"MessageDownloadingEpisode": "Ladataan jaksoa",
"MessageEpisodesQueuedForDownload": "{0} jaksoa on latausjonossa",
"MessageFetching": "Haetaan...",
"MessageFollowTheProjectOnGithub": "Seuraa projektia Githubissa",
"MessageItemDownloadCompleteFailedToCreate": "Kohteen lataus on valmis, mutta kirjastokohteen luominen epäonnistui",
"MessageLoading": "Ladataan...",
"MessageLoadingServerData": "Ladataan palvelintietoja...",
"MessageMarkAsFinished": "Merkitse valmiiksi",
"MessageNoBookmarks": "Ei kirjanmerkkejä",
"MessageNoItems": "Ei kohteita",
"MessageNoItemsFound": "Kohteita ei löytynyt",
"MessageNoMediaFolders": "Ei mediakansioita",
"MessageNoNetworkConnection": "Ei verkkoyhteyttä",
"MessageNoPodcastsFound": "Podcasteja ei löytynyt",
"MessageNoSeries": "Ei sarjoja",
"MessageNoUserPlaylists": "Sinulla ei ole soittolistoja",
"MessageReportBugsAndContribute": "Ilmoita virheistä, toivo ominaisuuksia ja osallistu",
"ToastBookmarkCreateFailed": "Kirjanmerkin luominen epäonnistui",
"ToastBookmarkRemoveFailed": "Kirjanmerkin poistaminen epäonnistui",
"ToastBookmarkUpdateFailed": "Kirjanmerkin päivittäminen epäonnistui",
"ToastDownloadNotAllowedOnCellular": "Lataaminen ei ole sallittua mobiilidatalla",
"ToastItemMarkedAsFinishedFailed": "Valmiiksi merkitseminen epäonnistui",
"ToastPlaylistCreateFailed": "Soittolistan luominen epäonnistui",
"ToastPodcastCreateFailed": "Podcastin luominen epäonnistui",
"ToastPodcastCreateSuccess": "Podcastin luominen onnistui",
"ToastStreamingNotAllowedOnCellular": "Suoratoisto ei ole sallittu mobiilidatalla"
}
+176 -158
View File
@@ -1,132 +1,139 @@
{ {
"ButtonAdd": "Ajouter", "ButtonAdd": "Ajouter",
"ButtonAddNewServer": "Add New Server", "ButtonAddNewServer": "Ajouter un serveur",
"ButtonAuthors": "Auteurs", "ButtonAuthors": "Auteurs",
"ButtonBack": "Back", "ButtonBack": "Reculer",
"ButtonCancel": "Annuler", "ButtonCancel": "Annuler",
"ButtonCancelTimer": "Cancel Timer", "ButtonCancelTimer": "Annuler la minuterie",
"ButtonClearFilter": "Effacer le filtre", "ButtonClearFilter": "Effacer le filtre",
"ButtonCloseFeed": "Fermer le flux", "ButtonCloseFeed": "Fermer le flux",
"ButtonCollections": "Collections", "ButtonCollections": "Collections",
"ButtonConnect": "Connect", "ButtonConnect": "Connecter",
"ButtonConnectToServer": "Connect to Server", "ButtonConnectToServer": "Se connecter au serveur",
"ButtonCreate": "Créer", "ButtonCreate": "Créer",
"ButtonCreateBookmark": "Create Bookmark", "ButtonCreateBookmark": "Créer un marque-page",
"ButtonCreateNewPlaylist": "Create New Playlist", "ButtonCreateNewPlaylist": "Créer une nouvelle liste de lecture",
"ButtonDelete": "Effacer", "ButtonDelete": "Supprimer",
"ButtonDeleteLocalEpisode": "Delete local episode", "ButtonDeleteLocalEpisode": "Supprimer l’épisode de lappareil",
"ButtonDeleteLocalFile": "Delete local file", "ButtonDeleteLocalFile": "Supprimer le fichier de lappareil",
"ButtonDeleteLocalItem": "Delete local item", "ButtonDeleteLocalItem": "Supprimer l’élément de lappareil",
"ButtonDisableAutoTimer": "Disable Auto Timer", "ButtonDisableAutoTimer": "Désactiver la minuterie automatique",
"ButtonDisconnect": "Disconnect", "ButtonDisconnect": "Déconnecter",
"ButtonHistory": "History", "ButtonGoToWebClient": "Ouvrir dans le navigateur",
"ButtonHistory": "Historique",
"ButtonHome": "Accueil", "ButtonHome": "Accueil",
"ButtonIssues": "Parutions", "ButtonIssues": "Problèmes",
"ButtonLatest": "Dernière version", "ButtonLatest": "Dernière version",
"ButtonLibrary": "Bibliothèque", "ButtonLibrary": "Bibliothèque",
"ButtonLocalMedia": "Local Media", "ButtonLocalMedia": "Médias locaux",
"ButtonManageLocalFiles": "Manage Local Files", "ButtonManageLocalFiles": "Gérer les fichiers locaux",
"ButtonNewFolder": "New Folder", "ButtonNewFolder": "Nouveau dossier",
"ButtonNextEpisode": "Next Episode", "ButtonNextEpisode": "Prochain épisode",
"ButtonOpenFeed": "Ouvrir le flux", "ButtonOpenFeed": "Ouvrir le flux",
"ButtonOverride": "Override", "ButtonOverride": "Remplacer",
"ButtonPause": "Pause", "ButtonPause": "Pause",
"ButtonPlay": "Écouter", "ButtonPlay": "Lire",
"ButtonPlayEpisode": "Lire l’épisode",
"ButtonPlaying": "En lecture", "ButtonPlaying": "En lecture",
"ButtonPlaylists": "Listes de lecture", "ButtonPlaylists": "Listes de lecture",
"ButtonRead": "Lire", "ButtonRead": "Lire",
"ButtonRemove": "Supprimer", "ButtonRemove": "Supprimer",
"ButtonRemoveFromServer": "Remove from Server", "ButtonRemoveFromServer": "Retirer du serveur",
"ButtonSave": "Sauvegarder", "ButtonSave": "Sauvegarder",
"ButtonSaveOrder": "Save Order", "ButtonSaveOrder": "Sauvegarder lordre",
"ButtonSearch": "Chercher", "ButtonSearch": "Chercher",
"ButtonSendEbookToDevice": "Send Ebook to Device", "ButtonSendEbookToDevice": "Envoyer le livre à lappareil",
"ButtonSeries": "Séries", "ButtonSeries": "Séries",
"ButtonSetTimer": "Set Timer", "ButtonSetTimer": "Régler la minuterie",
"ButtonStream": "Stream", "ButtonStream": "Flux",
"ButtonSubmit": "Soumettre", "ButtonSubmit": "Soumettre",
"ButtonSwitchServerUser": "Switch Server/User", "ButtonSwitchServerUser": "Changer de serveur / utilisateur",
"ButtonUserStats": "User Stats", "ButtonUserStats": "Statistiques utilisateur",
"ButtonYes": "Oui", "ButtonYes": "Oui",
"HeaderAccount": "Compte", "HeaderAccount": "Compte",
"HeaderAdvanced": "Avancé", "HeaderAdvanced": "Avancé",
"HeaderAudioTracks": "Pistes audio", "HeaderAudioTracks": "Pistes audio",
"HeaderChapters": "Chapitres", "HeaderChapters": "Chapitres",
"HeaderCollection": "Collection", "HeaderCollection": "Collection",
"HeaderCollectionItems": "Entrées de la Collection", "HeaderCollectionItems": "Entrées de la collection",
"HeaderConnectionStatus": "Connection Status", "HeaderConnectionStatus": "Status de connexion",
"HeaderDataSettings": "Paramètres de données",
"HeaderDetails": "Détails", "HeaderDetails": "Détails",
"HeaderDownloads": "Downloads", "HeaderDownloads": "Téléchargements",
"HeaderEbookFiles": "Fichier des livres numériques", "HeaderEbookFiles": "Fichiers des livres numériques",
"HeaderEpisodes": "Épisodes", "HeaderEpisodes": "Épisodes",
"HeaderEreaderSettings": "Options Ereader", "HeaderEreaderSettings": "Paramètres de la liseuse",
"HeaderLatestEpisodes": "Dernier épisodes", "HeaderLatestEpisodes": "Dernier épisodes",
"HeaderLibraries": "Bibliothèque", "HeaderLibraries": "Bibliothèque",
"HeaderLocalFolders": "Local Folders", "HeaderLocalFolders": "Dossiers locaux",
"HeaderLocalLibraryItems": "Local Library Items", "HeaderLocalLibraryItems": "Éléments de la bibliothèque locale",
"HeaderNewPlaylist": "New Playlist", "HeaderNewPlaylist": "Nouvelle liste de lecture",
"HeaderOpenRSSFeed": "Ouvrir Flux RSS", "HeaderOpenRSSFeed": "Ouvrir le flux RSS",
"HeaderPlaybackSettings": "Playback Settings", "HeaderPlaybackSettings": "Paramètres de lecture",
"HeaderPlaylist": "Liste de lecture", "HeaderPlaylist": "Liste de lecture",
"HeaderPlaylistItems": "Éléments de la liste de lecture", "HeaderPlaylistItems": "Éléments de la liste de lecture",
"HeaderRSSFeed": "RSS Feed", "HeaderRSSFeed": "Flux RSS",
"HeaderRSSFeedGeneral": "Détails de flux RSS", "HeaderRSSFeedGeneral": "Détails du flux RSS",
"HeaderRSSFeedIsOpen": "Le Flux RSS est actif", "HeaderRSSFeedIsOpen": "Le flux RSS est actif",
"HeaderSelectDownloadLocation": "Select Download Location", "HeaderSelectDownloadLocation": "Sélectionner le dossier de téléchargement",
"HeaderSettings": "Paramètres", "HeaderSettings": "Paramètres",
"HeaderSleepTimer": "Minuterie", "HeaderSleepTimer": "Minuterie",
"HeaderSleepTimerSettings": "Sleep Timer Settings", "HeaderSleepTimerSettings": "Paramètres du minuteur nocturne",
"HeaderStatsMinutesListeningChart": "Minutes d’écoute (7 derniers jours)", "HeaderStatsMinutesListeningChart": "Minutes d’écoute (7 derniers jours)",
"HeaderStatsRecentSessions": "Sessions récentes", "HeaderStatsRecentSessions": "Sessions récentes",
"HeaderTableOfContents": "Table des matières", "HeaderTableOfContents": "Table des matières",
"HeaderUserInterfaceSettings": "User Interface Settings", "HeaderUserInterfaceSettings": "Paramètres de l'interface",
"HeaderYourStats": "Vos statistiques", "HeaderYourStats": "Vos statistiques",
"LabelAddToPlaylist": "Ajouter à la liste de lecture",
"LabelAdded": "Ajouté", "LabelAdded": "Ajouté",
"LabelAddedAt": "Date dajout", "LabelAddedAt": "Date dajout",
"LabelAddToPlaylist": "Ajouter à la liste de lecture",
"LabelAll": "Tout", "LabelAll": "Tout",
"LabelAllowSeekingOnMediaControls": "Autoriser la recherche de position depuis la notification du lecteur multimédia",
"LabelAlways": "Toujours",
"LabelAskConfirmation": "Demander une confirmation",
"LabelAuthor": "Auteur", "LabelAuthor": "Auteur",
"LabelAuthorFirstLast": "Auteur (Prénom Nom)", "LabelAuthorFirstLast": "Auteur (Prénom Nom)",
"LabelAuthorLastFirst": "Auteur (Nom, Prénom)", "LabelAuthorLastFirst": "Auteur (Nom, Prénom)",
"LabelAuthors": "Auteurs", "LabelAuthors": "Auteurs",
"LabelAutoDownloadEpisodes": "Téléchargement automatique dépisode", "LabelAutoDownloadEpisodes": "Téléchargement automatique des épisodes",
"LabelAutoRewindTime": "Auto rewind time", "LabelAutoRewindTime": "Saut arrière automatique",
"LabelAutoSleepTimer": "Auto sleep timer", "LabelAutoSleepTimer": "Minuteur nocturne automatique",
"LabelAutoSleepTimerAutoRewind": "Auto sleep timer auto rewind", "LabelAutoSleepTimerAutoRewind": "Saut arrière automatique du minuteur nocturne",
"LabelAutoSleepTimerAutoRewindHelp": "When the auto sleep timer finishes, playing the item again will automatically rewind your position.", "LabelAutoSleepTimerAutoRewindHelp": "Lorsque le minuteur nocturne se termine, relire l'élément fera un saut en arrière.",
"LabelAutoSleepTimerHelp": "When playing media between the specified start and end times a sleep timer will automatically start.", "LabelAutoSleepTimerHelp": "Lorsqu'un éléments est lu entre l'heure de début et de fin, un minuteur nocturne se lance automatiquement.",
"LabelBooks": "Livres", "LabelBooks": "Livres",
"LabelChapterTrack": "Piste des chapitres",
"LabelChapters": "Chapitres", "LabelChapters": "Chapitres",
"LabelChapterTrack": "Chapter Track",
"LabelClosePlayer": "Fermer le lecteur", "LabelClosePlayer": "Fermer le lecteur",
"LabelCollapseSeries": "Réduire les séries", "LabelCollapseSeries": "Réduire les séries",
"LabelComplete": "Complet", "LabelComplete": "Complet",
"LabelContinueBooks": "Continue Books", "LabelContinueBooks": "Continuer les Livres",
"LabelContinueEpisodes": "Continue Episodes", "LabelContinueEpisodes": "Continuer les épisodes",
"LabelContinueListening": "Continue Listening", "LabelContinueListening": "Continuer la lecture",
"LabelContinueReading": "Continue Reading", "LabelContinueReading": "Continuer la lecture",
"LabelContinueSeries": "Continue Series", "LabelContinueSeries": "Continuer les séries",
"LabelCustomTime": "Custom time", "LabelCustomTime": "Temps personalisé",
"LabelDescription": "Description", "LabelDescription": "Description",
"LabelDisableAudioFadeOut": "Disable audio fade out", "LabelDisableAudioFadeOut": "Désactiver la diminution automatique du volume",
"LabelDisableAudioFadeOutHelp": "Audio volume will start decreasing when there is less than 1 minute remaining on the sleep timer. Enable this setting to not fade out.", "LabelDisableAudioFadeOutHelp": "Le volume audio diminuera lorsquil restera moins dune minute avant la mise en veille. Activez ce paramètre pour que le volume ne diminue pas.",
"LabelDisableAutoRewind": "Disable auto rewind", "LabelDisableAutoRewind": "Désactiver le saut arrière automatique",
"LabelDisableShakeToReset": "Disable shake to reset", "LabelDisableShakeToReset": "Désactiver secouer pour redémarrer",
"LabelDisableShakeToResetHelp": "Shaking your device while the timer is running OR within 2 minutes of the timer expiring will reset the sleep timer. Enable this setting to disable shake to reset.", "LabelDisableShakeToResetHelp": "Si vous secouez votre appareil pendant que la minuterie est en cours OU dans les 2 minutes qui suivent son expiration, alors la minuterie de sommeil sera réinitialisée. Activez ce paramètre pour désactiver la fonction « Secouer pour réinitialiser ».",
"LabelDisableVibrateOnReset": "Disable vibrate on reset", "LabelDisableVibrateOnReset": "Désactiver la vibration au redémarrage",
"LabelDisableVibrateOnResetHelp": "When the sleep timer gets reset your device will vibrate. Enable this setting to not vibrate when the sleep timer resets.", "LabelDisableVibrateOnResetHelp": "Lorsque le minuteur est redémarré, l'appareil vibre. Sélectionner pour désactiver les vibrations.",
"LabelDiscover": "Discover", "LabelDiscover": "Découvrir",
"LabelDownload": "Téléchargement", "LabelDownload": "Téléchargement",
"LabelDownloaded": "Downloaded", "LabelDownloadUsingCellular": "Télécharger via les données mobiles",
"LabelDownloaded": "Téléchargé",
"LabelDuration": "Durée", "LabelDuration": "Durée",
"LabelEbook": "Livre numérique", "LabelEbook": "Livre numérique",
"LabelEbooks": "Ebooks", "LabelEbooks": "Livres numériques",
"LabelEnable": "Activer", "LabelEnable": "Activer",
"LabelEnableMp3IndexSeeking": "Enable mp3 index seeking", "LabelEnableMp3IndexSeeking": "Activer la recherche par index pour mp3",
"LabelEnableMp3IndexSeekingHelp": "This setting should only be enabled if you have mp3 files that are not seeking correctly. Inaccurate seeking is most likely due to Variable birate (VBR) MP3 files. This setting will force index seeking, in which a time-to-byte mapping is built as the file is read. In some cases with large MP3 files there will be a delay when seeking towards the end of the file.", "LabelEnableMp3IndexSeekingHelp": "Ce paramètres ne devrai être activer que si vous avez des problèmes avec des fichiers mp3 que vous ne pouvez pas chercher. La mauvaise indexation est probablement du au Bitrate Variable (VBR) du fichier MP3. Ce paramètre va forcer l'indexation. Une correspondance Temps-Octet est construit durant la lecture. Dans certains cas, avec de gros fichier MP3, il y aura du délais lors de la recherche vers la fin du fichier.",
"LabelEnd": "Fin", "LabelEnd": "Fin",
"LabelEndOfChapter": "End of Chapter", "LabelEndOfChapter": "Fin du chapitre",
"LabelEndTime": "End time", "LabelEndTime": "Heure de Fin",
"LabelEpisode": "Épisode", "LabelEpisode": "Épisode",
"LabelFeedURL": "URL du flux", "LabelFeedURL": "URL du flux",
"LabelFile": "Fichier", "LabelFile": "Fichier",
@@ -135,86 +142,91 @@
"LabelFilename": "Nom de fichier", "LabelFilename": "Nom de fichier",
"LabelFinished": "Fini(e)", "LabelFinished": "Fini(e)",
"LabelFolder": "Dossier", "LabelFolder": "Dossier",
"LabelFontBoldness": "Graisse de la police",
"LabelFontScale": "Taille de la police de caractère", "LabelFontScale": "Taille de la police de caractère",
"LabelGenre": "Genre", "LabelGenre": "Genre",
"LabelGenres": "Genres", "LabelGenres": "Genres",
"LabelHapticFeedback": "Haptic feedback", "LabelHapticFeedback": "Retour haptique",
"LabelHasEbook": "Has ebook", "LabelHasEbook": "A un livre numérique",
"LabelHasSupplementaryEbook": "Has supplementary ebook", "LabelHasSupplementaryEbook": "A un livre numérique supplémentaire",
"LabelHeavy": "Heavy", "LabelHeavy": "Puissant",
"LabelHigh": "High", "LabelHigh": "Importante",
"LabelHost": "Hôte", "LabelHost": "Hôte",
"LabelIncomplete": "Incomplet",
"LabelInProgress": "En cours", "LabelInProgress": "En cours",
"LabelInternalAppStorage": "Internal App Storage", "LabelIncomplete": "Incomplet",
"LabelJumpBackwardsTime": "Jump backwards time", "LabelInternalAppStorage": "Stockage Interne de l'application",
"LabelJumpForwardsTime": "Jump forwards time", "LabelJumpBackwardsTime": "Durée du saut arrière",
"LabelJumpForwardsTime": "Durée du saut avant",
"LabelLanguage": "Langue", "LabelLanguage": "Langue",
"LabelLayout": "Layout", "LabelLayout": "Mise en page",
"LabelLayoutAuto": "Auto", "LabelLayoutAuto": "Automatique",
"LabelLayoutSinglePage": "Single page", "LabelLayoutSinglePage": "Page seule",
"LabelLight": "Light", "LabelLight": "Léger",
"LabelLineSpacing": "Interligne", "LabelLineSpacing": "Interligne",
"LabelListenAgain": "Listen Again", "LabelListenAgain": "Écouter à nouveau",
"LabelLocalBooks": "Local Books", "LabelLocalBooks": "Livres locaux",
"LabelLocalPodcasts": "Local Podcasts", "LabelLocalPodcasts": "Podcasts locaux",
"LabelLockOrientation": "Lock orientation", "LabelLockOrientation": "Verrouiller l'orientation",
"LabelLockPlayer": "Lock Player", "LabelLockPlayer": "Verrouiller le lecteur",
"LabelLow": "Low", "LabelLow": "Basse",
"LabelMediaType": "Type de média", "LabelMediaType": "Type de média",
"LabelMedium": "Medium", "LabelMedium": "Moyenne",
"LabelMore": "Plus", "LabelMore": "Plus",
"LabelMoreInfo": "Plus dinfo", "LabelMoreInfo": "Plus dinfos",
"LabelName": "Nom", "LabelName": "Nom",
"LabelNarrator": "Narrateur", "LabelNarrator": "Narrateur",
"LabelNarrators": "Narrateurs", "LabelNarrators": "Narrateurs",
"LabelNewestAuthors": "Newest Authors", "LabelNever": "Jamais",
"LabelNewestEpisodes": "Newest Episodes", "LabelNewestAuthors": "Auteurs récents",
"LabelNo": "No", "LabelNewestEpisodes": "Épisodes récents",
"LabelNotFinished": "Non terminé(e)", "LabelNo": "Non",
"LabelNotFinished": "Non terminé",
"LabelNotStarted": "Non Démarré(e)", "LabelNotStarted": "Non Démarré(e)",
"LabelOff": "Off", "LabelOff": "Éteint",
"LabelPassword": "Mot de passe", "LabelPassword": "Mot de passe",
"LabelPath": "Chemin", "LabelPath": "Chemin",
"LabelPlaybackDirect": "Direct", "LabelPlaybackDirect": "Direct",
"LabelPlaybackLocal": "Local", "LabelPlaybackLocal": "Local",
"LabelPlaybackSpeed": "Playback Speed", "LabelPlaybackSpeed": "Vitesse de lecture",
"LabelPlaybackTranscode": "Transcode", "LabelPlaybackTranscode": "Transcoder",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPreventIndexing": "Empêcher lindexation de votre flux par les bases de données iTunes et Google podcast", "LabelPreventIndexing": "Empêcher lindexation de votre flux par les bases de données iTunes et Google podcast",
"LabelProgress": "Progression", "LabelProgress": "Progression",
"LabelPubDate": "Date de publication", "LabelPubDate": "Date de publication",
"LabelPublishYear": "Année d’édition", "LabelPublishYear": "Année de publication",
"LabelRead": "Lire",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Courriel du propriétaire personnalisé", "LabelRSSFeedCustomOwnerEmail": "Courriel du propriétaire personnalisé",
"LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé", "LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé",
"LabelRSSFeedPreventIndexing": "Empêcher lindexation", "LabelRSSFeedPreventIndexing": "Empêcher lindexation",
"LabelRSSFeedSlug": "Identificateur dadresse du Flux RSS ", "LabelRSSFeedSlug": "Balise URL du flux RSS",
"LabelRead": "Lire",
"LabelReadAgain": "Lire à nouveau",
"LabelRecentSeries": "Séries récentes",
"LabelRecentlyAdded": "Ajouts récents",
"LabelRemoveFromPlaylist": "Supprimer de la Liste de Lecture",
"LabelScaleElapsedTimeBySpeed": "Traduire le temps restant en fonction de la vitesse de lecture",
"LabelSeason": "Saison", "LabelSeason": "Saison",
"LabelSelectADevice": "Select a device", "LabelSelectADevice": "Sélectionner un Appareil",
"LabelSeries": "Séries", "LabelSeries": "Séries",
"LabelServerAddress": "Server address", "LabelServerAddress": "Adresse du serveur",
"LabelSetEbookAsPrimary": "Définir comme principale", "LabelSetEbookAsPrimary": "Définir comme principale",
"LabelSetEbookAsSupplementary": "Définir comme supplémentaire", "LabelSetEbookAsSupplementary": "Définir comme supplémentaire",
"LabelShakeSensitivity": "Shake sensitivity", "LabelShakeSensitivity": "Sensibilité à la secousse",
"LabelShowAll": "Afficher Tout", "LabelShowAll": "Tout afficher",
"LabelSize": "Taille", "LabelSize": "Taille",
"LabelSleepTimer": "Minuterie", "LabelSleepTimer": "Minuterie",
"LabelStart": "Démarrer", "LabelStart": "Démarrer",
"LabelStartTime": "Heure de Démarrage", "LabelStartTime": "Heure de démarrage",
"LabelStatsBestDay": "Meilleur Jour", "LabelStatsBestDay": "Meilleur jour",
"LabelStatsDailyAverage": "Moyenne Journalière", "LabelStatsDailyAverage": "Moyenne Journalière",
"LabelStatsDays": "Jours", "LabelStatsDays": "Jours",
"LabelStatsDaysListened": "Jours d’écoute", "LabelStatsDaysListened": "Jours d’écoute",
"LabelStatsInARow": "daffilé(s)", "LabelStatsInARow": "daffilé(s)",
"LabelStatsItemsFinished": "Articles terminés", "LabelStatsItemsFinished": "Éléments terminés",
"LabelStatsMinutes": "minutes", "LabelStatsMinutes": "minutes",
"LabelStatsMinutesListening": "Minutes d’écoute", "LabelStatsMinutesListening": "Minutes d’écoute",
"LabelStatsWeekListening": "Écoute de la semaine", "LabelStatsWeekListening": "Écoute de la semaine",
"LabelStreamingUsingCellular": "Diffuser via les données mobiles",
"LabelTag": "Étiquette", "LabelTag": "Étiquette",
"LabelTags": "Étiquettes", "LabelTags": "Étiquettes",
"LabelTheme": "Thème", "LabelTheme": "Thème",
@@ -222,70 +234,76 @@
"LabelThemeLight": "Clair", "LabelThemeLight": "Clair",
"LabelTimeRemaining": "{0} restantes", "LabelTimeRemaining": "{0} restantes",
"LabelTitle": "Titre", "LabelTitle": "Titre",
"LabelTotalTrack": "Total Track", "LabelTotalSize": "Taille totale",
"LabelTotalTrack": "Pistes totale",
"LabelTracks": "Pistes", "LabelTracks": "Pistes",
"LabelType": "Type", "LabelType": "Type",
"LabelUnlockPlayer": "Unlock Player", "LabelUnlockPlayer": "Déverrouiller le lecteur",
"LabelUseBookshelfView": "Use bookshelf view", "LabelUseBookshelfView": "Utiliser l'interface bibliothèque",
"LabelUser": "Utilisateur", "LabelUser": "Utilisateur",
"LabelUsername": "Nom dutilisateur", "LabelUsername": "Nom dutilisateur",
"LabelVeryHigh": "Very High", "LabelVeryHigh": "Très importante",
"LabelVeryLow": "Very Low", "LabelVeryLow": "Très peu importante",
"LabelYourBookmarks": "Vos signets", "LabelYourBookmarks": "Vos favoris",
"LabelYourProgress": "Votre progression", "LabelYourProgress": "Votre progression",
"MessageAndroid10Downloads": "Android 10 and below will use internal app storage for downloads.", "MessageAndroid10Downloads": "Android 10 et en dessous utiliseront le stockage interne de l'application pour les téléchargements.",
"MessageAttemptingServerConnection": "Attempting server connection...", "MessageAttemptingServerConnection": "Tentative de connexion...",
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf server not connected", "MessageAudiobookshelfServerNotConnected": "Serveur Audiobookshelf non connecté",
"MessageAudiobookshelfServerRequired": "<strong>Important!</strong> This app is designed to work with an Audiobookshelf server that you or someone you know is hosting. This app does not provide any content.", "MessageAudiobookshelfServerRequired": "<strong>Important!</strong> Cette application est faite pour fonctionner avec un serveur Audiobookshelf. L'application ne fournit aucun contenu.",
"MessageBookshelfEmpty": "Bookshelf empty", "MessageBookshelfEmpty": "Bibliothèque vide",
"MessageConfirmDeleteLocalEpisode": "Remove local episode \"{0}\" from your device? The file on the server will be unaffected.", "MessageConfirmDeleteLocalEpisode": "Retirer l’épisode local « {0} » de votre appareil? Le fichier sur le serveur ne sera pas affecté.",
"MessageConfirmDeleteLocalFiles": "Remove local files of this item from your device? The files on the server and your progress will be unaffected.", "MessageConfirmDeleteLocalFiles": "Supprimer les fichiers locaux de cet élément de votre appareil ? Les fichiers sur le serveur ainsi que votre progression ne serons pas affectés.",
"MessageConfirmDiscardProgress": "Are you sure you want to reset your progress?", "MessageConfirmDiscardProgress": "Êtes-vous sûr·e de vouloir supprimer votre progression?",
"MessageConfirmMarkAsFinished": "Are you sure you want to mark this item as finished?", "MessageConfirmDownloadUsingCellular": "Vous êtes sur le point deffectuer un téléchargement en utilisant des données mobiles.Il se peut que des frais de données soient facturés par votre opérateur. Souhaitez-vous continuer?",
"MessageConfirmRemoveBookmark": "Are you sure you want to remove bookmark?", "MessageConfirmMarkAsFinished": "Êtes-vous sûr·e de vouloir marquer cette élement comme terminé ?",
"MessageDiscardProgress": "Discard Progress", "MessageConfirmRemoveBookmark": "Êtes-vous sûr·e de vouloir supprimer le marque-page?",
"MessageDownloadCompleteProcessing": "Download complete. Processing...", "MessageConfirmStreamingUsingCellular": "Vous êtes sur le point de diffuser du contenu en utilisant des données mobiles. Il se peut que des frais de données soient facturés par votre opérateur. Souhaitez-vous continuer?",
"MessageDownloading": "Downloading...", "MessageDiscardProgress": "Supprimer la progression",
"MessageDownloadCompleteProcessing": "Téléchargement terminé. Analyse…",
"MessageDownloading": "Téléchargement…",
"MessageDownloadingEpisode": "Téléchargement de l’épisode", "MessageDownloadingEpisode": "Téléchargement de l’épisode",
"MessageEpisodesQueuedForDownload": "{0} épisode(s) mis en file pour téléchargement", "MessageEpisodesQueuedForDownload": "{0} épisode(s) mis en file pour téléchargement",
"MessageFeedURLWillBe": "lURL du flux sera {0}", "MessageFeedURLWillBe": "lURL du flux sera {0}",
"MessageFetching": "Récupération…", "MessageFetching": "Récupération…",
"MessageFollowTheProjectOnGithub": "Follow the project on Github", "MessageFollowTheProjectOnGithub": "Suivre le projet sur GitHub",
"MessageItemDownloadCompleteFailedToCreate": "Item download complete but failed to create library item", "MessageItemDownloadCompleteFailedToCreate": "Le téléchargement est terminé mais lajout de l’élément dans la bibliothèque a échoué",
"MessageLoading": "Chargement…", "MessageLoading": "Chargement…",
"MessageLoadingServerData": "Loading server data...", "MessageLoadingServerData": "Chargement des données du serveur...",
"MessageMarkAsFinished": "Marquer comme terminé", "MessageMarkAsFinished": "Marquer comme terminé",
"MessageMediaLinkedToADifferentServer": "Media is linked to an Audiobookshelf server on a different address ({0}). Progress will be synced when connected to this server address.", "MessageMediaLinkedToADifferentServer": "L'élément est lié à un serveur Audiobookshelf sur une autre adresse ({0}). La progression sera synchroniser lors de la connexion au serveur à cette adresse.",
"MessageMediaLinkedToADifferentUser": "Media is linked to this server but was downloaded by a different user. Progress will only be synced to the user that downloaded it.", "MessageMediaLinkedToADifferentUser": "L'élément est lié a ce serveur, mais a ete téléchargé par un autre utilisateur. La progression ne sera synchronisé que pour l'utilisateur qui l'a téléchargé..",
"MessageMediaLinkedToServer": "Linked to server {0}", "MessageMediaLinkedToServer": "Lié au serveur {0}",
"MessageMediaLinkedToThisServer": "Downloaded media is linked to this server", "MessageMediaLinkedToThisServer": "L'élément téléchargé est lié à a ce serveur",
"MessageMediaNotLinkedToServer": "Media is not linked to an Audiobookshelf server. No progress will be synced.", "MessageMediaNotLinkedToServer": "L'élément n'est lié à aucun serveur. La progression ne sera pas synchronisé.",
"MessageNoBookmarks": "Aucun signet", "MessageNoBookmarks": "Aucun favoris",
"MessageNoChapters": "Aucun chapitre", "MessageNoChapters": "Aucun chapitre",
"MessageNoItems": "Aucun article", "MessageNoItems": "Aucun article",
"MessageNoItemsFound": "Aucun article trouvé", "MessageNoItemsFound": "Aucun élément trouvé",
"MessageNoListeningSessions": "Aucune session d’écoute en cours", "MessageNoListeningSessions": "Aucune session d’écoute en cours",
"MessageNoMediaFolders": "No Media Folders", "MessageNoMediaFolders": "Aucun dossier multimédia",
"MessageNoNetworkConnection": "No network connection", "MessageNoNetworkConnection": "Pas de connexion réseau",
"MessageNoPodcastsFound": "Aucun podcast trouvé", "MessageNoPodcastsFound": "Aucun podcast trouvé",
"MessageNoSeries": "Aucune série",
"MessageNoUpdatesWereNecessary": "Aucune mise à jour n’était nécessaire", "MessageNoUpdatesWereNecessary": "Aucune mise à jour n’était nécessaire",
"MessageNoUserPlaylists": "Vous navez aucune liste de lecture", "MessageNoUserPlaylists": "Vous navez aucune liste de lecture",
"MessageReportBugsAndContribute": "Remonter des anomalies, demander des fonctionnalités et contribuer sur", "MessageReportBugsAndContribute": "Signalez des anomalies, demandez des fonctionnalités et contribuez sur",
"MessageSocketConnectedOverMeteredCellular": "Socket connected over metered cellular", "MessageSocketConnectedOverMeteredCellular": "Socket connecté via données cellulaire limitée",
"MessageSocketConnectedOverMeteredWifi": "Socket connected over metered wifi", "MessageSocketConnectedOverMeteredWifi": "Socket connecté via Wi-Fi limitée",
"MessageSocketConnectedOverUnmeteredCellular": "Socket connected over unmetered cellular", "MessageSocketConnectedOverUnmeteredCellular": "Socket connecté via données cellulaire",
"MessageSocketConnectedOverUnmeteredWifi": "Socket connected over unmetered wifi", "MessageSocketConnectedOverUnmeteredWifi": "Socket connecté via Wi-Fi",
"MessageSocketNotConnected": "Socket not connected", "MessageSocketNotConnected": "Socket non connecté",
"NoteRSSFeedPodcastAppsHttps": "Attention : la majorité des application de podcast nécessite une adresse de flux en HTTPS.", "NoteRSSFeedPodcastAppsHttps": "Attention: la majorité des application de podcast nécessite une adresse de flux HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Attention : un ou plusieurs de vos épisodes ne possèdent pas de date de publication. Certaines applications de podcast le requièrent.", "NoteRSSFeedPodcastAppsPubDate": "Attention: un ou plusieurs de vos épisodes ne possèdent pas de date de publication. Certaines applications de podcast le requièrent.",
"ToastBookmarkCreateFailed": "Échec de la création de signet", "ToastBookmarkCreateFailed": "Échec de la création de marque-page",
"ToastBookmarkRemoveFailed": "Échec de la suppression de signet", "ToastBookmarkRemoveFailed": "Échec de la suppression de marque-page",
"ToastBookmarkUpdateFailed": "Échec de la mise à jour de signet", "ToastBookmarkUpdateFailed": "Échec de la mise à jour de marsue-page",
"ToastDownloadNotAllowedOnCellular": "Le téléchargement sur les données mobile nest pas autorisé",
"ToastItemMarkedAsFinishedFailed": "Échec de lannotation terminée", "ToastItemMarkedAsFinishedFailed": "Échec de lannotation terminée",
"ToastItemMarkedAsNotFinishedFailed": "Échec de lannotation non-terminée", "ToastItemMarkedAsNotFinishedFailed": "Échec de lannotation non-terminée",
"ToastPlaylistCreateFailed": "Échec de la création de la liste de lecture", "ToastPlaylistCreateFailed": "Échec de la création de la liste de lecture",
"ToastPodcastCreateFailed": "Échec de la création du Podcast", "ToastPodcastCreateFailed": "Échec de la création du Podcast",
"ToastPodcastCreateSuccess": "Podcast créé", "ToastPodcastCreateSuccess": "Podcast créé avec succès",
"ToastRSSFeedCloseFailed": "Échec de la fermeture du flux RSS", "ToastRSSFeedCloseFailed": "Échec de la fermeture du flux RSS",
"ToastRSSFeedCloseSuccess": "Flux RSS fermé" "ToastRSSFeedCloseSuccess": "Flux RSS fermé",
} "ToastStreamingNotAllowedOnCellular": "La diffusion sur les données mobile nest pas autorisée"
}
+27 -12
View File
@@ -19,6 +19,7 @@
"ButtonDeleteLocalItem": "Delete local item", "ButtonDeleteLocalItem": "Delete local item",
"ButtonDisableAutoTimer": "Disable Auto Timer", "ButtonDisableAutoTimer": "Disable Auto Timer",
"ButtonDisconnect": "Disconnect", "ButtonDisconnect": "Disconnect",
"ButtonGoToWebClient": "Go to Web Client",
"ButtonHistory": "History", "ButtonHistory": "History",
"ButtonHome": "ઘર", "ButtonHome": "ઘર",
"ButtonIssues": "સમસ્યાઓ", "ButtonIssues": "સમસ્યાઓ",
@@ -55,6 +56,7 @@
"HeaderCollection": "Collection", "HeaderCollection": "Collection",
"HeaderCollectionItems": "Collection Items", "HeaderCollectionItems": "Collection Items",
"HeaderConnectionStatus": "Connection Status", "HeaderConnectionStatus": "Connection Status",
"HeaderDataSettings": "Data Settings",
"HeaderDetails": "Details", "HeaderDetails": "Details",
"HeaderDownloads": "Downloads", "HeaderDownloads": "Downloads",
"HeaderEbookFiles": "Ebook Files", "HeaderEbookFiles": "Ebook Files",
@@ -81,10 +83,13 @@
"HeaderTableOfContents": "Table of Contents", "HeaderTableOfContents": "Table of Contents",
"HeaderUserInterfaceSettings": "User Interface Settings", "HeaderUserInterfaceSettings": "User Interface Settings",
"HeaderYourStats": "Your Stats", "HeaderYourStats": "Your Stats",
"LabelAddToPlaylist": "Add to Playlist",
"LabelAdded": "Added", "LabelAdded": "Added",
"LabelAddedAt": "Added At", "LabelAddedAt": "Added At",
"LabelAddToPlaylist": "Add to Playlist",
"LabelAll": "All", "LabelAll": "All",
"LabelAllowSeekingOnMediaControls": "Allow position seeking on media notification controls",
"LabelAlways": "Always",
"LabelAskConfirmation": "Ask for confirmation",
"LabelAuthor": "Author", "LabelAuthor": "Author",
"LabelAuthorFirstLast": "Author (First Last)", "LabelAuthorFirstLast": "Author (First Last)",
"LabelAuthorLastFirst": "Author (Last, First)", "LabelAuthorLastFirst": "Author (Last, First)",
@@ -96,8 +101,8 @@
"LabelAutoSleepTimerAutoRewindHelp": "When the auto sleep timer finishes, playing the item again will automatically rewind your position.", "LabelAutoSleepTimerAutoRewindHelp": "When the auto sleep timer finishes, playing the item again will automatically rewind your position.",
"LabelAutoSleepTimerHelp": "When playing media between the specified start and end times a sleep timer will automatically start.", "LabelAutoSleepTimerHelp": "When playing media between the specified start and end times a sleep timer will automatically start.",
"LabelBooks": "Books", "LabelBooks": "Books",
"LabelChapters": "Chapters",
"LabelChapterTrack": "Chapter Track", "LabelChapterTrack": "Chapter Track",
"LabelChapters": "Chapters",
"LabelClosePlayer": "Close player", "LabelClosePlayer": "Close player",
"LabelCollapseSeries": "Collapse Series", "LabelCollapseSeries": "Collapse Series",
"LabelComplete": "Complete", "LabelComplete": "Complete",
@@ -117,6 +122,7 @@
"LabelDisableVibrateOnResetHelp": "When the sleep timer gets reset your device will vibrate. Enable this setting to not vibrate when the sleep timer resets.", "LabelDisableVibrateOnResetHelp": "When the sleep timer gets reset your device will vibrate. Enable this setting to not vibrate when the sleep timer resets.",
"LabelDiscover": "Discover", "LabelDiscover": "Discover",
"LabelDownload": "Download", "LabelDownload": "Download",
"LabelDownloadUsingCellular": "Download using Cellular",
"LabelDownloaded": "Downloaded", "LabelDownloaded": "Downloaded",
"LabelDuration": "Duration", "LabelDuration": "Duration",
"LabelEbook": "Ebook", "LabelEbook": "Ebook",
@@ -144,8 +150,8 @@
"LabelHeavy": "Heavy", "LabelHeavy": "Heavy",
"LabelHigh": "High", "LabelHigh": "High",
"LabelHost": "Host", "LabelHost": "Host",
"LabelIncomplete": "Incomplete",
"LabelInProgress": "In Progress", "LabelInProgress": "In Progress",
"LabelIncomplete": "Incomplete",
"LabelInternalAppStorage": "Internal App Storage", "LabelInternalAppStorage": "Internal App Storage",
"LabelJumpBackwardsTime": "Jump backwards time", "LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time", "LabelJumpForwardsTime": "Jump forwards time",
@@ -168,6 +174,7 @@
"LabelName": "Name", "LabelName": "Name",
"LabelNarrator": "Narrator", "LabelNarrator": "Narrator",
"LabelNarrators": "Narrators", "LabelNarrators": "Narrators",
"LabelNever": "Never",
"LabelNewestAuthors": "Newest Authors", "LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes", "LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No", "LabelNo": "No",
@@ -186,14 +193,16 @@
"LabelProgress": "Progress", "LabelProgress": "Progress",
"LabelPubDate": "Pub Date", "LabelPubDate": "Pub Date",
"LabelPublishYear": "Publish Year", "LabelPublishYear": "Publish Year",
"LabelRead": "Read",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email", "LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
"LabelRSSFeedCustomOwnerName": "Custom owner Name", "LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedPreventIndexing": "Prevent Indexing", "LabelRSSFeedPreventIndexing": "Prevent Indexing",
"LabelRSSFeedSlug": "RSS Feed Slug", "LabelRSSFeedSlug": "RSS Feed Slug",
"LabelRead": "Read",
"LabelReadAgain": "Read Again",
"LabelRecentSeries": "Recent Series",
"LabelRecentlyAdded": "Recently Added",
"LabelRemoveFromPlaylist": "Remove from Playlist",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Season", "LabelSeason": "Season",
"LabelSelectADevice": "Select a device", "LabelSelectADevice": "Select a device",
"LabelSeries": "Series", "LabelSeries": "Series",
@@ -215,6 +224,7 @@
"LabelStatsMinutes": "minutes", "LabelStatsMinutes": "minutes",
"LabelStatsMinutesListening": "Minutes Listening", "LabelStatsMinutesListening": "Minutes Listening",
"LabelStatsWeekListening": "Week Listening", "LabelStatsWeekListening": "Week Listening",
"LabelStreamingUsingCellular": "Streaming using Cellular",
"LabelTag": "Tag", "LabelTag": "Tag",
"LabelTags": "Tags", "LabelTags": "Tags",
"LabelTheme": "Theme", "LabelTheme": "Theme",
@@ -241,8 +251,10 @@
"MessageConfirmDeleteLocalEpisode": "Remove local episode \"{0}\" from your device? The file on the server will be unaffected.", "MessageConfirmDeleteLocalEpisode": "Remove local episode \"{0}\" from your device? The file on the server will be unaffected.",
"MessageConfirmDeleteLocalFiles": "Remove local files of this item from your device? The files on the server and your progress will be unaffected.", "MessageConfirmDeleteLocalFiles": "Remove local files of this item from your device? The files on the server and your progress will be unaffected.",
"MessageConfirmDiscardProgress": "Are you sure you want to reset your progress?", "MessageConfirmDiscardProgress": "Are you sure you want to reset your progress?",
"MessageConfirmDownloadUsingCellular": "You are about to download using cellular data. This may include carrier data charges. Do you wish to continue?",
"MessageConfirmMarkAsFinished": "Are you sure you want to mark this item as finished?", "MessageConfirmMarkAsFinished": "Are you sure you want to mark this item as finished?",
"MessageConfirmRemoveBookmark": "Are you sure you want to remove bookmark?", "MessageConfirmRemoveBookmark": "Are you sure you want to remove bookmark?",
"MessageConfirmStreamingUsingCellular": "You are about to stream using cellular data. This may include carrier data charges. Do you wish to continue?",
"MessageDiscardProgress": "Discard Progress", "MessageDiscardProgress": "Discard Progress",
"MessageDownloadCompleteProcessing": "Download complete. Processing...", "MessageDownloadCompleteProcessing": "Download complete. Processing...",
"MessageDownloading": "Downloading...", "MessageDownloading": "Downloading...",
@@ -250,7 +262,7 @@
"MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download", "MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download",
"MessageFeedURLWillBe": "Feed URL will be {0}", "MessageFeedURLWillBe": "Feed URL will be {0}",
"MessageFetching": "Fetching...", "MessageFetching": "Fetching...",
"MessageFollowTheProjectOnGithub": "Follow the project on Github", "MessageFollowTheProjectOnGithub": "Follow the project on GitHub",
"MessageItemDownloadCompleteFailedToCreate": "Item download complete but failed to create library item", "MessageItemDownloadCompleteFailedToCreate": "Item download complete but failed to create library item",
"MessageLoading": "Loading...", "MessageLoading": "Loading...",
"MessageLoadingServerData": "Loading server data...", "MessageLoadingServerData": "Loading server data...",
@@ -268,24 +280,27 @@
"MessageNoMediaFolders": "No Media Folders", "MessageNoMediaFolders": "No Media Folders",
"MessageNoNetworkConnection": "No network connection", "MessageNoNetworkConnection": "No network connection",
"MessageNoPodcastsFound": "No podcasts found", "MessageNoPodcastsFound": "No podcasts found",
"MessageNoSeries": "No series",
"MessageNoUpdatesWereNecessary": "No updates were necessary", "MessageNoUpdatesWereNecessary": "No updates were necessary",
"MessageNoUserPlaylists": "You have no playlists", "MessageNoUserPlaylists": "You have no playlists",
"MessageReportBugsAndContribute": "Report bugs, request features, and contribute on", "MessageReportBugsAndContribute": "Report bugs, request features, and contribute on",
"MessageSocketConnectedOverMeteredCellular": "Socket connected over metered cellular", "MessageSocketConnectedOverMeteredCellular": "Socket connected over metered cellular",
"MessageSocketConnectedOverMeteredWifi": "Socket connected over metered wifi", "MessageSocketConnectedOverMeteredWifi": "Socket connected over metered Wi-Fi",
"MessageSocketConnectedOverUnmeteredCellular": "Socket connected over unmetered cellular", "MessageSocketConnectedOverUnmeteredCellular": "Socket connected over unmetered cellular",
"MessageSocketConnectedOverUnmeteredWifi": "Socket connected over unmetered wifi", "MessageSocketConnectedOverUnmeteredWifi": "Socket connected over unmetered Wi-Fi",
"MessageSocketNotConnected": "Socket not connected", "MessageSocketNotConnected": "Socket not connected",
"NoteRSSFeedPodcastAppsHttps": "Warning: Most podcast apps will require the RSS feed URL is using HTTPS", "NoteRSSFeedPodcastAppsHttps": "Warning: Most podcast apps will require the RSS feed URL is using HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.", "NoteRSSFeedPodcastAppsPubDate": "Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.",
"ToastBookmarkCreateFailed": "Failed to create bookmark", "ToastBookmarkCreateFailed": "Failed to create bookmark",
"ToastBookmarkRemoveFailed": "Failed to remove bookmark", "ToastBookmarkRemoveFailed": "Failed to remove bookmark",
"ToastBookmarkUpdateFailed": "Failed to update bookmark", "ToastBookmarkUpdateFailed": "Failed to update bookmark",
"ToastDownloadNotAllowedOnCellular": "Downloading is not allowed on cellular data",
"ToastItemMarkedAsFinishedFailed": "Failed to mark as Finished", "ToastItemMarkedAsFinishedFailed": "Failed to mark as Finished",
"ToastItemMarkedAsNotFinishedFailed": "Failed to mark as Not Finished", "ToastItemMarkedAsNotFinishedFailed": "Failed to mark as Not Finished",
"ToastPlaylistCreateFailed": "Failed to create playlist", "ToastPlaylistCreateFailed": "Failed to create playlist",
"ToastPodcastCreateFailed": "Failed to create podcast", "ToastPodcastCreateFailed": "Failed to create podcast",
"ToastPodcastCreateSuccess": "Podcast created successfully", "ToastPodcastCreateSuccess": "Podcast created successfully",
"ToastRSSFeedCloseFailed": "Failed to close RSS feed", "ToastRSSFeedCloseFailed": "Failed to close RSS feed",
"ToastRSSFeedCloseSuccess": "RSS feed closed" "ToastRSSFeedCloseSuccess": "RSS feed closed",
} "ToastStreamingNotAllowedOnCellular": "Streaming is not allowed on cellular data"
}
+27 -12
View File
@@ -19,6 +19,7 @@
"ButtonDeleteLocalItem": "Delete local item", "ButtonDeleteLocalItem": "Delete local item",
"ButtonDisableAutoTimer": "Disable Auto Timer", "ButtonDisableAutoTimer": "Disable Auto Timer",
"ButtonDisconnect": "Disconnect", "ButtonDisconnect": "Disconnect",
"ButtonGoToWebClient": "Go to Web Client",
"ButtonHistory": "History", "ButtonHistory": "History",
"ButtonHome": "घर", "ButtonHome": "घर",
"ButtonIssues": "समस्याएं", "ButtonIssues": "समस्याएं",
@@ -55,6 +56,7 @@
"HeaderCollection": "Collection", "HeaderCollection": "Collection",
"HeaderCollectionItems": "Collection Items", "HeaderCollectionItems": "Collection Items",
"HeaderConnectionStatus": "Connection Status", "HeaderConnectionStatus": "Connection Status",
"HeaderDataSettings": "Data Settings",
"HeaderDetails": "Details", "HeaderDetails": "Details",
"HeaderDownloads": "Downloads", "HeaderDownloads": "Downloads",
"HeaderEbookFiles": "Ebook Files", "HeaderEbookFiles": "Ebook Files",
@@ -81,10 +83,13 @@
"HeaderTableOfContents": "Table of Contents", "HeaderTableOfContents": "Table of Contents",
"HeaderUserInterfaceSettings": "User Interface Settings", "HeaderUserInterfaceSettings": "User Interface Settings",
"HeaderYourStats": "Your Stats", "HeaderYourStats": "Your Stats",
"LabelAddToPlaylist": "Add to Playlist",
"LabelAdded": "Added", "LabelAdded": "Added",
"LabelAddedAt": "Added At", "LabelAddedAt": "Added At",
"LabelAddToPlaylist": "Add to Playlist",
"LabelAll": "All", "LabelAll": "All",
"LabelAllowSeekingOnMediaControls": "Allow position seeking on media notification controls",
"LabelAlways": "Always",
"LabelAskConfirmation": "Ask for confirmation",
"LabelAuthor": "Author", "LabelAuthor": "Author",
"LabelAuthorFirstLast": "Author (First Last)", "LabelAuthorFirstLast": "Author (First Last)",
"LabelAuthorLastFirst": "Author (Last, First)", "LabelAuthorLastFirst": "Author (Last, First)",
@@ -96,8 +101,8 @@
"LabelAutoSleepTimerAutoRewindHelp": "When the auto sleep timer finishes, playing the item again will automatically rewind your position.", "LabelAutoSleepTimerAutoRewindHelp": "When the auto sleep timer finishes, playing the item again will automatically rewind your position.",
"LabelAutoSleepTimerHelp": "When playing media between the specified start and end times a sleep timer will automatically start.", "LabelAutoSleepTimerHelp": "When playing media between the specified start and end times a sleep timer will automatically start.",
"LabelBooks": "Books", "LabelBooks": "Books",
"LabelChapters": "Chapters",
"LabelChapterTrack": "Chapter Track", "LabelChapterTrack": "Chapter Track",
"LabelChapters": "Chapters",
"LabelClosePlayer": "Close player", "LabelClosePlayer": "Close player",
"LabelCollapseSeries": "Collapse Series", "LabelCollapseSeries": "Collapse Series",
"LabelComplete": "Complete", "LabelComplete": "Complete",
@@ -117,6 +122,7 @@
"LabelDisableVibrateOnResetHelp": "When the sleep timer gets reset your device will vibrate. Enable this setting to not vibrate when the sleep timer resets.", "LabelDisableVibrateOnResetHelp": "When the sleep timer gets reset your device will vibrate. Enable this setting to not vibrate when the sleep timer resets.",
"LabelDiscover": "Discover", "LabelDiscover": "Discover",
"LabelDownload": "Download", "LabelDownload": "Download",
"LabelDownloadUsingCellular": "Download using Cellular",
"LabelDownloaded": "Downloaded", "LabelDownloaded": "Downloaded",
"LabelDuration": "Duration", "LabelDuration": "Duration",
"LabelEbook": "Ebook", "LabelEbook": "Ebook",
@@ -144,8 +150,8 @@
"LabelHeavy": "Heavy", "LabelHeavy": "Heavy",
"LabelHigh": "High", "LabelHigh": "High",
"LabelHost": "Host", "LabelHost": "Host",
"LabelIncomplete": "Incomplete",
"LabelInProgress": "In Progress", "LabelInProgress": "In Progress",
"LabelIncomplete": "Incomplete",
"LabelInternalAppStorage": "Internal App Storage", "LabelInternalAppStorage": "Internal App Storage",
"LabelJumpBackwardsTime": "Jump backwards time", "LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time", "LabelJumpForwardsTime": "Jump forwards time",
@@ -168,6 +174,7 @@
"LabelName": "Name", "LabelName": "Name",
"LabelNarrator": "Narrator", "LabelNarrator": "Narrator",
"LabelNarrators": "Narrators", "LabelNarrators": "Narrators",
"LabelNever": "Never",
"LabelNewestAuthors": "Newest Authors", "LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes", "LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No", "LabelNo": "No",
@@ -186,14 +193,16 @@
"LabelProgress": "Progress", "LabelProgress": "Progress",
"LabelPubDate": "Pub Date", "LabelPubDate": "Pub Date",
"LabelPublishYear": "Publish Year", "LabelPublishYear": "Publish Year",
"LabelRead": "Read",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email", "LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
"LabelRSSFeedCustomOwnerName": "Custom owner Name", "LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedPreventIndexing": "Prevent Indexing", "LabelRSSFeedPreventIndexing": "Prevent Indexing",
"LabelRSSFeedSlug": "RSS Feed Slug", "LabelRSSFeedSlug": "RSS Feed Slug",
"LabelRead": "Read",
"LabelReadAgain": "Read Again",
"LabelRecentSeries": "Recent Series",
"LabelRecentlyAdded": "Recently Added",
"LabelRemoveFromPlaylist": "Remove from Playlist",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Season", "LabelSeason": "Season",
"LabelSelectADevice": "Select a device", "LabelSelectADevice": "Select a device",
"LabelSeries": "Series", "LabelSeries": "Series",
@@ -215,6 +224,7 @@
"LabelStatsMinutes": "minutes", "LabelStatsMinutes": "minutes",
"LabelStatsMinutesListening": "Minutes Listening", "LabelStatsMinutesListening": "Minutes Listening",
"LabelStatsWeekListening": "Week Listening", "LabelStatsWeekListening": "Week Listening",
"LabelStreamingUsingCellular": "Streaming using Cellular",
"LabelTag": "Tag", "LabelTag": "Tag",
"LabelTags": "Tags", "LabelTags": "Tags",
"LabelTheme": "Theme", "LabelTheme": "Theme",
@@ -241,8 +251,10 @@
"MessageConfirmDeleteLocalEpisode": "Remove local episode \"{0}\" from your device? The file on the server will be unaffected.", "MessageConfirmDeleteLocalEpisode": "Remove local episode \"{0}\" from your device? The file on the server will be unaffected.",
"MessageConfirmDeleteLocalFiles": "Remove local files of this item from your device? The files on the server and your progress will be unaffected.", "MessageConfirmDeleteLocalFiles": "Remove local files of this item from your device? The files on the server and your progress will be unaffected.",
"MessageConfirmDiscardProgress": "Are you sure you want to reset your progress?", "MessageConfirmDiscardProgress": "Are you sure you want to reset your progress?",
"MessageConfirmDownloadUsingCellular": "You are about to download using cellular data. This may include carrier data charges. Do you wish to continue?",
"MessageConfirmMarkAsFinished": "Are you sure you want to mark this item as finished?", "MessageConfirmMarkAsFinished": "Are you sure you want to mark this item as finished?",
"MessageConfirmRemoveBookmark": "Are you sure you want to remove bookmark?", "MessageConfirmRemoveBookmark": "Are you sure you want to remove bookmark?",
"MessageConfirmStreamingUsingCellular": "You are about to stream using cellular data. This may include carrier data charges. Do you wish to continue?",
"MessageDiscardProgress": "Discard Progress", "MessageDiscardProgress": "Discard Progress",
"MessageDownloadCompleteProcessing": "Download complete. Processing...", "MessageDownloadCompleteProcessing": "Download complete. Processing...",
"MessageDownloading": "Downloading...", "MessageDownloading": "Downloading...",
@@ -250,7 +262,7 @@
"MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download", "MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download",
"MessageFeedURLWillBe": "Feed URL will be {0}", "MessageFeedURLWillBe": "Feed URL will be {0}",
"MessageFetching": "Fetching...", "MessageFetching": "Fetching...",
"MessageFollowTheProjectOnGithub": "Follow the project on Github", "MessageFollowTheProjectOnGithub": "Follow the project on GitHub",
"MessageItemDownloadCompleteFailedToCreate": "Item download complete but failed to create library item", "MessageItemDownloadCompleteFailedToCreate": "Item download complete but failed to create library item",
"MessageLoading": "Loading...", "MessageLoading": "Loading...",
"MessageLoadingServerData": "Loading server data...", "MessageLoadingServerData": "Loading server data...",
@@ -268,24 +280,27 @@
"MessageNoMediaFolders": "No Media Folders", "MessageNoMediaFolders": "No Media Folders",
"MessageNoNetworkConnection": "No network connection", "MessageNoNetworkConnection": "No network connection",
"MessageNoPodcastsFound": "No podcasts found", "MessageNoPodcastsFound": "No podcasts found",
"MessageNoSeries": "No series",
"MessageNoUpdatesWereNecessary": "No updates were necessary", "MessageNoUpdatesWereNecessary": "No updates were necessary",
"MessageNoUserPlaylists": "You have no playlists", "MessageNoUserPlaylists": "You have no playlists",
"MessageReportBugsAndContribute": "Report bugs, request features, and contribute on", "MessageReportBugsAndContribute": "Report bugs, request features, and contribute on",
"MessageSocketConnectedOverMeteredCellular": "Socket connected over metered cellular", "MessageSocketConnectedOverMeteredCellular": "Socket connected over metered cellular",
"MessageSocketConnectedOverMeteredWifi": "Socket connected over metered wifi", "MessageSocketConnectedOverMeteredWifi": "Socket connected over metered Wi-Fi",
"MessageSocketConnectedOverUnmeteredCellular": "Socket connected over unmetered cellular", "MessageSocketConnectedOverUnmeteredCellular": "Socket connected over unmetered cellular",
"MessageSocketConnectedOverUnmeteredWifi": "Socket connected over unmetered wifi", "MessageSocketConnectedOverUnmeteredWifi": "Socket connected over unmetered Wi-Fi",
"MessageSocketNotConnected": "Socket not connected", "MessageSocketNotConnected": "Socket not connected",
"NoteRSSFeedPodcastAppsHttps": "Warning: Most podcast apps will require the RSS feed URL is using HTTPS", "NoteRSSFeedPodcastAppsHttps": "Warning: Most podcast apps will require the RSS feed URL is using HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.", "NoteRSSFeedPodcastAppsPubDate": "Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.",
"ToastBookmarkCreateFailed": "Failed to create bookmark", "ToastBookmarkCreateFailed": "Failed to create bookmark",
"ToastBookmarkRemoveFailed": "Failed to remove bookmark", "ToastBookmarkRemoveFailed": "Failed to remove bookmark",
"ToastBookmarkUpdateFailed": "Failed to update bookmark", "ToastBookmarkUpdateFailed": "Failed to update bookmark",
"ToastDownloadNotAllowedOnCellular": "Downloading is not allowed on cellular data",
"ToastItemMarkedAsFinishedFailed": "Failed to mark as Finished", "ToastItemMarkedAsFinishedFailed": "Failed to mark as Finished",
"ToastItemMarkedAsNotFinishedFailed": "Failed to mark as Not Finished", "ToastItemMarkedAsNotFinishedFailed": "Failed to mark as Not Finished",
"ToastPlaylistCreateFailed": "Failed to create playlist", "ToastPlaylistCreateFailed": "Failed to create playlist",
"ToastPodcastCreateFailed": "Failed to create podcast", "ToastPodcastCreateFailed": "Failed to create podcast",
"ToastPodcastCreateSuccess": "Podcast created successfully", "ToastPodcastCreateSuccess": "Podcast created successfully",
"ToastRSSFeedCloseFailed": "Failed to close RSS feed", "ToastRSSFeedCloseFailed": "Failed to close RSS feed",
"ToastRSSFeedCloseSuccess": "RSS feed closed" "ToastRSSFeedCloseSuccess": "RSS feed closed",
} "ToastStreamingNotAllowedOnCellular": "Streaming is not allowed on cellular data"
}
+94 -77
View File
@@ -1,140 +1,147 @@
{ {
"ButtonAdd": "Dodaj", "ButtonAdd": "Dodaj",
"ButtonAddNewServer": "Add New Server", "ButtonAddNewServer": "dodati novi poslužitelj",
"ButtonAuthors": "Autori", "ButtonAuthors": "Autori",
"ButtonBack": "Back", "ButtonBack": "Natrag",
"ButtonCancel": "Odustani", "ButtonCancel": "Odustani",
"ButtonCancelTimer": "Cancel Timer", "ButtonCancelTimer": "poništi mjerač vremena",
"ButtonClearFilter": "Clear Filter", "ButtonClearFilter": "Poništi filter",
"ButtonCloseFeed": "Zatvori feed", "ButtonCloseFeed": "Zatvori feed",
"ButtonCollections": "Kolekcije", "ButtonCollections": "Kolekcije",
"ButtonConnect": "Connect", "ButtonConnect": "spojiti",
"ButtonConnectToServer": "Connect to Server", "ButtonConnectToServer": "spojiti se na poslužitelj",
"ButtonCreate": "Napravi", "ButtonCreate": "Napravi",
"ButtonCreateBookmark": "Create Bookmark", "ButtonCreateBookmark": "stvoriti oznaku",
"ButtonCreateNewPlaylist": "Create New Playlist", "ButtonCreateNewPlaylist": "stvoriti novu listu muzike",
"ButtonDelete": "Obriši", "ButtonDelete": "Obriši",
"ButtonDeleteLocalEpisode": "Delete local episode", "ButtonDeleteLocalEpisode": "izbrisati lokalnu epizosdu",
"ButtonDeleteLocalFile": "Delete local file", "ButtonDeleteLocalFile": "Delete local file",
"ButtonDeleteLocalItem": "Delete local item", "ButtonDeleteLocalItem": "Delete local item",
"ButtonDisableAutoTimer": "Disable Auto Timer", "ButtonDisableAutoTimer": "poništiti automatski mjerač vremena",
"ButtonDisconnect": "Disconnect", "ButtonDisconnect": "odvezati se",
"ButtonHistory": "History", "ButtonGoToWebClient": "Go to Web Client",
"ButtonHistory": "istorija",
"ButtonHome": "Početna stranica", "ButtonHome": "Početna stranica",
"ButtonIssues": "Problemi", "ButtonIssues": "Problemi",
"ButtonLatest": "Najnovije", "ButtonLatest": "Najnovije",
"ButtonLibrary": "Biblioteka", "ButtonLibrary": "Biblioteka",
"ButtonLocalMedia": "Local Media", "ButtonLocalMedia": "Local Media",
"ButtonManageLocalFiles": "Manage Local Files", "ButtonManageLocalFiles": "uredi lokane fajlove",
"ButtonNewFolder": "New Folder", "ButtonNewFolder": "novi folder",
"ButtonNextEpisode": "Next Episode", "ButtonNextEpisode": "sledeća epizoda",
"ButtonOpenFeed": "Otvori feed", "ButtonOpenFeed": "Otvori feed",
"ButtonOverride": "Override", "ButtonOverride": "Override",
"ButtonPause": "Pause", "ButtonPause": "Pause",
"ButtonPlay": "Pokreni", "ButtonPlay": "Pokreni",
"ButtonPlaying": "Playing", "ButtonPlayEpisode": "Pokreni epizodu",
"ButtonPlaylists": "Playlists", "ButtonPlaying": "pušteno",
"ButtonPlaylists": "plejliste",
"ButtonRead": "Pročitaj", "ButtonRead": "Pročitaj",
"ButtonRemove": "Ukloni", "ButtonRemove": "Ukloni",
"ButtonRemoveFromServer": "Remove from Server", "ButtonRemoveFromServer": "ukloni sa servera",
"ButtonSave": "Spremi", "ButtonSave": "Spremi",
"ButtonSaveOrder": "Save Order", "ButtonSaveOrder": "sačuvaj narudžbu",
"ButtonSearch": "Traži", "ButtonSearch": "Traži",
"ButtonSendEbookToDevice": "Send Ebook to Device", "ButtonSendEbookToDevice": "pošalji elektronsku knjigu uređaju",
"ButtonSeries": "Serije", "ButtonSeries": "Serije",
"ButtonSetTimer": "Set Timer", "ButtonSetTimer": "Set Timer",
"ButtonStream": "Stream", "ButtonStream": "lajvuj",
"ButtonSubmit": "Submit", "ButtonSubmit": "Submit",
"ButtonSwitchServerUser": "Switch Server/User", "ButtonSwitchServerUser": "promeni server/korisnika",
"ButtonUserStats": "User Stats", "ButtonUserStats": "User Stats",
"ButtonYes": "Da", "ButtonYes": "Da",
"HeaderAccount": "Korisnički račun", "HeaderAccount": "Korisnički račun",
"HeaderAdvanced": "Napredno", "HeaderAdvanced": "Napredno",
"HeaderAudioTracks": "Audio Tracks", "HeaderAudioTracks": "audio trake",
"HeaderChapters": "Poglavlja", "HeaderChapters": "Poglavlja",
"HeaderCollection": "Kolekcija", "HeaderCollection": "Kolekcija",
"HeaderCollectionItems": "Stvari u kolekciji", "HeaderCollectionItems": "Stvari u kolekciji",
"HeaderConnectionStatus": "Connection Status", "HeaderConnectionStatus": "status konekcije",
"HeaderDataSettings": "podešavanja podataka",
"HeaderDetails": "Detalji", "HeaderDetails": "Detalji",
"HeaderDownloads": "Downloads", "HeaderDownloads": "Downloads",
"HeaderEbookFiles": "Ebook Files", "HeaderEbookFiles": "fajlovi elektronske knjige",
"HeaderEpisodes": "Epizode", "HeaderEpisodes": "Epizode",
"HeaderEreaderSettings": "Ereader Settings", "HeaderEreaderSettings": "podešavanje elektronskog čitača",
"HeaderLatestEpisodes": "Najnovije epizode", "HeaderLatestEpisodes": "Najnovije epizode",
"HeaderLibraries": "Biblioteke", "HeaderLibraries": "Biblioteke",
"HeaderLocalFolders": "Local Folders", "HeaderLocalFolders": "lokalni folderi",
"HeaderLocalLibraryItems": "Local Library Items", "HeaderLocalLibraryItems": "lokalne stvari iz biblioteke",
"HeaderNewPlaylist": "New Playlist", "HeaderNewPlaylist": "nova plejlista",
"HeaderOpenRSSFeed": "Otvori RSS Feed", "HeaderOpenRSSFeed": "Otvori RSS Feed",
"HeaderPlaybackSettings": "Playback Settings", "HeaderPlaybackSettings": "podešavanja vraćanja unazad",
"HeaderPlaylist": "Playlist", "HeaderPlaylist": "Playlist",
"HeaderPlaylistItems": "Playlist Items", "HeaderPlaylistItems": "Playlist Items",
"HeaderRSSFeed": "RSS Feed", "HeaderRSSFeed": "RSS Feed",
"HeaderRSSFeedGeneral": "RSS Details", "HeaderRSSFeedGeneral": "RSS Details",
"HeaderRSSFeedIsOpen": "RSS Feed je otvoren", "HeaderRSSFeedIsOpen": "RSS Feed je otvoren",
"HeaderSelectDownloadLocation": "Select Download Location", "HeaderSelectDownloadLocation": "izaberi lokaciju preuzimanja",
"HeaderSettings": "Postavke", "HeaderSettings": "Postavke",
"HeaderSleepTimer": "Sleep Timer", "HeaderSleepTimer": "merač vremena spavanja",
"HeaderSleepTimerSettings": "Sleep Timer Settings", "HeaderSleepTimerSettings": "podešavanja merača vremena spavanja",
"HeaderStatsMinutesListeningChart": "Minuta odslušanih (posljednjih 7 dana)", "HeaderStatsMinutesListeningChart": "Minuta odslušanih (posljednjih 7 dana)",
"HeaderStatsRecentSessions": "Nedavne sesije", "HeaderStatsRecentSessions": "Nedavne sesije",
"HeaderTableOfContents": "Table of Contents", "HeaderTableOfContents": "tabela kontenta",
"HeaderUserInterfaceSettings": "User Interface Settings", "HeaderUserInterfaceSettings": "podešavanja korisničkog interfejsa",
"HeaderYourStats": "Tvoja statistika", "HeaderYourStats": "Tvoja statistika",
"LabelAdded": "Added",
"LabelAddedAt": "Added At",
"LabelAddToPlaylist": "Add to Playlist", "LabelAddToPlaylist": "Add to Playlist",
"LabelAll": "All", "LabelAdded": "dodato",
"LabelAddedAt": "dodato u",
"LabelAll": "sve",
"LabelAllowSeekingOnMediaControls": "Dozvoljena provera pozicije na medija kontroli notifikacija",
"LabelAlways": "uvek",
"LabelAskConfirmation": "pitaj za dozvolu",
"LabelAuthor": "Autor", "LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Author (First Last)", "LabelAuthorFirstLast": "autor (prvi zadnji)",
"LabelAuthorLastFirst": "Author (Last, First)", "LabelAuthorLastFirst": "autor (zadnji, prvi)",
"LabelAuthors": "Autori", "LabelAuthors": "Autori",
"LabelAutoDownloadEpisodes": "Automatski preuzmi epizode", "LabelAutoDownloadEpisodes": "Automatski preuzmi epizode",
"LabelAutoRewindTime": "Auto rewind time", "LabelAutoRewindTime": "automatski vrati vreme",
"LabelAutoSleepTimer": "Auto sleep timer", "LabelAutoSleepTimer": "automatski merač vremena spavanja",
"LabelAutoSleepTimerAutoRewind": "Auto sleep timer auto rewind", "LabelAutoSleepTimerAutoRewind": "automatski merač vremena spavanja automatski vraćanje unazad",
"LabelAutoSleepTimerAutoRewindHelp": "When the auto sleep timer finishes, playing the item again will automatically rewind your position.", "LabelAutoSleepTimerAutoRewindHelp": "kada automatski merač vremena spavanja završi, puštanje stvari opet će automatski vratiti unazad vašu situaciju.",
"LabelAutoSleepTimerHelp": "When playing media between the specified start and end times a sleep timer will automatically start.", "LabelAutoSleepTimerHelp": "When playing media between the specified start and end times a sleep timer will automatically start.",
"LabelBooks": "Knjige", "LabelBooks": "Knjige",
"LabelChapters": "Chapters", "LabelChapterTrack": "Praćenje poglavlja",
"LabelChapterTrack": "Chapter Track", "LabelChapters": "poglavlja",
"LabelClosePlayer": "Close player", "LabelClosePlayer": "izaberi igrača",
"LabelCollapseSeries": "Collapse Series", "LabelCollapseSeries": "Collapse Series",
"LabelComplete": "Complete", "LabelComplete": "završeno",
"LabelContinueBooks": "Continue Books", "LabelContinueBooks": "nastavi knjige",
"LabelContinueEpisodes": "Continue Episodes", "LabelContinueEpisodes": "nastavi epizode",
"LabelContinueListening": "Continue Listening", "LabelContinueListening": "nastavi slušati",
"LabelContinueReading": "Continue Reading", "LabelContinueReading": "nastavi čitati",
"LabelContinueSeries": "Continue Series", "LabelContinueSeries": "nastavi serije",
"LabelCustomTime": "Custom time", "LabelCustomTime": "sopstveno vreme",
"LabelDescription": "Opis", "LabelDescription": "Opis",
"LabelDisableAudioFadeOut": "Disable audio fade out", "LabelDisableAudioFadeOut": "Disable audio fade out",
"LabelDisableAudioFadeOutHelp": "Audio volume will start decreasing when there is less than 1 minute remaining on the sleep timer. Enable this setting to not fade out.", "LabelDisableAudioFadeOutHelp": "Audio zvuk će se početi povećavati kada ima manje od minut merača vremena spavanja.",
"LabelDisableAutoRewind": "Disable auto rewind", "LabelDisableAutoRewind": "Disable auto rewind",
"LabelDisableShakeToReset": "Disable shake to reset", "LabelDisableShakeToReset": "Disable shake to reset",
"LabelDisableShakeToResetHelp": "Shaking your device while the timer is running OR within 2 minutes of the timer expiring will reset the sleep timer. Enable this setting to disable shake to reset.", "LabelDisableShakeToResetHelp": "Tresnja vašeg uređaja dok tajmer radi ili do 2 minuta od prekida tajmera će resetovati tajmer spavanja. Uključi ovo podešavanje da poništiš tresnju za resetovanje.",
"LabelDisableVibrateOnReset": "Disable vibrate on reset", "LabelDisableVibrateOnReset": "poništi vibriranje kada se resetuje",
"LabelDisableVibrateOnResetHelp": "When the sleep timer gets reset your device will vibrate. Enable this setting to not vibrate when the sleep timer resets.", "LabelDisableVibrateOnResetHelp": "Kada se tajmer spavanja resetuje vaš uređaj će vibrirati. Dozvolite ovo podešavanje da ne vibrira kada se tajmer spavanja resetuje.",
"LabelDiscover": "Discover", "LabelDiscover": "otkriti",
"LabelDownload": "Preuzmi", "LabelDownload": "Preuzmi",
"LabelDownloaded": "Downloaded", "LabelDownloadUsingCellular": "Download using Cellular",
"LabelDownloaded": "preuzeto",
"LabelDuration": "Trajanje", "LabelDuration": "Trajanje",
"LabelEbook": "Ebook", "LabelEbook": "elektronska knjiga",
"LabelEbooks": "Ebooks", "LabelEbooks": "elektronske knjige",
"LabelEnable": "Uključi", "LabelEnable": "Uključi",
"LabelEnableMp3IndexSeeking": "Enable mp3 index seeking", "LabelEnableMp3IndexSeeking": "Enable mp3 index seeking",
"LabelEnableMp3IndexSeekingHelp": "This setting should only be enabled if you have mp3 files that are not seeking correctly. Inaccurate seeking is most likely due to Variable birate (VBR) MP3 files. This setting will force index seeking, in which a time-to-byte mapping is built as the file is read. In some cases with large MP3 files there will be a delay when seeking towards the end of the file.", "LabelEnableMp3IndexSeekingHelp": "This setting should only be enabled if you have mp3 files that are not seeking correctly. Inaccurate seeking is most likely due to Variable birate (VBR) MP3 files. This setting will force index seeking, in which a time-to-byte mapping is built as the file is read. In some cases with large MP3 files there will be a delay when seeking towards the end of the file.",
"LabelEnd": "Kraj", "LabelEnd": "Kraj",
"LabelEndOfChapter": "End of Chapter", "LabelEndOfChapter": "kraj poglavlja",
"LabelEndTime": "End time", "LabelEndTime": "kraj vremena",
"LabelEpisode": "Epizoda", "LabelEpisode": "Epizoda",
"LabelFeedURL": "Feed URL", "LabelFeedURL": "Feed URL",
"LabelFile": "Datoteka", "LabelFile": "Datoteka",
"LabelFileBirthtime": "File Birthtime", "LabelFileBirthtime": "File Birthtime",
"LabelFileModified": "File Modified", "LabelFileModified": "fajl izmenjen",
"LabelFilename": "Ime datoteke", "LabelFilename": "Ime datoteke",
"LabelFinished": "Finished", "LabelFinished": "završen",
"LabelFolder": "Folder", "LabelFolder": "folder",
"LabelFontScale": "Font scale", "LabelFontScale": "Font scale",
"LabelGenre": "Genre", "LabelGenre": "Genre",
"LabelGenres": "Žanrovi", "LabelGenres": "Žanrovi",
@@ -144,8 +151,8 @@
"LabelHeavy": "Heavy", "LabelHeavy": "Heavy",
"LabelHigh": "High", "LabelHigh": "High",
"LabelHost": "Host", "LabelHost": "Host",
"LabelIncomplete": "Nepotpuno",
"LabelInProgress": "U tijeku", "LabelInProgress": "U tijeku",
"LabelIncomplete": "Nepotpuno",
"LabelInternalAppStorage": "Internal App Storage", "LabelInternalAppStorage": "Internal App Storage",
"LabelJumpBackwardsTime": "Jump backwards time", "LabelJumpBackwardsTime": "Jump backwards time",
"LabelJumpForwardsTime": "Jump forwards time", "LabelJumpForwardsTime": "Jump forwards time",
@@ -168,6 +175,7 @@
"LabelName": "Ime", "LabelName": "Ime",
"LabelNarrator": "Narrator", "LabelNarrator": "Narrator",
"LabelNarrators": "Naratori", "LabelNarrators": "Naratori",
"LabelNever": "Never",
"LabelNewestAuthors": "Newest Authors", "LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes", "LabelNewestEpisodes": "Newest Episodes",
"LabelNo": "No", "LabelNo": "No",
@@ -186,14 +194,16 @@
"LabelProgress": "Napredak", "LabelProgress": "Napredak",
"LabelPubDate": "Datam izdavanja", "LabelPubDate": "Datam izdavanja",
"LabelPublishYear": "Godina izdavanja", "LabelPublishYear": "Godina izdavanja",
"LabelRead": "Read",
"LabelReadAgain": "Read Again",
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email", "LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
"LabelRSSFeedCustomOwnerName": "Custom owner Name", "LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedPreventIndexing": "Prevent Indexing", "LabelRSSFeedPreventIndexing": "Prevent Indexing",
"LabelRSSFeedSlug": "RSS Feed Slug", "LabelRSSFeedSlug": "RSS Feed Slug",
"LabelRead": "Read",
"LabelReadAgain": "Read Again",
"LabelRecentSeries": "Recent Series",
"LabelRecentlyAdded": "Recently Added",
"LabelRemoveFromPlaylist": "Remove from Playlist",
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Sezona", "LabelSeason": "Sezona",
"LabelSelectADevice": "Select a device", "LabelSelectADevice": "Select a device",
"LabelSeries": "Serije", "LabelSeries": "Serije",
@@ -215,6 +225,7 @@
"LabelStatsMinutes": "minute", "LabelStatsMinutes": "minute",
"LabelStatsMinutesListening": "Minuta odslušano", "LabelStatsMinutesListening": "Minuta odslušano",
"LabelStatsWeekListening": "Tjedno slušanje", "LabelStatsWeekListening": "Tjedno slušanje",
"LabelStreamingUsingCellular": "Streaming using Cellular",
"LabelTag": "Tag", "LabelTag": "Tag",
"LabelTags": "Tags", "LabelTags": "Tags",
"LabelTheme": "Theme", "LabelTheme": "Theme",
@@ -222,6 +233,7 @@
"LabelThemeLight": "Light", "LabelThemeLight": "Light",
"LabelTimeRemaining": "{0} preostalo", "LabelTimeRemaining": "{0} preostalo",
"LabelTitle": "Naslov", "LabelTitle": "Naslov",
"LabelTotalSize": "Ukupna veličina",
"LabelTotalTrack": "Total Track", "LabelTotalTrack": "Total Track",
"LabelTracks": "Tracks", "LabelTracks": "Tracks",
"LabelType": "Tip", "LabelType": "Tip",
@@ -241,8 +253,10 @@
"MessageConfirmDeleteLocalEpisode": "Remove local episode \"{0}\" from your device? The file on the server will be unaffected.", "MessageConfirmDeleteLocalEpisode": "Remove local episode \"{0}\" from your device? The file on the server will be unaffected.",
"MessageConfirmDeleteLocalFiles": "Remove local files of this item from your device? The files on the server and your progress will be unaffected.", "MessageConfirmDeleteLocalFiles": "Remove local files of this item from your device? The files on the server and your progress will be unaffected.",
"MessageConfirmDiscardProgress": "Are you sure you want to reset your progress?", "MessageConfirmDiscardProgress": "Are you sure you want to reset your progress?",
"MessageConfirmDownloadUsingCellular": "You are about to download using cellular data. This may include carrier data charges. Do you wish to continue?",
"MessageConfirmMarkAsFinished": "Are you sure you want to mark this item as finished?", "MessageConfirmMarkAsFinished": "Are you sure you want to mark this item as finished?",
"MessageConfirmRemoveBookmark": "Are you sure you want to remove bookmark?", "MessageConfirmRemoveBookmark": "Are you sure you want to remove bookmark?",
"MessageConfirmStreamingUsingCellular": "You are about to stream using cellular data. This may include carrier data charges. Do you wish to continue?",
"MessageDiscardProgress": "Discard Progress", "MessageDiscardProgress": "Discard Progress",
"MessageDownloadCompleteProcessing": "Download complete. Processing...", "MessageDownloadCompleteProcessing": "Download complete. Processing...",
"MessageDownloading": "Downloading...", "MessageDownloading": "Downloading...",
@@ -250,7 +264,7 @@
"MessageEpisodesQueuedForDownload": "{0} Epizoda/-e u redu za preuzimanje", "MessageEpisodesQueuedForDownload": "{0} Epizoda/-e u redu za preuzimanje",
"MessageFeedURLWillBe": "Feed URL će biti {0}", "MessageFeedURLWillBe": "Feed URL će biti {0}",
"MessageFetching": "Dobavljam...", "MessageFetching": "Dobavljam...",
"MessageFollowTheProjectOnGithub": "Follow the project on Github", "MessageFollowTheProjectOnGithub": "Follow the project on GitHub",
"MessageItemDownloadCompleteFailedToCreate": "Item download complete but failed to create library item", "MessageItemDownloadCompleteFailedToCreate": "Item download complete but failed to create library item",
"MessageLoading": "Učitavam...", "MessageLoading": "Učitavam...",
"MessageLoadingServerData": "Loading server data...", "MessageLoadingServerData": "Loading server data...",
@@ -268,24 +282,27 @@
"MessageNoMediaFolders": "No Media Folders", "MessageNoMediaFolders": "No Media Folders",
"MessageNoNetworkConnection": "No network connection", "MessageNoNetworkConnection": "No network connection",
"MessageNoPodcastsFound": "Nijedan podcast pronađen", "MessageNoPodcastsFound": "Nijedan podcast pronađen",
"MessageNoSeries": "No series",
"MessageNoUpdatesWereNecessary": "Aktualiziranje nije bilo potrebno", "MessageNoUpdatesWereNecessary": "Aktualiziranje nije bilo potrebno",
"MessageNoUserPlaylists": "You have no playlists", "MessageNoUserPlaylists": "You have no playlists",
"MessageReportBugsAndContribute": "Prijavte bugove, zatržite featurese i doprinosite na", "MessageReportBugsAndContribute": "Prijavte bugove, zatržite featurese i doprinosite na",
"MessageSocketConnectedOverMeteredCellular": "Socket connected over metered cellular", "MessageSocketConnectedOverMeteredCellular": "Socket connected over metered cellular",
"MessageSocketConnectedOverMeteredWifi": "Socket connected over metered wifi", "MessageSocketConnectedOverMeteredWifi": "Socket connected over metered Wi-Fi",
"MessageSocketConnectedOverUnmeteredCellular": "Socket connected over unmetered cellular", "MessageSocketConnectedOverUnmeteredCellular": "Socket connected over unmetered cellular",
"MessageSocketConnectedOverUnmeteredWifi": "Socket connected over unmetered wifi", "MessageSocketConnectedOverUnmeteredWifi": "Socket connected over unmetered Wi-Fi",
"MessageSocketNotConnected": "Socket not connected", "MessageSocketNotConnected": "Socket not connected",
"NoteRSSFeedPodcastAppsHttps": "Upozorenje: Večina podcasta će trebati RSS feed URL koji koristi HTTPS", "NoteRSSFeedPodcastAppsHttps": "Upozorenje: Večina podcasta će trebati RSS feed URL koji koristi HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Upozorenje: 1 ili više vaših epizoda nemaju datum objavljivanja. Neke podcast aplikacije zahtjevaju to.", "NoteRSSFeedPodcastAppsPubDate": "Upozorenje: 1 ili više vaših epizoda nemaju datum objavljivanja. Neke podcast aplikacije zahtjevaju to.",
"ToastBookmarkCreateFailed": "Kreiranje knjižne bilješke neuspješno", "ToastBookmarkCreateFailed": "Kreiranje knjižne bilješke neuspješno",
"ToastBookmarkRemoveFailed": "Brisanje knjižne bilješke nauspješno", "ToastBookmarkRemoveFailed": "Brisanje knjižne bilješke nauspješno",
"ToastBookmarkUpdateFailed": "Aktualizacija knjižne bilješke neuspješna", "ToastBookmarkUpdateFailed": "Aktualizacija knjižne bilješke neuspješna",
"ToastDownloadNotAllowedOnCellular": "Downloading is not allowed on cellular data",
"ToastItemMarkedAsFinishedFailed": "Označi kao Završeno neuspješno", "ToastItemMarkedAsFinishedFailed": "Označi kao Završeno neuspješno",
"ToastItemMarkedAsNotFinishedFailed": "Označi kao Nezavršeno neuspješno", "ToastItemMarkedAsNotFinishedFailed": "Označi kao Nezavršeno neuspješno",
"ToastPlaylistCreateFailed": "Failed to create playlist", "ToastPlaylistCreateFailed": "Failed to create playlist",
"ToastPodcastCreateFailed": "Neuspješno kreiranje podcasta", "ToastPodcastCreateFailed": "Neuspješno kreiranje podcasta",
"ToastPodcastCreateSuccess": "Podcast uspješno kreiran", "ToastPodcastCreateSuccess": "Podcast uspješno kreiran",
"ToastRSSFeedCloseFailed": "Neuspješno zatvaranje RSS Feeda", "ToastRSSFeedCloseFailed": "Neuspješno zatvaranje RSS Feeda",
"ToastRSSFeedCloseSuccess": "RSS Feed zatvoren" "ToastRSSFeedCloseSuccess": "RSS Feed zatvoren",
} "ToastStreamingNotAllowedOnCellular": "Streaming is not allowed on cellular data"
}
+306
View File
@@ -0,0 +1,306 @@
{
"ButtonAdd": "Hozzáadás",
"ButtonAddNewServer": "Új szerver hozzáadása",
"ButtonAuthors": "Szerzők",
"ButtonBack": "Vissza",
"ButtonCancel": "Mégse",
"ButtonCancelTimer": "Időzítő megszakítása",
"ButtonClearFilter": "Szűrő törlése",
"ButtonCloseFeed": "Hírcsatorna bezárása",
"ButtonCollections": "Gyűjtemények",
"ButtonConnect": "Csatlakozás",
"ButtonConnectToServer": "Csatlakozás a szerverhez",
"ButtonCreate": "Létrehozás",
"ButtonCreateBookmark": "Könyvjelző létrehozása",
"ButtonCreateNewPlaylist": "Új lejátszási lista készítése",
"ButtonDelete": "Törlés",
"ButtonDeleteLocalEpisode": "Helyi epizód törlése",
"ButtonDeleteLocalFile": "Helyi fájl törlése",
"ButtonDeleteLocalItem": "Helyi elem törlése",
"ButtonDisableAutoTimer": "Automatikus időzítő letiltása",
"ButtonDisconnect": "Kapcsolat bontása",
"ButtonGoToWebClient": "Go to Web Client",
"ButtonHistory": "Előzmények",
"ButtonHome": "Kezdőlap",
"ButtonIssues": "Problémák",
"ButtonLatest": "Legújabb",
"ButtonLibrary": "Könyvtár",
"ButtonLocalMedia": "Helyi média",
"ButtonManageLocalFiles": "Helyi fájlok kezelése",
"ButtonNewFolder": "Új mappa",
"ButtonNextEpisode": "Következő epizód",
"ButtonOpenFeed": "Hírcsatorna megnyitása",
"ButtonOverride": "Felülírás",
"ButtonPause": "Szünet",
"ButtonPlay": "Lejátszás",
"ButtonPlaying": "Lejátszás folyamatban",
"ButtonPlaylists": "Lejátszási listák",
"ButtonRead": "Olvasás",
"ButtonRemove": "Eltávolítás",
"ButtonRemoveFromServer": "Eltávolítás a szerverről",
"ButtonSave": "Mentés",
"ButtonSaveOrder": "Sorrend mentése",
"ButtonSearch": "Keresés",
"ButtonSendEbookToDevice": "E-könyv küldése eszközre",
"ButtonSeries": "Sorozatok",
"ButtonSetTimer": "Időzítő beállítása",
"ButtonStream": "Streamelés",
"ButtonSubmit": "Beküldés",
"ButtonSwitchServerUser": "Szerver/Felhasználó váltása",
"ButtonUserStats": "Felhasználói statisztikák",
"ButtonYes": "Igen",
"HeaderAccount": "Fiók",
"HeaderAdvanced": "Haladó",
"HeaderAudioTracks": "Audiósávok",
"HeaderChapters": "Fejezetek",
"HeaderCollection": "Gyűjtemény",
"HeaderCollectionItems": "Gyűjtemény elemek",
"HeaderConnectionStatus": "Kapcsolat állapota",
"HeaderDataSettings": "Data Settings",
"HeaderDetails": "Részletek",
"HeaderDownloads": "Letöltések",
"HeaderEbookFiles": "E-könyv fájlok",
"HeaderEpisodes": "Epizódok",
"HeaderEreaderSettings": "E-olvasó beállításai",
"HeaderLatestEpisodes": "Legújabb epizódok",
"HeaderLibraries": "Könyvtárak",
"HeaderLocalFolders": "Helyi mappák",
"HeaderLocalLibraryItems": "Helyi könyvtári elemek",
"HeaderNewPlaylist": "Új lejátszási lista",
"HeaderOpenRSSFeed": "RSS hírcsatorna megnyitása",
"HeaderPlaybackSettings": "Lejátszási beállítások",
"HeaderPlaylist": "Lejátszási lista",
"HeaderPlaylistItems": "Lejátszási lista elemek",
"HeaderRSSFeed": "RSS Hírcsatorna",
"HeaderRSSFeedGeneral": "RSS részletek",
"HeaderRSSFeedIsOpen": "RSS hírcsatorna nyitva van",
"HeaderSelectDownloadLocation": "Letöltési hely kiválasztása",
"HeaderSettings": "Beállítások",
"HeaderSleepTimer": "Alvásidőzítő",
"HeaderSleepTimerSettings": "Alvásidőzítő beállításai",
"HeaderStatsMinutesListeningChart": "Hallgatási grafikon percekben (az elmúlt 7 napból)",
"HeaderStatsRecentSessions": "Legutóbbi munkamenetek",
"HeaderTableOfContents": "Tartalomjegyzék",
"HeaderUserInterfaceSettings": "Felhasználói felület beállításai",
"HeaderYourStats": "Saját statisztikák",
"LabelAddToPlaylist": "Hozzáadás a lejátszási listához",
"LabelAdded": "Hozzáadva",
"LabelAddedAt": "Hozzáadva ekkor",
"LabelAll": "Összes",
"LabelAllowSeekingOnMediaControls": "Pozíció keresés engedélyezése a média értesítési vezérlőkön",
"LabelAlways": "Always",
"LabelAskConfirmation": "Ask for confirmation",
"LabelAuthor": "Szerző",
"LabelAuthorFirstLast": "Szerző (Keresztnév Vezetéknév)",
"LabelAuthorLastFirst": "Szerző (Vezetéknév, Keresztnév)",
"LabelAuthors": "Szerző",
"LabelAutoDownloadEpisodes": "Epizódok automatikus letöltése",
"LabelAutoRewindTime": "Automatikus visszatekerési idő",
"LabelAutoSleepTimer": "Automatikus alvásidőzítő",
"LabelAutoSleepTimerAutoRewind": "Automatikus alvásidőzítő automatikus visszatekerése",
"LabelAutoSleepTimerAutoRewindHelp": "Amikor az automatikus alvásidőzítő befejeződik, az elem újrajátszásakor automatikusan visszatekeri a pozíciót.",
"LabelAutoSleepTimerHelp": "Amikor a megadott kezdési és befejezési idők között média lejátszása történik, egy alvásidőzítő automatikusan elindul.",
"LabelBooks": "Könyv",
"LabelChapterTrack": "Fejezet sáv",
"LabelChapters": "Fejezetek",
"LabelClosePlayer": "Lejátszó bezárása",
"LabelCollapseSeries": "Sorozatok összecsukása",
"LabelComplete": "Kész",
"LabelContinueBooks": "Könyvek folytatása",
"LabelContinueEpisodes": "Epizódok folytatása",
"LabelContinueListening": "Hallgatás folytatása",
"LabelContinueReading": "Olvasás folytatása",
"LabelContinueSeries": "Sorozat folytatása",
"LabelCustomTime": "Egyéni idő",
"LabelDescription": "Leírás",
"LabelDisableAudioFadeOut": "Hang fokozatos csendesítés letiltása",
"LabelDisableAudioFadeOutHelp": "Az audio hangerő csökkenése kezdődik, amikor kevesebb, mint 1 perc marad az alvásidőzítőből. Engedélyezze ezt a beállítást, hogy ne csendesedjen el fokozatosan a hang.",
"LabelDisableAutoRewind": "Automatikus visszatekerés letiltása",
"LabelDisableShakeToReset": "Rázás a visszaállításhoz letiltása",
"LabelDisableShakeToResetHelp": "Ha az időzítő futása alatt vagy az időzítő lejárta előtti 2 percben megrázza az eszközt, az alvásidőzítő visszaáll. Engedélyezze ezt a beállítást a rázásra történő visszaállítás letiltásához.",
"LabelDisableVibrateOnReset": "Rezgés kikapcsolása visszaállításkor",
"LabelDisableVibrateOnResetHelp": "Amikor az alvásidőzítő visszaállításra kerül, az eszköz rezegni fog. Engedélyezze ezt a beállítást, hogy ne rezegjen az alvásidőzítő visszaállításakor.",
"LabelDiscover": "Felfedezés",
"LabelDownload": "Letöltés",
"LabelDownloadUsingCellular": "Download using Cellular",
"LabelDownloaded": "Letöltve",
"LabelDuration": "Időtartam",
"LabelEbook": "E-könyv",
"LabelEbooks": "E-könyvek",
"LabelEnable": "Engedélyezés",
"LabelEnableMp3IndexSeeking": "Mp3 indexkeresés engedélyezése",
"LabelEnableMp3IndexSeekingHelp": "Ezt a beállítást csak akkor kell engedélyezni, ha vannak mp3 fájljai, amelyek nem megfelelően keresnek. A pontatlan keresés leginkább a változó bitráta (VBR) mp3 fájlok miatt lehetséges. Ez a beállítás indexkeresést fog kényszeríteni, amely során egy idő-bájt leképezés jön létre, ahogy a fájlt olvassák. Nagy mp3 fájlok esetén előfordulhat késedelem a fájl végéhez való kereséskor.",
"LabelEnd": "Vége",
"LabelEndOfChapter": "Fejezet vége",
"LabelEndTime": "Befejezés ideje",
"LabelEpisode": "Epizód",
"LabelFeedURL": "Hírcsatorna URL",
"LabelFile": "Fájl",
"LabelFileBirthtime": "Fájl létrehozásának ideje",
"LabelFileModified": "Fájl módosítva",
"LabelFilename": "Fájlnév",
"LabelFinished": "Befejezve",
"LabelFolder": "Mappa",
"LabelFontScale": "Betűméret skála",
"LabelGenre": "Műfaj",
"LabelGenres": "Műfajok",
"LabelHapticFeedback": "Tapintás visszajelzés",
"LabelHasEbook": "Van e-könyv",
"LabelHasSupplementaryEbook": "Van kiegészítő e-könyv",
"LabelHeavy": "Nehéz",
"LabelHigh": "Magas",
"LabelHost": "Házigazda",
"LabelInProgress": "Folyamatban",
"LabelIncomplete": "Befejezetlen",
"LabelInternalAppStorage": "Belső alkalmazástároló",
"LabelJumpBackwardsTime": "Visszaugrás ideje",
"LabelJumpForwardsTime": "Előreugrás ideje",
"LabelLanguage": "Nyelv",
"LabelLayout": "Elrendezés",
"LabelLayoutAuto": "Automatikus",
"LabelLayoutSinglePage": "Egyoldalas",
"LabelLight": "Könnyű",
"LabelLineSpacing": "Sorköz",
"LabelListenAgain": "Újrahallgatás",
"LabelLocalBooks": "Helyi könyvek",
"LabelLocalPodcasts": "Helyi podcastok",
"LabelLockOrientation": "Tájolás zárolása",
"LabelLockPlayer": "Lejátszó zárolása",
"LabelLow": "Alacsony",
"LabelMediaType": "Média típus",
"LabelMedium": "Közepes",
"LabelMore": "Több",
"LabelMoreInfo": "Több információ",
"LabelName": "Név",
"LabelNarrator": "Előadó",
"LabelNarrators": "Előadók",
"LabelNever": "Never",
"LabelNewestAuthors": "Legújabb szerzők",
"LabelNewestEpisodes": "Legújabb epizódok",
"LabelNo": "Nem",
"LabelNotFinished": "Nem befejezett",
"LabelNotStarted": "Nem kezdődött el",
"LabelOff": "Ki",
"LabelPassword": "Jelszó",
"LabelPath": "Útvonal",
"LabelPlaybackDirect": "Közvetlen",
"LabelPlaybackLocal": "Helyi",
"LabelPlaybackSpeed": "Lejátszási sebesség",
"LabelPlaybackTranscode": "Átkódolás",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcastok",
"LabelPreventIndexing": "Megakadályozza a hírcsatornájának indexelését az iTunes és a Google podcast könyvtárakban",
"LabelProgress": "Haladás",
"LabelPubDate": "Közzététel dátuma",
"LabelPublishYear": "Kiadás éve",
"LabelRSSFeedCustomOwnerEmail": "Egyéni tulajdonos e-mail",
"LabelRSSFeedCustomOwnerName": "Egyéni tulajdonos neve",
"LabelRSSFeedPreventIndexing": "Indexelés megakadályozása",
"LabelRSSFeedSlug": "RSS hírcsatorna rövid cím",
"LabelRead": "Olvasás",
"LabelReadAgain": "Újraolvasás",
"LabelRecentSeries": "Legutóbbi sorozatok",
"LabelRecentlyAdded": "Legutóbb hozzáadva",
"LabelRemoveFromPlaylist": "Remove from Playlist",
"LabelScaleElapsedTimeBySpeed": "Eltelt idő skálázása sebesség szerint",
"LabelSeason": "Évad",
"LabelSelectADevice": "Eszköz kiválasztása",
"LabelSeries": "Sorozat",
"LabelServerAddress": "Szerver címe",
"LabelSetEbookAsPrimary": "Beállítás elsődlegesként",
"LabelSetEbookAsSupplementary": "Beállítás kiegészítőként",
"LabelShakeSensitivity": "Rázás érzékenység",
"LabelShowAll": "Összes megjelenítése",
"LabelSize": "Méret",
"LabelSleepTimer": "Alvásidőzítő",
"LabelStart": "Kezdés",
"LabelStartTime": "Kezdési idő",
"LabelStatsBestDay": "Legjobb nap",
"LabelStatsDailyAverage": "Napi átlag",
"LabelStatsDays": "Napok",
"LabelStatsDaysListened": "Hallgatott napok",
"LabelStatsInARow": "egymás után",
"LabelStatsItemsFinished": "Befejezett elemek",
"LabelStatsMinutes": "perc",
"LabelStatsMinutesListening": "Hallgatás percekben",
"LabelStatsWeekListening": "Heti hallgatás",
"LabelStreamingUsingCellular": "Streaming using Cellular",
"LabelTag": "Címke",
"LabelTags": "Címkék",
"LabelTheme": "Téma",
"LabelThemeDark": "Sötét",
"LabelThemeLight": "Világos",
"LabelTimeRemaining": "{0} maradt",
"LabelTitle": "Cím",
"LabelTotalTrack": "Összes szám",
"LabelTracks": "Számok",
"LabelType": "Típus",
"LabelUnlockPlayer": "Lejátszó feloldása",
"LabelUseBookshelfView": "Polcnézet használata",
"LabelUser": "Felhasználó",
"LabelUsername": "Felhasználónév",
"LabelVeryHigh": "Nagyon magas",
"LabelVeryLow": "Nagyon alacsony",
"LabelYourBookmarks": "Könyvjelzőid",
"LabelYourProgress": "Haladásod",
"MessageAndroid10Downloads": "Az Android 10 és az alatti verziók belső alkalmazástárolót használnak a letöltésekhez.",
"MessageAttemptingServerConnection": "Szerverkapcsolat kísérlete...",
"MessageAudiobookshelfServerNotConnected": "Az Audiobookshelf szerver nem csatlakozik",
"MessageAudiobookshelfServerRequired": "<strong>Fontos!</strong> Ez az alkalmazás egy Audiobookshelf szervert igényel, amit Ön vagy valaki, akit ismer, üzemeltet. Ez az alkalmazás nem biztosít tartalmat.",
"MessageBookshelfEmpty": "A könyvespolc üres",
"MessageConfirmDeleteLocalEpisode": "\"{0}\" helyi epizód eltávolítása az eszközről? A szerveren lévő fájl nem érintett.",
"MessageConfirmDeleteLocalFiles": "Ezen elem helyi fájljainak eltávolítása az eszközről? A szerveren lévő fájlok és a haladás nem érintettek.",
"MessageConfirmDiscardProgress": "Biztosan alaphelyzetbe akarja állítani a haladást?",
"MessageConfirmDownloadUsingCellular": "You are about to download using cellular data. This may include carrier data charges. Do you wish to continue?",
"MessageConfirmMarkAsFinished": "Biztosan befejezettnek jelöli ezt az elemet?",
"MessageConfirmRemoveBookmark": "Biztosan eltávolítja a könyvjelzőt?",
"MessageConfirmStreamingUsingCellular": "You are about to stream using cellular data. This may include carrier data charges. Do you wish to continue?",
"MessageDiscardProgress": "Haladás elvetése",
"MessageDownloadCompleteProcessing": "Letöltés kész. Feldolgozás...",
"MessageDownloading": "Letöltés...",
"MessageDownloadingEpisode": "Epizód letöltése",
"MessageEpisodesQueuedForDownload": "{0} epizód letöltésre vár",
"MessageFeedURLWillBe": "A hírcsatorna URL-je lesz: {0}",
"MessageFetching": "Lekérdezés...",
"MessageFollowTheProjectOnGithub": "Kövesse a projektet a GitHubon",
"MessageItemDownloadCompleteFailedToCreate": "A könyv letöltése befejeződött, de nem sikerült létrehozni a könyvtári elemet",
"MessageLoading": "Betöltés...",
"MessageLoadingServerData": "Szerveradatok betöltése...",
"MessageMarkAsFinished": "Befejezettnek jelölés",
"MessageMediaLinkedToADifferentServer": "A média egy másik címen található Audiobookshelf szerverhez kapcsolódik ({0}). A haladás szinkronizálva lesz, amikor csatlakozik ehhez a szervercímhez.",
"MessageMediaLinkedToADifferentUser": "A média ehhez a szerverhez kapcsolódik, de egy másik felhasználó töltötte le. A haladás csak a letöltést végrehajtó felhasználóhoz lesz szinkronizálva.",
"MessageMediaLinkedToServer": "Szerverhez kapcsolt {0}",
"MessageMediaLinkedToThisServer": "A letöltött média ehhez a szerverhez kapcsolódik",
"MessageMediaNotLinkedToServer": "A média nem kapcsolódik egy Audiobookshelf szerverhez. Nem lesz szinkronizálva a haladás.",
"MessageNoBookmarks": "Nincsenek könyvjelzők",
"MessageNoChapters": "Nincsenek fejezetek",
"MessageNoItems": "Nincsenek elemek",
"MessageNoItemsFound": "Nincs találat",
"MessageNoListeningSessions": "Nincsenek hallgatási munkamenetek",
"MessageNoMediaFolders": "Nincsenek médiamappák",
"MessageNoNetworkConnection": "Nincs hálózati kapcsolat",
"MessageNoPodcastsFound": "Nem találhatók podcastok",
"MessageNoSeries": "No series",
"MessageNoUpdatesWereNecessary": "Nem volt szükséges frissítés",
"MessageNoUserPlaylists": "Nincsenek lejátszási listái",
"MessageReportBugsAndContribute": "Hibák jelentése, funkciók kérése és hozzájárulás a",
"MessageSocketConnectedOverMeteredCellular": "Csatlakoztatva mért mobilhálózaton keresztül",
"MessageSocketConnectedOverMeteredWifi": "Csatlakoztatva mért Wi-Fi hálózaton keresztül",
"MessageSocketConnectedOverUnmeteredCellular": "Csatlakoztatva korlátlan mobilhálózaton keresztül",
"MessageSocketConnectedOverUnmeteredWifi": "Csatlakoztatva korlátlan Wi-Fi hálózaton keresztül",
"MessageSocketNotConnected": "A socket nincs csatlakoztatva",
"NoteRSSFeedPodcastAppsHttps": "Figyelmeztetés: A legtöbb podcast alkalmazás megköveteli, hogy az RSS hírcsatorna URL-je HTTPS-t használjon",
"NoteRSSFeedPodcastAppsPubDate": "Figyelmeztetés: Egy vagy több epizódnak nincs közzétételi dátuma. Néhány podcast alkalmazás ezt megköveteli.",
"ToastBookmarkCreateFailed": "A könyvjelző létrehozása sikertelen",
"ToastBookmarkRemoveFailed": "A könyvjelző eltávolítása sikertelen",
"ToastBookmarkUpdateFailed": "A könyvjelző frissítése sikertelen",
"ToastDownloadNotAllowedOnCellular": "Downloading is not allowed on cellular data",
"ToastItemMarkedAsFinishedFailed": "Az elem befejezettnek jelölése sikertelen",
"ToastItemMarkedAsNotFinishedFailed": "Az elem befejezetlennek jelölése sikertelen",
"ToastPlaylistCreateFailed": "A lejátszási lista létrehozása sikertelen",
"ToastPodcastCreateFailed": "A podcast létrehozása sikertelen",
"ToastPodcastCreateSuccess": "A podcast sikeresen létrehozva",
"ToastRSSFeedCloseFailed": "Az RSS hírcsatorna bezárása sikertelen",
"ToastRSSFeedCloseSuccess": "Az RSS hírcsatorna sikeresen bezárva",
"ToastStreamingNotAllowedOnCellular": "Streaming is not allowed on cellular data"
}

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