Compare commits

..
Author SHA1 Message Date
advplyr ed2f9d2e45 Prevent focus and socket reconnect from both fetching server media progress 2026-05-10 11:12:17 -05:00
advplyr 68dafbba75 Sync local media open in player when socket reconnects after 30s+ 2026-05-10 11:05:17 -05:00
advplyr 8be08702c1 Fix regain focus check server progress for local media items open in player & sync more recent 2026-05-09 16:48:55 -05:00
advplyrandGitHub 2c2e77f6cd Merge pull request #1824 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2026-05-05 15:55:32 -05:00
LvanAlphenandHosted Weblate 12d02645fd Translated using Weblate (Dutch)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2026-05-04 22:12:49 +00:00
PimandHosted Weblate 4578339594 Translated using Weblate (Dutch)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2026-05-04 22:12:48 +00:00
LvanAlphenandHosted Weblate b81f78755d Translated using Weblate (Dutch)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2026-05-04 22:12:47 +00:00
Naoto IshikawaandHosted Weblate 82664d6c81 Translated using Weblate (Japanese)
Currently translated at 76.4% (286 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ja/
2026-05-04 22:12:47 +00:00
Pavel MiniutkaandHosted Weblate 9e06db58d2 Translated using Weblate (Belarusian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-05-04 22:12:46 +00:00
ugyesandHosted Weblate de3461af52 Translated using Weblate (Hungarian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2026-05-04 22:12:46 +00:00
Serhiy BobrovandHosted Weblate 61f676d106 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2026-05-04 22:12:45 +00:00
Владимир МакеевandHosted Weblate 82627b85a5 Translated using Weblate (Russian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2026-05-04 22:12:45 +00:00
Pavel MiniutkaandHosted Weblate 21bc6cc8b6 Translated using Weblate (Belarusian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-05-04 22:12:44 +00:00
PlazecandHosted Weblate 2295b1071e Translated using Weblate (Czech)
Currently translated at 98.6% (369 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2026-05-04 22:12:43 +00:00
Vadzim KurdzesauandHosted Weblate c75e376533 Translated using Weblate (Russian)
Currently translated at 97.5% (365 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2026-05-04 22:12:43 +00:00
A LandHosted Weblate 3a5e2af9e4 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bg/
2026-05-04 22:12:42 +00:00
AlexandHosted Weblate 30f1414ea5 Translated using Weblate (Slovak)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2026-05-04 22:12:42 +00:00
tfr tintandHosted Weblate 737364aa3a Translated using Weblate (Italian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2026-05-04 22:12:41 +00:00
xxzp3andHosted Weblate fd988afad1 Translated using Weblate (Danish)
Currently translated at 98.9% (370 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2026-05-04 22:12:41 +00:00
kuci-JKandHosted Weblate 56dc7f856c Translated using Weblate (Czech)
Currently translated at 98.1% (367 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2026-05-04 22:12:40 +00:00
Paolo RicciandHosted Weblate 5e4a41b0b5 Translated using Weblate (Italian)
Currently translated at 97.5% (365 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2026-05-04 22:12:40 +00:00
Pavel MiniutkaandHosted Weblate 66f426f6bb Translated using Weblate (Belarusian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-05-04 22:12:39 +00:00
Francisco SerradorandHosted Weblate 03efa352f5 Translated using Weblate (Spanish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2026-05-04 22:12:39 +00:00
Francisco SerradorandHosted Weblate 5c147955b7 Translated using Weblate (Spanish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2026-05-04 22:12:38 +00:00
Pavel MiniutkaandHosted Weblate 0a55573bd0 Translated using Weblate (Belarusian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-05-04 22:12:37 +00:00
Pavel MiniutkaandHosted Weblate de3812ac33 Translated using Weblate (Belarusian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-05-04 22:12:37 +00:00
Abdellatif AhammadandHosted Weblate b6de52eb91 Translated using Weblate (Arabic)
Currently translated at 96.5% (361 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2026-05-04 22:12:36 +00:00
advplyr 65ec1ec5d4 Update search to include podcast episodes #1793 2026-05-04 17:12:29 -05:00
advplyrandGitHub 614118d1d3 Merge pull request #1856 from EndToEndDisciple/patch-1
Update i18n.js to add Bulgarian language
2026-05-04 16:23:39 -05:00
advplyr e3999360f0 Update github template app versions 2026-04-24 05:18:19 -05:00
Radoslav MandevandGitHub 50d1b62b95 Update i18n.js to add Bulgarian language 2026-04-24 12:03:37 +02:00
advplyrandGitHub 1571bcd9f3 Merge pull request #1806 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2026-03-08 17:31:40 -05:00
Felix GrollandHosted Weblate 5531ced129 Translated using Weblate (German)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2026-03-04 05:09:49 +00:00
Grzegorz OrlowskiandHosted Weblate 2b258de5b8 Translated using Weblate (Polish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2026-03-04 05:09:48 +00:00
ArturandHosted Weblate 4e3d3f7752 Translated using Weblate (Polish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2026-03-04 05:09:47 +00:00
Torstein EideandHosted Weblate 63863a8800 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2026-03-02 09:09:49 +01:00
thehijackerandHosted Weblate d594fcb08b Translated using Weblate (Slovenian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2026-03-02 09:09:47 +01:00
Pavel MiniutkaandHosted Weblate 85aab8df06 Translated using Weblate (Belarusian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-02-26 10:09:46 +01:00
Pavel MiniutkaandHosted Weblate ec1585f7c1 Translated using Weblate (Belarusian)
Currently translated at 99.1% (371 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-02-24 19:09:50 +00:00
Pavel MiniutkaandHosted Weblate 3b3bda117c Translated using Weblate (Belarusian)
Currently translated at 96.5% (361 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-02-20 19:09:51 +01:00
lambolightingandHosted Weblate c37f2b71d4 Translated using Weblate (Greek)
Currently translated at 61.7% (231 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/el/
2026-02-20 19:09:50 +01:00
PlazecandHosted Weblate 04de5960f1 Translated using Weblate (Czech)
Currently translated at 97.0% (363 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2026-02-19 03:09:52 +01:00
GnebandHosted Weblate dad7ac4efd Translated using Weblate (Czech)
Currently translated at 97.0% (363 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2026-02-19 03:09:51 +01:00
Silviu BajenaruandHosted Weblate d2830c1603 Translated using Weblate (Romanian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ro/
2026-02-19 03:09:50 +01:00
peter cernyandHosted Weblate 6091fd6f79 Translated using Weblate (Slovak)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2026-02-19 03:09:48 +01:00
herny ucetandHosted Weblate c9935eca2b Translated using Weblate (Slovak)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2026-02-19 03:09:47 +01:00
GnebandHosted Weblate 38eacdd7b4 Translated using Weblate (Slovak)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2026-02-19 03:09:46 +01:00
Igor DobračaandHosted Weblate 1bde917b09 Translated using Weblate (Croatian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2026-02-16 06:11:22 +01:00
ugyesandHosted Weblate 0859e219fa Translated using Weblate (Hungarian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2026-02-16 06:11:22 +01:00
Karl BernstålandHosted Weblate dac9279320 Translated using Weblate (Swedish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2026-02-16 06:11:21 +01:00
advplyr 4fb9f6b92d Version bump 0.12.0-beta 2026-02-09 17:28:57 -06:00
advplyrandGitHub fabd579247 Merge pull request #1797 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2026-02-09 17:14:58 -06:00
Yusuke SakaiandHosted Weblate b4ee3b80a8 Translated using Weblate (Japanese)
Currently translated at 54.8% (205 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ja/
2026-02-10 00:14:41 +01:00
Bjorn JorgensenandHosted Weblate a75bdce9b4 Translated using Weblate (Danish)
Currently translated at 97.0% (363 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2026-02-10 00:14:41 +01:00
Luis LandeiroandHosted Weblate d48ac952a5 Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.1% (367 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2026-02-10 00:14:40 +01:00
advplyrandGitHub d83b2e032b Merge pull request #1749 from danyhoron/master
feat: implement download queue on iOS
2026-02-09 17:14:30 -06:00
advplyr 46c9c3f006 Fix downloading page filename overflow 2026-02-08 17:06:23 -06:00
advplyrandGitHub 4199add608 Merge pull request #1738 from eagleeyetom/chromecast-volume-fix-updated
Android cast: stabilize MediaSession volume handling; simplify Google…
2026-02-08 16:37:15 -06:00
advplyrandGitHub 8311643e73 Merge pull request #1774 from FreedomBen/dont-queue-background-display-events
Significantly improve Android UI performance when resuming from background
2026-02-08 16:08:49 -06:00
advplyrandGitHub 7775d5216f Merge pull request #1758 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2026-02-03 17:27:10 -06:00
Lluís FornsandHosted Weblate 6f2fc6b5c1 Translated using Weblate (Catalan)
Currently translated at 79.6% (298 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2026-02-03 20:10:04 +01:00
NapitaukiandHosted Weblate 235cac7ab4 Translated using Weblate (Finnish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2026-02-03 20:10:04 +01:00
dapitch666andHosted Weblate 93b4acfe13 Translated using Weblate (French)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2026-02-02 20:02:04 +00:00
Jan-Eric MyhrgrenandHosted Weblate bd7c73c48d Translated using Weblate (Swedish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2026-02-02 20:02:02 +00:00
Dawid KuźnickiandHosted Weblate 45ecca7ce4 Translated using Weblate (Polish)
Currently translated at 96.7% (362 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2026-02-02 20:02:00 +00:00
FiendFEARingandHosted Weblate 0a8c67cf13 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2026-02-02 20:01:59 +00:00
ugyesandHosted Weblate 316d3d8324 Translated using Weblate (Hungarian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2026-02-02 20:01:57 +00:00
enoshandHosted Weblate 182d45b07c Translated using Weblate (Hebrew)
Currently translated at 94.9% (355 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/he/
2026-02-02 20:01:55 +00:00
Delta UmhöferandHosted Weblate 58d8afca38 Translated using Weblate (German)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2026-02-01 00:26:04 +01:00
dv4yGY2UandHosted Weblate 136709d7ae Translated using Weblate (Turkish)
Currently translated at 98.8% (359 of 363 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/tr/
2026-02-01 00:26:04 +01:00
bittin1ddc447d824349b2andHosted Weblate b0d02aa769 Translated using Weblate (Swedish)
Currently translated at 100.0% (363 of 363 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2026-02-01 00:26:03 +01:00
Samuel GuerreroandHosted Weblate ca66a45ba4 Translated using Weblate (Spanish)
Currently translated at 99.1% (360 of 363 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2026-02-01 00:26:02 +01:00
FiendFEARingandHosted Weblate d7d4114ee6 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (363 of 363 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2026-02-01 00:26:02 +01:00
MantasandHosted Weblate e589c0a7ea Translated using Weblate (Lithuanian)
Currently translated at 52.3% (189 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/lt/
2026-02-01 00:26:01 +01:00
Kabika82andHosted Weblate eb2625a2df Translated using Weblate (Hungarian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2026-02-01 00:26:01 +01:00
N VisiandHosted Weblate aacbacf557 Translated using Weblate (Japanese)
Currently translated at 55.9% (202 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ja/
2026-02-01 00:26:00 +01:00
Henrik LyngeandHosted Weblate 0a7b129db2 Translated using Weblate (Danish)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2026-02-01 00:25:59 +01:00
herny ucetandHosted Weblate c927264b62 Translated using Weblate (Slovak)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2026-02-01 00:25:59 +01:00
xxzp3andHosted Weblate 808225351e Translated using Weblate (Danish)
Currently translated at 99.1% (358 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2026-02-01 00:25:58 +01:00
J. LavoieandHosted Weblate 40ee8d5221 Translated using Weblate (Italian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2026-02-01 00:25:58 +01:00
J. LavoieandHosted Weblate 8cc80e8378 Translated using Weblate (French)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2026-02-01 00:25:57 +01:00
MarcinandHosted Weblate db5f249c14 Translated using Weblate (Polish)
Currently translated at 99.7% (360 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2026-02-01 00:25:56 +01:00
MarioandHosted Weblate e86028e577 Translated using Weblate (German)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2026-02-01 00:25:56 +01:00
Jan-Eric MyhrgrenandHosted Weblate 54bed2bee1 Translated using Weblate (Swedish)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2026-02-01 00:25:55 +01:00
Petri HämäläinenandHosted Weblate 1de78165d0 Translated using Weblate (Finnish)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2026-02-01 00:25:55 +01:00
Ivan SmoliakovandHosted Weblate 4c9ed5e2bf Translated using Weblate (Russian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2026-02-01 00:25:54 +01:00
lambolightingandHosted Weblate a036a8bda6 Translated using Weblate (Greek)
Currently translated at 60.6% (219 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/el/
2026-02-01 00:25:53 +01:00
biuklijaandHosted Weblate 5a482e603e Translated using Weblate (Croatian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2026-02-01 00:25:53 +01:00
FiendFEARingandHosted Weblate d5eb8a341e Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2026-02-01 00:25:52 +01:00
Максим ГорпинічandHosted Weblate 9c74c47448 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2026-02-01 00:25:51 +01:00
AhetekandHosted Weblate 8584bebe6c Translated using Weblate (Polish)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2026-02-01 00:25:50 +01:00
advplyrandGitHub 39cd6c65f2 Merge pull request #1763 from randomdude583/fix/android-aac-seek-issue
Enable constant bitrate seeking to fix AAC seek reset in ExoPlayer
2026-01-31 17:25:39 -06:00
advplyrandGitHub 1056541dd2 Merge pull request #1772 from KiwiHour/sort-by-progress
added 'sort by progress' entries
2026-01-30 17:31:26 -06:00
advplyrandGitHub f9a3a4a218 Merge pull request #1775 from anstosa/ansel/playnextepisodesort
Play Next Episode correctly for serial podcasts
2026-01-30 17:11:54 -06:00
advplyrandGitHub 80bafd9281 Merge pull request #1755 from sir-wilhelm/localize
Localized various strings.
2026-01-30 16:59:16 -06:00
advplyr c06c17d3ae Update HeaderWelcome to include username 2026-01-30 16:50:40 -06:00
advplyr 2a7969fe68 Fix backButton dialog i18n strings 2026-01-30 16:47:51 -06:00
advplyr decda748ba Fix unnecessary $ in getString call 2026-01-30 16:22:57 -06:00
advplyrandGitHub 427f858be5 Merge pull request #1767 from FreedomBen/add-more-backwards-forward-options
Expand time interval options for jump controls
2026-01-29 17:38:42 -06:00
advplyr 1043fb3f3e fix i18n string order 2026-01-29 17:33:58 -06:00
advplyr 7c5e8cb046 Revert audio player playerControls padding 2026-01-29 17:32:34 -06:00
advplyr 5fb8baa186 Remove icons from jump options, remove unused computed properties 2026-01-29 17:17:29 -06:00
advplyr f63b04451b Update jump label to only show on fullscreen audio player 2026-01-29 17:08:12 -06:00
advplyr a80722ff79 Fix auth re-login error updating vuex state 2026-01-29 16:41:16 -06:00
Daniel HoronandGitHub 9d5cbd37c8 Merge branch 'advplyr:master' into master 2026-01-29 17:35:37 +02:00
KiwiHour fada8fccb1 made order-by menu scrollable 2026-01-12 00:07:34 +00:00
Ansel Santosa 9d650019cd Play Next Episode correctly for serial podcasts
The default episode sort correctly takes podcast type into account (oldest to newest for serial, newest to oldest for episodic). However the Play Next Episode button uses episodic sorting for all podcast types. This minimal change fixes that.
2026-01-07 22:19:27 -08:00
KiwiHourandGitHub 915f3c88bc Removed whitespaces around LabelLibrarySortByProgress 2026-01-06 01:48:36 +00:00
Benjamin Porter 140bc7fbc5 Skip progress updates when backgrounded
When we are backgrounded, these queue events build up and can cause
performance issues as the queue is processed.  We don't need to process
these when backgrounded, so skip them.

Also add small delay when resuming for webview to load before sending it
update events
2026-01-05 16:58:10 -07:00
KiwiHour 2a12c1e811 added 'sort by progress' entries, strings added for en-us 2026-01-05 23:50:15 +00:00
Benjamin Porter 5250adb36e Don't queue display events while backgrounded
When we are backgrounded, don't queue display events.  This prevents
a hang and sluggish replay of display events once we are re-foregrounded
later
2026-01-04 10:03:50 -07:00
Benjamin Porter f548163689 Make jump labels compatible with i18n/translation 2025-12-29 17:08:37 -07:00
Benjamin Porter 589ffcecca Move jump back/forward labels below the icon
Move jump back/forward labels below the icon.  Previously they
were on the side which looked a little awkward
2025-12-29 14:09:01 -07:00
Benjamin Porter d9c591b0ea Expand time interval options for Make jump controls
Fixes #1445

- Added additional options for customizing jump forward/backward.  Instead of just 5,
  10, and 30 seconds, we now have 5s, 10s, 15s, 30s, 60s, 2m, and 5m.
- Switched the jump interval to a list in the settings menu for better UX
- Reordered "Disable auto rewind time" for aesthetics in settings menu (it looked
  out of place after backwards time and forwards time were turned to dropdowns

The only downside of this (afaict), and what created the biggest challenge,
is the lack of numeric glyphs in the Material Symbols font.  They only have 5, 10,
and 30, which we were previously using.  Given that they don't have others, that means
we had to change strategies for displaying.  I switched all to generic backward and
forward icons and added the time interval as a label next to it so the user can still
tell at a glance what the setting is.  For consistency we don't use the numbered
glyphs anymore.  For more info on the Material Symbols font, see:
https://fonts.google.com/icons?icon.query=forward

However this does feel like two steps forward, one step back, though I can't think
of good options.  Other possibility would be to create SVGs, though I'm not skilled
enough to do that.  Personally this feels like a good compromise, but others will
probably differ. I considered using the numbered glyphs when the selected value was
5, 10, or 30, but when testing I found the inconsistency jarring and kind of janky.
2025-12-29 12:55:18 -07:00
sir-wilhelm 462021225f Localized various strings.
Confirm title, corresponding messages, welcome, and media type dropdown.
2025-12-28 10:36:17 -06:00
Alex 3f0bc74c21 Enable constant bitrate seeking to fix AAC seek reset in ExoPlayer 2025-12-24 13:03:27 -06:00
advplyr b2c2b625e3 Cleanup ItemSearchCard remove unused match properties, add LabelByAuthor string 2025-12-16 17:44:34 -06:00
advplyrandGitHub d3bd3427e3 Merge pull request #1733 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-12-14 16:35:05 -06:00
lambolightingandHosted Weblate a040a73174 Translated using Weblate (Greek)
Currently translated at 24.4% (88 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/el/
2025-12-14 22:00:20 +00:00
lambolightingandHosted Weblate 332d9f203e Translated using Weblate (Greek)
Currently translated at 8.3% (30 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/el/
2025-12-12 23:13:10 +00:00
Lucas JaksysandHosted Weblate 2d97329836 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2025-12-12 23:13:10 +00:00
kfctatertotandHosted Weblate 730d030623 Translated using Weblate (Arabic)
Currently translated at 99.7% (359 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-12-12 23:13:09 +00:00
kfctatertotandHosted Weblate 2e29daab1d Translated using Weblate (Spanish)
Currently translated at 99.7% (359 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2025-12-12 23:13:08 +00:00
zard KimandHosted Weblate d7b48b4f61 Translated using Weblate (Korean)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ko/
2025-12-12 23:13:08 +00:00
advplyrandHosted Weblate 24b98f10e3 Added translation using Weblate (Greek) 2025-12-12 23:13:07 +00:00
Igor DobračaandHosted Weblate 5fd6f5e4cc Translated using Weblate (Croatian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-12-12 23:13:06 +00:00
thehijackerandHosted Weblate c21af649c3 Translated using Weblate (Slovenian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2025-12-12 23:13:06 +00:00
Alessandro BurzioandHosted Weblate 1d8a23922e Translated using Weblate (Italian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2025-12-12 23:13:05 +00:00
J. LavoieandHosted Weblate 68893ab1f4 Translated using Weblate (German)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-12-12 23:13:04 +00:00
Petri HämäläinenandHosted Weblate dcf172bd98 Translated using Weblate (Finnish)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2025-12-12 23:13:04 +00:00
J. LavoieandHosted Weblate 6dd466a9ce Translated using Weblate (French)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2025-12-12 23:13:03 +00:00
advplyrandHosted Weblate ca222d4110 Added translation using Weblate (Icelandic) 2025-12-12 23:13:02 +00:00
Ivan SmoliakovandHosted Weblate c9af9582e3 Translated using Weblate (Russian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-12-12 23:13:01 +00:00
zard KimandHosted Weblate a3028127ce Translated using Weblate (Korean)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ko/
2025-12-12 23:13:01 +00:00
PlazecandHosted Weblate 23133194b2 Translated using Weblate (Czech)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-12-12 23:13:00 +00:00
Renaldas RepečkaandHosted Weblate b84555db6c Translated using Weblate (Lithuanian)
Currently translated at 51.9% (187 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/lt/
2025-12-12 23:12:59 +00:00
ArturandHosted Weblate 229298df1b Translated using Weblate (Polish)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-12-12 23:12:59 +00:00
Cèlia GarrigaandHosted Weblate fb270cb50b Translated using Weblate (Catalan)
Currently translated at 78.0% (281 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-12-12 23:12:58 +00:00
B0raxandHosted Weblate a861c880ba Translated using Weblate (German)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-12-12 23:12:57 +00:00
Jan-Eric MyhrgrenandHosted Weblate 9712d3f527 Translated using Weblate (Swedish)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-12-12 23:12:57 +00:00
ugyesandHosted Weblate 51a7ce41de Translated using Weblate (Hungarian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2025-12-12 23:12:56 +00:00
Максим ГорпинічandHosted Weblate 4c657c66d0 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-12-12 23:12:55 +00:00
FiendFEARingandHosted Weblate 61c3fafd94 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-12-12 23:12:54 +00:00
advplyr bab95530c8 Update WrappingMarquee to use textContent 2025-12-12 17:12:39 -06:00
Dani Horon 68afba45bc feat: implement download queue on iOS 2025-12-10 17:55:43 +02:00
eagleeyetom 1a52f19b32 Android cast: stabilize MediaSession volume handling; simplify Google Cast volume 2025-11-27 00:32:33 +01:00
advplyr aaef0a7e8d Update github issue templates with 0.11.0 2025-11-25 17:28:10 -06:00
advplyr 02d9023f9d Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2025-11-24 14:52:04 -06:00
advplyr 59a27cdd4d Add Turkish language option #1734 2025-11-24 14:51:59 -06:00
advplyr 9440782027 iOS version bump 0.11.0 2025-11-23 17:20:32 -06:00
advplyr f19ec05780 Version bump v0.11.0-beta 2025-11-23 17:00:45 -06:00
advplyr d95b75e317 Increase padding on reader toolbar for ios 2025-11-23 16:54:36 -06:00
advplyr a6dec31461 Revert #1727 2025-11-23 16:23:53 -06:00
advplyrandGitHub b12427daa6 Merge pull request #1727 from indiefan/lost-progress-fix
Pause and end playback if the Vue layer receives a progress update wh…
2025-11-23 15:50:41 -06:00
advplyrandGitHub 892686b213 Update components/app/AudioPlayer.vue 2025-11-23 15:44:41 -06:00
advplyr 81e71cd7a2 Fix disconnect in side drawer causing audio playing with player closed #1385 2025-11-23 15:16:44 -06:00
advplyrandGitHub 38a5e3637d Merge pull request #1730 from eagleeyetom/feature/cast-volume-fix
Android: fix Cast volume to control receiver
2025-11-23 14:31:15 -06:00
advplyr eb16e769fe Remove logo shown on server connect loading 2025-11-22 17:31:54 -06:00
advplyrandGitHub 9417f0dd3d Merge pull request #1732 from jamerst/days-in-a-row-today-optional
Don't require listening today for 'Days in a row' stat
2025-11-22 17:27:57 -06:00
advplyr 68d82f7a3a Fix unable to delete servers that are unreachable, include delete icon btn on server item #1703 2025-11-21 17:24:16 -06:00
James Tattersall b8fa041dcb Don't require listening today for 'Days in a row' stat 2025-11-21 16:54:13 +00:00
advplyrandGitHub 1df07dc58d Merge pull request #1688 from weilBaum9/master
Add font selection setting in epub e-reader to match web-client (#1373)
2025-11-19 17:01:39 -06:00
advplyr e63358f776 Fix e-reader settings modal on landscape 2025-11-19 17:01:06 -06:00
advplyr 9518189898 Add font family Sans and Serif translations 2025-11-19 16:46:39 -06:00
advplyrandGitHub 70a9cab6f2 Merge pull request #1706 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-11-19 08:06:56 -06:00
advplyrandHosted Weblate 495ec80ebd Added translation using Weblate (Korean) 2025-11-19 15:03:24 +01:00
Mikael EngströmandHosted Weblate 34454fee10 Translated using Weblate (Swedish)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-11-18 23:28:18 +00:00
Milo IvirandHosted Weblate fbac4be4a5 Translated using Weblate (Croatian)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-11-18 23:28:17 +00:00
phewiandHosted Weblate a150e6109e Translated using Weblate (Finnish)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2025-11-18 23:28:16 +00:00
Lucas Gonçalves JaksysandHosted Weblate f392075836 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2025-11-18 23:28:15 +00:00
Petri HämäläinenandHosted Weblate 5d9dd394ea Translated using Weblate (Finnish)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2025-11-18 23:28:14 +00:00
Aleksandr ZakirovandHosted Weblate 162c189d4f Translated using Weblate (Estonian)
Currently translated at 49.5% (177 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/et/
2025-11-18 23:28:13 +00:00
advplyrandGitHub 01902f8a79 Merge pull request #1679 from clayreimann/feat-ios-logs
feat: add logs on iOS
2025-11-18 17:28:00 -06:00
advplyr 399a01a937 iOS fix onLog listener event for realtime logs 2025-11-18 17:24:49 -06:00
eagleeyetom dcaa19071f Android: fix Cast volume to control receiver
- Route hardware volume keys to Cast via MediaSession VolumeProvider
- Use CastSession.volume (0–1) mapped to 0–100; clamp and step by 1
- Update provider with actual device volume after each change (prevents stalls)
- Drop stream-volume/polling attempts and other leftovers
- Expose get/set/adjust device volume commands to the player
2025-11-18 20:57:27 +01:00
advplyrandGitHub a6280805e6 Merge pull request #1685 from sir-wilhelm/chapter_duration_player_modal
Adds a player preference to use chapter duration in the chapters modal view
2025-11-16 17:51:11 -06:00
advplyr 3d8843109a Update chapters modal to always show both start and duration, add sticky header 2025-11-16 17:44:11 -06:00
advplyrandGitHub 16eaccc8e3 Merge pull request #1683 from sir-wilhelm/chapter_duration_chapters_table
Add chapter duration to chapters table on book page
2025-11-16 17:00:14 -06:00
advplyrandGitHub 9df996a507 Merge pull request #1666 from mfulgo/master
Allows widget on Samsung cover screens
2025-11-16 16:22:06 -06:00
Marke Hallowell fa0b9ed6ab Pause and end playback if the Vue layer receives a progress update while having no played track ref. 2025-11-15 16:49:00 -08:00
sir-wilhelm 9b8536a581 Adds a player setting to use chapter duration in the player Chapter modal.
Allows users to view chapter duration instead of start time in the player chapter list.
2025-11-04 19:37:31 -06:00
sir-wilhelm 9158f4f8f1 Add chapter duration next to start time.
This adds the duration to the chapter table on the book page.
2025-11-03 17:39:44 -06:00
advplyrandGitHub 072f19e157 Merge pull request #1647 from IzzySoft/fastlane
initial fastlane structures from IzzyOnDroid
2025-10-28 08:31:26 -05:00
advplyrandGitHub 44198fa17a Merge pull request #1701 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-10-21 17:27:00 -05:00
Petri HämäläinenandHosted Weblate 83ccb398c5 Translated using Weblate (Finnish)
Currently translated at 97.7% (349 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2025-10-17 00:06:25 +02:00
burghy86andHosted Weblate 824e335dec Translated using Weblate (Italian)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2025-10-17 00:06:24 +02:00
nlqogandHosted Weblate e28e9d71ae Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2025-10-14 05:07:24 +00:00
pmangroandHosted Weblate 46acc76f07 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2025-10-11 13:58:53 +02:00
CoxeandHosted Weblate 356fa4eb7c Translated using Weblate (Danish)
Currently translated at 99.7% (356 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-10-11 13:58:53 +02:00
pryszczoskorandHosted Weblate 09a6c631cb Translated using Weblate (Polish)
Currently translated at 99.4% (355 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-10-10 09:07:24 +00:00
HezhaandHosted Weblate cf19816e06 Translated using Weblate (Arabic)
Currently translated at 98.5% (352 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-10-08 22:55:30 +02:00
advplyrandGitHub 2c4b9f5a50 Merge pull request #1691 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-10-07 16:25:36 -05:00
AhetekandHosted Weblate 9e29d45fc3 Translated using Weblate (Polish)
Currently translated at 99.4% (355 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-10-07 03:02:02 +00:00
Kabika82andHosted Weblate b4e350a308 Translated using Weblate (Hungarian)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2025-10-07 03:02:01 +00:00
SmileFateandHosted Weblate 8274072b6a Translated using Weblate (Turkish)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/tr/
2025-10-01 09:02:06 +00:00
DRandHosted Weblate 1ddd146558 Translated using Weblate (Hebrew)
Currently translated at 98.5% (352 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/he/
2025-10-01 09:02:05 +00:00
JanandHosted Weblate 2d76fc6e8c Translated using Weblate (German)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-10-01 09:02:03 +00:00
B0raxandHosted Weblate e19e541a70 Translated using Weblate (German)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-10-01 09:02:02 +00:00
kuci-JKandHosted Weblate 3a56c82501 Translated using Weblate (Czech)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-10-01 09:02:00 +00:00
Petri HämäläinenandHosted Weblate 2f0e385ca2 Translated using Weblate (Finnish)
Currently translated at 97.4% (348 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2025-09-29 05:02:00 +00:00
Amirhossein GhorbanmehrandHosted Weblate 9241a4729c Translated using Weblate (Persian)
Currently translated at 15.4% (55 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fa/
2025-09-25 05:02:01 +02:00
SmileFateandHosted Weblate ea331bd4ef Translated using Weblate (Turkish)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/tr/
2025-09-25 05:02:00 +02:00
advplyrandHosted Weblate c2b598d98e Added translation using Weblate (Persian) 2025-09-23 15:38:29 +02:00
advplyrandGitHub 2fde05412c Merge pull request #1681 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-09-22 09:41:31 -05:00
GernomalyandHosted Weblate d8436d3273 Translated using Weblate (German)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-09-21 02:02:00 +02:00
weilBaum9 e662dae396 Add font selection setting in epub e-reader to match web-client (#1373) 2025-09-16 17:32:00 +02:00
Milo IvirandHosted Weblate e74c5d8609 Translated using Weblate (Croatian)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-09-11 10:01:59 +00:00
SatanowskiandHosted Weblate 3f05a8408d Translated using Weblate (Polish)
Currently translated at 99.4% (355 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-09-09 21:02:05 +00:00
Milo IvirandHosted Weblate 61f0ba4643 Translated using Weblate (Croatian)
Currently translated at 99.7% (356 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-09-09 21:02:04 +00:00
Clay Jensen-Reimann b773ec7690 feat: usable logs on iOS
- enable logs page on iOS
- centralize all logging
- wrap long identifiers in logs UI
- update editorconfig for swift files
- update podfile
2025-09-07 16:31:59 -05:00
peter cernyandHosted Weblate df294730a0 Translated using Weblate (Slovak)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2025-09-06 21:01:58 +00:00
Troj@andHosted Weblate ee1cd38fa2 Translated using Weblate (Belarusian)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2025-09-06 21:01:57 +00:00
jhonthanandHosted Weblate a012328f4c Translated using Weblate (Portuguese (Brazil))
Currently translated at 84.8% (303 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2025-09-06 21:01:56 +00:00
kuci-JKandHosted Weblate 1df9656191 Translated using Weblate (Czech)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-09-06 21:01:55 +00:00
petr-prikrylandHosted Weblate fde2d0887e Translated using Weblate (Czech)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-09-06 21:01:54 +00:00
advplyr 356e439715 Update app versions in github issue templates 0.10.1 2025-09-04 10:47:04 -05:00
Matt Fulgo 941e823374 Allows widget on Samsung cover screens
Adds meta-data to the widget, allowing it to be used on the cover screen
of Samsung devices such as the Flip.

The widget renders as a 4x4 grid (not an optimal layout) but functions
for launching the most recent audiobook or controlling playback.
2025-08-19 22:24:58 -04:00
Izzy f74a1ceb47 initial fastlane structures from IzzyOnDroid 2025-08-03 01:11:32 +02:00
114 changed files with 3012 additions and 947 deletions
+4
View File
@@ -6,3 +6,7 @@ indent_size = 2
charset = utf-8 charset = utf-8
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.swift]
indent_style = space
indent_size = 4
+4 -4
View File
@@ -72,10 +72,10 @@ body:
description: Please ensure your app is up to date. *If you are using a 3rd-party app, please reach out to them directly.* description: Please ensure your app is up to date. *If you are using a 3rd-party app, please reach out to them directly.*
multiple: true multiple: true
options: options:
- 'Android App - 0.10.0' - 'Android App - 0.12.0'
- 'iOS App - 0.10.0' - 'iOS App - 0.12.0'
- 'Android App - 0.9.81' - 'Android App - 0.11.0'
- 'iOS App - 0.9.81' - 'iOS App - 0.11.0'
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
+4 -4
View File
@@ -43,10 +43,10 @@ body:
description: Please ensure your app is up to date. *If you are using a 3rd-party app, please reach out to them directly.* description: Please ensure your app is up to date. *If you are using a 3rd-party app, please reach out to them directly.*
multiple: true multiple: true
options: options:
- 'Android App - 0.10.0' - 'Android App - 0.12.0'
- 'iOS App - 0.10.0' - 'iOS App - 0.12.0'
- 'Android App - 0.9.81' - 'Android App - 0.11.0'
- 'iOS App - 0.9.81' - 'iOS App - 0.11.0'
validations: validations:
required: true required: true
- type: textarea - type: textarea
+2 -2
View File
@@ -36,8 +36,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 114 versionCode 116
versionName "0.10.1-beta" versionName "0.12.0-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
manifestPlaceholders = [ manifestPlaceholders = [
"appAuthRedirectScheme": "com.audiobookshelf.app" "appAuthRedirectScheme": "com.audiobookshelf.app"
+4
View File
@@ -36,6 +36,10 @@
<meta-data <meta-data
android:name="android.appwidget.provider" android:name="android.appwidget.provider"
android:resource="@xml/media_player_widget_info" /> android:resource="@xml/media_player_widget_info" />
<meta-data
android:name="com.samsung.android.appwidget.provider"
android:resource="@xml/samsung_cover_widget_info" />
</receiver> </receiver>
<!-- Used by Android Auto --> <!-- Used by Android Auto -->
@@ -527,4 +527,18 @@ constructor(private val playerNotificationService: PlayerNotificationService) {
checkAutoSleepTimer() checkAutoSleepTimer()
} }
/**
* Sends the current sleep timer state to the client.
* Called when app resumes from background to sync UI state.
*/
fun sendCurrentSleepTimerState() {
if (sleepTimerRunning) {
val timeRemaining = getSleepTimerTimeRemainingSeconds(getPlaybackSpeed())
playerNotificationService.clientEventEmitter?.onSleepTimerSet(timeRemaining, isAutoSleepTimer)
} else {
// No timer running - send 0 to clear any stale UI state
playerNotificationService.clientEventEmitter?.onSleepTimerSet(0, false)
}
}
} }
@@ -3,6 +3,7 @@ package com.audiobookshelf.app.player
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import kotlin.math.roundToInt
import android.view.Surface import android.view.Surface
import android.view.SurfaceHolder import android.view.SurfaceHolder
import android.view.SurfaceView import android.view.SurfaceView
@@ -53,6 +54,10 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
private var myCurrentTrackGroups: TrackGroupArray private var myCurrentTrackGroups: TrackGroupArray
private var myCurrentTrackSelections: TrackSelectionArray private var myCurrentTrackSelections: TrackSelectionArray
// Cache for pending volume to handle async setVolume
private var pendingVolume: Int? = null
private var pendingVolumeSetAt: Long = 0L
var currentMediaItems:List<MediaItem> = mutableListOf() var currentMediaItems:List<MediaItem> = mutableListOf()
var remoteMediaClient:RemoteMediaClient? = null var remoteMediaClient:RemoteMediaClient? = null
var sessionAvailabilityListener:SessionAvailabilityListener? = null var sessionAvailabilityListener:SessionAvailabilityListener? = null
@@ -76,7 +81,10 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
COMMAND_GET_MEDIA_ITEMS_METADATA, COMMAND_GET_MEDIA_ITEMS_METADATA,
COMMAND_SET_MEDIA_ITEMS_METADATA, COMMAND_SET_MEDIA_ITEMS_METADATA,
COMMAND_CHANGE_MEDIA_ITEMS, COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_GET_TRACKS) COMMAND_GET_TRACKS,
COMMAND_GET_DEVICE_VOLUME,
COMMAND_SET_DEVICE_VOLUME,
COMMAND_ADJUST_DEVICE_VOLUME)
.build() .build()
var currentPlaybackState = Player.STATE_IDLE var currentPlaybackState = Player.STATE_IDLE
@@ -861,27 +869,66 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
} }
override fun getDeviceVolume(): Int { override fun getDeviceVolume(): Int {
return 0 // Short-lived cache without even-value quantization
val now = System.currentTimeMillis()
if (pendingVolume != null && (now - pendingVolumeSetAt) < 100) {
return pendingVolume!!
}
val session = castContext.sessionManager.currentCastSession
val vol = if (session != null && !session.isMute) {
(session.volume * 100).roundToInt().coerceIn(0, 100)
} else {
0
}
if (pendingVolume != null && vol == pendingVolume) {
pendingVolume = null
}
return vol
} }
override fun isDeviceMuted(): Boolean { override fun isDeviceMuted(): Boolean {
return false val session = castContext.sessionManager.currentCastSession
return session?.isMute ?: false
} }
override fun setDeviceVolume(volume: Int) { override fun setDeviceVolume(volume: Int) {
val session = castContext.sessionManager.currentCastSession
try {
val clamped = volume.coerceIn(0, 100)
val target = (clamped / 100.0).let { kotlin.math.round(it * 100.0) / 100.0 }
// Cache for 100ms to return in getDeviceVolume during async period
pendingVolume = clamped
pendingVolumeSetAt = System.currentTimeMillis()
session?.setVolume(target)
} catch (e: Exception) {
Log.e(tag, "Failed to set cast device volume", e)
}
} }
override fun increaseDeviceVolume() { override fun increaseDeviceVolume() {
val current = getDeviceVolume()
val next = current + 1
setDeviceVolume(next.coerceAtMost(100))
} }
override fun decreaseDeviceVolume() { override fun decreaseDeviceVolume() {
val current = getDeviceVolume()
val next = current - 1
setDeviceVolume(next.coerceAtLeast(0))
} }
override fun setDeviceMuted(muted: Boolean) { override fun setDeviceMuted(muted: Boolean) {
val session = castContext.sessionManager.currentCastSession
try {
session?.isMute = muted
} catch (e: Exception) {
Log.e(tag, "Failed to set cast device mute state", e)
}
} }
inner class StatusListener() : RemoteMediaClient.Callback(), SessionManagerListener<CastSession>, RemoteMediaClient.ProgressListener { inner class StatusListener() : RemoteMediaClient.Callback(), SessionManagerListener<CastSession>, RemoteMediaClient.ProgressListener {
@@ -18,6 +18,8 @@ import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaControllerCompat import android.support.v4.media.session.MediaControllerCompat
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import androidx.media.VolumeProviderCompat
import android.media.AudioManager
import android.support.v4.media.session.PlaybackStateCompat import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
@@ -94,6 +96,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private lateinit var mediaSessionConnector: MediaSessionConnector private lateinit var mediaSessionConnector: MediaSessionConnector
private lateinit var playerNotificationManager: PlayerNotificationManager private lateinit var playerNotificationManager: PlayerNotificationManager
lateinit var mediaSession: MediaSessionCompat lateinit var mediaSession: MediaSessionCompat
private var remoteVolumeProvider: VolumeProviderCompat? = null
private lateinit var transportControls: MediaControllerCompat.TransportControls private lateinit var transportControls: MediaControllerCompat.TransportControls
lateinit var mediaManager: MediaManager lateinit var mediaManager: MediaManager
@@ -477,6 +480,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
val dataSourceFactory = DefaultDataSource.Factory(ctx) val dataSourceFactory = DefaultDataSource.Factory(ctx)
val extractorsFactory = DefaultExtractorsFactory() val extractorsFactory = DefaultExtractorsFactory()
extractorsFactory.setConstantBitrateSeekingEnabled(true)
if (DeviceManager.deviceData.deviceSettings?.enableMp3IndexSeeking == true) { if (DeviceManager.deviceData.deviceSettings?.enableMp3IndexSeeking == true) {
// @see // @see
// https://exoplayer.dev/troubleshooting.html#why-is-seeking-inaccurate-in-some-mp3-files // https://exoplayer.dev/troubleshooting.html#why-is-seeking-inaccurate-in-some-mp3-files
@@ -491,6 +496,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
val dataSourceFactory = DefaultHttpDataSource.Factory() val dataSourceFactory = DefaultHttpDataSource.Factory()
val extractorsFactory = DefaultExtractorsFactory() val extractorsFactory = DefaultExtractorsFactory()
extractorsFactory.setConstantBitrateSeekingEnabled(true)
if (DeviceManager.deviceData.deviceSettings?.enableMp3IndexSeeking == true) { if (DeviceManager.deviceData.deviceSettings?.enableMp3IndexSeeking == true) {
// @see // @see
// https://exoplayer.dev/troubleshooting.html#why-is-seeking-inaccurate-in-some-mp3-files // https://exoplayer.dev/troubleshooting.html#why-is-seeking-inaccurate-in-some-mp3-files
@@ -705,11 +712,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
Log.d(tag, "switchToPlayer: Using Cast Player " + castPlayer?.deviceInfo) Log.d(tag, "switchToPlayer: Using Cast Player " + castPlayer?.deviceInfo)
mediaSessionConnector.setPlayer(castPlayer) mediaSessionConnector.setPlayer(castPlayer)
playerNotificationManager.setPlayer(castPlayer) playerNotificationManager.setPlayer(castPlayer)
setMediaSessionToCastVolume()
castPlayer as CastPlayer castPlayer as CastPlayer
} else { } else {
Log.d(tag, "switchToPlayer: Using ExoPlayer") Log.d(tag, "switchToPlayer: Using ExoPlayer")
mediaSessionConnector.setPlayer(mPlayer) mediaSessionConnector.setPlayer(mPlayer)
playerNotificationManager.setPlayer(mPlayer) playerNotificationManager.setPlayer(mPlayer)
setMediaSessionToLocalVolume()
mPlayer mPlayer
} }
@@ -726,6 +735,33 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} }
} }
private fun setMediaSessionToCastVolume() {
val currentVol = try { castPlayer?.getDeviceVolume() ?: 0 } catch (_: Exception) { 0 }
val provider = object : VolumeProviderCompat(VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE, 100, currentVol) {
override fun onSetVolumeTo(volume: Int) {
// Clamp, update UI immediately, then send to device
val clamped = volume.coerceIn(0, 100)
setCurrentVolume(clamped)
try { castPlayer?.setDeviceVolume(clamped) } catch (_: Exception) {}
}
override fun onAdjustVolume(direction: Int) {
// Use Android-provided step (1, 0, +1). Clamp, update UI immediately, then send.
val current = currentVolume
val target = (current + direction).coerceIn(0, 100)
setCurrentVolume(target)
try { castPlayer?.setDeviceVolume(target) } catch (_: Exception) {}
}
}
remoteVolumeProvider = provider
mediaSession.setPlaybackToRemote(provider)
}
private fun setMediaSessionToLocalVolume() {
mediaSession.setPlaybackToLocal(AudioManager.STREAM_MUSIC)
remoteVolumeProvider = null
}
fun getCurrentTrackStartOffsetMs(): Long { fun getCurrentTrackStartOffsetMs(): Long {
return if (currentPlayer.mediaItemCount > 1) { return if (currentPlayer.mediaItemCount > 1) {
val windowIndex = currentPlayer.currentMediaItemIndex val windowIndex = currentPlayer.currentMediaItemIndex
@@ -33,6 +33,9 @@ class AbsAudioPlayer : Plugin() {
private var isCastAvailable:Boolean = false private var isCastAvailable:Boolean = false
// Track foreground state to avoid flooding WebView with events while backgrounded
private var isInForeground: Boolean = true
override fun load() { override fun load() {
mainActivity = (activity as MainActivity) mainActivity = (activity as MainActivity)
apiHandler = ApiHandler(mainActivity) apiHandler = ApiHandler(mainActivity)
@@ -60,6 +63,8 @@ class AbsAudioPlayer : Plugin() {
} }
override fun onMetadata(metadata: PlaybackMetadata) { override fun onMetadata(metadata: PlaybackMetadata) {
// Skip frequent metadata updates when app is backgrounded to prevent event queue buildup
if (!isInForeground) return
notifyListeners("onMetadata", JSObject(jacksonMapper.writeValueAsString(metadata))) notifyListeners("onMetadata", JSObject(jacksonMapper.writeValueAsString(metadata)))
} }
@@ -68,6 +73,8 @@ class AbsAudioPlayer : Plugin() {
} }
override fun onSleepTimerSet(sleepTimeRemaining: Int, isAutoSleepTimer:Boolean) { override fun onSleepTimerSet(sleepTimeRemaining: Int, isAutoSleepTimer:Boolean) {
// Skip sleep timer updates when app is backgrounded to prevent event queue buildup
if (!isInForeground) return
val ret = JSObject() val ret = JSObject()
ret.put("value", sleepTimeRemaining) ret.put("value", sleepTimeRemaining)
ret.put("isAuto", isAutoSleepTimer) ret.put("isAuto", isAutoSleepTimer)
@@ -75,6 +82,8 @@ class AbsAudioPlayer : Plugin() {
} }
override fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress) { override fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress) {
// Skip progress updates when app is backgrounded to prevent event queue buildup
if (!isInForeground) return
notifyListeners("onLocalMediaProgressUpdate", JSObject(jacksonMapper.writeValueAsString(localMediaProgress))) notifyListeners("onLocalMediaProgressUpdate", JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
} }
@@ -118,6 +127,27 @@ class AbsAudioPlayer : Plugin() {
notifyListeners(evtName, ret) notifyListeners(evtName, ret)
} }
override fun handleOnPause() {
super.handleOnPause()
isInForeground = false
}
override fun handleOnResume() {
super.handleOnResume()
isInForeground = true
// Send current state to UI after resume to sync up (with small delay to let WebView fully resume)
if (::playerNotificationService.isInitialized && playerNotificationService.currentPlaybackSession != null) {
Handler(Looper.getMainLooper()).postDelayed({
playerNotificationService.sendClientMetadata(PlayerState.READY)
playerNotificationService.sleepTimerManager.sendCurrentSleepTimerState()
playerNotificationService.mediaProgressSyncer.currentLocalMediaProgress?.let {
playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it)
}
}, 100)
}
}
private fun initCastManager() { private fun initCastManager() {
val googleApi = GoogleApiAvailability.getInstance() val googleApi = GoogleApiAvailability.getInstance()
val statusCode = googleApi.isGooglePlayServicesAvailable(mainActivity) val statusCode = googleApi.isGooglePlayServicesAvailable(mainActivity)
@@ -11,4 +11,4 @@
android:targetCellWidth="4" android:targetCellWidth="4"
android:targetCellHeight="1" android:targetCellHeight="1"
android:updatePeriodMillis="86400000" android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen" /> android:widgetCategory="home_screen|keyguard" />
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<samsung-appwidget-provider
display="sub_screen"
privacyWidget="false" />
+7 -5
View File
@@ -39,11 +39,11 @@ class WrappingMarquee {
this.setMask(true) this.setMask(true)
let textScrollAmount = this.el.scrollWidth let textScrollAmount = this.el.scrollWidth
this.pEl.innerHTML = this.innerText + '&nbsp;'.repeat(15) this.pEl.textContent = this.innerText + '\u00A0'.repeat(15) // \u00A0 = non-breaking space
let totalScrollAmount = this.el.scrollWidth let totalScrollAmount = this.el.scrollWidth
let scrollDuration = totalScrollAmount * this.#scrollSpeed let scrollDuration = totalScrollAmount * this.#scrollSpeed
this.pEl.innerHTML = this.pEl.innerHTML + this.innerText this.pEl.textContent = this.pEl.textContent + this.innerText
let done = false let done = false
let start, previousTimeStamp let start, previousTimeStamp
@@ -55,13 +55,14 @@ class WrappingMarquee {
const elapsed = timeStamp - start const elapsed = timeStamp - start
if (this.isScrolling && previousTimeStamp !== timeStamp) { if (this.isScrolling && previousTimeStamp !== timeStamp) {
const amountToMove = Math.min(elapsed / scrollDuration * totalScrollAmount, totalScrollAmount) const amountToMove = Math.min((elapsed / scrollDuration) * totalScrollAmount, totalScrollAmount)
this.pEl.style.transform = `translateX(-${amountToMove}px)` this.pEl.style.transform = `translateX(-${amountToMove}px)`
if (amountToMove === totalScrollAmount) done = true if (amountToMove === totalScrollAmount) done = true
if (amountToMove > textScrollAmount) this.setMask(false) if (amountToMove > textScrollAmount) this.setMask(false)
} }
if (!this.isScrolling || done) { // canceled or done if (!this.isScrolling || done) {
// canceled or done
this.isScrolling = false this.isScrolling = false
this.pEl.style.transform = 'translateX(0px)' this.pEl.style.transform = 'translateX(0px)'
this.pEl.innerText = this.innerText this.pEl.innerText = this.innerText
@@ -69,7 +70,8 @@ class WrappingMarquee {
if (done) { if (done) {
this.startTimer() this.startTimer()
} }
} else if (elapsed < scrollDuration) { // step } else if (elapsed < scrollDuration) {
// step
previousTimeStamp = timeStamp previousTimeStamp = timeStamp
this.animationId = window.requestAnimationFrame(step) this.animationId = window.requestAnimationFrame(step)
} }
+40 -18
View File
@@ -69,16 +69,22 @@
<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="playerSettings.lockUi ? 'justify-center' : 'justify-between'"> <div class="flex items-center max-w-full" :class="playerSettings.lockUi ? 'justify-center' : 'justify-between'">
<span v-show="showFullscreen && !playerSettings.lockUi" class="material-symbols 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-symbols next-icon text-fg cursor-pointer" :class="showLoadingState ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
<span v-show="!playerSettings.lockUi" class="material-symbols jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">{{ jumpBackwardsIcon }}</span> <div v-show="!playerSettings.lockUi" class="jump-icon text-fg cursor-pointer flex flex-col items-center" :class="showLoadingState ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">
<span class="material-symbols text-3xl leading-none">replay</span>
<span v-if="showFullscreen" class="jump-label text-[10px] font-semibold leading-tight">{{ jumpBackwardsLabel }}</span>
</div>
<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-symbols fill" :class="{ 'text-white': coverRgb && !coverBgIsLight }">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span> <span v-if="!showLoadingState" class="material-symbols fill" :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="!playerSettings.lockUi" class="material-symbols jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">{{ jumpForwardIcon }}</span> <div v-show="!playerSettings.lockUi" class="jump-icon text-fg cursor-pointer flex flex-col items-center" :class="showLoadingState ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">
<span v-show="showFullscreen && !playerSettings.lockUi" class="material-symbols next-icon text-fg cursor-pointer" :class="nextChapter && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span> <span class="material-symbols text-3xl leading-none">forward_media</span>
<span v-if="showFullscreen" class="jump-label text-[10px] font-semibold leading-tight">{{ jumpForwardLabel }}</span>
</div>
<span v-show="showFullscreen && !playerSettings.lockUi" class="material-symbols next-icon text-fg cursor-pointer" :class="nextChapter && !showLoadingState ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
</div> </div>
</div> </div>
@@ -88,7 +94,7 @@
<div class="flex-grow" /> <div class="flex-grow" />
<p class="font-mono text-fg" style="font-size: 0.8rem">{{ timeRemainingPretty }}</p> <p class="font-mono text-fg" style="font-size: 0.8rem">{{ timeRemainingPretty }}</p>
</div> </div>
<div ref="track" class="h-1.5 w-full bg-track/50 relative rounded-full" :class="{ 'animate-pulse': isLoading }" @click.stop> <div ref="track" class="h-1.5 w-full bg-track/50 relative rounded-full" :class="{ 'animate-pulse': showLoadingState }" @click.stop>
<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" />
@@ -110,6 +116,7 @@ import { AbsAudioPlayer } from '@/plugins/capacitor'
import { Dialog } from '@capacitor/dialog' import { Dialog } from '@capacitor/dialog'
import { FastAverageColor } from 'fast-average-color' import { FastAverageColor } from 'fast-average-color'
import WrappingMarquee from '@/assets/WrappingMarquee.js' import WrappingMarquee from '@/assets/WrappingMarquee.js'
import jumpLabelMixin from '@/mixins/jumpLabel'
export default { export default {
props: { props: {
@@ -121,6 +128,7 @@ export default {
sleepTimeRemaining: Number, sleepTimeRemaining: Number,
serverLibraryItemId: String serverLibraryItemId: String
}, },
mixins: [jumpLabelMixin],
data() { data() {
return { return {
windowHeight: 0, windowHeight: 0,
@@ -149,6 +157,7 @@ export default {
lockUi: false lockUi: false
}, },
isLoading: false, isLoading: false,
isCheckingServerProgress: false,
isDraggingCursor: false, isDraggingCursor: false,
draggingTouchStartX: 0, draggingTouchStartX: 0,
draggingTouchStartTime: 0, draggingTouchStartTime: 0,
@@ -221,11 +230,11 @@ export default {
return items return items
}, },
jumpForwardIcon() { jumpForwardLabel() {
return this.$store.getters['globals/getJumpForwardIcon'](this.jumpForwardTime) return this.getJumpLabel(this.jumpForwardTime)
}, },
jumpBackwardsIcon() { jumpBackwardsLabel() {
return this.$store.getters['globals/getJumpBackwardsIcon'](this.jumpBackwardsTime) return this.getJumpLabel(this.jumpBackwardsTime)
}, },
jumpForwardTime() { jumpForwardTime() {
return this.$store.getters['getJumpForwardTime'] return this.$store.getters['getJumpForwardTime']
@@ -262,6 +271,9 @@ export default {
return 190 * heightScale return 190 * heightScale
} }
}, },
showLoadingState() {
return this.isLoading || this.isCheckingServerProgress
},
showCastBtn() { showCastBtn() {
return this.$store.state.isCastAvailable return this.$store.state.isCastAvailable
}, },
@@ -453,13 +465,13 @@ export default {
}, },
async jumpNextChapter() { async jumpNextChapter() {
await this.$hapticsImpact() await this.$hapticsImpact()
if (this.isLoading) return if (this.showLoadingState) return
if (!this.nextChapter) return if (!this.nextChapter) return
this.seek(this.nextChapter.start) this.seek(this.nextChapter.start)
}, },
async jumpChapterStart() { async jumpChapterStart() {
await this.$hapticsImpact() await this.$hapticsImpact()
if (this.isLoading) return if (this.showLoadingState) return
if (!this.currentChapter) { if (!this.currentChapter) {
return this.restart() return this.restart()
} }
@@ -489,12 +501,12 @@ export default {
}, },
async jumpBackwards() { async jumpBackwards() {
await this.$hapticsImpact() await this.$hapticsImpact()
if (this.isLoading) return if (this.showLoadingState) return
AbsAudioPlayer.seekBackward({ value: this.jumpBackwardsTime }) AbsAudioPlayer.seekBackward({ value: this.jumpBackwardsTime })
}, },
async jumpForward() { async jumpForward() {
await this.$hapticsImpact() await this.$hapticsImpact()
if (this.isLoading) return if (this.showLoadingState) return
AbsAudioPlayer.seekForward({ value: this.jumpForwardTime }) AbsAudioPlayer.seekForward({ value: this.jumpForwardTime })
}, },
setStreamReady() { setStreamReady() {
@@ -581,8 +593,12 @@ export default {
} }
const ptWidth = Math.round(percentDone * this.trackWidth) const ptWidth = Math.round(percentDone * this.trackWidth)
this.$refs.playedTrack.style.width = ptWidth + 'px' if (this.$refs.playedTrack) {
this.$refs.bufferedTrack.style.width = Math.round(bufferedPercent * this.trackWidth) + 'px' this.$refs.playedTrack.style.width = ptWidth + 'px'
}
if (this.$refs.bufferedTrack) {
this.$refs.bufferedTrack.style.width = Math.round(bufferedPercent * this.trackWidth) + 'px'
}
if (this.$refs.trackCursor) { if (this.$refs.trackCursor) {
this.$refs.trackCursor.style.left = ptWidth - 14 + 'px' this.$refs.trackCursor.style.left = ptWidth - 14 + 'px'
@@ -594,7 +610,7 @@ export default {
} }
}, },
seek(time) { seek(time) {
if (this.isLoading) return if (this.showLoadingState) return
if (this.seekLoading) { if (this.seekLoading) {
console.error('Already seek loading', this.seekedTime) console.error('Already seek loading', this.seekedTime)
return return
@@ -626,11 +642,14 @@ export default {
}, },
async playPauseClick() { async playPauseClick() {
await this.$hapticsImpact() await this.$hapticsImpact()
if (this.isLoading) return if (this.showLoadingState) return
this.isPlaying = !!((await AbsAudioPlayer.playPause()) || {}).playing this.isPlaying = !!((await AbsAudioPlayer.playPause()) || {}).playing
this.isEnded = false this.isEnded = false
}, },
setIsCheckingServerProgress(value) {
this.isCheckingServerProgress = !!value
},
play() { play() {
AbsAudioPlayer.playPlayer() AbsAudioPlayer.playPlayer()
this.startPlayInterval() this.startPlayInterval()
@@ -1087,6 +1106,9 @@ export default {
margin: 0px 0px; margin: 0px 0px;
font-size: 1.6rem; font-size: 1.6rem;
} }
#playerControls .jump-label {
margin-top: 2px;
}
#playerControls .play-btn { #playerControls .play-btn {
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1); transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
transition-property: padding, margin, height, width, min-width, min-height; transition-property: padding, margin, height, width, min-width, min-height;
+125 -39
View File
@@ -48,6 +48,9 @@ export default {
}, },
isIos() { isIos() {
return this.$platform === 'ios' return this.$platform === 'ios'
},
currentPlaybackSession() {
return this.$store.state.currentPlaybackSession
} }
}, },
methods: { methods: {
@@ -304,48 +307,129 @@ export default {
this.$refs.audioPlayer?.seek(currentTime) this.$refs.audioPlayer?.seek(currentTime)
}, },
/** /**
* When device gains focus then refresh the timestamps in the audio player * Fetch the current user's media progress from the server for a given library item / episode.
* Returns the server media progress object, or null if the request fails, times out, or the
* response doesn't match the requested library item.
*
* The audio player's loading state is shown while the request is in flight so the user
* doesn't tap play before we have a chance to update the timestamps. The request timeout
* is 7 seconds so a slow/unresponsive server doesn't block the user for long.
*/ */
deviceFocused(hasFocus) { async getServerMediaProgressForCurrentSession() {
if (!this.$store.state.currentPlaybackSession) return if (!this.$store.state.user.user || !this.$store.state.networkConnected) return null
const libraryItemId = this.currentPlaybackSession?.libraryItemId
const episodeId = this.currentPlaybackSession?.episodeId
if (!libraryItemId) return null
if (hasFocus) { if (this.$refs.audioPlayer?.isCheckingServerProgress) {
console.log('[AudioPlayerContainer] getServerMediaProgressForCurrentSession: already checking server progress')
return null
}
const url = episodeId ? `/api/me/progress/${libraryItemId}/${episodeId}` : `/api/me/progress/${libraryItemId}`
this.$refs.audioPlayer?.setIsCheckingServerProgress(true)
try {
const data = await this.$nativeHttp.get(url, { connectTimeout: 7000, readTimeout: 7000 })
if (!data || data.libraryItemId !== libraryItemId) return null
return data
} catch (error) {
console.error('[AudioPlayerContainer] Failed to get server media progress', error)
return null
} finally {
this.$refs.audioPlayer?.setIsCheckingServerProgress(false)
}
},
getLocalMediaProgressForCurrentSession() {
if (!this.currentPlaybackSession) return null
return this.$store.getters['globals/getLocalMediaProgressById'](this.currentPlaybackSession.localLibraryItem?.id, this.currentPlaybackSession.localEpisodeId)
},
/**
* Sync the server media progress with the local media progress
*/
async syncServerMediaProgressWithLocalMediaProgress(localMediaProgressId, serverMediaProgress) {
try {
const newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress({
localMediaProgressId,
mediaProgress: serverMediaProgress
})
if (newLocalMediaProgress?.id) {
this.$store.commit('globals/updateLocalMediaProgress', newLocalMediaProgress)
}
} catch (error) {
console.error('[AudioPlayerContainer] Failed to sync server progress with local media progress', error)
}
},
/**
* Check if the server media progress is more recent than the local media progress and sync if so
*/
async checkSyncServerProgressWithLocalProgress(localMediaProgress) {
if (!localMediaProgress) return
console.log('[AudioPlayerContainer] checkSyncServerProgressWithLocalProgress: checking server media progress for local media item open in player')
const serverMediaProgress = await this.getServerMediaProgressForCurrentSession()
if (!serverMediaProgress?.lastUpdate || serverMediaProgress.lastUpdate <= localMediaProgress.lastUpdate) return
console.log('[AudioPlayerContainer] checkSyncServerProgressWithLocalProgress: server progress is more recent than local progress. Server current time:', serverMediaProgress.currentTime, 'vs local', localMediaProgress.currentTime, `(server lastUpdate=${serverMediaProgress.lastUpdate} > local lastUpdate=${localMediaProgress.lastUpdate})`)
if (!this.$refs.audioPlayer?.isPlaying && serverMediaProgress.currentTime !== localMediaProgress.currentTime) {
// Use seek() so the native audio player's current session is updated
this.$refs.audioPlayer.seek(serverMediaProgress.currentTime)
}
await this.syncServerMediaProgressWithLocalMediaProgress(localMediaProgress.id, serverMediaProgress)
},
/**
* When socket is reconnected after a delay, if a local media item is open in the player (paused)
* we fetch the server media progress and sync it if it is more recent than the local progress
*
* If there is no socket connection we may have missed external progress updates
*/
async socketReconnected() {
if (!this.currentPlaybackSession) return
// dont update timestamps if player is playing
if (this.$refs.audioPlayer?.isPlaying) return
if (this.$refs.audioPlayer.isLocalPlayMethod) {
const localMediaProgress = this.getLocalMediaProgressForCurrentSession()
if (!localMediaProgress) {
console.error('[AudioPlayerContainer] socket reconnected: Local media progress not found')
return
}
await this.checkSyncServerProgressWithLocalProgress(localMediaProgress)
}
},
/**
* When device re-gains focus then refresh the timestamps in the audio player
* if local item is open then fetch the server media progress and update if more recent
*/
async deviceFocused(hasFocus) {
if (!this.currentPlaybackSession || !hasFocus) return
// dont update timestamps if player is playing
if (this.$refs.audioPlayer?.isPlaying) return
if (this.$refs.audioPlayer.isLocalPlayMethod) {
const localMediaProgress = this.getLocalMediaProgressForCurrentSession()
if (!localMediaProgress) {
console.error('[AudioPlayerContainer] device visibility: Local media progress not found')
return
}
console.log('[AudioPlayerContainer] device visibility: found local media progress', localMediaProgress.currentTime, 'last time in player is', this.currentTime)
this.$refs.audioPlayer.currentTime = localMediaProgress.currentTime
this.$refs.audioPlayer.timeupdate()
await this.checkSyncServerProgressWithLocalProgress(localMediaProgress)
} else {
// server item so fetch server media progress and update player time
console.log('[AudioPlayerContainer] device visibility: checking server media progress for server media item open in player')
const data = await this.getServerMediaProgressForCurrentSession()
if (!data) return
if (!this.$refs.audioPlayer?.isPlaying) { if (!this.$refs.audioPlayer?.isPlaying) {
const playbackSession = this.$store.state.currentPlaybackSession console.log('[AudioPlayerContainer] device visibility: got server media progress', data.currentTime, 'last time in player is', this.currentTime)
if (this.$refs.audioPlayer.isLocalPlayMethod) { // Only seek if the difference is greater than 1 second
const localLibraryItemId = playbackSession.localLibraryItem?.id if (Math.abs(data.currentTime - this.currentTime) > 1) {
const localEpisodeId = playbackSession.localEpisodeId // Use seek() so the native audio player's current session is updated
if (!localLibraryItemId) { this.$refs.audioPlayer.seek(data.currentTime)
console.error('[AudioPlayerContainer] device visibility: no local library item for session', JSON.stringify(playbackSession))
return
}
const localMediaProgress = this.$store.state.globals.localMediaProgress.find((mp) => {
if (localEpisodeId) return mp.localEpisodeId === localEpisodeId
return mp.localLibraryItemId === localLibraryItemId
})
if (localMediaProgress) {
console.log('[AudioPlayerContainer] device visibility: found local media progress', localMediaProgress.currentTime, 'last time in player is', this.currentTime)
this.$refs.audioPlayer.currentTime = localMediaProgress.currentTime
this.$refs.audioPlayer.timeupdate()
} else {
console.error('[AudioPlayerContainer] device visibility: Local media progress not found')
}
} else {
const libraryItemId = playbackSession.libraryItemId
const episodeId = playbackSession.episodeId
const url = episodeId ? `/api/me/progress/${libraryItemId}/${episodeId}` : `/api/me/progress/${libraryItemId}`
this.$nativeHttp
.get(url)
.then((data) => {
if (!this.$refs.audioPlayer?.isPlaying && data.libraryItemId === libraryItemId) {
console.log('[AudioPlayerContainer] device visibility: got server media progress', data.currentTime, 'last time in player is', this.currentTime)
this.$refs.audioPlayer.currentTime = data.currentTime
this.$refs.audioPlayer.timeupdate()
}
})
.catch((error) => {
console.error('[AudioPlayerContainer] device visibility: Failed to get progress', error)
})
} }
} }
} }
@@ -368,6 +452,7 @@ export default {
this.$eventBus.$on('user-settings', this.settingsUpdated) this.$eventBus.$on('user-settings', this.settingsUpdated)
this.$eventBus.$on('playback-time-update', this.playbackTimeUpdate) this.$eventBus.$on('playback-time-update', this.playbackTimeUpdate)
this.$eventBus.$on('device-focus-update', this.deviceFocused) this.$eventBus.$on('device-focus-update', this.deviceFocused)
this.$eventBus.$on('socket-reconnected', this.socketReconnected)
}, },
beforeDestroy() { beforeDestroy() {
this.onLocalMediaProgressUpdateListener?.remove() this.onLocalMediaProgressUpdateListener?.remove()
@@ -383,6 +468,7 @@ export default {
this.$eventBus.$off('user-settings', this.settingsUpdated) this.$eventBus.$off('user-settings', this.settingsUpdated)
this.$eventBus.$off('playback-time-update', this.playbackTimeUpdate) this.$eventBus.$off('playback-time-update', this.playbackTimeUpdate)
this.$eventBus.$off('device-focus-update', this.deviceFocused) this.$eventBus.$off('device-focus-update', this.deviceFocused)
this.$eventBus.$off('socket-reconnected', this.socketReconnected)
} }
} }
</script> </script>
+16 -14
View File
@@ -3,10 +3,7 @@
<div class="absolute top-0 left-0 w-full h-full bg-black transition-opacity duration-200" :class="show ? 'bg-opacity-60 pointer-events-auto' : 'bg-opacity-0'" @click="clickBackground" /> <div class="absolute top-0 left-0 w-full h-full bg-black transition-opacity duration-200" :class="show ? 'bg-opacity-60 pointer-events-auto' : 'bg-opacity-0'" @click="clickBackground" />
<div class="absolute top-0 right-0 w-64 h-full bg-bg transform transition-transform py-6 pointer-events-auto" :class="show ? '' : 'translate-x-64'" @click.stop> <div class="absolute top-0 right-0 w-64 h-full bg-bg transform transition-transform py-6 pointer-events-auto" :class="show ? '' : 'translate-x-64'" @click.stop>
<div class="px-6 mb-4"> <div class="px-6 mb-4">
<p v-if="user" class="text-base"> <p v-if="user" class="text-base" v-html="$getString('HeaderWelcome', [username])" />
Welcome,
<strong>{{ username }}</strong>
</p>
</div> </div>
<div class="w-full overflow-y-auto"> <div class="w-full overflow-y-auto">
@@ -134,14 +131,12 @@ export default {
to: '/settings' to: '/settings'
}) })
if (this.$platform !== 'ios') { items.push({
items.push({ icon: 'bug_report',
icon: 'bug_report', iconOutlined: true,
iconOutlined: true, text: this.$strings.ButtonLogs,
text: this.$strings.ButtonLogs, to: '/logs'
to: '/logs' })
})
}
if (this.serverConnectionConfig) { if (this.serverConnectionConfig) {
items.push({ items.push({
@@ -185,11 +180,18 @@ export default {
await this.$hapticsImpact() await this.$hapticsImpact()
await this.logout() await this.logout()
// Redirect to home page
if (this.$route.name !== 'bookshelf') { if (this.$route.name !== 'bookshelf') {
this.$router.replace('/bookshelf') this.$router.replace('/bookshelf')
} else {
location.reload()
} }
// If player is open and not playing locally, then close the player
if (this.$store.getters['getIsPlayerOpen']) {
this.$eventBus.$emit('close-stream')
}
// Close side drawer
this.show = false
}, },
touchstart(e) { touchstart(e) {
this.touchEvent = new TouchEvent(e) this.touchEvent = new TouchEvent(e)
+69
View File
@@ -0,0 +1,69 @@
<template>
<div class="flex h-full px-1 overflow-hidden">
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div class="grow px-2 audiobookSearchCardContent">
<p class="truncate text-sm">{{ title }}</p>
<p v-if="subtitle" class="truncate text-xs text-gray-300">{{ subtitle }}</p>
<p class="text-xs text-gray-200 truncate">{{ $getString('LabelByAuthor', [authorName]) }}</p>
</div>
</div>
</template>
<script>
export default {
props: {
episode: {
type: Object,
default: () => {}
},
libraryItem: {
type: Object,
default: () => {}
}
},
data() {
return {}
},
computed: {
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
coverWidth() {
if (this.bookCoverAspectRatio === 1) return 50 * 1.2
return 50
},
media() {
return this.libraryItem ? this.libraryItem.media || {} : {}
},
mediaMetadata() {
return this.media.metadata || {}
},
mediaType() {
return this.libraryItem ? this.libraryItem.mediaType : null
},
isPodcast() {
return this.mediaType === 'podcast'
},
title() {
return this.episode?.title || 'No Title'
},
subtitle() {
return this.mediaMetadata.title
},
authorName() {
return this.mediaMetadata.author
}
},
methods: {},
mounted() {}
}
</script>
<style>
.audiobookSearchCardContent {
width: calc(100% - 80px);
display: flex;
flex-direction: column;
justify-content: center;
}
</style>
+5 -42
View File
@@ -1,16 +1,10 @@
<template> <template>
<div class="flex h-full px-1 overflow-hidden"> <div class="flex h-full px-1 overflow-hidden">
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div class="flex-grow px-2 audiobookSearchCardContent"> <div class="grow px-2 audiobookSearchCardContent">
<p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p> <p class="truncate text-sm">{{ title }}</p>
<p v-else class="truncate text-sm" v-html="matchHtml" /> <p v-if="subtitle" class="truncate text-xs text-gray-300">{{ subtitle }}</p>
<p class="text-xs text-gray-200 truncate">{{ $getString('LabelByAuthor', [authorName]) }}</p>
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-fg-muted">{{ matchHtml }}</p>
<p v-if="matchKey !== 'authors'" class="text-xs text-fg truncate">by {{ authorName }}</p>
<p v-else class="truncate text-xs text-fg" v-html="matchHtml" />
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin' || matchKey === 'episode' || matchKey === 'narrators'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
</div> </div>
</div> </div>
</template> </template>
@@ -22,9 +16,7 @@ export default {
type: Object, type: Object,
default: () => {} default: () => {}
}, },
search: String, search: String
matchKey: String,
matchText: String
}, },
data() { data() {
return {} return {}
@@ -58,35 +50,6 @@ export default {
authorName() { authorName() {
if (this.isPodcast) return this.mediaMetadata.author if (this.isPodcast) return this.mediaMetadata.author
return this.mediaMetadata.authorName return this.mediaMetadata.authorName
},
matchHtml() {
if (!this.matchText || !this.search) return ''
if (this.matchKey === 'subtitle') return ''
var matchSplit = this.matchText.toLowerCase().split(this.search.toLowerCase().trim())
if (matchSplit.length < 2) return ''
var html = ''
var totalLenSoFar = 0
for (let i = 0; i < matchSplit.length - 1; i++) {
var indexOf = matchSplit[i].length
var firstPart = this.matchText.substr(totalLenSoFar, indexOf)
var actualWasThere = this.matchText.substr(totalLenSoFar + indexOf, this.search.length)
totalLenSoFar += indexOf + this.search.length
html += `${firstPart}<strong class="text-warning">${actualWasThere}</strong>`
}
var lastPart = this.matchText.substr(totalLenSoFar)
html += lastPart
if (this.matchKey === 'episode') return `<p class="truncate">Episode: ${html}</p>`
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
if (this.matchKey === 'subtitle') return `<p class="truncate">${html}</p>`
if (this.matchKey === 'authors') return `by ${html}`
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
if (this.matchKey === 'series') return `<p class="truncate">Series: ${html}</p>`
if (this.matchKey === 'narrators') return `<p class="truncate">Narrator: ${html}</p>`
return `${html}`
} }
}, },
methods: {}, methods: {},
+19 -17
View File
@@ -4,13 +4,16 @@
<!-- list of server connection configs --> <!-- list of server connection configs -->
<template v-if="!showForm"> <template v-if="!showForm">
<div v-for="config in serverConnectionConfigs" :key="config.id" class="border-b border-fg/10 py-4"> <div v-for="config in serverConnectionConfigs" :key="config.id" class="border-b border-fg/10 py-4">
<div class="flex items-center my-1 relative" @click="connectToServer(config)"> <div class="flex items-center my-1 relative space-x-2" @click="connectToServer(config)">
<span class="material-symbols text-xl text-fg-muted">dns</span> <div class="grow inline-flex items-center overflow-hidden">
<p class="pl-3 pr-6 text-base text-fg">{{ config.name }}</p> <p class="text-base text-fg truncate">{{ config.name }}</p>
</div>
<div class="absolute top-0 right-0 h-full px-4 flex items-center" @click.stop="editServerConfig(config)"> <div class="h-full w-6 flex items-center" @click.stop="editServerConfig(config)">
<span class="material-symbols text-2xl text-fg-muted">more_vert</span> <span class="material-symbols text-2xl text-fg-muted">more_vert</span>
</div> </div>
<div class="h-full w-6 flex items-center" @click.stop="removeServerConfigClick(config)">
<span class="material-symbols fill text-1.5xl text-fg-muted">delete</span>
</div>
</div> </div>
<!-- warning message if server connection config is using an old user id --> <!-- warning message if server connection config is using an old user id -->
<div v-if="!checkIdUuid(config.userId)" class="flex flex-nowrap justify-between items-center space-x-4 pt-4"> <div v-if="!checkIdUuid(config.userId)" class="flex flex-nowrap justify-between items-center space-x-4 pt-4">
@@ -57,7 +60,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" type="button" @click="removeServerConfigClick" /> <ui-icon-btn v-if="serverConfig.id" bg-color="error" icon="delete" type="button" @click="removeServerConfigClick(serverConfig)" />
<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>
@@ -76,9 +79,6 @@
<div :class="processing ? 'opacity-100' : 'opacity-0 pointer-events-none'" class="fixed w-full h-full top-0 left-0 bg-black/75 flex items-center justify-center z-30 transition-opacity duration-500"> <div :class="processing ? 'opacity-100' : 'opacity-0 pointer-events-none'" class="fixed w-full h-full top-0 left-0 bg-black/75 flex items-center justify-center z-30 transition-opacity duration-500">
<div> <div>
<div class="absolute top-0 left-0 w-full p-6 flex items-center flex-col justify-center z-0 short:hidden">
<img src="/Logo.png" class="h-20 w-20 mb-2" />
</div>
<svg class="animate-spin w-16 h-16" viewBox="0 0 24 24"> <svg class="animate-spin w-16 h-16" 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" />
</svg> </svg>
@@ -478,19 +478,19 @@ export default {
} }
} }
}, },
async removeServerConfigClick() { async removeServerConfigClick(serverConfig) {
if (!this.serverConfig.id) return if (!serverConfig.id) return
await this.$hapticsImpact() await this.$hapticsImpact()
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: `Remove this server config?` message: this.$strings.MessageConfirmDeleteServerConfig,
}) })
if (value) { if (value) {
this.processing = true this.processing = true
await this.$db.removeServerConnectionConfig(this.serverConfig.id) await this.$db.removeServerConnectionConfig(serverConfig.id)
const updatedDeviceData = { ...this.deviceData } const updatedDeviceData = { ...this.deviceData }
updatedDeviceData.serverConnectionConfigs = this.deviceData.serverConnectionConfigs.filter((scc) => scc.id != this.serverConfig.id) updatedDeviceData.serverConnectionConfigs = this.deviceData.serverConnectionConfigs.filter((scc) => scc.id != serverConfig.id)
this.$store.commit('setDeviceData', updatedDeviceData) this.$store.commit('setDeviceData', updatedDeviceData)
this.serverConfig = { this.serverConfig = {
@@ -502,6 +502,7 @@ export default {
this.processing = false this.processing = false
this.showAuth = false this.showAuth = false
this.showForm = !this.serverConnectionConfigs.length this.showForm = !this.serverConnectionConfigs.length
this.error = null
} }
}, },
async editServerConfig(serverConfig) { async editServerConfig(serverConfig) {
@@ -950,8 +951,9 @@ export default {
init() { init() {
if (this.$route.query.serverConnectionConfigId) { if (this.$route.query.serverConnectionConfigId) {
// Handle force re-login for servers using new JWT auth but still using an old token OR refresh token failed // Handle force re-login for servers using new JWT auth but still using an old token OR refresh token failed
this.serverConfig = this.serverConnectionConfigs.find((scc) => scc.id === this.$route.query.serverConnectionConfigId) const _serverConfig = this.serverConnectionConfigs.find((scc) => scc.id === this.$route.query.serverConnectionConfigId)
if (this.serverConfig) { if (_serverConfig) {
this.serverConfig = { ..._serverConfig }
this.setForceRelogin(this.$route.query.error) this.setForceRelogin(this.$route.query.error)
return return
} else { } else {
+44 -13
View File
@@ -1,5 +1,5 @@
<template> <template>
<modals-modal v-model="show" :width="400" height="100%"> <modals-modal v-model="show" :width="400" max-width="95%" height="100%">
<template #outer> <template #outer>
<div v-if="currentChapter" class="absolute top-10 left-4 z-40 pt-1" style="max-width: 80%"> <div v-if="currentChapter" class="absolute top-10 left-4 z-40 pt-1" style="max-width: 80%">
<p class="text-white text-lg truncate">{{ chapters.length }} {{ $strings.LabelChapters }}</p> <p class="text-white text-lg truncate">{{ chapters.length }} {{ $strings.LabelChapters }}</p>
@@ -8,19 +8,31 @@
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false"> <div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-secondary rounded-lg border border-fg/20" style="max-height: 75%" @click.stop> <div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-secondary rounded-lg border border-fg/20" style="max-height: 75%" @click.stop>
<div class="sticky top-0 z-10 bg-secondary grid grid-cols-[1fr_auto_auto] gap-2 px-3 py-2 border-b border-fg/10 transition-shadow" :class="{ 'shadow-md': isScrolled }">
<div>
<p class="text-fg-muted text-sm">{{ $strings.LabelChapters }}</p>
</div>
<div class="text-right" style="min-width: 60px">
<p class="text-fg-muted text-sm" style="letter-spacing: -0.5px">{{ $strings.LabelStart }}</p>
</div>
<div class="text-right" style="min-width: 60px">
<p class="text-fg-muted text-sm" style="letter-spacing: -0.5px">{{ $strings.LabelDuration }}</p>
</div>
</div>
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label"> <ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="chapter in chapters"> <li v-for="chapter in chapters" :key="chapter.id" :id="`chapter-row-${chapter.id}`" class="text-fg select-none relative cursor-pointer" :class="currentChapterId === chapter.id ? 'bg-primary bg-opacity-80' : ''" role="option" @click="clickedOption(chapter)">
<li :key="chapter.id" :id="`chapter-row-${chapter.id}`" class="text-fg select-none relative py-4 cursor-pointer" :class="currentChapterId === chapter.id ? 'bg-primary bg-opacity-80' : ''" role="option" @click="clickedOption(chapter)"> <div class="grid grid-cols-[1fr_auto_auto] gap-2 px-3 py-3 items-start">
<div class="relative flex items-center pl-3 pr-20"> <p class="font-normal line-clamp-2 text-sm text-fg/80">{{ chapter.title }}</p>
<p class="font-normal block truncate text-sm text-fg/80">{{ chapter.title }}</p> <div class="text-right" style="min-width: 60px">
<div class="absolute top-0 right-3 -mt-0.5"> <span class="font-mono text-fg-muted text-sm" style="letter-spacing: -0.5px">{{ $secondsToTimestamp(chapter.start / _playbackRate) }}</span>
<span class="font-mono text-fg-muted leading-3 text-sm" style="letter-spacing: -0.5px">{{ $secondsToTimestamp(chapter.start / _playbackRate) }}</span>
</div>
</div> </div>
<div class="text-right" style="min-width: 60px">
<span class="font-mono text-fg-muted text-sm" style="letter-spacing: -0.5px">{{ $secondsToTimestamp(Math.max(0, chapter.end - chapter.start) / _playbackRate) }}</span>
</div>
</div>
<div v-show="chapter.id === currentChapterId" class="w-0.5 h-full absolute top-0 left-0 bg-yellow-400" /> <div v-show="chapter.id === currentChapterId" class="w-0.5 h-full absolute top-0 left-0 bg-yellow-400" />
</li> </li>
</template>
</ul> </ul>
</div> </div>
</div> </div>
@@ -42,7 +54,9 @@ export default {
playbackRate: Number playbackRate: Number
}, },
data() { data() {
return {} return {
isScrolled: false
}
}, },
watch: { watch: {
value(newVal) { value(newVal) {
@@ -87,8 +101,25 @@ export default {
container.scrollTo({ top: offsetTop - containerHeight / 2 }) container.scrollTo({ top: offsetTop - containerHeight / 2 })
} }
} }
},
handleScroll() {
const container = this.$refs.container
if (container) {
this.isScrolled = container.scrollTop > 0
}
} }
}, },
mounted() {} mounted() {
const container = this.$refs.container
if (container) {
container.addEventListener('scroll', this.handleScroll)
}
},
beforeDestroy() {
const container = this.$refs.container
if (container) {
container.removeEventListener('scroll', this.handleScroll)
}
}
} }
</script> </script>
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<div ref="wrapper" class="modal modal-bg w-screen fixed bottom-0 left-0 flex items-center justify-center z-50" :class="threeQuartersScreen ? 'h-[75vh] min-h-[400px]' : 'h-screen'" @click.stop @touchstart.stop @touchend.stop> <div ref="wrapper" class="modal modal-bg w-screen fixed bottom-0 left-0 flex items-center justify-center z-50" :class="threeQuartersScreen ? 'h-[75vh] min-h-[400px] short:min-h-0 short:h-screen' : 'h-screen'" @click.stop @touchstart.stop @touchend.stop>
<div ref="content" class="relative text-fg h-full w-full bg-bg"> <div ref="content" class="relative text-fg h-full w-full bg-bg">
<slot /> <slot />
</div> </div>
+4 -4
View File
@@ -303,7 +303,7 @@ export default {
// Show confirm if item has progress since it will reset // Show confirm if item has progress since it will reset
if (this.userItemProgress && this.userItemProgress.progress > 0 && !this.userIsFinished) { if (this.userItemProgress && this.userItemProgress.progress > 0 && !this.userIsFinished) {
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: this.$strings.MessageConfirmMarkAsFinished message: this.$strings.MessageConfirmMarkAsFinished
}) })
if (!value) return if (!value) return
@@ -369,7 +369,7 @@ export default {
await this.$hapticsImpact() await this.$hapticsImpact()
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: this.$strings.MessageConfirmDiscardProgress message: this.$strings.MessageConfirmDiscardProgress
}) })
if (value) { if (value) {
@@ -407,7 +407,7 @@ export default {
} }
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: this.$getString('MessageConfirmDeleteLocalEpisode', [localFile.basePath]) message: this.$getString('MessageConfirmDeleteLocalEpisode', [localFile.basePath])
}) })
if (value) { if (value) {
@@ -432,7 +432,7 @@ export default {
await this.$hapticsImpact() await this.$hapticsImpact()
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: this.$strings.MessageConfirmDeleteLocalFiles message: this.$strings.MessageConfirmDeleteLocalFiles
}) })
if (value) { if (value) {
+13 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<modals-modal v-model="show" width="90%"> <modals-modal v-model="show" width="90%">
<div class="w-full h-full bg-primary rounded-lg border border-fg/20"> <div class="w-full h-full bg-primary rounded-lg border border-fg/20">
<ul class="w-full rounded-lg text-base" role="listbox" aria-labelledby="listbox-label"> <ul class="w-full rounded-lg text-base max-h-[70vh] overflow-y-auto overscroll-contain" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in items"> <template v-for="item in items">
<li :key="item.value" class="text-fg select-none relative py-4 pr-9 cursor-pointer" :class="item.value === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedOption(item.value)"> <li :key="item.value" class="text-fg select-none relative py-4 pr-9 cursor-pointer" :class="item.value === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedOption(item.value)">
<div class="flex items-center"> <div class="flex items-center">
@@ -64,6 +64,18 @@ export default {
text: this.$strings.LabelFileModified, text: this.$strings.LabelFileModified,
value: 'mtimeMs' value: 'mtimeMs'
}, },
{
text: this.$strings.LabelLibrarySortByProgress,
value: 'progress'
},
{
text: this.$strings.LabelLibrarySortByProgressStarted,
value: 'progress.createdAt'
},
{
text: this.$strings.LabelLibrarySortByProgressFinished,
value: 'progress.finishedAt'
},
{ {
text: this.$strings.LabelRandomly, text: this.$strings.LabelRandomly,
value: 'random' value: 'random'
+1 -1
View File
@@ -108,7 +108,7 @@ export default {
async cancelSleepTimer() { async cancelSleepTimer() {
if (this.isAuto) { if (this.isAuto) {
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: this.$strings.MessageConfirmDisableAutoTimer message: this.$strings.MessageConfirmDisableAutoTimer
}) })
if (!value) return if (!value) return
+2
View File
@@ -37,6 +37,7 @@ export default {
isRefreshingUI: false, isRefreshingUI: false,
ereaderSettings: { ereaderSettings: {
theme: 'dark', theme: 'dark',
font: 'serif',
fontScale: 100, fontScale: 100,
lineSpacing: 115, lineSpacing: 115,
textStroke: 0 textStroke: 0
@@ -134,6 +135,7 @@ export default {
const fontScale = settings.fontScale || 100 const fontScale = settings.fontScale || 100
this.rendition.themes.fontSize(`${fontScale}%`) this.rendition.themes.fontSize(`${fontScale}%`)
this.rendition.themes.font(settings.font)
this.rendition.spread(settings.spread || 'auto') this.rendition.spread(settings.spread || 'auto')
}, },
goToChapter(href) { goToChapter(href) {
+24 -2
View File
@@ -1,7 +1,7 @@
<template> <template>
<div v-if="show" :data-theme="ereaderTheme" class="group fixed top-0 left-0 right-0 layout-wrapper w-full z-40 pt-8 data-[theme=black]:bg-black data-[theme=black]:text-white data-[theme=dark]:bg-[#232323] data-[theme=dark]:text-white data-[theme=light]:bg-white data-[theme=light]:text-black" :class="{ 'reader-player-open': isPlayerOpen }"> <div v-if="show" :data-theme="ereaderTheme" class="group fixed top-0 left-0 right-0 layout-wrapper w-full z-40 pt-8 data-[theme=black]:bg-black data-[theme=black]:text-white data-[theme=dark]:bg-[#232323] data-[theme=dark]:text-white data-[theme=light]:bg-white data-[theme=light]:text-black" :class="{ 'reader-player-open': isPlayerOpen }">
<!-- toolbar --> <!-- toolbar -->
<div class="h-32 pt-10 w-full px-2 fixed top-0 left-0 z-30 transition-transform bg-bg text-fg" :class="showingToolbar ? 'translate-y-0' : '-translate-y-32'" :style="{ boxShadow: showingToolbar ? '0px 8px 8px #11111155' : '' }" @touchstart.stop @mousedown.stop @touchend.stop @mouseup.stop> <div class="w-full px-2 fixed top-0 left-0 z-30 transition-transform bg-bg text-fg" :class="`${showingToolbar ? 'translate-y-0' : '-translate-y-36'} ${isIos ? 'pt-14 h-36' : 'pt-10 h-32'}`" :style="{ boxShadow: showingToolbar ? '0px 8px 8px #11111155' : '' }" @touchstart.stop @mousedown.stop @touchend.stop @mouseup.stop>
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<button type="button" class="inline-flex mx-2" @click.stop="show = false"> <button type="button" class="inline-flex mx-2" @click.stop="show = false">
<span class="material-symbols text-3xl text-fg">chevron_left</span> <span class="material-symbols text-3xl text-fg">chevron_left</span>
@@ -62,7 +62,7 @@
<span class="material-symbols">close</span> <span class="material-symbols">close</span>
</button> </button>
</div> </div>
<div class="w-full overflow-y-auto overflow-x-hidden h-full max-h-[calc(75vh-85px)]"> <div class="w-full overflow-y-auto overflow-x-hidden h-[calc(75vh-85px)] min-h-[320px] short:min-h-0 short:h-[calc(100vh-85px)]">
<div class="w-full h-full px-4"> <div class="w-full h-full px-4">
<div class="flex items-center mb-6"> <div class="flex items-center mb-6">
<div class="w-32"> <div class="w-32">
@@ -70,6 +70,12 @@
</div> </div>
<ui-toggle-btns v-model="ereaderSettings.theme" name="theme" :items="themeItems" @input="settingsUpdated" /> <ui-toggle-btns v-model="ereaderSettings.theme" name="theme" :items="themeItems" @input="settingsUpdated" />
</div> </div>
<div class="flex items-center mb-6">
<div class="w-32">
<p class="text-sm">{{ $strings.LabelFontFamily }}</p>
</div>
<ui-toggle-btns v-model="ereaderSettings.font" name="font" :items="fontItems" @input="settingsUpdated" />
</div>
<div class="flex items-center mb-6"> <div class="flex items-center mb-6">
<div class="w-32"> <div class="w-32">
<p class="text-sm">{{ $strings.LabelFontScale }}</p> <p class="text-sm">{{ $strings.LabelFontScale }}</p>
@@ -141,6 +147,7 @@ export default {
isInittingWatchVolume: false, isInittingWatchVolume: false,
ereaderSettings: { ereaderSettings: {
theme: 'dark', theme: 'dark',
font: 'serif',
fontScale: 100, fontScale: 100,
lineSpacing: 115, lineSpacing: 115,
spread: 'auto', spread: 'auto',
@@ -180,6 +187,9 @@ export default {
this.$store.commit('setShowReader', val) this.$store.commit('setShowReader', val)
} }
}, },
isIos() {
return this.$platform === 'ios'
},
title() { title() {
return this.mediaMetadata.title || 'No Title' return this.mediaMetadata.title || 'No Title'
}, },
@@ -252,6 +262,18 @@ export default {
} }
] ]
}, },
fontItems() {
return [
{
text: this.$strings.LabelFontFamilySans,
value: 'sans-serif'
},
{
text: this.$strings.LabelFontFamilySerif,
value: 'serif'
}
]
},
readerComponentName() { readerComponentName() {
if (this.ebookType === 'epub') return 'readers-epub-reader' if (this.ebookType === 'epub') return 'readers-epub-reader'
else if (this.ebookType === 'mobi') return 'readers-mobi-reader' else if (this.ebookType === 'mobi') return 'readers-mobi-reader'
+8 -2
View File
@@ -183,10 +183,16 @@ export default {
daysInARow() { daysInARow() {
var count = 0 var count = 0
while (true) { while (true) {
var _date = this.$addDaysToToday(count * -1) const _date = this.$addDaysToToday(count * -1 - 1)
var datestr = this.$formatJsDate(_date, 'yyyy-MM-dd') const datestr = this.$formatJsDate(_date, 'yyyy-MM-dd')
if (!this.listeningStatsDays[datestr] || this.listeningStatsDays[datestr] === 0) { if (!this.listeningStatsDays[datestr] || this.listeningStatsDays[datestr] === 0) {
// don't require listening today to count towards days in a row, but do count it if already listened today
const today = this.$formatJsDate(new Date(), 'yyyy-MM-dd')
if (this.listeningStatsDays[today]) {
count++
}
return count return count
} }
count++ count++
+4
View File
@@ -15,6 +15,7 @@
<tr> <tr>
<th class="text-left">{{ $strings.LabelTitle }}</th> <th class="text-left">{{ $strings.LabelTitle }}</th>
<th class="text-center w-16">{{ $strings.LabelStart }}</th> <th class="text-center w-16">{{ $strings.LabelStart }}</th>
<th class="text-center w-16">{{ $strings.LabelDuration }}</th>
</tr> </tr>
<tr v-for="chapter in chapters" :key="chapter.id"> <tr v-for="chapter in chapters" :key="chapter.id">
<td> <td>
@@ -23,6 +24,9 @@
<td class="font-mono text-center underline w-16" @click.stop="goToTimestamp(chapter.start)"> <td class="font-mono text-center underline w-16" @click.stop="goToTimestamp(chapter.start)">
{{ $secondsToTimestamp(chapter.start) }} {{ $secondsToTimestamp(chapter.start) }}
</td> </td>
<td class="font-mono text-center">
{{ $secondsToTimestamp(Math.max(0, chapter.end - chapter.start)) }}
</td>
</tr> </tr>
</table> </table>
</transition> </transition>
+2 -2
View File
@@ -228,8 +228,8 @@ export default {
methods: { methods: {
async clearDownloadQueue() { async clearDownloadQueue() {
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: `Are you sure you want to clear episode download queue?` message: this.$strings.MessageConfirmDeleteEpisodeDownloadQueue
}) })
if (value) { if (value) {
@@ -0,0 +1 @@
<p>Zur Verwendung mit Ihrem Audiobookshelf-Server. Audiobookshelf ist ein kostenloser Open-Source-Hörbuch- und Podcast-Manager.</p><p>Verbinden Sie sich mit Ihrem selbst gehosteten Audiobookshelf-Server und streamen Sie Ihre Hörbücher und Podcasts direkt auf Ihr Gerät.</p>
@@ -0,0 +1 @@
Hörbuch- und Podcast-Player für Ihren selbst gehosteten Audiobookshelf-Server
@@ -0,0 +1 @@
<p>For use with your audiobookshelf server. Audiobookshelf is a free and open source audiobook and podcast manager.</p><p>Connect to your self-hosted audiobookshelf server and stream your audiobooks and podcasts directly to your device.</p>
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

@@ -0,0 +1 @@
Audiobook and podcast player for your self-hosted Audiobookshelf server
+8 -4
View File
@@ -36,6 +36,7 @@
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
65643F4DAA661FB0B247247E /* Pods_Audiobookshelf.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8F9A7CCC479333E44DC314BE /* Pods_Audiobookshelf.framework */; }; 65643F4DAA661FB0B247247E /* Pods_Audiobookshelf.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8F9A7CCC479333E44DC314BE /* Pods_Audiobookshelf.framework */; };
6F2516272E68DA3000F40541 /* LogEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F2516262E68DA3000F40541 /* LogEntry.swift */; };
E9D5504628AC1A3900C746DD /* LibraryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504528AC1A3900C746DD /* LibraryItem.swift */; }; E9D5504628AC1A3900C746DD /* LibraryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504528AC1A3900C746DD /* LibraryItem.swift */; };
E9D5504828AC1A7A00C746DD /* MediaType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504728AC1A7A00C746DD /* MediaType.swift */; }; E9D5504828AC1A7A00C746DD /* MediaType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504728AC1A7A00C746DD /* MediaType.swift */; };
E9D5504A28AC1AA600C746DD /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504928AC1AA600C746DD /* Metadata.swift */; }; E9D5504A28AC1AA600C746DD /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D5504928AC1AA600C746DD /* Metadata.swift */; };
@@ -111,6 +112,7 @@
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; }; 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
6F2516262E68DA3000F40541 /* LogEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEntry.swift; sourceTree = "<group>"; };
8F9A7CCC479333E44DC314BE /* Pods_Audiobookshelf.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Audiobookshelf.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8F9A7CCC479333E44DC314BE /* Pods_Audiobookshelf.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Audiobookshelf.framework; sourceTree = BUILT_PRODUCTS_DIR; };
BE96D57E131924D520D57057 /* Pods-Audiobookshelf.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Audiobookshelf.debug.xcconfig"; path = "Target Support Files/Pods-Audiobookshelf/Pods-Audiobookshelf.debug.xcconfig"; sourceTree = "<group>"; }; BE96D57E131924D520D57057 /* Pods-Audiobookshelf.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Audiobookshelf.debug.xcconfig"; path = "Target Support Files/Pods-Audiobookshelf/Pods-Audiobookshelf.debug.xcconfig"; sourceTree = "<group>"; };
D2F7F575384A63F1C47DE984 /* Pods-Audiobookshelf.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Audiobookshelf.release.xcconfig"; path = "Target Support Files/Pods-Audiobookshelf/Pods-Audiobookshelf.release.xcconfig"; sourceTree = "<group>"; }; D2F7F575384A63F1C47DE984 /* Pods-Audiobookshelf.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Audiobookshelf.release.xcconfig"; path = "Target Support Files/Pods-Audiobookshelf/Pods-Audiobookshelf.release.xcconfig"; sourceTree = "<group>"; };
@@ -231,6 +233,7 @@
E9D5506128AC1CC900C746DD /* PlayerState.swift */, E9D5506128AC1CC900C746DD /* PlayerState.swift */,
4DF74911287105C600AC7814 /* DeviceSettings.swift */, 4DF74911287105C600AC7814 /* DeviceSettings.swift */,
E9D5507428AEF93100C746DD /* PlayerSettings.swift */, E9D5507428AEF93100C746DD /* PlayerSettings.swift */,
6F2516262E68DA3000F40541 /* LogEntry.swift */,
E9D5506328AC1D3F00C746DD /* server */, E9D5506328AC1D3F00C746DD /* server */,
E9D5506428AC1D5800C746DD /* local */, E9D5506428AC1D5800C746DD /* local */,
E9D5506D28AC1E7400C746DD /* download */, E9D5506D28AC1E7400C746DD /* download */,
@@ -533,6 +536,7 @@
3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */, 3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */,
E9D5506628AC1D7300C746DD /* LocalLibraryItem.swift in Sources */, E9D5506628AC1D7300C746DD /* LocalLibraryItem.swift in Sources */,
E9D5504628AC1A3900C746DD /* LibraryItem.swift in Sources */, E9D5504628AC1A3900C746DD /* LibraryItem.swift in Sources */,
6F2516272E68DA3000F40541 /* LogEntry.swift in Sources */,
3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */, 3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */,
3AD4FCED28044E6C006DB301 /* Store.swift in Sources */, 3AD4FCED28044E6C006DB301 /* Store.swift in Sources */,
4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */, 4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */,
@@ -736,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 = 40; CURRENT_PROJECT_VERSION = 41;
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.10.1; MARKETING_VERSION = 0.11.0;
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)";
@@ -760,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 = 40; CURRENT_PROJECT_VERSION = 41;
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.10.1; MARKETING_VERSION = 0.11.0;
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 = "";
+13 -15
View File
@@ -5,8 +5,6 @@ import RealmSwift
@UIApplicationMain @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
private let logger = AppLogger(category: "AppDelegate")
lazy var window: UIWindow? = UIWindow(frame: UIScreen.main.bounds) lazy var window: UIWindow? = UIWindow(frame: UIScreen.main.bounds)
var backgroundCompletionHandler: (() -> Void)? var backgroundCompletionHandler: (() -> Void)?
@@ -17,13 +15,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
schemaVersion: 20, schemaVersion: 20,
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)") AbsLogger.info(message: "Realm schema version was \(oldSchemaVersion)")
migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in
newObject?["enableAltView"] = false newObject?["enableAltView"] = false
} }
} }
if (oldSchemaVersion < 4) { if (oldSchemaVersion < 4) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Reindexing server configs") AbsLogger.info(message: "Realm schema version was \(oldSchemaVersion)... Reindexing server configs")
var indexCounter = 1 var indexCounter = 1
migration.enumerateObjects(ofType: ServerConnectionConfig.className()) { oldObject, newObject in migration.enumerateObjects(ofType: ServerConnectionConfig.className()) { oldObject, newObject in
newObject?["index"] = indexCounter newObject?["index"] = indexCounter
@@ -31,44 +29,44 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
} }
} }
if (oldSchemaVersion < 5) { if (oldSchemaVersion < 5) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding lockOrientation setting") AbsLogger.info(message: "Realm schema version was \(oldSchemaVersion)... Adding lockOrientation setting")
migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in
newObject?["lockOrientation"] = "NONE" newObject?["lockOrientation"] = "NONE"
} }
} }
if (oldSchemaVersion < 6) { if (oldSchemaVersion < 6) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding hapticFeedback setting") AbsLogger.info(message: "Realm schema version was \(oldSchemaVersion)... Adding hapticFeedback setting")
migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in
newObject?["hapticFeedback"] = "LIGHT" newObject?["hapticFeedback"] = "LIGHT"
} }
} }
if (oldSchemaVersion < 15) { if (oldSchemaVersion < 15) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding languageCode setting") AbsLogger.info(message: "Realm schema version was \(oldSchemaVersion)... Adding languageCode setting")
migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in migration.enumerateObjects(ofType: DeviceSettings.className()) { oldObject, newObject in
newObject?["languageCode"] = "en-us" newObject?["languageCode"] = "en-us"
} }
} }
if (oldSchemaVersion < 16) { if (oldSchemaVersion < 16) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding chapterTrack setting") AbsLogger.info(message: "Realm schema version was \(oldSchemaVersion)... Adding chapterTrack setting")
migration.enumerateObjects(ofType: PlayerSettings.className()) { oldObject, newObject in migration.enumerateObjects(ofType: PlayerSettings.className()) { oldObject, newObject in
newObject?["chapterTrack"] = false newObject?["chapterTrack"] = false
} }
} }
if (oldSchemaVersion < 17) { if (oldSchemaVersion < 17) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding downloadUsingCellular and streamingUsingCellular settings") AbsLogger.info(message: "Realm schema version was \(oldSchemaVersion)... Adding downloadUsingCellular and streamingUsingCellular settings")
migration.enumerateObjects(ofType: PlayerSettings.className()) { oldObject, newObject in migration.enumerateObjects(ofType: PlayerSettings.className()) { oldObject, newObject in
newObject?["downloadUsingCellular"] = "ALWAYS" newObject?["downloadUsingCellular"] = "ALWAYS"
newObject?["streamingUsingCellular"] = "ALWAYS" newObject?["streamingUsingCellular"] = "ALWAYS"
} }
} }
if (oldSchemaVersion < 18) { if (oldSchemaVersion < 18) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding disableSleepTimerFadeOut settings") AbsLogger.info(message: "Realm schema version was \(oldSchemaVersion)... Adding disableSleepTimerFadeOut settings")
migration.enumerateObjects(ofType: PlayerSettings.className()) { oldObject, newObject in migration.enumerateObjects(ofType: PlayerSettings.className()) { oldObject, newObject in
newObject?["disableSleepTimerFadeOut"] = false newObject?["disableSleepTimerFadeOut"] = false
} }
} }
if (oldSchemaVersion < 20) { if (oldSchemaVersion < 20) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding version to ServerConnectionConfigs") AbsLogger.info(message: "Realm schema version was \(oldSchemaVersion)... Adding version to ServerConnectionConfigs")
migration.enumerateObjects(ofType: ServerConnectionConfig.className()) { oldObject, newObject in migration.enumerateObjects(ofType: ServerConnectionConfig.className()) { oldObject, newObject in
newObject?["version"] = "" newObject?["version"] = ""
} }
@@ -88,22 +86,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationDidEnterBackground(_ application: UIApplication) { func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
logger.log("Audiobookself is now in the background") AbsLogger.info(message: "Audiobookself is now in the background")
} }
func applicationWillEnterForeground(_ application: UIApplication) { func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
logger.log("Audiobookself is now in the foreground") AbsLogger.info(message: "Audiobookself is now in the foreground")
} }
func applicationDidBecomeActive(_ application: UIApplication) { func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
logger.log("Audiobookself is now active") AbsLogger.info(message: "Audiobookself is now active")
} }
func applicationWillTerminate(_ application: UIApplication) { func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
logger.log("Audiobookself is terminating") AbsLogger.info(message: "Audiobookself is terminating")
} }
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
+13 -12
View File
@@ -34,8 +34,6 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
CAPPluginMethod(name: "setSleepTimer", returnType: CAPPluginReturnPromise) CAPPluginMethod(name: "setSleepTimer", returnType: CAPPluginReturnPromise)
] ]
private let logger = AppLogger(category: "AbsAudioPlayer")
private var initialPlayWhenReady = false private var initialPlayWhenReady = false
private var monitor: NWPathMonitor? private var monitor: NWPathMonitor?
private let queue = DispatchQueue.global(qos: .background) private let queue = DispatchQueue.global(qos: .background)
@@ -82,7 +80,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
let startTimeOverride = call.getDouble("startTime") let startTimeOverride = call.getDouble("startTime")
if libraryItemId == nil { if libraryItemId == nil {
logger.error("provide library item id") AbsLogger.error(message: "No id provided")
return call.resolve() return call.resolve()
} }
@@ -93,7 +91,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
let item = Database.shared.getLocalLibraryItem(localLibraryItemId: libraryItemId!) let item = Database.shared.getLocalLibraryItem(localLibraryItemId: libraryItemId!)
let episode = item?.getPodcastEpisode(episodeId: episodeId) let episode = item?.getPodcastEpisode(episodeId: episodeId)
guard let playbackSession = item?.getPlaybackSession(episode: episode) else { guard let playbackSession = item?.getPlaybackSession(episode: episode) else {
logger.error("Failed to get local playback session") AbsLogger.error(message: "Failed to get local playback session")
return call.resolve([:]) return call.resolve([:])
} }
@@ -105,7 +103,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
try self.startPlaybackSession(playbackSession, playWhenReady: playWhenReady, playbackRate: playbackRate) try self.startPlaybackSession(playbackSession, playWhenReady: playWhenReady, playbackRate: playbackRate)
call.resolve(try playbackSession.asDictionary()) call.resolve(try playbackSession.asDictionary())
} catch(let exception) { } catch(let exception) {
logger.error("Failed to start session") AbsLogger.error(message: "Failed to start session for local item: \(exception)")
debugPrint(exception) debugPrint(exception)
call.resolve([:]) call.resolve([:])
} }
@@ -119,7 +117,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
try self?.startPlaybackSession(session, playWhenReady: playWhenReady, playbackRate: playbackRate) try self?.startPlaybackSession(session, playWhenReady: playWhenReady, playbackRate: playbackRate)
call.resolve(try session.asDictionary()) call.resolve(try session.asDictionary())
} catch(let exception) { } catch(let exception) {
self?.logger.error("Failed to start session") AbsLogger.error(message: "Failed to start streaming session")
debugPrint(exception) debugPrint(exception)
call.resolve([:]) call.resolve([:])
} }
@@ -127,19 +125,22 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
} }
} }
/// Stops playback and closes session.
@objc func closePlayback(_ call: CAPPluginCall) { @objc func closePlayback(_ call: CAPPluginCall) {
logger.log("Close playback") AbsLogger.info(message: "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,
"bufferedTime": PlayerHandler.getCurrentTime() ?? 0, "bufferedTime": PlayerHandler.getCurrentTime() ?? 0,
]) ])
} }
@objc func setPlaybackSpeed(_ call: CAPPluginCall) { @objc func setPlaybackSpeed(_ call: CAPPluginCall) {
let playbackRate = call.getFloat("value", 1.0) let playbackRate = call.getFloat("value", 1.0)
let settings = PlayerSettings.main() let settings = PlayerSettings.main()
@@ -152,7 +153,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
@objc func setChapterTrack(_ call: CAPPluginCall) { @objc func setChapterTrack(_ call: CAPPluginCall) {
let chapterTrack = call.getBool("enabled", true) let chapterTrack = call.getBool("enabled", true)
logger.log(String(chapterTrack)) AbsLogger.info(message: String(chapterTrack))
let settings = PlayerSettings.main() let settings = PlayerSettings.main()
try? settings.update { try? settings.update {
settings.chapterTrack = chapterTrack settings.chapterTrack = chapterTrack
@@ -225,7 +226,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
let seconds = time / 1000 let seconds = time / 1000
logger.log("chapter time: \(isChapterTime)") AbsLogger.info(message: "chapter time: \(isChapterTime)")
if isChapterTime { if isChapterTime {
PlayerHandler.setChapterSleepTime(stopAt: seconds) PlayerHandler.setChapterSleepTime(stopAt: seconds)
return call.resolve([ "success": true ]) return call.resolve([ "success": true ])
@@ -262,7 +263,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
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 }
guard let progressUpdate = try? localMediaProgress.asDictionary() else { return } guard let progressUpdate = try? localMediaProgress.asDictionary() else { return }
logger.log("Sending local progress back to the UI") AbsLogger.info(message: "Sending local progress back to the UI")
self.notifyListeners("onLocalMediaProgressUpdate", data: progressUpdate) self.notifyListeners("onLocalMediaProgressUpdate", data: progressUpdate)
} }
@@ -272,7 +273,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
let playWhenReady = PlayerHandler.getPlayWhenReady() let playWhenReady = PlayerHandler.getPlayWhenReady()
let libraryItemId = session?.libraryItemId ?? "" let libraryItemId = session?.libraryItemId ?? ""
let episodeId = session?.episodeId ?? nil let episodeId = session?.episodeId ?? nil
logger.log("Forcing Transcode") AbsLogger.info(message: "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
@@ -283,7 +284,7 @@ public class AbsAudioPlayer: CAPPlugin, CAPBridgedPlugin {
self.sendPlaybackSession(session: try session.asDictionary()) self.sendPlaybackSession(session: try session.asDictionary())
self.sendMetadata() self.sendMetadata()
} catch(let exception) { } catch(let exception) {
self?.logger.error("Failed to start transcoded session") AbsLogger.error(message: "Failed to start transcoded session")
debugPrint(exception) debugPrint(exception)
} }
} }
+12 -13
View File
@@ -50,7 +50,6 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
CAPPluginMethod(name: "updateLocalEbookProgress", returnType: CAPPluginReturnPromise) CAPPluginMethod(name: "updateLocalEbookProgress", returnType: CAPPluginReturnPromise)
] ]
private let logger = AppLogger(category: "AbsDatabase")
private let secureStorage = SecureStorage() private let secureStorage = SecureStorage()
// Used to notify the webview frontend that the token has been refreshed // Used to notify the webview frontend that the token has been refreshed
@@ -80,7 +79,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
if (refreshToken != "") { if (refreshToken != "") {
// Store refresh token securely if provided // Store refresh token securely if provided
let hasRefreshToken = secureStorage.storeRefreshToken(serverConnectionConfigId: id ?? "", refreshToken: refreshToken) let hasRefreshToken = secureStorage.storeRefreshToken(serverConnectionConfigId: id ?? "", refreshToken: refreshToken)
logger.log("Refresh token secured = \(hasRefreshToken)") AbsLogger.info(message: "Refresh token secured = \(hasRefreshToken)")
} }
let config = ServerConnectionConfig() let config = ServerConnectionConfig()
@@ -148,15 +147,16 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
let items = Database.shared.getLocalLibraryItems() let items = Database.shared.getLocalLibraryItems()
call.resolve([ "value": try items.asDictionaryArray()]) call.resolve([ "value": try items.asDictionaryArray()])
} catch(let exception) { } catch(let exception) {
logger.error("error while readling local library items") AbsLogger.error(message: "error reading local library items \(exception)")
debugPrint(exception) debugPrint(exception)
call.resolve() call.resolve()
} }
} }
@objc func getLocalLibraryItem(_ call: CAPPluginCall) { @objc func getLocalLibraryItem(_ call: CAPPluginCall) {
let id = call.getString("id") ?? ""
do { do {
let item = Database.shared.getLocalLibraryItem(localLibraryItemId: call.getString("id") ?? "") let item = Database.shared.getLocalLibraryItem(localLibraryItemId: id)
switch item { switch item {
case .some(let foundItem): case .some(let foundItem):
call.resolve(try foundItem.asDictionary()) call.resolve(try foundItem.asDictionary())
@@ -164,7 +164,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
call.resolve() call.resolve()
} }
} catch(let exception) { } catch(let exception) {
logger.error("error while readling local library items") AbsLogger.error(message: "error reading local library item[\(id)] \(exception)")
debugPrint(exception) debugPrint(exception)
call.resolve() call.resolve()
} }
@@ -180,8 +180,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
call.resolve() call.resolve()
} }
} catch(let exception) { } catch(let exception) {
logger.error("error while readling local library items") AbsLogger.error(message: "error while readling local library items: \(exception)", error: exception)
debugPrint(exception)
call.resolve() call.resolve()
} }
} }
@@ -194,8 +193,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
do { do {
call.resolve([ "value": try Database.shared.getAllLocalMediaProgress().asDictionaryArray() ]) call.resolve([ "value": try Database.shared.getAllLocalMediaProgress().asDictionaryArray() ])
} catch { } catch {
logger.error("Error while loading local media progress") AbsLogger.error(message: "Error while loading local media progress", error: error)
debugPrint(error)
call.resolve(["value": []]) call.resolve(["value": []])
} }
} }
@@ -212,7 +210,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
@objc func syncLocalSessionsWithServer(_ call: CAPPluginCall) { @objc func syncLocalSessionsWithServer(_ call: CAPPluginCall) {
let isFirstSync = call.getBool("isFirstSync", false) let isFirstSync = call.getBool("isFirstSync", false)
logger.log("syncLocalSessionsWithServer: Starting (First sync: \(isFirstSync))") AbsLogger.info(message: "Starting syncLocalSessionsWithServer isFirstSync=\(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()
@@ -244,12 +242,13 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
return return
} }
logger.log("syncServerMediaProgressWithLocalMediaProgress: Saving local media progress") AbsLogger.info(message: "Saving local media progress \(serverMediaProgress)")
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")
AbsLogger.error(message: "Failed to sync: \(error)")
debugPrint(error) debugPrint(error)
} }
} }
@@ -264,7 +263,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
localMediaProgressId += "-\(localEpisodeId ?? "")" localMediaProgressId += "-\(localEpisodeId ?? "")"
} }
logger.log("updateLocalMediaProgressFinished \(localMediaProgressId) | Is Finished: \(isFinished)") AbsLogger.info(message: "\(localMediaProgressId): isFinished=\(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)
@@ -337,7 +336,7 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
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)") AbsLogger.info(message: "\(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)
+98 -34
View File
@@ -19,8 +19,6 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
static private let downloadsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] static private let downloadsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
private let logger = AppLogger(category: "AbsDownloader")
private lazy var session: URLSession = { private lazy var session: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: "AbsDownloader") let config = URLSessionConfiguration.background(withIdentifier: "AbsDownloader")
let queue = OperationQueue() let queue = OperationQueue()
@@ -31,31 +29,69 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
private var downloadItemProgress = [String: DownloadItem]() private var downloadItemProgress = [String: DownloadItem]()
private var monitoringProgressTimer: Timer? private var monitoringProgressTimer: Timer?
// Download queue management
private let downloadQueueLock = NSLock()
private var pendingDownloadTasks: [DownloadItemPartTask] = []
private var activeDownloadTasks: Set<String> = [] // Track active task IDs
private let maxConcurrentDownloads = 3
// MARK: - Download Queue Management
private func startNextDownloadInQueue() {
downloadQueueLock.lock()
defer { downloadQueueLock.unlock() }
// Start downloads up to the max concurrent limit
while activeDownloadTasks.count < maxConcurrentDownloads && !pendingDownloadTasks.isEmpty {
let nextTask = pendingDownloadTasks.removeFirst()
activeDownloadTasks.insert(nextTask.partId)
AbsLogger.info(message: "Starting download for \(nextTask.filename) (\(activeDownloadTasks.count)/\(maxConcurrentDownloads) active, \(pendingDownloadTasks.count) pending)")
nextTask.task.resume()
}
}
private func markDownloadTaskCompleted(_ taskId: String) {
downloadQueueLock.lock()
activeDownloadTasks.remove(taskId)
downloadQueueLock.unlock()
// Try to start the next download
startNextDownloadInQueue()
}
// MARK: - Progress handling // MARK: - Progress handling
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in
let realm = try Realm() let realm = try Realm()
let partId = downloadItemPart.id
// Get fresh reference to the object in this realm
guard let liveDownloadItemPart = realm.object(ofType: DownloadItemPart.self, forPrimaryKey: partId) else {
throw LibraryItemDownloadError.downloadItemPartNotFound
}
try realm.write { try realm.write {
downloadItemPart.bytesDownloaded = downloadItemPart.fileSize liveDownloadItemPart.bytesDownloaded = liveDownloadItemPart.fileSize
downloadItemPart.progress = 100 liveDownloadItemPart.progress = 100
downloadItemPart.completed = true liveDownloadItemPart.completed = true
} }
do { do {
// Move the downloaded file into place // Move the downloaded file into place
guard let destinationUrl = downloadItemPart.destinationURL else { guard let destinationUrl = liveDownloadItemPart.destinationURL else {
throw LibraryItemDownloadError.downloadItemPartDestinationUrlNotDefined throw LibraryItemDownloadError.downloadItemPartDestinationUrlNotDefined
} }
try? FileManager.default.removeItem(at: destinationUrl) try? FileManager.default.removeItem(at: destinationUrl)
try FileManager.default.moveItem(at: location, to: destinationUrl) try FileManager.default.moveItem(at: location, to: destinationUrl)
try realm.write { try realm.write {
downloadItemPart.moved = true liveDownloadItemPart.moved = true
} }
} catch { } catch {
try realm.write { try realm.write {
downloadItemPart.failed = true liveDownloadItemPart.failed = true
} }
throw error throw error
} }
@@ -65,13 +101,26 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
handleDownloadTaskUpdate(downloadTask: task) { downloadItem, downloadItemPart in handleDownloadTaskUpdate(downloadTask: task) { downloadItem, downloadItemPart in
if let error = error { if let error = error {
try Realm().write { let realm = try Realm()
downloadItemPart.completed = true let partId = downloadItemPart.id
downloadItemPart.failed = true
// Get fresh reference to the object in this realm
guard let liveDownloadItemPart = realm.object(ofType: DownloadItemPart.self, forPrimaryKey: partId) else {
throw LibraryItemDownloadError.downloadItemPartNotFound
}
try realm.write {
liveDownloadItemPart.completed = true
liveDownloadItemPart.failed = true
} }
throw error throw error
} }
} }
// Mark this task as completed and start the next download in queue
if let taskId = task.taskDescription {
markDownloadTaskCompleted(taskId)
}
} }
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
@@ -81,9 +130,17 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
// Only update the progress if we received accurate progress data // Only update the progress if we received accurate progress data
if percentDownloaded >= 0.0 && percentDownloaded <= 100.0 { if percentDownloaded >= 0.0 && percentDownloaded <= 100.0 {
try Realm().write { let realm = try Realm()
downloadItemPart.bytesDownloaded = Double(totalBytesWritten) let partId = downloadItemPart.id
downloadItemPart.progress = percentDownloaded
// Get fresh reference to the object in this realm
guard let liveDownloadItemPart = realm.object(ofType: DownloadItemPart.self, forPrimaryKey: partId) else {
throw LibraryItemDownloadError.downloadItemPartNotFound
}
try realm.write {
liveDownloadItemPart.bytesDownloaded = Double(totalBytesWritten)
liveDownloadItemPart.progress = percentDownloaded
} }
} }
} }
@@ -104,7 +161,7 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
private func handleDownloadTaskUpdate(downloadTask: URLSessionTask, progressHandler: DownloadProgressHandler) { private func handleDownloadTaskUpdate(downloadTask: URLSessionTask, progressHandler: DownloadProgressHandler) {
do { do {
guard let downloadItemPartId = downloadTask.taskDescription else { throw LibraryItemDownloadError.noTaskDescription } guard let downloadItemPartId = downloadTask.taskDescription else { throw LibraryItemDownloadError.noTaskDescription }
logger.log("Received download update for \(downloadItemPartId)") AbsLogger.info(message: "Received download update for \(downloadItemPartId)")
// Find the download item // Find the download item
let downloadItem = Database.shared.getDownloadItem(downloadItemPartId: downloadItemPartId) let downloadItem = Database.shared.getDownloadItem(downloadItemPartId: downloadItemPartId)
@@ -119,7 +176,7 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
try progressHandler(downloadItem, part) try progressHandler(downloadItem, part)
try? self.notifyListeners("onDownloadItemPartUpdate", data: part.asDictionary()) try? self.notifyListeners("onDownloadItemPartUpdate", data: part.asDictionary())
} catch { } catch {
logger.error("Error while processing progress") AbsLogger.error(message: "Error while processing progress")
debugPrint(error) debugPrint(error)
} }
@@ -130,7 +187,7 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
} }
self.notifyDownloadProgress() self.notifyDownloadProgress()
} catch { } catch {
logger.error("DownloadItemError") AbsLogger.error(message: "DownloadItemError")
debugPrint(error) debugPrint(error)
} }
} }
@@ -138,18 +195,18 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
// We want to handle updating the UI in the background and throttled so we don't overload the UI with progress updates // We want to handle updating the UI in the background and throttled so we don't overload the UI with progress updates
private func notifyDownloadProgress() { private func notifyDownloadProgress() {
if self.monitoringProgressTimer?.isValid ?? false { if self.monitoringProgressTimer?.isValid ?? false {
logger.log("Already monitoring progress, no need to start timer again") AbsLogger.info(message: "Already monitoring progress, no need to start timer again")
} else { } else {
DispatchQueue.runOnMainQueue { DispatchQueue.runOnMainQueue {
self.monitoringProgressTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true, block: { [unowned self] t in self.monitoringProgressTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true, block: { [unowned self] t in
self.logger.log("Starting monitoring download progress...") AbsLogger.info(message: "Starting monitoring download progress...")
// Fetch active downloads in a thread-safe way // Fetch active downloads in a thread-safe way
func fetchActiveDownloads() -> [String: DownloadItem]? { func fetchActiveDownloads() -> [String: DownloadItem]? {
self.progressStatusQueue.sync { self.progressStatusQueue.sync {
let activeDownloads = self.downloadItemProgress let activeDownloads = self.downloadItemProgress
if activeDownloads.isEmpty { if activeDownloads.isEmpty {
logger.log("Finishing monitoring download progress...") AbsLogger.info(message: "Finishing monitoring download progress...")
t.invalidate() t.invalidate()
} }
return activeDownloads return activeDownloads
@@ -184,7 +241,7 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
if ( downloadItem.didDownloadSuccessfully() ) { if ( downloadItem.didDownloadSuccessfully() ) {
ApiClient.getLibraryItemWithProgress(libraryItemId: downloadItem.libraryItemId!, episodeId: downloadItem.episodeId) { [weak self] libraryItem in ApiClient.getLibraryItemWithProgress(libraryItemId: downloadItem.libraryItemId!, episodeId: downloadItem.episodeId) { [weak self] libraryItem in
guard let libraryItem = libraryItem else { self?.logger.error("LibraryItem not found"); return } guard let libraryItem = libraryItem else { AbsLogger.error(message: "LibraryItem not found"); return }
let localDirectory = libraryItem.id let localDirectory = libraryItem.id
var coverFile: String? var coverFile: String?
@@ -231,12 +288,12 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
var episodeId = call.getString("episodeId") var episodeId = call.getString("episodeId")
if ( episodeId == "null" ) { episodeId = nil } if ( episodeId == "null" ) { episodeId = nil }
logger.log("Download library item \(libraryItemId ?? "N/A") / episode \(episodeId ?? "N/A")") AbsLogger.info(message: "Download library item \(libraryItemId ?? "N/A") / episode \(episodeId ?? "N/A")")
guard let libraryItemId = libraryItemId else { return call.resolve(["error": "libraryItemId not specified"]) } guard let libraryItemId = libraryItemId else { return call.resolve(["error": "libraryItemId not specified"]) }
ApiClient.getLibraryItemWithProgress(libraryItemId: libraryItemId, episodeId: episodeId) { [weak self] libraryItem in ApiClient.getLibraryItemWithProgress(libraryItemId: libraryItemId, episodeId: episodeId) { [weak self] libraryItem in
if let libraryItem = libraryItem { if let libraryItem = libraryItem {
self?.logger.log("Got library item from server \(libraryItem.id)") AbsLogger.info(message: "Got library item from server \(libraryItem.id)")
do { do {
if let episodeId = episodeId { if let episodeId = episodeId {
// Download a podcast episode // Download a podcast episode
@@ -310,14 +367,19 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
// Persist in the database before status start coming in // Persist in the database before status start coming in
try Database.shared.saveDownloadItem(downloadItem) try Database.shared.saveDownloadItem(downloadItem)
// Start all the downloads // Add all tasks to the download queue
for task in tasks { downloadQueueLock.lock()
task.task.resume() pendingDownloadTasks.append(contentsOf: tasks)
} downloadQueueLock.unlock()
AbsLogger.info(message: "Added \(tasks.count) tasks to download queue. Starting downloads...")
// Start downloading (up to maxConcurrentDownloads at a time)
startNextDownloadInQueue()
} }
private func startLibraryItemTrackDownload(downloadItemId: String, item: LibraryItem, position: Int, track: AudioTrack, episode: PodcastEpisode?) throws -> DownloadItemPartTask { private func startLibraryItemTrackDownload(downloadItemId: String, item: LibraryItem, position: Int, track: AudioTrack, episode: PodcastEpisode?) throws -> DownloadItemPartTask {
logger.log("TRACK \(track.contentUrl!)") AbsLogger.info(message: "TRACK \(track.contentUrl!)")
// If we don't name metadata, then we can't proceed // If we don't name metadata, then we can't proceed
guard let filename = track.metadata?.filename else { guard let filename = track.metadata?.filename else {
@@ -334,7 +396,7 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
// Store the id on the task so the download item can be pulled from the database later // Store the id on the task so the download item can be pulled from the database later
task.taskDescription = part.id task.taskDescription = part.id
return DownloadItemPartTask(part: part, task: task) return DownloadItemPartTask(part: part, task: task, partId: part.id, filename: filename)
} }
private func startLibraryItemEbookDownload(downloadItemId: String, item: LibraryItem, ebookFile: EBookFile) throws -> DownloadItemPartTask { private func startLibraryItemEbookDownload(downloadItemId: String, item: LibraryItem, ebookFile: EBookFile) throws -> DownloadItemPartTask {
@@ -349,7 +411,7 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
// Store the id on the task so the download item can be pulled from the database later // Store the id on the task so the download item can be pulled from the database later
task.taskDescription = part.id task.taskDescription = part.id
return DownloadItemPartTask(part: part, task: task) return DownloadItemPartTask(part: part, task: task, partId: part.id, filename: filename)
} }
private func startLibraryItemCoverDownload(downloadItemId: String, item: LibraryItem) throws -> DownloadItemPartTask { private func startLibraryItemCoverDownload(downloadItemId: String, item: LibraryItem) throws -> DownloadItemPartTask {
@@ -369,7 +431,7 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
// Store the id on the task so the download item can be pulled from the database later // Store the id on the task so the download item can be pulled from the database later
task.taskDescription = part.id task.taskDescription = part.id
return DownloadItemPartTask(part: part, task: task) return DownloadItemPartTask(part: part, task: task, partId: part.id, filename: filename)
} }
private func urlForTrack(item: LibraryItem, track: AudioTrack) -> URL { private func urlForTrack(item: LibraryItem, track: AudioTrack) -> URL {
@@ -393,10 +455,10 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
private func createLibraryItemFileDirectory(item: LibraryItem) throws -> String { private func createLibraryItemFileDirectory(item: LibraryItem) throws -> String {
let itemDirectory = item.id let itemDirectory = item.id
logger.log("ITEM DIR \(itemDirectory)") AbsLogger.info(message: "ITEM DIR \(itemDirectory)")
guard AbsDownloader.itemDownloadFolder(path: itemDirectory) != nil else { guard AbsDownloader.itemDownloadFolder(path: itemDirectory) != nil else {
logger.error("Failed to CREATE LI DIRECTORY \(itemDirectory)") AbsLogger.error(message: "Failed to CREATE LI DIRECTORY \(itemDirectory)")
throw LibraryItemDownloadError.failedDirectory throw LibraryItemDownloadError.failedDirectory
} }
@@ -418,7 +480,7 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
return itemFolder return itemFolder
} catch { } catch {
AppLogger().error("Failed to CREATE LI DIRECTORY \(error)") AbsLogger.error(message: "Failed to CREATE LI DIRECTORY \(error)", error: error)
return nil return nil
} }
} }
@@ -433,6 +495,8 @@ typealias DownloadProgressHandler = (_ downloadItem: DownloadItem, _ downloadIte
struct DownloadItemPartTask { struct DownloadItemPartTask {
let part: DownloadItemPart let part: DownloadItemPart
let task: URLSessionDownloadTask let task: URLSessionDownloadTask
let partId: String // Cache the ID to avoid cross-thread Realm access
let filename: String // Cache the filename to avoid cross-thread Realm access
} }
enum LibraryItemDownloadError: String, Error { enum LibraryItemDownloadError: String, Error {
+11 -13
View File
@@ -23,12 +23,10 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
CAPPluginMethod(name: "deleteTrackFromItem", returnType: CAPPluginReturnPromise) CAPPluginMethod(name: "deleteTrackFromItem", returnType: CAPPluginReturnPromise)
] ]
private let logger = AppLogger(category: "AbsFileSystem")
@objc func selectFolder(_ call: CAPPluginCall) { @objc func selectFolder(_ call: CAPPluginCall) {
let mediaType = call.getString("mediaType") let mediaType = call.getString("mediaType")
logger.log("Select Folder for media type \(mediaType ?? "UNSET")") AbsLogger.info(message: "Select Folder for media type \(mediaType ?? "UNSET")")
call.unavailable("Not available on iOS") call.unavailable("Not available on iOS")
} }
@@ -36,7 +34,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
@objc func checkFolderPermission(_ call: CAPPluginCall) { @objc func checkFolderPermission(_ call: CAPPluginCall) {
let folderUrl = call.getString("folderUrl") let folderUrl = call.getString("folderUrl")
logger.log("checkFolderPermission for folder \(folderUrl ?? "UNSET")") AbsLogger.info(message: "checkFolderPermission for folder \(folderUrl ?? "UNSET")")
call.unavailable("Not available on iOS") call.unavailable("Not available on iOS")
} }
@@ -45,7 +43,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
let folderId = call.getString("folderId") let folderId = call.getString("folderId")
let forceAudioProbe = call.getBool("forceAudioProbe", false) let forceAudioProbe = call.getBool("forceAudioProbe", false)
logger.log("scanFolder \(folderId ?? "UNSET") | Force Probe = \(forceAudioProbe)") AbsLogger.info(message: "scanFolder \(folderId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
call.unavailable("Not available on iOS") call.unavailable("Not available on iOS")
} }
@@ -53,7 +51,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
@objc func removeFolder(_ call: CAPPluginCall) { @objc func removeFolder(_ call: CAPPluginCall) {
let folderId = call.getString("folderId") let folderId = call.getString("folderId")
logger.log("removeFolder \(folderId ?? "UNSET")") AbsLogger.info(message: "removeFolder \(folderId ?? "UNSET")")
call.unavailable("Not available on iOS") call.unavailable("Not available on iOS")
} }
@@ -61,7 +59,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
@objc func removeLocalLibraryItem(_ call: CAPPluginCall) { @objc func removeLocalLibraryItem(_ call: CAPPluginCall) {
let localLibraryItemId = call.getString("localLibraryItemId") let localLibraryItemId = call.getString("localLibraryItemId")
logger.log("removeLocalLibraryItem \(localLibraryItemId ?? "UNSET")") AbsLogger.info(message: "removeLocalLibraryItem \(localLibraryItemId ?? "UNSET")")
call.unavailable("Not available on iOS") call.unavailable("Not available on iOS")
} }
@@ -70,7 +68,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
let localLibraryItemId = call.getString("localLibraryItemId") let localLibraryItemId = call.getString("localLibraryItemId")
let forceAudioProbe = call.getBool("forceAudioProbe", false) let forceAudioProbe = call.getBool("forceAudioProbe", false)
logger.log("scanLocalLibraryItem \(localLibraryItemId ?? "UNSET") | Force Probe = \(forceAudioProbe)") AbsLogger.info(message: "scanLocalLibraryItem \(localLibraryItemId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
call.unavailable("Not available on iOS") call.unavailable("Not available on iOS")
} }
@@ -79,7 +77,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
let localLibraryItemId = call.getString("id") let localLibraryItemId = call.getString("id")
let contentUrl = call.getString("contentUrl") let contentUrl = call.getString("contentUrl")
logger.log("deleteItem \(localLibraryItemId ?? "UNSET") url \(contentUrl ?? "UNSET")") AbsLogger.info(message: "deleteItem \(localLibraryItemId ?? "UNSET") url \(contentUrl ?? "UNSET")")
var success = false var success = false
do { do {
@@ -89,7 +87,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
success = true success = true
} }
} catch { } catch {
logger.error("Failed to delete \(error)") AbsLogger.error(message: "Failed to delete \(error)")
success = false success = false
} }
@@ -100,7 +98,7 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
let localLibraryItemId = call.getString("id") let localLibraryItemId = call.getString("id")
let trackLocalFileId = call.getString("trackLocalFileId") let trackLocalFileId = call.getString("trackLocalFileId")
logger.log("deleteTrackFromItem \(localLibraryItemId ?? "UNSET") track file \(trackLocalFileId ?? "UNSET")") AbsLogger.info(message: "deleteTrackFromItem \(localLibraryItemId ?? "UNSET") track file \(trackLocalFileId ?? "UNSET")")
var success = false var success = false
if let localLibraryItemId = localLibraryItemId, let trackLocalFileId = trackLocalFileId, let item = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) { if let localLibraryItemId = localLibraryItemId, let trackLocalFileId = trackLocalFileId, let item = Database.shared.getLocalLibraryItem(localLibraryItemId: localLibraryItemId) {
@@ -120,12 +118,12 @@ public class AbsFileSystem: CAPPlugin, CAPBridgedPlugin {
success = true success = true
} }
} catch { } catch {
logger.error("Failed to delete \(error)") AbsLogger.error(message: "Failed to delete \(error)")
success = false success = false
} }
} }
} catch { } catch {
logger.error("Failed to delete \(error)") AbsLogger.error(message: "Failed to delete \(error)")
success = false success = false
} }
} }
+84 -7
View File
@@ -8,6 +8,16 @@
import Foundation import Foundation
import Capacitor import Capacitor
enum AbsLogLevel {
case info
case warn
case error
}
func fileName(file: String = #file) -> String {
return (file as NSString).lastPathComponent
}
@objc(AbsLogger) @objc(AbsLogger)
public class AbsLogger: CAPPlugin, CAPBridgedPlugin { public class AbsLogger: CAPPlugin, CAPBridgedPlugin {
public var identifier = "AbsLoggerPlugin" public var identifier = "AbsLoggerPlugin"
@@ -19,29 +29,96 @@ public class AbsLogger: CAPPlugin, CAPBridgedPlugin {
CAPPluginMethod(name: "clearLogs", returnType: CAPPluginReturnPromise) CAPPluginMethod(name: "clearLogs", returnType: CAPPluginReturnPromise)
] ]
private let logger = AppLogger(category: "AbsLogger") private static let shared: AbsLogger = {
AbsLogger()
}()
public static func info(_ tag: String = #file, message: String) {
try? shared.info(tag: fileName(file: tag), message: message)
}
public static func error(_ tag: String = #file, message: String, error: Error? = nil) {
try? shared.error(tag: fileName(file: tag), message: message)
}
private let loggers: Dictionary<String, AppLogger> = [:]
private func _logger(_ tag: String) -> AppLogger {
return loggers[tag, default: AppLogger(category: tag)]
}
private func saveLogEntry(_ level: AbsLogLevel, tag: String, message: String) throws {
let entry = LogEntry()
entry.tag = tag
entry.message = message
entry.level = "\(level)"
entry.timestamp = Int(Date().timeIntervalSince1970 * 1000)
try Database.shared.saveLog(entry)
self.notifyListeners("onLog", data: [
"id": entry.id,
"tag": entry.tag,
"message": entry.message,
"level": entry.level,
"timestamp": entry.timestamp
])
}
public func info(tag: String = "\(#file)", message: String) throws {
_logger(tag).log("[\(tag)] \(message)")
try saveLogEntry(.info, tag: tag, message: message)
}
public func error(tag: String = "\(#file)", message: String, error: Error? = nil) throws {
_logger(tag).error("[\(tag)] \(message)")
if let error = error { _logger(tag).error(error) }
try saveLogEntry(.error, tag: tag, message: message)
}
@objc func info(_ call: CAPPluginCall) { @objc func info(_ call: CAPPluginCall) {
let message = call.getString("message") ?? "" let message = call.getString("message") ?? ""
let tag = call.getString("tag") ?? "" let tag = call.getString("tag") ?? ""
logger.log("[\(tag)] \(message)") do {
call.resolve() try info(tag: tag, message: message)
call.resolve()
} catch {
call.reject("Failed to log \(message)", "101", error)
}
} }
@objc func error(_ call: CAPPluginCall) { @objc func error(_ call: CAPPluginCall) {
let message = call.getString("message") ?? "" let message = call.getString("message") ?? ""
let tag = call.getString("tag") ?? "" let tag = call.getString("tag") ?? ""
logger.error("[\(tag)] \(message)") do {
call.resolve() try error(tag: tag, message: message)
call.resolve()
} catch {
call.reject("Failed to log \(message)", "101", error)
}
} }
@objc func getAllLogs(_ call: CAPPluginCall) { @objc func getAllLogs(_ call: CAPPluginCall) {
call.unimplemented("Not implemented on iOS") do {
let logs = Database.shared.getAllLogs()
call.resolve([ "value": try logs.asDictionaryArray()])
} catch {
call.reject("Failed to get logs", "100", error)
}
} }
@objc func clearLogs(_ call: CAPPluginCall) { @objc func clearLogs(_ call: CAPPluginCall) {
call.unimplemented("Not implemented on iOS") do {
try Database.shared.clearLogs()
call.resolve()
} catch {
call.reject("Failed to clear logs", "100", error)
}
} }
} }
+17
View File
@@ -0,0 +1,17 @@
//
// LogEntry.swift
// Audiobookshelf
//
// Created by Christopher Jensen-Reimann on 9/3/25.
//
import Foundation
import RealmSwift
class LogEntry: Object, Codable {
@Persisted(primaryKey: true) var id: String = UUID().uuidString
@Persisted var tag: String = ""
@Persisted var level: String = ""
@Persisted var message: String = ""
@Persisted var timestamp: Int = 0
}
+25 -28
View File
@@ -26,7 +26,6 @@ enum PlayerStatus: Int {
class AudioPlayer: NSObject { class AudioPlayer: NSObject {
internal let queue = DispatchQueue(label: "ABSAudioPlayerQueue") internal let queue = DispatchQueue(label: "ABSAudioPlayerQueue")
internal let logger = AppLogger(category: "AudioPlayer")
private var status: PlayerStatus private var status: PlayerStatus
internal var rateManager: AudioPlayerRateManager internal var rateManager: AudioPlayerRateManager
@@ -81,7 +80,7 @@ class AudioPlayer: NSObject {
let playbackSession = self.getPlaybackSession() let playbackSession = self.getPlaybackSession()
guard let playbackSession = playbackSession else { guard let playbackSession = playbackSession else {
logger.error("Failed to fetch playback session. Player will not initialize") AbsLogger.error(message:"Failed to fetch playback session. Player will not initialize")
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil) NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
return return
} }
@@ -115,10 +114,10 @@ class AudioPlayer: NSObject {
} }
self.currentTrackIndex = getItemIndexForTime(time: playbackSession.currentTime) self.currentTrackIndex = getItemIndexForTime(time: playbackSession.currentTime)
logger.log("Starting track index \(self.currentTrackIndex) for start time \(playbackSession.currentTime)") AbsLogger.info(message:"Starting track index \(self.currentTrackIndex) for start time \(playbackSession.currentTime)")
let playerItems = self.allPlayerItems[self.currentTrackIndex..<self.allPlayerItems.count] let playerItems = self.allPlayerItems[self.currentTrackIndex..<self.allPlayerItems.count]
logger.log("Setting player items \(playerItems.count)") AbsLogger.info(message:"Setting player items \(playerItems.count)")
for item in Array(playerItems) { for item in Array(playerItems) {
self.audioPlayer.insert(item, after:self.audioPlayer.items().last) self.audioPlayer.insert(item, after:self.audioPlayer.items().last)
@@ -128,7 +127,7 @@ class AudioPlayer: NSObject {
setupQueueObserver() setupQueueObserver()
setupQueueItemStatusObserver() setupQueueItemStatusObserver()
logger.log("Audioplayer ready") AbsLogger.info(message:"Audioplayer ready")
} }
deinit { deinit {
@@ -146,8 +145,7 @@ class AudioPlayer: NSObject {
do { do {
try AVAudioSession.sharedInstance().setActive(false) try AVAudioSession.sharedInstance().setActive(false)
} catch { } catch {
logger.error("Failed to set AVAudioSession inactive") AbsLogger.error(message: "Failed to set AVAudioSession inactive", error: error)
logger.error(error)
} }
self.removeAudioSessionNotifications() self.removeAudioSessionNotifications()
@@ -253,14 +251,14 @@ class AudioPlayer: NSObject {
self.audioPlayer.currentItem.map { item in self.audioPlayer.currentItem.map { item in
self.currentTrackIndex = self.allPlayerItems.firstIndex(of:item) ?? 0 self.currentTrackIndex = self.allPlayerItems.firstIndex(of:item) ?? 0
if (self.currentTrackIndex != prevTrackIndex) { if (self.currentTrackIndex != prevTrackIndex) {
self.logger.log("New Current track index \(self.currentTrackIndex)") AbsLogger.info(message:"New Current track index \(self.currentTrackIndex)")
} }
} }
} }
} }
private func setupQueueItemStatusObserver() { private func setupQueueItemStatusObserver() {
logger.log("queueStatusObserver: Setting up") AbsLogger.info(message:"queueStatusObserver: Setting up")
// Listen for player item updates // Listen for player item updates
self.queueItemStatusObserver?.invalidate() self.queueItemStatusObserver?.invalidate()
@@ -275,13 +273,13 @@ class AudioPlayer: NSObject {
} }
private func handleQueueItemStatus(playerItem: AVPlayerItem) { private func handleQueueItemStatus(playerItem: AVPlayerItem) {
logger.log("queueStatusObserver: Current item status changed") AbsLogger.info(message:"queueStatusObserver: Current item status changed")
guard let playbackSession = self.getPlaybackSession() else { guard let playbackSession = self.getPlaybackSession() else {
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil) NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
return return
} }
if (playerItem.status == .readyToPlay) { if (playerItem.status == .readyToPlay) {
logger.log("queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)") AbsLogger.info(message:"queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)")
// Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session // Seek the player before initializing, so a currentTime of 0 does not appear in MediaProgress / session
let firstReady = self.status == .uninitialized let firstReady = self.status == .uninitialized
@@ -300,7 +298,7 @@ class AudioPlayer: NSObject {
self.status = .paused self.status = .paused
} }
} else if (playerItem.status == .failed) { } else if (playerItem.status == .failed) {
logger.error("queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")") AbsLogger.error(message:"queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")")
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil) NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
} }
} }
@@ -332,7 +330,7 @@ class AudioPlayer: NSObject {
} }
private func resumePlayback() { private func resumePlayback() {
logger.log("PLAY: Resuming playback") AbsLogger.info(message:"PLAY: Resuming playback")
self.markAudioSessionAs(active: true) self.markAudioSessionAs(active: true)
DispatchQueue.runOnMainQueue { DispatchQueue.runOnMainQueue {
@@ -351,7 +349,7 @@ class AudioPlayer: NSObject {
public func pause() { public func pause() {
guard self.isInitialized() else { return } guard self.isInitialized() else { return }
logger.log("PAUSE: Pausing playback") AbsLogger.info(message:"PAUSE: Pausing playback")
DispatchQueue.runOnMainQueue { DispatchQueue.runOnMainQueue {
self.audioPlayer.pause() self.audioPlayer.pause()
} }
@@ -371,7 +369,7 @@ class AudioPlayer: NSObject {
public func startFadeOut() { public func startFadeOut() {
guard self.isInitialized() else { return } guard self.isInitialized() else { return }
guard let currentTime = self.getCurrentTime() else { return } guard let currentTime = self.getCurrentTime() else { return }
logger.log("fadeOut: Fading out playback") AbsLogger.info(message:"fadeOut: Fading out playback")
// Define fade parameters. // Define fade parameters.
let fadeDuration: Float = 60.0 // total fade duration in seconds let fadeDuration: Float = 60.0 // total fade duration in seconds
@@ -409,7 +407,7 @@ class AudioPlayer: NSObject {
// Ensure volume is exactly zero and end fade. // Ensure volume is exactly zero and end fade.
self.audioPlayer.volume = targetVolume self.audioPlayer.volume = targetVolume
t.invalidate() t.invalidate()
self.logger.log("Fadeout: Fade complete, pausing playback") AbsLogger.info(message:"Fadeout: Fade complete, pausing playback")
self.pause() self.pause()
self.audioPlayer.volume = initialVolume self.audioPlayer.volume = initialVolume
self.seek(currentTime, from: "fadeOut") self.seek(currentTime, from: "fadeOut")
@@ -419,12 +417,12 @@ class AudioPlayer: NSObject {
} }
public func seek(_ to: Double, from: String) { public func seek(_ to: Double, from: String) {
logger.log("SEEK: Seek to \(to) from \(from)") AbsLogger.info(message:"SEEK: Seek to \(to) from \(from)")
guard let playbackSession = self.getPlaybackSession() else { return } guard let playbackSession = self.getPlaybackSession() else { return }
let indexOfSeek = getItemIndexForTime(time: to) let indexOfSeek = getItemIndexForTime(time: to)
logger.log("SEEK: Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)") AbsLogger.info(message:"SEEK: Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)")
if self.audioPlayer.currentItem == nil { if self.audioPlayer.currentItem == nil {
self.currentTrackIndex = indexOfSeek self.currentTrackIndex = indexOfSeek
@@ -488,16 +486,16 @@ class AudioPlayer: NSObject {
let currentTrack = playbackSession.audioTracks[self.currentTrackIndex] let currentTrack = playbackSession.audioTracks[self.currentTrackIndex]
let ctso = currentTrack.startOffset ?? 0.0 let ctso = currentTrack.startOffset ?? 0.0
let trackEnd = ctso + currentTrack.duration let trackEnd = ctso + currentTrack.duration
logger.log("SEEK: Seeking in current item \(to) (track START = \(ctso) END = \(trackEnd))") AbsLogger.info(message:"SEEK: Seeking in current item \(to) (track START = \(ctso) END = \(trackEnd))")
let boundedTime = min(max(to, ctso), trackEnd) let boundedTime = min(max(to, ctso), trackEnd)
let seekTime = boundedTime - ctso let seekTime = boundedTime - ctso
DispatchQueue.runOnMainQueue { DispatchQueue.runOnMainQueue {
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { [weak self] completed in self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { [weak self] completed in
self?.logger.log("SEEK: Completion handler called") AbsLogger.info(message:"SEEK: Completion handler called")
guard completed else { guard completed else {
self?.logger.log("SEEK: WARNING: seeking not completed (to \(seekTime)") AbsLogger.info(message:"SEEK: WARNING: seeking not completed (to \(seekTime)")
return return
} }
guard let self = self else { return } guard let self = self else { return }
@@ -574,7 +572,7 @@ class AudioPlayer: NSObject {
} else if (playbackSession.playMethod == PlayMethod.local.rawValue) { } else if (playbackSession.playMethod == PlayMethod.local.rawValue) {
guard let localFile = track.getLocalFile() else { guard let localFile = track.getLocalFile() else {
// Worst case we can stream the file // Worst case we can stream the file
logger.log("Unable to play local file. Resulting to streaming \(track.localFileId ?? "Unknown")") AbsLogger.info(message:"Unable to play local file. Resulting to streaming \(track.localFileId ?? "Unknown")")
let urlstr = "\(Store.serverConfig!.address)/api/items/\(itemId)/file/\(ino)?token=\(Store.serverConfig!.token)" let urlstr = "\(Store.serverConfig!.address)/api/items/\(itemId)/file/\(ino)?token=\(Store.serverConfig!.token)"
let url = URL(string: urlstr)! let url = URL(string: urlstr)!
return AVURLAsset(url: url) return AVURLAsset(url: url)
@@ -594,8 +592,7 @@ class AudioPlayer: NSObject {
do { do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio) try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio)
} catch { } catch {
logger.error("Failed to set AVAudioSession category") AbsLogger.error(message: "Failed to set AVAudioSession category", error: error)
logger.error(error)
} }
} }
@@ -603,7 +600,7 @@ class AudioPlayer: NSObject {
do { do {
try AVAudioSession.sharedInstance().setActive(active) try AVAudioSession.sharedInstance().setActive(active)
} catch { } catch {
logger.error("Failed to set audio session as active=\(active)") AbsLogger.error(message:"Failed to set audio session as active=\(active)")
} }
} }
@@ -620,7 +617,7 @@ class AudioPlayer: NSObject {
let reasonValue = userInfo[AVAudioSessionInterruptionReasonKey] as? UInt ?? 0 let reasonValue = userInfo[AVAudioSessionInterruptionReasonKey] as? UInt ?? 0
let reason = AVAudioSession.InterruptionReason(rawValue: reasonValue) let reason = AVAudioSession.InterruptionReason(rawValue: reasonValue)
if (reason == .appWasSuspended) { if (reason == .appWasSuspended) {
logger.log("AVAudioSession was suspended") AbsLogger.info(message:"AVAudioSession was suspended")
return return
} }
} }
@@ -812,14 +809,14 @@ class AudioPlayer: NSObject {
if context == &playerContext { if context == &playerContext {
if keyPath == #keyPath(AVPlayer.currentItem) { 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") AbsLogger.info(message:"WARNING: Item ended")
if audioPlayer.currentItem == nil { if audioPlayer.currentItem == nil {
// if the queue is rebuilding, we expect the current item may be nil // if the queue is rebuilding, we expect the current item may be nil
if self.isRebuildingQueue { if self.isRebuildingQueue {
return return
} }
logger.log("Player ended or next item is nil, marking ended") AbsLogger.info(message:"Player ended or next item is nil, marking ended")
self.markAudioSessionAs(active: false) self.markAudioSessionAs(active: false)
} }
} }
@@ -31,7 +31,7 @@ extension AudioPlayer {
} }
public func setSleepTimer(secondsUntilSleep: Double) { public func setSleepTimer(secondsUntilSleep: Double) {
logger.log("SLEEP TIMER: Sleeping in \(secondsUntilSleep) seconds") AbsLogger.info(message: "SLEEP TIMER: Sleeping in \(secondsUntilSleep) seconds")
self.removeSleepTimer() self.removeSleepTimer()
self.sleepTimeRemaining = secondsUntilSleep self.sleepTimeRemaining = secondsUntilSleep
@@ -53,7 +53,7 @@ extension AudioPlayer {
guard let currentTime = self.getCurrentTime() else { return } guard let currentTime = self.getCurrentTime() else { return }
guard stopAt >= currentTime else { return } guard stopAt >= currentTime else { return }
logger.log("SLEEP TIMER: Scheduling for chapter end \(stopAt)") AbsLogger.info(message: "SLEEP TIMER: Scheduling for chapter end \(stopAt)")
// Schedule the observation time // Schedule the observation time
self.sleepTimeChapterStopAt = stopAt self.sleepTimeChapterStopAt = stopAt
@@ -138,7 +138,7 @@ extension AudioPlayer {
} }
private func handleSleepEnd() { private func handleSleepEnd() {
logger.log("SLEEP TIMER: Pausing audio") AbsLogger.info(message: "SLEEP TIMER: Pausing audio")
self.pause() self.pause()
self.removeSleepTimer() self.removeSleepTimer()
} }
@@ -10,7 +10,6 @@ import AVFoundation
@available(iOS 16.0, *) @available(iOS 16.0, *)
class DefaultedAudioPlayerRateManager: NSObject, AudioPlayerRateManager { class DefaultedAudioPlayerRateManager: NSObject, AudioPlayerRateManager {
internal let logger = AppLogger(category: "DefaultedAudioPlayerRateManager")
internal var audioPlayer: AVPlayer internal var audioPlayer: AVPlayer
@@ -9,7 +9,6 @@ import Foundation
import AVFoundation import AVFoundation
class LegacyAudioPlayerRateManager: NSObject, AudioPlayerRateManager { class LegacyAudioPlayerRateManager: NSObject, AudioPlayerRateManager {
internal let logger = AppLogger(category: "AudioPlayer")
internal var audioPlayer: AVPlayer internal var audioPlayer: AVPlayer
@@ -54,7 +53,7 @@ class LegacyAudioPlayerRateManager: NSObject, AudioPlayerRateManager {
let playbackSpeedChanged = rate > 0.0 && rate != self.defaultRate && !(observed && rate == 1) let playbackSpeedChanged = rate > 0.0 && rate != self.defaultRate && !(observed && rate == 1)
if self.audioPlayer.rate != rate { if self.audioPlayer.rate != rate {
logger.log("setPlaybakRate rate changed from \(self.audioPlayer.rate) to \(rate)") AbsLogger.info(message: "setPlaybakRate rate changed from \(self.audioPlayer.rate) to \(rate)")
DispatchQueue.runOnMainQueue { DispatchQueue.runOnMainQueue {
self.audioPlayer.rate = rate self.audioPlayer.rate = rate
} }
@@ -73,7 +72,7 @@ class LegacyAudioPlayerRateManager: NSObject, AudioPlayerRateManager {
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 == &managerContext { if context == &managerContext {
if keyPath == #keyPath(AVPlayer.rate) { if keyPath == #keyPath(AVPlayer.rate) {
logger.log("playerContext observer player rate") AbsLogger.info(message: "playerContext observer player rate")
self.handlePlaybackRateChange(change?[.newKey] as? Float ?? 1.0, observed: true) self.handlePlaybackRateChange(change?[.newKey] as? Float ?? 1.0, observed: true)
} }
} else { } else {
+10 -13
View File
@@ -14,8 +14,6 @@ class PlayerProgress {
private static var TIME_BETWEEN_SESSION_SYNC_IN_SECONDS = 15.0 private static var TIME_BETWEEN_SESSION_SYNC_IN_SECONDS = 15.0
private let logger = AppLogger(category: "PlayerProgress")
private init() {} private init() {}
@@ -30,8 +28,7 @@ class PlayerProgress {
try await updateServerSessionFromLocalSession(session, rateLimitSync: !isStopping) try await updateServerSessionFromLocalSession(session, rateLimitSync: !isStopping)
} }
} catch { } catch {
logger.error("Failed to syncFromPlayer") AbsLogger.error(message: "Failed to syncFromPlayer \(error)", error: error)
logger.error(error)
} }
await UIApplication.shared.endBackgroundTask(backgroundToken) await UIApplication.shared.endBackgroundTask(backgroundToken)
} }
@@ -76,7 +73,7 @@ class PlayerProgress {
try localMediaProgress.updateFromPlaybackSession(session) try localMediaProgress.updateFromPlaybackSession(session)
logger.log("Local progress saved to the database") AbsLogger.info(message:"Local progress saved to the database")
// Send the local progress back to front-end // Send the local progress back to front-end
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil) NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.localProgress.rawValue), object: nil)
@@ -126,7 +123,7 @@ class PlayerProgress {
session = session.freeze() session = session.freeze()
guard safeToSync else { return } guard safeToSync else { return }
logger.log("Sending sessionId(\(session.id)) to server with currentTime(\(session.currentTime))") AbsLogger.info(message:"Sending sessionId(\(session.id)) to server with currentTime(\(session.currentTime))")
var success = false var success = false
if session.isLocal { if session.isLocal {
@@ -155,25 +152,25 @@ class PlayerProgress {
// TODO: Unused for now // TODO: Unused for now
private func updateLocalSessionFromServerMediaProgress() async throws { private func updateLocalSessionFromServerMediaProgress() async throws {
logger.log("updateLocalSessionFromServerMediaProgress: Checking if local media progress was updated on server") AbsLogger.info(message:"updateLocalSessionFromServerMediaProgress: Checking if local media progress was updated on server")
guard let session = try Realm(queue: nil).objects(PlaybackSession.self).last(where: { guard let session = try Realm(queue: nil).objects(PlaybackSession.self).last(where: {
$0.isActiveSession == true && $0.serverConnectionConfigId == Store.serverConfig?.id $0.isActiveSession == true && $0.serverConnectionConfigId == Store.serverConfig?.id
})?.freeze() else { })?.freeze() else {
logger.log("updateLocalSessionFromServerMediaProgress: Failed to get session") AbsLogger.info(message:"updateLocalSessionFromServerMediaProgress: Failed to get session")
return return
} }
// Fetch the current progress // Fetch the current progress
let progress = await ApiClient.getMediaProgress(libraryItemId: session.libraryItemId!, episodeId: session.episodeId) let progress = await ApiClient.getMediaProgress(libraryItemId: session.libraryItemId!, episodeId: session.episodeId)
guard let progress = progress else { guard let progress = progress else {
logger.log("updateLocalSessionFromServerMediaProgress: No progress object") AbsLogger.info(message:"updateLocalSessionFromServerMediaProgress: No progress object")
return return
} }
// Determine which session is newer // Determine which session is newer
let serverLastUpdate = progress.lastUpdate let serverLastUpdate = progress.lastUpdate
guard let localLastUpdate = session.updatedAt else { guard let localLastUpdate = session.updatedAt else {
logger.log("updateLocalSessionFromServerMediaProgress: No local session updatedAt") AbsLogger.info(message:"updateLocalSessionFromServerMediaProgress: No local session updatedAt")
return return
} }
let serverCurrentTime = progress.currentTime let serverCurrentTime = progress.currentTime
@@ -184,16 +181,16 @@ class PlayerProgress {
// Update the session, if needed // Update the session, if needed
if serverIsNewerThanLocal && currentTimeIsDifferent { if serverIsNewerThanLocal && currentTimeIsDifferent {
logger.log("updateLocalSessionFromServerMediaProgress: Server has newer time than local serverLastUpdate=\(serverLastUpdate) localLastUpdate=\(localLastUpdate)") AbsLogger.info(message:"updateLocalSessionFromServerMediaProgress: Server has newer time than local serverLastUpdate=\(serverLastUpdate) localLastUpdate=\(localLastUpdate)")
guard let session = session.thaw() else { return } guard let session = session.thaw() else { return }
try session.update { try session.update {
session.currentTime = serverCurrentTime session.currentTime = serverCurrentTime
session.updatedAt = serverLastUpdate session.updatedAt = serverLastUpdate
} }
logger.log("updateLocalSessionFromServerMediaProgress: Updated session currentTime newCurrentTime=\(serverCurrentTime) previousCurrentTime=\(localCurrentTime)") AbsLogger.info(message:"updateLocalSessionFromServerMediaProgress: Updated session currentTime newCurrentTime=\(serverCurrentTime) previousCurrentTime=\(localCurrentTime)")
PlayerHandler.seek(amount: session.currentTime) PlayerHandler.seek(amount: session.currentTime)
} else { } else {
logger.log("updateLocalSessionFromServerMediaProgress: Local session does not need updating; local has latest progress") AbsLogger.info(message:"updateLocalSessionFromServerMediaProgress: Local session does not need updating; local has latest progress")
} }
} }
+51 -52
View File
@@ -9,7 +9,6 @@ import Foundation
import Alamofire import Alamofire
class ApiClient { class ApiClient {
private static let logger = AppLogger(category: "ApiClient")
private static let secureStorage = SecureStorage() private static let secureStorage = SecureStorage()
public static func getData(from url: URL, completion: @escaping (UIImage?) -> Void) { public static func getData(from url: URL, completion: @escaping (UIImage?) -> Void) {
@@ -22,7 +21,7 @@ class ApiClient {
public static func postResource<T: Decodable>(endpoint: String, parameters: [String: Any], decodable: T.Type = T.self, callback: ((_ param: T) -> Void)?) { public static func postResource<T: Decodable>(endpoint: String, parameters: [String: Any], decodable: T.Type = T.self, callback: ((_ param: T) -> Void)?) {
if (Store.serverConfig == nil) { if (Store.serverConfig == nil) {
logger.error("Server config not set") AbsLogger.error(message: "Server config not set")
return return
} }
@@ -35,7 +34,7 @@ class ApiClient {
case .success(let obj): case .success(let obj):
callback?(obj) callback?(obj)
case .failure(let error): case .failure(let error):
logger.error("api request to \(endpoint) failed") AbsLogger.error(message: "api request to \(endpoint) failed")
print(error) print(error)
} }
} }
@@ -43,7 +42,7 @@ class ApiClient {
public static func postResource<T: Encodable, U: Decodable>(endpoint: String, parameters: T, decodable: U.Type = U.self, callback: ((_ param: U) -> Void)?) { public static func postResource<T: Encodable, U: Decodable>(endpoint: String, parameters: T, decodable: U.Type = U.self, callback: ((_ param: U) -> Void)?) {
if (Store.serverConfig == nil) { if (Store.serverConfig == nil) {
logger.error("Server config not set") AbsLogger.error(message: "Server config not set")
return return
} }
@@ -56,7 +55,7 @@ class ApiClient {
case .success(let obj): case .success(let obj):
callback?(obj) callback?(obj)
case .failure(let error): case .failure(let error):
logger.error("api request to \(endpoint) failed") AbsLogger.error(message: "api request to \(endpoint) failed")
print(error) print(error)
} }
} }
@@ -72,7 +71,7 @@ class ApiClient {
public static func postResource<T:Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) { public static func postResource<T:Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
if (Store.serverConfig == nil) { if (Store.serverConfig == nil) {
logger.error("Server config not set") AbsLogger.error(message: "Server config not set")
callback?(false) callback?(false)
return return
} }
@@ -86,7 +85,7 @@ class ApiClient {
case .success(_): case .success(_):
callback?(true) callback?(true)
case .failure(let error): case .failure(let error):
logger.error("api request to \(endpoint) failed") AbsLogger.error(message: "api request to \(endpoint) failed")
print(error) print(error)
callback?(false) callback?(false)
@@ -96,7 +95,7 @@ class ApiClient {
public static func patchResource<T: Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) { public static func patchResource<T: Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
if (Store.serverConfig == nil) { if (Store.serverConfig == nil) {
logger.error("Server config not set") AbsLogger.error(message: "Server config not set")
callback?(false) callback?(false)
return return
} }
@@ -110,7 +109,7 @@ class ApiClient {
case .success(_): case .success(_):
callback?(true) callback?(true)
case .failure(let error): case .failure(let error):
logger.error("api request to \(endpoint) failed") AbsLogger.error(message: "api request to \(endpoint) failed")
print(error) print(error)
callback?(false) callback?(false)
} }
@@ -127,7 +126,7 @@ class ApiClient {
public static func getResource<T: Decodable>(endpoint: String, decodable: T.Type = T.self, callback: ((_ param: T?) -> Void)?) { public static func getResource<T: Decodable>(endpoint: String, decodable: T.Type = T.self, callback: ((_ param: T?) -> Void)?) {
if (Store.serverConfig == nil) { if (Store.serverConfig == nil) {
logger.error("Server config not set") AbsLogger.error(message: "Server config not set")
callback?(nil) callback?(nil)
return return
} }
@@ -141,7 +140,7 @@ class ApiClient {
case .success(let obj): case .success(let obj):
callback?(obj) callback?(obj)
case .failure(let error): case .failure(let error):
logger.error("api request to \(endpoint) failed") AbsLogger.error(message: "api request to \(endpoint) failed")
print(error) print(error)
} }
} }
@@ -160,22 +159,22 @@ class ApiClient {
*/ */
private static func handleTokenRefresh<T: Decodable>(originalRequest: DataRequest, endpoint: String, method: HTTPMethod, parameters: Any?, decodable: T.Type, callback: ((_ param: T?) -> Void)?) { private static func handleTokenRefresh<T: Decodable>(originalRequest: DataRequest, endpoint: String, method: HTTPMethod, parameters: Any?, decodable: T.Type, callback: ((_ param: T?) -> Void)?) {
guard let serverConfig = Store.serverConfig else { guard let serverConfig = Store.serverConfig else {
logger.error("handleTokenRefresh: No server config available") AbsLogger.error(message: "handleTokenRefresh: No server config available")
callback?(nil) callback?(nil)
return return
} }
logger.log("handleTokenRefresh: Attempting to refresh auth tokens for server \(serverConfig.name)") AbsLogger.info(message: "handleTokenRefresh: Attempting to refresh auth tokens for server \(serverConfig.name)")
// Get refresh token from secure storage // Get refresh token from secure storage
guard let refreshToken = secureStorage.getRefreshToken(serverConnectionConfigId: serverConfig.id) else { guard let refreshToken = secureStorage.getRefreshToken(serverConnectionConfigId: serverConfig.id) else {
logger.error("handleTokenRefresh: No refresh token available for server \(serverConfig.name)") AbsLogger.error(message: "handleTokenRefresh: No refresh token available for server \(serverConfig.name)")
handleRefreshFailure() handleRefreshFailure()
callback?(nil) callback?(nil)
return return
} }
logger.log("handleTokenRefresh: Retrieved refresh token, attempting to refresh access token") AbsLogger.info(message: "handleTokenRefresh: Retrieved refresh token, attempting to refresh access token")
// Create refresh token request // Create refresh token request
let refreshHeaders: HTTPHeaders = [ let refreshHeaders: HTTPHeaders = [
@@ -190,23 +189,23 @@ class ApiClient {
case .success(let refreshResponse): case .success(let refreshResponse):
guard let user = refreshResponse.user, guard let user = refreshResponse.user,
!user.accessToken.isEmpty else { !user.accessToken.isEmpty else {
logger.error("handleTokenRefresh: No access token in refresh response for server \(serverConfig.name)") AbsLogger.error(message: "handleTokenRefresh: No access token in refresh response for server \(serverConfig.name)")
handleRefreshFailure() handleRefreshFailure()
callback?(nil) callback?(nil)
return return
} }
logger.log("handleTokenRefresh: Successfully obtained new access token") AbsLogger.info(message: "handleTokenRefresh: Successfully obtained new access token")
// Update tokens in secure storage and store // Update tokens in secure storage and store
updateTokens(newAccessToken: user.accessToken, newRefreshToken: user.refreshToken ?? refreshToken, serverConnectionConfigId: serverConfig.id) updateTokens(newAccessToken: user.accessToken, newRefreshToken: user.refreshToken ?? refreshToken, serverConnectionConfigId: serverConfig.id)
// Retry the original request with the new access token // Retry the original request with the new access token
logger.log("handleTokenRefresh: Retrying original request with new token") AbsLogger.info(message: "handleTokenRefresh: Retrying original request with new token")
retryOriginalRequest(endpoint: endpoint, method: method, parameters: parameters, decodable: decodable, newAccessToken: user.accessToken, callback: callback) retryOriginalRequest(endpoint: endpoint, method: method, parameters: parameters, decodable: decodable, newAccessToken: user.accessToken, callback: callback)
case .failure(let error): case .failure(let error):
logger.error("handleTokenRefresh: Refresh request failed for server \(serverConfig.name): \(error)") AbsLogger.error(message: "handleTokenRefresh: Refresh request failed for server \(serverConfig.name): \(error)")
handleRefreshFailure() handleRefreshFailure()
callback?(nil) callback?(nil)
} }
@@ -220,14 +219,14 @@ class ApiClient {
// Update the refresh token in secure storage if it's new // Update the refresh token in secure storage if it's new
if newRefreshToken != secureStorage.getRefreshToken(serverConnectionConfigId: serverConnectionConfigId) { if newRefreshToken != secureStorage.getRefreshToken(serverConnectionConfigId: serverConnectionConfigId) {
let hasStored = secureStorage.storeRefreshToken(serverConnectionConfigId: serverConnectionConfigId, refreshToken: newRefreshToken) let hasStored = secureStorage.storeRefreshToken(serverConnectionConfigId: serverConnectionConfigId, refreshToken: newRefreshToken)
logger.log("updateTokens: Updated refresh token in secure storage. Stored=\(hasStored)") AbsLogger.info(message: "updateTokens: Updated refresh token in secure storage. Stored=\(hasStored)")
} }
// Update access token on server connection config // Update access token on server connection config
Database.shared.updateServerConnectionConfigToken(newToken: newAccessToken) Database.shared.updateServerConnectionConfigToken(newToken: newAccessToken)
logger.log("updateTokens: Updated access token in server connection config") AbsLogger.info(message: "updateTokens: Updated access token in server connection config")
logger.log("updateTokens: Successfully refreshed auth tokens for server \(Store.serverConfig?.name ?? "unknown")") AbsLogger.info(message: "updateTokens: Successfully refreshed auth tokens for server \(Store.serverConfig?.name ?? "unknown")")
// Notify webview frontend about token refresh // Notify webview frontend about token refresh
if let callback = AbsDatabase.tokenRefreshCallback { if let callback = AbsDatabase.tokenRefreshCallback {
@@ -241,7 +240,7 @@ class ApiClient {
*/ */
private static func retryOriginalRequest<T: Decodable>(endpoint: String, method: HTTPMethod, parameters: Any?, decodable: T.Type, newAccessToken: String, callback: ((_ param: T?) -> Void)?) { private static func retryOriginalRequest<T: Decodable>(endpoint: String, method: HTTPMethod, parameters: Any?, decodable: T.Type, newAccessToken: String, callback: ((_ param: T?) -> Void)?) {
guard let serverConfig = Store.serverConfig else { guard let serverConfig = Store.serverConfig else {
logger.error("retryOriginalRequest: No server config available") AbsLogger.error(message: "retryOriginalRequest: No server config available")
callback?(nil) callback?(nil)
return return
} }
@@ -270,7 +269,7 @@ class ApiClient {
retryRequest = AF.request("\(serverConfig.address)/\(endpoint)", method: .patch, headers: headers) retryRequest = AF.request("\(serverConfig.address)/\(endpoint)", method: .patch, headers: headers)
} }
default: default:
logger.error("retryOriginalRequest: Unsupported method \(method)") AbsLogger.error(message: "retryOriginalRequest: Unsupported method \(method)")
callback?(nil) callback?(nil)
return return
} }
@@ -282,7 +281,7 @@ class ApiClient {
if let data = response.data, !data.isEmpty { if let data = response.data, !data.isEmpty {
// If it is a string return nil (e.g. express returns OK for 200 status codes) // If it is a string return nil (e.g. express returns OK for 200 status codes)
if let responseString = String(data: data, encoding: .utf8) { if let responseString = String(data: data, encoding: .utf8) {
logger.log("retryOriginalRequest: Got string response '\(responseString)'") AbsLogger.info(message: "retryOriginalRequest: Got string response '\(responseString)'")
callback?(nil) callback?(nil)
return return
} }
@@ -292,16 +291,16 @@ class ApiClient {
let decodedObject = try JSONDecoder().decode(decodable, from: data) let decodedObject = try JSONDecoder().decode(decodable, from: data)
callback?(decodedObject) callback?(decodedObject)
} catch { } catch {
logger.error("retryOriginalRequest: JSON decode failed: \(error)") AbsLogger.error(message: "retryOriginalRequest: JSON decode failed: \(error)", error: error)
callback?(nil) callback?(nil)
} }
} else { } else {
// Empty response // Empty response
logger.log("retryOriginalRequest: Empty response with success status \(statusCode)") AbsLogger.info(message: "retryOriginalRequest: Empty response with success status \(statusCode)")
callback?(nil) callback?(nil)
} }
} else { } else {
logger.error("retryOriginalRequest: Request failed with status \(response.response?.statusCode ?? 0)") AbsLogger.error(message: "retryOriginalRequest: Request failed with status \(response.response?.statusCode ?? 0)")
callback?(nil) callback?(nil)
} }
} }
@@ -312,7 +311,7 @@ class ApiClient {
* This will clear the current server connection and notify webview * This will clear the current server connection and notify webview
*/ */
private static func handleRefreshFailure() { private static func handleRefreshFailure() {
logger.log("handleRefreshFailure: Token refresh failed, clearing session") AbsLogger.info(message: "handleRefreshFailure: Token refresh failed, clearing session")
// Clear the current server connection // Clear the current server connection
Store.serverConfig = nil Store.serverConfig = nil
@@ -332,7 +331,7 @@ class ApiClient {
public static func getResourceWithTokenRefresh<T: Decodable>(endpoint: String, decodable: T.Type = T.self, callback: ((_ param: T?) -> Void)?) { public static func getResourceWithTokenRefresh<T: Decodable>(endpoint: String, decodable: T.Type = T.self, callback: ((_ param: T?) -> Void)?) {
if (Store.serverConfig == nil) { if (Store.serverConfig == nil) {
logger.error("Server config not set") AbsLogger.error(message: "Server config not set")
callback?(nil) callback?(nil)
return return
} }
@@ -345,14 +344,14 @@ class ApiClient {
request.responseDecodable(of: decodable) { response in request.responseDecodable(of: decodable) { response in
if let statusCode = response.response?.statusCode, statusCode == 401 { if let statusCode = response.response?.statusCode, statusCode == 401 {
logger.log("getResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh") AbsLogger.info(message: "getResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh")
handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .get, parameters: nil, decodable: decodable, callback: callback) handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .get, parameters: nil, decodable: decodable, callback: callback)
} else { } else {
switch response.result { switch response.result {
case .success(let obj): case .success(let obj):
callback?(obj) callback?(obj)
case .failure(let error): case .failure(let error):
logger.error("api request to \(endpoint) failed") AbsLogger.error(message: "api request to \(endpoint) failed")
print(error) print(error)
callback?(nil) callback?(nil)
} }
@@ -362,7 +361,7 @@ class ApiClient {
public static func postResourceWithTokenRefresh<T: Encodable, U: Decodable>(endpoint: String, parameters: T, decodable: U.Type = U.self, callback: ((_ param: U?) -> Void)?) { public static func postResourceWithTokenRefresh<T: Encodable, U: Decodable>(endpoint: String, parameters: T, decodable: U.Type = U.self, callback: ((_ param: U?) -> Void)?) {
if (Store.serverConfig == nil) { if (Store.serverConfig == nil) {
logger.error("Server config not set") AbsLogger.error(message: "Server config not set")
callback?(nil) callback?(nil)
return return
} }
@@ -375,14 +374,14 @@ class ApiClient {
request.responseDecodable(of: decodable) { response in request.responseDecodable(of: decodable) { response in
if let statusCode = response.response?.statusCode, statusCode == 401 { if let statusCode = response.response?.statusCode, statusCode == 401 {
logger.log("postResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh") AbsLogger.info(message: "postResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh")
handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .post, parameters: parameters, decodable: decodable, callback: callback) handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .post, parameters: parameters, decodable: decodable, callback: callback)
} else { } else {
switch response.result { switch response.result {
case .success(let obj): case .success(let obj):
callback?(obj) callback?(obj)
case .failure(let error): case .failure(let error):
logger.error("api request to \(endpoint) failed") AbsLogger.error(message: "api request to \(endpoint) failed")
print(error) print(error)
callback?(nil) callback?(nil)
} }
@@ -395,7 +394,7 @@ class ApiClient {
*/ */
public static func postResourceWithTokenRefresh<T: Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) { public static func postResourceWithTokenRefresh<T: Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
if (Store.serverConfig == nil) { if (Store.serverConfig == nil) {
logger.error("Server config not set") AbsLogger.error(message: "Server config not set")
callback?(false) callback?(false)
return return
} }
@@ -408,7 +407,7 @@ class ApiClient {
request.response { response in request.response { response in
if let statusCode = response.response?.statusCode, statusCode == 401 { if let statusCode = response.response?.statusCode, statusCode == 401 {
logger.log("postResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh") AbsLogger.info(message: "postResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh")
handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .post, parameters: parameters, decodable: EmptyResponse.self) { result in handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .post, parameters: parameters, decodable: EmptyResponse.self) { result in
callback?(result != nil) callback?(result != nil)
} }
@@ -417,7 +416,7 @@ class ApiClient {
case .success(_): case .success(_):
callback?(true) callback?(true)
case .failure(let error): case .failure(let error):
logger.error("api request to \(endpoint) failed") AbsLogger.error(message: "api request to \(endpoint) failed")
print(error) print(error)
callback?(false) callback?(false)
} }
@@ -427,7 +426,7 @@ class ApiClient {
public static func patchResourceWithTokenRefresh<T: Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) { public static func patchResourceWithTokenRefresh<T: Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
if (Store.serverConfig == nil) { if (Store.serverConfig == nil) {
logger.error("Server config not set") AbsLogger.error(message: "Server config not set")
callback?(false) callback?(false)
return return
} }
@@ -440,7 +439,7 @@ class ApiClient {
request.response { response in request.response { response in
if let statusCode = response.response?.statusCode, statusCode == 401 { if let statusCode = response.response?.statusCode, statusCode == 401 {
logger.log("patchResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh") AbsLogger.info(message: "patchResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh")
handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .patch, parameters: parameters, decodable: EmptyResponse.self) { result in handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .patch, parameters: parameters, decodable: EmptyResponse.self) { result in
callback?(result != nil) callback?(result != nil)
} }
@@ -449,7 +448,7 @@ class ApiClient {
case .success(_): case .success(_):
callback?(true) callback?(true)
case .failure(let error): case .failure(let error):
logger.error("api request to \(endpoint) failed") AbsLogger.error(message: "api request to \(endpoint) failed")
print(error) print(error)
callback?(false) callback?(false)
} }
@@ -489,7 +488,7 @@ class ApiClient {
// Use the new token refresh-enabled method // Use the new token refresh-enabled method
postResourceWithTokenRefresh(endpoint: endpoint, parameters: parameters, decodable: PlaybackSession.self) { session in postResourceWithTokenRefresh(endpoint: endpoint, parameters: parameters, decodable: PlaybackSession.self) { session in
guard let session = session else { guard let session = session else {
logger.error("startPlaybackSession: Failed to create playback session") AbsLogger.error(message: "startPlaybackSession: Failed to create playback session")
callback(PlaybackSession()) // Return empty session on failure callback(PlaybackSession()) // Return empty session on failure
return return
} }
@@ -533,14 +532,14 @@ class ApiClient {
let localMediaProgressList = Database.shared.getAllLocalMediaProgress().filter { let localMediaProgressList = Database.shared.getAllLocalMediaProgress().filter {
$0.serverConnectionConfigId == Store.serverConfig?.id $0.serverConnectionConfigId == Store.serverConfig?.id
}.map { $0.freeze() } }.map { $0.freeze() }
logger.log("syncLocalSessionsWithServer: Found \(localMediaProgressList.count) local media progress for server") AbsLogger.info(message: "syncLocalSessionsWithServer: Found \(localMediaProgressList.count) local media progress for server")
if (localMediaProgressList.isEmpty) { if (localMediaProgressList.isEmpty) {
logger.log("syncLocalSessionsWithServer: No local progress to sync") AbsLogger.info(message: "syncLocalSessionsWithServer: No local progress to sync")
} else { } else {
let currentUser = await ApiClient.getCurrentUser() let currentUser = await ApiClient.getCurrentUser()
guard let currentUser = currentUser else { guard let currentUser = currentUser else {
logger.log("syncLocalSessionsWithServer: No User") AbsLogger.info(message: "syncLocalSessionsWithServer: No User")
return return
} }
try currentUser.mediaProgress.forEach { mediaProgress in try currentUser.mediaProgress.forEach { mediaProgress in
@@ -552,12 +551,12 @@ class ApiClient {
} }
} }
if (localMediaProgress != nil && mediaProgress.lastUpdate > localMediaProgress!.lastUpdate) { if (localMediaProgress != nil && mediaProgress.lastUpdate > localMediaProgress!.lastUpdate) {
logger.log("syncLocalSessionsWithServer: Updating local media progress \(localMediaProgress!.id) with server media progress") AbsLogger.info(message: "syncLocalSessionsWithServer: Updating local media progress \(localMediaProgress!.id) with server media progress")
if let localMediaProgress = localMediaProgress?.thaw() { if let localMediaProgress = localMediaProgress?.thaw() {
try localMediaProgress.updateFromServerMediaProgress(mediaProgress) try localMediaProgress.updateFromServerMediaProgress(mediaProgress)
} }
} else if (localMediaProgress != nil) { } else if (localMediaProgress != nil) {
logger.log("syncLocalSessionsWithServer: Local progress for \(localMediaProgress!.id) is more recent then server progress") AbsLogger.info(message: "syncLocalSessionsWithServer: Local progress for \(localMediaProgress!.id) is more recent then server progress")
} }
} }
} }
@@ -566,13 +565,13 @@ 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 (first sync: \(isFirstSync))") AbsLogger.info(message: "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
logger.log("syncLocalSessionsWithServer: Handling \(session.displayTitle ?? "") (\(session.id)) \(session.isActiveSession)") AbsLogger.info(message: "syncLocalSessionsWithServer: Handling \(session.displayTitle ?? "") (\(session.id)) \(session.isActiveSession)")
// On first sync then remove all sessions // On first sync then remove all sessions
if (!session.isActiveSession || isFirstSync) { if (!session.isActiveSession || isFirstSync) {
if let session = session.thaw() { if let session = session.thaw() {
@@ -589,7 +588,7 @@ class ApiClient {
} }
public static func updateMediaProgress<T:Encodable>(libraryItemId: String, episodeId: String?, payload: T, callback: @escaping () -> Void) { public static func updateMediaProgress<T:Encodable>(libraryItemId: String, episodeId: String?, payload: T, callback: @escaping () -> Void) {
logger.log("updateMediaProgress \(libraryItemId) \(episodeId ?? "NIL") \(payload)") AbsLogger.info(message: "updateMediaProgress \(libraryItemId) \(episodeId ?? "NIL") \(payload)")
let endpoint = episodeId?.isEmpty ?? true ? "api/me/progress/\(libraryItemId)" : "api/me/progress/\(libraryItemId)/\(episodeId ?? "")" let endpoint = episodeId?.isEmpty ?? true ? "api/me/progress/\(libraryItemId)" : "api/me/progress/\(libraryItemId)/\(episodeId ?? "")"
patchResourceWithTokenRefresh(endpoint: endpoint, parameters: payload) { _ in patchResourceWithTokenRefresh(endpoint: endpoint, parameters: payload) { _ in
callback() callback()
@@ -597,7 +596,7 @@ class ApiClient {
} }
public static func getMediaProgress(libraryItemId: String, episodeId: String?) async -> MediaProgress? { public static func getMediaProgress(libraryItemId: String, episodeId: String?) async -> MediaProgress? {
logger.log("getMediaProgress \(libraryItemId) \(episodeId ?? "NIL")") AbsLogger.info(message: "getMediaProgress \(libraryItemId) \(episodeId ?? "NIL")")
let endpoint = episodeId?.isEmpty ?? true ? "api/me/progress/\(libraryItemId)" : "api/me/progress/\(libraryItemId)/\(episodeId ?? "")" let endpoint = episodeId?.isEmpty ?? true ? "api/me/progress/\(libraryItemId)" : "api/me/progress/\(libraryItemId)/\(episodeId ?? "")"
return await withCheckedContinuation { continuation in return await withCheckedContinuation { continuation in
getResourceWithTokenRefresh(endpoint: endpoint, decodable: MediaProgress.self) { result in getResourceWithTokenRefresh(endpoint: endpoint, decodable: MediaProgress.self) { result in
@@ -607,7 +606,7 @@ class ApiClient {
} }
public static func getCurrentUser() async -> User? { public static func getCurrentUser() async -> User? {
logger.log("getCurrentUser") AbsLogger.info(message: "getCurrentUser")
return await withCheckedContinuation { continuation in return await withCheckedContinuation { continuation in
getResourceWithTokenRefresh(endpoint: "api/me", decodable: User.self) { result in getResourceWithTokenRefresh(endpoint: "api/me", decodable: User.self) { result in
continuation.resume(returning: result) continuation.resume(returning: result)
+62 -8
View File
@@ -13,9 +13,13 @@ class Database {
return Database() return Database()
}() }()
private let logger = AppLogger(category: "Database") private init() {
do {
private init() {} try cleanExpiredLogs()
} catch {
debugPrint(error)
}
}
public func setServerConnectionConfig(config: ServerConnectionConfig) { public func setServerConnectionConfig(config: ServerConnectionConfig) {
let config = config let config = config
@@ -33,7 +37,7 @@ class Database {
existing.token = config.token existing.token = config.token
} }
} catch { } catch {
logger.error("failed to update server config") AbsLogger.error("setServerConn", message: "failed to update server config")
debugPrint(error) debugPrint(error)
} }
@@ -54,7 +58,7 @@ class Database {
realm.add(config) realm.add(config)
} }
} catch(let exception) { } catch(let exception) {
logger.error("failed to save server config") AbsLogger.error(message: "failed to save server config")
debugPrint(exception) debugPrint(exception)
} }
@@ -86,7 +90,7 @@ class Database {
} }
} }
} catch(let exception) { } catch(let exception) {
logger.error("failed to delete server config") AbsLogger.error(message: "failed to delete server config")
debugPrint(exception) debugPrint(exception)
} }
} }
@@ -117,7 +121,7 @@ class Database {
} }
} }
} catch(let exception) { } catch(let exception) {
logger.error("failed to save server config active index") AbsLogger.error(message: "failed to save server config active index")
debugPrint(exception) debugPrint(exception)
} }
} }
@@ -137,7 +141,7 @@ class Database {
realm.add(deviceSettings) realm.add(deviceSettings)
} }
} catch { } catch {
logger.error("failed to save device settings") AbsLogger.error(message: "failed to save device settings")
} }
} }
@@ -275,4 +279,54 @@ class Database {
return nil return nil
} }
} }
public func saveLog(_ log: LogEntry) throws {
let realm = try Realm()
return try realm.write { realm.add(log) }
}
public func getAllLogs() -> [LogEntry] {
do {
let realm = try Realm()
return realm.objects(LogEntry.self).toArray()
} catch {
debugPrint(error)
return []
}
}
public func clearLogs() throws {
do {
let realm = try! Realm()
try realm.write {
realm.objects(LogEntry.self).forEach { log in
realm.delete(log)
}
}
} catch {
AbsLogger.error(message: "\(error)", error: error)
throw error
}
}
private func cleanExpiredLogs() throws {
let realm = try Realm()
let numberOfHoursToKeep = 48
let keepLogCutoff = Date().addingTimeInterval(TimeInterval(-1 * numberOfHoursToKeep * 3600))
let allLogs = getAllLogs()
var logsRemoved = 0
try? realm.write {
allLogs.forEach { log in
if log.timestamp < Int(keepLogCutoff.timeIntervalSince1970) {
realm.delete(log)
logsRemoved += 1
}
}
}
if logsRemoved > 0 {
AbsLogger.info(message: "cleanLogs: Removed \(logsRemoved) logs older than \(numberOfHoursToKeep) hours")
}
}
} }
+78 -2
View File
@@ -65,8 +65,7 @@ extension CAPPluginCall {
let json = try JSONSerialization.data(withJSONObject: value) let json = try JSONSerialization.data(withJSONObject: value)
return try JSONDecoder().decode(type, from: json) return try JSONDecoder().decode(type, from: json)
} catch { } catch {
AppLogger().error("Failed to get json for \(key)") AbsLogger.error(message: "Failed to get json for \(key)", error: error)
debugPrint(error)
return nil return nil
} }
} }
@@ -106,3 +105,80 @@ extension URL {
return attributes?[.creationDate] as? Date return attributes?[.creationDate] as? Date
} }
} }
// MARK: - RealmSwift helpers
// From https://github.com/realm/realm-swift/issues/5859#issuecomment-589026869
extension Results {
func toArray<T: Object>() -> [T] {
var array = [T]()
for i in 0 ..< self.count {
if let result = self[i] as? T {
array.append(result.detached())
}
}
return array
}
}
extension List {
func toArray<T: Object>() -> [T] {
var array = [T]()
for i in 0 ..< self.count {
if let result = self[i] as? T {
array.append(result.detached())
}
}
return array
}
}
protocol DetachableObject: AnyObject {
func detached() -> Self
}
extension Object: DetachableObject {
func detached() -> Self {
let detached = type(of: self).init()
for property in objectSchema.properties {
guard let value = value(forKey: property.name) else { continue }
if property.isArray == true {
//Realm List property support
let detachable = value as? DetachableObject
detached.setValue(detachable?.detached(), forKey: property.name)
} else if property.type == .object {
//Realm Object property support
let detachable = value as? DetachableObject
detached.setValue(detachable?.detached(), forKey: property.name)
} else {
detached.setValue(value, forKey: property.name)
}
}
return detached
}
}
extension List: DetachableObject {
func detached() -> List<Element> {
let result = List<Element>()
forEach {
if let detachable = $0 as? DetachableObject {
let detached = detachable.detached() as! Element
result.append(detached)
} else {
result.append($0) //Primtives are pass by value; don't need to recreate
}
}
return result
}
func toArray() -> [Element] {
return Array(self.detached())
}
}
+27 -1
View File
@@ -24,6 +24,7 @@ export default {
inittingLibraries: false, inittingLibraries: false,
hasMounted: false, hasMounted: false,
disconnectTime: 0, disconnectTime: 0,
socketDisconnectedTime: 0,
timeLostFocus: 0, timeLostFocus: 0,
currentLang: null currentLang: null
} }
@@ -44,7 +45,7 @@ export default {
} else { } else {
var timeSinceDisconnect = Date.now() - this.disconnectTime var timeSinceDisconnect = Date.now() - this.disconnectTime
if (timeSinceDisconnect > 5000) { if (timeSinceDisconnect > 5000) {
console.log('Time since disconnect was', timeSinceDisconnect, 'sync with server') console.log('[default] Time since disconnect was', timeSinceDisconnect, 'sync with server')
setTimeout(() => { setTimeout(() => {
this.syncLocalSessions(false) this.syncLocalSessions(false)
}, 4000) }, 4000)
@@ -55,6 +56,28 @@ export default {
this.disconnectTime = Date.now() this.disconnectTime = Date.now()
} }
} }
},
socketConnected: {
handler(newVal, oldVal) {
if (!this.hasMounted) {
// watcher runs before mount, handling libraries/connection should be handled in mount
return
}
if (newVal) {
// if we havent been receiving socket events then external progress updates may have been missed
const timeSinceDisconnect = Date.now() - this.socketDisconnectedTime
if (timeSinceDisconnect > 30000 && this.isPlayerOpen) {
console.log('[default] socket reconnected after ' + timeSinceDisconnect + 'ms and player is open, triggering server media progress sync')
// used for triggering a server media progress sync if local media item is open in player
this.$eventBus.$emit('socket-reconnected')
} else {
console.log('[default] socket reconnected after ' + timeSinceDisconnect + 'ms')
}
} else {
console.log('[default] socket disconnected')
this.socketDisconnectedTime = Date.now()
}
}
} }
}, },
computed: { computed: {
@@ -67,6 +90,9 @@ export default {
networkConnected() { networkConnected() {
return this.$store.state.networkConnected return this.$store.state.networkConnected
}, },
socketConnected() {
return this.$store.state.socketConnected
},
user() { user() {
return this.$store.state.user.user return this.$store.state.user.user
}, },
+1 -1
View File
@@ -33,7 +33,7 @@ export default {
this.$strings.MessageConfirmStreamingUsingCellular this.$strings.MessageConfirmStreamingUsingCellular
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message message
}) })
return value return value
+12
View File
@@ -0,0 +1,12 @@
export default {
methods: {
getJumpLabel(seconds) {
const val = Number(seconds)
if (isNaN(val)) return ''
const useMinutes = val >= 120 // keep 60s as seconds
const key = useMinutes ? 'UnitMinutesShort' : 'UnitSecondsShort'
const unitValue = useMinutes ? val / 60 : val
return this.$getString(key, [this.$formatNumber(unitValue)])
}
}
}
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf-app", "name": "audiobookshelf-app",
"version": "0.10.1-beta", "version": "0.12.0-beta",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf-app", "name": "audiobookshelf-app",
"version": "0.10.1-beta", "version": "0.12.0-beta",
"dependencies": { "dependencies": {
"@capacitor-community/keep-awake": "^7.0.0", "@capacitor-community/keep-awake": "^7.0.0",
"@capacitor-community/volume-buttons": "^7.0.0", "@capacitor-community/volume-buttons": "^7.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-app", "name": "audiobookshelf-app",
"version": "0.10.1-beta", "version": "0.12.0-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",
+6
View File
@@ -74,6 +74,12 @@ export default {
console.log(`[categories] networkConnected false so fetching categories`) console.log(`[categories] networkConnected false so fetching categories`)
this.fetchCategories() this.fetchCategories()
} }
},
user(newVal, oldVal) {
if ((newVal && !oldVal) || (!newVal && oldVal)) {
console.log(`[categories] user changed so fetching categories`)
this.fetchCategories()
}
} }
}, },
computed: { computed: {
+1 -1
View File
@@ -148,7 +148,7 @@ export default {
// Show confirmation dialog and start downloading if user chooses so // Show confirmation dialog and start downloading if user chooses so
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: startDownloadMessage message: startDownloadMessage
}) })
if (value) { if (value) {
+1 -1
View File
@@ -11,7 +11,7 @@
<span v-else class="font-semibold text-fg">{{ Math.round(itemPart.progress) }}%</span> <span v-else class="font-semibold text-fg">{{ Math.round(itemPart.progress) }}%</span>
</div> </div>
<div class="flex-grow px-2"> <div class="flex-grow px-2">
<p class="truncate">{{ itemPart.filename }}</p> <p class="break-all">{{ itemPart.filename }}</p>
</div> </div>
</div> </div>
+4 -4
View File
@@ -346,7 +346,7 @@ export default {
} }
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: this.$getString('MessageConfirmDeleteLocalEpisode', [localFile.basePath]) message: this.$getString('MessageConfirmDeleteLocalEpisode', [localFile.basePath])
}) })
if (value) { if (value) {
@@ -514,7 +514,7 @@ export default {
await this.$hapticsImpact() await this.$hapticsImpact()
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: this.$strings.MessageConfirmDiscardProgress message: this.$strings.MessageConfirmDiscardProgress
}) })
if (value) { if (value) {
@@ -575,8 +575,8 @@ export default {
await this.$hapticsImpact() await this.$hapticsImpact()
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: `Are you sure you want to delete episode "${this.title}" from the server?\nWarning: This will delete the audio file.` message: this.$getString('MessageConfirmDeleteServerEpisode', [this.title])
}) })
if (value) { if (value) {
+8 -4
View File
@@ -535,7 +535,11 @@ export default {
if (this.isPodcast) { if (this.isPodcast) {
this.episodes.sort((a, b) => { this.episodes.sort((a, b) => {
return String(b.publishedAt).localeCompare(String(a.publishedAt), undefined, { numeric: true, sensitivity: 'base' }) if (this.podcastType === 'serial') {
return String(a.publishedAt).localeCompare(String(b.publishedAt), undefined, { numeric: true, sensitivity: 'base' })
} else {
return String(b.publishedAt).localeCompare(String(a.publishedAt), undefined, { numeric: true, sensitivity: 'base' })
}
}) })
let episode = this.episodes.find((ep) => { let episode = this.episodes.find((ep) => {
@@ -584,8 +588,8 @@ export default {
// If start time and is not already streaming then ask for confirmation // If start time and is not already streaming then ask for confirmation
if (startTime !== null && startTime !== undefined && !this.$store.getters['getIsMediaStreaming'](libraryItemId, null)) { if (startTime !== null && startTime !== undefined && !this.$store.getters['getIsMediaStreaming'](libraryItemId, null)) {
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: `Start playback for "${this.title}" at ${this.$secondsToTimestamp(startTime)}?` message: this.$getString('MessageConfirmPlaybackTime', [this.title, this.$secondsToTimestamp(startTime)])
}) })
if (!value) return if (!value) return
} }
@@ -673,7 +677,7 @@ export default {
} }
} }
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: startDownloadMessage message: startDownloadMessage
}) })
if (value) { if (value) {
+1 -1
View File
@@ -100,7 +100,7 @@ export default {
deleteMessage = `Are you sure you want to remove this folder and ${this.localLibraryItems.length} items? (does not delete anything in your file system)` deleteMessage = `Are you sure you want to remove this folder and ${this.localLibraryItems.length} items? (does not delete anything in your file system)`
} }
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: deleteMessage message: deleteMessage
}) })
if (value) { if (value) {
+1 -1
View File
@@ -22,7 +22,7 @@
</div> </div>
<div v-if="!isAndroid10OrBelow || overrideFolderRestriction" class="flex border-t border-fg/10 my-4 py-4"> <div v-if="!isAndroid10OrBelow || overrideFolderRestriction" class="flex border-t border-fg/10 my-4 py-4">
<div class="flex-grow pr-1"> <div class="flex-grow pr-1">
<ui-dropdown v-model="newFolderMediaType" placeholder="Select media type" :items="mediaTypeItems" /> <ui-dropdown v-model="newFolderMediaType" :placeholder="$strings.LabelSelectMediaType" :items="mediaTypeItems" />
</div> </div>
<ui-btn small class="w-28" color="success" @click="selectFolder">{{ $strings.ButtonNewFolder }}</ui-btn> <ui-btn small class="w-28" color="success" @click="selectFolder">{{ $strings.ButtonNewFolder }}</ui-btn>
</div> </div>
+3 -3
View File
@@ -369,7 +369,7 @@ export default {
confirmMessage += ' The file on the server will be unaffected.' confirmMessage += ' The file on the server will be unaffected.'
} }
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: confirmMessage message: confirmMessage
}) })
if (value) { if (value) {
@@ -395,7 +395,7 @@ export default {
confirmMessage += ' The file on the server will be unaffected.' confirmMessage += ' The file on the server will be unaffected.'
} }
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: confirmMessage message: confirmMessage
}) })
if (value) { if (value) {
@@ -412,7 +412,7 @@ export default {
confirmMessage += ' The files on the server and your progress will be unaffected.' confirmMessage += ' The files on the server and your progress will be unaffected.'
} }
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: confirmMessage message: confirmMessage
}) })
if (value) { if (value) {
+1 -1
View File
@@ -21,7 +21,7 @@
<div class="flex-grow"></div> <div class="flex-grow"></div>
<div class="text-xs text-gray-400">{{ log.tag }}</div> <div class="text-xs text-gray-400">{{ log.tag }}</div>
</div> </div>
<div class="text-xs">{{ maskServerAddress ? log.maskedMessage : log.message }}</div> <div class="text-xs break-words">{{ maskServerAddress ? log.maskedMessage : log.message }}</div>
</div> </div>
</div> </div>
+16 -4
View File
@@ -14,16 +14,25 @@
<template v-for="item in bookResults"> <template v-for="item in bookResults">
<div :key="item.libraryItem.id" class="w-full h-16 py-1"> <div :key="item.libraryItem.id" class="w-full h-16 py-1">
<nuxt-link :to="`/item/${item.libraryItem.id}`"> <nuxt-link :to="`/item/${item.libraryItem.id}`">
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" /> <cards-item-search-card :library-item="item.libraryItem" :search="lastSearch" />
</nuxt-link> </nuxt-link>
</div> </div>
</template> </template>
<p v-if="podcastResults.length" class="uppercase text-xs text-fg-muted my-1 px-1 font-semibold">{{ $strings.LabelPodcasts }}</p> <p v-if="podcastResults.length" class="font-semibold text-sm mb-1 mt-2">{{ $strings.LabelPodcasts }}</p>
<template v-for="item in podcastResults"> <template v-for="item in podcastResults">
<div :key="item.libraryItem.id" class="text-fg select-none relative py-1"> <div :key="item.libraryItem.id" class="text-fg select-none relative py-1">
<nuxt-link :to="`/item/${item.libraryItem.id}`"> <nuxt-link :to="`/item/${item.libraryItem.id}`">
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" /> <cards-item-search-card :library-item="item.libraryItem" :search="lastSearch" />
</nuxt-link>
</div>
</template>
<p v-if="episodeResults.length" class="font-semibold text-sm mb-1 mt-2">{{ $strings.HeaderEpisodes }}</p>
<template v-for="item in episodeResults">
<div :key="item.libraryItem.recentEpisode.id" class="text-fg select-none relative py-1">
<nuxt-link :to="`/item/${item.libraryItem.id}/${item.libraryItem.recentEpisode.id}`">
<cards-episode-search-card :episode="item.libraryItem.recentEpisode" :library-item="item.libraryItem" />
</nuxt-link> </nuxt-link>
</div> </div>
</template> </template>
@@ -77,6 +86,7 @@ export default {
isFetching: false, isFetching: false,
bookResults: [], bookResults: [],
podcastResults: [], podcastResults: [],
episodeResults: [],
seriesResults: [], seriesResults: [],
authorResults: [], authorResults: [],
narratorResults: [], narratorResults: [],
@@ -91,7 +101,7 @@ export default {
return this.$store.getters['libraries/getBookCoverAspectRatio'] return this.$store.getters['libraries/getBookCoverAspectRatio']
}, },
totalResults() { totalResults() {
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.podcastResults.length + this.narratorResults.length + this.tagResults.length return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.podcastResults.length + this.narratorResults.length + this.tagResults.length + this.episodeResults.length
} }
}, },
methods: { methods: {
@@ -104,6 +114,7 @@ export default {
if (!this.lastSearch) { if (!this.lastSearch) {
this.bookResults = [] this.bookResults = []
this.podcastResults = [] this.podcastResults = []
this.episodeResults = []
this.seriesResults = [] this.seriesResults = []
this.authorResults = [] this.authorResults = []
this.narratorResults = [] this.narratorResults = []
@@ -125,6 +136,7 @@ export default {
this.bookResults = results?.book || [] this.bookResults = results?.book || []
this.podcastResults = results?.podcast || [] this.podcastResults = results?.podcast || []
this.episodeResults = results?.episodes || []
this.seriesResults = results?.series || [] this.seriesResults = results?.series || []
this.authorResults = results?.authors || [] this.authorResults = results?.authors || []
this.narratorResults = results?.narrators || [] this.narratorResults = results?.narrators || []
+61 -42
View File
@@ -36,24 +36,24 @@
<!-- 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 class="py-3 flex items-center">
<p class="pr-4 w-36">{{ $strings.LabelJumpBackwardsTime }}</p>
<div @click.stop="showJumpBackwardsOptions">
<ui-text-input :value="jumpBackwardsOption" readonly append-icon="expand_more" style="width: 145px; max-width: 145px" />
</div>
</div>
<div class="py-3 flex items-center">
<p class="pr-4 w-36">{{ $strings.LabelJumpForwardsTime }}</p>
<div @click.stop="showJumpForwardOptions">
<ui-text-input :value="jumpForwardOption" readonly append-icon="expand_more" style="width: 145px; max-width: 145px" />
</div>
</div>
<div class="flex items-center py-3"> <div class="flex items-center py-3">
<div class="w-10 flex justify-center" @click="toggleDisableAutoRewind"> <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">
<div class="w-10 flex justify-center" @click="toggleJumpBackwards">
<span class="material-symbols text-4xl">{{ currentJumpBackwardsTimeIcon }}</span>
</div>
<p class="pl-4">{{ $strings.LabelJumpBackwardsTime }}</p>
</div>
<div class="flex items-center py-3">
<div class="w-10 flex justify-center" @click="toggleJumpForward">
<span class="material-symbols text-4xl">{{ currentJumpForwardTimeIcon }}</span>
</div>
<p class="pl-4">{{ $strings.LabelJumpForwardsTime }}</p>
</div>
<div v-if="!isiOS" class="flex items-center py-3"> <div v-if="!isiOS" class="flex items-center py-3">
<div class="w-10 flex justify-center" @click="toggleEnableMp3IndexSeeking"> <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" />
@@ -179,7 +179,7 @@
<ui-loading-indicator /> <ui-loading-indicator />
</div> </div>
<modals-dialog v-model="showMoreMenuDialog" :items="moreMenuItems" @action="clickMenuAction" /> <modals-dialog v-model="showMoreMenuDialog" :items="moreMenuItems" :selected="moreMenuSelected" @action="clickMenuAction" />
<modals-sleep-timer-length-modal v-model="showSleepTimerLengthModal" @change="sleepTimerLengthModalSelection" /> <modals-sleep-timer-length-modal v-model="showSleepTimerLengthModal" @change="sleepTimerLengthModalSelection" />
<modals-auto-sleep-timer-rewind-length-modal v-model="showAutoSleepTimerRewindLengthModal" @change="showAutoSleepTimerRewindLengthModalSelection" /> <modals-auto-sleep-timer-rewind-length-modal v-model="showAutoSleepTimerRewindLengthModal" @change="showAutoSleepTimerRewindLengthModalSelection" />
</div> </div>
@@ -187,8 +187,10 @@
<script> <script>
import { Dialog } from '@capacitor/dialog' import { Dialog } from '@capacitor/dialog'
import jumpLabelMixin from '@/mixins/jumpLabel'
export default { export default {
mixins: [jumpLabelMixin],
data() { data() {
return { return {
loading: false, loading: false,
@@ -352,15 +354,21 @@ export default {
isiOS() { isiOS() {
return this.$platform === 'ios' return this.$platform === 'ios'
}, },
jumpForwardItems() { jumpForwardSecondsOptions() {
return this.$store.state.globals.jumpForwardItems || [] return this.$store.state.globals.jumpForwardSecondsOptions || []
}, },
jumpBackwardsItems() { jumpBackwardsSecondsOptions() {
return this.$store.state.globals.jumpBackwardsItems || [] return this.$store.state.globals.jumpBackwardsSecondsOptions || []
}, },
languageOptionItems() { languageOptionItems() {
return this.$languageCodeOptions || [] return this.$languageCodeOptions || []
}, },
jumpForwardOption() {
return this.getJumpLabel(this.settings.jumpForwardTime)
},
jumpBackwardsOption() {
return this.getJumpLabel(this.settings.jumpBackwardsTime)
},
themeOptionItems() { themeOptionItems() {
return [ return [
{ {
@@ -377,20 +385,6 @@ export default {
} }
] ]
}, },
currentJumpForwardTimeIcon() {
return this.jumpForwardItems[this.currentJumpForwardTimeIndex].icon
},
currentJumpForwardTimeIndex() {
var index = this.jumpForwardItems.findIndex((jfi) => jfi.value === this.settings.jumpForwardTime)
return index >= 0 ? index : 1
},
currentJumpBackwardsTimeIcon() {
return this.jumpBackwardsItems[this.currentJumpBackwardsTimeIndex].icon
},
currentJumpBackwardsTimeIndex() {
var index = this.jumpBackwardsItems.findIndex((jfi) => jfi.value === this.settings.jumpBackwardsTime)
return index >= 0 ? index : 1
},
shakeSensitivityOption() { shakeSensitivityOption() {
const item = this.shakeSensitivityItems.find((i) => i.value === this.settings.shakeSensitivity) const item = this.shakeSensitivityItems.find((i) => i.value === this.settings.shakeSensitivity)
return item?.text || 'Error' return item?.text || 'Error'
@@ -434,7 +428,29 @@ export default {
else if (this.moreMenuSetting === 'downloadUsingCellular') return this.downloadUsingCellularItems else if (this.moreMenuSetting === 'downloadUsingCellular') return this.downloadUsingCellularItems
else if (this.moreMenuSetting === 'streamingUsingCellular') return this.streamingUsingCellularItems else if (this.moreMenuSetting === 'streamingUsingCellular') return this.streamingUsingCellularItems
else if (this.moreMenuSetting === 'androidAutoBrowseSeriesSequenceOrder') return this.androidAutoBrowseSeriesSequenceOrderItems else if (this.moreMenuSetting === 'androidAutoBrowseSeriesSequenceOrder') return this.androidAutoBrowseSeriesSequenceOrderItems
else if (this.moreMenuSetting === 'jumpForward')
return this.jumpForwardSecondsOptions.map((value) => ({
text: this.getJumpLabel(value),
value: value
}))
else if (this.moreMenuSetting === 'jumpBackwards')
return this.jumpBackwardsSecondsOptions.map((value) => ({
text: this.getJumpLabel(value),
value: value
}))
return [] return []
},
moreMenuSelected() {
if (this.moreMenuSetting === 'jumpForward') return this.settings.jumpForwardTime
if (this.moreMenuSetting === 'jumpBackwards') return this.settings.jumpBackwardsTime
if (this.moreMenuSetting === 'language') return this.settings.languageCode
if (this.moreMenuSetting === 'theme') return this.theme
if (this.moreMenuSetting === 'downloadUsingCellular') return this.settings.downloadUsingCellular
if (this.moreMenuSetting === 'streamingUsingCellular') return this.settings.streamingUsingCellular
if (this.moreMenuSetting === 'androidAutoBrowseSeriesSequenceOrder') return this.settings.androidAutoBrowseSeriesSequenceOrder
if (this.moreMenuSetting === 'shakeSensitivity') return this.settings.shakeSensitivity
if (this.moreMenuSetting === 'hapticFeedback') return this.settings.hapticFeedback
return null
} }
}, },
methods: { methods: {
@@ -468,6 +484,14 @@ export default {
this.moreMenuSetting = 'theme' this.moreMenuSetting = 'theme'
this.showMoreMenuDialog = true this.showMoreMenuDialog = true
}, },
showJumpForwardOptions() {
this.moreMenuSetting = 'jumpForward'
this.showMoreMenuDialog = true
},
showJumpBackwardsOptions() {
this.moreMenuSetting = 'jumpBackwards'
this.showMoreMenuDialog = true
},
showDownloadUsingCellularOptions() { showDownloadUsingCellularOptions() {
this.moreMenuSetting = 'downloadUsingCellular' this.moreMenuSetting = 'downloadUsingCellular'
this.showMoreMenuDialog = true this.showMoreMenuDialog = true
@@ -503,6 +527,12 @@ export default {
} else if (this.moreMenuSetting === 'androidAutoBrowseSeriesSequenceOrder') { } else if (this.moreMenuSetting === 'androidAutoBrowseSeriesSequenceOrder') {
this.settings.androidAutoBrowseSeriesSequenceOrder = action this.settings.androidAutoBrowseSeriesSequenceOrder = action
this.saveSettings() this.saveSettings()
} else if (this.moreMenuSetting === 'jumpForward') {
this.settings.jumpForwardTime = action
this.saveSettings()
} else if (this.moreMenuSetting === 'jumpBackwards') {
this.settings.jumpBackwardsTime = action
this.saveSettings()
} }
}, },
saveTheme(theme) { saveTheme(theme) {
@@ -598,17 +628,6 @@ export default {
this.$setOrientationLock(this.settings.lockOrientation) this.$setOrientationLock(this.settings.lockOrientation)
this.saveSettings() this.saveSettings()
}, },
toggleJumpForward() {
var next = (this.currentJumpForwardTimeIndex + 1) % 3
this.settings.jumpForwardTime = this.jumpForwardItems[next].value
this.saveSettings()
},
toggleJumpBackwards() {
var next = (this.currentJumpBackwardsTimeIndex + 4) % 3
if (next > 2) return
this.settings.jumpBackwardsTime = this.jumpBackwardsItems[next].value
this.saveSettings()
},
async saveSettings() { async saveSettings() {
await this.$hapticsImpact() await this.$hapticsImpact()
const updatedDeviceData = await this.$db.updateDeviceSettings({ ...this.settings }) const updatedDeviceData = await this.$db.updateDeviceSettings({ ...this.settings })
+2
View File
@@ -6,6 +6,7 @@ let $localStore = null
const languageCodeMap = { const languageCodeMap = {
bn: { label: 'বাংলা', dateFnsLocale: 'bn' }, bn: { label: 'বাংলা', dateFnsLocale: 'bn' },
bg: { label: 'Български', dateFnsLocale: 'bg' },
ca: { label: 'Català', dateFnsLocale: 'ca' }, ca: { label: 'Català', dateFnsLocale: 'ca' },
cs: { label: 'Čeština', dateFnsLocale: 'cs' }, cs: { label: 'Čeština', dateFnsLocale: 'cs' },
da: { label: 'Dansk', dateFnsLocale: 'da' }, da: { label: 'Dansk', dateFnsLocale: 'da' },
@@ -25,6 +26,7 @@ const languageCodeMap = {
ru: { label: 'Русский', dateFnsLocale: 'ru' }, ru: { label: 'Русский', dateFnsLocale: 'ru' },
sl: { label: 'Slovenščina', dateFnsLocale: 'sl' }, sl: { label: 'Slovenščina', dateFnsLocale: 'sl' },
sv: { label: 'Svenska', dateFnsLocale: 'sv' }, sv: { label: 'Svenska', dateFnsLocale: 'sv' },
tr: { label: 'Türkçe', dateFnsLocale: 'tr' },
uk: { label: 'Українська', dateFnsLocale: 'uk' }, uk: { label: 'Українська', dateFnsLocale: 'uk' },
'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' }, 'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' },
'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' } 'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }
+2 -2
View File
@@ -314,8 +314,8 @@ export default ({ store, app }, inject) => {
} }
if (!canGoBack) { if (!canGoBack) {
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: eventBus.$strings.HeaderConfirm,
message: `Did you want to exit the app?` message: eventBus.$strings.MessageConfirmAppExit
}) })
if (value) { if (value) {
App.exitApp() App.exitApp()
+1 -1
View File
@@ -110,7 +110,7 @@ class ServerSocket extends EventEmitter {
} }
onAuthFailed(data) { onAuthFailed(data) {
console.log('[SOCKET] Auth failed', data) console.log('[SOCKET] Auth failed: ' + (data?.message || 'Unknown reason'))
this.isAuthenticated = false this.isAuthenticated = false
} }
+12
View File
@@ -144,6 +144,12 @@ npx cap open android
Start coding! Start coding!
After making changes to the JS layer you need to rebuild the nuxt pages and sync them to the native shells:
```shell
npm run sync
```
### Mac Environment Setup for iOS ### Mac Environment Setup for iOS
Required Software: Required Software:
@@ -213,3 +219,9 @@ npx cap open ios
<br> <br>
Start coding! Start coding!
After making changes to the JS layer you need to rebuild the nuxt pages and sync them to the native shells:
```shell
npm run sync
```
+3 -37
View File
@@ -5,34 +5,8 @@ export const state = () => ({
series: null, series: null,
localMediaProgress: [], localMediaProgress: [],
lastSearch: null, lastSearch: null,
jumpForwardItems: [ jumpForwardSecondsOptions: [5, 10, 15, 30, 60, 120, 300],
{ jumpBackwardsSecondsOptions: [5, 10, 15, 30, 60, 120, 300],
icon: 'forward_5',
value: 5
},
{
icon: 'forward_10',
value: 10
},
{
icon: 'forward_30',
value: 30
}
],
jumpBackwardsItems: [
{
icon: 'replay_5',
value: 5
},
{
icon: 'replay_10',
value: 10
},
{
icon: 'replay_30',
value: 30
}
],
libraryIcons: ['database', 'audiobookshelf', 'books-1', 'books-2', 'book-1', 'microphone-1', 'microphone-3', 'radio', 'podcast', 'rss', 'headphones', 'music', 'file-picture', 'rocket', 'power', 'star', 'heart'], libraryIcons: ['database', 'audiobookshelf', 'books-1', 'books-2', 'book-1', 'microphone-1', 'microphone-3', 'radio', 'podcast', 'rss', 'headphones', 'music', 'file-picture', 'rocket', 'power', 'star', 'heart'],
selectedPlaylistItems: [], selectedPlaylistItems: [],
showPlaylistsAddCreateModal: false, showPlaylistsAddCreateModal: false,
@@ -111,15 +85,7 @@ export const getters = {
if (episodeId != null && lmp.episodeId != episodeId) return false if (episodeId != null && lmp.episodeId != episodeId) return false
return lmp.libraryItemId == libraryItemId return lmp.libraryItemId == libraryItemId
}) })
}, }
getJumpForwardIcon: (state) => (jumpForwardTime) => {
const item = state.jumpForwardItems.find((i) => i.value == jumpForwardTime)
return item ? item.icon : 'forward_10'
},
getJumpBackwardsIcon: (state) => (jumpBackwardsTime) => {
const item = state.jumpBackwardsItems.find((i) => i.value == jumpBackwardsTime)
return item ? item.icon : 'replay_10'
}
} }
export const actions = { export const actions = {
+10 -1
View File
@@ -7,7 +7,7 @@
"ButtonCancelTimer": "إلغاء المؤقت", "ButtonCancelTimer": "إلغاء المؤقت",
"ButtonClearFilter": "تصفية الفرز", "ButtonClearFilter": "تصفية الفرز",
"ButtonClearLogs": "مسح السجل", "ButtonClearLogs": "مسح السجل",
"ButtonCloseFeed": "إغلاق", "ButtonCloseFeed": "إغلاق الموجز",
"ButtonCollections": "المجموعات", "ButtonCollections": "المجموعات",
"ButtonConnect": "اتصل", "ButtonConnect": "اتصل",
"ButtonConnectToServer": "اتصال بالخادم", "ButtonConnectToServer": "اتصال بالخادم",
@@ -63,6 +63,7 @@
"HeaderChapters": "الفصول", "HeaderChapters": "الفصول",
"HeaderCollection": "مجموعة", "HeaderCollection": "مجموعة",
"HeaderCollectionItems": "عناصر المجموعة", "HeaderCollectionItems": "عناصر المجموعة",
"HeaderConfirm": "تأكيد",
"HeaderConnectionStatus": "حالة الاتصال", "HeaderConnectionStatus": "حالة الاتصال",
"HeaderDataSettings": "إعدادات البيانات", "HeaderDataSettings": "إعدادات البيانات",
"HeaderDetails": "التفاصيل", "HeaderDetails": "التفاصيل",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "الجلسات الأخيرة", "HeaderStatsRecentSessions": "الجلسات الأخيرة",
"HeaderTableOfContents": "جدول المحتويات", "HeaderTableOfContents": "جدول المحتويات",
"HeaderUserInterfaceSettings": "إعدادات واجهة المستخدم", "HeaderUserInterfaceSettings": "إعدادات واجهة المستخدم",
"HeaderWelcome": "مرحبا, <strong>{0}</strong>",
"HeaderYourStats": "إحصائياتك", "HeaderYourStats": "إحصائياتك",
"LabelAddToPlaylist": "أضف إلى قائمة التشغيل", "LabelAddToPlaylist": "أضف إلى قائمة التشغيل",
"LabelAddedAt": "أضيفت على", "LabelAddedAt": "أضيفت على",
@@ -146,6 +148,7 @@
"LabelEndOfChapter": "نهاية الفصل", "LabelEndOfChapter": "نهاية الفصل",
"LabelEndTime": "وقت النهاية", "LabelEndTime": "وقت النهاية",
"LabelEpisode": "الحلقة", "LabelEpisode": "الحلقة",
"LabelExplicit": "محتوى صريح",
"LabelFeedURL": "عنوان التغذية", "LabelFeedURL": "عنوان التغذية",
"LabelFile": "الملف", "LabelFile": "الملف",
"LabelFileBirthtime": "وقت انشاء الملف", "LabelFileBirthtime": "وقت انشاء الملف",
@@ -154,6 +157,9 @@
"LabelFinished": "المنجزة", "LabelFinished": "المنجزة",
"LabelFolder": "المجلد", "LabelFolder": "المجلد",
"LabelFontBoldness": "تعريض الخط", "LabelFontBoldness": "تعريض الخط",
"LabelFontFamily": "عائلة الخطوط",
"LabelFontFamilySans": "سانس",
"LabelFontFamilySerif": "سارف",
"LabelFontScale": "نطاق الخط", "LabelFontScale": "نطاق الخط",
"LabelGenre": "التصنيف", "LabelGenre": "التصنيف",
"LabelGenres": "التصانيف", "LabelGenres": "التصانيف",
@@ -297,6 +303,7 @@
"MessageDownloading": "جاري التنزيل...", "MessageDownloading": "جاري التنزيل...",
"MessageDownloadingEpisode": "جاري تنزيل الحلقة", "MessageDownloadingEpisode": "جاري تنزيل الحلقة",
"MessageEpisodesQueuedForDownload": "تمت إضافة {0} حلقة (حلقات) إلى قائمة انتظار التنزيل", "MessageEpisodesQueuedForDownload": "تمت إضافة {0} حلقة (حلقات) إلى قائمة انتظار التنزيل",
"MessageFailedToRefreshToken": "فشل في تحديث الرمز، إعادة تسجيل الدخول مطلوب",
"MessageFeedURLWillBe": "سيكون رابط التغذية هو {0}", "MessageFeedURLWillBe": "سيكون رابط التغذية هو {0}",
"MessageFetching": "جاري الجلب...", "MessageFetching": "جاري الجلب...",
"MessageFollowTheProjectOnGithub": "تابع المشروع على Github", "MessageFollowTheProjectOnGithub": "تابع المشروع على Github",
@@ -324,6 +331,8 @@
"MessageNoSeries": "لا توجد سلاسل", "MessageNoSeries": "لا توجد سلاسل",
"MessageNoUpdatesWereNecessary": "لا حاجة لأي تحديثات", "MessageNoUpdatesWereNecessary": "لا حاجة لأي تحديثات",
"MessageNoUserPlaylists": "ليس لديك أي قوائم تشغيل", "MessageNoUserPlaylists": "ليس لديك أي قوائم تشغيل",
"MessageOldServerAuthReLoginRequired": "تم تحسين المصادقة في الخادم v2.26.0 لضمان الأمان. يُطلب من جميع المستخدمين إعادة تسجيل الدخول.",
"MessageOldServerAuthWarning": "الخادم يستخدم مصادقة قديمة",
"MessageOldServerConnectionWarning": "تكوين اتصال الخادم يستخدم معرّف مستخدم قديم. يرجى حذف اتصال الخادم هذا وإعادة إضافته.", "MessageOldServerConnectionWarning": "تكوين اتصال الخادم يستخدم معرّف مستخدم قديم. يرجى حذف اتصال الخادم هذا وإعادة إضافته.",
"MessageOldServerConnectionWarningHelp": "لقد قمتَ بإعداد الاتصال بهذا الخادم قبل ترحيل قاعدة البيانات في الإصدار 2.3.0، الصادر في يونيو 2023. سيؤدي تحديث الخادم المستقبلي إلى إلغاء إمكانية تسجيل الدخول باستخدام هذا الاتصال القديم. يُرجى حذف اتصال الخادم الحالي والاتصال مرة أخرى (باستخدام نفس عنوان الخادم وبيانات الاعتماد). إذا كانت لديك أي وسائط مُنزّلة على هذا الجهاز، فيجب تنزيلها مرة أخرى لمزامنتها مع الخادم.", "MessageOldServerConnectionWarningHelp": "لقد قمتَ بإعداد الاتصال بهذا الخادم قبل ترحيل قاعدة البيانات في الإصدار 2.3.0، الصادر في يونيو 2023. سيؤدي تحديث الخادم المستقبلي إلى إلغاء إمكانية تسجيل الدخول باستخدام هذا الاتصال القديم. يُرجى حذف اتصال الخادم الحالي والاتصال مرة أخرى (باستخدام نفس عنوان الخادم وبيانات الاعتماد). إذا كانت لديك أي وسائط مُنزّلة على هذا الجهاز، فيجب تنزيلها مرة أخرى لمزامنتها مع الخادم.",
"MessagePodcastSearchField": "أدخل مصطلح البحث أو عنوان URL الخاص بتغذية RSS", "MessagePodcastSearchField": "أدخل مصطلح البحث أو عنوان URL الخاص بتغذية RSS",
+95 -77
View File
@@ -11,11 +11,11 @@
"ButtonCollections": "Калекцыі", "ButtonCollections": "Калекцыі",
"ButtonConnect": "Падлучыцца", "ButtonConnect": "Падлучыцца",
"ButtonConnectToServer": "Падлучыцца да сервера", "ButtonConnectToServer": "Падлучыцца да сервера",
"ButtonCreate": "Ствараць", "ButtonCreate": "Стварыць",
"ButtonCreateBookmark": "Стварыць закладку", "ButtonCreateBookmark": "Стварыць закладку",
"ButtonCreateNewPlaylist": "Стварыць новы спіс прайгравання", "ButtonCreateNewPlaylist": "Стварыць новы плэй-ліст",
"ButtonDelete": "Выдаліць", "ButtonDelete": "Выдаліць",
"ButtonDeleteLocalEpisode": "Выдаліць лакальны эпізод", "ButtonDeleteLocalEpisode": "Выдаліць лакальны выпуск",
"ButtonDeleteLocalFile": "Выдаліць лакальны файл", "ButtonDeleteLocalFile": "Выдаліць лакальны файл",
"ButtonDeleteLocalItem": "Выдаліць лакальны элемент", "ButtonDeleteLocalItem": "Выдаліць лакальны элемент",
"ButtonDisableAutoTimer": "Адключыць аўтаматычны таймер", "ButtonDisableAutoTimer": "Адключыць аўтаматычны таймер",
@@ -30,15 +30,15 @@
"ButtonLogs": "Журналы", "ButtonLogs": "Журналы",
"ButtonManageLocalFiles": "Кіраванне лакальнымі файламі", "ButtonManageLocalFiles": "Кіраванне лакальнымі файламі",
"ButtonMaskServerAddress": "Схаваць адрас сервера", "ButtonMaskServerAddress": "Схаваць адрас сервера",
"ButtonNewFolder": "Новая тэчка", "ButtonNewFolder": "Новая папка",
"ButtonNextEpisode": "Наступны эпізод", "ButtonNextEpisode": "Наступны выпуск",
"ButtonOk": "Добра", "ButtonOk": "ОК",
"ButtonOpenFeed": "Адкрыць стужку", "ButtonOpenFeed": "Адкрыць стужку",
"ButtonOverride": "Пераазначыць", "ButtonOverride": "Пераазначыць",
"ButtonPause": аўза", "ButtonPause": рыпыніць",
"ButtonPlay": "Прайграць", "ButtonPlay": "Прайграць",
"ButtonPlayEpisode": "Прайграць эпізод", "ButtonPlayEpisode": "Прайграць выпуск",
"ButtonPlaylists": "Спісы прайгравання", "ButtonPlaylists": "Плэй-лісты",
"ButtonRead": "Чытаць", "ButtonRead": "Чытаць",
"ButtonReadLess": "Чытаць менш", "ButtonReadLess": "Чытаць менш",
"ButtonReadMore": "Чытаць больш", "ButtonReadMore": "Чытаць больш",
@@ -59,31 +59,32 @@
"HeaderAccount": "Уліковы запіс", "HeaderAccount": "Уліковы запіс",
"HeaderAdvanced": "Дадаткова", "HeaderAdvanced": "Дадаткова",
"HeaderAndroidAutoSettings": "Налады Android Auto", "HeaderAndroidAutoSettings": "Налады Android Auto",
"HeaderAudioTracks": "Аўдыядарожкі", "HeaderAudioTracks": "Аўдыятрэкі",
"HeaderChapters": "Раздзелы", "HeaderChapters": "Раздзелы",
"HeaderCollection": "Калекцыя", "HeaderCollection": "Калекцыя",
"HeaderCollectionItems": "Элементы калекцыі", "HeaderCollectionItems": "Элементы калекцыі",
"HeaderConfirm": "Пацвердзіць",
"HeaderConnectionStatus": "Стан злучэння", "HeaderConnectionStatus": "Стан злучэння",
"HeaderDataSettings": "Налады дадзеных", "HeaderDataSettings": "Налады даных",
"HeaderDetails": "Падрабязнасці", "HeaderDetails": "Падрабязнасці",
"HeaderDownloads": "Спампоўкі", "HeaderDownloads": "Спампоўкі",
"HeaderEbookFiles": "Файлы электронных кніг", "HeaderEbookFiles": "Файлы электронных кніг",
"HeaderEpisodes": "Эпізоды", "HeaderEpisodes": "Выпускі",
"HeaderEreaderSettings": "Налады прылады для чытання", "HeaderEreaderSettings": "Налады прылады для чытання",
"HeaderLatestEpisodes": "Апошнія эпізоды", "HeaderLatestEpisodes": "Апошнія выпускі",
"HeaderLibraries": "Бібліятэкі", "HeaderLibraries": "Бібліятэкі",
"HeaderLocalFolders": "Лакальныя тэчкі", "HeaderLocalFolders": "Лакальныя папкі",
"HeaderLocalLibraryItems": "Элементы лакальнай бібліятэкі", "HeaderLocalLibraryItems": "Элементы лакальнай бібліятэкі",
"HeaderNewPlaylist": "Новы спіс прайгравання", "HeaderNewPlaylist": "Новы плэй-ліст",
"HeaderOpenRSSFeed": "Адкрыць RSS-стужку", "HeaderOpenRSSFeed": "Адкрыць RSS-стужку",
"HeaderPlaybackSettings": "Налады прайгравання", "HeaderPlaybackSettings": "Налады прайгравання",
"HeaderPlaylist": "Спіс прайгравання", "HeaderPlaylist": "Плэй-ліст",
"HeaderPlaylistItems": "Элементы спіса прайгравання", "HeaderPlaylistItems": "Элементы плэй-ліста",
"HeaderProgressSyncFailed": "Не ўдалося сінхранізаваць прагрэс", "HeaderProgressSyncFailed": "Не ўдалося сінхранізаваць прагрэс",
"HeaderRSSFeed": "RSS-стужка", "HeaderRSSFeed": "RSS-стужка",
"HeaderRSSFeedGeneral": "Падрабязнасці RSS", "HeaderRSSFeedGeneral": "Падрабязнасці RSS",
"HeaderRSSFeedIsOpen": "RSS-стужка адкрытая", "HeaderRSSFeedIsOpen": "RSS-стужка адкрытая",
"HeaderSelectDownloadLocation": "Абраць месцазнаходжання для спампоўкі", "HeaderSelectDownloadLocation": "Выберыце размяшчэнне для спампоўкі",
"HeaderSettings": "Налады", "HeaderSettings": "Налады",
"HeaderSleepTimer": "Таймер сну", "HeaderSleepTimer": "Таймер сну",
"HeaderSleepTimerSettings": "Налады таймера сну", "HeaderSleepTimerSettings": "Налады таймера сну",
@@ -91,8 +92,9 @@
"HeaderStatsRecentSessions": "Апошнія сеансы", "HeaderStatsRecentSessions": "Апошнія сеансы",
"HeaderTableOfContents": "Змест", "HeaderTableOfContents": "Змест",
"HeaderUserInterfaceSettings": "Налады інтэрфейсу карыстальніка", "HeaderUserInterfaceSettings": "Налады інтэрфейсу карыстальніка",
"HeaderWelcome": "Вітаем, <strong>{0}</strong>",
"HeaderYourStats": "Ваша статыстыка", "HeaderYourStats": "Ваша статыстыка",
"LabelAddToPlaylist": "Дадаць у спіс прайгравання", "LabelAddToPlaylist": "Дадаць у плэй-ліст",
"LabelAddedAt": "Дата дабаўлення", "LabelAddedAt": "Дата дабаўлення",
"LabelAddedDate": "Дададзена {0}", "LabelAddedDate": "Дададзена {0}",
"LabelAll": "Усе", "LabelAll": "Усе",
@@ -103,58 +105,62 @@
"LabelAndroidAutoBrowseSeriesSequenceOrder": "Парадак кніг у серыі", "LabelAndroidAutoBrowseSeriesSequenceOrder": "Парадак кніг у серыі",
"LabelAskConfirmation": "Запытваць пацверджанне", "LabelAskConfirmation": "Запытваць пацверджанне",
"LabelAuthor": "Аўтар", "LabelAuthor": "Аўтар",
"LabelAuthorFirstLast": "Аўтар (Імя Прозвішча)", "LabelAuthorFirstLast": "Аўтар (імя, прозвішча)",
"LabelAuthorLastFirst": "Аўтар (Прозвішча, Імя)", "LabelAuthorLastFirst": "Аўтар (прозвішча, імя)",
"LabelAuthors": "Аўтары", "LabelAuthors": "Аўтары",
"LabelAutoDownloadEpisodes": "Аўтаматычнае спампаванне эпізодаў", "LabelAutoDownloadEpisodes": "Аўтаматычна спампоўваць выпускі",
"LabelAutoRewindTime": "Час аўтаматычнай перамоткі назад", "LabelAutoRewindTime": "Час аўтаматычнай перамоткі назад",
"LabelAutoSleepTimer": "Аўтаматычны таймер сну", "LabelAutoSleepTimer": "Аўтаматычны таймер сну",
"LabelAutoSleepTimerAutoRewind": "Аўтаматычная перамотка таймера сну", "LabelAutoSleepTimerAutoRewind": "Аўтаматычная перамотка таймера сну",
"LabelAutoSleepTimerAutoRewindHelp": "Калі таймер аўтаматычнага сну скончыцца, паўторнае прайграванне элемента аўтаматычна перамотвае вашу пазіцыю.", "LabelAutoSleepTimerAutoRewindHelp": "Пасля заканчэння аўтаматычнага таймера сну паўторнае прайграванне элемента аўтаматычна перамотвае вашу пазіцыю.",
"LabelAutoSleepTimerHelp": "Калі медыя прайграецца ў вызначаны час паміж пачаткам і канцом, таймер сну будзе запушчаны аўтаматычна.", "LabelAutoSleepTimerHelp": "Калі медыя прайграецца ў вызначаны час паміж пачаткам і канцом, таймер сну будзе запушчаны аўтаматычна.",
"LabelBooks": "Кнігі", "LabelBooks": "Кнігі",
"LabelChapterTrack": "Дарожка раздзела", "LabelByAuthor": " {0}",
"LabelChapterTrack": "Трэк раздзела",
"LabelChapters": "Раздзелы", "LabelChapters": "Раздзелы",
"LabelClosePlayer": "Зачыніць прайгравальнік", "LabelClosePlayer": "Зачыніць прайгравальнік",
"LabelCollapseSeries": "Згарнуць серыі", "LabelCollapseSeries": "Згарнуць серыі",
"LabelComplete": "Завершана", "LabelComplete": "Завяршыць",
"LabelContinueBooks": "Працягнуць кнігі", "LabelContinueBooks": "Працягнуць кнігі",
"LabelContinueEpisodes": "Працягнуць эпізоды", "LabelContinueEpisodes": "Працягнуць выпускі",
"LabelContinueListening": "Працягваць слухаць", "LabelContinueListening": "Працяг праслухоўвання",
"LabelContinueReading": "Працягнуць чытанне", "LabelContinueReading": "Працягнуць чытанне",
"LabelContinueSeries": "Працягнуць серыі", "LabelContinueSeries": "Працягнуць серыі",
"LabelCustomTime": "Свой час", "LabelCustomTime": "Свой час",
"LabelDescription": "Апісанне", "LabelDescription": "Апісанне",
"LabelDisableAudioFadeOut": "Адключыць змяншэнне гучнасці", "LabelDisableAudioFadeOut": "Адключыць змяншэнне гучнасці",
"LabelDisableAudioFadeOutHelp": "Гучнасць аўдыё пачне змяншацца, калі на таймеры сну застанецца менш за 1 хвіліну. Уключыце гэтую наладу, каб пазбегнуць змяншэння гучнасці.", "LabelDisableAudioFadeOutHelp": "Гучнасць гуку пачне змяншацца, калі на таймеры сну застанецца менш за 1 хвіліну. Уключыце гэтую наладу, каб пазбегнуць змяншэння гучнасці.",
"LabelDisableAutoRewind": "Адключыць аўтаматычную перамотку назад", "LabelDisableAutoRewind": "Адключыць аўтаматычную перамотку назад",
"LabelDisableShakeToReset": "Адключыць скід страсеннем", "LabelDisableShakeToReset": "Адключыць скід страсеннем",
"LabelDisableShakeToResetHelp": "Калі патрасці прыладу падчас працы таймера АБО на працягу 2 хвілін пасля заканчэння таймера, таймер сну скідваецца. Уключыце гэтую наладу, каб адключыць скід страсеннем.", "LabelDisableShakeToResetHelp": "Калі патрасці прыладу падчас працы таймера АБО на працягу 2 хвілін пасля заканчэння таймера, таймер сну скідваецца. Уключыце гэтую наладу, каб адключыць скід страсеннем.",
"LabelDisableVibrateOnReset": "Адключыць вібрацыю пры скідзе", "LabelDisableVibrateOnReset": "Адключыць вібрацыю пры скідзе",
"LabelDisableVibrateOnResetHelp": "Калі таймер сну скідваецца, ваша прылада будзе вібраваць. Уключыце гэтую наладу, каб адключыць вібрацыю пры скідзе таймера сну.", "LabelDisableVibrateOnResetHelp": "Калі таймер сну скідваецца, ваша прылада будзе вібраваць. Уключыце гэтую наладу, каб адключыць вібрацыю пры скідзе таймера сну.",
"LabelDiscover": "Знайсці", "LabelDiscover": "Знаходкі",
"LabelDownload": "Спампаваць", "LabelDownload": "Спампаваць",
"LabelDownloadUsingCellular": "Спампоўка праз мабільны інтэрнэт", "LabelDownloadUsingCellular": "Спампоўваць праз мабільны інтэрнэт",
"LabelDownloaded": "Спампавана", "LabelDownloaded": "Спампавана",
"LabelDuration": "Працягласць", "LabelDuration": "Працягласць",
"LabelEbook": "Электронная кніга", "LabelEbook": "Электронная кніга",
"LabelEbooks": "Электронныя кнігі", "LabelEbooks": "Электронныя кнігі",
"LabelEnable": "Уключыць", "LabelEnable": "Уключыць",
"LabelEnableMp3IndexSeeking": "Уключыць індэксаванне для перамоткі MP3", "LabelEnableMp3IndexSeeking": "Уключыць пошук па індэксе mp3",
"LabelEnableMp3IndexSeekingHelp": "Гэтую наладу варта ўключаць толькі, калі ў вас ёсць MP3-файлы, якія няправільна перамотваюцца. Хутчэй за ўсё, гэта выклікана файламі з зменным бітрэйтам (VBR). Гэтая налада прымусова ўключае індэксаванне для перамоткі, пры якім падчас чытання файла ствараецца адпаведнасць паміж часам і байтамі. У некаторых выпадках з вялікімі MP3-файламі можа быць затрымка пры перемотцы да канца файла.", "LabelEnableMp3IndexSeekingHelp": "Гэтую наладу варта ўключаць толькі, калі ў вас ёсць MP3-файлы, якія няправільна перамотваюцца. Хутчэй за ўсё, гэта выклікана файламі з зменным бітрэйтам (VBR). Гэтая налада прымусова ўключае індэксаванне для перамоткі, пры якім падчас чытання файла ствараецца адпаведнасць паміж часам і байтамі. У некаторых выпадках з вялікімі MP3-файламі можа быць затрымка пры перемотцы да канца файла.",
"LabelEnd": "Канец", "LabelEnd": "Канец",
"LabelEndOfChapter": "Канец раздзела", "LabelEndOfChapter": "Канец раздзела",
"LabelEndTime": "Час заканчэння", "LabelEndTime": "Час заканчэння",
"LabelEpisode": "Эпізод", "LabelEpisode": "Выпуск",
"LabelExplicit": "Відверты", "LabelExplicit": "Непрыстойнае",
"LabelFeedURL": "URL стужкі", "LabelFeedURL": "URL стужкі",
"LabelFile": "Файл", "LabelFile": "Файл",
"LabelFileBirthtime": "Час стварэння файла", "LabelFileBirthtime": "Час стварэння файла",
"LabelFileModified": "Час змянення файла", "LabelFileModified": "Час змянення файла",
"LabelFilename": "Імя файла", "LabelFilename": "Назва файла",
"LabelFinished": "Скончана", "LabelFinished": "Завершана",
"LabelFolder": "Тэчка", "LabelFolder": "Папка",
"LabelFontBoldness": "Таўшчыня шрыфта", "LabelFontBoldness": "Таўшчыня шрыфта",
"LabelFontFamily": "Сямейства шрыфтоў",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Памер шрыфту", "LabelFontScale": "Памер шрыфту",
"LabelGenre": "Жанр", "LabelGenre": "Жанр",
"LabelGenres": "Жанры", "LabelGenres": "Жанры",
@@ -166,7 +172,7 @@
"LabelHost": "Хост", "LabelHost": "Хост",
"LabelInProgress": "У працэсе", "LabelInProgress": "У працэсе",
"LabelIncomplete": "Незавершана", "LabelIncomplete": "Незавершана",
"LabelInternalAppStorage": "Унутраная памяць прыкладання", "LabelInternalAppStorage": "Унутраная памяць праграмы",
"LabelJumpBackwardsTime": "Час пераходу назад", "LabelJumpBackwardsTime": "Час пераходу назад",
"LabelJumpForwardsTime": "Час пераходу наперад", "LabelJumpForwardsTime": "Час пераходу наперад",
"LabelKeepScreenAwake": "Трымаць экран уключаным", "LabelKeepScreenAwake": "Трымаць экран уключаным",
@@ -174,6 +180,9 @@
"LabelLayout": "Знешні выгляд", "LabelLayout": "Знешні выгляд",
"LabelLayoutAuto": "Аўто", "LabelLayoutAuto": "Аўто",
"LabelLayoutSinglePage": "Аднабаковы", "LabelLayoutSinglePage": "Аднабаковы",
"LabelLibrarySortByProgress": "Прагрэс: апошняе абнаўленне",
"LabelLibrarySortByProgressFinished": "Прагрэс: завершана",
"LabelLibrarySortByProgressStarted": "Прагрэс: пачата",
"LabelLight": "Святло", "LabelLight": "Святло",
"LabelLineSpacing": "Міжрадковы інтэрвал", "LabelLineSpacing": "Міжрадковы інтэрвал",
"LabelListenAgain": "Паслухаць зноў", "LabelListenAgain": "Паслухаць зноў",
@@ -187,9 +196,9 @@
"LabelMissing": "Адсутнічае", "LabelMissing": "Адсутнічае",
"LabelMore": "Больш", "LabelMore": "Больш",
"LabelMoreInfo": "Больш інфармацыі", "LabelMoreInfo": "Больш інфармацыі",
"LabelName": "Імя", "LabelName": "Назва",
"LabelNarrator": "Чытальнік", "LabelNarrator": "Дыктар",
"LabelNarrators": "Чытальнікі", "LabelNarrators": "Дыктары",
"LabelNavigateWithVolume": "Навігацыя з дапамогай клавіш гучнасці", "LabelNavigateWithVolume": "Навігацыя з дапамогай клавіш гучнасці",
"LabelNavigateWithVolumeMirrored": "Люстраваны", "LabelNavigateWithVolumeMirrored": "Люстраваны",
"LabelNavigateWithVolumeWhilePlaying": "Дазволіць клавішам гучнасці кіраваць навігацыяй падчас прайгравання", "LabelNavigateWithVolumeWhilePlaying": "Дазволіць клавішам гучнасці кіраваць навігацыяй падчас прайгравання",
@@ -197,13 +206,13 @@
"LabelNavigateWithVolumeWhilePlayingEnabled": "Уключана", "LabelNavigateWithVolumeWhilePlayingEnabled": "Уключана",
"LabelNever": "Ніколі", "LabelNever": "Ніколі",
"LabelNewestAuthors": "Новыя аўтары", "LabelNewestAuthors": "Новыя аўтары",
"LabelNewestEpisodes": "Новыя эпізоды", "LabelNewestEpisodes": "Найноўшыя выпускі",
"LabelNo": "Не", "LabelNo": "Не",
"LabelNotFinished": "Не скончана", "LabelNotFinished": "Незавершана",
"LabelNotStarted": "Не пачата", "LabelNotStarted": "Не пачата",
"LabelNumEpisodes": "{0} эпізодаў", "LabelNumEpisodes": "{0} выпускаў",
"LabelNumEpisodesIncomplete": "{0} эпізодаў, {1} незавершаных", "LabelNumEpisodesIncomplete": "{0} выпускаў, {1} незавершаных",
"LabelNumberOfEpisodes": "# з эпізодаў", "LabelNumberOfEpisodes": "Колькасць выпускаў",
"LabelOff": "Выключына", "LabelOff": "Выключына",
"LabelOn": "Уключана", "LabelOn": "Уключана",
"LabelPassword": "Пароль", "LabelPassword": "Пароль",
@@ -229,10 +238,11 @@
"LabelReadAgain": "Чытаць зноў", "LabelReadAgain": "Чытаць зноў",
"LabelRecentSeries": "Апошнія серыі", "LabelRecentSeries": "Апошнія серыі",
"LabelRecentlyAdded": "Нядаўна дададзеныя", "LabelRecentlyAdded": "Нядаўна дададзеныя",
"LabelRemoveFromPlaylist": "Выдаліць з спіса прайгравання", "LabelRemoveFromPlaylist": "Выдаліць з плэй-ліста",
"LabelScaleElapsedTimeBySpeed": "Прыстасаваць мінулы час да хуткасці", "LabelScaleElapsedTimeBySpeed": "Прыстасаваць мінулы час да хуткасці",
"LabelSeason": "Сезон", "LabelSeason": "Сезон",
"LabelSelectADevice": "Выбраць прыладу", "LabelSelectADevice": "Выбраць прыладу",
"LabelSelectMediaType": "Выберыце тып медыя",
"LabelSequenceAscending": "Паслядоўнасць па ўзрастанні", "LabelSequenceAscending": "Паслядоўнасць па ўзрастанні",
"LabelSequenceDescending": "Паслядоўнасць па змяншэнні", "LabelSequenceDescending": "Паслядоўнасць па змяншэнні",
"LabelSeries": "Серыі", "LabelSeries": "Серыі",
@@ -243,16 +253,16 @@
"LabelShowAll": "Паказаць усё", "LabelShowAll": "Паказаць усё",
"LabelSize": "Памер", "LabelSize": "Памер",
"LabelSleepTimer": "Таймер сну", "LabelSleepTimer": "Таймер сну",
"LabelSleepTimerAlmostDoneChime": "Праіграйце звонак, калі амаль скончана", "LabelSleepTimerAlmostDoneChime": "Гукавы сігнал, калі амаль звершаны",
"LabelSleepTimerAlmostDoneChimeHelp": "Памякчыць звонак, калі на таймеры сну застанецца 30 секунд", "LabelSleepTimerAlmostDoneChimeHelp": "Памякчыць звонак, калі на таймеры сну застанецца 30 секунд",
"LabelStart": "Пачаць", "LabelStart": "Пачатак",
"LabelStartTime": "Час пачатку", "LabelStartTime": "Час пачатку",
"LabelStatsBestDay": "Лепшы дзень", "LabelStatsBestDay": "Найлепшы дзень",
"LabelStatsDailyAverage": "У сярэднім за дзень", "LabelStatsDailyAverage": "У сярэднім за дзень",
"LabelStatsDays": "Дзён", "LabelStatsDays": "Дзён",
"LabelStatsDaysListened": "Дзён праслухана", "LabelStatsDaysListened": "Дзён праслухана",
"LabelStatsInARow": "без перапынку", "LabelStatsInARow": "без перапынку",
"LabelStatsItemsFinished": "Скончаныя элементы", "LabelStatsItemsFinished": "Элементаў завершана",
"LabelStatsMinutes": "хвілін", "LabelStatsMinutes": "хвілін",
"LabelStatsMinutesListening": "Хвілін праслухоўвання", "LabelStatsMinutesListening": "Хвілін праслухоўвання",
"LabelStatsWeekListening": "Праслухана за тыдзень", "LabelStatsWeekListening": "Праслухана за тыдзень",
@@ -264,14 +274,14 @@
"LabelThemeDark": "Цёмная", "LabelThemeDark": "Цёмная",
"LabelThemeLight": "Светлая", "LabelThemeLight": "Светлая",
"LabelTimeRemaining": "Засталося {0}", "LabelTimeRemaining": "Засталося {0}",
"LabelTitle": "Назва", "LabelTitle": "Загаловак",
"LabelTotalSize": "Агульны памер", "LabelTotalSize": "Агульны памер",
"LabelTotalTrack": "Агульная колькасць дарожак", "LabelTotalTrack": "Усяго трэкаў",
"LabelTracks": "Дарожкі", "LabelTracks": "Трэкі",
"LabelType": "Тып", "LabelType": "Тып",
"LabelUnknown": "Невядома", "LabelUnknown": "Невядома",
"LabelUnlockPlayer": "Разблакаваць прайгравальнік", "LabelUnlockPlayer": "Разблакаваць прайгравальнік",
"LabelUseBookshelfView": "Выкарыстоўваць выгляд кніжнай паліцы", "LabelUseBookshelfView": "Паказваць кніжныя паліцы",
"LabelUser": "Карыстальнік", "LabelUser": "Карыстальнік",
"LabelUsername": "Імя карыстальніка", "LabelUsername": "Імя карыстальніка",
"LabelVeryHigh": "Вельмі высокі", "LabelVeryHigh": "Вельмі высокі",
@@ -280,24 +290,30 @@
"LabelYearReviewShow": "Азнаёміцца з вынікамі года", "LabelYearReviewShow": "Азнаёміцца з вынікамі года",
"LabelYourBookmarks": "Вашы закладкі", "LabelYourBookmarks": "Вашы закладкі",
"LabelYourProgress": "Ваш прагрэс", "LabelYourProgress": "Ваш прагрэс",
"MessageAndroid10Downloads": "На Android 10 і ніжэй для спампоўкі будзе выкарыстоўвацца ўнутраная памяць прыкладання.", "MessageAndroid10Downloads": "На Android 10 і ніжэй для спамповак будзе выкарыстоўвацца ўнутраная памяць праграмы.",
"MessageAttemptingServerConnection": "Спроба злучэння з серверам...", "MessageAttemptingServerConnection": "Спроба злучэння з серверам...",
"MessageAudiobookshelfServerNotConnected": "Сервер Audiobookshelf не падлучаны", "MessageAudiobookshelfServerNotConnected": "Сервер Audiobookshelf не падлучаны",
"MessageAudiobookshelfServerRequired": "<strong>Важна!</strong> Гэта праграма прызначана для працы з серверам Audiobookshelf, які вы ці хтосьці іншы абслугоўваеце. Праграма не змяшчае ўласнага кантэнту.", "MessageAudiobookshelfServerRequired": "<strong>Важна!</strong> Гэта праграма прызначана для працы з серверам Audiobookshelf, які вы ці хтосьці іншы абслугоўваеце. Праграма не змяшчае ўласнага кантэнту.",
"MessageBookshelfEmpty": "Кніжная паліца пустая", "MessageBookshelfEmpty": "Кніжная паліца пустая",
"MessageConfirmDeleteLocalEpisode": "Выдаліць лакальны эпізод \"{0}\" з вашай прылады? Файл на серверы застанецца нязменным.", "MessageConfirmAppExit": "Вы хацелі выйсці з праграмы?",
"MessageConfirmDeleteLocalFiles": "Выдаліць лакальныя файлы гэтага элемента з вашай прылады? Файлы на серверы і ваш прагрэс застануцца нязменнымі.", "MessageConfirmDeleteEpisodeDownloadQueue": "Вы ўпэўнены, што хочаце ачысціць чаргу спампоўвання выпускаў?",
"MessageConfirmDisableAutoTimer": "Вы ўпэўнены, што жадаеце адключыць аўтаматычны таймер на сённяшні дзень? Таймер будзе зноў уключаны па заканчэнні гэтага перыяду аўтаматычнага таймера сну або пры перазапуску праграмы.", "MessageConfirmDeleteLocalEpisode": "Выдаліць лакальны выпуск \"{0}\" з вашай прылады? Файл на серверы застанецца нязменным.",
"MessageConfirmDiscardProgress": "Вы ўпэўнены, што жадаеце скінуць свой прагрэс?", "MessageConfirmDeleteLocalFiles": "Выдаліць лакальныя файлы гэтага элемента з вашай прылады? Файлы на серверы і прагрэс застануцца нязменнымі.",
"MessageConfirmDownloadUsingCellular": "Вы збіраецеся спампаваць, выкарыстоўваючы мабільны інтэрнэт. Гэта можа прывесці да дадатковых плат ад аператара. Вы жадаеце працягнуць?", "MessageConfirmDeleteServerConfig": "Выдаліць гэту канфігурацыю сервера?",
"MessageConfirmMarkAsFinished": "Вы ўпэўнены, што жадаеце пазначыць гэты элемент як скончаны?", "MessageConfirmDeleteServerEpisode": "Вы ўпэўнены, што хочаце выдаліць выпуск \"{0}\" з сервера?\nУвага: будзе выдалены аўдыяфайл.",
"MessageConfirmRemoveBookmark": "Вы ўпэўнены, што жадаеце выдаліць закладку?", "MessageConfirmDisableAutoTimer": "Вы ўпэўнены, што хочаце адключыць аўтаматычны таймер на сённяшні дзень? Таймер будзе зноў уключаны па заканчэнні перыяду гэтага аўтаматычнага таймера сну або пры перазапуску праграмы.",
"MessageConfirmStreamingUsingCellular": "Вы збіраецеся трансляваць кантэнт праз мабільны інтэрнэт. Гэта можа прывесці да дадатковых платаў ад вашага аператара. Вы жадаеце працягнуць?", "MessageConfirmDiscardProgress": "Вы ўпэўнены, што хочаце скінуць прагрэс?",
"MessageConfirmDownloadUsingCellular": "Вы збіраецеся спампаваць праз мабільны інтэрнэт. Гэта можа прывесці да дадатковых плат ад аператара. Хочаце працягнуць?",
"MessageConfirmMarkAsFinished": "Вы ўпэўнены, што хочаце пазначыць гэты элемент як завершаны?",
"MessageConfirmPlaybackTime": "Пачаць прайграванне \"{0}\" у {1}?",
"MessageConfirmRemoveBookmark": "Вы ўпэўнены, што хочаце выдаліць закладку?",
"MessageConfirmStreamingUsingCellular": "Вы збіраецеся трансляваць кантэнт праз мабільны інтэрнэт. Гэта можа прывесці да дадатковых плат ад аператара. Хочаце працягнуць?",
"MessageDiscardProgress": "Скінуць прагрэс", "MessageDiscardProgress": "Скінуць прагрэс",
"MessageDownloadCompleteProcessing": "Спампоўка завершана. Апрацоўка...", "MessageDownloadCompleteProcessing": "Спампоўванне завершана. Апрацоўка...",
"MessageDownloading": "Спампоўванне...", "MessageDownloading": "Спампоўванне...",
"MessageDownloadingEpisode": "Спампоўка эпізоду", "MessageDownloadingEpisode": "Спампоўванне выпуску",
"MessageEpisodesQueuedForDownload": "{0} эпізод(аў) у чарзе для спампоўкі", "MessageEpisodesQueuedForDownload": "{0} выпуск(-аў) у чарзе спампоўвання",
"MessageFailedToRefreshToken": "Не ўдалося абнавіць токен, патрабуецца паўторны ўваход",
"MessageFeedURLWillBe": "URL стужкі будзе {0}", "MessageFeedURLWillBe": "URL стужкі будзе {0}",
"MessageFetching": "Атрыманне...", "MessageFetching": "Атрыманне...",
"MessageFollowTheProjectOnGithub": "Сачыце за праектам на Github", "MessageFollowTheProjectOnGithub": "Сачыце за праектам на Github",
@@ -305,8 +321,8 @@
"MessageItemMissing": "Элемент адсутнічае і патрабуе выпраўлення на серверы. Звычайна элемент пазначаецца як адсутны, таму што файлавыя шляхі недаступныя.", "MessageItemMissing": "Элемент адсутнічае і патрабуе выпраўлення на серверы. Звычайна элемент пазначаецца як адсутны, таму што файлавыя шляхі недаступныя.",
"MessageLoading": "Загрузка...", "MessageLoading": "Загрузка...",
"MessageLoadingServerData": "Загрузка даных з сервера...", "MessageLoadingServerData": "Загрузка даных з сервера...",
"MessageLocalFolderDescription": "\"Унутраная памяць прыкладання\" даступная толькі для гэтага прыкладання. Гэта прыкладанне працуе толькі з медыя, якія былі спампаваныя непасрэдна праз яго. Каб іншыя прыкладанні маглі атрымаць доступ да медыя, спампаванага гэтым прыкладаннем, можна выкарыстоўваць агульныя тэчкі.", "MessageLocalFolderDescription": "\"Унутраная памяць праграмы\" даступная толькі для гэтай праграмы. Гэта праграма працуе толькі з медыя, якія былі спампаваныя непасрэдна праз яго. Каб іншыя праграмы маглі атрымаць доступ да медыя, спампаванага гэтай праграмай, можна выкарыстоўваць абагуленыя папкі.",
"MessageMarkAsFinished": "Пазначыць як скончана", "MessageMarkAsFinished": "Пазначыць як завершаную",
"MessageMediaLinkedToADifferentServer": "Медыя звязаны з серверам Audiobookshelf па іншым адрасе ({0}). Сінхранізацыя прагрэсу адбудзецца пры падключэнні да гэтага адрасу сервера.", "MessageMediaLinkedToADifferentServer": "Медыя звязаны з серверам Audiobookshelf па іншым адрасе ({0}). Сінхранізацыя прагрэсу адбудзецца пры падключэнні да гэтага адрасу сервера.",
"MessageMediaLinkedToADifferentUser": "Медыя звязаны з гэтым серверам, але быў спампаваны іншым карыстальнікам. Прагрэс будзе сінхранізавацца толькі для карыстальніка, які яго спампаваў.", "MessageMediaLinkedToADifferentUser": "Медыя звязаны з гэтым серверам, але быў спампаваны іншым карыстальнікам. Прагрэс будзе сінхранізавацца толькі для карыстальніка, які яго спампаваў.",
"MessageMediaLinkedToServer": "Звязана з серверам {0}", "MessageMediaLinkedToServer": "Звязана з серверам {0}",
@@ -319,12 +335,12 @@
"MessageNoItemsFound": "Элементы не знойдзены", "MessageNoItemsFound": "Элементы не знойдзены",
"MessageNoListeningSessions": "Няма сеансаў праслухоўвання", "MessageNoListeningSessions": "Няма сеансаў праслухоўвання",
"MessageNoLogs": "Няма журналаў", "MessageNoLogs": "Няма журналаў",
"MessageNoMediaFolders": "Няма тэчак з медыя", "MessageNoMediaFolders": "Няма папак з медыя",
"MessageNoNetworkConnection": "Няма падлучэння да сеткі", "MessageNoNetworkConnection": "Няма падлучэння да сеткі",
"MessageNoPodcastsFound": "Падкасты не знойдзены", "MessageNoPodcastsFound": "Падкасты не знойдзены",
"MessageNoSeries": "Няма серый", "MessageNoSeries": "Няма серый",
"MessageNoUpdatesWereNecessary": "Абнаўленні не патрабаваліся", "MessageNoUpdatesWereNecessary": "Абнаўленні не патрабаваліся",
"MessageNoUserPlaylists": "У вас няма спісаў прайгравання", "MessageNoUserPlaylists": "У вас няма плэй-лістоў",
"MessageOldServerAuthReLoginRequired": "Аўтэнтыфікацыя была палепшана для бяспекі ў серверы v2.26.0. Усім карыстальнікам патрабуецца паўторна ўвайсці ў сістэму.", "MessageOldServerAuthReLoginRequired": "Аўтэнтыфікацыя была палепшана для бяспекі ў серверы v2.26.0. Усім карыстальнікам патрабуецца паўторна ўвайсці ў сістэму.",
"MessageOldServerAuthWarning": "Сервер выкарыстоўвае састарэлую аўтэнтыфікацыю", "MessageOldServerAuthWarning": "Сервер выкарыстоўвае састарэлую аўтэнтыфікацыю",
"MessageOldServerAuthWarningHelp": "Гэты сервер працуе пад версіяй, старэйшай за 2.26.0. У версіі 2.26.0 была дададзена больш бяспечная сістэма аўтэнтыфікацыі. Настойліва рэкамендуецца абнавіць сервер да апошняй версіі. Калі вы ўжо абнавілі сервер, увайдзіце зноў, каб выкарыстоўваць новую аўтэнтыфікацыю.", "MessageOldServerAuthWarningHelp": "Гэты сервер працуе пад версіяй, старэйшай за 2.26.0. У версіі 2.26.0 была дададзена больш бяспечная сістэма аўтэнтыфікацыі. Настойліва рэкамендуецца абнавіць сервер да апошняй версіі. Калі вы ўжо абнавілі сервер, увайдзіце зноў, каб выкарыстоўваць новую аўтэнтыфікацыю.",
@@ -332,9 +348,9 @@
"MessageOldServerConnectionWarningHelp": "Вы першапачаткова наладзілі падключэнне да гэтага сервера да міграцыі базы даных у версіі 2.3.0, якая выйшла ў чэрвені 2023 года. У будучым абнаўленні сервера магчымасць уваходу праз гэтае старое падключэнне будзе выдалена. Калі ласка, выдаліце існуючае падключэнне да сервера і падключыцеся зноў (выкарыстоўваючы той жа адрас сервера і ўліковыя даныя). Калі на гэтай прыладзе ёсць спампаваныя медыяфайлы, іх трэба будзе спампаваць зноў для сінхранізацыі з серверам.", "MessageOldServerConnectionWarningHelp": "Вы першапачаткова наладзілі падключэнне да гэтага сервера да міграцыі базы даных у версіі 2.3.0, якая выйшла ў чэрвені 2023 года. У будучым абнаўленні сервера магчымасць уваходу праз гэтае старое падключэнне будзе выдалена. Калі ласка, выдаліце існуючае падключэнне да сервера і падключыцеся зноў (выкарыстоўваючы той жа адрас сервера і ўліковыя даныя). Калі на гэтай прыладзе ёсць спампаваныя медыяфайлы, іх трэба будзе спампаваць зноў для сінхранізацыі з серверам.",
"MessagePodcastSearchField": "Увядзіце пошукавы запыт або URL RSS-стужкі", "MessagePodcastSearchField": "Увядзіце пошукавы запыт або URL RSS-стужкі",
"MessageProgressSyncFailed": "Апошняя спроба адправіць ваш прагрэс праслухоўвання на сервер не ўдалася. Спробы сінхранізаваць прагрэс будуць працягвацца кожныя 15 секунд на працягу 1 хвіліны падчас прайгравання медыяфайла.", "MessageProgressSyncFailed": "Апошняя спроба адправіць ваш прагрэс праслухоўвання на сервер не ўдалася. Спробы сінхранізаваць прагрэс будуць працягвацца кожныя 15 секунд на працягу 1 хвіліны падчас прайгравання медыяфайла.",
"MessageReportBugsAndContribute": "Паведамляйце пра памылкі, прапануйце новыя функцыі і ўдзельнічайце на", "MessageReportBugsAndContribute": "Паведамляйце пра памылкі, прапануйце функцыі і ўносьце свой уклад на",
"MessageSeriesAlreadyDownloaded": "Вы ўжо спампавалі ўсе кнігі з гэтай серыі.", "MessageSeriesAlreadyDownloaded": "Вы ўжо спампавалі ўсе кнігі з гэтай серыі.",
"MessageSeriesDownloadConfirm": "Спампаваць адсутныя {0} кніг(і) з {1} файл(амі), агульным памерам {2}, у тэчку {3}?", "MessageSeriesDownloadConfirm": "Спампаваць адсутныя {0} кніг(і) з {1} файлам(-і), агульным памерам {2}, у папку {3}?",
"MessageSeriesDownloadConfirmIos": "Спампаваць адсутныя {0} кніг(і) з {1} файл(амі), агульным памерам {2}?", "MessageSeriesDownloadConfirmIos": "Спампаваць адсутныя {0} кніг(і) з {1} файл(амі), агульным памерам {2}?",
"MessageSocketConnectedOverMeteredCellular": "Сокет падлучаны праз мабільны інтэрнэт з абмежаваным трафікам", "MessageSocketConnectedOverMeteredCellular": "Сокет падлучаны праз мабільны інтэрнэт з абмежаваным трафікам",
"MessageSocketConnectedOverMeteredWifi": "Сокет падлучаны праз Wi-Fi з абмежаваным трафікам", "MessageSocketConnectedOverMeteredWifi": "Сокет падлучаны праз Wi-Fi з абмежаваным трафікам",
@@ -342,17 +358,19 @@
"MessageSocketConnectedOverUnmeteredWifi": "Сокет падлучаны праз Wi-Fi без абмежавання трафіку", "MessageSocketConnectedOverUnmeteredWifi": "Сокет падлучаны праз Wi-Fi без абмежавання трафіку",
"MessageSocketNotConnected": "Сокет не падлучаны", "MessageSocketNotConnected": "Сокет не падлучаны",
"NoteRSSFeedPodcastAppsHttps": "Папярэджанне: большасць праграм для падкастаў патрабуюць, каб URL RSS-стужкі выкарыстоўваў HTTPS", "NoteRSSFeedPodcastAppsHttps": "Папярэджанне: большасць праграм для падкастаў патрабуюць, каб URL RSS-стужкі выкарыстоўваў HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Папярэджанне: адзін ці больш вашых эпізодаў не маюць даты публікацыі. Некаторыя праграмы для падкастаў патрабуюць гэтага.", "NoteRSSFeedPodcastAppsPubDate": "Папярэджанне: адзін ці больш выпускаў не маюць даты публікацыі. Некаторыя праграмы для падкастаў патрабуюць гэтага.",
"ToastBookmarkCreateFailed": "Не ўдалося стварыць закладку", "ToastBookmarkCreateFailed": "Не ўдалося стварыць закладку",
"ToastBookmarkRemoveFailed": "Не ўдалося выдаліць закладку", "ToastBookmarkRemoveFailed": "Не ўдалося выдаліць закладку",
"ToastBookmarkUpdateFailed": "Не ўдалося абнавіць закладку", "ToastBookmarkUpdateFailed": "Не ўдалося абнавіць закладку",
"ToastDownloadNotAllowedOnCellular": "Спампоўванне забаронена праз мабільны інтэрнэт", "ToastDownloadNotAllowedOnCellular": "Спампоўванне забаронена праз мабільны інтэрнэт",
"ToastItemMarkedAsFinishedFailed": "Не ўдалося пазначыць як Скончана", "ToastItemMarkedAsFinishedFailed": "Не ўдалося пазначыць як завершаны",
"ToastItemMarkedAsNotFinishedFailed": "Не ўдалося пазначыць як Незавершанае", "ToastItemMarkedAsNotFinishedFailed": "Не ўдалося пазначыць як незавершаны",
"ToastPlaylistCreateFailed": "Не ўдалося стварыць спіс прайгравання", "ToastPlaylistCreateFailed": "Не ўдалося стварыць плэй-ліст",
"ToastPodcastCreateFailed": "Не ўдалося стварыць падкаст", "ToastPodcastCreateFailed": "Не ўдалося стварыць падкаст",
"ToastPodcastCreateSuccess": "Падкаст паспяхова створаны", "ToastPodcastCreateSuccess": "Падкаст паспяхова створаны",
"ToastRSSFeedCloseFailed": "Не ўдалося закрыць RSS-стужку", "ToastRSSFeedCloseFailed": "Не ўдалося закрыць RSS-стужку",
"ToastRSSFeedCloseSuccess": "RSS-стужка закрыта", "ToastRSSFeedCloseSuccess": "RSS-стужка закрыта",
"ToastStreamingNotAllowedOnCellular": "Патокавая перадача праз мабільную сувязь забаронена" "ToastStreamingNotAllowedOnCellular": "Патокавая перадача праз мабільную сувязь забаронена",
"UnitMinutesShort": "{0} хв",
"UnitSecondsShort": "{0} с"
} }
+18 -1
View File
@@ -63,6 +63,7 @@
"HeaderChapters": "Глави", "HeaderChapters": "Глави",
"HeaderCollection": "Колекция", "HeaderCollection": "Колекция",
"HeaderCollectionItems": "Елемент в колекция", "HeaderCollectionItems": "Елемент в колекция",
"HeaderConfirm": "Потвърди",
"HeaderConnectionStatus": "Състояние на връзката", "HeaderConnectionStatus": "Състояние на връзката",
"HeaderDataSettings": "Настройки на данните", "HeaderDataSettings": "Настройки на данните",
"HeaderDetails": "Детайли", "HeaderDetails": "Детайли",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Последни сесии", "HeaderStatsRecentSessions": "Последни сесии",
"HeaderTableOfContents": "Съдържание", "HeaderTableOfContents": "Съдържание",
"HeaderUserInterfaceSettings": "Настройки на потребителския интерфейс", "HeaderUserInterfaceSettings": "Настройки на потребителския интерфейс",
"HeaderWelcome": "Добре дошли, <strong>{0}</strong>",
"HeaderYourStats": "Вашата статистика", "HeaderYourStats": "Вашата статистика",
"LabelAddToPlaylist": "Добави в плейлист", "LabelAddToPlaylist": "Добави в плейлист",
"LabelAddedAt": "Добавено в", "LabelAddedAt": "Добавено в",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Когато автоматичният таймер на изключване изтече, повторното пускане на елемента автоматично ще върне позицията ви назад.", "LabelAutoSleepTimerAutoRewindHelp": "Когато автоматичният таймер на изключване изтече, повторното пускане на елемента автоматично ще върне позицията ви назад.",
"LabelAutoSleepTimerHelp": "Когато се възпроизвеждате медия между посочените начален и краен час, автоматично ще стартира таймер за изключване.", "LabelAutoSleepTimerHelp": "Когато се възпроизвеждате медия между посочените начален и краен час, автоматично ще стартира таймер за изключване.",
"LabelBooks": "Книги", "LabelBooks": "Книги",
"LabelByAuthor": "от {0}",
"LabelChapterTrack": "Трак на глава", "LabelChapterTrack": "Трак на глава",
"LabelChapters": "Глави", "LabelChapters": "Глави",
"LabelClosePlayer": "Затвори плейъра", "LabelClosePlayer": "Затвори плейъра",
@@ -155,6 +158,9 @@
"LabelFinished": "Дата на приключване", "LabelFinished": "Дата на приключване",
"LabelFolder": "Папка", "LabelFolder": "Папка",
"LabelFontBoldness": "Дебелина на шрифта", "LabelFontBoldness": "Дебелина на шрифта",
"LabelFontFamily": "Шрифт",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Мащаб на шрифта", "LabelFontScale": "Мащаб на шрифта",
"LabelGenre": "Жанр", "LabelGenre": "Жанр",
"LabelGenres": "Жанрове", "LabelGenres": "Жанрове",
@@ -174,6 +180,9 @@
"LabelLayout": "Оформление", "LabelLayout": "Оформление",
"LabelLayoutAuto": "Авто", "LabelLayoutAuto": "Авто",
"LabelLayoutSinglePage": "Единична страница", "LabelLayoutSinglePage": "Единична страница",
"LabelLibrarySortByProgress": "Прогрес: Последно обновление",
"LabelLibrarySortByProgressFinished": "Прогрес: Приключено",
"LabelLibrarySortByProgressStarted": "Прогрес: Започнато",
"LabelLight": "Светло", "LabelLight": "Светло",
"LabelLineSpacing": "Междуредие", "LabelLineSpacing": "Междуредие",
"LabelListenAgain": "Слушай отново", "LabelListenAgain": "Слушай отново",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Скалируй изминалото време спрямо скоростта", "LabelScaleElapsedTimeBySpeed": "Скалируй изминалото време спрямо скоростта",
"LabelSeason": "Сезон", "LabelSeason": "Сезон",
"LabelSelectADevice": "Избери устройство", "LabelSelectADevice": "Избери устройство",
"LabelSelectMediaType": "Изберете тип на медията",
"LabelSequenceAscending": "Възходяща последователност", "LabelSequenceAscending": "Възходяща последователност",
"LabelSequenceDescending": "Низходяща последователност", "LabelSequenceDescending": "Низходяща последователност",
"LabelSeries": "От сериите", "LabelSeries": "От сериите",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf сървър не е включен", "MessageAudiobookshelfServerNotConnected": "Audiobookshelf сървър не е включен",
"MessageAudiobookshelfServerRequired": "<strong>ВАЖНО!</strong> Това приложение е създадено да работи с Audiobookshelf сървър, който вие или някой, когото познавате, хоства. Това приложение не предоставя никакво съдържание.", "MessageAudiobookshelfServerRequired": "<strong>ВАЖНО!</strong> Това приложение е създадено да работи с Audiobookshelf сървър, който вие или някой, когото познавате, хоства. Това приложение не предоставя никакво съдържание.",
"MessageBookshelfEmpty": "Библиотеката е празна", "MessageBookshelfEmpty": "Библиотеката е празна",
"MessageConfirmAppExit": "Искате ли да излезете от апликацията?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Сигурни ли сте, че искате да изчистите опашката за сваляне на епизоди?",
"MessageConfirmDeleteLocalEpisode": "Премахни \"{0}\" епизода от устройството? Файловете на сървъра няма да бъдат премахнати.", "MessageConfirmDeleteLocalEpisode": "Премахни \"{0}\" епизода от устройството? Файловете на сървъра няма да бъдат премахнати.",
"MessageConfirmDeleteLocalFiles": "Да премахна ли локалните файлове на този елемент от вашето устройство? Файловете на сървъра и вашият напредък няма да бъдат засегнати.", "MessageConfirmDeleteLocalFiles": "Да премахна ли локалните файлове на този елемент от вашето устройство? Файловете на сървъра и вашият напредък няма да бъдат засегнати.",
"MessageConfirmDeleteServerConfig": "Премахнете тази сървърна настройка?",
"MessageConfirmDeleteServerEpisode": "Сигурни ли сте че искате да изтрийете епизод \"{0}\" от сървъра?\nПредупреждение: Това ще изтрийе и аудио файла.",
"MessageConfirmDisableAutoTimer": "Сигурни ли сте, че искате да деактивирате автоматичния таймер за остатъка от днешния ден? Таймерът ще бъде активиран отново в края на този период на автоматичния таймер за изключване или ако рестартирате приложението.", "MessageConfirmDisableAutoTimer": "Сигурни ли сте, че искате да деактивирате автоматичния таймер за остатъка от днешния ден? Таймерът ще бъде активиран отново в края на този период на автоматичния таймер за изключване или ако рестартирате приложението.",
"MessageConfirmDiscardProgress": "Сигурни ли сте, че искате да нулирате напредъка си?", "MessageConfirmDiscardProgress": "Сигурни ли сте, че искате да нулирате напредъка си?",
"MessageConfirmDownloadUsingCellular": "Ще изтегляте, използвайки мобилни данни. Това може да включва такси за данни от оператора. Искате ли да продължите?", "MessageConfirmDownloadUsingCellular": "Ще изтегляте, използвайки мобилни данни. Това може да включва такси за данни от оператора. Искате ли да продължите?",
"MessageConfirmMarkAsFinished": "Сигурни ли сте, че искате да маркирате този елемент като завършен?", "MessageConfirmMarkAsFinished": "Сигурни ли сте, че искате да маркирате този елемент като завършен?",
"MessageConfirmPlaybackTime": "Започни възпроизвеждане на \"{0}\" в {1}?",
"MessageConfirmRemoveBookmark": "Сигурни ли сте, че искате да премахнете отметката?", "MessageConfirmRemoveBookmark": "Сигурни ли сте, че искате да премахнете отметката?",
"MessageConfirmStreamingUsingCellular": "Ще стриймвате, използвайки мобилни данни. Това може да включва такси за данни от оператора. Искате ли да продължите?", "MessageConfirmStreamingUsingCellular": "Ще стриймвате, използвайки мобилни данни. Това може да включва такси за данни от оператора. Искате ли да продължите?",
"MessageDiscardProgress": "Нулирай прогреса", "MessageDiscardProgress": "Нулирай прогреса",
@@ -355,5 +370,7 @@
"ToastPodcastCreateSuccess": "Подкаст успешно създаден", "ToastPodcastCreateSuccess": "Подкаст успешно създаден",
"ToastRSSFeedCloseFailed": "Неуспешно затваряне на RSS емисията", "ToastRSSFeedCloseFailed": "Неуспешно затваряне на RSS емисията",
"ToastRSSFeedCloseSuccess": "RSS емисията е затворена", "ToastRSSFeedCloseSuccess": "RSS емисията е затворена",
"ToastStreamingNotAllowedOnCellular": "Стриймването не е разрешено чрез мобилни данни" "ToastStreamingNotAllowedOnCellular": "Стриймването не е разрешено чрез мобилни данни",
"UnitMinutesShort": "{0}мин",
"UnitSecondsShort": "{0}сек"
} }
+18 -1
View File
@@ -63,6 +63,7 @@
"HeaderChapters": "Capítols", "HeaderChapters": "Capítols",
"HeaderCollection": "Col·lecció", "HeaderCollection": "Col·lecció",
"HeaderCollectionItems": "Elements de la col·lecció", "HeaderCollectionItems": "Elements de la col·lecció",
"HeaderConfirm": "Confirma",
"HeaderConnectionStatus": "Estat de la connexió", "HeaderConnectionStatus": "Estat de la connexió",
"HeaderDataSettings": "Paràmetres de dades", "HeaderDataSettings": "Paràmetres de dades",
"HeaderDetails": "Detalls", "HeaderDetails": "Detalls",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Sessions recents", "HeaderStatsRecentSessions": "Sessions recents",
"HeaderTableOfContents": "Sumari", "HeaderTableOfContents": "Sumari",
"HeaderUserInterfaceSettings": "Paràmetres de la interfície d'usuari", "HeaderUserInterfaceSettings": "Paràmetres de la interfície d'usuari",
"HeaderWelcome": "Benvingut, <strong>{0}</strong>",
"HeaderYourStats": "Les vostres estadístiques", "HeaderYourStats": "Les vostres estadístiques",
"LabelAddToPlaylist": "Afegeix a la llista de reproducció", "LabelAddToPlaylist": "Afegeix a la llista de reproducció",
"LabelAddedAt": "Afegit a", "LabelAddedAt": "Afegit a",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Quan el temporitzador d'auto sleep acaba, reproduir l'element altra vegada farà que es rebobine automàticament a la teua posició.", "LabelAutoSleepTimerAutoRewindHelp": "Quan el temporitzador d'auto sleep acaba, reproduir l'element altra vegada farà que es rebobine automàticament a la teua posició.",
"LabelAutoSleepTimerHelp": "Quan es reprodueix entre hores especificades, el temporitzador s'activarà automàticament.", "LabelAutoSleepTimerHelp": "Quan es reprodueix entre hores especificades, el temporitzador s'activarà automàticament.",
"LabelBooks": "Llibres", "LabelBooks": "Llibres",
"LabelByAuthor": "per {0}",
"LabelChapterTrack": "Seguiment de capítol", "LabelChapterTrack": "Seguiment de capítol",
"LabelChapters": "Capítols", "LabelChapters": "Capítols",
"LabelClosePlayer": "Tanca el reproductor", "LabelClosePlayer": "Tanca el reproductor",
@@ -126,11 +129,12 @@
"LabelCustomTime": "Temps personalitzat", "LabelCustomTime": "Temps personalitzat",
"LabelDescription": "Descripció", "LabelDescription": "Descripció",
"LabelDisableAudioFadeOut": "Desactivar desaparició d'àudio", "LabelDisableAudioFadeOut": "Desactivar desaparició d'àudio",
"LabelDisableAudioFadeOutHelp": "El volum de l'audio començara a reduïr-se quan quede menys d'1 minut del temporitzador d'anar a dormir. Habilita este ajustament per a no reduïr el volum.", "LabelDisableAudioFadeOutHelp": "El volum de l'audio començara a reduïr-se quan quedi menys d'1 minut del temporitzador d'anar a dormir. Habilita aquest ajustament per a no reduïr el volum.",
"LabelDisableAutoRewind": "Desactivar rebobinatge automàtic", "LabelDisableAutoRewind": "Desactivar rebobinatge automàtic",
"LabelDisableShakeToReset": "Desactivar sacsejar per reiniciar", "LabelDisableShakeToReset": "Desactivar sacsejar per reiniciar",
"LabelDisableShakeToResetHelp": "Si sacseges el teu dispositiu quan el temporitzador és actiu o durant els 2 minuts després que hagi expirat, el temporitzador es reinicia. Activa aquesta opció per evitar-ho.", "LabelDisableShakeToResetHelp": "Si sacseges el teu dispositiu quan el temporitzador és actiu o durant els 2 minuts després que hagi expirat, el temporitzador es reinicia. Activa aquesta opció per evitar-ho.",
"LabelDisableVibrateOnReset": "Desactivar la vibració al reiniciar", "LabelDisableVibrateOnReset": "Desactivar la vibració al reiniciar",
"LabelDisableVibrateOnResetHelp": "Quan el temporitzador d'apagat es reinicia, el dispositiu vibra. Activa aquesta opció perquè no vibri quan es reiniciï el temporitzador.",
"LabelDiscover": "Descobrir", "LabelDiscover": "Descobrir",
"LabelDownload": "Descarregar", "LabelDownload": "Descarregar",
"LabelDownloadUsingCellular": "Descarregar utilitzant dades mòbils", "LabelDownloadUsingCellular": "Descarregar utilitzant dades mòbils",
@@ -144,6 +148,7 @@
"LabelEndOfChapter": "Final del capítol", "LabelEndOfChapter": "Final del capítol",
"LabelEndTime": "Hora de finalització", "LabelEndTime": "Hora de finalització",
"LabelEpisode": "Episodi", "LabelEpisode": "Episodi",
"LabelExplicit": "Explícit",
"LabelFeedURL": "Font URL", "LabelFeedURL": "Font URL",
"LabelFile": "Fitxer", "LabelFile": "Fitxer",
"LabelFileBirthtime": "Fitxer creat a", "LabelFileBirthtime": "Fitxer creat a",
@@ -152,22 +157,31 @@
"LabelFinished": "Acabat", "LabelFinished": "Acabat",
"LabelFolder": "Carpeta", "LabelFolder": "Carpeta",
"LabelFontBoldness": "Gruix de la lletra", "LabelFontBoldness": "Gruix de la lletra",
"LabelFontFamily": "Família tipogràfica",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Escala de la lletra", "LabelFontScale": "Escala de la lletra",
"LabelGenre": "Gènere", "LabelGenre": "Gènere",
"LabelGenres": "Gèneres", "LabelGenres": "Gèneres",
"LabelHapticFeedback": "Resposta hàptica", "LabelHapticFeedback": "Resposta hàptica",
"LabelHasEbook": "Té llibre electrònic", "LabelHasEbook": "Té llibre electrònic",
"LabelHasSupplementaryEbook": "Té llibre electrònic suplementari", "LabelHasSupplementaryEbook": "Té llibre electrònic suplementari",
"LabelHeavy": "Pesat",
"LabelHigh": "Alt", "LabelHigh": "Alt",
"LabelHost": "Amfitrió", "LabelHost": "Amfitrió",
"LabelInProgress": "En procés", "LabelInProgress": "En procés",
"LabelIncomplete": "Incomplet", "LabelIncomplete": "Incomplet",
"LabelInternalAppStorage": "Emmagatzematge intern de l'aplicació", "LabelInternalAppStorage": "Emmagatzematge intern de l'aplicació",
"LabelJumpBackwardsTime": "Saltar enrere", "LabelJumpBackwardsTime": "Saltar enrere",
"LabelJumpForwardsTime": "Salt endavant en el temps",
"LabelKeepScreenAwake": "Manté la pantalla encesa",
"LabelLanguage": "Llengua", "LabelLanguage": "Llengua",
"LabelLayout": "Disposició", "LabelLayout": "Disposició",
"LabelLayoutAuto": "Automàtic", "LabelLayoutAuto": "Automàtic",
"LabelLayoutSinglePage": "Pàgina única", "LabelLayoutSinglePage": "Pàgina única",
"LabelLibrarySortByProgress": "Progrés: Última actualització",
"LabelLibrarySortByProgressFinished": "Progrés: Finalitzat",
"LabelLibrarySortByProgressStarted": "Progrés: Començat",
"LabelLight": "Clar", "LabelLight": "Clar",
"LabelLineSpacing": "Interlineat", "LabelLineSpacing": "Interlineat",
"LabelListenAgain": "Escoltar de nou", "LabelListenAgain": "Escoltar de nou",
@@ -175,14 +189,17 @@
"LabelLocalPodcasts": "Pòdcasts locals", "LabelLocalPodcasts": "Pòdcasts locals",
"LabelLockOrientation": "Bloca l'orientació", "LabelLockOrientation": "Bloca l'orientació",
"LabelLockPlayer": "Bloca el reproductor", "LabelLockPlayer": "Bloca el reproductor",
"LabelLow": "Baix",
"LabelMediaType": "Tipus de mitjà", "LabelMediaType": "Tipus de mitjà",
"LabelMedium": "Mitjà", "LabelMedium": "Mitjà",
"LabelMissing": "Falta",
"LabelMore": "Més", "LabelMore": "Més",
"LabelMoreInfo": "Més informació", "LabelMoreInfo": "Més informació",
"LabelName": "Nom", "LabelName": "Nom",
"LabelNarrator": "Narrador", "LabelNarrator": "Narrador",
"LabelNarrators": "Narradors", "LabelNarrators": "Narradors",
"LabelNavigateWithVolume": "Navega amb les tecles de volum", "LabelNavigateWithVolume": "Navega amb les tecles de volum",
"LabelNavigateWithVolumeMirrored": "Emmirallat",
"LabelNavigateWithVolumeWhilePlayingDisabled": "⭘", "LabelNavigateWithVolumeWhilePlayingDisabled": "⭘",
"LabelNavigateWithVolumeWhilePlayingEnabled": "", "LabelNavigateWithVolumeWhilePlayingEnabled": "",
"LabelNever": "Mai", "LabelNever": "Mai",
+26 -9
View File
@@ -41,7 +41,7 @@
"ButtonPlaylists": "Seznamy skladeb", "ButtonPlaylists": "Seznamy skladeb",
"ButtonRead": "Číst", "ButtonRead": "Číst",
"ButtonReadLess": "Číst méně", "ButtonReadLess": "Číst méně",
"ButtonReadMore": "Číst další", "ButtonReadMore": "Číst více",
"ButtonRemove": "Odstranit", "ButtonRemove": "Odstranit",
"ButtonRemoveFromServer": "Odstranit ze serveru", "ButtonRemoveFromServer": "Odstranit ze serveru",
"ButtonSave": "Uložit", "ButtonSave": "Uložit",
@@ -63,6 +63,7 @@
"HeaderChapters": "Kapitoly", "HeaderChapters": "Kapitoly",
"HeaderCollection": "Kolekce", "HeaderCollection": "Kolekce",
"HeaderCollectionItems": "Položky kolekce", "HeaderCollectionItems": "Položky kolekce",
"HeaderConfirm": "Potvrzení",
"HeaderConnectionStatus": "Stav připojení", "HeaderConnectionStatus": "Stav připojení",
"HeaderDataSettings": "Nastavení dat", "HeaderDataSettings": "Nastavení dat",
"HeaderDetails": "Podrobnosti", "HeaderDetails": "Podrobnosti",
@@ -82,7 +83,7 @@
"HeaderProgressSyncFailed": "Chyba při synchronizaci pokroku", "HeaderProgressSyncFailed": "Chyba při synchronizaci pokroku",
"HeaderRSSFeed": "RSS kanál", "HeaderRSSFeed": "RSS kanál",
"HeaderRSSFeedGeneral": "Detaily RSS", "HeaderRSSFeedGeneral": "Detaily RSS",
"HeaderRSSFeedIsOpen": "Kanál RSS je otevřen", "HeaderRSSFeedIsOpen": "Informační kanál RSS je otevřený",
"HeaderSelectDownloadLocation": "Vyberte umístění pro stažení", "HeaderSelectDownloadLocation": "Vyberte umístění pro stažení",
"HeaderSettings": "Nastavení", "HeaderSettings": "Nastavení",
"HeaderSleepTimer": "Časovač spánku", "HeaderSleepTimer": "Časovač spánku",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Poslední sezení", "HeaderStatsRecentSessions": "Poslední sezení",
"HeaderTableOfContents": "Obsah", "HeaderTableOfContents": "Obsah",
"HeaderUserInterfaceSettings": "Nastavení uživatelského rozhraní", "HeaderUserInterfaceSettings": "Nastavení uživatelského rozhraní",
"HeaderWelcome": "Vítejte, <strong>{0}</strong>",
"HeaderYourStats": "Vaše statistiky", "HeaderYourStats": "Vaše statistiky",
"LabelAddToPlaylist": "Přidat do seznamu skladeb", "LabelAddToPlaylist": "Přidat do seznamu skladeb",
"LabelAddedAt": "Přidáno v", "LabelAddedAt": "Přidáno v",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Když dojde k uspání automatickým časovačem spánku, pozice přehrávání je posunuta zpět o vybraný čas.", "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": "Během přehrávání média v časovém rozmezí \"Od\" a \"Do\" se automaticky spustí časovač spánku.", "LabelAutoSleepTimerHelp": "Během přehrávání média v časovém rozmezí \"Od\" a \"Do\" se automaticky spustí časovač spánku.",
"LabelBooks": "Knihy", "LabelBooks": "Knihy",
"LabelByAuthor": "od {0}",
"LabelChapterTrack": "Stopa kapitoly", "LabelChapterTrack": "Stopa kapitoly",
"LabelChapters": "Kapitoly", "LabelChapters": "Kapitoly",
"LabelClosePlayer": "Zavřít přehrávač", "LabelClosePlayer": "Zavřít přehrávač",
@@ -146,7 +149,7 @@
"LabelEndOfChapter": "Konec kapitoly", "LabelEndOfChapter": "Konec kapitoly",
"LabelEndTime": "Do", "LabelEndTime": "Do",
"LabelEpisode": "Epizoda", "LabelEpisode": "Epizoda",
"LabelExplicit": "Explicitně", "LabelExplicit": "Explicitní",
"LabelFeedURL": "URL kanálu", "LabelFeedURL": "URL kanálu",
"LabelFile": "Soubor", "LabelFile": "Soubor",
"LabelFileBirthtime": "Čas vytvoření souboru", "LabelFileBirthtime": "Čas vytvoření souboru",
@@ -155,13 +158,16 @@
"LabelFinished": "Dokončeno", "LabelFinished": "Dokončeno",
"LabelFolder": "Složka", "LabelFolder": "Složka",
"LabelFontBoldness": "Výraznost písma", "LabelFontBoldness": "Výraznost písma",
"LabelFontScale": "Měřítko písma", "LabelFontFamily": "Rodina písem",
"LabelFontFamilySans": "Bezpatkové",
"LabelFontFamilySerif": "Patkové",
"LabelFontScale": "Velikost písma",
"LabelGenre": "Žánr", "LabelGenre": "Žánr",
"LabelGenres": "Žánry", "LabelGenres": "Žánry",
"LabelHapticFeedback": "Haptická odezva", "LabelHapticFeedback": "Haptická odezva",
"LabelHasEbook": "Má e-knihu", "LabelHasEbook": "Má e-knihu",
"LabelHasSupplementaryEbook": " doplňkovou e-knihu", "LabelHasSupplementaryEbook": "Obsahuje doplňkovou e-knihu",
"LabelHeavy": "Těžké", "LabelHeavy": "Silná",
"LabelHigh": "Vysoké", "LabelHigh": "Vysoké",
"LabelHost": "Hostitel", "LabelHost": "Hostitel",
"LabelInProgress": "Probíhá", "LabelInProgress": "Probíhá",
@@ -174,7 +180,10 @@
"LabelLayout": "Rozvržení", "LabelLayout": "Rozvržení",
"LabelLayoutAuto": "Automatické", "LabelLayoutAuto": "Automatické",
"LabelLayoutSinglePage": "Jedna stránka", "LabelLayoutSinglePage": "Jedna stránka",
"LabelLight": "Světlé", "LabelLibrarySortByProgress": "Pokrok: naposledy aktualizováno",
"LabelLibrarySortByProgressFinished": "Pokrok: dokončeno",
"LabelLibrarySortByProgressStarted": "Pokrok: začato",
"LabelLight": "Slabá",
"LabelLineSpacing": "Řádkování", "LabelLineSpacing": "Řádkování",
"LabelListenAgain": "Poslouchat znovu", "LabelListenAgain": "Poslouchat znovu",
"LabelLocalBooks": "Místní knihy", "LabelLocalBooks": "Místní knihy",
@@ -204,7 +213,7 @@
"LabelNumEpisodes": "{0} epizod", "LabelNumEpisodes": "{0} epizod",
"LabelNumEpisodesIncomplete": "{0} epizod, {1} nekompletní", "LabelNumEpisodesIncomplete": "{0} epizod, {1} nekompletní",
"LabelNumberOfEpisodes": "Počet epizod", "LabelNumberOfEpisodes": "Počet epizod",
"LabelOff": "Vypnout", "LabelOff": "Vypnutá",
"LabelOn": "Zapnuto", "LabelOn": "Zapnuto",
"LabelPassword": "Heslo", "LabelPassword": "Heslo",
"LabelPath": "Cesta", "LabelPath": "Cesta",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Škálovat uplynulý čas podle rychlosti", "LabelScaleElapsedTimeBySpeed": "Škálovat uplynulý čas podle rychlosti",
"LabelSeason": "Sezóna", "LabelSeason": "Sezóna",
"LabelSelectADevice": "Vyberte zařízení", "LabelSelectADevice": "Vyberte zařízení",
"LabelSelectMediaType": "Vyberte typ média",
"LabelSequenceAscending": "Řadit vzestupně", "LabelSequenceAscending": "Řadit vzestupně",
"LabelSequenceDescending": "Řadit sestupně", "LabelSequenceDescending": "Řadit sestupně",
"LabelSeries": "Série", "LabelSeries": "Série",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Server Audiobookshelf není připojen", "MessageAudiobookshelfServerNotConnected": "Server Audiobookshelf není připojen",
"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.", "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": "Knihovna je prázdná", "MessageBookshelfEmpty": "Knihovna je prázdná",
"MessageConfirmAppExit": "Opravdu chcete opustit aplikaci?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Přejete si skutečně vymazat frontu stahování epizod?",
"MessageConfirmDeleteLocalEpisode": "Odebrat místní epizodu „{0}“ ze zařízení? Soubor na serveru zůstane nezměněný.", "MessageConfirmDeleteLocalEpisode": "Odebrat místní epizodu „{0}“ ze zařízení? Soubor na serveru zůstane nezměněný.",
"MessageConfirmDeleteLocalFiles": "Odebrat místní soubory této položky ze zařízení? Soubory na serveru a váš pokrok nebudou ovlivněny.", "MessageConfirmDeleteLocalFiles": "Odebrat místní soubory této položky ze zařízení? Soubory na serveru a váš pokrok nebudou ovlivněny.",
"MessageConfirmDeleteServerConfig": "Odebrat tuto konfiguraci serveru?",
"MessageConfirmDeleteServerEpisode": "Přejete si skutečně odstranit epizodu \"{0}\" z serveru?\nUpozornění: Toto odstraní zvukový soubor.",
"MessageConfirmDisableAutoTimer": "Určitě chcete vypnout automatický časovač pro zbytek dnešního dne? Časovač bude opět aktivován po uběhnutí doby automatického spánku nebo po restartování aplikace.", "MessageConfirmDisableAutoTimer": "Určitě chcete vypnout automatický časovač pro zbytek dnešního dne? Časovač bude opět aktivován po uběhnutí doby automatického spánku nebo po restartování aplikace.",
"MessageConfirmDiscardProgress": "Opravdu chcete zahodit svůj pokrok?", "MessageConfirmDiscardProgress": "Opravdu chcete zahodit svůj pokrok?",
"MessageConfirmDownloadUsingCellular": "Chystáte se stahovat přes mobilní data. Toto může zahrnovat poplatky za mobilní data. Chcete pokračovat?", "MessageConfirmDownloadUsingCellular": "Chystáte se stahovat přes mobilní data. Toto může zahrnovat poplatky za mobilní data. Chcete pokračovat?",
"MessageConfirmMarkAsFinished": "Opravdu chcete tuto položku označit jako dokončenou?", "MessageConfirmMarkAsFinished": "Opravdu chcete tuto položku označit jako dokončenou?",
"MessageConfirmPlaybackTime": "Spustit přehrávání pro \"{0}\" v {1}?",
"MessageConfirmRemoveBookmark": "Opravdu chcete odebrat záložku?", "MessageConfirmRemoveBookmark": "Opravdu chcete odebrat záložku?",
"MessageConfirmStreamingUsingCellular": "Chystáte se přehrávat přes mobilní data. Toto může zahrnovat poplatky za mobilní data. Chcete pokračovat?", "MessageConfirmStreamingUsingCellular": "Chystáte se přehrávat přes mobilní data. Toto může zahrnovat poplatky za mobilní data. Chcete pokračovat?",
"MessageDiscardProgress": "Zahodit pokrok", "MessageDiscardProgress": "Zahodit pokrok",
@@ -355,5 +370,7 @@
"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" "ToastStreamingNotAllowedOnCellular": "Přehrávání přes mobilní data není povoleno",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+21 -5
View File
@@ -63,6 +63,7 @@
"HeaderChapters": "Kapitler", "HeaderChapters": "Kapitler",
"HeaderCollection": "Samling", "HeaderCollection": "Samling",
"HeaderCollectionItems": "Samlingselementer", "HeaderCollectionItems": "Samlingselementer",
"HeaderConfirm": "Bekræft",
"HeaderConnectionStatus": "Forbindelsesstatus", "HeaderConnectionStatus": "Forbindelsesstatus",
"HeaderDataSettings": "Data indstillinger", "HeaderDataSettings": "Data indstillinger",
"HeaderDetails": "Detaljer", "HeaderDetails": "Detaljer",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Seneste Sessions", "HeaderStatsRecentSessions": "Seneste Sessions",
"HeaderTableOfContents": "Indholdsfortegnelse", "HeaderTableOfContents": "Indholdsfortegnelse",
"HeaderUserInterfaceSettings": "Indstillinger for brugergrænseflade", "HeaderUserInterfaceSettings": "Indstillinger for brugergrænseflade",
"HeaderWelcome": "Velkommen, <strong>{0}</strong>",
"HeaderYourStats": "Dine Statistikker", "HeaderYourStats": "Dine Statistikker",
"LabelAddToPlaylist": "Tilføj til Afspilningsliste", "LabelAddToPlaylist": "Tilføj til Afspilningsliste",
"LabelAddedAt": "Tilføjet", "LabelAddedAt": "Tilføjet",
@@ -108,11 +110,12 @@
"LabelAuthors": "Forfattere", "LabelAuthors": "Forfattere",
"LabelAutoDownloadEpisodes": "Auto Download Episoder", "LabelAutoDownloadEpisodes": "Auto Download Episoder",
"LabelAutoRewindTime": "Automatisk tilbagespolingstid", "LabelAutoRewindTime": "Automatisk tilbagespolingstid",
"LabelAutoSleepTimer": "Auto sleep timer", "LabelAutoSleepTimer": "Auto søvn-timer",
"LabelAutoSleepTimerAutoRewind": "Automatisk sleep timer automatisk tilbagespoling", "LabelAutoSleepTimerAutoRewind": "Automatisk sleep timer automatisk tilbagespoling",
"LabelAutoSleepTimerAutoRewindHelp": "Når den automatiske sleep-timer er færdig, vil genafspilning af elementet automatisk spole din position tilbage.", "LabelAutoSleepTimerAutoRewindHelp": "Når den automatiske sleep-timer er færdig, vil genafspilning af elementet automatisk spole din position tilbage.",
"LabelAutoSleepTimerHelp": "Når der afspilles medie mellem de angivne start- og sluttidspunkter, starter en sleep-timer automatisk.", "LabelAutoSleepTimerHelp": "Når der afspilles medie mellem de angivne start- og sluttidspunkter, starter en sleep-timer automatisk.",
"LabelBooks": "Bøger", "LabelBooks": "Bøger",
"LabelByAuthor": "Efter Forfatter",
"LabelChapterTrack": "Kapitel spor", "LabelChapterTrack": "Kapitel spor",
"LabelChapters": "Kapitler", "LabelChapters": "Kapitler",
"LabelClosePlayer": "Luk afspiller", "LabelClosePlayer": "Luk afspiller",
@@ -155,6 +158,9 @@
"LabelFinished": "Færdig", "LabelFinished": "Færdig",
"LabelFolder": "Mappe", "LabelFolder": "Mappe",
"LabelFontBoldness": "Skrift tykkelse", "LabelFontBoldness": "Skrift tykkelse",
"LabelFontFamily": "Skrifttypefamilie",
"LabelFontFamilySans": "Font Sans",
"LabelFontFamilySerif": "Font Serif",
"LabelFontScale": "Skriftstørrelse", "LabelFontScale": "Skriftstørrelse",
"LabelGenre": "Genre", "LabelGenre": "Genre",
"LabelGenres": "Genrer", "LabelGenres": "Genrer",
@@ -174,6 +180,9 @@
"LabelLayout": "Layout", "LabelLayout": "Layout",
"LabelLayoutAuto": "Automatisk", "LabelLayoutAuto": "Automatisk",
"LabelLayoutSinglePage": "Enkel Side", "LabelLayoutSinglePage": "Enkel Side",
"LabelLibrarySortByProgress": "Fremgang: Sidst opdateret",
"LabelLibrarySortByProgressFinished": "Fremgang: Afsluttet",
"LabelLibrarySortByProgressStarted": "Fremgang: Startet",
"LabelLight": "Lys", "LabelLight": "Lys",
"LabelLineSpacing": "Linjeafstand", "LabelLineSpacing": "Linjeafstand",
"LabelListenAgain": "Lyt Igen", "LabelListenAgain": "Lyt Igen",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Skalér Forløbet Tid med Hastighed", "LabelScaleElapsedTimeBySpeed": "Skalér Forløbet Tid med Hastighed",
"LabelSeason": "Sæson", "LabelSeason": "Sæson",
"LabelSelectADevice": "Vælg en Enhed", "LabelSelectADevice": "Vælg en Enhed",
"LabelSelectMediaType": "Vælg medietype",
"LabelSequenceAscending": "Sekvens Stigende", "LabelSequenceAscending": "Sekvens Stigende",
"LabelSequenceDescending": "Sekvens Faldende", "LabelSequenceDescending": "Sekvens Faldende",
"LabelSeries": "Serie", "LabelSeries": "Serie",
@@ -283,10 +293,13 @@
"MessageAndroid10Downloads": "Android 10 og mindre vil bruge intern lagerplads til downloads.", "MessageAndroid10Downloads": "Android 10 og mindre vil bruge intern lagerplads til downloads.",
"MessageAttemptingServerConnection": "Prøver at tilgå server...", "MessageAttemptingServerConnection": "Prøver at tilgå server...",
"MessageAudiobookshelfServerNotConnected": "Audiobookshel server er ikke tilsluttet", "MessageAudiobookshelfServerNotConnected": "Audiobookshel server er ikke tilsluttet",
"MessageAudiobookshelfServerRequired": "<strong>Vigtig</strong> Denne app er designet til at fungere med en Audiobookshelf server, som du eller en du kender hoster. Denne app levere ikke noget indhold.", "MessageAudiobookshelfServerRequired": "<strong>Vigtigt!</strong> Denne app er designet til at fungere med en Audiobookshelf-server, som du eller en du kender hoster. Denne app leverer ikke noget indhold.",
"MessageBookshelfEmpty": "Bogreol Tom", "MessageBookshelfEmpty": "Bogreol Tom",
"MessageConfirmAppExit": "Ville du lukke appen?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Er du sikker på, at du vil rydde downloadkøen for episoder?",
"MessageConfirmDeleteLocalEpisode": "Slet lokal episode \"{0}\" fra din enhed? Filen på serveren vil ikke blive påvirket.", "MessageConfirmDeleteLocalEpisode": "Slet lokal episode \"{0}\" fra din enhed? Filen på serveren vil ikke blive påvirket.",
"MessageConfirmDeleteLocalFiles": "Slet lokale filer af denne type fra din enhed? Filerne på serveren og din fremgang vil ikke blive påvirket.", "MessageConfirmDeleteLocalFiles": "Slet lokale filer af denne type fra din enhed? Filerne på serveren og din fremgang vil ikke blive påvirket.",
"MessageConfirmDeleteServerConfig": "Fjern denne serverkonfiguration?",
"MessageConfirmDisableAutoTimer": "Er du sikker på at du vil slå auto-timer fra resten af dagen? Timeren vil blive slået til ved slutningen af auto-sleep timer perioden eller appen genstartes.", "MessageConfirmDisableAutoTimer": "Er du sikker på at du vil slå auto-timer fra resten af dagen? Timeren vil blive slået til ved slutningen af auto-sleep timer perioden eller appen genstartes.",
"MessageConfirmDiscardProgress": "Er du sikker på at du vil nulstille din fremgang?", "MessageConfirmDiscardProgress": "Er du sikker på at du vil nulstille din fremgang?",
"MessageConfirmDownloadUsingCellular": "Du er ved at downloade via mobildata. Dette kan medføre ekstra omkostninger fra din teleoperatør. Vil du fortsætte?", "MessageConfirmDownloadUsingCellular": "Du er ved at downloade via mobildata. Dette kan medføre ekstra omkostninger fra din teleoperatør. Vil du fortsætte?",
@@ -298,6 +311,7 @@
"MessageDownloading": "Downloader...", "MessageDownloading": "Downloader...",
"MessageDownloadingEpisode": "Downloader episode", "MessageDownloadingEpisode": "Downloader episode",
"MessageEpisodesQueuedForDownload": "{0} episoder er sat i kø til download", "MessageEpisodesQueuedForDownload": "{0} episoder er sat i kø til download",
"MessageFailedToRefreshToken": "Mislykkedes med at opdatere token, nyt loginforsøg nødvendigt",
"MessageFeedURLWillBe": "Feed-URL vil være {0}", "MessageFeedURLWillBe": "Feed-URL vil være {0}",
"MessageFetching": "Henter...", "MessageFetching": "Henter...",
"MessageFollowTheProjectOnGithub": "Følg projektet på GitHub", "MessageFollowTheProjectOnGithub": "Følg projektet på GitHub",
@@ -347,12 +361,14 @@
"ToastBookmarkRemoveFailed": "Mislykkedes fjernelse af bogmærke", "ToastBookmarkRemoveFailed": "Mislykkedes fjernelse af bogmærke",
"ToastBookmarkUpdateFailed": "Mislykkedes opdatering af bogmærke", "ToastBookmarkUpdateFailed": "Mislykkedes opdatering af bogmærke",
"ToastDownloadNotAllowedOnCellular": "Det er ikke tillad at downloade over mobildata", "ToastDownloadNotAllowedOnCellular": "Det er ikke tillad at downloade over mobildata",
"ToastItemMarkedAsFinishedFailed": "Mislykkedes markering som afsluttet", "ToastItemMarkedAsFinishedFailed": "Markering som afsluttet mislykkedes",
"ToastItemMarkedAsNotFinishedFailed": "Mislykkedes markering som ikke afsluttet", "ToastItemMarkedAsNotFinishedFailed": "Markering som ikke afsluttet mislykkedes",
"ToastPlaylistCreateFailed": "Mislykkedes oprettelse af afspilningsliste", "ToastPlaylistCreateFailed": "Mislykkedes oprettelse af afspilningsliste",
"ToastPodcastCreateFailed": "Mislykkedes oprettelse af podcast", "ToastPodcastCreateFailed": "Mislykkedes oprettelse af podcast",
"ToastPodcastCreateSuccess": "Podcast oprettet med succes", "ToastPodcastCreateSuccess": "Podcast oprettet med succes",
"ToastRSSFeedCloseFailed": "Mislykkedes lukning af RSS-feed", "ToastRSSFeedCloseFailed": "Mislykkedes lukning af RSS-feed",
"ToastRSSFeedCloseSuccess": "RSS-feed lukket", "ToastRSSFeedCloseSuccess": "RSS-feed lukket",
"ToastStreamingNotAllowedOnCellular": "Det er ikke tillad at streame over mobildata" "ToastStreamingNotAllowedOnCellular": "Det er ikke tillad at streame over mobildata",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+29 -12
View File
@@ -32,7 +32,7 @@
"ButtonMaskServerAddress": "Server Adresse verstecken", "ButtonMaskServerAddress": "Server Adresse verstecken",
"ButtonNewFolder": "Neuer Ordner", "ButtonNewFolder": "Neuer Ordner",
"ButtonNextEpisode": "Nächste Episode", "ButtonNextEpisode": "Nächste Episode",
"ButtonOk": "Ok", "ButtonOk": "OK",
"ButtonOpenFeed": "Feed öffnen", "ButtonOpenFeed": "Feed öffnen",
"ButtonOverride": "Überschreiben", "ButtonOverride": "Überschreiben",
"ButtonPause": "Pausieren", "ButtonPause": "Pausieren",
@@ -40,18 +40,18 @@
"ButtonPlayEpisode": "Episode abspielen", "ButtonPlayEpisode": "Episode abspielen",
"ButtonPlaylists": "Wiedergabelisten", "ButtonPlaylists": "Wiedergabelisten",
"ButtonRead": "Lesen", "ButtonRead": "Lesen",
"ButtonReadLess": "weniger Anzeigen", "ButtonReadLess": "Weniger Anzeigen",
"ButtonReadMore": "Mehr Anzeigen", "ButtonReadMore": "Mehr Anzeigen",
"ButtonRemove": "Entfernen", "ButtonRemove": "Entfernen",
"ButtonRemoveFromServer": "Vom Server entfernen", "ButtonRemoveFromServer": "Vom Server entfernen",
"ButtonSave": "Speichern", "ButtonSave": "Speichern",
"ButtonSaveOrder": "Reihenfolge speichern", "ButtonSaveOrder": "Reihenfolge speichern",
"ButtonSearch": "Suchen", "ButtonSearch": "Suchen",
"ButtonSendEbookToDevice": "E-Buch an Gerät senden", "ButtonSendEbookToDevice": "E-Book an Gerät senden",
"ButtonSeries": "Serien", "ButtonSeries": "Serien",
"ButtonSetTimer": "Setze Timer", "ButtonSetTimer": "Setze Timer",
"ButtonStream": "Streamen", "ButtonStream": "Streamen",
"ButtonSubmit": "Ok", "ButtonSubmit": "Absenden",
"ButtonSwitchServerUser": "Server/Benutzer wechseln", "ButtonSwitchServerUser": "Server/Benutzer wechseln",
"ButtonUnmaskServerAddress": "Server Adresse anzeigen", "ButtonUnmaskServerAddress": "Server Adresse anzeigen",
"ButtonUserStats": "Nutzerstatistiken", "ButtonUserStats": "Nutzerstatistiken",
@@ -63,17 +63,18 @@
"HeaderChapters": "Kapitel", "HeaderChapters": "Kapitel",
"HeaderCollection": "Sammlung", "HeaderCollection": "Sammlung",
"HeaderCollectionItems": "Sammlungseinträge", "HeaderCollectionItems": "Sammlungseinträge",
"HeaderConfirm": "Bestätigen",
"HeaderConnectionStatus": "Verbindungsstatus", "HeaderConnectionStatus": "Verbindungsstatus",
"HeaderDataSettings": "Daten-Einstellungen", "HeaderDataSettings": "Daten-Einstellungen",
"HeaderDetails": "Details", "HeaderDetails": "Details",
"HeaderDownloads": "Downloads", "HeaderDownloads": "Downloads",
"HeaderEbookFiles": "E-Buch-Dateien", "HeaderEbookFiles": "E-Book-Dateien",
"HeaderEpisodes": "Episoden", "HeaderEpisodes": "Episoden",
"HeaderEreaderSettings": "Einstellungen zum Lesen", "HeaderEreaderSettings": "E-Reader-Einstellungen",
"HeaderLatestEpisodes": "Neueste Episoden", "HeaderLatestEpisodes": "Neueste Episoden",
"HeaderLibraries": "Bibliotheken", "HeaderLibraries": "Bibliotheken",
"HeaderLocalFolders": "Lokale Ordner", "HeaderLocalFolders": "Lokale Ordner",
"HeaderLocalLibraryItems": "Elemente der lokalen Bibliothek", "HeaderLocalLibraryItems": "Objekte in der lokalen Bibliothek",
"HeaderNewPlaylist": "Neue Wiedergabeliste", "HeaderNewPlaylist": "Neue Wiedergabeliste",
"HeaderOpenRSSFeed": "RSS-Feed öffnen", "HeaderOpenRSSFeed": "RSS-Feed öffnen",
"HeaderPlaybackSettings": "Wiedergabe-Einstellungen", "HeaderPlaybackSettings": "Wiedergabe-Einstellungen",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Neueste Ereignisse", "HeaderStatsRecentSessions": "Neueste Ereignisse",
"HeaderTableOfContents": "Inhaltsverzeichnis", "HeaderTableOfContents": "Inhaltsverzeichnis",
"HeaderUserInterfaceSettings": "Einstellungen der Benutzeroberfläche", "HeaderUserInterfaceSettings": "Einstellungen der Benutzeroberfläche",
"HeaderWelcome": "Willkommen, <strong>{0}</strong>",
"HeaderYourStats": "Eigene Statistiken", "HeaderYourStats": "Eigene Statistiken",
"LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen", "LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen",
"LabelAddedAt": "Hinzugefügt am", "LabelAddedAt": "Hinzugefügt am",
@@ -113,13 +115,14 @@
"LabelAutoSleepTimerAutoRewindHelp": "Wenn die Schlummerfunktion 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 eine Schlummerfunktion gestartet.", "LabelAutoSleepTimerHelp": "Bei der Wiedergabe von Medien zwischen der angegebenen Start- und Endzeit wird automatisch eine Schlummerfunktion gestartet.",
"LabelBooks": "Bücher", "LabelBooks": "Bücher",
"LabelByAuthor": "von {0}",
"LabelChapterTrack": "Kapitelspur", "LabelChapterTrack": "Kapitelspur",
"LabelChapters": "Kapitel", "LabelChapters": "Kapitel",
"LabelClosePlayer": "Player schließen", "LabelClosePlayer": "Player schließen",
"LabelCollapseSeries": "Serien einklappen", "LabelCollapseSeries": "Serien einklappen",
"LabelComplete": "Vollständig", "LabelComplete": "Vollständig",
"LabelContinueBooks": "Bücher fortfahren", "LabelContinueBooks": "Bücher fortsetzen",
"LabelContinueEpisodes": "Episoden fortfahren", "LabelContinueEpisodes": "Episoden fortsetzen",
"LabelContinueListening": "Weiterhören", "LabelContinueListening": "Weiterhören",
"LabelContinueReading": "Weiterlesen", "LabelContinueReading": "Weiterlesen",
"LabelContinueSeries": "Serie fortsetzen", "LabelContinueSeries": "Serie fortsetzen",
@@ -155,6 +158,9 @@
"LabelFinished": "Beendet", "LabelFinished": "Beendet",
"LabelFolder": "Ordner", "LabelFolder": "Ordner",
"LabelFontBoldness": "Schriftstärke", "LabelFontBoldness": "Schriftstärke",
"LabelFontFamily": "Schriftfamilie",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serifen",
"LabelFontScale": "Schriftgröße", "LabelFontScale": "Schriftgröße",
"LabelGenre": "Kategorie", "LabelGenre": "Kategorie",
"LabelGenres": "Kategorien", "LabelGenres": "Kategorien",
@@ -174,6 +180,9 @@
"LabelLayout": "Ansicht", "LabelLayout": "Ansicht",
"LabelLayoutAuto": "Auto", "LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Eine Seite", "LabelLayoutSinglePage": "Eine Seite",
"LabelLibrarySortByProgress": "Fortschritt: Letzte Aktualisierung",
"LabelLibrarySortByProgressFinished": "Fortschritt: Beendet",
"LabelLibrarySortByProgressStarted": "Fortschritt: Gestartet",
"LabelLight": "Leicht", "LabelLight": "Leicht",
"LabelLineSpacing": "Zeilenabstand", "LabelLineSpacing": "Zeilenabstand",
"LabelListenAgain": "Erneut anhören", "LabelListenAgain": "Erneut anhören",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Vergangene Zeit anhand der Geschwindigkeit skalieren", "LabelScaleElapsedTimeBySpeed": "Vergangene Zeit anhand der Geschwindigkeit skalieren",
"LabelSeason": "Staffel", "LabelSeason": "Staffel",
"LabelSelectADevice": "Wähle ein Gerät", "LabelSelectADevice": "Wähle ein Gerät",
"LabelSelectMediaType": "Wähle die Art des Mediums",
"LabelSequenceAscending": "Reihenfolge aufsteigend", "LabelSequenceAscending": "Reihenfolge aufsteigend",
"LabelSequenceDescending": "Reihenfolge absteigend", "LabelSequenceDescending": "Reihenfolge absteigend",
"LabelSeries": "Serien", "LabelSeries": "Serien",
@@ -285,12 +295,17 @@
"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 du oder jemand, den du kennst, 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",
"MessageConfirmAppExit": "Möchten Sie die App schließen?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Sind Sie sicher, dass Sie die Warteschlange für Epsioden-Downloads leeren wollen?",
"MessageConfirmDeleteLocalEpisode": "Soll die lokale Episode \"{0}\" von deinem Gerät entfernt werden? 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": "Sollen lokale Dateien dieses Elements von deinem Gerät entfernt werden? 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.",
"MessageConfirmDeleteServerConfig": "Diese Server-Konfiguration entfernen?",
"MessageConfirmDeleteServerEpisode": "Sind Sie sicher, dass Sie die Episode \"{0}\" vom Server löschen wollen?\nWarnung: Damit wird die Audio-Datei gelöscht.",
"MessageConfirmDisableAutoTimer": "Bist du sicher, dass du den automatischen Timer für den Rest des Tages deaktivieren möchtest? Der Timer wird am Ende dieser automatischen Schlaf-Timer-Periode oder beim Neustart der App wieder aktiviert.", "MessageConfirmDisableAutoTimer": "Bist du sicher, dass du den automatischen Timer für den Rest des Tages deaktivieren möchtest? Der Timer wird am Ende dieser automatischen Schlaf-Timer-Periode oder beim Neustart der App wieder aktiviert.",
"MessageConfirmDiscardProgress": "Bist du sicher, dass du deinen Fortschritt zurücksetzen willst?", "MessageConfirmDiscardProgress": "Bist du sicher, dass du deinen Fortschritt zurücksetzen willst?",
"MessageConfirmDownloadUsingCellular": "Du bist dabei, über mobile Daten herunterzuladen. Dies kann zu Gebühren deines Mobilfunkanbieters führen. Möchtest du fortfahren?", "MessageConfirmDownloadUsingCellular": "Du bist dabei, über mobile Daten herunterzuladen. Dies kann zu Gebühren deines Mobilfunkanbieters führen. Möchtest du fortfahren?",
"MessageConfirmMarkAsFinished": "Bist du sicher, dass du diesen Artikel als beendet markieren willst?", "MessageConfirmMarkAsFinished": "Bist du sicher, dass du diesen Artikel als beendet markieren willst?",
"MessageConfirmPlaybackTime": "Wiedergabe von \"{0}\" bei {1} starten?",
"MessageConfirmRemoveBookmark": "Bist du sicher, dass du das Lesezeichen entfernen willst?", "MessageConfirmRemoveBookmark": "Bist du sicher, dass du das Lesezeichen entfernen willst?",
"MessageConfirmStreamingUsingCellular": "Du bist dabei, über mobile Daten zu streamen. Dies kann zu Gebühren deines Mobilfunkanbieters führen. Möchtest du fortfahren?", "MessageConfirmStreamingUsingCellular": "Du bist dabei, über mobile Daten zu streamen. Dies kann zu Gebühren deines Mobilfunkanbieters führen. Möchtest du fortfahren?",
"MessageDiscardProgress": "Fortschritt verwerfen", "MessageDiscardProgress": "Fortschritt verwerfen",
@@ -335,8 +350,8 @@
"MessageProgressSyncFailed": "Der letzte Versuch den aktuellen Hörfortschritt an den Server zu melden ist fehlgeschlagen. Die Anfragen den Fortschritt zu synchronisieren wird alle 15 Sekunden bis 1 Minute versucht, während Medien abgespielt werden.", "MessageProgressSyncFailed": "Der letzte Versuch den aktuellen Hörfortschritt an den Server zu melden ist fehlgeschlagen. Die Anfragen den Fortschritt zu synchronisieren wird alle 15 Sekunden bis 1 Minute versucht, während Medien abgespielt werden.",
"MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und mitwirken", "MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und mitwirken",
"MessageSeriesAlreadyDownloaded": "Du hast bereits alle Bücher in dieser Serie heruntergeladen.", "MessageSeriesAlreadyDownloaded": "Du hast bereits alle Bücher in dieser Serie heruntergeladen.",
"MessageSeriesDownloadConfirm": "Lade {0} Bücher mit {1} Daten, zusammen {2}, in den Ordner {3} herunter?", "MessageSeriesDownloadConfirm": "Fehlende(s) {0} Buch/Bücher mit {1} Datei(en), insgesamt {2}, in den Ordner {3} herunterladen?",
"MessageSeriesDownloadConfirmIos": "Lade {0} Bücher mit {1} Daten, zusammen {2}, herunter?", "MessageSeriesDownloadConfirmIos": "Fehlende(s) {0} Buch/Bücher mit {1} Datei(en), insgesamt {2}, herunterladen?",
"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",
@@ -355,5 +370,7 @@
"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" "ToastStreamingNotAllowedOnCellular": "Das Streamen über mobile Daten ist nicht erlaubt",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+233
View File
@@ -0,0 +1,233 @@
{
"ButtonAdd": "Προσθήκη",
"ButtonAddNewServer": "Προσθήκη Νέου Διακομιστή",
"ButtonAuthors": "Συγγραφείς",
"ButtonBack": "Πίσω",
"ButtonCancel": "Ακύρωση",
"ButtonCancelTimer": "Ακύρωση Χρονόμετρου",
"ButtonClearFilter": "Διαγραφή Φίλτρου",
"ButtonClearLogs": "Διαγραφή Αρχείων Καταγραφής",
"ButtonCloseFeed": "Κλείσιμο Τροφοδοσίας",
"ButtonCollections": "Συλλογές",
"ButtonConnect": "Σύνδεση",
"ButtonConnectToServer": "Σύνδεση σε Διακομιστή",
"ButtonCreate": "Δημιουργία",
"ButtonCreateBookmark": "Δημιουργία Σελιδοδείκτη",
"ButtonCreateNewPlaylist": "Δημιουργία Νέας Λίστας Αναπαραγωγής",
"ButtonDelete": "Διαγραφή",
"ButtonDeleteLocalEpisode": "Διαγραφή τοπικού επεισοδίου",
"ButtonDeleteLocalFile": "Διαγραφή τοπικού αρχείου",
"ButtonDeleteLocalItem": "Διαγραφή τοπικού αντικειμένου",
"ButtonDisableAutoTimer": "Απενεργοποίηση Αυτόματου Χρονόμετρου",
"ButtonDisconnect": "Αποσύνδεση Σύνδσης",
"ButtonGoToWebClient": "Μετάβαση στον Web Client",
"ButtonHistory": "Ιστορικό",
"ButtonHome": "Αρχική",
"ButtonIssues": "Θέματα",
"ButtonLatest": "Τελευταία",
"ButtonLibrary": "Βιβλιοθήκη",
"ButtonLocalMedia": "Τοπικά Μέσα",
"ButtonLogs": "Αρχεία Καταγραφής",
"ButtonManageLocalFiles": "Διαχείριση Τοπικών Αρχείων",
"ButtonMaskServerAddress": "Απόκρυψη διέθυνσης διακομιστή",
"ButtonNewFolder": "Νέος Φάκελος",
"ButtonNextEpisode": "Επόμενο Επεισόδιο",
"ButtonOk": "Εντάξει",
"ButtonOpenFeed": "Άνοιγμα Τροφοδοσίας",
"ButtonOverride": "Παράκαμψη",
"ButtonPause": "Παύση",
"ButtonPlay": "Αναπαραγωγή",
"ButtonPlayEpisode": "Αναπαραγωγή Επεισοδίου",
"ButtonPlaylists": "Λίστες Αναπαραγωγής",
"ButtonRead": "Ανάγνωση",
"ButtonReadLess": "Ανάγνωση λιγότερων",
"ButtonReadMore": "Διάβασε περισσότερα",
"ButtonRemove": "Αφαίρεση",
"ButtonRemoveFromServer": "Αφαίρεση απ'τον Διακομιστή",
"ButtonSave": "Αποθήκευση",
"ButtonSaveOrder": "Αποθήκευση Ταξινόμησης",
"ButtonSearch": "Αναζήτηση",
"ButtonSendEbookToDevice": "Αποστολή Ebook σε Συσκευή",
"ButtonSeries": "Σειρά",
"ButtonSetTimer": "Ορισμός Χρονοδιακόπτη",
"ButtonStream": "Μετάδοση",
"ButtonSubmit": "Υποβολή",
"ButtonSwitchServerUser": "Αλλαγή Διακομιστή/Χρήστη",
"ButtonUnmaskServerAddress": "Αφαίρεση απόκρυψης διακομιστή",
"ButtonUserStats": "Στατιστικά Χρήστη",
"ButtonYes": "Ναι",
"HeaderAccount": "Λογαριασμός",
"HeaderAdvanced": "Για Προχωρημένους",
"HeaderAndroidAutoSettings": "Αυτόματες Ρυθμίσεις Διαχειριστή",
"HeaderAudioTracks": "Κομμάτια Ήχου",
"HeaderChapters": "Κεφάλαια",
"HeaderCollection": "Συλλογή",
"HeaderCollectionItems": "Αντικείμενα Συλλογής",
"HeaderConfirm": "Επιβεβαίωση",
"HeaderConnectionStatus": "Στατιστικά Σύνδεσης",
"HeaderDataSettings": "Ρυθμίσεις Δεδομένων",
"HeaderDetails": "Λεπτομέρειες",
"HeaderDownloads": "Λήψεις",
"HeaderEbookFiles": "Αρχεία Ebook",
"HeaderEpisodes": "Επεισόδια",
"HeaderEreaderSettings": "Ρυθμίσεις Ereader",
"HeaderLatestEpisodes": "Τελευταία Επεισόδια",
"HeaderLibraries": "Βιβλιοθήκες",
"HeaderLocalFolders": "Τοπικοί Φάκελοι",
"HeaderLocalLibraryItems": "Αντικείμενα Τοπικής Βιβλιοθήκης",
"HeaderNewPlaylist": "Νέα Λίστα Αναπαραγωγής",
"HeaderOpenRSSFeed": "Άνοιγμα Τροφοδοσίας RSS",
"HeaderPlaybackSettings": "Ρυθμίσεις Αναπαραγωγής",
"HeaderPlaylist": "Λίστα Αναπαραγωγής",
"HeaderPlaylistItems": "Αντικείμενα Λίστας Αναπαραγωγής",
"HeaderProgressSyncFailed": "Αποτυχία Συγχρονισμού Προόδου",
"HeaderRSSFeed": "Τροφοδοσία RSS",
"HeaderRSSFeedGeneral": "Λεπτομέρειες RSS",
"HeaderRSSFeedIsOpen": "Η Τροφοδοσία RSS είναι Ανοιχτή",
"HeaderSelectDownloadLocation": "Επιλογή Τοποθεσίας Λήψης",
"HeaderSettings": "Ρυθμίσεις",
"HeaderSleepTimer": "Χρονοδιακόπτης Ύπνου",
"HeaderSleepTimerSettings": "Ρυθμίσεις Χρονοδιακόπτη Ύπνου",
"HeaderStatsMinutesListeningChart": "Λεπτά Ακρόασης (τελευταίες 7 ημέρες)",
"HeaderStatsRecentSessions": "Πρόσφατες Συνεδρίες",
"HeaderTableOfContents": "Πίνακας Περιεχομένων",
"HeaderUserInterfaceSettings": "Ρυθμίσεις Διεπαφής Χρήστη",
"HeaderWelcome": "Καλωσήρθες, <strong>{0}</strong>",
"HeaderYourStats": "Τα Στατιστικά Σας",
"LabelAddToPlaylist": "Προσθήκη στην Λίστα Αναπαραγωγής",
"LabelAddedAt": "Προστέθηκε Στις",
"LabelAddedDate": "Προστέθηκε {0}",
"LabelAll": "Όλα",
"LabelAllowSeekingOnMediaControls": "Να επιτρέπεται η αλλαγή θέσης από τα χειριστήρια ειδοποιήσεων πολυμέσων",
"LabelAlways": "Πάντα",
"LabelAndroidAutoBrowseLimitForGrouping": "Όριο λίστας κατά αλφαβητική σειρά",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "Μην χρησιμοποιείτε την αλφαβητική λίστα περιορισμού όταν υπάρχουν λιγότερα από αυτά τα στοιχεία για εμφάνιση",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "Ταξινόμηση βιβλίων σειράς",
"LabelAskConfirmation": "Ερώτηση για επιβεβαίωση",
"LabelAuthor": "Συγγραφέας",
"LabelAuthorFirstLast": "Συγγραφέας (Όνομα Επώνυμο)",
"LabelAuthorLastFirst": "Συγγραφέας (Επώνυμο, Όνομα)",
"LabelAuthors": "Συγγραφείς",
"LabelAutoDownloadEpisodes": "Αυτόματο Κατέβασμα Επεισοδίων",
"LabelAutoRewindTime": "Χρόνος αυτόματης επαναφοράς",
"LabelAutoSleepTimer": "Αυτόματος Χρονοδιακόπτης Ύπνου",
"LabelAutoSleepTimerAutoRewind": "Αυτόματη επαναφορά κατά τον χρονοδιακόπτη ύπνου",
"LabelAutoSleepTimerAutoRewindHelp": "Όταν ολοκληρωθεί ο αυτόματος χρονοδιακόπτης ύπνου, η επαναφορά της αναπαραγωγής θα γυρίζει αυτόματα τη θέση σας πίσω.",
"LabelAutoSleepTimerHelp": "Όταν αναπαράγεται περιεχόμενο μεταξύ των καθορισμένων χρόνων έναρξης και λήξης, ο χρονοδιακόπτης ύπνου θα ξεκινά αυτόματα.",
"LabelBooks": "Βιβλία",
"LabelByAuthor": "κατά {0}",
"LabelChapterTrack": "Κομμάτι Κεφαλαίου",
"LabelChapters": "Κεφάλαια",
"LabelClosePlayer": "Κλείσιμο αναπαραγωγής",
"LabelCollapseSeries": "Σύμπτυξη Σειράς",
"LabelComplete": "Ολοκλήρωση",
"LabelContinueBooks": "Συνέχεια Βιβλίων",
"LabelContinueEpisodes": "Συνέχεια Επεισοδίων",
"LabelContinueListening": "Συνέχεια Ακρόασης",
"LabelContinueReading": "Συνέχεια Ανάγνωσης",
"LabelContinueSeries": "Συνέχεια Σειράς",
"LabelCustomTime": "Προσαρμοσμένη ώρα",
"LabelDescription": "Περιγραφή",
"LabelDisableAudioFadeOut": "Απενεργοποίηση σταδιακής μείωσης ήχου",
"LabelDisableAudioFadeOutHelp": "Η ένταση του ήχου θα αρχίσει να μειώνεται όταν απομένει λιγότερο από 1 λεπτό στον χρονοδιακόπτη ύπνου. Ενεργοποιήστε αυτή τη ρύθμιση για να μην γίνεται σταδιακή μείωση.",
"LabelDisableAutoRewind": "Απενεργοποίηση αυτόματης επαναφοράς",
"LabelDownload": "Λήψη",
"LabelDownloadUsingCellular": "Λήψη με χρήση δεδομένων",
"LabelDownloaded": "Ληφθέντα",
"LabelDuration": "Διάρκεια",
"LabelEbook": "Ebook",
"LabelEbooks": "Ebooks",
"LabelEnable": "Ενεργοποίηση",
"LabelEnd": "Τέλος",
"LabelEndOfChapter": "Τέλος Κεφαλαίου",
"LabelEpisode": "Επεισόδιο",
"LabelFile": "Αρχείο",
"LabelFilename": "Όνομα Αρχείου",
"LabelFinished": "Ολοκληρώθηκε",
"LabelFolder": "Φάκελος",
"LabelFontFamily": "Οικογένεια Γραμματοσειράς",
"LabelGenre": "Είδος",
"LabelGenres": "Είδη",
"LabelHost": "Διακομιστής",
"LabelInProgress": "Σε Εξέλιξη",
"LabelLanguage": "Γλώσσα",
"LabelLayoutAuto": "Αυτόματα",
"LabelLayoutSinglePage": "Μονή Σελίδα",
"LabelLight": "Ανοιχτό",
"LabelListenAgain": "Επανάληψη Ακρόασης",
"LabelLocalBooks": "Τοπικά Βιβλία",
"LabelLocalPodcasts": "Τοπικά Podcasts",
"LabelLow": "Χαμηλά",
"LabelMediaType": "Τύπος Πολυμέσων",
"LabelMore": "Περισσότερα",
"LabelMoreInfo": "Περισσότερες Πληροφορίες",
"LabelName": "Όνομα",
"LabelNarrator": "Αφηγητής",
"LabelNarrators": "Αφηγητές",
"LabelNavigateWithVolumeMirrored": "Καθρεπτισμός",
"LabelNavigateWithVolumeWhilePlayingDisabled": "Ανενεργό",
"LabelNavigateWithVolumeWhilePlayingEnabled": "Ενεργό",
"LabelNever": "Ποτέ",
"LabelNewestAuthors": "Πρόσφατοι Συγγραφείς",
"LabelNewestEpisodes": "Πρόσφατα Επεισόδια",
"LabelNo": "Όχι",
"LabelNotStarted": "Δεν Έχει Ξεκινήσει",
"LabelNumEpisodes": "{0} επεισόδια",
"LabelNumberOfEpisodes": "# Επεισοδίων",
"LabelOff": "Ανενεργό",
"LabelOn": "Ενεργό",
"LabelPassword": "Κωδικός Πρόσβασης",
"LabelPath": "Διαδρομή",
"LabelPlaybackLocal": "Τοπικά",
"LabelPlaybackSpeed": "Ταχύτητα Αναπαραγωγής",
"LabelProgress": "Πρόοδος",
"LabelPublishYear": "Χρονολογία Έκδοσης",
"LabelPublishedDate": "Εκδόθηκε {0}",
"LabelRandomly": "Τυχαία",
"LabelRead": "Ανάγνωση",
"LabelReadAgain": "Ανάγνωση Ξανά",
"LabelRecentSeries": "Πρόσφατη Σειρά",
"LabelRecentlyAdded": "Προστέθηκαν Πρόσφατα",
"LabelRemoveFromPlaylist": "Αφαίρεση από την Λίστα Αναπαραγωγής",
"LabelSelectADevice": "Επιλογή συσκευής",
"LabelSeries": "Σειρά",
"LabelServerAddress": "Διεύθυνση Διακομιστή",
"LabelSetEbookAsPrimary": "Ορισμός ως πρωτεύων",
"LabelShowAll": "Εμφάνιση Όλων",
"LabelSize": "Μέγεθος",
"LabelSleepTimer": "Χρονοδιακόπτης Ύπνου",
"LabelStart": "Έναρξη",
"LabelStatsBestDay": "Καλύτερη Ημέρα",
"LabelStatsDailyAverage": "Ημερήσιος Μέσος Όρος",
"LabelStatsDays": "Ημέρες",
"LabelStatsDaysListened": "Ημέρες Ακρόασης",
"LabelStatsInARow": "Σε σειρά",
"LabelStatsItemsFinished": "Ολοκληρωμένα Αντικείμενα",
"LabelStatsMinutes": "λεπτά",
"LabelStatsMinutesListening": "Λεπτά Ακρόασης",
"LabelStatsWeekListening": "Εβδομαδιαία Ακρόαση",
"LabelTheme": "Θέμα",
"LabelThemeBlack": "Μαύρο",
"LabelThemeDark": "Σκοτεινό",
"LabelThemeLight": "Φωτεινό",
"LabelTimeRemaining": "{0} απομένουν",
"LabelTitle": "Τίτλος",
"LabelTotalSize": "Συνολικό Μέγεθος",
"LabelTracks": "Κομμάτια",
"LabelType": "Τύπος",
"LabelUnknown": "Άγνωστο",
"LabelUseBookshelfView": "Χρήση προβολής βιβλιοθήκης",
"LabelUser": "Χρήστης",
"LabelUsername": "Όνομα Χρήστη",
"LabelVeryHigh": "Πολύ Ψηλά",
"LabelVeryLow": "Πολύ Χαμηλά",
"LabelYourProgress": "Η Πρόοδος Σας",
"MessageDownloadCompleteProcessing": "Η λήψη ολοκληρώθηκε. Επεξεργασία...",
"MessageDownloading": "Λήψη...",
"MessageDownloadingEpisode": "Λήψη επεισοδίου",
"MessageLoading": "Φόρτωση...",
"MessageMarkAsFinished": "Σήμανση ως Ολοκληρωμένο",
"MessageNoItemsFound": "Δεν βρέθηκαν αντικείμενα",
"MessageNoNetworkConnection": "Δεν υπάρχει σύνδεση δικτύου",
"MessageNoUserPlaylists": "Δεν έχετε λίστες αναπαραγωγής"
}
+18 -1
View File
@@ -63,6 +63,7 @@
"HeaderChapters": "Chapters", "HeaderChapters": "Chapters",
"HeaderCollection": "Collection", "HeaderCollection": "Collection",
"HeaderCollectionItems": "Collection Items", "HeaderCollectionItems": "Collection Items",
"HeaderConfirm": "Confirm",
"HeaderConnectionStatus": "Connection Status", "HeaderConnectionStatus": "Connection Status",
"HeaderDataSettings": "Data Settings", "HeaderDataSettings": "Data Settings",
"HeaderDetails": "Details", "HeaderDetails": "Details",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Recent Sessions", "HeaderStatsRecentSessions": "Recent Sessions",
"HeaderTableOfContents": "Table of Contents", "HeaderTableOfContents": "Table of Contents",
"HeaderUserInterfaceSettings": "User Interface Settings", "HeaderUserInterfaceSettings": "User Interface Settings",
"HeaderWelcome": "Welcome, <strong>{0}</strong>",
"HeaderYourStats": "Your Stats", "HeaderYourStats": "Your Stats",
"LabelAddToPlaylist": "Add to Playlist", "LabelAddToPlaylist": "Add to Playlist",
"LabelAddedAt": "Added At", "LabelAddedAt": "Added At",
@@ -113,6 +115,7 @@
"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",
"LabelByAuthor": "by {0}",
"LabelChapterTrack": "Chapter Track", "LabelChapterTrack": "Chapter Track",
"LabelChapters": "Chapters", "LabelChapters": "Chapters",
"LabelClosePlayer": "Close player", "LabelClosePlayer": "Close player",
@@ -155,6 +158,9 @@
"LabelFinished": "Finished", "LabelFinished": "Finished",
"LabelFolder": "Folder", "LabelFolder": "Folder",
"LabelFontBoldness": "Font Boldness", "LabelFontBoldness": "Font Boldness",
"LabelFontFamily": "Font family",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Font scale", "LabelFontScale": "Font scale",
"LabelGenre": "Genre", "LabelGenre": "Genre",
"LabelGenres": "Genres", "LabelGenres": "Genres",
@@ -174,6 +180,9 @@
"LabelLayout": "Layout", "LabelLayout": "Layout",
"LabelLayoutAuto": "Auto", "LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page", "LabelLayoutSinglePage": "Single page",
"LabelLibrarySortByProgress": "Progress: Last Updated",
"LabelLibrarySortByProgressFinished": "Progress: Finished",
"LabelLibrarySortByProgressStarted": "Progress: Started",
"LabelLight": "Light", "LabelLight": "Light",
"LabelLineSpacing": "Line spacing", "LabelLineSpacing": "Line spacing",
"LabelListenAgain": "Listen Again", "LabelListenAgain": "Listen Again",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed", "LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Season", "LabelSeason": "Season",
"LabelSelectADevice": "Select a device", "LabelSelectADevice": "Select a device",
"LabelSelectMediaType": "Select media type",
"LabelSequenceAscending": "Sequence Ascending", "LabelSequenceAscending": "Sequence Ascending",
"LabelSequenceDescending": "Sequence Descending", "LabelSequenceDescending": "Sequence Descending",
"LabelSeries": "Series", "LabelSeries": "Series",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf server not connected", "MessageAudiobookshelfServerNotConnected": "Audiobookshelf server not connected",
"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> 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.",
"MessageBookshelfEmpty": "Bookshelf empty", "MessageBookshelfEmpty": "Bookshelf empty",
"MessageConfirmAppExit":"Did you want to exit the app?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Are you sure you want to clear episode download queue?",
"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.",
"MessageConfirmDeleteServerConfig": "Remove this server config?",
"MessageConfirmDeleteServerEpisode": "Are you sure you want to delete episode \"{0}\" from the server?\nWarning: This will delete the audio file.",
"MessageConfirmDisableAutoTimer": "Are you sure you want to disable the auto timer for the rest of today? The timer will be re-enabled at the end of this auto-sleep timer period, or if you restart the app.", "MessageConfirmDisableAutoTimer": "Are you sure you want to disable the auto timer for the rest of today? The timer will be re-enabled at the end of this auto-sleep timer period, or if you restart the app.",
"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?", "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?",
"MessageConfirmPlaybackTime":"Start playback for \"{0}\" at {1}?",
"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?", "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",
@@ -355,5 +370,7 @@
"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" "ToastStreamingNotAllowedOnCellular": "Streaming is not allowed on cellular data",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+85 -63
View File
@@ -5,7 +5,7 @@
"ButtonBack": "Atrás", "ButtonBack": "Atrás",
"ButtonCancel": "Cancelar", "ButtonCancel": "Cancelar",
"ButtonCancelTimer": "Cancelar temporizador", "ButtonCancelTimer": "Cancelar temporizador",
"ButtonClearFilter": "Quitar filtros", "ButtonClearFilter": "Vaciar filtro",
"ButtonClearLogs": "Vaciar registros", "ButtonClearLogs": "Vaciar registros",
"ButtonCloseFeed": "Cerrar suministro", "ButtonCloseFeed": "Cerrar suministro",
"ButtonCollections": "Colecciones", "ButtonCollections": "Colecciones",
@@ -18,16 +18,16 @@
"ButtonDeleteLocalEpisode": "Eliminar episodio local", "ButtonDeleteLocalEpisode": "Eliminar episodio local",
"ButtonDeleteLocalFile": "Eliminar archivo local", "ButtonDeleteLocalFile": "Eliminar archivo local",
"ButtonDeleteLocalItem": "Eliminar elemento local", "ButtonDeleteLocalItem": "Eliminar elemento local",
"ButtonDisableAutoTimer": "Desactivar temporizador automático", "ButtonDisableAutoTimer": "Inhabilitar temporizador automático",
"ButtonDisconnect": "Desconectar", "ButtonDisconnect": "Desconectar",
"ButtonGoToWebClient": "Ir al cliente web", "ButtonGoToWebClient": "Ir al cliente web",
"ButtonHistory": "Historial", "ButtonHistory": "Historial",
"ButtonHome": "Inicio", "ButtonHome": "Inicio",
"ButtonIssues": "Problemas", "ButtonIssues": "Incidencias",
"ButtonLatest": "Más recientes", "ButtonLatest": "Más recientes",
"ButtonLibrary": "Biblioteca", "ButtonLibrary": "Biblioteca",
"ButtonLocalMedia": "Medios locales", "ButtonLocalMedia": "Medios locales",
"ButtonLogs": "Registros", "ButtonLogs": "Bitácoras",
"ButtonManageLocalFiles": "Gestionar archivos locales", "ButtonManageLocalFiles": "Gestionar archivos locales",
"ButtonMaskServerAddress": "Enmascarar dirección de servidor", "ButtonMaskServerAddress": "Enmascarar dirección de servidor",
"ButtonNewFolder": "Carpeta nueva", "ButtonNewFolder": "Carpeta nueva",
@@ -49,102 +49,105 @@
"ButtonSearch": "Buscar", "ButtonSearch": "Buscar",
"ButtonSendEbookToDevice": "Enviar libro al dispositivo", "ButtonSendEbookToDevice": "Enviar libro al dispositivo",
"ButtonSeries": "Series", "ButtonSeries": "Series",
"ButtonSetTimer": "Ajustar el temporizador", "ButtonSetTimer": "Ajustar cronómetro",
"ButtonStream": "En directo", "ButtonStream": "En directo",
"ButtonSubmit": "Enviar", "ButtonSubmit": "Entregar",
"ButtonSwitchServerUser": "Cambiar servidor/usuario", "ButtonSwitchServerUser": "Cambiar servidor/usuario",
"ButtonUnmaskServerAddress": "Desenmascarar dirección de servidor", "ButtonUnmaskServerAddress": "Desenmascarar dirección de servidor",
"ButtonUserStats": "Estadísticas de usuario", "ButtonUserStats": "Estadísticas de usuario",
"ButtonYes": "Sí", "ButtonYes": "Sí",
"HeaderAccount": "Cuenta", "HeaderAccount": "Cuenta",
"HeaderAdvanced": "Avanzado", "HeaderAdvanced": "Avanzado",
"HeaderAndroidAutoSettings": "Configuración de Android Auto", "HeaderAndroidAutoSettings": "Ajustes automáticos de Android",
"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 de colección",
"HeaderConnectionStatus": "Estado de la conexión", "HeaderConfirm": "Confirmar",
"HeaderDataSettings": "Configuración de datos", "HeaderConnectionStatus": "Estado de conexión",
"HeaderDataSettings": "Ajustes de datos",
"HeaderDetails": "Detalles", "HeaderDetails": "Detalles",
"HeaderDownloads": "Descargas", "HeaderDownloads": "Descargas",
"HeaderEbookFiles": "Archivos de libros digitales", "HeaderEbookFiles": "Archivos de libros digitales",
"HeaderEpisodes": "Episodios", "HeaderEpisodes": "Episodios",
"HeaderEreaderSettings": "Configuración del lector", "HeaderEreaderSettings": "Ajustes del Lector-e",
"HeaderLatestEpisodes": "Episodios más recientes", "HeaderLatestEpisodes": "Episodios más recientes",
"HeaderLibraries": "Bibliotecas", "HeaderLibraries": "Bibliotecas",
"HeaderLocalFolders": "Carpetas locales", "HeaderLocalFolders": "Carpetas locales",
"HeaderLocalLibraryItems": "Elementos de la biblioteca local", "HeaderLocalLibraryItems": "Elementos de Biblioteca Local",
"HeaderNewPlaylist": "Nueva lista de reproducción", "HeaderNewPlaylist": "Nueva lista de reproducción",
"HeaderOpenRSSFeed": "Abrir suministro RSS", "HeaderOpenRSSFeed": "Abrir suministro RSS",
"HeaderPlaybackSettings": "Configuración de reproducción", "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",
"HeaderProgressSyncFailed": "Falló la sincronización del progreso", "HeaderProgressSyncFailed": "Progreso de sincronización incorrecto",
"HeaderRSSFeed": "Suministro RSS", "HeaderRSSFeed": "Suministro RSS",
"HeaderRSSFeedGeneral": "Detalles de RSS", "HeaderRSSFeedGeneral": "Detalles de RSS",
"HeaderRSSFeedIsOpen": "El suministro RSS está abierto", "HeaderRSSFeedIsOpen": "El suministro RSS está abierto",
"HeaderSelectDownloadLocation": "Seleccionar ubicación de descarga", "HeaderSelectDownloadLocation": "Seleccionar lugar de descarga",
"HeaderSettings": "Configuración", "HeaderSettings": "Ajustes",
"HeaderSleepTimer": "Temporizador de apagado", "HeaderSleepTimer": "Cronómetro de dormida",
"HeaderSleepTimerSettings": "Ajustes del temporizador para dormir", "HeaderSleepTimerSettings": "Ajustes del temporizador para dormida",
"HeaderStatsMinutesListeningChart": "Minutos escuchando (últimos 7 días)", "HeaderStatsMinutesListeningChart": "Minutos escuchando (últimos 7 días)",
"HeaderStatsRecentSessions": "Sesiones recientes", "HeaderStatsRecentSessions": "Sesiones recientes",
"HeaderTableOfContents": "Sumario", "HeaderTableOfContents": "Sumario",
"HeaderUserInterfaceSettings": "Configuración de interfaz de usuario", "HeaderUserInterfaceSettings": "Ajustes de interfaz de usuario",
"HeaderWelcome": "Bienvenido/a, <strong>{0}</strong>",
"HeaderYourStats": "Sus estadísticas", "HeaderYourStats": "Sus estadísticas",
"LabelAddToPlaylist": "Añadir a lista de reproducción", "LabelAddToPlaylist": "Añadir a lista de reproducción",
"LabelAddedAt": "Añadido", "LabelAddedAt": "Añadido en",
"LabelAddedDate": "{0} Añadido", "LabelAddedDate": "Añadido {0}",
"LabelAll": "Todos", "LabelAll": "Todos",
"LabelAllowSeekingOnMediaControls": "Permitir la búsqueda de posición en los controles de notificación de medios", "LabelAllowSeekingOnMediaControls": "Concede la búsqueda de posición en los controles de notificación de medios",
"LabelAlways": "Siempre", "LabelAlways": "Siempre",
"LabelAndroidAutoBrowseLimitForGrouping": "Limite del despliegue alfabético", "LabelAndroidAutoBrowseLimitForGrouping": "Límite del despliegue alfabético",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "No utilice el despliegue alfabético cuando haya menos de esta cantidad de elementos para mostrar", "LabelAndroidAutoBrowseLimitForGroupingHelp": "No utilice el despliegue alfabético cuando haya menos de esta cantidad de elementos para mostrar",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "Orden de libros de Series", "LabelAndroidAutoBrowseSeriesSequenceOrder": "Ordenación de libros de Series",
"LabelAskConfirmation": "Pedir confirmación", "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": "AutoDescargar episodios",
"LabelAutoRewindTime": "Tiempo de rebobinado automático", "LabelAutoRewindTime": "Tiempo de autorebobinado",
"LabelAutoSleepTimer": "Temporizador de apagado automático", "LabelAutoSleepTimer": "Cronómetro de autoapagado",
"LabelAutoSleepTimerAutoRewind": "Temporizador de apagado automático con rebobinado automático", "LabelAutoSleepTimerAutoRewind": "Cronómetro de autoapagado con autorebobinado",
"LabelAutoSleepTimerAutoRewindHelp": "Cuando el temporizador de auto apagado finaliza, reproducir el elemento nuevamente rebobinará automáticamente tu posición.", "LabelAutoSleepTimerAutoRewindHelp": "Cuando el cronómetro de autoapagado finaliza, reproducir el elemento nuevamente autorebobinará tu posición.",
"LabelAutoSleepTimerHelp": "Cuando se reproduce contenido multimedia entre las horas de inicio y finalización especificadas, se activará automáticamente un temporizador de apagado.", "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",
"LabelByAuthor": "por {0}",
"LabelChapterTrack": "Seguimiento de Capítulo", "LabelChapterTrack": "Seguimiento de Capítulo",
"LabelChapters": "Capítulos", "LabelChapters": "Capítulos",
"LabelClosePlayer": "Cerrar reproductor", "LabelClosePlayer": "Cerrar reproductor",
"LabelCollapseSeries": "Colapsar serie", "LabelCollapseSeries": "Colapsar Series",
"LabelComplete": "Completo", "LabelComplete": "Completo",
"LabelContinueBooks": "Continuar libros", "LabelContinueBooks": "Continuar libros",
"LabelContinueEpisodes": "Continuar episodios", "LabelContinueEpisodes": "Continuar Episodios",
"LabelContinueListening": "Seguir escuchando", "LabelContinueListening": "Seguir Escuchando",
"LabelContinueReading": "Continuar leyendo", "LabelContinueReading": "Continuar leyendo",
"LabelContinueSeries": "Continuar series", "LabelContinueSeries": "Continuar series",
"LabelCustomTime": "Tiempo personalizado", "LabelCustomTime": "Tiempo personalizado",
"LabelDescription": "Descripción", "LabelDescription": "Descripción",
"LabelDisableAudioFadeOut": "Desactivar el fundido de audio", "LabelDisableAudioFadeOut": "Desactivar el fundido de audio",
"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.", "LabelDisableAudioFadeOutHelp": "El volumen de audio comenzará a disminuir cuando quede menos de 1 minuto en el temporizador de dormida. Habilite este ajuste para que no se desvanezca.",
"LabelDisableAutoRewind": "Desactivar rebobinado automático", "LabelDisableAutoRewind": "Desactivar autorebobinado",
"LabelDisableShakeToReset": "Desactivar agitar para reiniciar", "LabelDisableShakeToReset": "Desactivar agitar para restablecer",
"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.", "LabelDisableShakeToResetHelp": "Si agitas el dispositivo mientras el temporizador está en marcha o en los 2 minutos siguientes a la caducidad del temporizador, éste se reiniciará. Habilita estos ajustes para desactivar el restablecimiento al agitar.",
"LabelDisableVibrateOnReset": "Desactivar vibración al reiniciar", "LabelDisableVibrateOnReset": "Desactivar vibración al reiniciar",
"LabelDisableVibrateOnResetHelp": "Cuando el temporizador de apagado se reinicia, el dispositivo vibra. Activa esta opción para que no vibre cuando se reinicie el temporizador.", "LabelDisableVibrateOnResetHelp": "Cuando el temporizador de apagado se reinicia, el dispositivo vibra. Activa esta opción para que no vibre cuando se duerme el temporizador.",
"LabelDiscover": "Descubrir", "LabelDiscover": "Descubrir",
"LabelDownload": "Descargar", "LabelDownload": "Descargar",
"LabelDownloadUsingCellular": "Descargar usando el móvil", "LabelDownloadUsingCellular": "Descargar usando el móvil",
"LabelDownloaded": "Descargado", "LabelDownloaded": "Descargado",
"LabelDuration": "Duración", "LabelDuration": "Duración",
"LabelEbook": "Libro electrónico", "LabelEbook": "Libro-e",
"LabelEbooks": "Libros electrónicos", "LabelEbooks": "Libros-e",
"LabelEnable": "Activar", "LabelEnable": "Activar",
"LabelEnableMp3IndexSeeking": "Activar la búsqueda de índices mp3", "LabelEnableMp3IndexSeeking": "Activar la búsqueda de índices mp3",
"LabelEnableMp3IndexSeekingHelp": "Esta configuración solo debe activarse si tiene archivos MP3 en los que no se puede buscar correctamente. La búsqueda inexacta probablemente se deba a archivos MP3 con tasas de bits variables (VBR). Esta configuración forzará la búsqueda de índice, en la que se construye una asignación 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.", "LabelEnableMp3IndexSeekingHelp": "Estos ajustes solo estaría activado si tiene archivos mp3 en los que no se puede buscar correctamente. La búsqueda inexacta probablemente se deba a archivos mp3 con tasas de los bit variables (VBR). Este ajuste forzará la búsqueda de índice, en la que se construye una asignación 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": "Fin del capítulo", "LabelEndOfChapter": "Fin del capítulo",
"LabelEndTime": "Hora de finalización", "LabelEndTime": "Tiempo de finalización",
"LabelEpisode": "Episodio", "LabelEpisode": "Episodio",
"LabelExplicit": "Explícito", "LabelExplicit": "Explícito",
"LabelFeedURL": "URL del suministro", "LabelFeedURL": "URL del suministro",
@@ -152,35 +155,41 @@
"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": "Finalizado",
"LabelFolder": "Carpeta", "LabelFolder": "Carpeta",
"LabelFontBoldness": "Peso tipográfico", "LabelFontBoldness": "Tipográfico sin Negrita",
"LabelFontFamily": "Familia tipográfica",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Escala de letra", "LabelFontScale": "Escala de letra",
"LabelGenre": "Género", "LabelGenre": "Género",
"LabelGenres": "Géneros", "LabelGenres": "Géneros",
"LabelHapticFeedback": "Respuesta háptica", "LabelHapticFeedback": "Comentario háptico",
"LabelHasEbook": "Tiene un libro", "LabelHasEbook": "Tiene libro-e",
"LabelHasSupplementaryEbook": "Tiene un libro complementario", "LabelHasSupplementaryEbook": "Tiene un libro-e suplementario",
"LabelHeavy": "Pesado", "LabelHeavy": "Denso",
"LabelHigh": "Alto", "LabelHigh": "Alto",
"LabelHost": "Anfitrión", "LabelHost": "Anfitrión",
"LabelInProgress": "En proceso", "LabelInProgress": "En proceso",
"LabelIncomplete": "Incompleto", "LabelIncomplete": "Incompleto",
"LabelInternalAppStorage": "Almacenamiento interno de aplicaciones", "LabelInternalAppStorage": "Almacenamiento Interno de App",
"LabelJumpBackwardsTime": "Saltar atrás en el tiempo", "LabelJumpBackwardsTime": "Volver en el tiempo",
"LabelJumpForwardsTime": "Salto adelante en el tiempo", "LabelJumpForwardsTime": "Avanzar en el tiempo",
"LabelKeepScreenAwake": "Mantener la pantalla encendida", "LabelKeepScreenAwake": "Mantener la pantalla encendida",
"LabelLanguage": "Idioma", "LabelLanguage": "Idioma",
"LabelLayout": "Disposición", "LabelLayout": "Disposición",
"LabelLayoutAuto": "Automático", "LabelLayoutAuto": "Automático",
"LabelLayoutSinglePage": "Página única", "LabelLayoutSinglePage": "Página única",
"LabelLibrarySortByProgress": "Progreso: Último actualizado",
"LabelLibrarySortByProgressFinished": "Progreso: Finalizado",
"LabelLibrarySortByProgressStarted": "Progreso: Iniciado",
"LabelLight": "Claro", "LabelLight": "Claro",
"LabelLineSpacing": "Interlineado", "LabelLineSpacing": "Interlineado",
"LabelListenAgain": "Volver a escuchar", "LabelListenAgain": "Volver a escuchar",
"LabelLocalBooks": "Libros Locales", "LabelLocalBooks": "Libros Locales",
"LabelLocalPodcasts": "Pódcast locales", "LabelLocalPodcasts": "Pódcast locales",
"LabelLockOrientation": "Bloquear orientación", "LabelLockOrientation": "Bloquear orientación",
"LabelLockPlayer": "Bloquear el reproductor", "LabelLockPlayer": "Bloquear reproductor",
"LabelLow": "Bajo", "LabelLow": "Bajo",
"LabelMediaType": "Tipo de multimedia", "LabelMediaType": "Tipo de multimedia",
"LabelMedium": "Medio", "LabelMedium": "Medio",
@@ -191,7 +200,7 @@
"LabelNarrator": "Narrador", "LabelNarrator": "Narrador",
"LabelNarrators": "Narradores", "LabelNarrators": "Narradores",
"LabelNavigateWithVolume": "Navegar con las teclas de volumen", "LabelNavigateWithVolume": "Navegar con las teclas de volumen",
"LabelNavigateWithVolumeMirrored": "Espejo", "LabelNavigateWithVolumeMirrored": "Espejado",
"LabelNavigateWithVolumeWhilePlaying": "También puede utilizar los botones de volumen para desplazarse durante la reproducción", "LabelNavigateWithVolumeWhilePlaying": "También puede utilizar los botones de volumen para desplazarse durante la reproducción",
"LabelNavigateWithVolumeWhilePlayingDisabled": "Apagado", "LabelNavigateWithVolumeWhilePlayingDisabled": "Apagado",
"LabelNavigateWithVolumeWhilePlayingEnabled": "Encender", "LabelNavigateWithVolumeWhilePlayingEnabled": "Encender",
@@ -199,11 +208,11 @@
"LabelNewestAuthors": "Autores más nuevos", "LabelNewestAuthors": "Autores más nuevos",
"LabelNewestEpisodes": "Episodios más nuevos", "LabelNewestEpisodes": "Episodios más nuevos",
"LabelNo": "No", "LabelNo": "No",
"LabelNotFinished": "No terminado", "LabelNotFinished": "No finalizado",
"LabelNotStarted": "Sin iniciar", "LabelNotStarted": "Sin iniciar",
"LabelNumEpisodes": "{0} episodios", "LabelNumEpisodes": "{0} episodios",
"LabelNumEpisodesIncomplete": "{0} episodios, {1} incompletos", "LabelNumEpisodesIncomplete": "{0} episodios, {1} incompletos",
"LabelNumberOfEpisodes": "N.º de episodios", "LabelNumberOfEpisodes": "Nº de episodios",
"LabelOff": "Apagado", "LabelOff": "Apagado",
"LabelOn": "Encendido", "LabelOn": "Encendido",
"LabelPassword": "Contraseña", "LabelPassword": "Contraseña",
@@ -214,15 +223,16 @@
"LabelPlaybackTranscode": "Transcodificar", "LabelPlaybackTranscode": "Transcodificar",
"LabelPodcast": "Pódcast", "LabelPodcast": "Pódcast",
"LabelPodcasts": "Pódcast", "LabelPodcasts": "Pódcast",
"LabelPreventIndexing": "Evite que los directorios de pódcast de iTunes y Google indicen su suministro", "LabelPreventIndexing": "Evite que los directorios de pódcast de iTunes y Google indexen su suministro",
"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",
"LabelPublishedDate": "Publicado {0}", "LabelPublishedDate": "Publicado {0}",
"LabelRSSFeedCustomOwnerEmail": "Correo electrónico de dueño personalizado", "LabelRSSFeedCustomOwnerEmail": "Correo-e de propietario personalizado",
"LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado", "LabelRSSFeedCustomOwnerName": "Nombre de propietario personalizado",
"LabelRSSFeedOpen": "Fuente RSS Abierta",
"LabelRSSFeedPreventIndexing": "Evitar indización", "LabelRSSFeedPreventIndexing": "Evitar indización",
"LabelRSSFeedSlug": "«Slug» de suministro RSS", "LabelRSSFeedSlug": "Ficha de suministro RSS",
"LabelRandomly": "Aleatorio", "LabelRandomly": "Aleatorio",
"LabelRead": "Leído", "LabelRead": "Leído",
"LabelReadAgain": "Volver a leer", "LabelReadAgain": "Volver a leer",
@@ -232,16 +242,17 @@
"LabelScaleElapsedTimeBySpeed": "Escala el tiempo transcurrido según la velocidad", "LabelScaleElapsedTimeBySpeed": "Escala el tiempo transcurrido según la velocidad",
"LabelSeason": "Temporada", "LabelSeason": "Temporada",
"LabelSelectADevice": "Seleccione un dispositivo", "LabelSelectADevice": "Seleccione un dispositivo",
"LabelSelectMediaType": "Seleccionar tipo de medio",
"LabelSequenceAscending": "Secuencia ascendente", "LabelSequenceAscending": "Secuencia ascendente",
"LabelSequenceDescending": "Secuencia descendente", "LabelSequenceDescending": "Secuencia descendente",
"LabelSeries": "Serie", "LabelSeries": "Series",
"LabelServerAddress": "Dirección del servidor", "LabelServerAddress": "Dirección del servidor",
"LabelSetEbookAsPrimary": "Establecer como primario", "LabelSetEbookAsPrimary": "Establecer como primario",
"LabelSetEbookAsSupplementary": "Establecer como suplementario", "LabelSetEbookAsSupplementary": "Establecer como suplementario",
"LabelShakeSensitivity": "Sensibilidad a la sacudida", "LabelShakeSensitivity": "Sensibilidad a la sacudida",
"LabelShowAll": "Mostrar todo", "LabelShowAll": "Mostrar todo",
"LabelSize": "Tamaño", "LabelSize": "Tamaño",
"LabelSleepTimer": "Temporizador de apagado", "LabelSleepTimer": "Temporizador de dormida",
"LabelSleepTimerAlmostDoneChime": "Timbrar casi al terminar", "LabelSleepTimerAlmostDoneChime": "Timbrar casi al terminar",
"LabelSleepTimerAlmostDoneChimeHelp": "Reproducir una señal acústica cuando queden 30 segundos en el temporizador", "LabelSleepTimerAlmostDoneChimeHelp": "Reproducir una señal acústica cuando queden 30 segundos en el temporizador",
"LabelStart": "Iniciar", "LabelStart": "Iniciar",
@@ -282,14 +293,19 @@
"MessageAndroid10Downloads": "Android 10 e inferiores utilizarán el almacenamiento interno de aplicaciones para las descargas.", "MessageAndroid10Downloads": "Android 10 e inferiores utilizarán el almacenamiento interno de aplicaciones para las descargas.",
"MessageAttemptingServerConnection": "Intentando conectar con el servidor...", "MessageAttemptingServerConnection": "Intentando conectar con el servidor...",
"MessageAudiobookshelfServerNotConnected": "Servidor de Audiobookshelf no conectado", "MessageAudiobookshelfServerNotConnected": "Servidor de Audiobookshelf no conectado",
"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.", "MessageAudiobookshelfServerRequired": "<strong>¡Importante!</strong> Esta aplicación está diseñada para trabajar con un servidor Audiobookshelf que o alguien que conozcas es el host. Esta aplicación no proporciona ningún contenido.",
"MessageBookshelfEmpty": "Estantería vacía", "MessageBookshelfEmpty": "Estantería vacía",
"MessageConfirmAppExit": "¿Desea salir de la aplicación?",
"MessageConfirmDeleteEpisodeDownloadQueue": "¿Seguro que desea vaciar la cola de descarga del episodio?",
"MessageConfirmDeleteLocalEpisode": "¿Quiere eliminar el episodio local «{0}» del dispositivo? El archivo en el servidor no se verá afectado.", "MessageConfirmDeleteLocalEpisode": "¿Quiere eliminar el episodio local «{0}» del dispositivo? El archivo en el servidor no se verá afectado.",
"MessageConfirmDeleteLocalFiles": "¿Quiere quitar los archivos locales de este elemento del dispositivo? Los archivos del servidor y su progreso no se verán afectados.", "MessageConfirmDeleteLocalFiles": "¿Quiere quitar los archivos locales de este elemento del dispositivo? Los archivos del servidor y su progreso no se verán afectados.",
"MessageConfirmDeleteServerConfig": "¿Quito esta configuración del servidor?",
"MessageConfirmDeleteServerEpisode": "¿Seguro que desea eliminar el episodio «{0}» desde el servidor?\nAdvertencia: esto eliminará el archivo de sonido.",
"MessageConfirmDisableAutoTimer": "¿Confirma que quiere desactivar el temporizador automático por el resto del día? El temporizador volverá a activarse al final de este periodo de temporizador de apagado automático, o si reinicia la aplicación.", "MessageConfirmDisableAutoTimer": "¿Confirma que quiere desactivar el temporizador automático por el resto del día? El temporizador volverá a activarse al final de este periodo de temporizador de apagado automático, o si reinicia la aplicación.",
"MessageConfirmDiscardProgress": "¿Confirma que quiere restablecer su progreso?", "MessageConfirmDiscardProgress": "¿Confirma que quiere restablecer su progreso?",
"MessageConfirmDownloadUsingCellular": "Está a punto de efectuar una descarga con datos móviles. Esto puede conllevar tarifas de datos de la operadora. ¿Quiere continuar?", "MessageConfirmDownloadUsingCellular": "Está a punto de efectuar una descarga con datos móviles. Esto puede conllevar tarifas de datos de la operadora. ¿Quiere continuar?",
"MessageConfirmMarkAsFinished": "¿Confirma que quiere marcar este elemento como terminado?", "MessageConfirmMarkAsFinished": "¿Confirma que quiere marcar este elemento como terminado?",
"MessageConfirmPlaybackTime": "¿Inicio la reproducción para «{0}» en {1}?",
"MessageConfirmRemoveBookmark": "¿Confirma que quiere quitar el marcador?", "MessageConfirmRemoveBookmark": "¿Confirma que quiere quitar el marcador?",
"MessageConfirmStreamingUsingCellular": "Está a punto de hacer una transmisión con datos móviles. Esto puede conllevar tarifas de datos de la operadora. ¿Quiere continuar?", "MessageConfirmStreamingUsingCellular": "Está a punto de hacer una transmisión con datos móviles. Esto puede conllevar tarifas de datos de la operadora. ¿Quiere continuar?",
"MessageDiscardProgress": "Descartar progreso", "MessageDiscardProgress": "Descartar progreso",
@@ -297,6 +313,7 @@
"MessageDownloading": "Descargando...", "MessageDownloading": "Descargando...",
"MessageDownloadingEpisode": "Descargando episodio", "MessageDownloadingEpisode": "Descargando episodio",
"MessageEpisodesQueuedForDownload": "{0} episodio(s) en cola para descargar", "MessageEpisodesQueuedForDownload": "{0} episodio(s) en cola para descargar",
"MessageFailedToRefreshToken": "No se pudo actualizar el token, se requiere reiniciar la sesión",
"MessageFeedURLWillBe": "El URL del suministro será {0}", "MessageFeedURLWillBe": "El URL del suministro será {0}",
"MessageFetching": "Recuperando...", "MessageFetching": "Recuperando...",
"MessageFollowTheProjectOnGithub": "Siga el proyecto en GitHub", "MessageFollowTheProjectOnGithub": "Siga el proyecto en GitHub",
@@ -324,6 +341,9 @@
"MessageNoSeries": "Ninguna serie", "MessageNoSeries": "Ninguna serie",
"MessageNoUpdatesWereNecessary": "No fue necesario actualizar", "MessageNoUpdatesWereNecessary": "No fue necesario actualizar",
"MessageNoUserPlaylists": "No tiene ninguna lista de reproducción", "MessageNoUserPlaylists": "No tiene ninguna lista de reproducción",
"MessageOldServerAuthReLoginRequired": "La autenticación se ha mejorado para seguridad en la versión v2.26.0 del servidor. Todos los usuarios deberán reiniciar sesión.",
"MessageOldServerAuthWarning": "El servidor está utilizando un método de autenticación anticuado",
"MessageOldServerAuthWarningHelp": "Este servidor está ejecutando una versión más antigua que la v2.26.0. Ha sido añadido un sistema de autenticación más seguro en v2.26.0. Se recomienda encarecidamente que actualice el servidor a la última versión. Si ya tiene el servidor actualizado, acceda de nuevo para utilizar la autenticación nueva.",
"MessageOldServerConnectionWarning": "La configuración de la conexión al servidor utiliza un identificador de usuario antiguo. Elimine y vuelva a añadir esta conexión al servidor.", "MessageOldServerConnectionWarning": "La configuración de la conexión al servidor utiliza un identificador de usuario antiguo. Elimine y vuelva a añadir esta conexión al servidor.",
"MessageOldServerConnectionWarningHelp": "Usted configuró originalmente la conexión a este servidor antes de la migración de la base de datos en la versión 2.3.0, publicada en junio de 2023. Una futura actualización del servidor eliminará la posibilidad de iniciar sesión con esta conexión antigua. Por favor, elimine la conexión existente al servidor y conéctese de nuevo (utilizando la misma dirección del servidor y las mismas credenciales). Si tiene algún medio descargado en este dispositivo, será necesario descargarlo de nuevo para sincronizarlo con el servidor.", "MessageOldServerConnectionWarningHelp": "Usted configuró originalmente la conexión a este servidor antes de la migración de la base de datos en la versión 2.3.0, publicada en junio de 2023. Una futura actualización del servidor eliminará la posibilidad de iniciar sesión con esta conexión antigua. Por favor, elimine la conexión existente al servidor y conéctese de nuevo (utilizando la misma dirección del servidor y las mismas credenciales). Si tiene algún medio descargado en este dispositivo, será necesario descargarlo de nuevo para sincronizarlo con el servidor.",
"MessagePodcastSearchField": "Introduzca el término de búsqueda o el URL del suministro RSS", "MessagePodcastSearchField": "Introduzca el término de búsqueda o el URL del suministro RSS",
@@ -350,5 +370,7 @@
"ToastPodcastCreateSuccess": "Se creó el pódcast correctamente", "ToastPodcastCreateSuccess": "Se creó el pódcast correctamente",
"ToastRSSFeedCloseFailed": "Error al cerrar el suministro RSS", "ToastRSSFeedCloseFailed": "Error al cerrar el suministro RSS",
"ToastRSSFeedCloseSuccess": "Suministro RSS cerrado", "ToastRSSFeedCloseSuccess": "Suministro RSS cerrado",
"ToastStreamingNotAllowedOnCellular": "El streaming no está permitido con datos móviles" "ToastStreamingNotAllowedOnCellular": "El streaming no está permitido con datos móviles",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+2
View File
@@ -166,7 +166,9 @@
"LabelStatsBestDay": "Parim päev", "LabelStatsBestDay": "Parim päev",
"LabelStatsDays": "Päevi", "LabelStatsDays": "Päevi",
"LabelStatsInARow": "järjest", "LabelStatsInARow": "järjest",
"LabelStatsItemsFinished": "Lõpetatud raamatud",
"LabelStatsMinutes": "minuteid", "LabelStatsMinutes": "minuteid",
"LabelStatsWeekListening": "Nädala keskmine",
"LabelThemeBlack": "Must", "LabelThemeBlack": "Must",
"LabelThemeDark": "Pime", "LabelThemeDark": "Pime",
"LabelThemeLight": "Hele", "LabelThemeLight": "Hele",
+57
View File
@@ -0,0 +1,57 @@
{
"ButtonAdd": "افزودن",
"ButtonAddNewServer": "افزودن سرور جدید",
"ButtonAuthors": "ناشر",
"ButtonBack": "بازگشت",
"ButtonCancel": "انصراف",
"ButtonCancelTimer": "لغو تایمر",
"ButtonClearFilter": "حذف صافی",
"ButtonClearLogs": "حذف گزارش‌ها",
"ButtonCloseFeed": "بستن فید",
"ButtonCollections": "مجموعه ها",
"ButtonConnect": "اتصال",
"ButtonConnectToServer": "اتصال به سرور",
"ButtonCreate": "ساختن",
"ButtonCreateBookmark": "ساخت نشانک",
"ButtonCreateNewPlaylist": "ساخت لیست پخش جدید",
"ButtonDelete": "حذف",
"ButtonDeleteLocalEpisode": "حذف اپیزود های محلی",
"ButtonDeleteLocalFile": "حذف فایل های محلی",
"ButtonDeleteLocalItem": "حذف آیتم های محلی",
"ButtonDisableAutoTimer": "غیر فعال کردن تایم خودکار",
"ButtonDisconnect": "قطع کردن",
"ButtonGoToWebClient": "رفتن به نسخه تحت وب سمت کاربر",
"ButtonHistory": "تاریخچه",
"ButtonHome": "خانه",
"ButtonIssues": "مشکلات",
"ButtonLatest": "جدیدترین",
"ButtonLibrary": "کتابخانه",
"ButtonLocalMedia": "رسانه‌های محلی",
"ButtonLogs": "گزارش‌ها",
"ButtonManageLocalFiles": "مدیریت فایل های محلی",
"ButtonMaskServerAddress": "پنهان کردن آدرس سرور",
"ButtonNewFolder": "پوشه جدید",
"ButtonNextEpisode": "اپیزود بعدی",
"ButtonOk": "تایید",
"ButtonOpenFeed": "باز کردن فید",
"ButtonOverride": "جایگزینی",
"ButtonPause": "توقف",
"ButtonPlay": "پخش",
"ButtonPlayEpisode": "پخش اپیزود",
"ButtonPlaylists": "لیست پخش",
"ButtonRead": "خواندن",
"ButtonReadLess": "خواندن کمتر",
"ButtonReadMore": "خواندن بیشتر",
"ButtonRemove": "حذف",
"ButtonRemoveFromServer": "حذف از سرور",
"ButtonSave": "ذخیره",
"ButtonSaveOrder": "ذخیره ترتیب",
"ButtonSearch": "جستجو",
"ButtonSendEbookToDevice": "ارسال کتاب الکترونیکی به دستگاه",
"ButtonSeries": "مجموعه",
"ButtonSetTimer": "تنظیم تایمر",
"ButtonStream": "پخش آنلاین",
"ButtonSubmit": "ثبت",
"ButtonSwitchServerUser": "تعویض سرور/کاربر",
"ButtonUnmaskServerAddress": "نمایش آدرس سرور"
}
+31 -2
View File
@@ -27,7 +27,9 @@
"ButtonLatest": "Viimeisimmät", "ButtonLatest": "Viimeisimmät",
"ButtonLibrary": "Kirjasto", "ButtonLibrary": "Kirjasto",
"ButtonLocalMedia": "Paikallinen media", "ButtonLocalMedia": "Paikallinen media",
"ButtonLogs": "Lokit",
"ButtonManageLocalFiles": "Hallitse paikallsiia tiedostoja", "ButtonManageLocalFiles": "Hallitse paikallsiia tiedostoja",
"ButtonMaskServerAddress": "Piilota palvelimen osoite",
"ButtonNewFolder": "Uusi kansio", "ButtonNewFolder": "Uusi kansio",
"ButtonNextEpisode": "Seuraava jakso", "ButtonNextEpisode": "Seuraava jakso",
"ButtonOk": "Hyvä on", "ButtonOk": "Hyvä on",
@@ -51,6 +53,7 @@
"ButtonStream": "Suoratoisto", "ButtonStream": "Suoratoisto",
"ButtonSubmit": "Lähetä", "ButtonSubmit": "Lähetä",
"ButtonSwitchServerUser": "Vaihda palvelinta/käyttäjää", "ButtonSwitchServerUser": "Vaihda palvelinta/käyttäjää",
"ButtonUnmaskServerAddress": "Näytä palvelimen osoite",
"ButtonUserStats": "Käyttäjän tilastot", "ButtonUserStats": "Käyttäjän tilastot",
"ButtonYes": "Kyllä", "ButtonYes": "Kyllä",
"HeaderAccount": "Tili", "HeaderAccount": "Tili",
@@ -60,6 +63,7 @@
"HeaderChapters": "Luvut", "HeaderChapters": "Luvut",
"HeaderCollection": "Kokoelma", "HeaderCollection": "Kokoelma",
"HeaderCollectionItems": "Kokoelman osat", "HeaderCollectionItems": "Kokoelman osat",
"HeaderConfirm": "Vahvista",
"HeaderConnectionStatus": "Kokoelman tila", "HeaderConnectionStatus": "Kokoelman tila",
"HeaderDataSettings": "Tietojen asetukset", "HeaderDataSettings": "Tietojen asetukset",
"HeaderDetails": "Yksityiskohdat", "HeaderDetails": "Yksityiskohdat",
@@ -88,6 +92,7 @@
"HeaderStatsRecentSessions": "Viimeaikaiset istunnot", "HeaderStatsRecentSessions": "Viimeaikaiset istunnot",
"HeaderTableOfContents": "Sisällysluettelo", "HeaderTableOfContents": "Sisällysluettelo",
"HeaderUserInterfaceSettings": "Käyttöliittymän asetukset", "HeaderUserInterfaceSettings": "Käyttöliittymän asetukset",
"HeaderWelcome": "Tervetuloa, <strong>{0</strong>",
"HeaderYourStats": "Tilastosi", "HeaderYourStats": "Tilastosi",
"LabelAddToPlaylist": "Lisää soittolistaan", "LabelAddToPlaylist": "Lisää soittolistaan",
"LabelAddedAt": "Lisätty listalle", "LabelAddedAt": "Lisätty listalle",
@@ -110,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Kun automaattinen uniajastin päättyy, ensi kerralla toistetaan osa kuunenltua uudelleen.", "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.", "LabelAutoSleepTimerHelp": "Toiston alkaessa tällä aikävälillä unikytkin kytketään automaattisesti käyttöön.",
"LabelBooks": "Kirjat", "LabelBooks": "Kirjat",
"LabelByAuthor": "Tekijältä: {0}",
"LabelChapterTrack": "Luvun raita", "LabelChapterTrack": "Luvun raita",
"LabelChapters": "Luvut", "LabelChapters": "Luvut",
"LabelClosePlayer": "Sulje soitin", "LabelClosePlayer": "Sulje soitin",
@@ -143,6 +149,7 @@
"LabelEndOfChapter": "Luvun loppu", "LabelEndOfChapter": "Luvun loppu",
"LabelEndTime": "Päättymisaika", "LabelEndTime": "Päättymisaika",
"LabelEpisode": "Jakso", "LabelEpisode": "Jakso",
"LabelExplicit": "Sopimaton",
"LabelFeedURL": "Syötteen URL", "LabelFeedURL": "Syötteen URL",
"LabelFile": "Tiedosto", "LabelFile": "Tiedosto",
"LabelFileBirthtime": "Tiedoston syntymäaika", "LabelFileBirthtime": "Tiedoston syntymäaika",
@@ -151,6 +158,9 @@
"LabelFinished": "Valmis", "LabelFinished": "Valmis",
"LabelFolder": "Kansio", "LabelFolder": "Kansio",
"LabelFontBoldness": "Kirjasintyyppien lihavointi", "LabelFontBoldness": "Kirjasintyyppien lihavointi",
"LabelFontFamily": "Fonttiperhe",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Kirjasintyyppien skaalautuminen", "LabelFontScale": "Kirjasintyyppien skaalautuminen",
"LabelGenre": "Lajityyppi", "LabelGenre": "Lajityyppi",
"LabelGenres": "Lajityypit", "LabelGenres": "Lajityypit",
@@ -170,6 +180,9 @@
"LabelLayout": "Asettelu", "LabelLayout": "Asettelu",
"LabelLayoutAuto": "Automaattinen", "LabelLayoutAuto": "Automaattinen",
"LabelLayoutSinglePage": "Yksi sivu", "LabelLayoutSinglePage": "Yksi sivu",
"LabelLibrarySortByProgress": "Edistyminen: Viimeksi päivitetty",
"LabelLibrarySortByProgressFinished": "Edistyminen: Valmis",
"LabelLibrarySortByProgressStarted": "Edistyminen: Aloitettu",
"LabelLight": "Vaalea", "LabelLight": "Vaalea",
"LabelLineSpacing": "Riviväli", "LabelLineSpacing": "Riviväli",
"LabelListenAgain": "Kuuntele uudelleen", "LabelListenAgain": "Kuuntele uudelleen",
@@ -217,17 +230,19 @@
"LabelPublishedDate": "Julkaistu {0}", "LabelPublishedDate": "Julkaistu {0}",
"LabelRSSFeedCustomOwnerEmail": "Mukautettu omistajan sähköposti", "LabelRSSFeedCustomOwnerEmail": "Mukautettu omistajan sähköposti",
"LabelRSSFeedCustomOwnerName": "Mukautettu omistajan nimi", "LabelRSSFeedCustomOwnerName": "Mukautettu omistajan nimi",
"LabelRSSFeedOpen": "RSS Syöte Avoin",
"LabelRSSFeedPreventIndexing": "Estä indeksointi", "LabelRSSFeedPreventIndexing": "Estä indeksointi",
"LabelRSSFeedSlug": "RSS-syöte Slug", "LabelRSSFeedSlug": "RSS-syöte Slug",
"LabelRandomly": "Satunnaisesti", "LabelRandomly": "Satunnaisesti",
"LabelRead": "Lue", "LabelRead": "Lue",
"LabelReadAgain": "Lue uudelleen", "LabelReadAgain": "Lue uudelleen",
"LabelRecentSeries": "Viimeisimmät sarjat", "LabelRecentSeries": "Viimeisimmät sarjat",
"LabelRecentlyAdded": "Viimeeksi lisätyt", "LabelRecentlyAdded": "Viimeksi lisätyt",
"LabelRemoveFromPlaylist": "Poista soittolistalta", "LabelRemoveFromPlaylist": "Poista soittolistalta",
"LabelScaleElapsedTimeBySpeed": "Skaalaa kulunut aika nopeuden mukaan", "LabelScaleElapsedTimeBySpeed": "Skaalaa kulunut aika nopeuden mukaan",
"LabelSeason": "Kausi", "LabelSeason": "Kausi",
"LabelSelectADevice": "Valitse laite", "LabelSelectADevice": "Valitse laite",
"LabelSelectMediaType": "Valitse mediatyyppi",
"LabelSequenceAscending": "Nouseva sekvenssi", "LabelSequenceAscending": "Nouseva sekvenssi",
"LabelSequenceDescending": "Laskeva sekvenssi", "LabelSequenceDescending": "Laskeva sekvenssi",
"LabelSeries": "Sarja", "LabelSeries": "Sarja",
@@ -238,6 +253,8 @@
"LabelShowAll": "Näytä kaikki", "LabelShowAll": "Näytä kaikki",
"LabelSize": "Koko", "LabelSize": "Koko",
"LabelSleepTimer": "Uniajastin", "LabelSleepTimer": "Uniajastin",
"LabelSleepTimerAlmostDoneChime": "Soita merkkiääni, kun on ajastin on lähellä loppua",
"LabelSleepTimerAlmostDoneChimeHelp": "Soita merkkiääni, kun uniajastimessa on 30 sekuntia jäljellä",
"LabelStart": "Aloita", "LabelStart": "Aloita",
"LabelStartTime": "Aloitusaika", "LabelStartTime": "Aloitusaika",
"LabelStatsBestDay": "Paras päivä", "LabelStatsBestDay": "Paras päivä",
@@ -278,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf palvelinta ei ole yhdistetty", "MessageAudiobookshelfServerNotConnected": "Audiobookshelf palvelinta ei ole yhdistetty",
"MessageAudiobookshelfServerRequired": "<strong>Tärkeää!</strong> Tämä sovellus on suunniteltu toimimaan sinun tai tuttavasi Audiobookshelf-palvelimen kanssa. Tämä sovellus ei tarjoa sisältöä.", "MessageAudiobookshelfServerRequired": "<strong>Tärkeää!</strong> Tämä sovellus on suunniteltu toimimaan sinun tai tuttavasi Audiobookshelf-palvelimen kanssa. Tämä sovellus ei tarjoa sisältöä.",
"MessageBookshelfEmpty": "Kirjahylly on tyhjä", "MessageBookshelfEmpty": "Kirjahylly on tyhjä",
"MessageConfirmAppExit": "Haluatko poistua sovelluksesta?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Haluatko varmasti tyhjentää jaksojen latausjonon?",
"MessageConfirmDeleteLocalEpisode": "Poista jakso \"{0}\" laitteeltasi? Tämä ei vaikuta palvelimella olevaan tiedostoon.", "MessageConfirmDeleteLocalEpisode": "Poista jakso \"{0}\" laitteeltasi? Tämä ei vaikuta palvelimella olevaan tiedostoon.",
"MessageConfirmDeleteLocalFiles": "Poista tämän kohteen tiedostot laitteestasi? Tämä ei vaikuta palvelimella oleviin tiedostoihin tai edistymiseesi.", "MessageConfirmDeleteLocalFiles": "Poista tämän kohteen tiedostot laitteestasi? Tämä ei vaikuta palvelimella oleviin tiedostoihin tai edistymiseesi.",
"MessageConfirmDeleteServerConfig": "Poistetaanko palvelimen konfiguraatio?",
"MessageConfirmDeleteServerEpisode": "Haluatko varmasti poistaa jakson \"{0}\" palvelimelta?\nVaroitus: Tämä poistaa äänitiedoston.",
"MessageConfirmDisableAutoTimer": "Oletko varma, että haluat poistaa automaattisen ajastimen käytöstä loppupäivän ajaksi? Ajastin otetaan uudelleen käyttöön tämän automaattisen uniajastimen jakson lopussa tai jos käynnistät sovelluksen uudelleen.", "MessageConfirmDisableAutoTimer": "Oletko varma, että haluat poistaa automaattisen ajastimen käytöstä loppupäivän ajaksi? Ajastin otetaan uudelleen käyttöön tämän automaattisen uniajastimen jakson lopussa tai jos käynnistät sovelluksen uudelleen.",
"MessageConfirmDiscardProgress": "Haluatko varmasti nollata edistymisesi?", "MessageConfirmDiscardProgress": "Haluatko varmasti nollata edistymisesi?",
"MessageConfirmDownloadUsingCellular": "Olet lataamassa mobiilidatan avulla. Tämä voi aiheuttaa operaattorin datamaksuja. Haluatko jatkaa?", "MessageConfirmDownloadUsingCellular": "Olet lataamassa mobiilidatan avulla. Tämä voi aiheuttaa operaattorin datamaksuja. Haluatko jatkaa?",
"MessageConfirmMarkAsFinished": "Haluatko varmasti merkitä tämän kohteen valmiiksi?", "MessageConfirmMarkAsFinished": "Haluatko varmasti merkitä tämän kohteen valmiiksi?",
"MessageConfirmPlaybackTime": "Toistetaanko \"{0}\" kohdasta {1}?",
"MessageConfirmRemoveBookmark": "Haluatko varmasti poistaa kirjanmerkin?", "MessageConfirmRemoveBookmark": "Haluatko varmasti poistaa kirjanmerkin?",
"MessageConfirmStreamingUsingCellular": "Olet suoratoistamassa matkapuhelindatan avulla. Tämä voi aiheuttaa operaattorin datamaksuja. Haluatko jatkaa?", "MessageConfirmStreamingUsingCellular": "Olet suoratoistamassa matkapuhelindatan avulla. Tämä voi aiheuttaa operaattorin datamaksuja. Haluatko jatkaa?",
"MessageDiscardProgress": "Hylkää edistyminen", "MessageDiscardProgress": "Hylkää edistyminen",
@@ -291,6 +313,7 @@
"MessageDownloading": "Ladataan...", "MessageDownloading": "Ladataan...",
"MessageDownloadingEpisode": "Ladataan jaksoa", "MessageDownloadingEpisode": "Ladataan jaksoa",
"MessageEpisodesQueuedForDownload": "{0} jaksoa on latausjonossa", "MessageEpisodesQueuedForDownload": "{0} jaksoa on latausjonossa",
"MessageFailedToRefreshToken": "Tunnuksen uusiminen epäonnistui, kirjaudu uudelleen sisään",
"MessageFeedURLWillBe": "Syötteen URL tulee olemaan {0}", "MessageFeedURLWillBe": "Syötteen URL tulee olemaan {0}",
"MessageFetching": "Haetaan...", "MessageFetching": "Haetaan...",
"MessageFollowTheProjectOnGithub": "Seuraa projektia Githubissa", "MessageFollowTheProjectOnGithub": "Seuraa projektia Githubissa",
@@ -311,12 +334,16 @@
"MessageNoItems": "Ei kohteita", "MessageNoItems": "Ei kohteita",
"MessageNoItemsFound": "Kohteita ei löytynyt", "MessageNoItemsFound": "Kohteita ei löytynyt",
"MessageNoListeningSessions": "Ei kuunteluistuntoja", "MessageNoListeningSessions": "Ei kuunteluistuntoja",
"MessageNoLogs": "Ei lokeja",
"MessageNoMediaFolders": "Ei mediakansioita", "MessageNoMediaFolders": "Ei mediakansioita",
"MessageNoNetworkConnection": "Ei verkkoyhteyttä", "MessageNoNetworkConnection": "Ei verkkoyhteyttä",
"MessageNoPodcastsFound": "Podcasteja ei löytynyt", "MessageNoPodcastsFound": "Podcasteja ei löytynyt",
"MessageNoSeries": "Ei sarjoja", "MessageNoSeries": "Ei sarjoja",
"MessageNoUpdatesWereNecessary": "Päivityksiä ei tarvittu", "MessageNoUpdatesWereNecessary": "Päivityksiä ei tarvittu",
"MessageNoUserPlaylists": "Sinulla ei ole soittolistoja", "MessageNoUserPlaylists": "Sinulla ei ole soittolistoja",
"MessageOldServerAuthReLoginRequired": "Todennuksen tietoturvaa on parannettu palvelimen versiossa 2.26.0. Kaikkien käyttäjien on kirjauduttava uudelleen sisään.",
"MessageOldServerAuthWarning": "Palvelun käyttää vanhentunutta todennusta",
"MessageOldServerAuthWarningHelp": "Palvelimessa on vanhempi versio kuin v.2.26.0. Turvallisempi todennus lisättiin versioon v.2.26.0. On suositeltavaa päivittää palvelin uusimpaan versioon. Mikäli olet jo päivittänyt, kirjaudu uudelleen sisään ja käytä uutta todennusta sisäänkirjautumisessa.",
"MessageOldServerConnectionWarning": "Palvelinyhteysmääritys käyttää vanhaa käyttäjätunnusta. Poista tämä palvelinyhteys ja lisää se uudelleen.", "MessageOldServerConnectionWarning": "Palvelinyhteysmääritys käyttää vanhaa käyttäjätunnusta. Poista tämä palvelinyhteys ja lisää se uudelleen.",
"MessageOldServerConnectionWarningHelp": "Määritit alun perin yhteyden tähän palvelimeen ennen tietokannan siirtoa versiossa 2.3.0, joka julkaistiin kesäkuussa 2023. Tuleva palvelinpäivitys poistaa mahdollisuuden kirjautua sisään tällä vanhalla yhteydellä. Poista olemassa oleva palvelinyhteys ja muodosta yhteys uudelleen (käyttäen samaa palvelinosoitetta ja tunnistetietoja). Jos sinulla on ladattua mediaa tällä laitteella, media on ladattava uudelleen synkronoitavaksi palvelimen kanssa.", "MessageOldServerConnectionWarningHelp": "Määritit alun perin yhteyden tähän palvelimeen ennen tietokannan siirtoa versiossa 2.3.0, joka julkaistiin kesäkuussa 2023. Tuleva palvelinpäivitys poistaa mahdollisuuden kirjautua sisään tällä vanhalla yhteydellä. Poista olemassa oleva palvelinyhteys ja muodosta yhteys uudelleen (käyttäen samaa palvelinosoitetta ja tunnistetietoja). Jos sinulla on ladattua mediaa tällä laitteella, media on ladattava uudelleen synkronoitavaksi palvelimen kanssa.",
"MessagePodcastSearchField": "Syötä hakutermi tai RSS-syötteen URL-osoite", "MessagePodcastSearchField": "Syötä hakutermi tai RSS-syötteen URL-osoite",
@@ -343,5 +370,7 @@
"ToastPodcastCreateSuccess": "Podcastin luominen onnistui", "ToastPodcastCreateSuccess": "Podcastin luominen onnistui",
"ToastRSSFeedCloseFailed": "RSS syötteen sulkeminen epäonnistui", "ToastRSSFeedCloseFailed": "RSS syötteen sulkeminen epäonnistui",
"ToastRSSFeedCloseSuccess": "RSS syöte suljettu", "ToastRSSFeedCloseSuccess": "RSS syöte suljettu",
"ToastStreamingNotAllowedOnCellular": "Suoratoisto ei ole sallittu mobiilidatalla" "ToastStreamingNotAllowedOnCellular": "Suoratoisto ei ole sallittu mobiilidatalla",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+20 -3
View File
@@ -63,6 +63,7 @@
"HeaderChapters": "Chapitres", "HeaderChapters": "Chapitres",
"HeaderCollection": "Collection", "HeaderCollection": "Collection",
"HeaderCollectionItems": "Entrées de la collection", "HeaderCollectionItems": "Entrées de la collection",
"HeaderConfirm": "Confirmer",
"HeaderConnectionStatus": "Status de connexion", "HeaderConnectionStatus": "Status de connexion",
"HeaderDataSettings": "Paramètres de données", "HeaderDataSettings": "Paramètres de données",
"HeaderDetails": "Détails", "HeaderDetails": "Détails",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Sessions récentes", "HeaderStatsRecentSessions": "Sessions récentes",
"HeaderTableOfContents": "Table des matières", "HeaderTableOfContents": "Table des matières",
"HeaderUserInterfaceSettings": "Paramètres de l'interface", "HeaderUserInterfaceSettings": "Paramètres de l'interface",
"HeaderWelcome": "Bienvenue, <strong>{0}</strong>",
"HeaderYourStats": "Vos statistiques", "HeaderYourStats": "Vos statistiques",
"LabelAddToPlaylist": "Ajouter à la liste de lecture", "LabelAddToPlaylist": "Ajouter à la liste de lecture",
"LabelAddedAt": "Date dajout", "LabelAddedAt": "Date dajout",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Lorsque le minuteur nocturne se termine, relire l'élément fera un saut en arrière.", "LabelAutoSleepTimerAutoRewindHelp": "Lorsque le minuteur nocturne se termine, relire l'élément fera un saut en arrière.",
"LabelAutoSleepTimerHelp": "Lorsqu'un éléments est lu entre l'heure de début et de fin, un minuteur nocturne se lance automatiquement.", "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",
"LabelByAuthor": "de {0}",
"LabelChapterTrack": "Piste des chapitres", "LabelChapterTrack": "Piste des chapitres",
"LabelChapters": "Chapitres", "LabelChapters": "Chapitres",
"LabelClosePlayer": "Fermer le lecteur", "LabelClosePlayer": "Fermer le lecteur",
@@ -155,7 +158,10 @@
"LabelFinished": "Fini(e)", "LabelFinished": "Fini(e)",
"LabelFolder": "Dossier", "LabelFolder": "Dossier",
"LabelFontBoldness": "Graisse de la police", "LabelFontBoldness": "Graisse de la police",
"LabelFontScale": "Taille de la police de caractère", "LabelFontFamily": "Famille de caractères",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Avec empattements",
"LabelFontScale": "Taille de la police",
"LabelGenre": "Genre", "LabelGenre": "Genre",
"LabelGenres": "Genres", "LabelGenres": "Genres",
"LabelHapticFeedback": "Retour haptique", "LabelHapticFeedback": "Retour haptique",
@@ -174,7 +180,10 @@
"LabelLayout": "Mise en page", "LabelLayout": "Mise en page",
"LabelLayoutAuto": "Automatique", "LabelLayoutAuto": "Automatique",
"LabelLayoutSinglePage": "Page seule", "LabelLayoutSinglePage": "Page seule",
"LabelLight": "Léger", "LabelLibrarySortByProgress": "Progression : Mise à jour",
"LabelLibrarySortByProgressFinished": "Progression : Terminé",
"LabelLibrarySortByProgressStarted": "Progression : En cours",
"LabelLight": "Clair",
"LabelLineSpacing": "Interligne", "LabelLineSpacing": "Interligne",
"LabelListenAgain": "Écouter à nouveau", "LabelListenAgain": "Écouter à nouveau",
"LabelLocalBooks": "Livres locaux", "LabelLocalBooks": "Livres locaux",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Traduire le temps restant en fonction de la vitesse de lecture", "LabelScaleElapsedTimeBySpeed": "Traduire le temps restant en fonction de la vitesse de lecture",
"LabelSeason": "Saison", "LabelSeason": "Saison",
"LabelSelectADevice": "Sélectionner un Appareil", "LabelSelectADevice": "Sélectionner un Appareil",
"LabelSelectMediaType": "Sélectionnez le type de media",
"LabelSequenceAscending": "Séquence ascendante", "LabelSequenceAscending": "Séquence ascendante",
"LabelSequenceDescending": "Séquence descendante", "LabelSequenceDescending": "Séquence descendante",
"LabelSeries": "Séries", "LabelSeries": "Séries",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Serveur Audiobookshelf non connecté", "MessageAudiobookshelfServerNotConnected": "Serveur Audiobookshelf non connecté",
"MessageAudiobookshelfServerRequired": "<strong>Important!</strong> Cette application est faite pour fonctionner avec un serveur Audiobookshelf. L'application ne fournit aucun contenu.", "MessageAudiobookshelfServerRequired": "<strong>Important!</strong> Cette application est faite pour fonctionner avec un serveur Audiobookshelf. L'application ne fournit aucun contenu.",
"MessageBookshelfEmpty": "Bibliothèque vide", "MessageBookshelfEmpty": "Bibliothèque vide",
"MessageConfirmAppExit": "Voulez-vous quitter l'application?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Voulez-vous vraiment vider la file d'attente de téléchargement des épisodes?",
"MessageConfirmDeleteLocalEpisode": "Retirer l’épisode local « {0} » de votre appareil? Le fichier sur le serveur ne sera pas affecté.", "MessageConfirmDeleteLocalEpisode": "Retirer l’épisode local « {0} » de votre appareil? Le fichier sur le serveur ne sera pas affecté.",
"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.", "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.",
"MessageConfirmDeleteServerConfig": "Supprimer la configuration de ce serveur?",
"MessageConfirmDeleteServerEpisode": "Voulez-vous vraiment supprimer l'épisode \"{0}\" du serveur?\nAttention : le fichier audio sera supprimé.",
"MessageConfirmDisableAutoTimer": "Êtes-vous sûr·e de vouloir désactiver le minuteur automatique pour le reste de la journée? Le minuteur sera réactivé à la fin de cette période de décompte automatique, ou si vous redémarrez l'application.", "MessageConfirmDisableAutoTimer": "Êtes-vous sûr·e de vouloir désactiver le minuteur automatique pour le reste de la journée? Le minuteur sera réactivé à la fin de cette période de décompte automatique, ou si vous redémarrez l'application.",
"MessageConfirmDiscardProgress": "Êtes-vous sûr·e de vouloir supprimer votre progression?", "MessageConfirmDiscardProgress": "Êtes-vous sûr·e de vouloir supprimer votre progression?",
"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?", "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?",
"MessageConfirmMarkAsFinished": "Êtes-vous sûr·e de vouloir marquer cette élement comme terminé ?", "MessageConfirmMarkAsFinished": "Êtes-vous sûr·e de vouloir marquer cette élement comme terminé ?",
"MessageConfirmPlaybackTime": "Lancer la lecture de \"{0}\" à {1}?",
"MessageConfirmRemoveBookmark": "Êtes-vous sûr·e de vouloir supprimer le marque-page?", "MessageConfirmRemoveBookmark": "Êtes-vous sûr·e de vouloir supprimer le marque-page?",
"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?", "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?",
"MessageDiscardProgress": "Supprimer la progression", "MessageDiscardProgress": "Supprimer la progression",
@@ -355,5 +370,7 @@
"ToastPodcastCreateSuccess": "Podcast créé avec succès", "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" "ToastStreamingNotAllowedOnCellular": "La diffusion sur les données mobile nest pas autorisée",
"UnitMinutesShort": "{0}min",
"UnitSecondsShort": "{0}sec"
} }
+4
View File
@@ -146,6 +146,7 @@
"LabelEndOfChapter": "סוף הפרק", "LabelEndOfChapter": "סוף הפרק",
"LabelEndTime": "זמן סיום", "LabelEndTime": "זמן סיום",
"LabelEpisode": "פרק", "LabelEpisode": "פרק",
"LabelExplicit": "מפורש",
"LabelFeedURL": "כתובת ערוץ", "LabelFeedURL": "כתובת ערוץ",
"LabelFile": "קובץ", "LabelFile": "קובץ",
"LabelFileBirthtime": "זמן יצירת הקובץ", "LabelFileBirthtime": "זמן יצירת הקובץ",
@@ -173,6 +174,9 @@
"LabelLayout": "Layout", "LabelLayout": "Layout",
"LabelLayoutAuto": "אוטומטי", "LabelLayoutAuto": "אוטומטי",
"LabelLayoutSinglePage": "עמוד יחיד", "LabelLayoutSinglePage": "עמוד יחיד",
"LabelLibrarySortByProgress": "התקדמות: עודכן לאחרונה",
"LabelLibrarySortByProgressFinished": "התקדמות: הושלם",
"LabelLibrarySortByProgressStarted": "התקדמות: הותחל",
"LabelLight": "בהיר", "LabelLight": "בהיר",
"LabelLineSpacing": "מרווח שורה", "LabelLineSpacing": "מרווח שורה",
"LabelListenAgain": "האזן שוב", "LabelListenAgain": "האזן שוב",
+43 -25
View File
@@ -63,6 +63,7 @@
"HeaderChapters": "Poglavlja", "HeaderChapters": "Poglavlja",
"HeaderCollection": "Zbirka", "HeaderCollection": "Zbirka",
"HeaderCollectionItems": "Stavke u zbirci", "HeaderCollectionItems": "Stavke u zbirci",
"HeaderConfirm": "Potvrdi",
"HeaderConnectionStatus": "Stanje veze", "HeaderConnectionStatus": "Stanje veze",
"HeaderDataSettings": "Podatkovne postavke", "HeaderDataSettings": "Podatkovne postavke",
"HeaderDetails": "Pojedinosti", "HeaderDetails": "Pojedinosti",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Nedavne sesije", "HeaderStatsRecentSessions": "Nedavne sesije",
"HeaderTableOfContents": "Sadržaj", "HeaderTableOfContents": "Sadržaj",
"HeaderUserInterfaceSettings": "Postavke korisničkog sučelja", "HeaderUserInterfaceSettings": "Postavke korisničkog sučelja",
"HeaderWelcome": "Dobrodošao, <strong>{0}</strong>",
"HeaderYourStats": "Vaša statistika", "HeaderYourStats": "Vaša statistika",
"LabelAddToPlaylist": "Dodaj na popis za izvođenje", "LabelAddToPlaylist": "Dodaj na popis za izvođenje",
"LabelAddedAt": "Dodano", "LabelAddedAt": "Dodano",
@@ -100,7 +102,7 @@
"LabelAlways": "Uvijek", "LabelAlways": "Uvijek",
"LabelAndroidAutoBrowseLimitForGrouping": "Ograničenje za abecednu kliznu traku", "LabelAndroidAutoBrowseLimitForGrouping": "Ograničenje za abecednu kliznu traku",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "Nemoj koristiti abecednu kliznu traku kada popis ima manje od ovoliko stavki", "LabelAndroidAutoBrowseLimitForGroupingHelp": "Nemoj koristiti abecednu kliznu traku kada popis ima manje od ovoliko stavki",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "Redoslijed knjiga serija", "LabelAndroidAutoBrowseSeriesSequenceOrder": "Redoslijed knjiga serijala",
"LabelAskConfirmation": "Zatraži potvrdu", "LabelAskConfirmation": "Zatraži potvrdu",
"LabelAuthor": "Autor", "LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Ime Prezime)", "LabelAuthorFirstLast": "Autor (Ime Prezime)",
@@ -110,14 +112,15 @@
"LabelAutoRewindTime": "Automatsko vraćanje unatrag", "LabelAutoRewindTime": "Automatsko vraćanje unatrag",
"LabelAutoSleepTimer": "Automatski timer za spavanje", "LabelAutoSleepTimer": "Automatski timer za spavanje",
"LabelAutoSleepTimerAutoRewind": "Automatsko vraćanje unatrag timera za spavanje", "LabelAutoSleepTimerAutoRewind": "Automatsko vraćanje unatrag timera za spavanje",
"LabelAutoSleepTimerAutoRewindHelp": "Nakon što se aktivira timer za spavanje, stavka koju ste slušali automatski će se vratiti na zadani položaj.", "LabelAutoSleepTimerAutoRewindHelp": "Kada automatsk timer za spavanje završi, ponovna reprodukcija stavke će se automatski vratiti na zadani položaj.",
"LabelAutoSleepTimerHelp": "Automatski timer za spavanje uključit će se u zadano vrijeme prilikom izvođenja medija.", "LabelAutoSleepTimerHelp": "Automatski timer za spavanje uključit će se u zadano vrijeme prilikom izvođenja medija.",
"LabelBooks": "Knjige", "LabelBooks": "Knjige",
"LabelByAuthor": "autor: {0}",
"LabelChapterTrack": "Zapis poglavlja", "LabelChapterTrack": "Zapis poglavlja",
"LabelChapters": "Poglavlja", "LabelChapters": "Poglavlja",
"LabelClosePlayer": "Zatvori reproduktor", "LabelClosePlayer": "Zatvori reproduktor",
"LabelCollapseSeries": "Serijale prikaži sažeto", "LabelCollapseSeries": "Sažmi serijal",
"LabelComplete": "Dovršeno", "LabelComplete": "Potpuno",
"LabelContinueBooks": "Nastavi knjige", "LabelContinueBooks": "Nastavi knjige",
"LabelContinueEpisodes": "Nastavi nastavke", "LabelContinueEpisodes": "Nastavi nastavke",
"LabelContinueListening": "Nastavi slušati", "LabelContinueListening": "Nastavi slušati",
@@ -151,10 +154,13 @@
"LabelFile": "Datoteka", "LabelFile": "Datoteka",
"LabelFileBirthtime": "Datoteka stvorena", "LabelFileBirthtime": "Datoteka stvorena",
"LabelFileModified": "Datoteka izmijenjena", "LabelFileModified": "Datoteka izmijenjena",
"LabelFilename": "Naziv datoteke", "LabelFilename": "Ime datoteke",
"LabelFinished": "Dovršeno", "LabelFinished": "Završeno",
"LabelFolder": "Mapa", "LabelFolder": "Mapa",
"LabelFontBoldness": "Debljina slova", "LabelFontBoldness": "Debljina slova",
"LabelFontFamily": "Skup pisma",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Veličina slova", "LabelFontScale": "Veličina slova",
"LabelGenre": "Žanr", "LabelGenre": "Žanr",
"LabelGenres": "Žanrovi", "LabelGenres": "Žanrovi",
@@ -174,6 +180,9 @@
"LabelLayout": "Prikaz", "LabelLayout": "Prikaz",
"LabelLayoutAuto": "Automatski", "LabelLayoutAuto": "Automatski",
"LabelLayoutSinglePage": "Jedna stranica", "LabelLayoutSinglePage": "Jedna stranica",
"LabelLibrarySortByProgress": "Napredak: Zadnje ažuriranje",
"LabelLibrarySortByProgressFinished": "Napredak: Završeno",
"LabelLibrarySortByProgressStarted": "Napredak: Započeto",
"LabelLight": "Lagano", "LabelLight": "Lagano",
"LabelLineSpacing": "Razmak između redaka", "LabelLineSpacing": "Razmak između redaka",
"LabelListenAgain": "Ponovno poslušaj", "LabelListenAgain": "Ponovno poslušaj",
@@ -199,7 +208,7 @@
"LabelNewestAuthors": "Najnoviji autori", "LabelNewestAuthors": "Najnoviji autori",
"LabelNewestEpisodes": "Najnoviji nastavci", "LabelNewestEpisodes": "Najnoviji nastavci",
"LabelNo": "Ne", "LabelNo": "Ne",
"LabelNotFinished": "Nije dovršeno", "LabelNotFinished": "Nezavršeno",
"LabelNotStarted": "Nije započeto", "LabelNotStarted": "Nije započeto",
"LabelNumEpisodes": "{0} nastavaka", "LabelNumEpisodes": "{0} nastavaka",
"LabelNumEpisodesIncomplete": "{0} nastavaka, {1} nedovršeno", "LabelNumEpisodesIncomplete": "{0} nastavaka, {1} nedovršeno",
@@ -227,12 +236,13 @@
"LabelRandomly": "Nasumično", "LabelRandomly": "Nasumično",
"LabelRead": "Čitaj", "LabelRead": "Čitaj",
"LabelReadAgain": "Ponovno čitaj", "LabelReadAgain": "Ponovno čitaj",
"LabelRecentSeries": "Najnoviji serijali", "LabelRecentSeries": "Nedavni serijali",
"LabelRecentlyAdded": "Nedavno dodano", "LabelRecentlyAdded": "Nedavno dodano",
"LabelRemoveFromPlaylist": "Ukloni s popisa za izvođenje", "LabelRemoveFromPlaylist": "Ukloni s popisa za izvođenje",
"LabelScaleElapsedTimeBySpeed": "Prilagodi preostalo vrijeme brzini izvođenja", "LabelScaleElapsedTimeBySpeed": "Prilagodi preostalo vrijeme brzini izvođenja",
"LabelSeason": "Sezona", "LabelSeason": "Sezona",
"LabelSelectADevice": "Odabir uređaja", "LabelSelectADevice": "Odabir uređaja",
"LabelSelectMediaType": "Odaberi vrstu medija",
"LabelSequenceAscending": "Redoslijed uzlazno", "LabelSequenceAscending": "Redoslijed uzlazno",
"LabelSequenceDescending": "Redoslijed silazno", "LabelSequenceDescending": "Redoslijed silazno",
"LabelSeries": "Serijal", "LabelSeries": "Serijal",
@@ -243,7 +253,7 @@
"LabelShowAll": "Prikaži sve", "LabelShowAll": "Prikaži sve",
"LabelSize": "Veličina", "LabelSize": "Veličina",
"LabelSleepTimer": "Timer za spavanje", "LabelSleepTimer": "Timer za spavanje",
"LabelSleepTimerAlmostDoneChime": "Zvučno upozorenje prije isteka", "LabelSleepTimerAlmostDoneChime": "Odsviraj zvuk upozorenja neposredno prije isteka",
"LabelSleepTimerAlmostDoneChimeHelp": "Reproduciraj zvučno upozorenje 30 sekundi prije isteka timera za spavanje", "LabelSleepTimerAlmostDoneChimeHelp": "Reproduciraj zvučno upozorenje 30 sekundi prije isteka timera za spavanje",
"LabelStart": "Početak", "LabelStart": "Početak",
"LabelStartTime": "Vrijeme pokretanja", "LabelStartTime": "Vrijeme pokretanja",
@@ -252,7 +262,7 @@
"LabelStatsDays": "Dani", "LabelStatsDays": "Dani",
"LabelStatsDaysListened": "Dana slušano", "LabelStatsDaysListened": "Dana slušano",
"LabelStatsInARow": "uzastopno", "LabelStatsInARow": "uzastopno",
"LabelStatsItemsFinished": "Dovršenih stavki", "LabelStatsItemsFinished": "Završene stavke",
"LabelStatsMinutes": "minute", "LabelStatsMinutes": "minute",
"LabelStatsMinutesListening": "Minuta odslušano", "LabelStatsMinutesListening": "Minuta odslušano",
"LabelStatsWeekListening": "Tjedno slušanje", "LabelStatsWeekListening": "Tjedno slušanje",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Poslužitelj Audiobookshelf nije povezan", "MessageAudiobookshelfServerNotConnected": "Poslužitelj Audiobookshelf nije povezan",
"MessageAudiobookshelfServerRequired": "<strong>Važno!</strong> Ova je aplikacija predviđena za rad s poslužiteljem Audiobookshelf na vašem poslužitelju kojim upravljate vi ili netko koga znate. Ova aplikacija ne nudi nikakav sadržaj.", "MessageAudiobookshelfServerRequired": "<strong>Važno!</strong> Ova je aplikacija predviđena za rad s poslužiteljem Audiobookshelf na vašem poslužitelju kojim upravljate vi ili netko koga znate. Ova aplikacija ne nudi nikakav sadržaj.",
"MessageBookshelfEmpty": "Polica s knjigama je prazna", "MessageBookshelfEmpty": "Polica s knjigama je prazna",
"MessageConfirmAppExit": "Želiš izaći iz aplikacije?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Sigurno želiš izbrisati red čekanja za preuzimanje epizode?",
"MessageConfirmDeleteLocalEpisode": "Želite li ukloniti preuzeti nastavak \"{0}\" sa svog uređaja? Ovo ne utječe na datoteku na poslužitelju.", "MessageConfirmDeleteLocalEpisode": "Želite li ukloniti preuzeti nastavak \"{0}\" sa svog uređaja? Ovo ne utječe na datoteku na poslužitelju.",
"MessageConfirmDeleteLocalFiles": "Želite li ukloniti preuzete datoteke ove stavke sa svog uređaja? Ovo ne utječe na datoteke na poslužitelju niti na vaš napredak.", "MessageConfirmDeleteLocalFiles": "Želite li ukloniti preuzete datoteke ove stavke sa svog uređaja? Ovo ne utječe na datoteke na poslužitelju niti na vaš napredak.",
"MessageConfirmDeleteServerConfig": "Ukloniti ovu konfiguraciju poslužitelja?",
"MessageConfirmDeleteServerEpisode": "Sigurno želiš izbrisati epizodu \"{0}\" sa servera?\nUpozorenje: Ovim će se izbrisati audio datoteka.",
"MessageConfirmDisableAutoTimer": "Sigurno želite onemogućiti automatski timer do kraja dana? Timer će se ponovno aktivirati po isteku trenutnog automatskog timera ili ako ponovno pokrenete aplikaciju.", "MessageConfirmDisableAutoTimer": "Sigurno želite onemogućiti automatski timer do kraja dana? Timer će se ponovno aktivirati po isteku trenutnog automatskog timera ili ako ponovno pokrenete aplikaciju.",
"MessageConfirmDiscardProgress": "Sigurno želite resetirati napredak?", "MessageConfirmDiscardProgress": "Sigurno želite resetirati napredak?",
"MessageConfirmDownloadUsingCellular": "Spremate se pokrenuti preuzimanje koristeći mobilne podatke. Ovo može uzrokovati dodatne troškove. Želite li nastaviti?", "MessageConfirmDownloadUsingCellular": "Spremate se pokrenuti preuzimanje koristeći mobilne podatke. Ovo može uzrokovati dodatne troškove. Želite li nastaviti?",
"MessageConfirmMarkAsFinished": "Sigurno želite označiti ovu stavku dovršenom?", "MessageConfirmMarkAsFinished": "Sigurno želite označiti ovu stavku kao završenu?",
"MessageConfirmPlaybackTime": "Započeti reprodukciju za \"{0}\" u {1}?",
"MessageConfirmRemoveBookmark": "Sigurno želite ukloniti knjižnu oznaku?", "MessageConfirmRemoveBookmark": "Sigurno želite ukloniti knjižnu oznaku?",
"MessageConfirmStreamingUsingCellular": "Pokrenut ćete strujanje korištenjem mobilnih podataka. Ovo može uzrokovati dodatne troškove. Želite li nastaviti?", "MessageConfirmStreamingUsingCellular": "Pokrenut ćete strujanje korištenjem mobilnih podataka. Ovo može uzrokovati dodatne troškove. Želite li nastaviti?",
"MessageDiscardProgress": "Zanemari napredak", "MessageDiscardProgress": "Zanemari napredak",
@@ -298,15 +313,16 @@
"MessageDownloading": "Preuzimanje...", "MessageDownloading": "Preuzimanje...",
"MessageDownloadingEpisode": "Preuzimam nastavak", "MessageDownloadingEpisode": "Preuzimam nastavak",
"MessageEpisodesQueuedForDownload": "{0} nastavak(a) u redu za preuzimanje", "MessageEpisodesQueuedForDownload": "{0} nastavak(a) u redu za preuzimanje",
"MessageFailedToRefreshToken": "Osvježavanje tokena nije uspjelo, potrebna je ponovna prijava",
"MessageFeedURLWillBe": "URL izvora bit će {0}", "MessageFeedURLWillBe": "URL izvora bit će {0}",
"MessageFetching": "Dohvaćam...", "MessageFetching": "Dohvaćanje...",
"MessageFollowTheProjectOnGithub": "Pratite projekt na GitHubu", "MessageFollowTheProjectOnGithub": "Pratite projekt na GitHubu",
"MessageItemDownloadCompleteFailedToCreate": "Stavka je uspješno preuzeta, ali izrada knjižničke stavke nije uspjela", "MessageItemDownloadCompleteFailedToCreate": "Stavka je uspješno preuzeta, ali izrada knjižničke stavke nije uspjela",
"MessageItemMissing": "Stavka nedostaje i treba je popraviti na poslužitelju. Ovo se u pravilu događa ako datotekama nije moguće pristupiti.", "MessageItemMissing": "Stavka nedostaje i treba je popraviti na poslužitelju. Ovo se u pravilu događa ako datotekama nije moguće pristupiti.",
"MessageLoading": "Učitavam...", "MessageLoading": "Učitavanje...",
"MessageLoadingServerData": "Učitavam podatke poslužitelja...", "MessageLoadingServerData": "Učitavanje podataka poslužitelja...",
"MessageLocalFolderDescription": "„Interna pohrana aplikacije” je dostupna samo s ovom aplikacijom. Ova aplikacija podržava samo medije koji su preuzeti izravno putem aplikacije. Dijeljene mape pohrane mogu se koristiti kako bi se ostalim aplikacijama omogućio pristup medijima koji su preuzeti pomoću ove aplikacije.", "MessageLocalFolderDescription": "„Interna pohrana aplikacije” je dostupna samo s ovom aplikacijom. Ova aplikacija podržava samo medije koji su preuzeti izravno putem aplikacije. Dijeljene mape pohrane mogu se koristiti kako bi se ostalim aplikacijama omogućio pristup medijima koji su preuzeti pomoću ove aplikacije.",
"MessageMarkAsFinished": "Označi kao dovršeno", "MessageMarkAsFinished": "Označi kao završeno",
"MessageMediaLinkedToADifferentServer": "Medijski zapis vezan je s Audiobookshelf poslužiteljem na drugoj adresi ({0}). Napredak će se sinkronizirati samo dok ste povezani s poslužiteljem na toj adresi.", "MessageMediaLinkedToADifferentServer": "Medijski zapis vezan je s Audiobookshelf poslužiteljem na drugoj adresi ({0}). Napredak će se sinkronizirati samo dok ste povezani s poslužiteljem na toj adresi.",
"MessageMediaLinkedToADifferentUser": "Medijski zapis povezan je s ovim poslužiteljem, ali preuzeo ga je drugi korisnik. Napredak će se sinkronizirati isključivo korisniku koji ga je preuzeo.", "MessageMediaLinkedToADifferentUser": "Medijski zapis povezan je s ovim poslužiteljem, ali preuzeo ga je drugi korisnik. Napredak će se sinkronizirati isključivo korisniku koji ga je preuzeo.",
"MessageMediaLinkedToServer": "Povezani s poslužiteljem {0}", "MessageMediaLinkedToServer": "Povezani s poslužiteljem {0}",
@@ -332,10 +348,10 @@
"MessageOldServerConnectionWarningHelp": "Vezu s ovim poslužiteljem postavili ste prije migracije baze podataka u inačici 2.3.0 objavljenoj u lipnju 2023. Nadolazeće ažuriranje poslužitelja onemogućit će prijavu korištenjem ove stare veze. Molimo izbrišite postojeću vezu s poslužiteljem i ponovno se povežite (korištenjem iste adrese poslužitelja i korisničkih podataka). Ukoliko imate preuzetih medijskih datoteka na ovom uređaju, morat ćete ih iznova preuzeti da bi se sinkronizirali s poslužiteljem.", "MessageOldServerConnectionWarningHelp": "Vezu s ovim poslužiteljem postavili ste prije migracije baze podataka u inačici 2.3.0 objavljenoj u lipnju 2023. Nadolazeće ažuriranje poslužitelja onemogućit će prijavu korištenjem ove stare veze. Molimo izbrišite postojeću vezu s poslužiteljem i ponovno se povežite (korištenjem iste adrese poslužitelja i korisničkih podataka). Ukoliko imate preuzetih medijskih datoteka na ovom uređaju, morat ćete ih iznova preuzeti da bi se sinkronizirali s poslužiteljem.",
"MessagePodcastSearchField": "Upišite izraz za pretraživanje ili URL RSS izvora", "MessagePodcastSearchField": "Upišite izraz za pretraživanje ili URL RSS izvora",
"MessageProgressSyncFailed": "Zadnji pokušaj slanja napretka slušanja poslužitelju nije uspio. Nastavit ćemo s pokušajima slanja napretka svakih 15 do 60 sekundi dok god traje izvođenje medijske datoteke.", "MessageProgressSyncFailed": "Zadnji pokušaj slanja napretka slušanja poslužitelju nije uspio. Nastavit ćemo s pokušajima slanja napretka svakih 15 do 60 sekundi dok god traje izvođenje medijske datoteke.",
"MessageReportBugsAndContribute": "Prijavite pogreške, zatražite značajke i doprinesite na", "MessageReportBugsAndContribute": "Prijavite pogreške, zatražite funkcije i doprinesite na",
"MessageSeriesAlreadyDownloaded": "Već ste preuzeli sve knjige iz ovog serijala.", "MessageSeriesAlreadyDownloaded": "Već ste preuzeli sve knjige ovog serijala.",
"MessageSeriesDownloadConfirm": "Želite li preuzeti {0} knjiga koje nedostaju s {1} datoteka, ukupno {2} u mapu {3}?", "MessageSeriesDownloadConfirm": "Želite li preuzeti nedostajuće {0} knjige sa {1} datotekom/a, ukupno {2} u mapu {3}?",
"MessageSeriesDownloadConfirmIos": "Preuzeti {0} knjiga koje nedostaju s {1} datoteka, ukupno {2}?", "MessageSeriesDownloadConfirmIos": "Želite li preuzeti nedostajuće {0} knjige sa {1} datotekoam/a, ukupno {2}?",
"MessageSocketConnectedOverMeteredCellular": "Socket je povezan preko mobilnih podataka", "MessageSocketConnectedOverMeteredCellular": "Socket je povezan preko mobilnih podataka",
"MessageSocketConnectedOverMeteredWifi": "Socket je povezan preko mobilne mreže s praćenjem potrošnje", "MessageSocketConnectedOverMeteredWifi": "Socket je povezan preko mobilne mreže s praćenjem potrošnje",
"MessageSocketConnectedOverUnmeteredCellular": "Socket je povezan preko mobilne mreže kod koje se ne prati potrošnja", "MessageSocketConnectedOverUnmeteredCellular": "Socket je povezan preko mobilne mreže kod koje se ne prati potrošnja",
@@ -347,12 +363,14 @@
"ToastBookmarkRemoveFailed": "Brisanje knjižne bilješke nije uspjelo", "ToastBookmarkRemoveFailed": "Brisanje knjižne bilješke nije uspjelo",
"ToastBookmarkUpdateFailed": "Ažuriranje knjižne oznake nije uspjelo", "ToastBookmarkUpdateFailed": "Ažuriranje knjižne oznake nije uspjelo",
"ToastDownloadNotAllowedOnCellular": "Preuzimanje korištenjem mobilnih podataka onemogućeno je", "ToastDownloadNotAllowedOnCellular": "Preuzimanje korištenjem mobilnih podataka onemogućeno je",
"ToastItemMarkedAsFinishedFailed": "Označavanje kao Dovršeno nije uspjelo", "ToastItemMarkedAsFinishedFailed": "Označavanje kao „Završeno nije uspjelo",
"ToastItemMarkedAsNotFinishedFailed": "Označavanje kao Nije dovršeno nije uspjelo", "ToastItemMarkedAsNotFinishedFailed": "Označavanje kao „Nezavršeno nije uspjelo",
"ToastPlaylistCreateFailed": "Popis za izvođenje nije izrađen", "ToastPlaylistCreateFailed": "Izrada popisa za izvođenje nije uspjela",
"ToastPodcastCreateFailed": "Podcast nije izrađen", "ToastPodcastCreateFailed": "Stvaranje podcasta nije uspjelo",
"ToastPodcastCreateSuccess": "Podcast uspješno izrađen", "ToastPodcastCreateSuccess": "Podcast uspješno izrađen",
"ToastRSSFeedCloseFailed": "RSS izvor nije uspješno zatvoren", "ToastRSSFeedCloseFailed": "Zatvaranje RSS izvora nije uspjelo",
"ToastRSSFeedCloseSuccess": "RSS izvor zatvoren", "ToastRSSFeedCloseSuccess": "RSS izvor zatvoren",
"ToastStreamingNotAllowedOnCellular": "Strujanje korištenjem mobilnih podataka onemogućeno je" "ToastStreamingNotAllowedOnCellular": "Strujanje korištenjem mobilnih podataka onemogućeno je",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+26 -9
View File
@@ -63,6 +63,7 @@
"HeaderChapters": "Fejezetek", "HeaderChapters": "Fejezetek",
"HeaderCollection": "Gyűjtemény", "HeaderCollection": "Gyűjtemény",
"HeaderCollectionItems": "Gyűjtemény elemek", "HeaderCollectionItems": "Gyűjtemény elemek",
"HeaderConfirm": "Megerősítés",
"HeaderConnectionStatus": "Kapcsolat állapota", "HeaderConnectionStatus": "Kapcsolat állapota",
"HeaderDataSettings": "Adat beállítások", "HeaderDataSettings": "Adat beállítások",
"HeaderDetails": "Részletek", "HeaderDetails": "Részletek",
@@ -87,10 +88,11 @@
"HeaderSettings": "Beállítások", "HeaderSettings": "Beállítások",
"HeaderSleepTimer": "Alvásidőzítő", "HeaderSleepTimer": "Alvásidőzítő",
"HeaderSleepTimerSettings": "Alvásidőzítő beállításai", "HeaderSleepTimerSettings": "Alvásidőzítő beállításai",
"HeaderStatsMinutesListeningChart": "Hallgatási grafikon percekben (az elmúlt 7 napból)", "HeaderStatsMinutesListeningChart": "Hallgatási grafikon percben (az elmúlt 7 napból)",
"HeaderStatsRecentSessions": "Legutóbbi munkamenetek", "HeaderStatsRecentSessions": "Legutóbbi munkamenetek",
"HeaderTableOfContents": "Tartalomjegyzék", "HeaderTableOfContents": "Tartalomjegyzék",
"HeaderUserInterfaceSettings": "Felhasználói felület beállításai", "HeaderUserInterfaceSettings": "Felhasználói felület beállításai",
"HeaderWelcome": "Üdv, <strong>{0}</strong>",
"HeaderYourStats": "Saját statisztikák", "HeaderYourStats": "Saját statisztikák",
"LabelAddToPlaylist": "Hozzáadás a lejátszási listához", "LabelAddToPlaylist": "Hozzáadás a lejátszási listához",
"LabelAddedAt": "Hozzáadva ekkor", "LabelAddedAt": "Hozzáadva ekkor",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Amikor az automatikus alvásidőzítő befejeződik, az elem újrajátszásakor automatikusan visszatekeri a pozíciót.", "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.", "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", "LabelBooks": "Könyv",
"LabelByAuthor": "{0} által",
"LabelChapterTrack": "Fejezet sáv", "LabelChapterTrack": "Fejezet sáv",
"LabelChapters": "Fejezetek", "LabelChapters": "Fejezetek",
"LabelClosePlayer": "Lejátszó bezárása", "LabelClosePlayer": "Lejátszó bezárása",
@@ -126,7 +129,7 @@
"LabelCustomTime": "Egyéni idő", "LabelCustomTime": "Egyéni idő",
"LabelDescription": "Leírás", "LabelDescription": "Leírás",
"LabelDisableAudioFadeOut": "Hang fokozatos csendesítés letiltása", "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.", "LabelDisableAudioFadeOutHelp": "A hangerő csökkenni kezd az alvásidőzítő utolsó percében. Kapcsold be ezt a beállítást a halkítás kikapcsolásához.",
"LabelDisableAutoRewind": "Automatikus visszatekerés letiltása", "LabelDisableAutoRewind": "Automatikus visszatekerés letiltása",
"LabelDisableShakeToReset": "Rázás a visszaállításhoz 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.", "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.",
@@ -155,12 +158,15 @@
"LabelFinished": "Befejezve", "LabelFinished": "Befejezve",
"LabelFolder": "Mappa", "LabelFolder": "Mappa",
"LabelFontBoldness": "Betű vastagság", "LabelFontBoldness": "Betű vastagság",
"LabelFontFamily": "Betűcsalád",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Betűméret skála", "LabelFontScale": "Betűméret skála",
"LabelGenre": "Műfaj", "LabelGenre": "Műfaj",
"LabelGenres": "Műfajok", "LabelGenres": "Műfajok",
"LabelHapticFeedback": "Tapintás visszajelzés", "LabelHapticFeedback": "Tapintás visszajelzés",
"LabelHasEbook": "Van e-könyv", "LabelHasEbook": "Van e-könyv",
"LabelHasSupplementaryEbook": "Van kiegészítő e-könyv", "LabelHasSupplementaryEbook": "Van kiegészítő e-könyve",
"LabelHeavy": "Nehéz", "LabelHeavy": "Nehéz",
"LabelHigh": "Magas", "LabelHigh": "Magas",
"LabelHost": "Kiszolgáló", "LabelHost": "Kiszolgáló",
@@ -174,6 +180,9 @@
"LabelLayout": "Elrendezés", "LabelLayout": "Elrendezés",
"LabelLayoutAuto": "Automatikus", "LabelLayoutAuto": "Automatikus",
"LabelLayoutSinglePage": "Egyoldalas", "LabelLayoutSinglePage": "Egyoldalas",
"LabelLibrarySortByProgress": "Folyamat: Legutóbbi frissítés",
"LabelLibrarySortByProgressFinished": "Folyamat: Befejezve",
"LabelLibrarySortByProgressStarted": "Folyamat: Elindult",
"LabelLight": "Könnyű", "LabelLight": "Könnyű",
"LabelLineSpacing": "Sorköz", "LabelLineSpacing": "Sorköz",
"LabelListenAgain": "Újrahallgatás", "LabelListenAgain": "Újrahallgatás",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Eltelt idő skálázása sebesség szerint", "LabelScaleElapsedTimeBySpeed": "Eltelt idő skálázása sebesség szerint",
"LabelSeason": "Évad", "LabelSeason": "Évad",
"LabelSelectADevice": "Eszköz kiválasztása", "LabelSelectADevice": "Eszköz kiválasztása",
"LabelSelectMediaType": "Médiatípus kiválasztása",
"LabelSequenceAscending": "Emelkedő sorrend", "LabelSequenceAscending": "Emelkedő sorrend",
"LabelSequenceDescending": "Csökkenő sorrend", "LabelSequenceDescending": "Csökkenő sorrend",
"LabelSeries": "Sorozat", "LabelSeries": "Sorozat",
@@ -249,10 +259,10 @@
"LabelStartTime": "Kezdési idő", "LabelStartTime": "Kezdési idő",
"LabelStatsBestDay": "Legjobb nap", "LabelStatsBestDay": "Legjobb nap",
"LabelStatsDailyAverage": "Napi átlag", "LabelStatsDailyAverage": "Napi átlag",
"LabelStatsDays": "Napok", "LabelStatsDays": "Nap",
"LabelStatsDaysListened": "Hallgatással töltött napok", "LabelStatsDaysListened": "Hallgatással töltött nap",
"LabelStatsInARow": "egymás után", "LabelStatsInARow": "egymás után",
"LabelStatsItemsFinished": "Befejezett elemek", "LabelStatsItemsFinished": "Befejezett elem",
"LabelStatsMinutes": "perc", "LabelStatsMinutes": "perc",
"LabelStatsMinutesListening": "Hallgatás percekben", "LabelStatsMinutesListening": "Hallgatás percekben",
"LabelStatsWeekListening": "Heti hallgatás", "LabelStatsWeekListening": "Heti hallgatás",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Az Audiobookshelf szerver nem csatlakozik", "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.", "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", "MessageBookshelfEmpty": "A könyvespolc üres",
"MessageConfirmAppExit": "Biztosan ki szeretne lépni az alkalmazásból?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Biztosan törölni szeretné az epizódletöltési várólistát?",
"MessageConfirmDeleteLocalEpisode": "\"{0}\" helyi epizód eltávolítása az eszközről? A szerveren lévő fájl nem érintett.", "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.", "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.",
"MessageConfirmDeleteServerConfig": "Eltávolítja ezt a szerverkonfigurációt?",
"MessageConfirmDeleteServerEpisode": "Biztosan törölni szeretnéd a(z) „{0}” epizódot a szerverről?\nFigyelem: ezzel az audiófájl is törlődik.",
"MessageConfirmDisableAutoTimer": "Biztos, hogy a mai nap hátralévő részére ki akarja kapcsolni az automatikus időzítőt? Az időzítő újra aktiválódik az automatikus alvásidőzítő időszak végén, vagy ha újraindítja az alkalmazást.", "MessageConfirmDisableAutoTimer": "Biztos, hogy a mai nap hátralévő részére ki akarja kapcsolni az automatikus időzítőt? Az időzítő újra aktiválódik az automatikus alvásidőzítő időszak végén, vagy ha újraindítja az alkalmazást.",
"MessageConfirmDiscardProgress": "Biztosan alaphelyzetbe akarja állítani a haladást?", "MessageConfirmDiscardProgress": "Biztosan alaphelyzetbe akarja állítani a haladást?",
"MessageConfirmDownloadUsingCellular": "Mobiladat-hálózaton keresztül készülsz letölteni. Ez adatforgalmi díjakkal járhat. Szeretnéd folytatni?", "MessageConfirmDownloadUsingCellular": "Mobiladat-hálózaton keresztül készülsz letölteni. Ez adatforgalmi díjakkal járhat. Szeretnéd folytatni?",
"MessageConfirmMarkAsFinished": "Biztosan befejezettnek jelöli ezt az elemet?", "MessageConfirmMarkAsFinished": "Biztosan befejezettnek jelöli ezt az elemet?",
"MessageConfirmPlaybackTime": "Elindítod a(z) „{0}” lejátszását {1}-tól?",
"MessageConfirmRemoveBookmark": "Biztosan eltávolítja a könyvjelzőt?", "MessageConfirmRemoveBookmark": "Biztosan eltávolítja a könyvjelzőt?",
"MessageConfirmStreamingUsingCellular": "Mobiladat-hálózaton keresztül készülsz streamelni. Ez adatforgalmi díjakkal járhat. Szeretnéd folytatni?", "MessageConfirmStreamingUsingCellular": "Mobiladat-hálózaton keresztül készülsz streamelni. Ez adatforgalmi díjakkal járhat. Szeretnéd folytatni?",
"MessageDiscardProgress": "Haladás elvetése", "MessageDiscardProgress": "Haladás elvetése",
@@ -308,11 +323,11 @@
"MessageLoadingServerData": "Szerveradatok betöltése...", "MessageLoadingServerData": "Szerveradatok betöltése...",
"MessageLocalFolderDescription": "A „Belső alkalmazás-tároló” csak ezzel az alkalmazással érhető el. Ez az alkalmazás csak a közvetlenül az alkalmazáson keresztül letöltött médiát támogatja. A megosztott tároló mappák segítségével más alkalmazások is hozzáférhetnek az alkalmazás által letöltött médiához.", "MessageLocalFolderDescription": "A „Belső alkalmazás-tároló” csak ezzel az alkalmazással érhető el. Ez az alkalmazás csak a közvetlenül az alkalmazáson keresztül letöltött médiát támogatja. A megosztott tároló mappák segítségével más alkalmazások is hozzáférhetnek az alkalmazás által letöltött médiához.",
"MessageMarkAsFinished": "Befejezettnek jelölés", "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.", "MessageMediaLinkedToADifferentServer": "A média egy másik címen ({0}) lévő Audiobookshelf szerverhez van kapcsolva. A folyamatjelölés akkor lesz szinkronizálva, amikor ehhez a szervercímhez csatlakozik.",
"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.", "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}", "MessageMediaLinkedToServer": "Szerverhez kapcsolt {0}",
"MessageMediaLinkedToThisServer": "A letöltött média ehhez a szerverhez kapcsolódik", "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.", "MessageMediaNotLinkedToServer": "A média nincs Audiobookshelf szerverhez kapcsolva. A folyamatjelölés nem lesz szinkronizálva.",
"MessageNoBookmarks": "Nincsenek könyvjelzők", "MessageNoBookmarks": "Nincsenek könyvjelzők",
"MessageNoChapters": "Nincsenek fejezetek", "MessageNoChapters": "Nincsenek fejezetek",
"MessageNoCollections": "Nincs gyűjtemény", "MessageNoCollections": "Nincs gyűjtemény",
@@ -355,5 +370,7 @@
"ToastPodcastCreateSuccess": "A podcast sikeresen létrehozva", "ToastPodcastCreateSuccess": "A podcast sikeresen létrehozva",
"ToastRSSFeedCloseFailed": "Az RSS hírcsatorna bezárása sikertelen", "ToastRSSFeedCloseFailed": "Az RSS hírcsatorna bezárása sikertelen",
"ToastRSSFeedCloseSuccess": "Az RSS hírcsatorna sikeresen bezárva", "ToastRSSFeedCloseSuccess": "Az RSS hírcsatorna sikeresen bezárva",
"ToastStreamingNotAllowedOnCellular": "Streamelés mobilinternettel nem engedélyezett" "ToastStreamingNotAllowedOnCellular": "Streamelés mobilinternettel nem engedélyezett",
"UnitMinutesShort": "{0}p",
"UnitSecondsShort": "{0}mp"
} }
+1
View File
@@ -0,0 +1 @@
{}
+21 -3
View File
@@ -50,7 +50,7 @@
"ButtonSendEbookToDevice": "Invia il libro al dispositivo", "ButtonSendEbookToDevice": "Invia il libro al dispositivo",
"ButtonSeries": "Serie", "ButtonSeries": "Serie",
"ButtonSetTimer": "Imposta Timer", "ButtonSetTimer": "Imposta Timer",
"ButtonStream": "Stream", "ButtonStream": "Flusso",
"ButtonSubmit": "Invia", "ButtonSubmit": "Invia",
"ButtonSwitchServerUser": "Cambia Server/Utente", "ButtonSwitchServerUser": "Cambia Server/Utente",
"ButtonUnmaskServerAddress": "Mostra l'indirizzo server", "ButtonUnmaskServerAddress": "Mostra l'indirizzo server",
@@ -63,6 +63,7 @@
"HeaderChapters": "Capitoli", "HeaderChapters": "Capitoli",
"HeaderCollection": "Raccolta", "HeaderCollection": "Raccolta",
"HeaderCollectionItems": "Elementi della raccolta", "HeaderCollectionItems": "Elementi della raccolta",
"HeaderConfirm": "Conferma",
"HeaderConnectionStatus": "Stato connessione", "HeaderConnectionStatus": "Stato connessione",
"HeaderDataSettings": "Impostazioni dei dati", "HeaderDataSettings": "Impostazioni dei dati",
"HeaderDetails": "Dettagli", "HeaderDetails": "Dettagli",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Sessioni Recenti", "HeaderStatsRecentSessions": "Sessioni Recenti",
"HeaderTableOfContents": "Indice", "HeaderTableOfContents": "Indice",
"HeaderUserInterfaceSettings": "Impostazioni interfaccia utente", "HeaderUserInterfaceSettings": "Impostazioni interfaccia utente",
"HeaderWelcome": "Benvenuto, <strong>{0}</strong>",
"HeaderYourStats": "Statistiche personali", "HeaderYourStats": "Statistiche personali",
"LabelAddToPlaylist": "Aggiungi alla playlist", "LabelAddToPlaylist": "Aggiungi alla playlist",
"LabelAddedAt": "Aggiunto il", "LabelAddedAt": "Aggiunto il",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Al termine del timer di spegnimento automatico, la riproduzione dell'elemento riavvolgerà automaticamente la tua posizione.", "LabelAutoSleepTimerAutoRewindHelp": "Al termine del timer di spegnimento automatico, la riproduzione dell'elemento riavvolgerà automaticamente la tua posizione.",
"LabelAutoSleepTimerHelp": "Durante la riproduzione di contenuti multimediali tra l'ora di inizio e quella di fine specificate, verrà avviato automaticamente un timer di spegnimento.", "LabelAutoSleepTimerHelp": "Durante la riproduzione di contenuti multimediali tra l'ora di inizio e quella di fine specificate, verrà avviato automaticamente un timer di spegnimento.",
"LabelBooks": "Libri", "LabelBooks": "Libri",
"LabelByAuthor": "di {0}",
"LabelChapterTrack": "Traccia capitolo", "LabelChapterTrack": "Traccia capitolo",
"LabelChapters": "Capitoli", "LabelChapters": "Capitoli",
"LabelClosePlayer": "Chiudi player", "LabelClosePlayer": "Chiudi player",
@@ -155,6 +158,9 @@
"LabelFinished": "Finita", "LabelFinished": "Finita",
"LabelFolder": "Cartella", "LabelFolder": "Cartella",
"LabelFontBoldness": "Grassetto", "LabelFontBoldness": "Grassetto",
"LabelFontFamily": "Famiglia caratteri",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Dimensione font", "LabelFontScale": "Dimensione font",
"LabelGenre": "Genere", "LabelGenre": "Genere",
"LabelGenres": "Generi", "LabelGenres": "Generi",
@@ -174,6 +180,9 @@
"LabelLayout": "Disposizione", "LabelLayout": "Disposizione",
"LabelLayoutAuto": "Automatico", "LabelLayoutAuto": "Automatico",
"LabelLayoutSinglePage": "Pagina singola", "LabelLayoutSinglePage": "Pagina singola",
"LabelLibrarySortByProgress": "Progresso: ultimo aggiornamento",
"LabelLibrarySortByProgressFinished": "Progresso: finito",
"LabelLibrarySortByProgressStarted": "Progresso: iniziato",
"LabelLight": "Leggera", "LabelLight": "Leggera",
"LabelLineSpacing": "Interlinea", "LabelLineSpacing": "Interlinea",
"LabelListenAgain": "Ascolta ancora", "LabelListenAgain": "Ascolta ancora",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Scala il tempo trascorso in base alla velocità", "LabelScaleElapsedTimeBySpeed": "Scala il tempo trascorso in base alla velocità",
"LabelSeason": "Stagione", "LabelSeason": "Stagione",
"LabelSelectADevice": "Seletiona dispositivo", "LabelSelectADevice": "Seletiona dispositivo",
"LabelSelectMediaType": "Seleziona il tipo di supporto",
"LabelSequenceAscending": "Sequenza ascendente", "LabelSequenceAscending": "Sequenza ascendente",
"LabelSequenceDescending": "Sequenza decrescente", "LabelSequenceDescending": "Sequenza decrescente",
"LabelSeries": "Serie", "LabelSeries": "Serie",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf server non connesso", "MessageAudiobookshelfServerNotConnected": "Audiobookshelf server non connesso",
"MessageAudiobookshelfServerRequired": "<strong>Importante!</strong> Questa app è progettata per funzionare con un server Audiobookshelf ospitato da te o da qualcuno che conosci. Questa app non fornisce alcun contenuto.", "MessageAudiobookshelfServerRequired": "<strong>Importante!</strong> Questa app è progettata per funzionare con un server Audiobookshelf ospitato da te o da qualcuno che conosci. Questa app non fornisce alcun contenuto.",
"MessageBookshelfEmpty": "Scaffale vuoto", "MessageBookshelfEmpty": "Scaffale vuoto",
"MessageConfirmAppExit": "Volevi uscire dall'app?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Sei sicuro di voler cancellare la coda di download degli episodi?",
"MessageConfirmDeleteLocalEpisode": "Rimuovi episodi locali \"{0}\" dal tuo dispositivo? i file sul server non verranno toccati.", "MessageConfirmDeleteLocalEpisode": "Rimuovi episodi locali \"{0}\" dal tuo dispositivo? i file sul server non verranno toccati.",
"MessageConfirmDeleteLocalFiles": "Rimuovi i file locali dell'oggetto? I file sul server e i progressi non verranno toccati.", "MessageConfirmDeleteLocalFiles": "Rimuovi i file locali dell'oggetto? I file sul server e i progressi non verranno toccati.",
"MessageConfirmDeleteServerConfig": "Rimuovere questa configurazione del server?",
"MessageConfirmDeleteServerEpisode": "Sei sicuro di voler eliminare l'episodio \"{0}\" dal server?\nAttenzione: questa operazione eliminerà il file audio.",
"MessageConfirmDisableAutoTimer": "Vuoi davvero disattivare il timer automatico per il resto della giornata? Il timer verrà riattivato alla fine di questo periodo di spegnimento automatico o se riavvii l'app.", "MessageConfirmDisableAutoTimer": "Vuoi davvero disattivare il timer automatico per il resto della giornata? Il timer verrà riattivato alla fine di questo periodo di spegnimento automatico o se riavvii l'app.",
"MessageConfirmDiscardProgress": "Sei sicuro/sicura di voler ripristinare i tuoi progressi?", "MessageConfirmDiscardProgress": "Sei sicuro/sicura di voler ripristinare i tuoi progressi?",
"MessageConfirmDownloadUsingCellular": "Stai per eseguire il download utilizzando la rete dati cellulare. Ciò potrebbe includere addebiti per i dati dell'operatore. Vuoi continuare?", "MessageConfirmDownloadUsingCellular": "Stai per eseguire il download utilizzando la rete dati cellulare. Ciò potrebbe includere addebiti per i dati dell'operatore. Vuoi continuare?",
"MessageConfirmMarkAsFinished": "Sei sicuro/sicura di voler contrassegnare questo elemento come finito?", "MessageConfirmMarkAsFinished": "Sei sicuro/sicura di voler contrassegnare questo elemento come finito?",
"MessageConfirmPlaybackTime": "Avviare la riproduzione per \"{0}\" alle {1}?",
"MessageConfirmRemoveBookmark": "Sei sicuro/sicura di voler rimuovere il segnalibro?", "MessageConfirmRemoveBookmark": "Sei sicuro/sicura di voler rimuovere il segnalibro?",
"MessageConfirmStreamingUsingCellular": "Stai per eseguire lo streaming utilizzando la rete dati. Ciò potrebbe includere addebiti per i dati dell'operatore. Vuoi continuare?", "MessageConfirmStreamingUsingCellular": "Stai per eseguire lo streaming utilizzando la rete dati. Ciò potrebbe includere addebiti per i dati dell'operatore. Vuoi continuare?",
"MessageDiscardProgress": "Elimina i progressi", "MessageDiscardProgress": "Elimina i progressi",
@@ -298,6 +313,7 @@
"MessageDownloading": "Scaricamento…", "MessageDownloading": "Scaricamento…",
"MessageDownloadingEpisode": "Scaricamento dellepisodio in corso", "MessageDownloadingEpisode": "Scaricamento dellepisodio in corso",
"MessageEpisodesQueuedForDownload": "{0} episodio(i) in coda per lo scaricamento", "MessageEpisodesQueuedForDownload": "{0} episodio(i) in coda per lo scaricamento",
"MessageFailedToRefreshToken": "Rinnovo del Token fallito, riautenticazione richiesta",
"MessageFeedURLWillBe": "lURL del flusso sarà {0}", "MessageFeedURLWillBe": "lURL del flusso sarà {0}",
"MessageFetching": "Recupero info…", "MessageFetching": "Recupero info…",
"MessageFollowTheProjectOnGithub": "Segui il progetto su GitHub", "MessageFollowTheProjectOnGithub": "Segui il progetto su GitHub",
@@ -318,7 +334,7 @@
"MessageNoItems": "Nessun oggetto", "MessageNoItems": "Nessun oggetto",
"MessageNoItemsFound": "Nessun oggetto trovato", "MessageNoItemsFound": "Nessun oggetto trovato",
"MessageNoListeningSessions": "Nessuna sessione di ascolto", "MessageNoListeningSessions": "Nessuna sessione di ascolto",
"MessageNoLogs": "Nessun registro", "MessageNoLogs": "Nessun rapporto",
"MessageNoMediaFolders": "Nessuna cartella media", "MessageNoMediaFolders": "Nessuna cartella media",
"MessageNoNetworkConnection": "Nessuna connessione di rete", "MessageNoNetworkConnection": "Nessuna connessione di rete",
"MessageNoPodcastsFound": "Nessun podcast trovato", "MessageNoPodcastsFound": "Nessun podcast trovato",
@@ -354,5 +370,7 @@
"ToastPodcastCreateSuccess": "Podcast creato correttamente", "ToastPodcastCreateSuccess": "Podcast creato correttamente",
"ToastRSSFeedCloseFailed": "Errore chiusura flusso RSS", "ToastRSSFeedCloseFailed": "Errore chiusura flusso RSS",
"ToastRSSFeedCloseSuccess": "Flusso RSS chiuso", "ToastRSSFeedCloseSuccess": "Flusso RSS chiuso",
"ToastStreamingNotAllowedOnCellular": "Lo streaming non è consentito sui dati mobili" "ToastStreamingNotAllowedOnCellular": "Lo streaming non è consentito sui dati mobili",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+143 -19
View File
@@ -40,15 +40,17 @@
"ButtonPlayEpisode": "エピソードを再生", "ButtonPlayEpisode": "エピソードを再生",
"ButtonPlaylists": "プレイリスト", "ButtonPlaylists": "プレイリスト",
"ButtonRead": "読む", "ButtonRead": "読む",
"ButtonReadLess": "閉じる", "ButtonReadLess": "少なく表示",
"ButtonReadMore": "もっと見る", "ButtonReadMore": "もっと見る",
"ButtonRemove": "削除", "ButtonRemove": "削除",
"ButtonRemoveFromServer": "サーバーから削除", "ButtonRemoveFromServer": "サーバーから削除",
"ButtonSave": "保存", "ButtonSave": "保存",
"ButtonSaveOrder": "並び替えを保存", "ButtonSaveOrder": "並び替えを保存",
"ButtonSearch": "検索", "ButtonSearch": "検索",
"ButtonSendEbookToDevice": "Ebookをデバイスに転送",
"ButtonSeries": "シリーズ", "ButtonSeries": "シリーズ",
"ButtonSetTimer": "タイマーを設定", "ButtonSetTimer": "タイマーを設定",
"ButtonStream": "ストリーム",
"ButtonSubmit": "送信", "ButtonSubmit": "送信",
"ButtonSwitchServerUser": "サーバー/ユーザーの切り替え", "ButtonSwitchServerUser": "サーバー/ユーザーの切り替え",
"ButtonUnmaskServerAddress": "サーバーアドレスを表示", "ButtonUnmaskServerAddress": "サーバーアドレスを表示",
@@ -61,6 +63,7 @@
"HeaderChapters": "チャプター", "HeaderChapters": "チャプター",
"HeaderCollection": "コレクション", "HeaderCollection": "コレクション",
"HeaderCollectionItems": "コレクションの項目", "HeaderCollectionItems": "コレクションの項目",
"HeaderConfirm": "確認",
"HeaderConnectionStatus": "接続状況", "HeaderConnectionStatus": "接続状況",
"HeaderDataSettings": "データ設定", "HeaderDataSettings": "データ設定",
"HeaderDetails": "詳細", "HeaderDetails": "詳細",
@@ -74,91 +77,212 @@
"HeaderLocalLibraryItems": "ローカルライブラリーの項目", "HeaderLocalLibraryItems": "ローカルライブラリーの項目",
"HeaderNewPlaylist": "新しいプレイリスト", "HeaderNewPlaylist": "新しいプレイリスト",
"HeaderOpenRSSFeed": "RSS Feedを開く", "HeaderOpenRSSFeed": "RSS Feedを開く",
"HeaderPlaybackSettings": "再生設定",
"HeaderPlaylist": "プレイリスト", "HeaderPlaylist": "プレイリスト",
"HeaderPlaylistItems": "プレイリストアイテム", "HeaderPlaylistItems": "プレイリストアイテム",
"HeaderProgressSyncFailed": "進行中の同期失敗", "HeaderProgressSyncFailed": "進行中の同期失敗",
"HeaderRSSFeed": "RSS Feed", "HeaderRSSFeed": "RSSフィード",
"HeaderRSSFeedGeneral": "RSS 詳細", "HeaderRSSFeedGeneral": "RSS 詳細",
"HeaderRSSFeedIsOpen": "RSSフィードが開いています",
"HeaderSelectDownloadLocation": "保存場所の選択", "HeaderSelectDownloadLocation": "保存場所の選択",
"HeaderSettings": "設定", "HeaderSettings": "設定",
"HeaderSleepTimer": "スリープタイマー", "HeaderSleepTimer": "スリープタイマー",
"HeaderSleepTimerSettings": "スリープタイマー設定", "HeaderSleepTimerSettings": "スリープタイマー設定",
"HeaderStatsMinutesListeningChart": "過去7日間の視聴時間(分)", "HeaderStatsMinutesListeningChart": "過去7日間のリスニング時間(分)",
"HeaderUserInterfaceSettings": "UI設定", "HeaderStatsRecentSessions": "最近の再生履歴",
"HeaderTableOfContents": "目次",
"HeaderUserInterfaceSettings": "画面設定",
"HeaderWelcome": "ようこそ <strong>{0}</strong>",
"HeaderYourStats": "再生統計",
"LabelAddToPlaylist": "プレイリストの追加", "LabelAddToPlaylist": "プレイリストの追加",
"LabelAddedAt": "追加日時",
"LabelAddedDate": "追加日時 {0}",
"LabelAll": "すべて",
"LabelAllowSeekingOnMediaControls": "メディア通知コントロールでの位置情報の取得を許可", "LabelAllowSeekingOnMediaControls": "メディア通知コントロールでの位置情報の取得を許可",
"LabelAlways": "常に",
"LabelAndroidAutoBrowseLimitForGrouping": "アルファベット順表示の制限",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "表示項目数がこの数未満の場合、アルファベット順ドローダウンを使用しません",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "シリーズ内の順序",
"LabelAskConfirmation": "確認を求める", "LabelAskConfirmation": "確認を求める",
"LabelAuthor": "者", "LabelAuthor": "者",
"LabelAuthorFirstLast": "者(名 氏)", "LabelAuthorFirstLast": "者(名 氏)",
"LabelAuthorLastFirst": "者(氏 名)", "LabelAuthorLastFirst": "者(氏 名)",
"LabelAuthors": "者", "LabelAuthors": "者",
"LabelAutoDownloadEpisodes": "エピソードの自動ダウンロード", "LabelAutoDownloadEpisodes": "エピソードの自動ダウンロード",
"LabelAutoRewindTime": "自動巻き戻し時間",
"LabelAutoSleepTimer": "自動スリープタイマー", "LabelAutoSleepTimer": "自動スリープタイマー",
"LabelBooks": "ほん", "LabelAutoSleepTimerAutoRewind": "スリープタイマー時の自動巻き戻し",
"LabelAutoSleepTimerAutoRewindHelp": "自動スリープタイマーが終了した後、再生を再開すると再生位置が自動的に巻き戻されます。",
"LabelAutoSleepTimerHelp": "指定した開始時刻から終了時刻の間にメディアを再生すると、スリープタイマーが自動的に開始されます。",
"LabelBooks": "本",
"LabelByAuthor": "by {0}",
"LabelChapterTrack": "チャプタートラック", "LabelChapterTrack": "チャプタートラック",
"LabelChapters": "チャプター", "LabelChapters": "チャプター",
"LabelClosePlayer": "プレヤーを閉じる", "LabelClosePlayer": "プレヤーを閉じる",
"LabelCollapseSeries": "シリーズを折りたたむ",
"LabelComplete": "完了", "LabelComplete": "完了",
"LabelContinueListening": "続きから聞く", "LabelContinueBooks": "続きを読む本",
"LabelContinueEpisodes": "続きのエピソード",
"LabelContinueListening": "続きから聴く",
"LabelContinueReading": "続きを読む",
"LabelContinueSeries": "シリーズを続ける",
"LabelCustomTime": "指定時間",
"LabelDescription": "説明", "LabelDescription": "説明",
"LabelDisableAudioFadeOut": "オーディオフェードアウトを無効", "LabelDisableAudioFadeOut": "オーディオフェードアウトを無効",
"LabelDisableAudioFadeOutHelp": "スリープタイマーの残り時間が1分を切ると、音量が徐々に小さくなります。フェードアウトしないようにするには、この設定を有効にしてください。", "LabelDisableAudioFadeOutHelp": "スリープタイマーの残り時間が1分を切ると、音量が徐々に小さくなります。フェードアウトしないようにするには、この設定を有効にしてください。",
"LabelDisableAutoRewind": "自動巻き戻しを無効", "LabelDisableAutoRewind": "自動巻き戻しを無効",
"LabelDisableShakeToReset": "シェイクでリセットを無効化",
"LabelDisableShakeToResetHelp": "タイマー作動中、またはタイマー終了後2分以内に端末を振ると、スリープタイマーがリセットされます。この設定を有効にすると、振ってリセットする機能を無効にできます。", "LabelDisableShakeToResetHelp": "タイマー作動中、またはタイマー終了後2分以内に端末を振ると、スリープタイマーがリセットされます。この設定を有効にすると、振ってリセットする機能を無効にできます。",
"LabelDisableVibrateOnReset": "リセット時の振動を無効化",
"LabelDisableVibrateOnResetHelp": "スリープタイマーがリセットされると、デバイスが振動します。この設定を有効にすると、スリープタイマーがリセットされた際に振動しなくなります。", "LabelDisableVibrateOnResetHelp": "スリープタイマーがリセットされると、デバイスが振動します。この設定を有効にすると、スリープタイマーがリセットされた際に振動しなくなります。",
"LabelDiscover": "おすすめ",
"LabelDownload": "ダウンロード", "LabelDownload": "ダウンロード",
"LabelDownloadUsingCellular": "モバイル通信を使用してダウンロード", "LabelDownloadUsingCellular": "モバイル通信を使用してダウンロード",
"LabelDownloaded": "ダウンロード完了", "LabelDownloaded": "ダウンロード完了",
"LabelDuration": "長さ", "LabelDuration": "長さ",
"LabelEbook": "Eブック", "LabelEbook": "電子書籍",
"LabelEbooks": "Eブック", "LabelEbooks": "電子書籍",
"LabelEnable": "有効", "LabelEnable": "有効",
"LabelEnableMp3IndexSeeking": "mp3インデックス検索を有効化", "LabelEnableMp3IndexSeeking": "mp3インデックス検索を有効化",
"LabelEnableMp3IndexSeekingHelp": "この設定は、正しくシークできないMP3ファイルがある場合にのみ有効にしてください。シークの不正確さは、可変ビットレート(VBR)のMP3ファイルが原因である可能性が高いです。この設定はインデックスシークを強制し、ファイルの読み取り時に時間-バイトマッピングを構築します。大きなMP3ファイルの場合、ファイルの終わり付近をシークする際に遅延が生じる場合があります。", "LabelEnableMp3IndexSeekingHelp": "この設定は、正しくシークできないMP3ファイルがある場合にのみ有効にしてください。シークの不正確さは、可変ビットレート(VBR)のMP3ファイルが原因である可能性が高いです。この設定はインデックスシークを強制し、ファイルの読み取り時に時間-バイトマッピングを構築します。大きなMP3ファイルの場合、ファイルの終わり付近をシークする際に遅延が生じる場合があります。",
"LabelEnd": "終了",
"LabelEndOfChapter": "チャプターの最後", "LabelEndOfChapter": "チャプターの最後",
"LabelEndTime": "終了時間", "LabelEndTime": "終了時間",
"LabelEpisode": "エピソード", "LabelEpisode": "エピソード",
"LabelFeedURL": "Feed URL", "LabelExplicit": "露骨な表現",
"LabelFeedURL": "フィードURL",
"LabelFile": "ファイル", "LabelFile": "ファイル",
"LabelFileBirthtime": "ファイル作成日時",
"LabelFileModified": "ファイル更新日時",
"LabelFilename": "ファイル名", "LabelFilename": "ファイル名",
"LabelFinished": "完了", "LabelFinished": "完了",
"LabelFolder": "フォルダ", "LabelFolder": "フォルダ",
"LabelFontBoldness": "フォントの太さ", "LabelFontBoldness": "フォントの太さ",
"LabelFontFamily": "フォントファミリー",
"LabelFontFamilySans": "サン",
"LabelFontFamilySerif": "セリフ",
"LabelFontScale": "フォントサイズ", "LabelFontScale": "フォントサイズ",
"LabelGenre": "ジャンル", "LabelGenre": "ジャンル",
"LabelGenres": "ジャンル", "LabelGenres": "ジャンル",
"LabelHapticFeedback": "ハプティックフィードバック",
"LabelHasEbook": "電子書籍あり",
"LabelHasSupplementaryEbook": "付属電子書籍あり",
"LabelHeavy": "思い",
"LabelHigh": "高い",
"LabelHost": "ホスト", "LabelHost": "ホスト",
"LabelInProgress": "進行中", "LabelInProgress": "進行中",
"LabelIncomplete": "未完了",
"LabelInternalAppStorage": "アプリ内ストレージ", "LabelInternalAppStorage": "アプリ内ストレージ",
"LabelKeepScreenAwake": "画面をスリープにしない", "LabelKeepScreenAwake": "画面をスリープにしない",
"LabelLanguage": "言語", "LabelLanguage": "言語",
"LabelLayout": "レイアウト", "LabelLayout": "レイアウト",
"LabelLayoutAuto": "自動", "LabelLayoutAuto": "自動",
"LabelLayoutSinglePage": "単ページ", "LabelLayoutSinglePage": "単ページ",
"LabelLibrarySortByProgress": "進捗: 最終更新",
"LabelLibrarySortByProgressFinished": "進捗: 完了",
"LabelLibrarySortByProgressStarted": "進捗: 開始済み",
"LabelLight": "ライト", "LabelLight": "ライト",
"LabelLineSpacing": "行間", "LabelLineSpacing": "行間",
"LabelListenAgain": "再度視聴", "LabelListenAgain": "もう一度聴く",
"LabelLocalBooks": "ローカルブック", "LabelLocalBooks": "ローカルブック",
"LabelLocalPodcasts": "ローカルポッドキャスト", "LabelLocalPodcasts": "ローカルポッドキャスト",
"LabelLockPlayer": "プレイヤーのロック", "LabelLockPlayer": "プレイヤーのロック",
"LabelMediaType": "メディアの種類", "LabelMediaType": "メディアの種類",
"LabelMissing": "消失",
"LabelMore": "多い",
"LabelMoreInfo": "追加情報", "LabelMoreInfo": "追加情報",
"LabelName": "名", "LabelName": "名",
"LabelNarrator": "ナレーター", "LabelNarrator": "ナレーター",
"LabelNarrators": "ナレーター", "LabelNarrators": "ナレーター",
"LabelNavigateWithVolume": "音量キーで操作", "LabelNavigateWithVolume": "音量キーで操作",
"LabelNavigateWithVolumeWhilePlaying": "再生中に音量キーで操作を許可", "LabelNavigateWithVolumeWhilePlaying": "再生中に音量キーで操作を許可",
"LabelNavigateWithVolumeWhilePlayingDisabled": "オフ", "LabelNavigateWithVolumeWhilePlayingDisabled": "オフ",
"LabelNavigateWithVolumeWhilePlayingEnabled": "オン", "LabelNavigateWithVolumeWhilePlayingEnabled": "オン",
"LabelNewestAuthors": "最新の者", "LabelNewestAuthors": "最新の者",
"LabelNewestEpisodes": "最新エピソード", "LabelNewestEpisodes": "最新エピソード",
"LabelNotFinished": "未完了",
"LabelNotStarted": "未開始",
"LabelNumEpisodes": "{0} エピソード", "LabelNumEpisodes": "{0} エピソード",
"LabelNumEpisodesIncomplete": "{0} エピソード, {1} 未完了", "LabelNumEpisodesIncomplete": "{0} エピソード, {1} 未完了",
"LabelNumberOfEpisodes": "エピソード数",
"LabelPassword": "パスワード", "LabelPassword": "パスワード",
"LabelPath": "パス", "LabelPath": "パス",
"LabelPlaybackLocal": "ローカル", "LabelPlaybackLocal": "ローカル",
"LabelPodcast": "ポッドキャスト", "LabelPodcast": "ポッドキャスト",
"LabelPodcasts": "ポッドキャスト", "LabelPodcasts": "ポッドキャスト",
"LabelPreventIndexing": "フィードがiTunesおよびGoogleのポッドキャストディレクトリにインデックス登録されるのを防ぎます", "LabelPreventIndexing": "フィードがiTunesおよびGoogleのポッドキャストディレクトリにインデックス登録されるのを防ぎます",
"LabelPublishYear": "公開年" "LabelProgress": "進捗",
"LabelPubDate": "公開日",
"LabelPublishYear": "公開年",
"LabelPublishedDate": "{0}発行",
"LabelRSSFeedCustomOwnerEmail": "カスタムオーナーメール",
"LabelRSSFeedCustomOwnerName": "カスタムオーナー名",
"LabelRSSFeedOpen": "RSSフィードを公開",
"LabelRSSFeedPreventIndexing": "インデックス化を防止",
"LabelRSSFeedSlug": "RSSフィードスラグ",
"LabelRandomly": "ランダム",
"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": "タイプ",
"LabelUnknown": "不明",
"LabelUser": "ユーザー",
"LabelUsername": "ユーザー名",
"LabelYearReviewHide": "年間振り返りを非表示",
"LabelYearReviewShow": "年間振り返りを表示",
"LabelYourBookmarks": "ブックマーク",
"LabelYourProgress": "進捗状況",
"MessageDownloadingEpisode": "エピソードをダウンロード中",
"MessageEpisodesQueuedForDownload": "{0}件のエピソードがダウンロードキューに追加されました",
"MessageFeedURLWillBe": "フィードURLは {0} になります",
"MessageFetching": "取得中...",
"MessageLoading": "読み込み中...",
"MessageMarkAsFinished": "完了済みにする",
"MessageNoBookmarks": "ブックマークなし",
"MessageNoChapters": "チャプターなし",
"MessageNoCollections": "コレクションなし",
"MessageNoItems": "アイテムなし",
"MessageNoItemsFound": "アイテムが見つかりません",
"MessageNoListeningSessions": "リスニングセッションなし",
"MessageNoPodcastsFound": "ポッドキャストが見つかりません",
"MessageNoUpdatesWereNecessary": "更新は不要でした",
"MessageNoUserPlaylists": "プレイリストがありません",
"MessagePodcastSearchField": "検索キーワードまたはRSSフィードURLを入力",
"MessageReportBugsAndContribute": "バグ報告、機能リクエスト、コントリビューション:",
"NoteRSSFeedPodcastAppsHttps": "警告: ほとんどのポッドキャストアプリはHTTPSのRSSフィードURLが必要です",
"NoteRSSFeedPodcastAppsPubDate": "警告: 1件以上のエピソードに公開日がありません。一部のポッドキャストアプリではこれが必要です。",
"ToastBookmarkCreateFailed": "ブックマークの作成に失敗しました",
"ToastItemMarkedAsFinishedFailed": "完了としてマークするのに失敗しました",
"ToastItemMarkedAsNotFinishedFailed": "未完了としてマークするのに失敗しました",
"ToastPlaylistCreateFailed": "プレイリストの作成に失敗しました",
"ToastPodcastCreateFailed": "ポッドキャストの作成に失敗しました",
"ToastPodcastCreateSuccess": "ポッドキャストを作成しました",
"ToastRSSFeedCloseFailed": "RSSフィードのクローズに失敗しました",
"ToastRSSFeedCloseSuccess": "RSSフィードをクローズしました"
} }
+362
View File
@@ -0,0 +1,362 @@
{
"ButtonAdd": "추가",
"ButtonAddNewServer": "새로운 서버 추가",
"ButtonAuthors": "작가",
"ButtonBack": "뒤로",
"ButtonCancel": "취소",
"ButtonCancelTimer": "타이머 취소",
"ButtonClearFilter": "필터 지우기",
"ButtonClearLogs": "로그 지우기",
"ButtonCloseFeed": "피드 닫기",
"ButtonCollections": "컬렉션",
"ButtonConnect": "접속",
"ButtonConnectToServer": "서버에 연결",
"ButtonCreate": "만들기",
"ButtonCreateBookmark": "북마크 만들기",
"ButtonCreateNewPlaylist": "새로운 재생목록 만들기",
"ButtonDelete": "삭제",
"ButtonDeleteLocalEpisode": "로컬 에피소드 삭제",
"ButtonDeleteLocalFile": "로컬 파일 삭제",
"ButtonDeleteLocalItem": "로컬 항목 삭제",
"ButtonDisableAutoTimer": "자동 타이머 비활성화",
"ButtonDisconnect": "연결 해제",
"ButtonGoToWebClient": "웹 클라이언트로 이동",
"ButtonHistory": "히스토리",
"ButtonHome": "홈",
"ButtonIssues": "이슈",
"ButtonLatest": "최신",
"ButtonLibrary": "라이브러리",
"ButtonLocalMedia": "로컬 미디어",
"ButtonLogs": "로그",
"ButtonManageLocalFiles": "로컬 파일 관리",
"ButtonMaskServerAddress": "마스크 서버 주소",
"ButtonNewFolder": "새 폴더",
"ButtonNextEpisode": "다음 에피소드",
"ButtonOk": "확인",
"ButtonOpenFeed": "피드 열기",
"ButtonOverride": "Override",
"ButtonPause": "일시정지",
"ButtonPlay": "재생",
"ButtonPlayEpisode": "에피소드 재생",
"ButtonPlaylists": "재생목록",
"ButtonRead": "읽기",
"ButtonReadLess": "덜 읽기",
"ButtonReadMore": "더 읽기",
"ButtonRemove": "제거",
"ButtonRemoveFromServer": "서버에서 제거",
"ButtonSave": "저장",
"ButtonSaveOrder": "저장 순서",
"ButtonSearch": "검색",
"ButtonSendEbookToDevice": "기기로 전자책 보내기",
"ButtonSeries": "시리즈",
"ButtonSetTimer": "타이머 설정",
"ButtonStream": "스트림",
"ButtonSubmit": "제출",
"ButtonSwitchServerUser": "서버/사용자 전환",
"ButtonUnmaskServerAddress": "서버 주소 마스크 해제",
"ButtonUserStats": "사용자 통계",
"ButtonYes": "예",
"HeaderAccount": "계정",
"HeaderAdvanced": "고급",
"HeaderAndroidAutoSettings": "안드로이드 오토 설정",
"HeaderAudioTracks": "오디오 트랙",
"HeaderChapters": "챕터",
"HeaderCollection": "컬렉션",
"HeaderCollectionItems": "컬렉션 항목",
"HeaderConnectionStatus": "연결 상태",
"HeaderDataSettings": "데이터 설정",
"HeaderDetails": "세부사항",
"HeaderDownloads": "다운로드",
"HeaderEbookFiles": "전자책 파일",
"HeaderEpisodes": "에피소드",
"HeaderEreaderSettings": "전자책 리더 설정",
"HeaderLatestEpisodes": "최신 에피소드",
"HeaderLibraries": "라이브러리",
"HeaderLocalFolders": "로컬 폴더",
"HeaderLocalLibraryItems": "로컬 라이브러리 항목",
"HeaderNewPlaylist": "새로운 플레이리스트",
"HeaderOpenRSSFeed": "RSS 피드 열기",
"HeaderPlaybackSettings": "재생 설정",
"HeaderPlaylist": "재생목록",
"HeaderPlaylistItems": "재생 목록 항목",
"HeaderProgressSyncFailed": "진행률 동기화 실패",
"HeaderRSSFeed": "RSS 피드",
"HeaderRSSFeedGeneral": "RSS 자세히",
"HeaderRSSFeedIsOpen": "RSS 피드가 열려 있습니다",
"HeaderSelectDownloadLocation": "다운로드 위치 선택",
"HeaderSettings": "설정",
"HeaderSleepTimer": "슬립 타이머",
"HeaderSleepTimerSettings": "슬립 타이머 설정",
"HeaderStatsMinutesListeningChart": "청취 시간(지난 7일)",
"HeaderStatsRecentSessions": "최근 세션",
"HeaderTableOfContents": "목차",
"HeaderUserInterfaceSettings": "사용자 인터페이스 설정",
"HeaderYourStats": "내 통계",
"LabelAddToPlaylist": "재생목록에 추가",
"LabelAddedAt": "추가됨",
"LabelAddedDate": "{0} 추가됨",
"LabelAll": "모두",
"LabelAllowSeekingOnMediaControls": "미디어 알림 컨트롤에서 위치 탐색 허용",
"LabelAlways": "항상",
"LabelAndroidAutoBrowseLimitForGrouping": "알파벳별 인출 한도",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "표시할 항목 수가 이 금액보다 적을 경우 알파벳 순으로 삭감하지 마십시오.",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "시리즈 책 주문",
"LabelAskConfirmation": "확인을 요청하세요",
"LabelAuthor": "작가",
"LabelAuthorFirstLast": "작가 (이름 성)",
"LabelAuthorLastFirst": "작가 (성, 이름)",
"LabelAuthors": "작가",
"LabelAutoDownloadEpisodes": "에피소드 자동 다운로드",
"LabelAutoRewindTime": "자동 되감기 시간",
"LabelAutoSleepTimer": "자동 슬립 타이머",
"LabelAutoSleepTimerAutoRewind": "자동 슬립 타이머 자동 되감기",
"LabelAutoSleepTimerAutoRewindHelp": "자동 슬립 타이머가 끝나면 해당 항목을 다시 재생하면 자동으로 위치가 되돌려집니다.",
"LabelAutoSleepTimerHelp": "지정된 시작 및 종료 시간 사이에 미디어를 재생하면 슬립 타이머가 자동으로 시작됩니다.",
"LabelBooks": "전자책",
"LabelChapterTrack": "챕터 트랙",
"LabelChapters": "챕터",
"LabelClosePlayer": "플레이어 닫기",
"LabelCollapseSeries": "시리즈 펴기",
"LabelComplete": "완료",
"LabelContinueBooks": "책 계속 읽기",
"LabelContinueEpisodes": "에피소드 계속하기",
"LabelContinueListening": "계속 듣기",
"LabelContinueReading": "계속 읽기",
"LabelContinueSeries": "시리즈 계속하기",
"LabelCustomTime": "사용자 지정 시간",
"LabelDescription": "설명",
"LabelDisableAudioFadeOut": "오디오 페이드아웃 비활성화",
"LabelDisableAudioFadeOutHelp": "슬립 타이머가 1분 미만으로 남았을 때 오디오 볼륨이 감소하기 시작합니다. 이 설정을 활성화하면 볼륨이 점점 작아지지 않습니다.",
"LabelDisableAutoRewind": "자동 되감기 비활성화",
"LabelDisableShakeToReset": "흔들어서 재설정을 해제하세요",
"LabelDisableShakeToResetHelp": "타이머가 작동하는 동안 또는 타이머가 만료되기 2분 전에 기기를 흔들면 슬립 타이머가 재설정됩니다. 이 설정을 활성화하면 흔들어서 재설정하는 기능을 비활성화할 수 있습니다.",
"LabelDisableVibrateOnReset": "재설정 시 진동 비활성화",
"LabelDisableVibrateOnResetHelp": "슬립 타이머가 재설정되면 기기가 진동합니다. 슬립 타이머가 재설정될 때 진동하지 않도록 하려면 이 설정을 활성화하세요.",
"LabelDiscover": "발견",
"LabelDownload": "다운로드",
"LabelDownloadUsingCellular": "셀룰러를 사용하여 다운로드",
"LabelDownloaded": "다운로드됨",
"LabelDuration": "기간",
"LabelEbook": "전자책",
"LabelEbooks": "전자책",
"LabelEnable": "활성화",
"LabelEnableMp3IndexSeeking": "mp3 인덱스 검색 활성화",
"LabelEnableMp3IndexSeekingHelp": "이 설정은 MP3 파일의 탐색이 제대로 이루어지지 않는 경우에만 활성화해야 합니다. 탐색이 부정확한 이유는 가변 비트레이트(VBR) MP3 파일 때문일 가능성이 높습니다. 이 설정은 파일을 읽을 때 시간-바이트 매핑이 생성되는 인덱스 탐색을 강제로 실행합니다. 대용량 MP3 파일의 경우 파일 끝부분을 탐색할 때 지연이 발생할 수 있습니다.",
"LabelEnd": "마지막",
"LabelEndOfChapter": "챕터의 마지작",
"LabelEndTime": "종료 시간",
"LabelEpisode": "에피소드",
"LabelExplicit": "명백한",
"LabelFeedURL": "피드 URL",
"LabelFile": "파일",
"LabelFileBirthtime": "파일 생년월일",
"LabelFileModified": "파일 수정됨",
"LabelFilename": "파일 이름",
"LabelFinished": "완료됨",
"LabelFolder": "폴더",
"LabelFontBoldness": "글꼴 굵기",
"LabelFontFamily": "글꼴 모양",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "글꼴 크기",
"LabelGenre": "장르",
"LabelGenres": "장르",
"LabelHapticFeedback": "햅틱 피드백",
"LabelHasEbook": "보유한 전자책",
"LabelHasSupplementaryEbook": "보유한 보충 전자책",
"LabelHeavy": "무거운",
"LabelHigh": "높게",
"LabelHost": "호스트",
"LabelInProgress": "진행 중",
"LabelIncomplete": "불완전한",
"LabelInternalAppStorage": "내부 앱 저장소",
"LabelJumpBackwardsTime": "뒤로 시간 점프",
"LabelJumpForwardsTime": "앞으로 시간 점프",
"LabelKeepScreenAwake": "화면 항상 켜기",
"LabelLanguage": "언어",
"LabelLayout": "레이아웃",
"LabelLayoutAuto": "자동",
"LabelLayoutSinglePage": "단일 페이지",
"LabelLight": "Light",
"LabelLineSpacing": "줄 간격",
"LabelListenAgain": "다시 듣기",
"LabelLocalBooks": "로컬 전자책",
"LabelLocalPodcasts": "로컬 팟케스트",
"LabelLockOrientation": "잠금 방향",
"LabelLockPlayer": "플레이어 잠금",
"LabelLow": "낮게",
"LabelMediaType": "미디어 유형",
"LabelMedium": "중간",
"LabelMissing": "누락",
"LabelMore": "더보기",
"LabelMoreInfo": "정보 더 보기",
"LabelName": "이름",
"LabelNarrator": "내레이터",
"LabelNarrators": "내레이터",
"LabelNavigateWithVolume": "볼륨 키로 탐색",
"LabelNavigateWithVolumeMirrored": "미러링됨",
"LabelNavigateWithVolumeWhilePlaying": "재생 중 볼륨 키를 사용하여 탐색할 수 있도록 허용",
"LabelNavigateWithVolumeWhilePlayingDisabled": "오프",
"LabelNavigateWithVolumeWhilePlayingEnabled": "온",
"LabelNever": "Never",
"LabelNewestAuthors": "최신 작가",
"LabelNewestEpisodes": "최신 에피소드",
"LabelNo": "아니오",
"LabelNotFinished": "완료되지 않음",
"LabelNotStarted": "시작되지 않음",
"LabelNumEpisodes": "{0}개 에피소드",
"LabelNumEpisodesIncomplete": "{0}개 에피소드, {1}개 미완료",
"LabelNumberOfEpisodes": "# 의 에피소드",
"LabelOff": "오프",
"LabelOn": "온",
"LabelPassword": "비밀번호",
"LabelPath": "경로",
"LabelPlaybackDirect": "직접",
"LabelPlaybackLocal": "로컬",
"LabelPlaybackSpeed": "재생 속도",
"LabelPlaybackTranscode": "트랜스코드",
"LabelPodcast": "팟케스트",
"LabelPodcasts": "팟케스트",
"LabelPreventIndexing": "iTunes 및 Google Podcast 디렉토리에서 피드가 인덱싱되는 것을 방지합니다.",
"LabelProgress": "진행",
"LabelPubDate": "출판 날짜",
"LabelPublishYear": "출판 연도",
"LabelPublishedDate": "출판됨 {0}",
"LabelRSSFeedCustomOwnerEmail": "사용자 정의 이메일",
"LabelRSSFeedCustomOwnerName": "사용자 정의 이름",
"LabelRSSFeedOpen": "RSS 피드 열기",
"LabelRSSFeedPreventIndexing": "인덱싱 방지",
"LabelRSSFeedSlug": "RSS 피드 슬러그",
"LabelRandomly": "무작위로",
"LabelRead": "읽기",
"LabelReadAgain": "다시 읽기",
"LabelRecentSeries": "최근 시리즈",
"LabelRecentlyAdded": "최근 추가됨",
"LabelRemoveFromPlaylist": "재생 목록에서 제거",
"LabelScaleElapsedTimeBySpeed": "속도에 따른 경과 시간 스케일링",
"LabelSeason": "시즌",
"LabelSelectADevice": "기기를 선택하세요",
"LabelSequenceAscending": "시퀀스 오름차순",
"LabelSequenceDescending": "시퀀스 내림차순",
"LabelSeries": "시리즈",
"LabelServerAddress": "서버 주소",
"LabelSetEbookAsPrimary": "기본으로 설정",
"LabelSetEbookAsSupplementary": "보충으로 설정",
"LabelShakeSensitivity": "흔들림 감도",
"LabelShowAll": "모두 표시",
"LabelSize": "크기",
"LabelSleepTimer": "슬립 타이머",
"LabelSleepTimerAlmostDoneChime": "거의 끝나면 차임벨을 울리세요",
"LabelSleepTimerAlmostDoneChimeHelp": "슬립 타이머가 30초 남았을 때 차임음을 재생합니다.",
"LabelStart": "시작",
"LabelStartTime": "시작 시간",
"LabelStatsBestDay": "최고의 날",
"LabelStatsDailyAverage": "일일 평균",
"LabelStatsDays": "일",
"LabelStatsDaysListened": "청취 일수",
"LabelStatsInARow": "in a row",
"LabelStatsItemsFinished": "완료된 항목",
"LabelStatsMinutes": "분",
"LabelStatsMinutesListening": "청취 분수",
"LabelStatsWeekListening": "주간 청취",
"LabelStreamingUsingCellular": "셀룰러를 이용한 스트리밍",
"LabelTag": "태그",
"LabelTags": "태그",
"LabelTheme": "테마",
"LabelThemeBlack": "Black",
"LabelThemeDark": "Dark",
"LabelThemeLight": "Light",
"LabelTimeRemaining": "{0} 남음",
"LabelTitle": "제목",
"LabelTotalSize": "전체 크기",
"LabelTotalTrack": "총 트랙",
"LabelTracks": "트랙",
"LabelType": "유형",
"LabelUnknown": "미상",
"LabelUnlockPlayer": "플레이어 잠금 해제",
"LabelUseBookshelfView": "bookshelf view 사용",
"LabelUser": "사용자",
"LabelUsername": "사용자 이름",
"LabelVeryHigh": "매우 높음",
"LabelVeryLow": "매우 낮음",
"LabelYearReviewHide": "연간 리뷰 숨기기",
"LabelYearReviewShow": "올해의 리뷰 보기",
"LabelYourBookmarks": "내 북마크",
"LabelYourProgress": "내 진행 상황",
"MessageAndroid10Downloads": "Android 10 이하에서는 다운로드를 위해 내부 앱 저장소를 사용합니다.",
"MessageAttemptingServerConnection": "서버 연결을 시도 중입니다...",
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf 서버가 연결되지 않았습니다",
"MessageAudiobookshelfServerRequired": "<strong>중요!</strong> 이 앱은 본인이나 아는 사람이 호스팅하는 Audiobookshelf 서버와 함께 사용하도록 설계되었습니다. 이 앱은 어떠한 콘텐츠도 제공하지 않습니다.",
"MessageBookshelfEmpty": "책장이 비어 있음",
"MessageConfirmDeleteLocalEpisode": "기기에서 로컬 에피소드 \"{0}\"을(를) 삭제하시겠습니까? 서버에 있는 파일은 영향을 받지 않습니다.",
"MessageConfirmDeleteLocalFiles": "이 항목의 로컬 파일을 기기에서 제거하시겠습니까? 서버의 파일과 진행 상황에는 아무런 영향이 없습니다.",
"MessageConfirmDisableAutoTimer": "오늘 남은 시간 동안 자동 타이머를 비활성화하시겠습니까? 자동 슬립 타이머 기간이 끝나거나 앱을 다시 시작하면 타이머가 다시 활성화됩니다.",
"MessageConfirmDiscardProgress": "진행 상황을 재설정하시겠습니까?",
"MessageConfirmDownloadUsingCellular": "셀룰러 데이터를 사용하여 다운로드하려고 합니다. 이동통신사 데이터 요금이 부과될 수 있습니다. 계속하시겠습니까?",
"MessageConfirmMarkAsFinished": "이 항목을 완료로 표시하시겠습니까?",
"MessageConfirmRemoveBookmark": "북마크를 삭제하시겠습니까?",
"MessageConfirmStreamingUsingCellular": "셀룰러 데이터를 사용하여 스트리밍하려고 합니다. 이동통신사 데이터 요금이 부과될 수 있습니다. 계속하시겠습니까?",
"MessageDiscardProgress": "진행 상황 삭제",
"MessageDownloadCompleteProcessing": "다운로드가 완료되었습니다. 처리 중입니다...",
"MessageDownloading": "다운로드 중...",
"MessageDownloadingEpisode": "에피소드 다운로드 중",
"MessageEpisodesQueuedForDownload": "{0}개의 에피소드가 다운로드 대기 중입니다.",
"MessageFailedToRefreshToken": "토큰 새로 고침에 실패했습니다. 다시 로그인해야 합니다.",
"MessageFeedURLWillBe": "피드 URL은 {0}입니다.",
"MessageFetching": "가져오는 중...",
"MessageFollowTheProjectOnGithub": "Github에서 프로젝트를 팔로우하세요",
"MessageItemDownloadCompleteFailedToCreate": "항목 다운로드가 완료되었지만 라이브러리 항목을 생성하지 못했습니다.",
"MessageItemMissing": "항목이 누락되어 서버에서 수정해야 합니다. 일반적으로 파일 경로에 접근할 수 없기 때문에 항목이 누락으로 표시됩니다.",
"MessageLoading": "로딩중...",
"MessageLoadingServerData": "서버 데이터를 로딩 중입니다...",
"MessageLocalFolderDescription": "''내부 앱 저장소'는 이 앱에서만 접근할 수 있습니다. 이 앱은 앱을 통해 직접 다운로드한 미디어만 지원합니다. 공유 저장소 폴더를 사용하면 다른 앱이 이 앱에서 다운로드한 미디어에 접근할 수 있습니다.",
"MessageMarkAsFinished": "완료로 표시",
"MessageMediaLinkedToADifferentServer": "미디어가 다른 주소({0})의 오디오북셸프 서버에 연결되어 있습니다. 이 서버 주소에 연결되면 진행 상황이 동기화됩니다.",
"MessageMediaLinkedToADifferentUser": "미디어가 이 서버에 연결되어 있지만 다른 사용자가 다운로드했습니다. 진행 상황은 해당 미디어를 다운로드한 사용자에게만 동기화됩니다.",
"MessageMediaLinkedToServer": "서버 {0}에 연결됨",
"MessageMediaLinkedToThisServer": "다운로드한 미디어가 이 서버에 연결되었습니다.",
"MessageMediaNotLinkedToServer": "미디어가 오디오북 셸프 서버에 연결되지 않았습니다. 진행 상황이 동기화되지 않습니다.",
"MessageNoBookmarks": "북마크 없음",
"MessageNoChapters": "챕터 없음",
"MessageNoCollections": "컬렉션 없음",
"MessageNoItems": "항목 없음",
"MessageNoItemsFound": "항목을 찾을 수 없음",
"MessageNoListeningSessions": "청취 세션 없음",
"MessageNoLogs": "로그 없음",
"MessageNoMediaFolders": "미디어 폴더 없음",
"MessageNoNetworkConnection": "네트워크 연결이 없음",
"MessageNoPodcastsFound": "팟캐스트를 찾을 수 없음",
"MessageNoSeries": "시리즈 없음",
"MessageNoUpdatesWereNecessary": "업데이트가 필요하지 않음",
"MessageNoUserPlaylists": "재생목록이 없음",
"MessageOldServerAuthReLoginRequired": "서버 v2.26.0에서는 보안을 위해 인증 기능이 개선되었습니다. 모든 사용자는 다시 로그인해야 합니다.",
"MessageOldServerAuthWarning": "서버가 오래된 인증을 사용하고 있습니다.",
"MessageOldServerAuthWarningHelp": "이 서버는 v2.26.0 이전 버전을 사용하고 있습니다. v2.26.0에서는 더욱 안전한 인증 시스템이 추가되었습니다. 서버를 최신 버전으로 업데이트하는 것을 강력히 권장합니다. 이미 서버를 업데이트하셨다면, 새로운 인증을 사용하려면 다시 로그인하세요.",
"MessageOldServerConnectionWarning": "서버 연결 구성에서 이전 사용자 ID를 사용하고 있습니다. 이 서버 연결을 삭제하고 다시 추가하세요.",
"MessageOldServerConnectionWarningHelp": "2023년 6월에 출시된 2.3.0 버전의 데이터베이스 마이그레이션 이전에 이 서버에 대한 연결을 설정했습니다. 향후 서버 업데이트에서는 이 이전 연결로 로그인할 수 있는 기능이 제거됩니다. 기존 서버 연결을 삭제하고 동일한 서버 주소와 사용자 인증 정보를 사용하여 다시 연결하세요. 이 기기에 다운로드한 미디어가 있는 경우, 서버와 동기화하려면 미디어를 다시 다운로드해야 합니다.",
"MessagePodcastSearchField": "검색어 또는 RSS 피드 URL을 입력하세요",
"MessageProgressSyncFailed": "서버에 청취 진행 상황을 보고하려는 최근 시도가 실패했습니다. 미디어 재생 중에는 15초에서 1분 간격으로 진행 상황 동기화 요청이 계속 시도됩니다.",
"MessageReportBugsAndContribute": "버그 보고, 기능 요청 및 기여",
"MessageSeriesAlreadyDownloaded": "이 시리즈의 모든 책을 이미 다운로드했습니다.",
"MessageSeriesDownloadConfirm": "{1}개의 파일과 함께 누락된 {0}권의 책, 총 {2}권을 {3} 폴더로 다운로드하시겠습니까?",
"MessageSeriesDownloadConfirmIos": "{1}개의 파일과 함께 누락된 {0}권의 책을 다운로드하시겠습니까? 총 {2}개입니다.",
"MessageSocketConnectedOverMeteredCellular": "미터링된 셀룰러를 통해 연결된 소켓",
"MessageSocketConnectedOverMeteredWifi": "미터기 Wi-Fi로 연결된 소켓",
"MessageSocketConnectedOverUnmeteredCellular": "무제한 셀룰러를 통해 연결된 소켓",
"MessageSocketConnectedOverUnmeteredWifi": "무제한 Wi-Fi로 연결된 소켓",
"MessageSocketNotConnected": "소켓이 연결되지 않음",
"NoteRSSFeedPodcastAppsHttps": "경고: 대부분의 팟캐스트 앱에서는 RSS 피드 URL이 HTTPS를 사용해야 합니다.",
"NoteRSSFeedPodcastAppsPubDate": "경고: 1개 이상의 에피소드에 게시 날짜가 없습니다. 일부 팟캐스트 앱에서는 게시 날짜가 필요합니다.",
"ToastBookmarkCreateFailed": "북마크를 생성하지 못했습니다.",
"ToastBookmarkRemoveFailed": "북마크를 삭제하지 못했습니다.",
"ToastBookmarkUpdateFailed": "북마크 업데이트에 실패했습니다",
"ToastDownloadNotAllowedOnCellular": "셀룰러 데이터에서는 다운로드가 허용되지 않습니다.",
"ToastItemMarkedAsFinishedFailed": "완료로 표시하지 못했습니다.",
"ToastItemMarkedAsNotFinishedFailed": "완료되지 않음으로 표시하는 데 실패했습니다.",
"ToastPlaylistCreateFailed": "플레이리스트를 생성하지 못했습니다",
"ToastPodcastCreateFailed": "팟캐스트를 만들지 못했습니다",
"ToastPodcastCreateSuccess": "팟캐스트가 성공적으로 생성되었습니다.",
"ToastRSSFeedCloseFailed": "RSS 피드를 닫지 못했습니다.",
"ToastRSSFeedCloseSuccess": "RSS 피드가 닫혔습니다",
"ToastStreamingNotAllowedOnCellular": "셀룰러 데이터에서는 스트리밍이 허용되지 않습니다."
}

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