Compare commits

...
Author SHA1 Message Date
advplyr 00ee7b662e iOS version bump 0.10.1 2025-09-03 17:27:41 -05:00
advplyr b222b2a5e5 Version bump v0.10.1-beta 2025-09-03 16:19:04 -05:00
advplyrandGitHub 15b212734e Merge pull request #1669 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-09-03 17:00:11 -04:00
icutehunterandHosted Weblate 631ba52ffb Translated using Weblate (Turkish)
Currently translated at 76.7% (274 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/tr/
2025-09-03 01:01:59 +02:00
kuci-JKandHosted Weblate a4b58a893d Translated using Weblate (Czech)
Currently translated at 98.8% (353 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-09-03 01:01:57 +02:00
HnatiucbandHosted Weblate 47a3fd9462 Translated using Weblate (Romanian)
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/ro/
2025-08-31 23:41:26 +02:00
HnatiucbandHosted Weblate abcd56f0c5 Translated using Weblate (Romanian)
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/ro/
2025-08-31 23:41:25 +02:00
HnatiucbandHosted Weblate b498b5eafa Translated using Weblate (Romanian)
Currently translated at 23.2% (83 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ro/
2025-08-31 23:41:25 +02:00
tngchandHosted Weblate be114e7789 Translated using Weblate (Japanese)
Currently translated at 45.3% (162 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ja/
2025-08-31 23:41:24 +02:00
Jan-Eric MyhrgrenandHosted Weblate 8c44142e70 Translated using Weblate (Swedish)
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/sv/
2025-08-31 23:41:23 +02:00
HnatiucbandHosted Weblate 5a655f77d2 Translated using Weblate (Romanian)
Currently translated at 19.3% (69 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ro/
2025-08-31 23:41:23 +02:00
Jan-Eric MyhrgrenandHosted Weblate c825f791d0 Translated using Weblate (Swedish)
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/sv/
2025-08-31 23:41:22 +02:00
Jan-Eric MyhrgrenandHosted Weblate 83a9d8a162 Translated using Weblate (Swedish)
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/sv/
2025-08-31 23:41:22 +02:00
Mathias FrancoandHosted Weblate acfa2e2183 Translated using Weblate (Dutch)
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/nl/
2025-08-31 23:41:21 +02:00
Jan-Eric MyhrgrenandHosted Weblate 591933f3d8 Translated using Weblate (Swedish)
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/sv/
2025-08-31 23:41:21 +02:00
idojius86andHosted Weblate 982f94d955 Translated using Weblate (Spanish)
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/es/
2025-08-31 23:41:20 +02:00
advplyr 95bf28d7c6 Fix tab focus for appbar and sidenav, add tabindex to book card 2025-08-31 16:41:13 -05:00
advplyrandGitHub 48a299de45 Merge pull request #1675 from laurisvr/remove-busy-loops
Remove busy loops
2025-08-31 16:12:07 -04:00
Lauris van RijnandLauris van Rijn 719e517dda fix(androidauto): async handling of browseTree init instead of busy‑loop
Removed blocking `while (!browseTree.isInitialized){}` in
`onLoadChildren`. Added `waitForBrowseTree` and `onBrowseTreeInitialized`
helpers to queue pending results until browseTree is ready. All
browseTree assignments now call `onBrowseTreeInitialized()`. This avoids
ANRs and high CPU when Android Auto requests children before init.
2025-08-29 00:43:02 +02:00
Lauris van RijnandLauris van Rijn 361c55c5ac fix(media): remove busy‑wait loop in library personalization loading
Replaced the infinite `while(libraryPersonalizationsDone > 0){}` spin‑loop
with an async counter callback. This prevents pegging the CPU if one
personalization never completes, and allows completion to trigger via
AtomicInteger decrement. Now the final callback fires only when all
libraries have finished loading.
2025-08-29 00:42:48 +02:00
advplyrandGitHub b157fff229 Merge pull request #1653 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-08-23 17:46:51 -04:00
thehijackerandHosted Weblate e23e6417ee Translated using Weblate (Slovenian)
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/sl/
2025-08-23 05:01:54 +00:00
Yurt PageandHosted Weblate ad77209832 Translated using Weblate (Russian)
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/ru/
2025-08-23 05:01:53 +00:00
Zhelyan RadoevandHosted Weblate 4ceb427d9c Translated using Weblate (Bulgarian)
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/bg/
2025-08-20 05:02:15 +00:00
Ivan SmoliakovandHosted Weblate 16a26130f2 Translated using Weblate (Russian)
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/ru/
2025-08-20 05:02:14 +00:00
numerfoltandHosted Weblate e7773c9e88 Translated using Weblate (German)
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/de/
2025-08-20 05:02:13 +00:00
B0raxandHosted Weblate 1b38b92177 Translated using Weblate (German)
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/de/
2025-08-20 05:02:12 +00:00
Laurin SorgendandHosted Weblate ef180f08b0 Translated using Weblate (German)
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/de/
2025-08-20 05:02:11 +00:00
Zhelyan RadoevandHosted Weblate 6de45a3b88 Translated using Weblate (Bulgarian)
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/bg/
2025-08-18 17:02:14 +02:00
FiendFEARingandHosted Weblate ceb9537b73 Translated using Weblate (Chinese (Simplified Han script))
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/zh_Hans/
2025-08-18 17:02:12 +02:00
Максим ГорпинічandHosted Weblate 976bf73388 Translated using Weblate (Ukrainian)
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/uk/
2025-08-18 17:02:10 +02:00
ugyesandHosted Weblate 6c7f2d96f3 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-08-18 17:02:09 +02:00
CharlieandHosted Weblate ae2fc1fcfb Translated using Weblate (French)
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/fr/
2025-08-18 17:02:07 +02:00
owlcollectorandHosted Weblate 3aad7e9e07 Translated using Weblate (Japanese)
Currently translated at 16.5% (59 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ja/
2025-08-17 00:55:15 +02:00
Zhelyan RadoevandHosted Weblate e43f1e75c8 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bg/
2025-08-17 00:55:14 +02:00
Максим ГорпинічandHosted Weblate bc5505e12d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-08-17 00:55:13 +02:00
Hang PhamandHosted Weblate 19a2703ea4 Translated using Weblate (Vietnamese)
Currently translated at 80.0% (285 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/vi/
2025-08-17 00:55:13 +02:00
SneakyandHosted Weblate 8a8f7d4f27 Translated using Weblate (Swedish)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-08-17 00:55:12 +02:00
Paolo RicciandHosted Weblate e350b4970f Translated using Weblate (Italian)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2025-08-17 00:55:11 +02:00
owlcollectorandHosted Weblate 4be28bc579 Translated using Weblate (Japanese)
Currently translated at 7.8% (28 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ja/
2025-08-17 00:55:11 +02:00
J. LavoieandHosted Weblate 13855a8682 Translated using Weblate (German)
Currently translated at 99.7% (355 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-08-17 00:55:10 +02:00
advplyr 4be1598eca Fix oidc button not showing on re-login, fix oidc re-login showing config already exists #1638 #1634 2025-08-16 17:55:00 -05:00
advplyr 7aebcd92c3 Fix refresh token not persisted on new server connections #1634 2025-08-15 17:41:23 -05:00
advplyr bd8668f0bf Fix rss feed modal not showing full URL once open #1652 2025-08-08 17:39:27 -05:00
advplyrandGitHub c6a7c6fec2 Merge pull request #1637 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-08-07 18:45:51 -04:00
Aleksandr ZakirovandHosted Weblate e367fa8a86 Translated using Weblate (Estonian)
Currently translated at 49.1% (175 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/et/
2025-08-07 03:01:53 +02:00
weblate.user.1274andHosted Weblate ecdbabfa17 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2025-08-07 03:01:52 +02:00
Kent HenriksenandHosted Weblate 796189ad48 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2025-08-07 03:01:51 +02:00
Ashish WadekarandHosted Weblate 76d1f2b29b Translated using Weblate (Hindi)
Currently translated at 13.2% (47 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hi/
2025-08-07 03:01:50 +02:00
Camille de LuneandHosted Weblate 73599815ba Translated using Weblate (French)
Currently translated at 99.7% (355 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2025-08-07 03:01:49 +02:00
Grzegorz OrlowskiandHosted Weblate 66372e9743 Translated using Weblate (Polish)
Currently translated at 99.4% (354 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-08-05 04:02:09 +02:00
Grzegorz OrlowskiandHosted Weblate 0d7f92129b Translated using Weblate (Polish)
Currently translated at 98.0% (349 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-08-03 12:02:20 +02:00
Remco SchrijverandHosted Weblate 9da49d6116 Translated using Weblate (Dutch)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2025-07-31 20:02:32 +02:00
Troj@andHosted Weblate 7eeb9095e8 Translated using Weblate (Belarusian)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2025-07-30 02:08:19 +00:00
thehijackerandHosted Weblate 58281bb5ce Translated using Weblate (Slovenian)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2025-07-30 02:08:18 +00:00
ugyesandHosted Weblate f24dd3b289 Translated using Weblate (Hungarian)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2025-07-30 02:08:17 +00:00
B0raxandHosted Weblate 51f2e5e7ec Translated using Weblate (German)
Currently translated at 99.7% (355 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-07-30 02:08:16 +00:00
numerfoltandHosted Weblate a937800ef0 Translated using Weblate (German)
Currently translated at 99.7% (355 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-07-30 02:08:16 +00:00
Mikkel Dupont OlesenandHosted Weblate c34d14edb5 Translated using Weblate (Danish)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-07-27 09:04:25 +00:00
DmitryandHosted Weblate 8722e91c0f Translated using Weblate (Russian)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-07-23 08:03:14 +02:00
kuci-JKandHosted Weblate 8038a8ac66 Translated using Weblate (Czech)
Currently translated at 99.1% (353 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-07-23 08:03:14 +02:00
Pavel VachekandHosted Weblate 69a54c5fa9 Translated using Weblate (Czech)
Currently translated at 99.1% (353 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-07-23 08:03:14 +02:00
advplyrandGitHub aa508887f3 Merge pull request #1636 from advplyr/fix_android_transcode_track_url
Fix track URL used for transcodes on Android #1635
2025-07-22 15:15:47 -05:00
advplyr a53a96ecf5 Update web player track index to fallback to 1 2025-07-22 14:46:31 -05:00
advplyr 82cddde15f Fix track URL used for transcodes on Android #1635 2025-07-22 14:16:35 -05:00
advplyr def47fd5e2 Update gh issue template app versions 2025-07-21 08:57:56 -05:00
advplyr 04c70f54aa iOS version bump 0.10.0 2025-07-19 17:17:27 -05:00
advplyr 03e2feddb3 Version bump v0.10.0-beta 2025-07-19 16:55:54 -05:00
advplyrandGitHub fc69909dd0 Merge pull request #1625 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-07-19 16:47:20 -05:00
FiendFEARingandHosted Weblate 15fa42986a Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-19 15:03:05 +02:00
SunSpringandHosted Weblate d8b4cf98dd Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-19 15:03:04 +02:00
Максим ГорпинічandHosted Weblate ce62f3b273 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-07-19 15:03:03 +02:00
biuklijaandHosted Weblate 8be2e3af0c Translated using Weblate (Croatian)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-07-19 15:03:01 +02:00
GernomalyandHosted Weblate e44c574068 Translated using Weblate (German)
Currently translated at 99.4% (354 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-07-19 15:03:01 +02:00
SunSpringandHosted Weblate c31aa17ce2 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-17 22:45:49 +00:00
Максим ГорпинічandHosted Weblate decac596d0 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-07-17 22:45:48 +00:00
Grzegorz OrlowskiandHosted Weblate 2d9604aa07 Translated using Weblate (Polish)
Currently translated at 98.8% (349 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-07-17 22:45:47 +00:00
Fredrik LindqvistandHosted Weblate 79c4ffa6c5 Translated using Weblate (Swedish)
Currently translated at 99.7% (352 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-07-17 22:45:47 +00:00
JannikandHosted Weblate 571b3a2c86 Translated using Weblate (German)
Currently translated at 99.7% (352 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-07-17 22:45:46 +00:00
FiendFEARingandHosted Weblate fa31fba586 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-17 22:45:45 +00:00
Simple16andHosted Weblate 2f3b261392 Translated using Weblate (Russian)
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-07-17 22:45:45 +00:00
biuklijaandHosted Weblate afd3cd6716 Translated using Weblate (Croatian)
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-07-17 22:45:44 +00:00
FiendFEARingandHosted Weblate be99ef87ad Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-17 22:45:44 +00:00
Kabika82andHosted Weblate 572fc8545f Translated using Weblate (Hungarian)
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2025-07-17 22:45:43 +00:00
advplyrandGitHub 239a943172 Merge pull request #1618 from advplyr/new_jwt_auth
Update auth to handle refresh tokens
2025-07-17 17:45:33 -05:00
advplyr 87614bc78a Update auth message, update force re-login to pull auth methods to support oidc 2025-07-17 17:37:41 -05:00
advplyr 224d75fac5 Update old auth alert messages, add link to github discussion 2025-07-15 17:41:30 -05:00
advplyr f4e0a6121f Update readers to handle token refresh 2025-07-15 17:22:45 -05:00
advplyr 80ee88b488 Merge branch 'master' into new_jwt_auth 2025-07-11 16:58:50 -05:00
advplyrandGitHub beb654825f Merge pull request #1623 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-07-11 16:58:32 -05:00
advplyr 79d8ccbf52 Update login query param to x-return-tokens header 2025-07-11 16:00:48 -05:00
thehijackerandHosted Weblate b48c74eca0 Translated using Weblate (Slovenian)
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2025-07-11 00:18:44 +02:00
FiendFEARingandHosted Weblate 19e3dee706 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-11 00:18:44 +02:00
SunSpringandHosted Weblate e1be86c3ce Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-11 00:18:44 +02:00
SunSpringandHosted Weblate 11f7eddd7b Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-11 00:18:44 +02:00
RajandHosted Weblate 7f28d2ea70 Translated using Weblate (Gujarati)
Currently translated at 9.3% (33 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/gu/
2025-07-11 00:18:44 +02:00
Richard PožgayandHosted Weblate 4607a8e274 Translated using Weblate (Czech)
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-07-11 00:18:44 +02:00
MichalandHosted Weblate 26c559104a Translated using Weblate (Slovak)
Currently translated at 99.7% (352 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2025-07-11 00:18:44 +02:00
Максим ГорпинічandHosted Weblate e15e884e65 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-07-11 00:18:43 +02:00
advplyr b29401909f Fix comic extracted metadata icon 2025-07-10 17:18:38 -05:00
advplyr beb5e1a56c OIDC to support new access tokens 2025-07-07 17:20:22 -05:00
advplyr be08efeca3 iOS update retry request handler to handle string bodies 2025-07-07 06:15:51 -05:00
advplyr 4534ffaead Handle re-authenticating socket 2025-07-06 11:32:21 -05:00
advplyr d35dd2df1a Merge branch 'master' into new_jwt_auth 2025-07-06 09:07:17 -05:00
advplyr 2f3a9a5d96 Update play button on list book to check if player is starting 2025-07-05 18:06:24 -05:00
advplyr fab94cd363 Handle native app token refresh failure notification 2025-07-05 17:40:57 -05:00
advplyr b06274866d iOS update to use new track endpoint and remove token from cover url depending on version 2025-07-05 17:31:25 -05:00
advplyr 5766c49f61 Update iOS ApiClient to handle token refresh 2025-07-05 16:46:37 -05:00
advplyr bc927d4c35 Add SecureStorage to iOS and implement refresh token methods 2025-07-05 14:52:41 -05:00
advplyr 5804c54656 Add version to ServerConnectionConfig on iOS 2025-07-05 14:28:02 -05:00
advplyr 52f86cbce9 Fix plugin listener handlers, add message for configs using old server auth, show server version on account page 2025-07-05 12:20:27 -05:00
advplyr f6e2e4010f Update to not logout from server when switching servers, force users to re-login if using old auth 2025-07-05 11:21:20 -05:00
advplyr 44613e12f1 Update serverConnectionConfig to include server version, update server track URL and server cover image URL based on server version 2025-07-05 09:28:40 -05:00
advplyr b99e0b112b Merge branch 'master' into new_jwt_auth 2025-07-05 08:06:40 -05:00
advplyrandGitHub 80b565c23f Merge pull request #1622 from advplyr/play_from_library_list_rows
Add play button to book library list view
2025-07-04 18:22:30 -05:00
advplyr bee88c43e5 Add play button to book library list view 2025-07-04 18:07:05 -05:00
advplyr 467fedbfe7 Update to use x-refresh-token header, update logout to clear refresh token, add AbsLogger logs for android refresh 2025-07-04 17:41:19 -05:00
advplyr ba4e9ab7e3 Add explicit library filters 2025-07-03 17:40:02 -05:00
advplyrandGitHub 1b4fa5e44a Merge pull request #1610 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-07-03 17:32:02 -05:00
FiendFEARingandHosted Weblate e92c9b2990 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-02 20:16:41 +02:00
FiendFEARingandHosted Weblate f23f8e9ade Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-02 20:16:39 +02:00
DavevanIerselandHosted Weblate dc00aeece4 Translated using Weblate (Dutch)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2025-07-02 20:16:37 +02:00
advplyr d8cdb7073e Update auth to handle refresh tokens 2025-07-01 11:33:51 -05:00
Vito0912andHosted Weblate 4fdea9b0ec Translated using Weblate (German)
Currently translated at 99.7% (351 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-06-30 00:32:39 +02:00
Vito0912andHosted Weblate 3ded7687f7 Translated using Weblate (German)
Currently translated at 99.7% (351 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-06-30 00:32:38 +02:00
Eigen_artandHosted Weblate 42119425af Translated using Weblate (Ukrainian)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-06-30 00:32:38 +02:00
Dan JohansenandHosted Weblate 93c7e1c44a Translated using Weblate (Danish)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-06-30 00:32:37 +02:00
Michael FörsterandHosted Weblate 381fd84839 Translated using Weblate (German)
Currently translated at 99.7% (351 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-06-30 00:32:36 +02:00
advplyr 67bab72783 Update podcast library latest page number of episodes to 50 #1615 2025-06-29 17:32:29 -05:00
advplyrandGitHub b9c084a442 Merge pull request #1612 from advplyr/create_bookmarks_focus
Update create bookmark to not autofocus input and use placeholder
2025-06-22 17:51:26 -05:00
advplyr a77f345596 Update create bookmark to not autofocus input and leave empty with placeholder 2025-06-22 17:36:09 -05:00
advplyrandGitHub 6a2f487ed5 Merge pull request #1608 from NickSkier/ui_black_theme
Add: UI Black/OLED theme
2025-06-20 16:55:41 -05:00
advplyr 616cf9029d Fix audio player colors for black theme 2025-06-20 16:51:19 -05:00
NickSkier 3f21b4172b Add: UI Black/OLED theme 2025-06-20 01:05:46 +03:00
advplyrandGitHub 62f6a11522 Merge pull request #1589 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-06-19 16:24:42 -05:00
burghy86andHosted Weblate ae1dce455d Translated using Weblate (Italian)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2025-06-19 23:01:48 +02:00
B0raxandHosted Weblate 7b5dc52620 Translated using Weblate (German)
Currently translated at 99.7% (351 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-06-19 23:01:47 +02:00
Mathias FrancoandHosted Weblate 0bf50de54d Translated using Weblate (Dutch)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2025-06-17 22:01:57 +02:00
biuklijaandHosted Weblate 1eec470902 Translated using Weblate (Croatian)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-06-17 22:01:56 +02:00
petr-prikrylandHosted Weblate 867dc69479 Translated using Weblate (Czech)
Currently translated at 99.1% (349 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-06-17 22:01:54 +02:00
ugyesandHosted Weblate 9792f4ebc1 Translated using Weblate (Hungarian)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2025-06-12 11:02:03 +02:00
Dawid KuźnickiandHosted Weblate a44fc41caa Translated using Weblate (Polish)
Currently translated at 98.2% (346 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-06-10 09:06:17 +00:00
Grzegorz OrlowskiandHosted Weblate 1927dbf0ad Translated using Weblate (Polish)
Currently translated at 98.2% (346 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-06-10 09:06:16 +00:00
RekentekandHosted Weblate 7052674f32 Translated using Weblate (Dutch)
Currently translated at 98.0% (345 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2025-06-10 09:06:16 +00:00
Daniel SchosserandHosted Weblate 9a487f3bb6 Translated using Weblate (German)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-06-10 09:06:15 +00:00
David Havndrup MunchandHosted Weblate d6242c350d Translated using Weblate (Danish)
Currently translated at 98.0% (345 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-06-10 09:06:14 +00:00
PlazecandHosted Weblate 4cc4aceb41 Translated using Weblate (Czech)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-06-10 09:06:13 +00:00
peter cernyandHosted Weblate dd8f4d6201 Translated using Weblate (Slovak)
Currently translated at 99.7% (351 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2025-06-07 21:01:56 +02:00
Usama KhalilandHosted Weblate f44dcdf124 Translated using Weblate (Arabic)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-06-07 21:01:54 +02:00
thehijackerandHosted Weblate 65e8f2317e Translated using Weblate (Slovenian)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2025-06-07 21:01:51 +02:00
SunSpringandHosted Weblate 1c4f43db7c Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-06-07 21:01:49 +02:00
max grakovandHosted Weblate 24b9df6fbd Translated using Weblate (Russian)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-06-07 21:01:47 +02:00
Максим ГорпинічandHosted Weblate c46891ecb0 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-06-06 00:01:47 +02:00
Anders NormanandHosted Weblate e723601f6b Translated using Weblate (Norwegian Bokmål)
Currently translated at 71.5% (251 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2025-06-04 00:17:08 +02:00
DRandHosted Weblate 77545df174 Translated using Weblate (Hebrew)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/he/
2025-06-04 00:17:07 +02:00
Grzegorz OrlowskiandHosted Weblate 7e136b17a5 Translated using Weblate (Polish)
Currently translated at 98.2% (345 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-06-04 00:17:07 +02:00
Adolfo Jayme BarrientosandHosted Weblate a970a06f57 Translated using Weblate (Catalan)
Currently translated at 80.0% (281 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-06-04 00:17:06 +02:00
ABS translatorandHosted Weblate 95dd78d4aa Translated using Weblate (Arabic)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-06-04 00:17:05 +02:00
Grzegorz OrlowskiandHosted Weblate eeaf33be20 Translated using Weblate (Polish)
Currently translated at 96.8% (340 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-06-04 00:17:04 +02:00
Adolfo Jayme BarrientosandHosted Weblate 7ed0cd26fe Translated using Weblate (Spanish)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2025-06-04 00:17:03 +02:00
advplyr d26403c800 Add book/podcast library filter for RSS Feed Open 2025-06-03 17:16:54 -05:00
advplyrandGitHub 5ed6e3a6a3 Merge pull request #1573 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-05-24 17:11:31 -05:00
peter cernyandHosted Weblate 7e83405370 Translated using Weblate (Slovak)
Currently translated at 99.7% (350 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2025-05-24 00:19:32 +02:00
biuklijaandHosted Weblate a5de31da2b Translated using Weblate (Croatian)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-05-24 00:19:31 +02:00
peter cernyandHosted Weblate 39c06005ed Translated using Weblate (Slovak)
Currently translated at 93.7% (329 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2025-05-24 00:19:30 +02:00
Usama KhalilandHosted Weblate 9479160044 Translated using Weblate (Arabic)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-05-24 00:19:30 +02:00
Usama KhalilandHosted Weblate 46ec7369e5 Translated using Weblate (Arabic)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-05-24 00:19:29 +02:00
ABS translatorandHosted Weblate 424e7d742b Translated using Weblate (Arabic)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-05-24 00:19:28 +02:00
Usama KhalilandHosted Weblate 44036399b9 Translated using Weblate (Arabic)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-05-24 00:19:28 +02:00
Adolfo Jayme BarrientosandHosted Weblate e4394da467 Translated using Weblate (Catalan)
Currently translated at 79.4% (279 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-05-24 00:19:27 +02:00
Usama KhalilandHosted Weblate b837ada3f8 Translated using Weblate (Arabic)
Currently translated at 76.6% (269 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-05-24 00:19:27 +02:00
Adolfo Jayme BarrientosandHosted Weblate a623de89aa Translated using Weblate (Spanish)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2025-05-24 00:19:26 +02:00
peter cernyandHosted Weblate 2768407907 Translated using Weblate (Slovak)
Currently translated at 85.4% (300 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2025-05-24 00:19:26 +02:00
polarwoodandHosted Weblate 64ed35d2b1 Translated using Weblate (Turkish)
Currently translated at 77.7% (273 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/tr/
2025-05-24 00:19:25 +02:00
J. LavoieandHosted Weblate b92f4dfa02 Translated using Weblate (Italian)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2025-05-24 00:19:25 +02:00
J. LavoieandHosted Weblate e4f02b7a51 Translated using Weblate (French)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2025-05-24 00:19:24 +02:00
J. LavoieandHosted Weblate 24c3f45018 Translated using Weblate (German)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-05-24 00:19:23 +02:00
MichalandHosted Weblate fcf55eb581 Translated using Weblate (Slovak)
Currently translated at 54.4% (191 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2025-05-24 00:19:23 +02:00
peter cernyandHosted Weblate 9ade7add32 Translated using Weblate (Slovak)
Currently translated at 54.4% (191 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2025-05-24 00:19:22 +02:00
Jaakko RantamäkiandHosted Weblate d16dfcb4b4 Translated using Weblate (Finnish)
Currently translated at 98.2% (345 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2025-05-24 00:19:22 +02:00
Максим ГорпинічandHosted Weblate 37c58a5d32 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-05-24 00:19:21 +02:00
peter cernyandHosted Weblate e6cdde4f70 Translated using Weblate (Slovak)
Currently translated at 13.6% (48 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2025-05-24 00:19:20 +02:00
Oleg IvasenkoandHosted Weblate 93c0adecbd Translated using Weblate (Russian)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-05-24 00:19:20 +02:00
advplyrandGitHub 33d44a5181 Merge pull request #1584 from advplyr/abridged_indicator
Add abridged indicator #1477
2025-05-23 17:19:11 -05:00
advplyr 80e6fa7e7a Add abridged indicator #1477 2025-05-23 17:04:26 -05:00
advplyrandGitHub b8eed48112 Merge pull request #1582 from advplyr/handle_android_edgetoedge
Fix android system bars overlapping UI #1564 #1574
2025-05-21 17:52:19 -05:00
93 changed files with 4370 additions and 605 deletions
+2 -4
View File
@@ -72,12 +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.*
multiple: true
options:
- 'Android App - 0.10.0'
- 'iOS App - 0.10.0'
- 'Android App - 0.9.81'
- 'iOS App - 0.9.81'
- 'Android App - 0.9.80'
- 'iOS App - 0.9.80'
- 'Android App - 0.9.79'
- 'iOS App - 0.9.79'
validations:
required: true
- type: dropdown
+2 -4
View File
@@ -43,12 +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.*
multiple: true
options:
- 'Android App - 0.10.0'
- 'iOS App - 0.10.0'
- 'Android App - 0.9.81'
- 'iOS App - 0.9.81'
- 'Android App - 0.9.80'
- 'iOS App - 0.9.80'
- 'Android App - 0.9.79'
- 'iOS App - 0.9.79'
validations:
required: true
- type: textarea
+2 -2
View File
@@ -36,8 +36,8 @@ android {
applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 112
versionName "0.9.81-beta"
versionCode 114
versionName "0.10.1-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
manifestPlaceholders = [
"appAuthRedirectScheme": "com.audiobookshelf.app"
@@ -14,6 +14,7 @@ data class AudioTrack(
var metadata: FileMetadata?,
var isLocal: Boolean,
var localFileId: String?,
// TODO: This should no longer be necessary
var serverIndex: Int? // Need to know if server track index is different
) {
@@ -32,11 +32,14 @@ enum class AndroidAutoBrowseSeriesSequenceOrderSetting {
ASC, DESC
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class ServerConnectionConfig(
var id:String,
var index:Int,
var name:String,
var address:String,
// version added after 0.9.81-beta
var version:String?,
var userId:String,
var username:String,
var token:String,
@@ -53,6 +53,11 @@ class LibraryItem(
return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
}
// As of v2.17.0 token is not needed with cover image requests
if (DeviceManager.isServerVersionGreaterThanOrEqualTo("2.17.0")) {
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover")
}
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")
}
@@ -149,6 +149,16 @@ class PlaybackSession(
return total
}
@JsonIgnore
fun checkIsServerVersionGte(compareVersion: String): Boolean {
// Safety check this playback session is the same one currently connected (should always be)
if (DeviceManager.serverConnectionConfigId != serverConnectionConfigId) {
return false
}
return DeviceManager.isServerVersionGreaterThanOrEqualTo(compareVersion)
}
@JsonIgnore
fun getCoverUri(ctx: Context): Uri {
if (localLibraryItem?.coverContentUrl != null) {
@@ -168,12 +178,27 @@ class PlaybackSession(
if (coverPath == null)
return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
// As of v2.17.0 token is not needed with cover image requests
if (checkIsServerVersionGte("2.17.0")) {
return Uri.parse("$serverAddress/api/items/$libraryItemId/cover")
}
return Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")
}
@JsonIgnore
fun getContentUri(audioTrack: AudioTrack): Uri {
if (isLocal) return Uri.parse(audioTrack.contentUrl) // Local content url
// As of v2.22.0 tracks use a different endpoint
// See: https://github.com/advplyr/audiobookshelf/pull/4263
if (checkIsServerVersionGte("2.22.0")) {
return if (isDirectPlay) {
Uri.parse("$serverAddress/public/session/$id/track/${audioTrack.index}")
} else {
// Transcode uses HlsRouter on server
Uri.parse("$serverAddress${audioTrack.contentUrl}")
}
}
return Uri.parse("$serverAddress${audioTrack.contentUrl}?token=${DeviceManager.token}")
}
@@ -264,14 +289,16 @@ class PlaybackSession(
com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_AUDIOBOOK_CHAPTER
)
// As of v2.17.0 token is not needed with cover image requests
val coverUri = if (checkIsServerVersionGte("2.17.0")) {
Uri.parse("$serverAddress/api/items/$libraryItemId/cover")
} else {
Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")
}
// Cast always uses server cover uri
coverPath?.let {
castMetadata.addImage(
WebImage(
Uri.parse(
"$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}"
)
)
)
castMetadata.addImage(WebImage(coverUri))
}
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_TITLE, displayTitle ?: "")
@@ -41,6 +41,7 @@ object DeviceManager {
get() = serverConnectionConfig?.userId ?: ""
val token
get() = serverConnectionConfig?.token ?: ""
val serverVersion get() = serverConnectionConfig?.version ?: ""
val isConnectedToServer
get() = serverConnectionConfig != null
@@ -111,6 +112,41 @@ object DeviceManager {
return id?.let { deviceData.serverConnectionConfigs.find { it.id == id } }
}
/**
* Check if the currently connected server version is >= compareVersion
* Abs server only uses major.minor.patch
* Note: Version is returned in Abs auth payloads starting v2.6.0
* Note: Version is saved with the server connection config starting after v0.9.81
*
* @example
* serverVersion=2.25.1
* isServerVersionGreaterThanOrEqualTo("2.26.0") = false
*
* serverVersion=2.26.1
* isServerVersionGreaterThanOrEqualTo("2.26.0") = true
*/
fun isServerVersionGreaterThanOrEqualTo(compareVersion:String):Boolean {
if (serverVersion == "") return false
if (compareVersion == "") return true
val serverVersionParts = serverVersion.split(".").map { it.toIntOrNull() ?: 0 }
val compareVersionParts = compareVersion.split(".").map { it.toIntOrNull() ?: 0 }
// Compare major, minor, and patch components
for (i in 0 until maxOf(serverVersionParts.size, compareVersionParts.size)) {
val serverVersionComponent = serverVersionParts.getOrElse(i) { 0 }
val compareVersionComponent = compareVersionParts.getOrElse(i) { 0 }
if (serverVersionComponent < compareVersionComponent) {
return false // Server version is less than compareVersion
} else if (serverVersionComponent > compareVersionComponent) {
return true // Server version is greater than compareVersion
}
}
return true // versions are equal in major, minor, and patch
}
/**
* Checks the network connectivity status.
* @param ctx The context to use for checking connectivity.
@@ -0,0 +1,124 @@
package com.audiobookshelf.app.managers
import android.content.Context
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import android.util.Log
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
class SecureStorage(private val context: Context) {
companion object {
private const val TAG = "SecureStorage"
private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
private const val KEY_ALIAS = "AudiobookshelfRefreshTokens"
private const val TRANSFORMATION = "AES/GCM/NoPadding"
private const val IV_LENGTH = 12
private const val TAG_LENGTH = 128
}
private val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply {
load(null)
}
/**
* Encrypts and stores a refresh token for a specific server connection
*/
fun storeRefreshToken(serverConnectionId: String, refreshToken: String): Boolean {
return try {
val key = getOrCreateKey()
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, key)
val encryptedBytes = cipher.doFinal(refreshToken.toByteArray(Charsets.UTF_8))
val combined = cipher.iv + encryptedBytes
val encoded = Base64.encodeToString(combined, Base64.DEFAULT)
val sharedPrefs = context.getSharedPreferences("SecureStorage", Context.MODE_PRIVATE)
sharedPrefs.edit().putString("refresh_token_$serverConnectionId", encoded).apply()
Log.d(TAG, "Successfully stored encrypted refresh token for server: $serverConnectionId")
true
} catch (e: Exception) {
Log.e(TAG, "Failed to store refresh token for server: $serverConnectionId", e)
false
}
}
/**
* Retrieves and decrypts a refresh token for a specific server connection
*/
fun getRefreshToken(serverConnectionId: String): String? {
return try {
val sharedPrefs = context.getSharedPreferences("SecureStorage", Context.MODE_PRIVATE)
val encoded = sharedPrefs.getString("refresh_token_$serverConnectionId", null) ?: return null
val combined = Base64.decode(encoded, Base64.DEFAULT)
val iv = combined.copyOfRange(0, IV_LENGTH)
val encryptedBytes = combined.copyOfRange(IV_LENGTH, combined.size)
val key = getOrCreateKey()
val cipher = Cipher.getInstance(TRANSFORMATION)
val spec = GCMParameterSpec(TAG_LENGTH, iv)
cipher.init(Cipher.DECRYPT_MODE, key, spec)
val decryptedBytes = cipher.doFinal(encryptedBytes)
String(decryptedBytes, Charsets.UTF_8)
} catch (e: Exception) {
Log.e(TAG, "Failed to retrieve refresh token for server: $serverConnectionId", e)
null
}
}
/**
* Removes a refresh token for a specific server connection
*/
fun removeRefreshToken(serverConnectionId: String): Boolean {
return try {
val sharedPrefs = context.getSharedPreferences("SecureStorage", Context.MODE_PRIVATE)
sharedPrefs.edit().remove("refresh_token_$serverConnectionId").apply()
Log.d(TAG, "Successfully removed refresh token for server: $serverConnectionId")
true
} catch (e: Exception) {
Log.e(TAG, "Failed to remove refresh token for server: $serverConnectionId", e)
false
}
}
/**
* Checks if a refresh token exists for a specific server connection
*/
fun hasRefreshToken(serverConnectionId: String): Boolean {
val sharedPrefs = context.getSharedPreferences("SecureStorage", Context.MODE_PRIVATE)
return sharedPrefs.contains("refresh_token_$serverConnectionId")
}
private fun getOrCreateKey(): SecretKey {
return if (keyStore.containsAlias(KEY_ALIAS)) {
keyStore.getKey(KEY_ALIAS, null) as SecretKey
} else {
createKey()
}
}
private fun createKey(): SecretKey {
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER)
val keyGenSpec = KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(false)
.setRandomizedEncryptionRequired(true)
.build()
keyGenerator.init(keyGenSpec)
return keyGenerator.generateKey()
}
}
@@ -15,6 +15,7 @@ import org.json.JSONException
import org.json.JSONObject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import java.util.concurrent.atomic.AtomicInteger
class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
val tag = "MediaManager"
@@ -175,21 +176,20 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
* Load personalized shelves from server for all libraries.
* [cb] resolves when all libraries are processed
*/
fun populatePersonalizedDataForAllLibraries(cb: () -> Unit ) {
serverLibraries.forEach {
libraryPersonalizationsDone++
Log.d(tag, "Loading personalization for library ${it.name} - ${it.id} - ${it.mediaType}")
populatePersonalizedDataForLibrary(it.id) {
Log.d(tag, "Loaded personalization for library ${it.name} - ${it.id} - ${it.mediaType}")
libraryPersonalizationsDone--
fun populatePersonalizedDataForAllLibraries(cb: () -> Unit) {
val remaining = AtomicInteger(serverLibraries.size)
serverLibraries.forEach { lib ->
Log.d(tag, "Loading personalization for library ${lib.name}")
populatePersonalizedDataForLibrary(lib.id) {
Log.d(tag, "Loaded personalization for library ${lib.name}")
if (remaining.decrementAndGet() == 0) {
Log.d(tag, "Finished loading all library personalization data")
allLibraryPersonalizationsDone = true
cb()
}
}
}
while (libraryPersonalizationsDone > 0) { }
Log.d(tag, "Finished loading all library personalization data")
allLibraryPersonalizationsDone = true
cb()
}
/**
@@ -1087,6 +1087,26 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private val DOWNLOADS_ROOT = "__DOWNLOADS__"
private val CONTINUE_ROOT = "__CONTINUE__"
private lateinit var browseTree: BrowseTree
private val browseTreeInitListeners = mutableListOf<() -> Unit>()
private fun waitForBrowseTree(cb: () -> Unit)
{
if (this::browseTree.isInitialized)
{
cb()
}
else
{
browseTreeInitListeners += cb
}
}
private fun onBrowseTreeInitialized()
{
// Called after browseTree is assigned for the first time
browseTreeInitListeners.forEach { it.invoke() }
browseTreeInitListeners.clear()
}
// Only allowing android auto or similar to access media browser service
// normal loading of audiobooks is handled in webview (not natively)
@@ -1257,6 +1277,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
mediaManager.serverLibraries,
mediaManager.allLibraryPersonalizationsDone
)
onBrowseTreeInitialized()
val children =
browseTree[parentMediaId]?.map { item ->
Log.d(tag, "Found top menu item: ${item.description.title}")
@@ -1291,6 +1312,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
mediaManager.serverLibraries,
mediaManager.allLibraryPersonalizationsDone
)
onBrowseTreeInitialized()
val children =
browseTree[parentMediaId]?.map { item ->
Log.d(tag, "Found top menu item: ${item.description.title}")
@@ -1303,22 +1325,40 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
AbsLogger.info(tag, "onLoadChildren: Android auto data loaded")
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
} else if (parentMediaId == LIBRARIES_ROOT || parentMediaId == RECENTLY_ROOT) {
} else if (parentMediaId == LIBRARIES_ROOT || parentMediaId == RECENTLY_ROOT)
{
Log.d(tag, "First load done: $firstLoadDone")
if (!firstLoadDone) {
if (!firstLoadDone)
{
result.sendResult(null)
return
}
// Wait until top-menu is initialized
while (!this::browseTree.isInitialized) {}
val children =
browseTree[parentMediaId]?.map { item ->
Log.d(tag, "[MENU: $parentMediaId] Showing list item ${item.description.title}")
MediaBrowserCompat.MediaItem(
item.description,
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
}
if (!this::browseTree.isInitialized)
{
// ✅ good: detach and wait for init
result.detach()
waitForBrowseTree {
val children = browseTree[parentMediaId]?.map { item ->
Log.d(tag, "[MENU: $parentMediaId] Showing list item ${item.description.title}")
MediaBrowserCompat.MediaItem(
item.description,
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
}
return
}
// Already initialized: just return
val children = browseTree[parentMediaId]?.map { item ->
Log.d(tag, "[MENU: $parentMediaId] Showing list item ${item.description.title}")
MediaBrowserCompat.MediaItem(
item.description,
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
)
}
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
} else if (mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library
Log.d(tag, "Loading items for library $parentMediaId")
@@ -8,6 +8,7 @@ import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.media.MediaEventManager
import com.audiobookshelf.app.server.ApiHandler
import com.audiobookshelf.app.managers.SecureStorage
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
@@ -22,17 +23,21 @@ class AbsDatabase : Plugin() {
val tag = "AbsDatabase"
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
lateinit var mainActivity: MainActivity
lateinit var apiHandler: ApiHandler
private lateinit var mainActivity: MainActivity
private lateinit var apiHandler: ApiHandler
private lateinit var secureStorage: SecureStorage
data class LocalMediaProgressPayload(val value:List<LocalMediaProgress>)
data class LocalLibraryItemsPayload(val value:List<LocalLibraryItem>)
data class LocalFoldersPayload(val value:List<LocalFolder>)
data class ServerConnConfigPayload(val id:String?, val index:Int, val name:String?, val userId:String, val username:String, val token:String, val address:String?, val customHeaders:Map<String,String>?)
data class ServerConnConfigPayload(val id:String?, val index:Int, val name:String?, val userId:String, val username:String, var version:String, val token:String, val refreshToken:String?, val address:String?, val customHeaders:Map<String,String>?)
override fun load() {
mainActivity = (activity as MainActivity)
apiHandler = ApiHandler(mainActivity)
ApiHandler.absDatabaseNotifyListeners = ::notifyListeners
secureStorage = SecureStorage(mainActivity)
DeviceManager.dbManager.cleanLocalMediaProgress()
DeviceManager.dbManager.cleanLocalLibraryItems()
@@ -120,7 +125,9 @@ class AbsDatabase : Plugin() {
val userId = serverConfigPayload.userId
val username = serverConfigPayload.username
val token = serverConfigPayload.token
val serverVersion = serverConfigPayload.version
val accessToken = serverConfigPayload.token
val refreshToken = serverConfigPayload.refreshToken // Refresh only sent after login or refresh
GlobalScope.launch(Dispatchers.IO) {
if (serverConnectionConfig == null) { // New Server Connection
@@ -129,7 +136,16 @@ class AbsDatabase : Plugin() {
// Create new server connection config
val sscId = DeviceManager.getBase64Id("$serverAddress@$username")
val sscIndex = DeviceManager.deviceData.serverConnectionConfigs.size
serverConnectionConfig = ServerConnectionConfig(sscId, sscIndex, "$serverAddress ($username)", serverAddress, userId, username, token, serverConfigPayload.customHeaders)
// Store refresh token securely if provided
val hasRefreshToken = if (!refreshToken.isNullOrEmpty()) {
secureStorage.storeRefreshToken(sscId, refreshToken)
} else {
false
}
Log.d(tag, "Refresh token secured = $hasRefreshToken")
serverConnectionConfig = ServerConnectionConfig(sscId, sscIndex, "$serverAddress ($username)", serverAddress, serverVersion, userId, username, accessToken, serverConfigPayload.customHeaders)
// Add and save
DeviceManager.deviceData.serverConnectionConfigs.add(serverConnectionConfig!!)
@@ -137,14 +153,21 @@ class AbsDatabase : Plugin() {
DeviceManager.dbManager.saveDeviceData(DeviceManager.deviceData)
} else {
var shouldSave = false
if (serverConnectionConfig?.username != username || serverConnectionConfig?.token != token) {
if (serverConnectionConfig?.username != username || serverConnectionConfig?.token != accessToken || serverConnectionConfig?.version != serverVersion) {
serverConnectionConfig?.userId = userId
serverConnectionConfig?.username = username
serverConnectionConfig?.name = "${serverConnectionConfig?.address} (${serverConnectionConfig?.username})"
serverConnectionConfig?.token = token
serverConnectionConfig?.version = serverVersion
serverConnectionConfig?.token = accessToken
shouldSave = true
}
// Update refresh token if provided
if (!refreshToken.isNullOrEmpty()) {
val stored = secureStorage.storeRefreshToken(serverConnectionConfig!!.id, refreshToken)
Log.d(tag, "Refresh token secured = $stored")
}
// Set last connection config
if (DeviceManager.deviceData.lastServerConnectionConfigId != serverConfigPayload.id) {
DeviceManager.deviceData.lastServerConnectionConfigId = serverConfigPayload.id
@@ -163,6 +186,10 @@ class AbsDatabase : Plugin() {
fun removeServerConnectionConfig(call:PluginCall) {
GlobalScope.launch(Dispatchers.IO) {
val serverConnectionConfigId = call.getString("serverConnectionConfigId", "").toString()
// Remove refresh token if it exists
secureStorage.removeRefreshToken(serverConnectionConfigId)
DeviceManager.deviceData.serverConnectionConfigs = DeviceManager.deviceData.serverConnectionConfigs.filter { it.id != serverConnectionConfigId } as MutableList<ServerConnectionConfig>
if (DeviceManager.deviceData.lastServerConnectionConfigId == serverConnectionConfigId) {
DeviceManager.deviceData.lastServerConnectionConfigId = null
@@ -175,6 +202,42 @@ class AbsDatabase : Plugin() {
}
}
@PluginMethod
fun getRefreshToken(call:PluginCall) {
val serverConnectionConfigId = call.getString("serverConnectionConfigId", "").toString()
GlobalScope.launch(Dispatchers.IO) {
val refreshToken = secureStorage.getRefreshToken(serverConnectionConfigId)
if (refreshToken != null) {
val result = JSObject()
result.put("refreshToken", refreshToken)
call.resolve(result)
} else {
call.resolve()
}
}
}
@PluginMethod
fun clearRefreshToken(call:PluginCall) {
val serverConnectionConfigId = call.getString("serverConnectionConfigId", "").toString()
val refreshToken = secureStorage.removeRefreshToken(serverConnectionConfigId)
val result = JSObject()
result.put("success", refreshToken)
call.resolve(result)
}
@PluginMethod
fun getAccessToken(call:PluginCall) {
val serverConnectionConfigId = call.getString("serverConnectionConfigId", "").toString()
val serverConnectionConfig = DeviceManager.deviceData.serverConnectionConfigs.find { it.id == serverConnectionConfigId }
val token = serverConnectionConfig?.token ?: ""
val ret = JSObject()
ret.put("token", token)
call.resolve(ret)
}
@PluginMethod
fun logout(call:PluginCall) {
GlobalScope.launch(Dispatchers.IO) {
@@ -14,6 +14,7 @@ import com.audiobookshelf.app.media.SyncResult
import com.audiobookshelf.app.models.User
import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.plugins.AbsLogger
import com.audiobookshelf.app.managers.SecureStorage
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
@@ -33,9 +34,19 @@ import java.util.concurrent.TimeUnit
class ApiHandler(var ctx:Context) {
val tag = "ApiHandler"
companion object {
// For sending data back to the Webview frontend
lateinit var absDatabaseNotifyListeners:(String, JSObject) -> Unit
fun checkAbsDatabaseNotifyListenersInitted():Boolean {
return ::absDatabaseNotifyListeners.isInitialized
}
}
private var defaultClient = OkHttpClient()
private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build()
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
private var secureStorage = SecureStorage(ctx)
data class LocalSessionsSyncRequestPayload(val sessions:List<PlaybackSession>, val deviceInfo:DeviceInfo)
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -110,6 +121,13 @@ class ApiHandler(var ctx:Context) {
override fun onResponse(call: Call, response: Response) {
response.use {
if (it.code == 401) {
// Handle 401 Unauthorized by attempting token refresh
AbsLogger.info(tag, "makeRequest: 401 Unauthorized for request to \"${request.url}\" - attempt token refresh")
handleTokenRefresh(request, httpClient, cb)
return
}
if (!it.isSuccessful) {
val jsobj = JSObject()
jsobj.put("error", "Unexpected code $response")
@@ -142,6 +160,269 @@ class ApiHandler(var ctx:Context) {
})
}
/**
* Handles token refresh when a 401 Unauthorized response is received
* This function will:
* 1. Get the refresh token from secure storage for the current server connection
* 2. Make a request to /auth/refresh endpoint with the refresh token
* 3. Update the stored tokens with the new access token
* 4. Retry the original request with the new access token
* 5. If refresh fails, handle logout
*
* @param originalRequest The original request that failed with 401
* @param httpClient The HTTP client to use for the request
* @param callback The callback to return the response
*/
private fun handleTokenRefresh(originalRequest: Request, httpClient: OkHttpClient?, callback: (JSObject) -> Unit) {
try {
AbsLogger.info(tag, "handleTokenRefresh: Attempting to refresh auth tokens for server ${DeviceManager.serverConnectionConfigString}")
// Get current server connection config ID
val serverConnectionConfigId = DeviceManager.serverConnectionConfigId
if (serverConnectionConfigId.isEmpty()) {
AbsLogger.error(tag, "handleTokenRefresh: Unable to refresh auth tokens. No server connection config ID")
val errorObj = JSObject()
errorObj.put("error", "No server connection available")
callback(errorObj)
return
}
// Get refresh token from secure storage
val refreshToken = secureStorage.getRefreshToken(serverConnectionConfigId)
if (refreshToken.isNullOrEmpty()) {
AbsLogger.error(tag, "handleTokenRefresh: Unable to refresh auth tokens. No refresh token available for server ${DeviceManager.serverConnectionConfigString}")
val errorObj = JSObject()
errorObj.put("error", "No refresh token available")
callback(errorObj)
return
}
Log.d(tag, "handleTokenRefresh: Retrieved refresh token, attempting to refresh access token")
// Create refresh token request
val refreshEndpoint = "${DeviceManager.serverAddress}/auth/refresh"
val refreshRequest = Request.Builder()
.url(refreshEndpoint)
.addHeader("x-refresh-token", refreshToken)
.addHeader("Content-Type", "application/json")
.post(EMPTY_REQUEST)
.build()
// Make the refresh request
val client = httpClient ?: defaultClient
client.newCall(refreshRequest).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e(tag, "handleTokenRefresh: Failed to connect to refresh endpoint", e)
AbsLogger.error(tag, "handleTokenRefresh: Failed to connect to refresh endpoint for server ${DeviceManager.serverConnectionConfigString} (error: ${e.message})")
handleRefreshFailure(callback)
}
override fun onResponse(call: Call, response: Response) {
response.use {
if (!it.isSuccessful) {
AbsLogger.error(tag, "handleTokenRefresh: Refresh request failed with status ${it.code} for server ${DeviceManager.serverConnectionConfigString}")
handleRefreshFailure(callback)
return
}
val bodyString = it.body!!.string()
try {
val responseJson = JSONObject(bodyString)
val userObj = responseJson.optJSONObject("user")
if (userObj == null) {
AbsLogger.error(tag, "handleTokenRefresh: No user object in refresh response for server ${DeviceManager.serverConnectionConfigString}")
handleRefreshFailure(callback)
return
}
val newAccessToken = userObj.optString("accessToken")
val newRefreshToken = userObj.optString("refreshToken")
if (newAccessToken.isEmpty()) {
AbsLogger.error(tag, "handleTokenRefresh: No access token in refresh response for server ${DeviceManager.serverConnectionConfigString}")
handleRefreshFailure(callback)
return
}
Log.d(tag, "handleTokenRefresh: Successfully obtained new access token")
// Update tokens in secure storage and device manager
updateTokens(newAccessToken, newRefreshToken.ifEmpty { refreshToken }, serverConnectionConfigId)
// Retry the original request with the new access token
Log.d(tag, "handleTokenRefresh: Retrying original request with new token")
retryOriginalRequest(originalRequest, newAccessToken, httpClient, callback)
} catch (e: Exception) {
Log.e(tag, "handleTokenRefresh: Failed to parse refresh response", e)
AbsLogger.error(tag, "handleTokenRefresh: Failed to parse refresh response for server ${DeviceManager.serverConnectionConfigString} (error: ${e.message})")
handleRefreshFailure(callback)
}
}
}
})
} catch (e: Exception) {
Log.e(tag, "handleTokenRefresh: Unexpected error during token refresh", e)
handleRefreshFailure(callback)
}
}
/**
* Updates the stored tokens with new access and refresh tokens
*
* @param newAccessToken The new access token
* @param newRefreshToken The new refresh token (or existing one if not provided)
*/
private fun updateTokens(newAccessToken: String, newRefreshToken: String, serverConnectionConfigId: String) {
try {
// Update the refresh token in secure storage if it's new
if (newRefreshToken != secureStorage.getRefreshToken(serverConnectionConfigId)) {
secureStorage.storeRefreshToken(serverConnectionConfigId, newRefreshToken)
Log.d(tag, "updateTokens: Updated refresh token in secure storage")
}
// Update the access token in the current server connection config
DeviceManager.serverConnectionConfig?.let { config ->
config.token = newAccessToken
DeviceManager.dbManager.saveDeviceData(DeviceManager.deviceData)
Log.d(tag, "updateTokens: Updated access token in server connection config")
}
// Send access token to Webview frontend
if (checkAbsDatabaseNotifyListenersInitted()) {
val tokenJsObject = JSObject()
tokenJsObject.put("accessToken", newAccessToken)
absDatabaseNotifyListeners("onTokenRefresh", tokenJsObject)
} else {
// Can happen if Webview is never run
Log.i(tag, "AbsDatabaseNotifyListeners is not initialized so cannot send new access token")
}
AbsLogger.info(tag, "updateTokens: Successfully refreshed auth tokens for server ${DeviceManager.serverConnectionConfigString}")
} catch (e: Exception) {
Log.e(tag, "updateTokens: Failed to update tokens", e)
AbsLogger.error(tag, "updateTokens: Failed to refresh auth tokens for server ${DeviceManager.serverConnectionConfigString} (error: ${e.message})")
}
}
/**
* Retries the original request with the new access token
*
* @param originalRequest The original request to retry
* @param newAccessToken The new access token to use
* @param httpClient The HTTP client to use
* @param callback The callback to return the response
*/
private fun retryOriginalRequest(originalRequest: Request, newAccessToken: String, httpClient: OkHttpClient?, callback: (JSObject) -> Unit) {
try {
// Create a new request with the updated authorization header
val newRequest = originalRequest.newBuilder()
.removeHeader("Authorization")
.addHeader("Authorization", "Bearer $newAccessToken")
.build()
Log.d(tag, "retryOriginalRequest: Retrying request to ${newRequest.url}")
// Make the retry request
val client = httpClient ?: defaultClient
client.newCall(newRequest).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e(tag, "retryOriginalRequest: Failed to retry request", e)
AbsLogger.error(tag, "retryOriginalRequest: Failed to retry request after token refresh for server ${DeviceManager.serverConnectionConfigString} (error: ${e.message})")
val errorObj = JSObject()
errorObj.put("error", "Failed to retry request after token refresh")
callback(errorObj)
}
override fun onResponse(call: Call, response: Response) {
response.use {
if (!it.isSuccessful) {
Log.e(tag, "retryOriginalRequest: Retry request failed with status ${it.code}")
AbsLogger.error(tag, "retryOriginalRequest: Retry request failed with status ${it.code} for server ${DeviceManager.serverConnectionConfigString}")
val errorObj = JSObject()
errorObj.put("error", "Retry request failed with status ${it.code}")
callback(errorObj)
return
}
val bodyString = it.body!!.string()
if (bodyString == "OK") {
callback(JSObject())
} else {
try {
var jsonObj = JSObject()
if (bodyString.startsWith("[")) {
val array = JSArray(bodyString)
jsonObj.put("value", array)
} else {
jsonObj = JSObject(bodyString)
}
callback(jsonObj)
} catch(je:JSONException) {
Log.e(tag, "retryOriginalRequest: Invalid JSON response ${je.localizedMessage} from body $bodyString")
val errorObj = JSObject()
errorObj.put("error", "Invalid response body")
callback(errorObj)
}
}
}
}
})
} catch (e: Exception) {
Log.e(tag, "retryOriginalRequest: Unexpected error during retry", e)
AbsLogger.error(tag, "retryOriginalRequest: Unexpected error during retry for server ${DeviceManager.serverConnectionConfigString}")
val errorObj = JSObject()
errorObj.put("error", "Failed to retry request")
callback(errorObj)
}
}
/**
* Handles the case when token refresh fails
* This will clear the current session and notify the callback
*
* @param callback The callback to return the error
*/
private fun handleRefreshFailure(callback: (JSObject) -> Unit) {
try {
Log.d(tag, "handleRefreshFailure: Token refresh failed, clearing session")
// Clear the current server connection
DeviceManager.serverConnectionConfig = null
DeviceManager.deviceData.lastServerConnectionConfigId = null
DeviceManager.dbManager.saveDeviceData(DeviceManager.deviceData)
// Remove refresh token from secure storage
val serverConnectionConfigId = DeviceManager.serverConnectionConfigId
if (serverConnectionConfigId.isNotEmpty()) {
secureStorage.removeRefreshToken(serverConnectionConfigId)
}
val errorObj = JSObject()
errorObj.put("error", "Authentication failed - please login again")
callback(errorObj)
if (checkAbsDatabaseNotifyListenersInitted()) {
val tokenJsObject = JSObject()
tokenJsObject.put("error", "Token refresh failed")
if (serverConnectionConfigId.isNotEmpty()) {
tokenJsObject.put("serverConnectionConfigId", serverConnectionConfigId)
}
absDatabaseNotifyListeners("onTokenRefreshFailure", tokenJsObject)
} else {
// Can happen if Webview is never run
Log.i(tag, "AbsDatabaseNotifyListeners is not initialized so cannot send token refresh failure notification")
}
} catch (e: Exception) {
Log.e(tag, "handleRefreshFailure: Error during failure handling", e)
val errorObj = JSObject()
errorObj.put("error", "Authentication failed")
callback(errorObj)
}
}
fun getCurrentUser(cb: (User?) -> Unit) {
getRequest("/api/me", null, null) {
if (it.has("error")) {
+19
View File
@@ -22,6 +22,25 @@
--gradient-minimized-audio-player: linear-gradient(145deg, rgba(38, 38, 38, 0.5) 0%, rgba(38, 38, 38, 0.9) 20%, rgb(38, 38, 38) 60%);
}
html[data-theme='black'] {
color: white;
--color-bg: 0 0 0;
--color-bg-hover: 0 0 0;
--color-fg: 230 237 243;
--color-fg-muted: 120 126 132;
--color-primary: 0 0 0;
--color-secondary: 0 0 0;
--color-border: 55 62 65;
--color-bg-toggle: 0 0 0;
--color-bg-toggle-selected: 35 35 35;
--color-track-cursor: 229 231 235;
--color-track: 107 114 128;
--color-track-buffered: 75 85 99;
--gradient-item-page: rgb(0, 0, 0);
--gradient-audio-player: rgb(0, 0, 0);
--gradient-minimized-audio-player: rgb(0, 0, 0);
}
html[data-theme='light'] {
color: black;
--color-bg: 255 255 255;
+12 -12
View File
@@ -4,14 +4,14 @@
<nuxt-link v-show="!showBack" to="/" class="mr-3">
<img src="/Logo.png" class="h-10 w-10" />
</nuxt-link>
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center mr-2 cursor-pointer">
<a v-if="showBack" @click="back" aria-label="Back" class="rounded-full h-10 w-10 flex items-center justify-center mr-2 cursor-pointer">
<span class="material-symbols text-3xl text-fg">arrow_back</span>
</a>
<div v-if="user && currentLibrary">
<div class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
<button type="button" aria-label="Show library modal" class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
<ui-library-icon :icon="currentLibraryIcon" :size="4" font-size="base" />
<p class="text-sm leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
</div>
</button>
</div>
<widgets-connection-indicator />
@@ -21,19 +21,19 @@
<widgets-download-progress-indicator />
<!-- Must be connected to a server to cast, only supports media items on server -->
<div v-show="isCastAvailable && user" class="mx-2 cursor-pointer flex items-center" @click="castClick">
<button type="button" aria-label="Cast" v-show="isCastAvailable && user" class="mx-2 cursor-pointer flex items-center" @click="castClick">
<span class="material-symbols text-2xl leading-none">
{{ isCasting ? 'cast_connected' : 'cast' }}
</span>
</div>
</button>
<nuxt-link v-if="user" class="mx-1.5 flex items-center h-10" to="/search">
<nuxt-link v-if="user" class="mx-1.5 flex items-center h-10" to="/search" aria-label="Search">
<span class="material-symbols text-2xl leading-none">search</span>
</nuxt-link>
<div class="h-7 mx-1.5">
<span class="material-symbols" style="font-size: 1.75rem" @click="clickShowSideDrawer">menu</span>
</div>
<button type="button" aria-label="Toggle side drawer" class="h-7 mx-1.5" @click="clickShowSideDrawer">
<span class="material-symbols" style="font-size: 1.75rem">menu</span>
</button>
</div>
</div>
</template>
@@ -100,14 +100,14 @@ export default {
this.isCastAvailable = data && data.value
}
},
mounted() {
async mounted() {
AbsAudioPlayer.getIsCastAvailable().then((data) => {
this.isCastAvailable = data && data.value
})
this.onCastAvailableUpdateListener = AbsAudioPlayer.addListener('onCastAvailableUpdate', this.onCastAvailableUpdate)
this.onCastAvailableUpdateListener = await AbsAudioPlayer.addListener('onCastAvailableUpdate', this.onCastAvailableUpdate)
},
beforeDestroy() {
if (this.onCastAvailableUpdateListener) this.onCastAvailableUpdateListener.remove()
this.onCastAvailableUpdateListener?.remove()
}
}
</script>
+18 -27
View File
@@ -4,15 +4,15 @@
<div class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: var(--gradient-audio-player)" />
<div class="top-4 left-4 absolute cursor-pointer">
<span class="material-symbols text-5xl" :class="{ 'text-black text-opacity-75': coverBgIsLight }" @click="collapseFullscreen">keyboard_arrow_down</span>
<span class="material-symbols text-5xl" :class="{ 'text-black text-opacity-75': coverBgIsLight && theme !== 'black' }" @click="collapseFullscreen">keyboard_arrow_down</span>
</div>
<div v-show="showCastBtn" class="top-6 right-16 absolute cursor-pointer">
<span class="material-symbols text-3xl" :class="coverBgIsLight ? 'text-black' : ''" @click="castClick">{{ isCasting ? 'cast_connected' : 'cast' }}</span>
<span class="material-symbols text-3xl" :class="coverBgIsLight && theme !== 'black' ? 'text-black' : ''" @click="castClick">{{ isCasting ? 'cast_connected' : 'cast' }}</span>
</div>
<div class="top-6 right-4 absolute cursor-pointer">
<span class="material-symbols text-3xl" :class="{ 'text-black text-opacity-75': coverBgIsLight }" @click="showMoreMenuDialog = true">more_vert</span>
<span class="material-symbols text-3xl" :class="{ 'text-black text-opacity-75': coverBgIsLight && theme !== 'black' }" @click="showMoreMenuDialog = true">more_vert</span>
</div>
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" :class="{ 'text-black text-opacity-75': coverBgIsLight }" style="font-size: 10px">{{ isDirectPlayMethod ? $strings.LabelPlaybackDirect : isLocalPlayMethod ? $strings.LabelPlaybackLocal : $strings.LabelPlaybackTranscode }}</p>
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" :class="{ 'text-black text-opacity-75': coverBgIsLight && theme !== 'black' }" style="font-size: 10px">{{ isDirectPlayMethod ? $strings.LabelPlaybackDirect : isLocalPlayMethod ? $strings.LabelPlaybackLocal : $strings.LabelPlaybackTranscode }}</p>
</div>
<div v-if="playerSettings.useChapterTrack && playerSettings.useTotalTrack && showFullscreen" class="absolute total-track w-full z-30 px-6">
@@ -140,13 +140,6 @@ export default {
readyTrackWidth: 0,
seekedTime: 0,
seekLoading: false,
onPlaybackSessionListener: null,
onPlaybackClosedListener: null,
onPlayingUpdateListener: null,
onMetadataListener: null,
onProgressSyncFailing: null,
onProgressSyncSuccess: null,
onPlaybackSpeedChangedListener: null,
touchStartY: 0,
touchStartTime: 0,
playerSettings: {
@@ -182,6 +175,9 @@ export default {
}
},
computed: {
theme() {
return document.documentElement.dataset.theme || 'dark'
},
menuItems() {
const items = []
// TODO: Implement on iOS
@@ -880,14 +876,14 @@ export default {
async init() {
await this.loadPlayerSettings()
this.onPlaybackSessionListener = AbsAudioPlayer.addListener('onPlaybackSession', this.onPlaybackSession)
this.onPlaybackClosedListener = AbsAudioPlayer.addListener('onPlaybackClosed', this.onPlaybackClosed)
this.onPlaybackFailedListener = AbsAudioPlayer.addListener('onPlaybackFailed', this.onPlaybackFailed)
this.onPlayingUpdateListener = AbsAudioPlayer.addListener('onPlayingUpdate', this.onPlayingUpdate)
this.onMetadataListener = AbsAudioPlayer.addListener('onMetadata', this.onMetadata)
this.onProgressSyncFailing = AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
this.onProgressSyncSuccess = AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
this.onPlaybackSpeedChangedListener = AbsAudioPlayer.addListener('onPlaybackSpeedChanged', this.onPlaybackSpeedChanged)
AbsAudioPlayer.addListener('onPlaybackSession', this.onPlaybackSession)
AbsAudioPlayer.addListener('onPlaybackClosed', this.onPlaybackClosed)
AbsAudioPlayer.addListener('onPlaybackFailed', this.onPlaybackFailed)
AbsAudioPlayer.addListener('onPlayingUpdate', this.onPlayingUpdate)
AbsAudioPlayer.addListener('onMetadata', this.onMetadata)
AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
AbsAudioPlayer.addListener('onPlaybackSpeedChanged', this.onPlaybackSpeedChanged)
},
async screenOrientationChange() {
if (this.isRefreshingUI) return
@@ -981,14 +977,9 @@ export default {
document.body.removeEventListener('touchend', this.touchend)
document.body.removeEventListener('touchmove', this.touchmove)
if (this.onPlayingUpdateListener) this.onPlayingUpdateListener.remove()
if (this.onMetadataListener) this.onMetadataListener.remove()
if (this.onPlaybackSessionListener) this.onPlaybackSessionListener.remove()
if (this.onPlaybackClosedListener) this.onPlaybackClosedListener.remove()
if (this.onPlaybackFailedListener) this.onPlaybackFailedListener.remove()
if (this.onProgressSyncFailing) this.onProgressSyncFailing.remove()
if (this.onProgressSyncSuccess) this.onProgressSyncSuccess.remove()
if (this.onPlaybackSpeedChangedListener) this.onPlaybackSpeedChangedListener.remove()
if (AbsAudioPlayer.removeAllListeners) {
AbsAudioPlayer.removeAllListeners()
}
clearInterval(this.playInterval)
}
}
+9 -9
View File
@@ -351,11 +351,11 @@ export default {
}
}
},
mounted() {
this.onLocalMediaProgressUpdateListener = AbsAudioPlayer.addListener('onLocalMediaProgressUpdate', this.onLocalMediaProgressUpdate)
this.onSleepTimerEndedListener = AbsAudioPlayer.addListener('onSleepTimerEnded', this.onSleepTimerEnded)
this.onSleepTimerSetListener = AbsAudioPlayer.addListener('onSleepTimerSet', this.onSleepTimerSet)
this.onMediaPlayerChangedListener = AbsAudioPlayer.addListener('onMediaPlayerChanged', this.onMediaPlayerChanged)
async mounted() {
this.onLocalMediaProgressUpdateListener = await AbsAudioPlayer.addListener('onLocalMediaProgressUpdate', this.onLocalMediaProgressUpdate)
this.onSleepTimerEndedListener = await AbsAudioPlayer.addListener('onSleepTimerEnded', this.onSleepTimerEnded)
this.onSleepTimerSetListener = await AbsAudioPlayer.addListener('onSleepTimerSet', this.onSleepTimerSet)
this.onMediaPlayerChangedListener = await AbsAudioPlayer.addListener('onMediaPlayerChanged', this.onMediaPlayerChanged)
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
@@ -370,10 +370,10 @@ export default {
this.$eventBus.$on('device-focus-update', this.deviceFocused)
},
beforeDestroy() {
if (this.onLocalMediaProgressUpdateListener) this.onLocalMediaProgressUpdateListener.remove()
if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove()
if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove()
if (this.onMediaPlayerChangedListener) this.onMediaPlayerChangedListener.remove()
this.onLocalMediaProgressUpdateListener?.remove()
this.onSleepTimerEndedListener?.remove()
this.onSleepTimerSetListener?.remove()
this.onMediaPlayerChangedListener?.remove()
this.$eventBus.$off('abs-ui-ready', this.onReady)
this.$eventBus.$off('play-item', this.playLibraryItem)
+3 -17
View File
@@ -11,11 +11,11 @@
<div class="w-full overflow-y-auto">
<template v-for="item in navItems">
<button v-if="item.action" :key="item.text" class="w-full hover:bg-bg/60 flex items-center py-3 px-6 text-fg-muted" @click="clickAction(item.action)">
<button v-if="item.action" :key="item.text" :tabindex="show ? 0 : -1" class="w-full hover:bg-bg/60 flex items-center py-3 px-6 text-fg-muted" @click="clickAction(item.action)">
<span class="material-symbols fill text-lg">{{ item.icon }}</span>
<p class="pl-4">{{ item.text }}</p>
</button>
<nuxt-link v-else :to="item.to" :key="item.text" class="w-full hover:bg-bg/60 flex items-center py-3 px-6 text-fg" :class="currentRoutePath.startsWith(item.to) ? 'bg-bg-hover/50' : 'text-fg-muted'">
<nuxt-link v-else :to="item.to" :key="item.text" :tabindex="show ? 0 : -1" class="w-full hover:bg-bg/60 flex items-center py-3 px-6 text-fg" :class="currentRoutePath.startsWith(item.to) ? 'bg-bg-hover/50' : 'text-fg-muted'">
<span class="material-symbols fill text-lg">{{ item.icon }}</span>
<p class="pl-4">{{ item.text }}</p>
</nuxt-link>
@@ -179,21 +179,7 @@ export default {
this.show = false
},
async logout() {
if (this.user) {
if (this.$store.getters['getIsPlayerOpen']) {
this.$eventBus.$emit('close-stream')
}
await this.$nativeHttp.post('/logout').catch((error) => {
console.error('Failed to logout', error)
})
}
this.$socket.logout()
await this.$db.logout()
this.$localStore.removeLastLibraryId()
this.$store.commit('user/logout')
this.$store.commit('libraries/setCurrentLibrary', null)
await this.$store.dispatch('user/logout')
},
async disconnect() {
await this.$hapticsImpact()
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<div ref="card" :id="`book-card-${index}`" :style="{ minWidth: width + 'px', maxWidth: width + 'px', height: height + 'px' }" class="rounded-sm z-10 bg-primary cursor-pointer box-shadow-book" @click="clickCard">
<div ref="card" tabindex="0" :id="`book-card-${index}`" :style="{ minWidth: width + 'px', maxWidth: width + 'px', height: height + 'px' }" class="rounded-sm z-10 bg-primary cursor-pointer box-shadow-book" @click="clickCard">
<!-- When cover image does not fill -->
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
<div class="absolute cover-bg" ref="coverBg" />
+60 -6
View File
@@ -14,7 +14,7 @@
<!-- No progress shown for collapsed series or podcasts in library -->
<div v-if="!isPodcast && !collapsedSeries" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * userProgressPercent + 'px' }"></div>
</div>
<div class="flex-grow px-2">
<div class="flex-grow pl-2" :class="showPlayButton ? 'pr-12' : 'pr-2'">
<p class="whitespace-normal line-clamp-2" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">
<span v-if="seriesSequence">#{{ seriesSequence }}&nbsp;</span>{{ displayTitle }}
</p>
@@ -29,6 +29,16 @@
{{ $getString('LabelNumEpisodes', [numEpisodes]) }}
</p>
</div>
<div v-if="showPlayButton" class="absolute top-0 bottom-0 right-0 h-full flex items-center justify-center z-20 pr-1">
<button type="button" class="relative rounded-full bg-fg-muted/50" :class="{ 'p-2': !playerIsStartingForThisMedia }" @click.stop.prevent="play">
<span v-if="!playerIsStartingForThisMedia" class="material-symbols text-2xl fill text-white">{{ playerIsPlaying ? 'pause' : 'play_arrow' }}</span>
<div v-else class="p-2 text-fg w-10 h-10 flex items-center justify-center bg-fg-muted/80 rounded-full overflow-hidden">
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
</div>
</button>
</div>
<div v-if="localLibraryItem || isLocal" class="absolute top-0 right-0 z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<span class="material-symbols text-2xl text-success">download_done</span>
@@ -129,6 +139,9 @@ export default {
libraryItemId() {
return this._libraryItem.id
},
localLibraryItemId() {
return this.localLibraryItem?.id
},
series() {
return this.mediaMetadata.series
},
@@ -217,13 +230,31 @@ export default {
return this.isMissing || this.isInvalid
},
isStreaming() {
return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId
return this.isPlaying && !this.store.getters['getIsCurrentSessionLocal']
},
isPlaying() {
if (this.localLibraryItemId && this.store.getters['getIsMediaStreaming'](this.localLibraryItemId)) return true
return this.store.getters['getIsMediaStreaming'](this.libraryItemId)
},
playerIsPlaying() {
return this.store.state.playerIsPlaying && (this.isStreaming || this.isPlaying)
},
playerIsStartingPlayback() {
// Play has been pressed and waiting for native play response
return this.store.state.playerIsStartingPlayback
},
playerIsStartingForThisMedia() {
const mediaId = this.store.state.playerStartingPlaybackMediaId
return mediaId === this.libraryItemId
},
isCasting() {
return this.store.state.isCasting
},
showReadButton() {
return !this.isSelectionMode && !this.showPlayButton && this.hasEbook
},
showPlayButton() {
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.numTracks && !this.isStreaming
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.numTracks && !this.isPodcast
},
showSmallEBookIcon() {
return !this.isSelectionMode && this.hasEbook
@@ -293,9 +324,32 @@ export default {
this.selected = !this.selected
this.$emit('select', this.libraryItem)
},
play() {
var eventBus = this.$eventBus || this.$nuxt.$eventBus
eventBus.$emit('play-item', { libraryItemId: this.libraryItemId })
async play() {
if (this.playerIsStartingPlayback) return
const hapticsImpact = this.$hapticsImpact || this.$nuxt.$hapticsImpact
if (hapticsImpact) {
await hapticsImpact()
}
const eventBus = this.$eventBus || this.$nuxt.$eventBus
if (this.playerIsPlaying) {
eventBus.$emit('pause-item')
} else {
// Audiobook
let libraryItemId = this.libraryItemId
// When casting use server library item
if (this.localLibraryItem && !this.isCasting) {
libraryItemId = this.localLibraryItem.id
} else if (this.hasLocal) {
libraryItemId = this.localLibraryItem.id
}
this.store.commit('setPlayerIsStartingPlayback', this.libraryItemId)
eventBus.$emit('play-item', { libraryItemId, serverLibraryItemId: this.libraryItemId })
}
},
destroy() {
// destroy the vue listeners, etc
+129 -50
View File
@@ -17,6 +17,11 @@
<p class="text-xs text-warning">{{ $strings.MessageOldServerConnectionWarning }}</p>
<ui-btn class="text-xs whitespace-nowrap" :padding-x="2" :padding-y="1" @click="showOldUserIdWarningDialog">{{ $strings.LabelMoreInfo }}</ui-btn>
</div>
<!-- warning message if server connection config is using an old auth method -->
<div v-if="config.version && checkIsUsingOldAuth(config)" class="flex flex-nowrap justify-between items-center space-x-4 pt-4">
<p class="text-xs text-warning">{{ $strings.MessageOldServerAuthWarning }}</p>
<ui-btn class="text-xs whitespace-nowrap" :padding-x="2" :padding-y="1" @click="showOldAuthWarningDialog">{{ $strings.LabelMoreInfo }}</ui-btn>
</div>
</div>
<div class="my-1 py-4 w-full">
<ui-btn class="w-full" @click="newServerConfigClick">{{ $strings.ButtonAddNewServer }}</ui-btn>
@@ -25,7 +30,7 @@
<!-- form to add a new server connection config -->
<div v-else class="w-full">
<!-- server address input -->
<form v-if="!showAuth" @submit.prevent="submit" novalidate class="w-full">
<form v-if="!showAuth" @submit.prevent="submit(false)" novalidate class="w-full">
<div v-if="serverConnectionConfigs.length" class="flex items-center mb-4" @click="showServerList">
<span class="material-symbols text-fg-muted">arrow_back</span>
</div>
@@ -155,9 +160,23 @@ export default {
cancelText: this.$strings.ButtonOk
})
},
async showOldAuthWarningDialog() {
const confirmResult = await Dialog.confirm({
title: 'Old Server Auth Warning',
message: this.$strings.MessageOldServerAuthWarningHelp,
cancelButtonTitle: this.$strings.ButtonReadMore
})
if (!confirmResult.value) {
window.open('https://github.com/advplyr/audiobookshelf/discussions/4460', '_blank')
}
},
checkIdUuid(userId) {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(userId)
},
checkIsUsingOldAuth(config) {
if (!config.version) return true
return !this.$isValidVersion(config.version, '2.26.0')
},
/**
* Initiates the login process using OpenID via OAuth2.0.
* 1. Verifying the server's address
@@ -374,26 +393,36 @@ export default {
} catch (error) {} // No Error handling needed
try {
// Returns the same user response payload as /login
const response = await CapacitorHttp.get({
url: backendEndpoint
})
if (!response.data || !response.data.user || !response.data.user.token) {
throw new Error('Token data is missing in the response.')
// v2.26.0+ returns accessToken and refreshToken on user object
if (!response.data?.user?.token && !response.data?.user?.accessToken) {
throw new Error('Token is missing in response.')
}
this.serverConfig.token = response.data.user.token
const user = response.data.user
this.serverConfig.token = user.accessToken || user.token
// TODO: Is it necessary to authenticate again?
const payload = await this.authenticateToken()
if (!payload) {
throw new Error('Authentication failed with the provided token.')
}
const duplicateConfig = this.serverConnectionConfigs.find((scc) => scc.address === this.serverConfig.address && scc.username === payload.user.username)
const duplicateConfig = this.serverConnectionConfigs.find((scc) => scc.address === this.serverConfig.address && scc.username === payload.user.username && scc.id !== this.serverConfig.id)
if (duplicateConfig) {
throw new Error('Config already exists for this address and username.')
}
// For v2.26.0+ re-attach accessToken and refreshToken to user object because /authorize does not return them
if (user.accessToken) {
payload.user.accessToken = user.accessToken
payload.user.refreshToken = user.refreshToken
}
this.setUserAndConnection(payload)
} catch (error) {
console.error('[SSO] Error in exchangeCodeForToken: ', error)
@@ -436,12 +465,17 @@ export default {
}
this.error = null
var payload = await this.authenticateToken()
const payload = await this.authenticateToken()
if (payload) {
// Will NOT include access token and refresh token
this.setUserAndConnection(payload)
} else {
this.showAuth = true
let error = this.error
if (await this.submit(true)) {
this.showForm = true
this.error = error
}
}
},
async removeServerConfigClick() {
@@ -470,12 +504,14 @@ export default {
this.showForm = !this.serverConnectionConfigs.length
}
},
editServerConfig(serverConfig) {
async editServerConfig(serverConfig) {
this.serverConfig = {
...serverConfig
}
this.showForm = true
this.showAuth = true
if (await this.submit(true)) {
this.showForm = true
}
},
async newServerConfigClick() {
await this.$hapticsImpact()
@@ -504,7 +540,7 @@ export default {
try {
var urlObject = new URL(url)
if (protocolOverride) urlObject.protocol = protocolOverride
return urlObject.href
return urlObject.href.replace(/\/$/, '') // Remove trailing slash
} catch (error) {
console.error('Invalid URL', error)
return null
@@ -597,7 +633,12 @@ export default {
})
},
requestServerLogin() {
return this.postRequest(`${this.serverConfig.address}/login`, { username: this.serverConfig.username, password: this.password || '' }, this.serverConfig.customHeaders, 20000)
const headers = {
// Tells the Abs server to return the refresh token
'x-return-tokens': 'true',
...(this.serverConfig.customHeaders || {})
}
return this.postRequest(`${this.serverConfig.address}/login`, { username: this.serverConfig.username, password: this.password || '' }, headers, 20000)
.then((data) => {
if (!data.user) {
console.error(data.error)
@@ -616,9 +657,8 @@ export default {
return false
})
},
async submit() {
if (!this.networkConnected) return
if (!this.serverConfig.address) return
async submit(preventAutoLogin = false) {
if (!this.networkConnected || !this.serverConfig.address) return false
const initialAddress = this.serverConfig.address
// Did the user specify a protocol?
@@ -631,6 +671,7 @@ export default {
this.authMethods = []
try {
console.log('[ServerConnectForm] submit tryServerUrl: ' + this.serverConfig.address)
// Try the server URL. If it fails and the protocol was not provided, try with http instead of https
const statusData = await this.tryServerUrl(this.serverConfig.address, !protocolProvided)
if (this.validateLoginFormResponse(statusData, this.serverConfig.address, protocolProvided)) {
@@ -639,12 +680,17 @@ export default {
this.oauth.buttonText = statusData.data.authFormData?.authOpenIDButtonText || 'Login with OpenID'
this.serverConfig.version = statusData.data.serverVersion
if (statusData.data.authFormData?.authOpenIDAutoLaunch) {
if (statusData.data.authFormData?.authOpenIDAutoLaunch && !preventAutoLogin) {
this.clickLoginWithOpenId()
}
return true
} else {
console.log('[ServerConnectForm] submit validateLoginFormResponse failed: ' + this.serverConfig.address)
return false
}
} catch (error) {
this.handleLoginFormError(error)
return false
} finally {
this.processing = false
}
@@ -770,26 +816,6 @@ export default {
prependProtocolIfNeeded(address) {
return address.startsWith('http://') || address.startsWith('https://') ? address : `https://${address}`
},
/**
* Compares two semantic versioning strings to determine if the current version meets
* or exceeds the minimum version requirement.
*
* @param {string} currentVersion - The current version string to compare, e.g., "1.2.3".
* @param {string} minVersion - The minimum version string required, e.g., "1.0.0".
* @returns {boolean} - Returns true if the current version is greater than or equal
* to the minimum version, false otherwise.
*/
isValidVersion(currentVersion, minVersion) {
const currentParts = currentVersion.split('.').map(Number)
const minParts = minVersion.split('.').map(Number)
for (let i = 0; i < minParts.length; i++) {
if (currentParts[i] > minParts[i]) return true
if (currentParts[i] < minParts[i]) return false
}
return true
},
async submitAuth() {
if (!this.networkConnected) return
if (!this.serverConfig.username) {
@@ -806,9 +832,10 @@ export default {
this.error = null
this.processing = true
var payload = await this.requestServerLogin()
const payload = await this.requestServerLogin()
this.processing = false
if (payload) {
// Will include access token and refresh token
this.setUserAndConnection(payload)
}
},
@@ -821,18 +848,30 @@ export default {
this.$store.commit('libraries/setEReaderDevices', ereaderDevices)
this.$setServerLanguageCode(serverSettings.language)
// Set library - Use last library if set and available fallback to default user library
var lastLibraryId = await this.$localStore.getLastLibraryId()
if (lastLibraryId && (!user.librariesAccessible.length || user.librariesAccessible.includes(lastLibraryId))) {
this.$store.commit('libraries/setCurrentLibrary', lastLibraryId)
} else if (userDefaultLibraryId) {
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
this.serverConfig.userId = user.id
this.serverConfig.username = user.username
if (this.$isValidVersion(serverSettings.version, '2.26.0')) {
// Tokens only returned from /login endpoint
if (user.accessToken) {
this.serverConfig.token = user.accessToken
this.serverConfig.refreshToken = user.refreshToken
} else {
// Detect if the connection config is using the old token. If so, force re-login
if (this.serverConfig.token === user.token || user.isOldToken) {
this.setForceRelogin('oldAuthToken')
return
}
// If the token was updated during a refresh (in nativeHttp.js) it gets updated in the store, so refetch
this.serverConfig.token = this.$store.getters['user/getToken'] || this.serverConfig.token
}
} else {
// Server version before new JWT auth, use old user.token
this.serverConfig.token = user.token
}
this.serverConfig.userId = user.id
this.serverConfig.token = user.token
this.serverConfig.username = user.username
delete this.serverConfig.version
this.serverConfig.version = serverSettings.version
var serverConnectionConfig = await this.$db.setServerConnectionConfig(this.serverConfig)
@@ -849,7 +888,16 @@ export default {
}
}
// Set library - Use last library if set and available fallback to default user library
const lastLibraryId = await this.$localStore.getLastLibraryId()
if (lastLibraryId && (!user.librariesAccessible.length || user.librariesAccessible.includes(lastLibraryId))) {
this.$store.commit('libraries/setCurrentLibrary', lastLibraryId)
} else if (userDefaultLibraryId) {
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
}
this.$store.commit('user/setUser', user)
this.$store.commit('user/setAccessToken', serverConnectionConfig.token)
this.$store.commit('user/setServerConnectionConfig', serverConnectionConfig)
this.$socket.connect(this.serverConfig.address, this.serverConfig.token)
@@ -865,7 +913,13 @@ export default {
this.error = null
this.processing = true
const authRes = await this.postRequest(`${this.serverConfig.address}/api/authorize`, null, { Authorization: `Bearer ${this.serverConfig.token}` }).catch((error) => {
const nativeHttpOptions = {
headers: {
Authorization: `Bearer ${this.serverConfig.token}`
},
serverConnectionConfig: this.serverConfig
}
const authRes = await this.$nativeHttp.post(`${this.serverConfig.address}/api/authorize`, null, nativeHttpOptions).catch((error) => {
console.error('[ServerConnectForm] Server auth failed', error)
const errorMsg = error.message || error
this.error = 'Failed to authorize'
@@ -874,14 +928,39 @@ export default {
}
return false
})
console.log('[ServerConnectForm] authRes=', authRes)
this.processing = false
return authRes
},
async setForceRelogin(error) {
console.log('[ServerConnectForm] setForceRelogin', error, this.serverConfig.address)
// This calls /status on the server and sets the auth methods
const result = await this.submit(true)
if (result) {
this.showForm = true
if (error === 'oldAuthToken') {
this.error = this.$strings.MessageOldServerAuthReLoginRequired
} else if (error === 'refreshTokenFailed') {
this.error = this.$strings.MessageFailedToRefreshToken
}
}
},
init() {
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
this.serverConfig = this.serverConnectionConfigs.find((scc) => scc.id === this.$route.query.serverConnectionConfigId)
if (this.serverConfig) {
this.setForceRelogin(this.$route.query.error)
return
} else {
console.error('[ServerConnectForm] init with serverConnectionConfigId but no serverConfig found', this.$route.query.serverConnectionConfigId)
}
}
if (this.lastServerConnectionConfig) {
console.log('[ServerConnectForm] init with lastServerConnectionConfig', this.lastServerConnectionConfig)
this.connectToServer(this.lastServerConnectionConfig)
} else {
this.showForm = !this.serverConnectionConfigs.length
+6 -12
View File
@@ -17,7 +17,7 @@
<p class="text-xl font-mono">{{ this.$secondsToTimestamp(currentTime / _playbackRate) }}</p>
</div>
<ui-text-input-with-label v-model="newBookmarkTitle" ref="noteInput" label="Note" />
<ui-text-input-with-label v-model="newBookmarkTitle" :placeholder="bookmarkPlaceholder()" :autofocus="false" ref="noteInput" label="Note" />
<div class="flex justify-end mt-6">
<ui-btn color="success" class="w-full" @click.stop="submitBookmark">{{ selectedBookmark ? 'Update' : 'Create' }}</ui-btn>
</div>
@@ -93,6 +93,10 @@ export default {
}
},
methods: {
bookmarkPlaceholder() {
// using a method prevents caching the date
return this.$formatDate(Date.now(), 'MMM dd, yyyy HH:mm')
},
editBookmark(bm) {
this.selectedBookmark = bm
this.newBookmarkTitle = bm.title
@@ -157,18 +161,8 @@ export default {
},
createBookmark() {
this.selectedBookmark = null
this.newBookmarkTitle = this.$formatDate(Date.now(), 'MMM dd, yyyy HH:mm')
this.newBookmarkTitle = ''
this.showBookmarkTitleInput = true
// Auto focus the input and select the text
this.$nextTick(() => {
if (this.$refs.noteInput?.$refs.input?.$refs.input) {
this.$refs.noteInput.$refs.input.$refs.input.focus()
setTimeout(() => {
this.$refs.noteInput?.$refs.input?.$refs.input?.select()
}, 10)
}
})
},
async submitBookmark() {
await this.$hapticsImpact()
+66 -29
View File
@@ -54,8 +54,39 @@ export default {
},
data() {
return {
sublist: null,
bookItems: [
sublist: null
}
},
watch: {
show(newVal) {
if (!newVal) {
if (this.sublist && !this.selectedItemSublist) this.sublist = null
if (!this.sublist && this.selectedItemSublist) this.sublist = this.selectedItemSublist
}
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
selected: {
get() {
return this.filterBy
},
set(val) {
this.$emit('update:filterBy', val)
}
},
userCanAccessExplicitContent() {
return this.$store.getters['user/getUserCanAccessExplicitContent']
},
bookItems() {
const items = [
{
text: this.$strings.LabelAll,
value: 'all'
@@ -104,9 +135,26 @@ export default {
text: this.$strings.ButtonIssues,
value: 'issues',
sublist: false
},
{
text: this.$strings.LabelRSSFeedOpen,
value: 'feed-open',
sublist: false
}
],
podcastItems: [
]
if (this.userCanAccessExplicitContent) {
items.push({
text: this.$strings.LabelExplicit,
value: 'explicit',
sublist: false
})
}
return items
},
podcastItems() {
const items = [
{
text: this.$strings.LabelAll,
value: 'all'
@@ -120,34 +168,23 @@ export default {
text: this.$strings.LabelTag,
value: 'tags',
sublist: true
},
{
text: this.$strings.LabelRSSFeedOpen,
value: 'feed-open',
sublist: false
}
]
}
},
watch: {
show(newVal) {
if (!newVal) {
if (this.sublist && !this.selectedItemSublist) this.sublist = null
if (!this.sublist && this.selectedItemSublist) this.sublist = this.selectedItemSublist
}
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
selected: {
get() {
return this.filterBy
},
set(val) {
this.$emit('update:filterBy', val)
if (this.userCanAccessExplicitContent) {
items.push({
text: this.$strings.LabelExplicit,
value: 'explicit',
sublist: false
})
}
return items
},
isPodcast() {
return this.$store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast'
+6 -2
View File
@@ -12,9 +12,9 @@
<div class="w-full relative">
<h1 class="text-lg mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</h1>
<ui-text-input v-model="currentFeed.feedUrl" class="text-sm" readonly />
<ui-text-input v-model="fullFeedUrl" class="text-sm" readonly />
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base" :class="linkCopied ? 'text-success' : 'text-fg-muted'" @click="copyToClipboard(currentFeed.feedUrl)">{{ linkCopied ? 'check' : 'content_copy' }}</span>
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base" :class="linkCopied ? 'text-success' : 'text-fg-muted'" @click="copyToClipboard(fullFeedUrl)">{{ linkCopied ? 'check' : 'content_copy' }}</span>
</div>
<div v-if="currentFeed.meta" class="mt-5">
@@ -119,6 +119,10 @@ export default {
demoFeedUrl() {
return `${this.serverAddress}/feed/${this.newFeedSlug}`
},
fullFeedUrl() {
if (!this.currentFeed) return ''
return `${this.serverAddress}${this.currentFeed.feedUrl}`
},
isHttp() {
return !!this.serverAddress?.startsWith('http://')
}
+2 -7
View File
@@ -77,9 +77,6 @@ export default {
}
},
computed: {
userToken() {
return this.$store.getters['user/getToken']
},
libraryItemId() {
return this.libraryItem?.id
},
@@ -245,11 +242,9 @@ export default {
async extract() {
this.loading = true
// TODO: Handle JWT auth refresh
const buff = await this.$axios.$get(this.url, {
responseType: 'blob',
headers: {
Authorization: `Bearer ${this.userToken}`
}
responseType: 'blob'
})
const archive = await Archive.open(buff)
+14 -6
View File
@@ -49,9 +49,6 @@ export default {
}
},
computed: {
userToken() {
return this.$store.getters['user/getToken']
},
/** @returns {string} */
libraryItemId() {
return this.libraryItem?.id
@@ -298,15 +295,26 @@ export default {
/** @type {EpubReader} */
const reader = this
// Use axios to make request because we have token refresh logic in interceptor
const customRequest = async (url) => {
try {
return this.$axios.$get(url, {
responseType: 'arraybuffer'
})
} catch (error) {
console.error('EpubReader.initEpub customRequest failed:', error)
throw error
}
}
console.log('[EpubReader] initEpub', reader.url)
/** @type {ePub.Book} */
reader.book = new ePub(reader.url, {
width: window.innerWidth,
height: window.innerHeight - this.readerHeightOffset,
openAs: 'epub',
requestHeaders: {
Authorization: `Bearer ${this.userToken}`
}
requestMethod: this.isLocal ? null : customRequest
})
/** @type {ePub.Rendition} */
+4 -10
View File
@@ -22,11 +22,7 @@ export default {
data() {
return {}
},
computed: {
userToken() {
return this.$store.getters['user/getToken']
}
},
computed: {},
methods: {
addHtmlCss() {
let iframe = document.getElementsByTagName('iframe')[0]
@@ -84,11 +80,9 @@ export default {
},
async initMobi() {
// Fetch mobi file as blob
// TODO: Handle JWT auth refresh
var buff = await this.$axios.$get(this.url, {
responseType: 'blob',
headers: {
Authorization: `Bearer ${this.userToken}`
}
responseType: 'blob'
})
var reader = new FileReader()
reader.onload = async (event) => {
@@ -133,4 +127,4 @@ export default {
overflow-x: hidden;
overflow-y: auto;
}
</style>
</style>
+57 -3
View File
@@ -49,7 +49,8 @@ export default {
numPages: 0,
windowWidth: 0,
windowHeight: 0,
pdfDocInitParams: null
pdfDocInitParams: null,
isRefreshing: false
}
},
computed: {
@@ -109,6 +110,10 @@ export default {
},
isPlayerOpen() {
return this.$store.getters['getIsPlayerOpen']
},
ebookUrl() {
const serverAddress = this.$store.getters['user/getServerAddress']
return this.isLocal ? this.url : `${serverAddress}${this.url}`
}
},
methods: {
@@ -164,7 +169,56 @@ export default {
this.page++
this.updateProgress()
},
error(err) {
async handleRefreshFailure() {
try {
console.log('[PdfReader] Handling refresh failure - logging out user')
const serverConnectionConfigId = this.$store.getters['user/getServerConnectionConfigId']
// Clear store
await this.$store.dispatch('user/logout')
if (serverConnectionConfigId) {
// Clear refresh token for server connection config
await this.$db.clearRefreshToken(serverConnectionConfigId)
}
if (window.location.pathname !== '/connect') {
window.location.href = '/connect?error=refreshTokenFailed&serverConnectionConfigId=' + serverConnectionConfigId
}
} catch (error) {
console.error('[PdfReader] Failed to handle refresh failure:', error)
}
},
async refreshToken() {
if (this.isRefreshing) return
this.isRefreshing = true
// Cannot use axios with this pdf reader so we need to handle the refresh separately
// Should work on migrating to a different pdf reader in the future
const newAccessToken = await this.$store.dispatch('user/refreshToken').catch((error) => {
console.error('Failed to refresh token', error)
return null
})
if (!newAccessToken) {
this.handleRefreshFailure()
return
}
// Force Vue to re-render the PDF component by creating a new object
this.pdfDocInitParams = {
url: this.ebookUrl,
httpHeaders: {
Authorization: `Bearer ${newAccessToken}`
}
}
this.isRefreshing = false
},
async error(err) {
if (err && err.status === 401) {
console.log('Received 401 error, refreshing token')
await this.refreshToken()
return
}
console.error(err)
},
screenOrientationChange() {
@@ -173,7 +227,7 @@ export default {
},
init() {
this.pdfDocInitParams = {
url: this.url,
url: this.ebookUrl,
httpHeaders: {
Authorization: `Bearer ${this.userToken}`
}
+5 -4
View File
@@ -14,7 +14,7 @@
<span class="material-symbols text-2xl text-fg">settings</span>
</button>
<button v-if="comicHasMetadata" type="button" class="inline-flex mx-2" @click.stop="clickMetadataBtn">
<span class="material-symbols text-2xl text-fg">more-vert</span>
<span class="material-symbols text-2xl text-fg">more</span>
</button>
</div>
@@ -260,6 +260,7 @@ export default {
return null
},
ebookFile() {
if (!this.media) return null
// ebook file id is passed when reading a supplementary ebook
if (this.ebookFileId) {
return this.selectedLibraryItem.libraryFiles.find((lf) => lf.ino === this.ebookFileId)
@@ -304,11 +305,11 @@ export default {
if (this.localContentUrl) {
return Capacitor.convertFileSrc(this.localContentUrl)
}
const serverAddress = this.$store.getters['user/getServerAddress']
if (this.ebookFileId) {
return `${serverAddress}/api/items/${this.selectedLibraryItem.id}/ebook/${this.ebookFileId}`
return `/api/items/${this.selectedLibraryItem.id}/ebook/${this.ebookFileId}`
}
return `${serverAddress}/api/items/${this.selectedLibraryItem.id}/ebook`
return `/api/items/${this.selectedLibraryItem.id}/ebook`
},
isPlayerOpen() {
return this.$store.getters['getIsPlayerOpen']
+5 -1
View File
@@ -1,6 +1,6 @@
<template>
<div class="relative">
<input v-model="input" ref="input" autofocus :type="type" :disabled="disabled" :readonly="readonly" autocorrect="off" autocapitalize="none" autocomplete="off" :placeholder="placeholder" class="py-2 w-full outline-none bg-primary disabled:text-fg-muted" :class="inputClass" @keyup="keyup" />
<input v-model="input" ref="input" :autofocus="autofocus" :type="type" :disabled="disabled" :readonly="readonly" autocorrect="off" autocapitalize="none" autocomplete="off" :placeholder="placeholder" class="py-2 w-full outline-none bg-primary disabled:text-fg-muted" :class="inputClass" @keyup="keyup" />
<div v-if="prependIcon" class="absolute top-0 left-0 h-full px-2 flex items-center justify-center">
<span class="material-symbols text-lg">{{ prependIcon }}</span>
</div>
@@ -22,6 +22,10 @@ export default {
disabled: Boolean,
readonly: Boolean,
borderless: Boolean,
autofocus: {
type: Boolean,
default: true
},
bg: {
type: String,
default: 'bg'
+7 -2
View File
@@ -1,7 +1,7 @@
<template>
<div class="w-full">
<p class="pb-0.5 text-sm font-semibold">{{ label }}</p>
<ui-text-input v-model="inputValue" ref="input" :disabled="disabled" :type="type" text-size="base" class="w-full" />
<ui-text-input v-model="inputValue" ref="input" :disabled="disabled" :type="type" :placeholder="placeholder" :autofocus="autofocus" text-size="base" class="w-full" />
</div>
</template>
@@ -14,7 +14,12 @@ export default {
type: String,
default: 'text'
},
disabled: Boolean
disabled: Boolean,
placeholder: String,
autofocus: {
type: Boolean,
default: true
}
},
data() {
return {}
+68
View File
@@ -0,0 +1,68 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="12px" height="12px" viewBox="0 0 512 512" class="ml-1">
<path
fill="white"
d="M 89.00,40.12
C 89.00,40.12 127.00,40.12 127.00,40.12
127.00,40.12 198.00,40.12 198.00,40.12
198.00,40.12 416.00,40.12 416.00,40.12
446.58,40.05 472.95,66.42 473.00,97.00
473.00,97.00 473.00,303.00 473.00,303.00
473.00,303.00 473.00,418.00 473.00,418.00
472.65,447.55 445.06,472.95 416.00,473.00
416.00,473.00 210.00,473.00 210.00,473.00
210.00,473.00 95.00,473.00 95.00,473.00
65.45,472.65 40.05,445.06 40.00,416.00
40.00,416.00 40.00,136.00 40.00,136.00
40.00,136.00 40.00,109.00 40.00,109.00
40.00,109.00 40.00,96.00 40.00,96.00
40.07,81.58 46.89,67.14 57.01,57.01
61.17,52.86 64.86,50.13 70.00,47.31
77.25,43.33 81.02,42.18 89.00,40.12 Z
M 372.00,392.00
C 372.00,392.00 364.02,364.00 364.02,364.00
364.02,364.00 350.72,319.00 350.72,319.00
350.72,319.00 310.42,183.00 310.42,183.00
310.42,183.00 296.86,137.00 296.86,137.00
296.86,137.00 291.30,121.99 291.30,121.99
291.30,121.99 284.00,121.00 284.00,121.00
284.00,121.00 230.00,121.00 230.00,121.00
230.00,121.00 222.51,122.02 222.51,122.02
222.51,122.02 216.86,137.00 216.86,137.00
216.86,137.00 203.28,183.00 203.28,183.00
203.28,183.00 163.28,318.00 163.28,318.00
163.28,318.00 148.71,367.00 148.71,367.00
148.71,367.00 142.00,392.00 142.00,392.00
142.00,392.00 183.00,392.00 183.00,392.00
183.00,392.00 190.86,390.43 190.86,390.43
190.86,390.43 195.86,375.00 195.86,375.00
195.86,375.00 206.00,338.00 206.00,338.00
206.00,338.00 293.00,338.00 293.00,338.00
295.64,338.01 299.26,337.65 301.30,339.60
303.23,341.43 304.80,348.22 305.58,351.00
305.58,351.00 313.00,378.00 313.00,378.00
316.91,391.63 315.20,391.98 325.00,392.00
325.00,392.00 372.00,392.00 372.00,392.00 Z
M 254.00,170.00
C 254.00,170.00 256.00,170.00 256.00,170.00
256.00,170.00 263.12,197.00 263.12,197.00
263.12,197.00 282.88,268.00 282.88,268.00
282.88,268.00 290.00,296.00 290.00,296.00
290.00,296.00 219.00,296.00 219.00,296.00
219.00,296.00 230.58,253.00 230.58,253.00
230.58,253.00 254.00,170.00 254.00,170.00 Z"
/>
</svg>
</template>
<script>
export default {
props: {},
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
@@ -78,15 +78,15 @@ export default {
this.$store.commit('globals/updateDownloadItemPart', itemPart)
}
},
mounted() {
this.downloadItemListener = AbsDownloader.addListener('onDownloadItem', (data) => this.onDownloadItem(data))
this.itemPartUpdateListener = AbsDownloader.addListener('onDownloadItemPartUpdate', (data) => this.onDownloadItemPartUpdate(data))
this.completeListener = AbsDownloader.addListener('onItemDownloadComplete', (data) => this.onItemDownloadComplete(data))
async mounted() {
this.downloadItemListener = await AbsDownloader.addListener('onDownloadItem', (data) => this.onDownloadItem(data))
this.itemPartUpdateListener = await AbsDownloader.addListener('onDownloadItemPartUpdate', (data) => this.onDownloadItemPartUpdate(data))
this.completeListener = await AbsDownloader.addListener('onItemDownloadComplete', (data) => this.onItemDownloadComplete(data))
},
beforeDestroy() {
if (this.downloadItemListener) this.downloadItemListener.remove()
if (this.completeListener) this.completeListener.remove()
if (this.itemPartUpdateListener) this.itemPartUpdateListener.remove()
this.downloadItemListener?.remove()
this.completeListener?.remove()
this.itemPartUpdateListener?.remove()
}
}
</script>
+8 -4
View File
@@ -25,6 +25,7 @@
4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B957282EEA14008272D4 /* AbsFileSystem.swift */; };
4D91EEC62A40F28D004807ED /* EBookFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D91EEC52A40F28D004807ED /* EBookFile.swift */; };
4DABC04F2B0139CA000F6264 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DABC04E2B0139CA000F6264 /* User.swift */; };
4DB441E12E19B8BF0056C8F1 /* SecureStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DB441E02E19B8BF0056C8F1 /* SecureStorage.swift */; };
4DF6C7172DB58ABF004059F1 /* AbsLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DF6C7162DB58ABF004059F1 /* AbsLogger.swift */; };
4DF74912287105C600AC7814 /* DeviceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DF74911287105C600AC7814 /* DeviceSettings.swift */; };
4DFE2DA32D345C390000B204 /* MyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DFE2DA22D345C390000B204 /* MyViewController.swift */; };
@@ -98,6 +99,7 @@
4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = "<group>"; };
4D91EEC52A40F28D004807ED /* EBookFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EBookFile.swift; sourceTree = "<group>"; };
4DABC04E2B0139CA000F6264 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
4DB441E02E19B8BF0056C8F1 /* SecureStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureStorage.swift; sourceTree = "<group>"; };
4DF6C7162DB58ABF004059F1 /* AbsLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsLogger.swift; sourceTree = "<group>"; };
4DF74911287105C600AC7814 /* DeviceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceSettings.swift; sourceTree = "<group>"; };
4DFE2DA22D345C390000B204 /* MyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyViewController.swift; sourceTree = "<group>"; };
@@ -239,6 +241,7 @@
3AFCB5E627EA23F700ECCC05 /* util */ = {
isa = PBXGroup;
children = (
4DB441E02E19B8BF0056C8F1 /* SecureStorage.swift */,
E9FA07E228C82848005520B0 /* Logger.swift */,
3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */,
3AD4FCEA280443DD006DB301 /* Database.swift */,
@@ -550,6 +553,7 @@
E9D5505828AC1C1A00C746DD /* Library.swift in Sources */,
3AD4FCEB280443DD006DB301 /* Database.swift in Sources */,
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */,
4DB441E12E19B8BF0056C8F1 /* SecureStorage.swift in Sources */,
4DABC04F2B0139CA000F6264 /* User.swift in Sources */,
E9D5506828AC1DC300C746DD /* LocalPodcastEpisode.swift in Sources */,
EACB38162BCCA1500060DA4A /* DefaultedAudioPlayerRateManager.swift in Sources */,
@@ -732,12 +736,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 38;
CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.81;
MARKETING_VERSION = 0.10.1;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -756,12 +760,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 38;
CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.81;
MARKETING_VERSION = 0.10.1;
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
+9 -4
View File
@@ -14,7 +14,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Override point for customization after application launch.
let configuration = Realm.Configuration(
schemaVersion: 19,
schemaVersion: 20,
migrationBlock: { [weak self] migration, oldSchemaVersion in
if (oldSchemaVersion < 1) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)")
@@ -65,9 +65,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding disableSleepTimerFadeOut settings")
migration.enumerateObjects(ofType: PlayerSettings.className()) { oldObject, newObject in
newObject?["disableSleepTimerFadeOut"] = false
}
}
}
}
if (oldSchemaVersion < 20) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding version to ServerConnectionConfigs")
migration.enumerateObjects(ofType: ServerConnectionConfig.className()) { oldObject, newObject in
newObject?["version"] = ""
}
}
}
)
Realm.Configuration.defaultConfiguration = configuration
+46 -2
View File
@@ -33,6 +33,8 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "setCurrentServerConnectionConfig", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "removeServerConnectionConfig", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getRefreshToken", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "clearRefreshToken", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "logout", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getDeviceData", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getLocalLibraryItems", returnType: CAPPluginReturnPromise),
@@ -49,25 +51,44 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
]
private let logger = AppLogger(category: "AbsDatabase")
private let secureStorage = SecureStorage()
// Used to notify the webview frontend that the token has been refreshed
static var tokenRefreshCallback: ((String, [String: Any]) -> Void)?
override public func load() {
AbsDatabase.tokenRefreshCallback = { [weak self] eventName, data in
self?.notifyListeners(eventName, data: data)
}
}
@objc func setCurrentServerConnectionConfig(_ call: CAPPluginCall) {
var id = call.getString("id")
let address = call.getString("address", "")
let version = call.getString("version", "")
let userId = call.getString("userId", "")
let username = call.getString("username", "")
let token = call.getString("token", "")
let refreshToken = call.getString("refreshToken", "") // Refresh only sent after login or refresh
let name = "\(address) (\(username))"
if id == nil {
id = "\(address)@\(username)".toBase64()
}
if (refreshToken != "") {
// Store refresh token securely if provided
let hasRefreshToken = secureStorage.storeRefreshToken(serverConnectionConfigId: id ?? "", refreshToken: refreshToken)
logger.log("Refresh token secured = \(hasRefreshToken)")
}
let config = ServerConnectionConfig()
config.id = id ?? ""
config.index = 0
config.name = name
config.address = address
config.version = version
config.userId = userId
config.username = username
config.token = token
@@ -76,12 +97,35 @@ public class AbsDatabase: CAPPlugin, CAPBridgedPlugin {
let savedConfig = Store.serverConfig // Fetch the latest value
call.resolve(convertServerConnectionConfigToJSON(config: savedConfig!))
}
@objc func removeServerConnectionConfig(_ call: CAPPluginCall) {
let id = call.getString("serverConnectionConfigId", "")
// Remove refresh token if it exists
_ = secureStorage.removeRefreshToken(serverConnectionConfigId: id)
Database.shared.deleteServerConnectionConfig(id: id)
call.resolve()
}
@objc func getRefreshToken(_ call: CAPPluginCall) {
let serverConnectionConfigId = call.getString("serverConnectionConfigId", "")
let refreshToken = secureStorage.getRefreshToken(serverConnectionConfigId: serverConnectionConfigId)
if let refreshToken = refreshToken {
call.resolve(["refreshToken": refreshToken])
} else {
call.resolve()
}
}
@objc func clearRefreshToken(_ call: CAPPluginCall) {
let serverConnectionConfigId = call.getString("serverConnectionConfigId", "")
let success = secureStorage.removeRefreshToken(serverConnectionConfigId: serverConnectionConfigId)
call.resolve(["success": success])
}
@objc func logout(_ call: CAPPluginCall) {
Store.serverConfig = nil
call.resolve()
@@ -13,6 +13,7 @@ class ServerConnectionConfig: Object {
@Persisted(indexed: true) var index: Int = 1
@Persisted var name: String = ""
@Persisted var address: String = ""
@Persisted var version: String = ""
@Persisted var userId: String = ""
@Persisted var username: String = ""
@Persisted var token: String = ""
@@ -29,6 +30,7 @@ func convertServerConnectionConfigToJSON(config: ServerConnectionConfig) -> Dict
"name": config.name,
"index": config.index,
"address": config.address,
"version": config.version,
"userId": config.userId,
"username": config.username,
"token": config.token,
+12 -3
View File
@@ -561,8 +561,15 @@ class AudioPlayer: NSObject {
guard let playbackSession = self.getPlaybackSession() else { return nil }
if (playbackSession.playMethod == PlayMethod.directplay.rawValue) {
let urlstr = "\(Store.serverConfig!.address)/api/items/\(itemId)/file/\(ino)?token=\(Store.serverConfig!.token)"
let url = URL(string: urlstr)!
// As of v2.22.0 tracks use a different endpoint
// See: https://github.com/advplyr/audiobookshelf/pull/4263
let contentUrl: String
if Store.isServerVersionGreaterThanOrEqualTo("2.22.0") {
contentUrl = "\(Store.serverConfig!.address)/public/session/\(playbackSession.id)/track/\(track.index ?? 1)"
} else {
contentUrl = "\(Store.serverConfig!.address)/api/items/\(itemId)/file/\(ino)?token=\(Store.serverConfig!.token)"
}
let url = URL(string: contentUrl)!
return AVURLAsset(url: url)
} else if (playbackSession.playMethod == PlayMethod.local.rawValue) {
guard let localFile = track.getLocalFile() else {
@@ -577,7 +584,9 @@ class AudioPlayer: NSObject {
let headers: [String: String] = [
"Authorization": "Bearer \(Store.serverConfig!.token)"
]
return AVURLAsset(url: URL(string: "\(Store.serverConfig!.address)\(track.contentUrl ?? "")")!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
let contentUrl = "\(Store.serverConfig!.address)\(track.contentUrl ?? "")"
return AVURLAsset(url: URL(string: contentUrl)!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
}
}
+391 -22
View File
@@ -10,6 +10,7 @@ import Alamofire
class ApiClient {
private static let logger = AppLogger(category: "ApiClient")
private static let secureStorage = SecureStorage()
public static func getData(from url: URL, completion: @escaping (UIImage?) -> Void) {
URLSession.shared.dataTask(with: url, completionHandler: {(data, response, error) in
@@ -146,6 +147,318 @@ class ApiClient {
}
}
// MARK: - Token Refresh Handling
/**
* Handles token refresh when a 401 Unauthorized response is received
* This function will:
* 1. Get the refresh token from secure storage for the current server connection
* 2. Make a request to /auth/refresh endpoint with the refresh token
* 3. Update the connection config with the new accessToken and put the refreshToken in secure storage
* 4. Retry the original request with the new access token
* 5. If refresh fails, handle logout
*/
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 {
logger.error("handleTokenRefresh: No server config available")
callback?(nil)
return
}
logger.log("handleTokenRefresh: Attempting to refresh auth tokens for server \(serverConfig.name)")
// Get refresh token from secure storage
guard let refreshToken = secureStorage.getRefreshToken(serverConnectionConfigId: serverConfig.id) else {
logger.error("handleTokenRefresh: No refresh token available for server \(serverConfig.name)")
handleRefreshFailure()
callback?(nil)
return
}
logger.log("handleTokenRefresh: Retrieved refresh token, attempting to refresh access token")
// Create refresh token request
let refreshHeaders: HTTPHeaders = [
"x-refresh-token": refreshToken,
"Content-Type": "application/json"
]
let refreshRequest = AF.request("\(serverConfig.address)/auth/refresh", method: .post, headers: refreshHeaders)
refreshRequest.responseDecodable(of: RefreshResponse.self) { response in
switch response.result {
case .success(let refreshResponse):
guard let user = refreshResponse.user,
!user.accessToken.isEmpty else {
logger.error("handleTokenRefresh: No access token in refresh response for server \(serverConfig.name)")
handleRefreshFailure()
callback?(nil)
return
}
logger.log("handleTokenRefresh: Successfully obtained new access token")
// Update tokens in secure storage and store
updateTokens(newAccessToken: user.accessToken, newRefreshToken: user.refreshToken ?? refreshToken, serverConnectionConfigId: serverConfig.id)
// Retry the original request with the new access token
logger.log("handleTokenRefresh: Retrying original request with new token")
retryOriginalRequest(endpoint: endpoint, method: method, parameters: parameters, decodable: decodable, newAccessToken: user.accessToken, callback: callback)
case .failure(let error):
logger.error("handleTokenRefresh: Refresh request failed for server \(serverConfig.name): \(error)")
handleRefreshFailure()
callback?(nil)
}
}
}
/**
* Updates the stored tokens with new access and refresh tokens
*/
private static func updateTokens(newAccessToken: String, newRefreshToken: String, serverConnectionConfigId: String) {
// Update the refresh token in secure storage if it's new
if newRefreshToken != secureStorage.getRefreshToken(serverConnectionConfigId: serverConnectionConfigId) {
let hasStored = secureStorage.storeRefreshToken(serverConnectionConfigId: serverConnectionConfigId, refreshToken: newRefreshToken)
logger.log("updateTokens: Updated refresh token in secure storage. Stored=\(hasStored)")
}
// Update access token on server connection config
Database.shared.updateServerConnectionConfigToken(newToken: newAccessToken)
logger.log("updateTokens: Updated access token in server connection config")
logger.log("updateTokens: Successfully refreshed auth tokens for server \(Store.serverConfig?.name ?? "unknown")")
// Notify webview frontend about token refresh
if let callback = AbsDatabase.tokenRefreshCallback {
let tokenData: [String: Any] = ["accessToken": newAccessToken]
callback("onTokenRefresh", tokenData)
}
}
/**
* Retries the original request with the new access token
*/
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 {
logger.error("retryOriginalRequest: No server config available")
callback?(nil)
return
}
let headers: HTTPHeaders = [
"Authorization": "Bearer \(newAccessToken)"
]
let retryRequest: DataRequest
switch method {
case .get:
retryRequest = AF.request("\(serverConfig.address)/\(endpoint)", method: .get, headers: headers)
case .post:
if let parameters = parameters as? [String: Any] {
retryRequest = AF.request("\(serverConfig.address)/\(endpoint)", method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: headers)
} else if let encodableParams = parameters as? Encodable {
retryRequest = AF.request("\(serverConfig.address)/\(endpoint)", method: .post, parameters: encodableParams, encoder: JSONParameterEncoder.default, headers: headers)
} else {
retryRequest = AF.request("\(serverConfig.address)/\(endpoint)", method: .post, headers: headers)
}
case .patch:
if let encodableParams = parameters as? Encodable {
retryRequest = AF.request("\(serverConfig.address)/\(endpoint)", method: .patch, parameters: encodableParams, encoder: JSONParameterEncoder.default, headers: headers)
} else {
retryRequest = AF.request("\(serverConfig.address)/\(endpoint)", method: .patch, headers: headers)
}
default:
logger.error("retryOriginalRequest: Unsupported method \(method)")
callback?(nil)
return
}
// Handle the response
retryRequest.response { response in
if let statusCode = response.response?.statusCode, (200...299).contains(statusCode) {
// Check if response has data
if let data = response.data, !data.isEmpty {
// If it is a string return nil (e.g. express returns OK for 200 status codes)
if let responseString = String(data: data, encoding: .utf8) {
logger.log("retryOriginalRequest: Got string response '\(responseString)'")
callback?(nil)
return
}
// If not a string, try JSON
do {
let decodedObject = try JSONDecoder().decode(decodable, from: data)
callback?(decodedObject)
} catch {
logger.error("retryOriginalRequest: JSON decode failed: \(error)")
callback?(nil)
}
} else {
// Empty response
logger.log("retryOriginalRequest: Empty response with success status \(statusCode)")
callback?(nil)
}
} else {
logger.error("retryOriginalRequest: Request failed with status \(response.response?.statusCode ?? 0)")
callback?(nil)
}
}
}
/**
* Handles the case when token refresh fails
* This will clear the current server connection and notify webview
*/
private static func handleRefreshFailure() {
logger.log("handleRefreshFailure: Token refresh failed, clearing session")
// Clear the current server connection
Store.serverConfig = nil
// Remove refresh token from secure storage
if let serverConfig = Store.serverConfig {
_ = secureStorage.removeRefreshToken(serverConnectionConfigId: serverConfig.id)
}
// Notify webview frontend about token refresh failure
if let callback = AbsDatabase.tokenRefreshCallback {
callback("onTokenRefreshFailure", ["error": "Token refresh failed"])
}
}
// MARK: - Enhanced API Methods with Token Refresh
public static func getResourceWithTokenRefresh<T: Decodable>(endpoint: String, decodable: T.Type = T.self, callback: ((_ param: T?) -> Void)?) {
if (Store.serverConfig == nil) {
logger.error("Server config not set")
callback?(nil)
return
}
let headers: HTTPHeaders = [
"Authorization": "Bearer \(Store.serverConfig!.token)"
]
let request = AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .get, headers: headers)
request.responseDecodable(of: decodable) { response in
if let statusCode = response.response?.statusCode, statusCode == 401 {
logger.log("getResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh")
handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .get, parameters: nil, decodable: decodable, callback: callback)
} else {
switch response.result {
case .success(let obj):
callback?(obj)
case .failure(let error):
logger.error("api request to \(endpoint) failed")
print(error)
callback?(nil)
}
}
}
}
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) {
logger.error("Server config not set")
callback?(nil)
return
}
let headers: HTTPHeaders = [
"Authorization": "Bearer \(Store.serverConfig!.token)"
]
let request = AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, headers: headers)
request.responseDecodable(of: decodable) { response in
if let statusCode = response.response?.statusCode, statusCode == 401 {
logger.log("postResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh")
handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .post, parameters: parameters, decodable: decodable, callback: callback)
} else {
switch response.result {
case .success(let obj):
callback?(obj)
case .failure(let error):
logger.error("api request to \(endpoint) failed")
print(error)
callback?(nil)
}
}
}
}
/**
* POST request for endpoints that only return success/failure
*/
public static func postResourceWithTokenRefresh<T: Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
if (Store.serverConfig == nil) {
logger.error("Server config not set")
callback?(false)
return
}
let headers: HTTPHeaders = [
"Authorization": "Bearer \(Store.serverConfig!.token)"
]
let request = AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, headers: headers)
request.response { response in
if let statusCode = response.response?.statusCode, statusCode == 401 {
logger.log("postResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh")
handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .post, parameters: parameters, decodable: EmptyResponse.self) { result in
callback?(result != nil)
}
} else {
switch response.result {
case .success(_):
callback?(true)
case .failure(let error):
logger.error("api request to \(endpoint) failed")
print(error)
callback?(false)
}
}
}
}
public static func patchResourceWithTokenRefresh<T: Encodable>(endpoint: String, parameters: T, callback: ((_ success: Bool) -> Void)?) {
if (Store.serverConfig == nil) {
logger.error("Server config not set")
callback?(false)
return
}
let headers: HTTPHeaders = [
"Authorization": "Bearer \(Store.serverConfig!.token)"
]
let request = AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .patch, parameters: parameters, encoder: JSONParameterEncoder.default, headers: headers)
request.response { response in
if let statusCode = response.response?.statusCode, statusCode == 401 {
logger.log("patchResourceWithTokenRefresh: 401 Unauthorized for request to \(endpoint) - attempting token refresh")
handleTokenRefresh(originalRequest: request, endpoint: endpoint, method: .patch, parameters: parameters, decodable: EmptyResponse.self) { result in
callback?(result != nil)
}
} else {
switch response.result {
case .success(_):
callback?(true)
case .failure(let error):
logger.error("api request to \(endpoint) failed")
print(error)
callback?(false)
}
}
}
}
// MARK: - API Functions
public static func startPlaybackSession(libraryItemId: String, episodeId: String?, forceTranscode:Bool, callback: @escaping (_ param: PlaybackSession) -> Void) {
var endpoint = "api/items/\(libraryItemId)/play"
if episodeId != nil {
@@ -160,20 +473,28 @@ class ApiClient {
}
}
let parameters: [String: Any] = [
"forceDirectPlay": !forceTranscode ? "1" : "",
"forceTranscode": forceTranscode ? "1" : "",
"mediaPlayer": "AVPlayer",
"deviceInfo": [
"deviceId": UIDevice.current.identifierForVendor?.uuidString,
"manufacturer": "Apple",
"model": modelCode,
"clientVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
]
]
ApiClient.postResource(endpoint: endpoint, parameters: parameters, decodable: PlaybackSession.self) { obj in
let session = obj
// Create an Encodable struct for the parameters
let parameters = PlaybackSessionRequest(
forceDirectPlay: !forceTranscode ? "1" : "",
forceTranscode: forceTranscode ? "1" : "",
mediaPlayer: "AVPlayer",
deviceInfo: DeviceInfo(
deviceId: UIDevice.current.identifierForVendor?.uuidString,
manufacturer: "Apple",
model: modelCode,
clientVersion: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
)
)
// Use the new token refresh-enabled method
postResourceWithTokenRefresh(endpoint: endpoint, parameters: parameters, decodable: PlaybackSession.self) { session in
guard let session = session else {
logger.error("startPlaybackSession: Failed to create playback session")
callback(PlaybackSession()) // Return empty session on failure
return
}
// Set server connection info on the session
session.serverConnectionConfigId = Store.serverConfig!.id
session.serverAddress = Store.serverConfig!.address
@@ -182,15 +503,28 @@ class ApiClient {
}
public static func reportPlaybackProgress(report: PlaybackReport, sessionId: String) async -> Bool {
return await postResource(endpoint: "api/session/\(sessionId)/sync", parameters: report)
return await withCheckedContinuation { continuation in
postResourceWithTokenRefresh(endpoint: "api/session/\(sessionId)/sync", parameters: report) { success in
continuation.resume(returning: success)
}
}
}
public static func reportLocalPlaybackProgress(_ session: PlaybackSession) async -> Bool {
return await postResource(endpoint: "api/session/local", parameters: session)
return await withCheckedContinuation { continuation in
postResourceWithTokenRefresh(endpoint: "api/session/local", parameters: session) { success in
continuation.resume(returning: success)
}
}
}
public static func reportAllLocalPlaybackSessions(_ sessions: [PlaybackSession]) async -> Bool {
return await postResource(endpoint: "api/session/local-all", parameters: LocalPlaybackSessionSyncAllPayload(sessions: sessions, deviceInfo: sessions.first?.deviceInfo))
return await withCheckedContinuation { continuation in
let payload = LocalPlaybackSessionSyncAllPayload(sessions: sessions, deviceInfo: sessions.first?.deviceInfo)
postResourceWithTokenRefresh(endpoint: "api/session/local-all", parameters: payload) { success in
continuation.resume(returning: success)
}
}
}
public static func syncLocalSessionsWithServer(isFirstSync: Bool) async {
@@ -257,7 +591,7 @@ class ApiClient {
public static func updateMediaProgress<T:Encodable>(libraryItemId: String, episodeId: String?, payload: T, callback: @escaping () -> Void) {
logger.log("updateMediaProgress \(libraryItemId) \(episodeId ?? "NIL") \(payload)")
let endpoint = episodeId?.isEmpty ?? true ? "api/me/progress/\(libraryItemId)" : "api/me/progress/\(libraryItemId)/\(episodeId ?? "")"
patchResource(endpoint: endpoint, parameters: payload) { success in
patchResourceWithTokenRefresh(endpoint: endpoint, parameters: payload) { _ in
callback()
}
}
@@ -265,21 +599,29 @@ class ApiClient {
public static func getMediaProgress(libraryItemId: String, episodeId: String?) async -> MediaProgress? {
logger.log("getMediaProgress \(libraryItemId) \(episodeId ?? "NIL")")
let endpoint = episodeId?.isEmpty ?? true ? "api/me/progress/\(libraryItemId)" : "api/me/progress/\(libraryItemId)/\(episodeId ?? "")"
return await getResource(endpoint: endpoint, decodable: MediaProgress.self)
return await withCheckedContinuation { continuation in
getResourceWithTokenRefresh(endpoint: endpoint, decodable: MediaProgress.self) { result in
continuation.resume(returning: result)
}
}
}
public static func getCurrentUser() async -> User? {
logger.log("getCurrentUser")
return await getResource(endpoint: "api/me", decodable: User.self)
return await withCheckedContinuation { continuation in
getResourceWithTokenRefresh(endpoint: "api/me", decodable: User.self) { result in
continuation.resume(returning: result)
}
}
}
public static func getLibraryItemWithProgress(libraryItemId:String, episodeId:String?, callback: @escaping (_ param: LibraryItem?) -> Void) {
public static func getLibraryItemWithProgress(libraryItemId: String, episodeId: String?, callback: @escaping (_ param: LibraryItem?) -> Void) {
var endpoint = "api/items/\(libraryItemId)?expanded=1&include=progress"
if episodeId != nil {
endpoint += "&episodeId=\(episodeId!)"
}
ApiClient.getResource(endpoint: endpoint, decodable: LibraryItem.self) { obj in
getResourceWithTokenRefresh(endpoint: endpoint, decodable: LibraryItem.self) { obj in
callback(obj)
}
}
@@ -338,3 +680,30 @@ struct Connectivity {
return self.sharedInstance.isReachable
}
}
// MARK: - Response Models
struct RefreshResponse: Decodable {
let user: RefreshUser?
}
struct RefreshUser: Decodable {
let accessToken: String
let refreshToken: String?
}
struct EmptyResponse: Decodable {}
struct PlaybackSessionRequest: Encodable {
let forceDirectPlay: String
let forceTranscode: String
let mediaPlayer: String
let deviceInfo: DeviceInfo
}
struct DeviceInfo: Encodable {
let deviceId: String?
let manufacturer: String
let model: String?
let clientVersion: String?
}
+14
View File
@@ -27,6 +27,7 @@ class Database {
try existing.update {
existing.name = config.name
existing.address = config.address
existing.version = config.version
existing.userId = config.userId
existing.username = config.username
existing.token = config.token
@@ -60,6 +61,19 @@ class Database {
setLastActiveConfigIndex(index: config.index)
}
}
public func updateServerConnectionConfigToken(newToken: String) {
do {
let realm = try Realm()
if let config = realm.objects(ServerConnectionConfig.self).first(where: { $0.index == getLastActiveConfigIndex() }) {
try realm.write {
config.token = newToken
}
}
} catch {
debugPrint("Failed to update server connection config token: \(error)")
}
}
public func deleteServerConnectionConfig(id: String) {
let realm = try! Realm()
+10 -2
View File
@@ -22,8 +22,16 @@ struct NowPlayingMetadata {
return item.coverUrl
} else {
guard let config = Store.serverConfig else { return nil }
guard let url = URL(string: "\(config.address)/api/items/\(itemId)/cover?token=\(config.token)") else { return nil }
return url
// As of v2.17.0 token is not needed with cover image requests
let coverUrlString: String
if Store.isServerVersionGreaterThanOrEqualTo("2.17.0") {
coverUrlString = "\(config.address)/api/items/\(itemId)/cover"
} else {
coverUrlString = "\(config.address)/api/items/\(itemId)/cover?token=\(config.token)"
}
return URL(string: coverUrlString)
}
}
}
+116
View File
@@ -0,0 +1,116 @@
//
// SecureStorage.swift
// App
//
// Created by advplyr on 2025-07-05
//
import Foundation
import Security
class SecureStorage {
private static let tag = "SecureStorage"
private static let serviceName = "AudiobookshelfRefreshTokens"
/**
* Encrypts and stores a refresh token for a specific server connection
*/
func storeRefreshToken(serverConnectionConfigId: String, refreshToken: String) -> Bool {
let key = "refresh_token_\(serverConnectionConfigId)"
guard let data = refreshToken.data(using: .utf8) else {
print("\(Self.tag): Failed to convert refresh token to data")
return false
}
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: Self.serviceName,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
]
// First, try to delete any existing item
SecItemDelete(query as CFDictionary)
// Then add the new item
let status = SecItemAdd(query as CFDictionary, nil)
if status == errSecSuccess {
print("\(Self.tag): Successfully stored encrypted refresh token for server: \(serverConnectionConfigId)")
return true
} else {
print("\(Self.tag): Failed to store refresh token for server: \(serverConnectionConfigId), status: \(status)")
return false
}
}
/**
* Retrieves and decrypts a refresh token for a specific server connection
*/
func getRefreshToken(serverConnectionConfigId: String) -> String? {
let key = "refresh_token_\(serverConnectionConfigId)"
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: Self.serviceName,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecSuccess,
let data = result as? Data,
let refreshToken = String(data: data, encoding: .utf8) {
return refreshToken
} else {
print("\(Self.tag): Failed to retrieve refresh token for server: \(serverConnectionConfigId), status: \(status)")
return nil
}
}
/**
* Removes a refresh token for a specific server connection
*/
func removeRefreshToken(serverConnectionConfigId: String) -> Bool {
let key = "refresh_token_\(serverConnectionConfigId)"
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: Self.serviceName,
kSecAttrAccount as String: key
]
let status = SecItemDelete(query as CFDictionary)
if status == errSecSuccess || status == errSecItemNotFound {
print("\(Self.tag): Successfully removed refresh token for server: \(serverConnectionConfigId)")
return true
} else {
print("\(Self.tag): Failed to remove refresh token for server: \(serverConnectionConfigId), status: \(status)")
return false
}
}
/**
* Checks if a refresh token exists for a specific server connection
*/
func hasRefreshToken(serverConnectionConfigId: String) -> Bool {
let key = "refresh_token_\(serverConnectionConfigId)"
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: Self.serviceName,
kSecAttrAccount as String: key,
kSecReturnData as String: false,
kSecMatchLimit as String: kSecMatchLimitOne
]
let status = SecItemCopyMatching(query as CFDictionary, nil)
return status == errSecSuccess
}
}
+42
View File
@@ -29,4 +29,46 @@ class Store {
}
}
}
/**
* Check if the currently connected server version is >= compareVersion
* Abs server only uses major.minor.patch
* Note: Version is returned in Abs auth payloads starting v2.6.0
* Note: Version is saved with the server connection config starting after v0.9.81
*
* @example
* serverVersion=2.25.1
* isServerVersionGreaterThanOrEqualTo("2.26.0") = false
*
* serverVersion=2.26.1
* isServerVersionGreaterThanOrEqualTo("2.26.0") = true
*/
public static func isServerVersionGreaterThanOrEqualTo(_ compareVersion: String) -> Bool {
guard let serverConfig = serverConfig, !serverConfig.version.isEmpty else {
return false
}
if compareVersion.isEmpty {
return true
}
let serverVersionParts = serverConfig.version.split(separator: ".").compactMap { Int($0) }
let compareVersionParts = compareVersion.split(separator: ".").compactMap { Int($0) }
// Compare major, minor, and patch components
let maxLength = max(serverVersionParts.count, compareVersionParts.count)
for i in 0..<maxLength {
let serverVersionComponent = i < serverVersionParts.count ? serverVersionParts[i] : 0
let compareVersionComponent = i < compareVersionParts.count ? compareVersionParts[i] : 0
if serverVersionComponent < compareVersionComponent {
return false // Server version is less than compareVersion
} else if serverVersionComponent > compareVersionComponent {
return true // Server version is greater than compareVersion
}
}
return true // versions are equal in major, minor, and patch
}
}
+27 -16
View File
@@ -99,21 +99,6 @@ export default {
await this.$store.dispatch('user/loadUserSettings')
},
async postRequest(url, data, headers, connectTimeout = 30000) {
const options = {
url,
headers,
data,
connectTimeout
}
const response = await CapacitorHttp.post(options)
console.log('[default] POST request response', response)
if (response.status >= 400) {
throw new Error(response.data)
} else {
return response.data
}
},
async attemptConnection() {
console.warn('[default] attemptConnection')
if (!this.networkConnected) {
@@ -145,10 +130,18 @@ export default {
AbsLogger.info({ tag: 'default', message: `attemptConnection: Got server config, attempt authorize (${serverConfig.name})` })
const authRes = await this.postRequest(`${serverConfig.address}/api/authorize`, null, { Authorization: `Bearer ${serverConfig.token}` }, 6000).catch((error) => {
const nativeHttpOptions = {
headers: {
Authorization: `Bearer ${serverConfig.token}`
},
connectTimeout: 6000,
serverConnectionConfig: serverConfig
}
const authRes = await this.$nativeHttp.post(`${serverConfig.address}/api/authorize`, null, nativeHttpOptions).catch((error) => {
AbsLogger.error({ tag: 'default', message: `attemptConnection: Server auth failed (${serverConfig.name})` })
return false
})
if (!authRes) {
this.attemptingConnection = false
return
@@ -158,6 +151,22 @@ export default {
this.$store.commit('setServerSettings', serverSettings)
this.$store.commit('libraries/setEReaderDevices', ereaderDevices)
if (this.$isValidVersion(serverSettings.version, '2.26.0')) {
// Check if the server is using the new JWT auth and is still using an old token in the server config
// If so, redirect to /connect and request to re-login
if (serverConfig.token === user.token || user.isOldToken) {
this.attemptingConnection = false
AbsLogger.info({ tag: 'default', message: `attemptConnection: Server is using new JWT auth but config is still using an old token (server version: ${serverSettings.version}) (${serverConfig.name})` })
// Clear last server config
await this.$store.dispatch('user/logout')
this.$router.push(`/connect?error=oldAuthToken&serverConnectionConfigId=${serverConfig.id}`)
return
}
// Token may have been refreshed during the authorize call so refetch from store
serverConfig.token = this.$store.getters['user/getToken'] || serverConfig.token
}
// Set library - Use last library if set and available fallback to default user library
const lastLibraryId = await this.$localStore.getLastLibraryId()
if (lastLibraryId && (!user.librariesAccessible.length || user.librariesAccessible.includes(lastLibraryId))) {
@@ -165,9 +174,11 @@ export default {
} else if (userDefaultLibraryId) {
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
}
serverConfig.version = serverSettings.version
const serverConnectionConfig = await this.$db.setServerConnectionConfig(serverConfig)
this.$store.commit('user/setUser', user)
this.$store.commit('user/setAccessToken', serverConnectionConfig.token)
this.$store.commit('user/setServerConnectionConfig', serverConnectionConfig)
this.$socket.connect(serverConnectionConfig.address, serverConnectionConfig.token)
+1 -1
View File
@@ -30,7 +30,7 @@ export default {
css: ['@/assets/tailwind.css', '@/assets/app.css'],
plugins: ['@/plugins/server.js', '@/plugins/db.js', '@/plugins/localStore.js', '@/plugins/init.client.js', '@/plugins/axios.js', '@/plugins/nativeHttp.js', '@/plugins/capacitor/index.js', '@/plugins/capacitor/AbsAudioPlayer.js', '@/plugins/toast.js', '@/plugins/constants.js', '@/plugins/haptics.js', '@/plugins/i18n.js'],
plugins: ['@/plugins/server.js', '@/plugins/db.js', '@/plugins/localStore.js', '@/plugins/init.client.js', '@/plugins/axios.js', '@/plugins/capacitor/index.js', '@/plugins/capacitor/AbsAudioPlayer.js', '@/plugins/nativeHttp.js', '@/plugins/toast.js', '@/plugins/constants.js', '@/plugins/haptics.js', '@/plugins/i18n.js'],
components: true,
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "audiobookshelf-app",
"version": "0.9.81-beta",
"version": "0.10.1-beta",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf-app",
"version": "0.9.81-beta",
"version": "0.10.1-beta",
"dependencies": {
"@capacitor-community/keep-awake": "^7.0.0",
"@capacitor-community/volume-buttons": "^7.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-app",
"version": "0.9.81-beta",
"version": "0.10.1-beta",
"author": "advplyr",
"scripts": {
"dev": "nuxt --hostname 0.0.0.0 --port 1337",
+9 -11
View File
@@ -4,6 +4,10 @@
<ui-text-input-with-label :value="username" :label="$strings.LabelUsername" disabled class="my-2" />
<div v-if="serverVersion" class="text-sm text-fg">
<p>Server version: v{{ serverVersion }}</p>
</div>
<ui-btn color="primary flex items-center justify-between gap-2 ml-auto text-base mt-8" @click="logout">{{ $strings.ButtonSwitchServerUser }}<span class="material-symbols" style="font-size: 1.1rem">logout</span></ui-btn>
<div class="flex justify-center items-center my-4 left-0 right-0 bottom-0 absolute">
@@ -43,22 +47,16 @@ export default {
},
serverAddress() {
return this.serverConnectionConfig.address
},
serverVersion() {
// Saved in server connection config after 0.9.81
return this.serverConnectionConfig.version
}
},
methods: {
async logout() {
await this.$hapticsImpact()
if (this.user) {
await this.$nativeHttp.post('/logout').catch((error) => {
console.error(error)
})
}
this.$socket.logout()
await this.$db.logout()
this.$localStore.removeLastLibraryId()
this.$store.commit('user/logout')
this.$store.commit('libraries/setCurrentLibrary', null)
await this.$store.dispatch('user/logout')
this.$router.push('/connect')
}
},
+1 -1
View File
@@ -62,7 +62,7 @@ export default {
async loadRecentEpisodes(page = 0) {
this.loadedLibraryId = this.currentLibraryId
this.processing = true
const episodePayload = await this.$nativeHttp.get(`/api/libraries/${this.currentLibraryId}/recent-episodes?limit=25&page=${page}`).catch((error) => {
const episodePayload = await this.$nativeHttp.get(`/api/libraries/${this.currentLibraryId}/recent-episodes?limit=50&page=${page}`).catch((error) => {
console.error('Failed to get recent episodes', error)
this.$toast.error('Failed to get recent episodes')
return null
+1 -1
View File
@@ -37,9 +37,9 @@ export default {
computed: {},
methods: {
async init() {
await this.$store.dispatch('setupNetworkListener')
this.deviceData = await this.$db.getDeviceData()
this.$store.commit('setDeviceData', this.deviceData)
await this.$store.dispatch('setupNetworkListener')
}
},
mounted() {
+4
View File
@@ -31,6 +31,7 @@
<div class="flex items-center justify-center">
<h1 class="text-xl font-semibold">{{ title }}</h1>
<widgets-explicit-indicator v-if="isExplicit" />
<widgets-abridged-indicator v-if="isAbridged" />
</div>
<p v-if="subtitle" class="text-fg text-base">{{ subtitle }}</p>
</div>
@@ -438,6 +439,9 @@ export default {
isExplicit() {
return !!this.mediaMetadata.explicit
},
isAbridged() {
return !!this.mediaMetadata.abridged
},
showPlay() {
return !this.isMissing && !this.isInvalid && (this.numTracks || this.episodes.length)
},
+1 -1
View File
@@ -200,7 +200,7 @@ export default {
this.onMediaItemHistoryUpdatedListener = await AbsAudioPlayer.addListener('onMediaItemHistoryUpdated', this.onMediaItemHistoryUpdated)
},
beforeDestroy() {
if (this.onMediaItemHistoryUpdatedListener) this.onMediaItemHistoryUpdatedListener.remove()
this.onMediaItemHistoryUpdatedListener?.remove()
}
}
</script>
+4
View File
@@ -363,6 +363,10 @@ export default {
},
themeOptionItems() {
return [
{
text: this.$strings.LabelThemeBlack,
value: 'black'
},
{
text: this.$strings.LabelThemeDark,
value: 'dark'
+113 -12
View File
@@ -1,17 +1,50 @@
export default function ({ $axios, store }) {
$axios.onRequest(config => {
export default function ({ $axios, store, $db }) {
// Track if we're currently refreshing to prevent multiple refresh attempts
let isRefreshing = false
let failedQueue = []
const processQueue = (error, token = null) => {
failedQueue.forEach(({ resolve, reject }) => {
if (error) {
reject(error)
} else {
resolve(token)
}
})
failedQueue = []
}
/**
* Handles the case when token refresh fails
* @param {string} [serverConnectionConfigId]
* @returns {Promise} - Promise that resolves when logout is complete
*/
const handleRefreshFailure = async (serverConnectionConfigId) => {
try {
console.log('[axios] Handling refresh failure - logging out user')
// Clear store
await store.dispatch('user/logout')
if (serverConnectionConfigId) {
// Clear refresh token for server connection config
await $db.clearRefreshToken(serverConnectionConfigId)
}
if (window.location.pathname !== '/connect') {
window.location.href = '/connect?error=refreshTokenFailed&serverConnectionConfigId=' + serverConnectionConfigId
}
} catch (error) {
console.error('[axios] Failed to handle refresh failure:', error)
}
}
$axios.onRequest((config) => {
console.log('[Axios] Making request to ' + config.url)
if (config.url.startsWith('http:') || config.url.startsWith('https:') || config.url.startsWith('capacitor:')) {
return
}
const customHeaders = store.getters['user/getCustomHeaders']
if (customHeaders) {
for (const key in customHeaders) {
config.headers.common[key] = customHeaders[key]
}
}
const bearerToken = store.getters['user/getToken']
if (bearerToken) {
config.headers.common['Authorization'] = `Bearer ${bearerToken}`
@@ -26,7 +59,75 @@ export default function ({ $axios, store }) {
console.log('[Axios] Request out', config.url)
})
$axios.onError(error => {
console.error('Axios error code', error)
$axios.onError(async (error) => {
const originalRequest = error.config
const code = parseInt(error.response && error.response.status)
const message = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
console.error('Axios error', code, message)
// Handle 401 Unauthorized (token expired)
if (code === 401 && !originalRequest._retry) {
// Skip refresh for auth endpoints to prevent infinite loops
if (originalRequest.url.endsWith('/auth/refresh') || originalRequest.url.endsWith('/login')) {
await handleRefreshFailure(store.getters['user/getServerConnectionConfigId'])
return Promise.reject(error)
}
if (isRefreshing) {
// If already refreshing, queue this request
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject })
})
.then((token) => {
if (!originalRequest.headers) {
originalRequest.headers = {}
}
originalRequest.headers['Authorization'] = `Bearer ${token}`
return $axios(originalRequest)
})
.catch((err) => {
return Promise.reject(err)
})
}
originalRequest._retry = true
isRefreshing = true
try {
// Attempt to refresh the token
// Updates store if successful, otherwise clears store and throw error
const newAccessToken = await store.dispatch('user/refreshToken')
if (!newAccessToken) {
console.error('No new access token received')
return Promise.reject(error)
}
// Update the original request with new token
if (!originalRequest.headers) {
originalRequest.headers = {}
}
originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`
// Process any queued requests
processQueue(null, newAccessToken)
// Retry the original request
return $axios(originalRequest)
} catch (refreshError) {
console.error('Token refresh failed:', refreshError)
// Process queued requests with error
processQueue(refreshError, null)
await handleRefreshFailure(store.getters['user/getServerConnectionConfigId'])
return Promise.reject(refreshError)
} finally {
isRefreshing = false
}
}
return Promise.reject(error)
})
}
}
+15 -2
View File
@@ -70,7 +70,12 @@ class AbsAudioPlayerWeb extends WebPlugin {
const deviceInfo = {
deviceId: this.getDeviceId()
}
const playbackSession = await $axios.$post(route, { deviceInfo, mediaPlayer: 'html5-mobile', forceDirectPlay: true })
const reqBody = {
deviceInfo,
mediaPlayer: 'html5-mobile',
forceDirectPlay: true
}
const playbackSession = await $axios.$post(route, reqBody)
if (playbackSession) {
if (startTime !== undefined && startTime !== null) playbackSession.currentTime = startTime
this.setAudioPlayer(playbackSession, playWhenReady)
@@ -245,7 +250,15 @@ class AbsAudioPlayerWeb extends WebPlugin {
this.trackStartTime = Math.max(0, this.startTime - (this.currentTrack.startOffset || 0))
const serverAddressUrl = new URL(vuexStore.getters['user/getServerAddress'])
const serverHost = `${serverAddressUrl.protocol}//${serverAddressUrl.host}`
this.player.src = `${serverHost}${this.currentTrack.contentUrl}?token=${vuexStore.getters['user/getToken']}`
let sessionTrackUrl = null
if (this.currentTrack.contentUrl?.startsWith('/hls')) {
sessionTrackUrl = this.currentTrack.contentUrl
} else {
sessionTrackUrl = `/public/session/${this.playbackSession.id}/track/${this.currentTrack.index || 1}`
}
this.player.src = `${serverHost}${sessionTrackUrl}`
console.log(`[AbsAudioPlayer] Loading track src ${this.player.src}`)
this.player.load()
this.player.playbackRate = this.playbackRate
+133 -85
View File
@@ -1,4 +1,17 @@
import { registerPlugin, Capacitor, WebPlugin } from '@capacitor/core';
import { registerPlugin, Capacitor, WebPlugin } from '@capacitor/core'
/**
* @typedef {Object} ServerConnectionConfig
* @property {string} id
* @property {number} index
* @property {string} name
* @property {string} address
* @property {string} version
* @property {string} userId
* @property {string} username
* @property {string} token
* @property {string} [refreshToken] - Only passed in when setting config, then stored in secure storage
*/
class AbsDatabaseWeb extends WebPlugin {
constructor() {
@@ -19,17 +32,30 @@ class AbsDatabaseWeb extends WebPlugin {
return deviceData
}
/**
*
* @param {ServerConnectionConfig} serverConnectionConfig
* @returns {Promise<ServerConnectionConfig>}
*/
async setCurrentServerConnectionConfig(serverConnectionConfig) {
var deviceData = await this.getDeviceData()
var ssc = deviceData.serverConnectionConfigs.find(_ssc => _ssc.id == serverConnectionConfig.id)
var ssc = deviceData.serverConnectionConfigs.find((_ssc) => _ssc.id == serverConnectionConfig.id)
if (ssc) {
deviceData.lastServerConnectionConfigId = ssc.id
ssc.name = `${ssc.address} (${serverConnectionConfig.username})`
ssc.token = serverConnectionConfig.token
ssc.userId = serverConnectionConfig.userId
ssc.username = serverConnectionConfig.username
ssc.version = serverConnectionConfig.version
ssc.customHeaders = serverConnectionConfig.customHeaders || {}
if (serverConnectionConfig.refreshToken) {
console.log('[AbsDatabase] Updating refresh token...', serverConnectionConfig.refreshToken)
// Only using local storage for web version that is only used for testing
localStorage.setItem(`refresh_token_${ssc.id}`, serverConnectionConfig.refreshToken)
}
localStorage.setItem('device', JSON.stringify(deviceData))
} else {
ssc = {
@@ -40,8 +66,16 @@ class AbsDatabaseWeb extends WebPlugin {
username: serverConnectionConfig.username,
address: serverConnectionConfig.address,
token: serverConnectionConfig.token,
version: serverConnectionConfig.version,
customHeaders: serverConnectionConfig.customHeaders || {}
}
if (serverConnectionConfig.refreshToken) {
console.log('[AbsDatabase] Setting refresh token...', serverConnectionConfig.refreshToken)
// Only using local storage for web version that is only used for testing
localStorage.setItem(`refresh_token_${ssc.id}`, serverConnectionConfig.refreshToken)
}
deviceData.serverConnectionConfigs.push(ssc)
deviceData.lastServerConnectionConfigId = ssc.id
localStorage.setItem('device', JSON.stringify(deviceData))
@@ -49,14 +83,26 @@ class AbsDatabaseWeb extends WebPlugin {
return ssc
}
async getRefreshToken({ serverConnectionConfigId }) {
console.log('[AbsDatabase] Getting refresh token...', serverConnectionConfigId)
const refreshToken = localStorage.getItem(`refresh_token_${serverConnectionConfigId}`)
return refreshToken ? { refreshToken } : null
}
async clearRefreshToken({ serverConnectionConfigId }) {
console.log('[AbsDatabase] Clearing refresh token...', serverConnectionConfigId)
localStorage.removeItem(`refresh_token_${serverConnectionConfigId}`)
}
async removeServerConnectionConfig(serverConnectionConfigCallObject) {
var serverConnectionConfigId = serverConnectionConfigCallObject.serverConnectionConfigId
var deviceData = await this.getDeviceData()
deviceData.serverConnectionConfigs = deviceData.serverConnectionConfigs.filter(ssc => ssc.id != serverConnectionConfigId)
deviceData.serverConnectionConfigs = deviceData.serverConnectionConfigs.filter((ssc) => ssc.id != serverConnectionConfigId)
localStorage.setItem('device', JSON.stringify(deviceData))
}
async logout() {
console.log('[AbsDatabase] Logging out...')
var deviceData = await this.getDeviceData()
deviceData.lastServerConnectionConfigId = null
localStorage.setItem('device', JSON.stringify(deviceData))
@@ -85,79 +131,81 @@ class AbsDatabaseWeb extends WebPlugin {
}
async getLocalLibraryItems(payload) {
return {
value: [{
id: 'local_test',
libraryItemId: 'test34',
serverAddress: 'https://abs.test.com',
serverUserId: 'test56',
folderId: 'test1',
absolutePath: 'a',
contentUrl: 'c',
isInvalid: false,
mediaType: 'book',
media: {
metadata: {
title: 'Test Book',
authorName: 'Test Author Name'
value: [
{
id: 'local_test',
libraryItemId: 'test34',
serverAddress: 'https://abs.test.com',
serverUserId: 'test56',
folderId: 'test1',
absolutePath: 'a',
contentUrl: 'c',
isInvalid: false,
mediaType: 'book',
media: {
metadata: {
title: 'Test Book',
authorName: 'Test Author Name'
},
coverPath: null,
tags: [],
audioFiles: [],
chapters: [],
tracks: [
{
index: 1,
startOffset: 0,
duration: 10000,
title: 'Track Title 1',
contentUrl: 'test',
mimeType: 'audio/mpeg',
metadata: null,
isLocal: true,
localFileId: 'lf1',
audioProbeResult: {}
},
{
index: 2,
startOffset: 0,
duration: 15000,
title: 'Track Title 2',
contentUrl: 'test2',
mimeType: 'audio/mpeg',
metadata: null,
isLocal: true,
localFileId: 'lf2',
audioProbeResult: {}
},
{
index: 3,
startOffset: 0,
duration: 20000,
title: 'Track Title 3',
contentUrl: 'test3',
mimeType: 'audio/mpeg',
metadata: null,
isLocal: true,
localFileId: 'lf3',
audioProbeResult: {}
}
]
},
coverPath: null,
tags: [],
audioFiles: [],
chapters: [],
tracks: [
localFiles: [
{
index: 1,
startOffset: 0,
duration: 10000,
title: 'Track Title 1',
id: 'lf1',
filename: 'lf1.mp3',
contentUrl: 'test',
absolutePath: 'test',
simplePath: 'test',
mimeType: 'audio/mpeg',
metadata: null,
isLocal: true,
localFileId: 'lf1',
audioProbeResult: {}
},
{
index: 2,
startOffset: 0,
duration: 15000,
title: 'Track Title 2',
contentUrl: 'test2',
mimeType: 'audio/mpeg',
metadata: null,
isLocal: true,
localFileId: 'lf2',
audioProbeResult: {}
},
{
index: 3,
startOffset: 0,
duration: 20000,
title: 'Track Title 3',
contentUrl: 'test3',
mimeType: 'audio/mpeg',
metadata: null,
isLocal: true,
localFileId: 'lf3',
audioProbeResult: {}
size: 39048290
}
]
},
localFiles: [
{
id: 'lf1',
filename: 'lf1.mp3',
contentUrl: 'test',
absolutePath: 'test',
simplePath: 'test',
mimeType: 'audio/mpeg',
size: 39048290
}
],
coverContentUrl: null,
coverAbsolutePath: null,
isLocal: true
}]
],
coverContentUrl: null,
coverAbsolutePath: null,
isLocal: true
}
]
}
}
async getLocalLibraryItemsInFolder({ folderId }) {
@@ -167,7 +215,7 @@ class AbsDatabaseWeb extends WebPlugin {
return this.getLocalLibraryItems().then((data) => data.value[0])
}
async getLocalLibraryItemByLId({ libraryItemId }) {
return this.getLocalLibraryItems().then((data) => data.value.find(lli => lli.libraryItemId == libraryItemId))
return this.getLocalLibraryItems().then((data) => data.value.find((lli) => lli.libraryItemId == libraryItemId))
}
async getAllLocalMediaProgress() {
return {
@@ -182,7 +230,7 @@ class AbsDatabaseWeb extends WebPlugin {
isFinished: false,
lastUpdate: 394089090,
startedAt: 239048209,
finishedAt: null,
finishedAt: null
// For local lib items from server to support server sync
// var serverConnectionConfigId:String?,
// var serverAddress:String?,
@@ -240,7 +288,7 @@ class AbsDatabaseWeb extends WebPlugin {
serverSyncAttempted: true,
serverSyncSuccess: true,
serverSyncMessage: null,
timestamp: Date.now() - (1000 * 60 * 22) + 13000 // 22 mins ago + 13s
timestamp: Date.now() - 1000 * 60 * 22 + 13000 // 22 mins ago + 13s
},
{
name: 'Play',
@@ -250,7 +298,7 @@ class AbsDatabaseWeb extends WebPlugin {
serverSyncAttempted: false,
serverSyncSuccess: null,
serverSyncMessage: null,
timestamp: Date.now() - (1000 * 60 * 22) // 22 mins ago
timestamp: Date.now() - 1000 * 60 * 22 // 22 mins ago
},
{
name: 'Pause',
@@ -260,7 +308,7 @@ class AbsDatabaseWeb extends WebPlugin {
serverSyncAttempted: true,
serverSyncSuccess: false,
serverSyncMessage: null,
timestamp: Date.now() - (1000 * 60 * 60) + (58000) // 1 hour ago + 58s
timestamp: Date.now() - 1000 * 60 * 60 + 58000 // 1 hour ago + 58s
},
{
name: 'Save',
@@ -270,7 +318,7 @@ class AbsDatabaseWeb extends WebPlugin {
serverSyncAttempted: true,
serverSyncSuccess: true,
serverSyncMessage: null,
timestamp: Date.now() - (1000 * 60 * 60) + (45000) // 1 hour ago + 45s
timestamp: Date.now() - 1000 * 60 * 60 + 45000 // 1 hour ago + 45s
},
{
name: 'Save',
@@ -280,7 +328,7 @@ class AbsDatabaseWeb extends WebPlugin {
serverSyncAttempted: true,
serverSyncSuccess: true,
serverSyncMessage: null,
timestamp: Date.now() - (1000 * 60 * 60) + (30000) // 1 hour ago + 30s
timestamp: Date.now() - 1000 * 60 * 60 + 30000 // 1 hour ago + 30s
},
{
name: 'Save',
@@ -290,7 +338,7 @@ class AbsDatabaseWeb extends WebPlugin {
serverSyncAttempted: true,
serverSyncSuccess: true,
serverSyncMessage: null,
timestamp: Date.now() - (1000 * 60 * 60) + (15000) // 1 hour ago + 15s
timestamp: Date.now() - 1000 * 60 * 60 + 15000 // 1 hour ago + 15s
},
{
name: 'Play',
@@ -300,7 +348,7 @@ class AbsDatabaseWeb extends WebPlugin {
serverSyncAttempted: false,
serverSyncSuccess: null,
serverSyncMessage: null,
timestamp: Date.now() - (1000 * 60 * 60) // 1 hour ago
timestamp: Date.now() - 1000 * 60 * 60 // 1 hour ago
},
{
name: 'Stop',
@@ -310,7 +358,7 @@ class AbsDatabaseWeb extends WebPlugin {
serverSyncAttempted: true,
serverSyncSuccess: true,
serverSyncMessage: null,
timestamp: Date.now() - (1000 * 60 * 60 * 25) + 10000 // 25 hours ago + 10s
timestamp: Date.now() - 1000 * 60 * 60 * 25 + 10000 // 25 hours ago + 10s
},
{
name: 'Seek',
@@ -320,7 +368,7 @@ class AbsDatabaseWeb extends WebPlugin {
serverSyncAttempted: true,
serverSyncSuccess: true,
serverSyncMessage: null,
timestamp: Date.now() - (1000 * 60 * 60 * 25) + 2000 // 25 hours ago + 2s
timestamp: Date.now() - 1000 * 60 * 60 * 25 + 2000 // 25 hours ago + 2s
},
{
name: 'Play',
@@ -330,7 +378,7 @@ class AbsDatabaseWeb extends WebPlugin {
serverSyncAttempted: false,
serverSyncSuccess: null,
serverSyncMessage: null,
timestamp: Date.now() - (1000 * 60 * 60 * 25) // 25 hours ago
timestamp: Date.now() - 1000 * 60 * 60 * 25 // 25 hours ago
},
{
name: 'Play',
@@ -340,7 +388,7 @@ class AbsDatabaseWeb extends WebPlugin {
serverSyncAttempted: false,
serverSyncSuccess: null,
serverSyncMessage: null,
timestamp: Date.now() - (1000 * 60 * 60 * 50) // 50 hours ago
timestamp: Date.now() - 1000 * 60 * 60 * 50 // 50 hours ago
}
]
}
@@ -351,4 +399,4 @@ const AbsDatabase = registerPlugin('AbsDatabase', {
web: () => new AbsDatabaseWeb()
})
export { AbsDatabase }
export { AbsDatabase }
+2 -1
View File
@@ -2,9 +2,10 @@ import Vue from 'vue'
import { AbsAudioPlayer } from './AbsAudioPlayer'
import { AbsDownloader } from './AbsDownloader'
import { AbsFileSystem } from './AbsFileSystem'
import { AbsDatabase } from './AbsDatabase'
import { AbsLogger } from './AbsLogger'
import { Capacitor } from '@capacitor/core'
Vue.prototype.$platform = Capacitor.getPlatform()
export { AbsAudioPlayer, AbsDownloader, AbsFileSystem, AbsLogger }
export { AbsAudioPlayer, AbsDownloader, AbsFileSystem, AbsLogger, AbsDatabase }
+44 -6
View File
@@ -1,7 +1,7 @@
import { AbsDatabase } from './capacitor/AbsDatabase'
class DbService {
constructor() { }
constructor() {}
getDeviceData() {
return AbsDatabase.getDeviceData().then((data) => {
@@ -10,6 +10,26 @@ class DbService {
})
}
/**
* Retrieves refresh token from secure storage
* @param {string} serverConnectionConfigId
* @return {Promise<string|null>}
*/
async getRefreshToken(serverConnectionConfigId) {
const refreshTokenData = await AbsDatabase.getRefreshToken({ serverConnectionConfigId })
return refreshTokenData?.refreshToken
}
/**
* Clears refresh token from secure storage
* @param {string} serverConnectionConfigId
* @returns {Promise<boolean>}
*/
async clearRefreshToken(serverConnectionConfigId) {
const result = await AbsDatabase.clearRefreshToken({ serverConnectionConfigId })
return !!result?.success
}
setServerConnectionConfig(serverConnectionConfig) {
return AbsDatabase.setCurrentServerConnectionConfig(serverConnectionConfig).then((data) => {
console.log('Set server connection config', JSON.stringify(data))
@@ -29,10 +49,12 @@ class DbService {
}
getLocalFolders() {
return AbsDatabase.getLocalFolders().then((data) => data.value).catch((error) => {
console.error('Failed to load', error)
return null
})
return AbsDatabase.getLocalFolders()
.then((data) => data.value)
.catch((error) => {
console.error('Failed to load', error)
return null
})
}
getLocalFolder(folderId) {
@@ -103,4 +125,20 @@ class DbService {
export default ({ app, store }, inject) => {
inject('db', new DbService())
}
// Listen for token refresh events from native app
AbsDatabase.addListener('onTokenRefresh', (data) => {
console.log('[db] onTokenRefresh', data)
store.commit('user/setAccessToken', data.accessToken)
})
// Listen for token refresh failure events from native app
AbsDatabase.addListener('onTokenRefreshFailure', async (data) => {
console.log('[db] onTokenRefreshFailure', data)
// Clear store and redirect to login page
await store.dispatch('user/logout')
if (window.location.pathname !== '/connect') {
window.location.href = '/connect?error=refreshTokenFailed&serverConnectionConfigId=' + data.serverConnectionConfigId || ''
}
})
}
+25
View File
@@ -245,10 +245,35 @@ Vue.prototype.$sanitizeSlug = (str) => {
return str
}
/**
* Compares two semantic versioning strings to determine if the current version meets
* or exceeds the minimum version requirement.
* Only supports 3 part versions, e.g. "1.2.3"
*
* @param {string} currentVersion - The current version string to compare, e.g., "1.2.3".
* @param {string} minVersion - The minimum version string required, e.g., "1.0.0".
* @returns {boolean} - Returns true if the current version is greater than or equal
* to the minimum version, false otherwise.
*/
function isValidVersion(currentVersion, minVersion) {
if (!currentVersion || !minVersion) return false
const currentParts = currentVersion.split('.').map(Number)
const minParts = minVersion.split('.').map(Number)
for (let i = 0; i < minParts.length; i++) {
if (currentParts[i] > minParts[i]) return true
if (currentParts[i] < minParts[i]) return false
}
return true
}
export default ({ store, app }, inject) => {
const eventBus = new Vue()
inject('eventBus', eventBus)
inject('isValidVersion', isValidVersion)
// Set theme
app.$localStore?.getTheme()?.then((theme) => {
if (theme) {
+201 -8
View File
@@ -1,10 +1,14 @@
import { CapacitorHttp } from '@capacitor/core'
export default function ({ store }, inject) {
export default function ({ store, $db, $socket }, inject) {
const nativeHttp = {
request(method, _url, data, options = {}) {
async request(method, _url, data, options = {}) {
// When authorizing before a config is set, server config gets passed in as an option
let serverConnectionConfig = options.serverConnectionConfig || store.state.user.serverConnectionConfig
delete options.serverConnectionConfig
let url = _url
const headers = {}
let headers = {}
if (!url.startsWith('http') && !url.startsWith('capacitor')) {
const bearerToken = store.getters['user/getToken']
if (bearerToken) {
@@ -12,22 +16,31 @@ export default function ({ store }, inject) {
} else {
console.warn('[nativeHttp] No Bearer Token for request')
}
const serverUrl = store.getters['user/getServerAddress']
if (serverUrl) {
url = `${serverUrl}${url}`
if (serverConnectionConfig?.address) {
url = `${serverConnectionConfig.address}${url}`
}
}
if (data) {
headers['Content-Type'] = 'application/json'
}
if (options.headers) {
headers = { ...headers, ...options.headers }
delete options.headers
}
console.log(`[nativeHttp] Making ${method} request to ${url}`)
return CapacitorHttp.request({
method,
url,
data,
headers,
...options
}).then(res => {
}).then((res) => {
if (res.status === 401) {
console.error(`[nativeHttp] 401 status for url "${url}"`)
// Handle refresh token automatically
return this.handleTokenRefresh(method, url, data, headers, options, serverConnectionConfig)
}
if (res.status >= 400) {
console.error(`[nativeHttp] ${res.status} status for url "${url}"`)
throw new Error(res.data)
@@ -35,6 +48,186 @@ export default function ({ store }, inject) {
return res.data
})
},
/**
* Handles token refresh when a 401 Unauthorized response is received
* @param {string} method - HTTP method
* @param {string} url - Full URL
* @param {*} data - Request data
* @param {Object} headers - Request headers
* @param {Object} options - Additional options
* @param {{ id: string, address: string, version: string }} serverConnectionConfig
* @returns {Promise} - Promise that resolves with the response data
*/
async handleTokenRefresh(method, url, data, headers, options, serverConnectionConfig) {
try {
console.log('[nativeHttp] Attempting to refresh token...')
if (!serverConnectionConfig?.id) {
console.error('[nativeHttp] No server connection config ID available for token refresh')
throw new Error('No server connection available')
}
// Get refresh token from secure storage
const refreshToken = await $db.getRefreshToken(serverConnectionConfig.id)
if (!refreshToken) {
console.error('[nativeHttp] No refresh token available')
throw new Error('No refresh token available')
}
// Attempt to refresh the token
const newTokens = await this.refreshAccessToken(refreshToken, serverConnectionConfig.address)
if (!newTokens?.accessToken) {
console.error('[nativeHttp] Failed to refresh access token')
throw new Error('Failed to refresh access token')
}
// Update the store with new tokens
await this.updateTokens(newTokens, serverConnectionConfig)
// Retry the original request with the new token
console.log('[nativeHttp] Retrying original request with new token...')
const retryResponse = await CapacitorHttp.request({
method,
url,
data,
headers: {
...headers,
Authorization: `Bearer ${newTokens.accessToken}`
},
...options
})
if (retryResponse.status >= 400) {
console.error(`[nativeHttp] Retry request failed with status ${retryResponse.status}`)
throw new Error(retryResponse.data)
}
return retryResponse.data
} catch (error) {
console.error('[nativeHttp] Token refresh failed:', error)
// If refresh fails, redirect to login
await this.handleRefreshFailure(serverConnectionConfig?.id)
throw error
}
},
/**
* Refreshes the access token using the refresh token
* @param {string} refreshToken - The refresh token
* @param {string} serverAddress - The server address
* @returns {Promise<Object|null>} - Promise that resolves with new tokens or null
*/
async refreshAccessToken(refreshToken, serverAddress) {
try {
if (!serverAddress) {
throw new Error('No server address available')
}
console.log('[nativeHttp] Refreshing access token...')
const response = await CapacitorHttp.post({
url: `${serverAddress}/auth/refresh`,
headers: {
'Content-Type': 'application/json',
'x-refresh-token': refreshToken
},
data: {}
})
if (response.status !== 200) {
console.error('[nativeHttp] Token refresh request failed:', response.status)
return null
}
const userResponseData = response.data
if (!userResponseData.user?.accessToken) {
console.error('[nativeHttp] No access token in refresh response')
return null
}
console.log('[nativeHttp] Successfully refreshed access token')
return {
accessToken: userResponseData.user.accessToken,
// Refresh token gets returned when refresh token is sent in x-refresh-token header
refreshToken: userResponseData.user.refreshToken
}
} catch (error) {
console.error('[nativeHttp] Failed to refresh access token:', error)
return null
}
},
/**
* Updates the store and secure storage with new tokens
* @param {Object} tokens - Object containing accessToken and refreshToken
* @param {{ id: string, address: string, version: string }} serverConnectionConfig
* @returns {Promise} - Promise that resolves when tokens are updated
*/
async updateTokens(tokens, serverConnectionConfig) {
try {
if (!serverConnectionConfig?.id) {
throw new Error('No server connection config ID available')
}
// Update the config with new tokens
const updatedConfig = {
...serverConnectionConfig,
token: tokens.accessToken,
refreshToken: tokens.refreshToken
}
// Save updated config to secure storage, persists refresh token in secure storage
const savedConfig = await $db.setServerConnectionConfig(updatedConfig)
// Update the store
store.commit('user/setAccessToken', tokens.accessToken)
// Re-authenticate socket if necessary
if ($socket?.connected && !$socket.isAuthenticated) {
$socket.sendAuthenticate()
} else if (!$socket) {
console.warn('[nativeHttp] Socket not available, cannot re-authenticate')
}
if (savedConfig) {
store.commit('user/setServerConnectionConfig', savedConfig)
}
console.log('[nativeHttp] Successfully updated tokens in store and secure storage')
} catch (error) {
console.error('[nativeHttp] Failed to update tokens:', error)
throw error
}
},
/**
* Handles the case when token refresh fails
* @param {string} [serverConnectionConfigId]
* @returns {Promise} - Promise that resolves when logout is complete
*/
async handleRefreshFailure(serverConnectionConfigId) {
try {
console.log('[nativeHttp] Handling refresh failure - logging out user')
// Clear store
await store.dispatch('user/logout')
if (serverConnectionConfigId) {
// Clear refresh token for server connection config
await $db.clearRefreshToken(serverConnectionConfigId)
}
// Redirect to login page
if (window.location.pathname !== '/connect') {
window.location.href = '/connect?error=refreshTokenFailed&serverConnectionConfigId=' + serverConnectionConfigId
}
} catch (error) {
console.error('[nativeHttp] Failed to handle refresh failure:', error)
}
},
get(url, options = {}) {
return this.request('GET', url, undefined, options)
},
@@ -49,4 +242,4 @@ export default function ({ store }, inject) {
}
}
inject('nativeHttp', nativeHttp)
}
}
+14 -3
View File
@@ -9,7 +9,7 @@ class ServerSocket extends EventEmitter {
this.socket = null
this.connected = false
this.serverAddress = null
this.token = null
this.isAuthenticated = false
this.lastReconnectAttemptTime = 0
}
@@ -26,7 +26,6 @@ class ServerSocket extends EventEmitter {
connect(serverAddress, token) {
this.serverAddress = serverAddress
this.token = token
const serverUrl = new URL(serverAddress)
const serverHost = `${serverUrl.protocol}//${serverUrl.host}`
@@ -53,6 +52,7 @@ class ServerSocket extends EventEmitter {
this.socket.on('connect', this.onConnect.bind(this))
this.socket.on('disconnect', this.onDisconnect.bind(this))
this.socket.on('init', this.onInit.bind(this))
this.socket.on('auth_failed', this.onAuthFailed.bind(this))
this.socket.on('user_updated', this.onUserUpdated.bind(this))
this.socket.on('user_item_progress_updated', this.onUserItemProgressUpdated.bind(this))
this.socket.on('playlist_added', this.onPlaylistAdded.bind(this))
@@ -61,6 +61,11 @@ class ServerSocket extends EventEmitter {
this.socket.io.on('reconnect_failed', this.onReconnectFailed.bind(this))
}
sendAuthenticate() {
// Required to connect a socket to a user
this.socket.emit('auth', this.$store.getters['user/getToken'])
}
removeListeners() {
if (!this.socket) return
this.socket.removeAllListeners()
@@ -74,7 +79,7 @@ class ServerSocket extends EventEmitter {
this.connected = true
this.$store.commit('setSocketConnected', true)
this.emit('connection-update', true)
this.socket.emit('auth', this.token) // Required to connect a user with their socket
this.sendAuthenticate()
}
onReconnectAttempt(attemptNumber) {
@@ -101,6 +106,12 @@ class ServerSocket extends EventEmitter {
onInit(data) {
console.log('[SOCKET] Initial socket data received', data)
this.emit('initialized', true)
this.isAuthenticated = true
}
onAuthFailed(data) {
console.log('[SOCKET] Auth failed', data)
this.isAuthenticated = false
}
onUserUpdated(data) {
+107 -16
View File
@@ -1,7 +1,10 @@
import { Browser } from '@capacitor/browser'
import { AbsLogger } from '@/plugins/capacitor'
import { CapacitorHttp } from '@capacitor/core'
export const state = () => ({
user: null,
accessToken: null,
serverConnectionConfig: null,
settings: {
mobileOrderBy: 'addedAt',
@@ -17,7 +20,7 @@ export const getters = {
getIsRoot: (state) => state.user && state.user.type === 'root',
getIsAdminOrUp: (state) => state.user && (state.user.type === 'admin' || state.user.type === 'root'),
getToken: (state) => {
return state.user?.token || null
return state.accessToken || null
},
getServerConnectionConfigId: (state) => {
return state.serverConnectionConfig?.id || null
@@ -28,19 +31,18 @@ export const getters = {
getServerConfigName: (state) => {
return state.serverConnectionConfig?.name || null
},
getCustomHeaders: (state) => {
return state.serverConnectionConfig?.customHeaders || null
},
getUserMediaProgress: (state) => (libraryItemId, episodeId = null) => {
if (!state.user?.mediaProgress) return null
return state.user.mediaProgress.find(li => {
if (episodeId && li.episodeId !== episodeId) return false
return li.libraryItemId == libraryItemId
})
},
getUserMediaProgress:
(state) =>
(libraryItemId, episodeId = null) => {
if (!state.user?.mediaProgress) return null
return state.user.mediaProgress.find((li) => {
if (episodeId && li.episodeId !== episodeId) return false
return li.libraryItemId == libraryItemId
})
},
getUserBookmarksForItem: (state) => (libraryItemId) => {
if (!state?.user?.bookmarks) return []
return state.user.bookmarks.filter(bm => bm.libraryItemId === libraryItemId)
return state.user.bookmarks.filter((bm) => bm.libraryItemId === libraryItemId)
},
getUserSetting: (state) => (key) => {
return state.settings?.[key] || null
@@ -53,6 +55,9 @@ export const getters = {
},
getUserCanDownload: (state) => {
return !!state.user?.permissions?.download
},
getUserCanAccessExplicitContent: (state) => {
return !!state.user?.permissions?.accessExplicitContent
}
}
@@ -132,24 +137,110 @@ export const actions = {
} catch (error) {
console.error('Error opening browser', error)
}
},
async logout({ state, commit }, logoutFromServer = false) {
// Logging out from server deletes the session so the refresh token is no longer valid
// Currently this is not being used to support switching servers without logging back in (assuming refresh token is still valid)
// We may want to make this change in the future
if (state.serverConnectionConfig && logoutFromServer) {
const refreshToken = await this.$db.getRefreshToken(state.serverConnectionConfig.id)
const options = {}
if (refreshToken) {
// Refresh token is used to delete the session on the server
options.headers = {
'x-refresh-token': refreshToken
}
}
// Logout from server
await this.$nativeHttp.post('/logout', null, options).catch((error) => {
console.error('Failed to logout', error)
})
}
await this.$db.logout()
this.$socket.logout()
this.$localStore.removeLastLibraryId()
commit('logout')
commit('libraries/setCurrentLibrary', null, { root: true })
await AbsLogger.info({ tag: 'user', message: `Logged out from server ${state.serverConnectionConfig?.name || 'Not connected'}` })
},
async refreshToken({ getters, commit, state }) {
const refreshToken = await this.$db.getRefreshToken(getters.getServerConnectionConfigId)
if (!refreshToken) {
console.error('No refresh token found')
return null
}
const serverAddress = getters.getServerAddress
const response = await CapacitorHttp.post({
url: `${serverAddress}/auth/refresh`,
headers: {
'Content-Type': 'application/json',
'x-refresh-token': refreshToken
},
data: {}
})
if (response.status !== 200) {
console.error('[user] Token refresh request failed:', response.status)
return null
}
const userResponseData = response.data
if (!userResponseData.user?.accessToken) {
console.error('[user] No access token in refresh response')
return null
}
// Update the config with new tokens
const updatedConfig = {
...state.serverConnectionConfig,
token: userResponseData.user.accessToken,
refreshToken: userResponseData.user.refreshToken
}
// Save updated config to secure storage, persists refresh token in secure storage
const savedConfig = await this.$db.setServerConnectionConfig(updatedConfig)
// Update the store
commit('setAccessToken', userResponseData.user.accessToken)
// Re-authenticate socket if necessary
if (this.$socket?.connected && !this.$socket.isAuthenticated) {
this.$socket.sendAuthenticate()
} else if (!this.$socket) {
console.warn('[user] Socket not available, cannot re-authenticate')
}
if (savedConfig) {
commit('setServerConnectionConfig', savedConfig)
}
return userResponseData.user.accessToken
}
}
export const mutations = {
logout(state) {
state.user = null
state.accessToken = null
state.serverConnectionConfig = null
},
setUser(state, user) {
state.user = user
},
setAccessToken(state, accessToken) {
console.log('[user] setAccessToken', accessToken)
state.accessToken = accessToken
},
removeMediaProgress(state, id) {
if (!state.user) return
state.user.mediaProgress = state.user.mediaProgress.filter(mp => mp.id != id)
state.user.mediaProgress = state.user.mediaProgress.filter((mp) => mp.id != id)
},
updateUserMediaProgress(state, data) {
if (!data || !state.user) return
const mediaProgressIndex = state.user.mediaProgress.findIndex(mp => mp.id === data.id)
const mediaProgressIndex = state.user.mediaProgress.findIndex((mp) => mp.id === data.id)
if (mediaProgressIndex >= 0) {
state.user.mediaProgress.splice(mediaProgressIndex, 1, data)
} else {
@@ -174,9 +265,9 @@ export const mutations = {
},
deleteBookmark(state, { libraryItemId, time }) {
if (!state.user?.bookmarks) return
state.user.bookmarks = state.user.bookmarks.filter(bm => {
state.user.bookmarks = state.user.bookmarks.filter((bm) => {
if (bm.libraryItemId === libraryItemId && bm.time === time) return false
return true
})
}
}
}
+115 -9
View File
@@ -6,12 +6,13 @@
"ButtonCancel": "إلغاء",
"ButtonCancelTimer": "إلغاء المؤقت",
"ButtonClearFilter": "تصفية الفرز",
"ButtonClearLogs": "مسح السجل",
"ButtonCloseFeed": "إغلاق",
"ButtonCollections": "المجموعات",
"ButtonConnect": "اتصل",
"ButtonConnectToServer": "اتصال بالخادم",
"ButtonCreate": "إنشاء",
"ButtonCreateBookmark": "إنشاء إشارة مرجعية",
"ButtonCreateBookmark": "إنشاء علامة مرجعية",
"ButtonCreateNewPlaylist": "إنشاء قائمة تشغيل جديدة",
"ButtonDelete": "حذف",
"ButtonDeleteLocalEpisode": "احذف الحلقة المحلية",
@@ -28,6 +29,7 @@
"ButtonLocalMedia": "الوسائط المحلية",
"ButtonLogs": "سجلات",
"ButtonManageLocalFiles": "إدارة الملفات المحلية",
"ButtonMaskServerAddress": "إخفاء عنوان الخادم",
"ButtonNewFolder": "مجلد جديد",
"ButtonNextEpisode": "الحلقة التالية",
"ButtonOk": "موافق",
@@ -51,6 +53,7 @@
"ButtonStream": "بث",
"ButtonSubmit": "إرسال",
"ButtonSwitchServerUser": "تبديل الخادم/المستخدم",
"ButtonUnmaskServerAddress": "إظهار عنوان الخادم",
"ButtonUserStats": "إحصائيات المستخدم",
"ButtonYes": "نعم",
"HeaderAccount": "الحساب",
@@ -72,7 +75,7 @@
"HeaderLocalFolders": "المجلدات المحلية",
"HeaderLocalLibraryItems": "عناصر المكتبة المحلية",
"HeaderNewPlaylist": "قائمة تشغيل جديدة",
"HeaderOpenRSSFeed": "فتح تغذية RSS",
"HeaderOpenRSSFeed": "عرض تغذية RSS",
"HeaderPlaybackSettings": "إعدادات التشغيل",
"HeaderPlaylist": "قائمة تشغيل",
"HeaderPlaylistItems": "عناصر قائمة التشغيل",
@@ -170,7 +173,7 @@
"LabelLayout": "التنسيق",
"LabelLayoutAuto": "تلقائي",
"LabelLayoutSinglePage": "صفحة واحدة",
"LabelLight": "خفيف/فاتح",
"LabelLight": "فاتح",
"LabelLineSpacing": "تباعد الأسطر",
"LabelListenAgain": "الاستماع مجدداً",
"LabelLocalBooks": "الكتب المحلية",
@@ -190,7 +193,7 @@
"LabelNavigateWithVolumeMirrored": "معكوس",
"LabelNavigateWithVolumeWhilePlaying": "السماح لمفاتيح التحكم بالصوت بالتنقل أثناء التشغيل",
"LabelNavigateWithVolumeWhilePlayingDisabled": "إيقاف",
"LabelNavigateWithVolumeWhilePlayingEnabled": "تشغيل",
"LabelNavigateWithVolumeWhilePlayingEnabled": "مُفعَّل",
"LabelNever": "أبداً",
"LabelNewestAuthors": "أجدد المؤلفين",
"LabelNewestEpisodes": "أجدد الحلقات",
@@ -201,7 +204,7 @@
"LabelNumEpisodesIncomplete": "{0} حلقات، {1} غير مكتمل",
"LabelNumberOfEpisodes": "# من الحلقات",
"LabelOff": "ايقاف",
"LabelOn": "تشغيل",
"LabelOn": "مُفعَّل",
"LabelPassword": "كلمة المرور",
"LabelPath": "مسار",
"LabelPlaybackDirect": "مباشر",
@@ -209,7 +212,7 @@
"LabelPlaybackSpeed": "سرعة التشغيل",
"LabelPlaybackTranscode": "تحويل الترميز",
"LabelPodcast": "مدونة صوتية",
"LabelPodcasts": "مدونات صوتية",
"LabelPodcasts": "بودكاست",
"LabelPreventIndexing": "منع فهرسة تغذيتك بواسطة دليل آيتونز وقوقل بودكاست",
"LabelProgress": "تقدم",
"LabelPubDate": "تاريخ النشر",
@@ -217,8 +220,9 @@
"LabelPublishedDate": "منشور {0}",
"LabelRSSFeedCustomOwnerEmail": "البريد الالكتروني المخصص للمالك",
"LabelRSSFeedCustomOwnerName": "الاسم المخصص للمالك",
"LabelRSSFeedOpen": "موجز RSS مفتوح",
"LabelRSSFeedPreventIndexing": "منع الفهرسة",
"LabelRSSFeedSlug": "رابط تغذية RSS",
"LabelRSSFeedSlug": "اسم تعريف تغذية RSS",
"LabelRandomly": "عشوائياً",
"LabelRead": "اقرأ",
"LabelReadAgain": "اقرأ مرة أخرى",
@@ -235,7 +239,7 @@
"LabelSetEbookAsPrimary": "تعيين كرئيسي",
"LabelSetEbookAsSupplementary": "تعيين كتكميلي",
"LabelShakeSensitivity": "حساسية الرج",
"LabelShowAll": "اظهار الكل",
"LabelShowAll": "إظهار الكل",
"LabelSize": "الحجم",
"LabelSleepTimer": "مؤقت النوم",
"LabelSleepTimerAlmostDoneChime": "تشغيل نغمة عند اقتراب النهاية",
@@ -244,5 +248,107 @@
"LabelStartTime": "وقت البدء",
"LabelStatsBestDay": "أفضل يوم",
"LabelStatsDailyAverage": "المتوسط اليومي",
"LabelStatsDays": "أيام"
"LabelStatsDays": "أيام",
"LabelStatsDaysListened": "أيام الاستماع",
"LabelStatsInARow": "على التوالي",
"LabelStatsItemsFinished": "العناصر المنتهية",
"LabelStatsMinutes": "دقائق",
"LabelStatsMinutesListening": "دقائق الاستماع",
"LabelStatsWeekListening": "استماع هذا الأسبوع",
"LabelStreamingUsingCellular": "البث عبر شبكة الجوال",
"LabelTag": "علامة",
"LabelTags": "علامات",
"LabelTheme": "النمط",
"LabelThemeBlack": "أسود",
"LabelThemeDark": "غامق",
"LabelThemeLight": "فاتح",
"LabelTimeRemaining": "{0} متبقية",
"LabelTitle": "عنوان",
"LabelTotalSize": "الحجم الكلي",
"LabelTotalTrack": "إجمالي المسارات",
"LabelTracks": "المسارات",
"LabelType": "نوع",
"LabelUnknown": "مجهول",
"LabelUnlockPlayer": "تفعيل المشغل",
"LabelUseBookshelfView": "تفعيل عرض الرف",
"LabelUser": "مستخدم",
"LabelUsername": "اسم المستخدم",
"LabelVeryHigh": "عالي جدًا",
"LabelVeryLow": "منخفض جدًا",
"LabelYearReviewHide": "إخفاء ملخص العام",
"LabelYearReviewShow": "عرض ملخص العام",
"LabelYourBookmarks": "علاماتك المرجعية",
"LabelYourProgress": "تقدمك",
"MessageAndroid10Downloads": "سيستخدم نظام أندرويد 10 والإصدارات الأقدم وحدة التخزين الداخلية.",
"MessageAttemptingServerConnection": "جاري محاولة الاتصال بالخادم...",
"MessageAudiobookshelfServerNotConnected": "خادم Audiobookshelf غير متصل",
"MessageAudiobookshelfServerRequired": "<strong>مهم!</strong> هذا التطبيق مصمم للعمل مع خادم Audiobookshelf تستضيفه أنت أو شخص تعرفه. هذا التطبيق لا يقدم أي محتوى.",
"MessageBookshelfEmpty": "الرف فارغ",
"MessageConfirmDeleteLocalEpisode": "هل تريد بالتأكيد إزالة الحلقة المحلية \"{0}\" من جهازك؟ لن يتأثر الملف الموجود على الخادم.",
"MessageConfirmDeleteLocalFiles": "هل تريد بالتأكيد إزالة الملفات المحلية لهذا العنصر من جهازك؟ الملفات الموجودة على الخادم وتقدمك لن يتأثرا.",
"MessageConfirmDisableAutoTimer": "هل أنت متأكد أنك تريد تعطيل المؤقت التلقائي لبقية اليوم؟ سيتم إعادة تفعيل المؤقت في نهاية فترة مؤقت النوم التلقائي هذه، أو إذا قمت بإعادة تشغيل التطبيق.",
"MessageConfirmDiscardProgress": "هل أنت متأكد أنك تريد إعادة تعيين تقدمك؟",
"MessageConfirmDownloadUsingCellular": "أنت على وشك التنزيل باستخدام بيانات الجوال. قد يشمل ذلك رسوم استخدام بيانات من شركة الاتصالات. هل ترغب في المتابعة؟",
"MessageConfirmMarkAsFinished": "هل أنت متأكد أنك تريد وضع علامة \"مكتمل\" على هذا العنصر؟",
"MessageConfirmRemoveBookmark": "هل أنت متأكد أنك تريد إزالة العلامة المرجعية؟",
"MessageConfirmStreamingUsingCellular": "أنت على وشك البث باستخدام بيانات الجوال. قد يشمل ذلك رسوم استخدام بيانات من شركة الاتصالات. هل ترغب في المتابعة؟",
"MessageDiscardProgress": "إلغاء التقدم",
"MessageDownloadCompleteProcessing": "اكتمل التنزيل. جاري المعالجة...",
"MessageDownloading": "جاري التنزيل...",
"MessageDownloadingEpisode": "جاري تنزيل الحلقة",
"MessageEpisodesQueuedForDownload": "تمت إضافة {0} حلقة (حلقات) إلى قائمة انتظار التنزيل",
"MessageFeedURLWillBe": "سيكون رابط التغذية هو {0}",
"MessageFetching": "جاري الجلب...",
"MessageFollowTheProjectOnGithub": "تابع المشروع على Github",
"MessageItemDownloadCompleteFailedToCreate": "اكتمل تنزيل العنصر ولكن فشل إنشاء عنصر المكتبة",
"MessageItemMissing": "العنصر مفقود ويجب إصلاحه على الخادم. عادةً ما يتم وضع علامة \"مفقود\" على العنصر لأن مسارات الملفات غير قابلة للوصول.",
"MessageLoading": "جاري التحميل...",
"MessageLoadingServerData": "جاري تحميل بيانات الخادم...",
"MessageLocalFolderDescription": "\"وحدة تخزين التطبيق الداخلية\" لا يمكن الوصول إليها إلا بواسطة هذا التطبيق. هذا التطبيق يدعم فقط الوسائط التي تم تنزيلها مباشرةً من خلال التطبيق. يمكن استخدام مجلدات التخزين المشتركة للسماح لتطبيقات أخرى بالوصول إلى الوسائط التي تم تنزيلها بواسطة هذا التطبيق.",
"MessageMarkAsFinished": "وضع علامة \"تم الإنتهاء\"",
"MessageMediaLinkedToADifferentServer": "الوسائط مرتبطة بخادم Audiobookshelf على عنوان مختلف ({0}). ستتم مزامنة التقدم عند الاتصال بعنوان هذا الخادم.",
"MessageMediaLinkedToADifferentUser": "الوسائط مرتبطة بهذا الخادم ولكن تم تنزيلها بواسطة مستخدم مختلف. ستتم مزامنة التقدم فقط مع المستخدم الذي قام بتنزيلها.",
"MessageMediaLinkedToServer": "مرتبط بالخادم {0}",
"MessageMediaLinkedToThisServer": "الوسائط التي تم تنزيلها مرتبطة بهذا الخادم",
"MessageMediaNotLinkedToServer": "الوسائط غير مرتبطة بخادم Audiobookshelf. لن تتم مزامنة أي تقدم.",
"MessageNoBookmarks": "لا توجد علامات مرجعية",
"MessageNoChapters": "لا توجد فصول",
"MessageNoCollections": "لا توجد مجموعات",
"MessageNoItems": "لا توجد عناصر",
"MessageNoItemsFound": "لم يتم العثور على عناصر",
"MessageNoListeningSessions": "لا توجد جلسات استماع",
"MessageNoLogs": "لا توجد سجلات",
"MessageNoMediaFolders": "لا توجد مجلدات وسائط",
"MessageNoNetworkConnection": "لا يوجد اتصال بالشبكة",
"MessageNoPodcastsFound": "لم يتم العثور على أي بودكاست",
"MessageNoSeries": "لا توجد سلاسل",
"MessageNoUpdatesWereNecessary": "لا حاجة لأي تحديثات",
"MessageNoUserPlaylists": "ليس لديك أي قوائم تشغيل",
"MessageOldServerConnectionWarning": "تكوين اتصال الخادم يستخدم معرّف مستخدم قديم. يرجى حذف اتصال الخادم هذا وإعادة إضافته.",
"MessageOldServerConnectionWarningHelp": "لقد قمتَ بإعداد الاتصال بهذا الخادم قبل ترحيل قاعدة البيانات في الإصدار 2.3.0، الصادر في يونيو 2023. سيؤدي تحديث الخادم المستقبلي إلى إلغاء إمكانية تسجيل الدخول باستخدام هذا الاتصال القديم. يُرجى حذف اتصال الخادم الحالي والاتصال مرة أخرى (باستخدام نفس عنوان الخادم وبيانات الاعتماد). إذا كانت لديك أي وسائط مُنزّلة على هذا الجهاز، فيجب تنزيلها مرة أخرى لمزامنتها مع الخادم.",
"MessagePodcastSearchField": "أدخل مصطلح البحث أو عنوان URL الخاص بتغذية RSS",
"MessageProgressSyncFailed": "فشلت آخر محاولة لإرسال تقدم استماعك إلى الخادم. ستستمر محاولات مزامنة التقدم كل 15 ثانية إلى دقيقة واحدة أثناء تشغيل الوسائط.",
"MessageReportBugsAndContribute": "أبلغ عن الأخطاء، واطلب الميزات، وساهم في",
"MessageSeriesAlreadyDownloaded": "لقد قمت بالفعل بتنزيل جميع الكتب في هذه السلسلة.",
"MessageSeriesDownloadConfirm": "هل تريد تنزيل {0} كتابًا (كتب) مفقودًا (مفقودة) مع {1} ملفًا (ملفات)، بإجمالي {2} إلى المجلد {3}؟",
"MessageSeriesDownloadConfirmIos": "هل تريد تنزيل {0} كتابًا (كتب) مفقودًا (مفقودة) مع {1} ملفًا (ملفات)، بإجمالي {2}؟",
"MessageSocketConnectedOverMeteredCellular": "تم توصيل المقبس عبر شبكة الجوال ذات الاستهلاك المحدود",
"MessageSocketConnectedOverMeteredWifi": "تم توصيل المقبس عبر شبكة الواي فاي ذات الاستهلاك المحدود",
"MessageSocketConnectedOverUnmeteredCellular": "تم توصيل المقبس عبر شبكة الجوال غير ذات الاستهلاك المحدود",
"MessageSocketConnectedOverUnmeteredWifi": "المقبس متصل عبر شبكة واي فاي غير محدودة",
"MessageSocketNotConnected": "المقبس غير متصل",
"NoteRSSFeedPodcastAppsHttps": "تحذير: تتطلب معظم تطبيقات البث الصوتي أن يكون عنوان URL الخاص بتغذية RSS يستخدم HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "تحذير: حلقة واحدة أو أكثر من حلقاتك ليس لها تاريخ نشر. بعض تطبيقات البودكاست تتطلب هذا.",
"ToastBookmarkCreateFailed": "فشل في إنشاء الإشارة المرجعية",
"ToastBookmarkRemoveFailed": "فشل في إزالة الإشارة المرجعية",
"ToastBookmarkUpdateFailed": "فشل في تحديث الإشارة المرجعية",
"ToastDownloadNotAllowedOnCellular": "لا يسمح بالتنزيل على البيانات الخلوية",
"ToastItemMarkedAsFinishedFailed": "فشل وضع علامة \"مكتمل\"",
"ToastItemMarkedAsNotFinishedFailed": "فشل وضع علامة \"غير مكتمل\"",
"ToastPlaylistCreateFailed": "فشل إنشاء قائمة التشغيل",
"ToastPodcastCreateFailed": "فشل إنشاء البودكاست",
"ToastPodcastCreateSuccess": "تم إنشاء البودكاست بنجاح",
"ToastRSSFeedCloseFailed": "فشل إغلاق مغذّي RSS",
"ToastRSSFeedCloseSuccess": "تم إغلاق مغذّي RSS",
"ToastStreamingNotAllowedOnCellular": "لا يُسمح بالبث عبر البيانات الخلوية"
}
+14 -1
View File
@@ -6,6 +6,7 @@
"ButtonCancel": "Скасаваць",
"ButtonCancelTimer": "Скасаваць таймер",
"ButtonClearFilter": "Ачысціць фільтр",
"ButtonClearLogs": "Ачысціць журналы",
"ButtonCloseFeed": "Закрыць стужку",
"ButtonCollections": "Калекцыі",
"ButtonConnect": "Падлучыцца",
@@ -26,7 +27,9 @@
"ButtonLatest": "Апошняе",
"ButtonLibrary": "Бібліятэка",
"ButtonLocalMedia": "Лакальныя медыя",
"ButtonLogs": "Журналы",
"ButtonManageLocalFiles": "Кіраванне лакальнымі файламі",
"ButtonMaskServerAddress": "Схаваць адрас сервера",
"ButtonNewFolder": "Новая тэчка",
"ButtonNextEpisode": "Наступны эпізод",
"ButtonOk": "Добра",
@@ -50,6 +53,7 @@
"ButtonStream": "Трансляцыя",
"ButtonSubmit": "Адправіць",
"ButtonSwitchServerUser": "Змяніць Сервер/Карыстальніка",
"ButtonUnmaskServerAddress": "Паказаць адрас сервера",
"ButtonUserStats": "Статыстыка карыстальніка",
"ButtonYes": "Так",
"HeaderAccount": "Уліковы запіс",
@@ -78,7 +82,7 @@
"HeaderProgressSyncFailed": "Не ўдалося сінхранізаваць прагрэс",
"HeaderRSSFeed": "RSS-стужка",
"HeaderRSSFeedGeneral": "Падрабязнасці RSS",
"HeaderRSSFeedIsOpen": "RSS-стужка адкрыта",
"HeaderRSSFeedIsOpen": "RSS-стужка адкрытая",
"HeaderSelectDownloadLocation": "Абраць месцазнаходжання для спампоўкі",
"HeaderSettings": "Налады",
"HeaderSleepTimer": "Таймер сну",
@@ -142,6 +146,7 @@
"LabelEndOfChapter": "Канец раздзела",
"LabelEndTime": "Час заканчэння",
"LabelEpisode": "Эпізод",
"LabelExplicit": "Відверты",
"LabelFeedURL": "URL стужкі",
"LabelFile": "Файл",
"LabelFileBirthtime": "Час стварэння файла",
@@ -216,6 +221,7 @@
"LabelPublishedDate": "Апублікавана {0}",
"LabelRSSFeedCustomOwnerEmail": "Карыстальніцкая электронная пошта ўладальніка",
"LabelRSSFeedCustomOwnerName": "Карыстальніцкае імя ўладальніка",
"LabelRSSFeedOpen": "RSS-стужка адкрыта",
"LabelRSSFeedPreventIndexing": "Прадухіліць індэксацыю",
"LabelRSSFeedSlug": "Ідэнтыфікатар RSS-стужкі",
"LabelRandomly": "Выпадкова",
@@ -237,6 +243,8 @@
"LabelShowAll": "Паказаць усё",
"LabelSize": "Памер",
"LabelSleepTimer": "Таймер сну",
"LabelSleepTimerAlmostDoneChime": "Праіграйце звонак, калі амаль скончана",
"LabelSleepTimerAlmostDoneChimeHelp": "Памякчыць звонак, калі на таймеры сну застанецца 30 секунд",
"LabelStart": "Пачаць",
"LabelStartTime": "Час пачатку",
"LabelStatsBestDay": "Лепшы дзень",
@@ -252,6 +260,7 @@
"LabelTag": "Метка",
"LabelTags": "Меткі",
"LabelTheme": "Тэма",
"LabelThemeBlack": "Чорны",
"LabelThemeDark": "Цёмная",
"LabelThemeLight": "Светлая",
"LabelTimeRemaining": "Засталося {0}",
@@ -309,12 +318,16 @@
"MessageNoItems": "Няма элементаў",
"MessageNoItemsFound": "Элементы не знойдзены",
"MessageNoListeningSessions": "Няма сеансаў праслухоўвання",
"MessageNoLogs": "Няма журналаў",
"MessageNoMediaFolders": "Няма тэчак з медыя",
"MessageNoNetworkConnection": "Няма падлучэння да сеткі",
"MessageNoPodcastsFound": "Падкасты не знойдзены",
"MessageNoSeries": "Няма серый",
"MessageNoUpdatesWereNecessary": "Абнаўленні не патрабаваліся",
"MessageNoUserPlaylists": "У вас няма спісаў прайгравання",
"MessageOldServerAuthReLoginRequired": "Аўтэнтыфікацыя была палепшана для бяспекі ў серверы v2.26.0. Усім карыстальнікам патрабуецца паўторна ўвайсці ў сістэму.",
"MessageOldServerAuthWarning": "Сервер выкарыстоўвае састарэлую аўтэнтыфікацыю",
"MessageOldServerAuthWarningHelp": "Гэты сервер працуе пад версіяй, старэйшай за 2.26.0. У версіі 2.26.0 была дададзена больш бяспечная сістэма аўтэнтыфікацыі. Настойліва рэкамендуецца абнавіць сервер да апошняй версіі. Калі вы ўжо абнавілі сервер, увайдзіце зноў, каб выкарыстоўваць новую аўтэнтыфікацыю.",
"MessageOldServerConnectionWarning": "Канфігурацыя падлучэння да сервера выкарыстоўвае стары ідэнтыфікатар карыстальніка. Калі ласка, выдаліце і зноў дадайце гэта падлучэнне да сервера.",
"MessageOldServerConnectionWarningHelp": "Вы першапачаткова наладзілі падключэнне да гэтага сервера да міграцыі базы даных у версіі 2.3.0, якая выйшла ў чэрвені 2023 года. У будучым абнаўленні сервера магчымасць уваходу праз гэтае старое падключэнне будзе выдалена. Калі ласка, выдаліце існуючае падключэнне да сервера і падключыцеся зноў (выкарыстоўваючы той жа адрас сервера і ўліковыя даныя). Калі на гэтай прыладзе ёсць спампаваныя медыяфайлы, іх трэба будзе спампаваць зноў для сінхранізацыі з серверам.",
"MessagePodcastSearchField": "Увядзіце пошукавы запыт або URL RSS-стужкі",
+16 -1
View File
@@ -6,6 +6,7 @@
"ButtonCancel": "Отказ",
"ButtonCancelTimer": "Отмени Таймер",
"ButtonClearFilter": "Изчисти филтър",
"ButtonClearLogs": "Изчисти логове",
"ButtonCloseFeed": "Затвори стената",
"ButtonCollections": "Колекции",
"ButtonConnect": "Свържи се",
@@ -26,7 +27,9 @@
"ButtonLatest": "Последни",
"ButtonLibrary": "Библиотека",
"ButtonLocalMedia": "Локална медия",
"ButtonLogs": "Логове",
"ButtonManageLocalFiles": "Управление на локални файлове",
"ButtonMaskServerAddress": "Маскирай адреса на сървърът",
"ButtonNewFolder": "Създай нова папка",
"ButtonNextEpisode": "Следващ епизод",
"ButtonOk": "Приемам",
@@ -50,6 +53,7 @@
"ButtonStream": "Стрийм",
"ButtonSubmit": "Изпрати",
"ButtonSwitchServerUser": "Смени сървър/потребител",
"ButtonUnmaskServerAddress": "Демаскирай адреса на съвърът",
"ButtonUserStats": "Потребителски статистики",
"ButtonYes": "Да",
"HeaderAccount": "Профил",
@@ -111,7 +115,7 @@
"LabelBooks": "Книги",
"LabelChapterTrack": "Трак на глава",
"LabelChapters": "Глави",
"LabelClosePlayer": "Затвори",
"LabelClosePlayer": "Затвори плейъра",
"LabelCollapseSeries": "Скрий сериите",
"LabelComplete": "Приключено",
"LabelContinueBooks": "Продължи книги",
@@ -142,6 +146,7 @@
"LabelEndOfChapter": "Край на глава",
"LabelEndTime": "Оставащо време",
"LabelEpisode": "Епизод",
"LabelExplicit": "Експлицитно",
"LabelFeedURL": "URL на емисия",
"LabelFile": "Файл",
"LabelFileBirthtime": "Дата на създаване на файла",
@@ -216,6 +221,7 @@
"LabelPublishedDate": "Публикувани {0}",
"LabelRSSFeedCustomOwnerEmail": "Персонализиран имейл на собственика",
"LabelRSSFeedCustomOwnerName": "Персонализирано име на собственика",
"LabelRSSFeedOpen": "RSS Feed е отворен",
"LabelRSSFeedPreventIndexing": "Предотвратете индексиране",
"LabelRSSFeedSlug": "идентификатор на RSS емисия",
"LabelRandomly": "Случайно",
@@ -237,6 +243,8 @@
"LabelShowAll": "Покажи всички",
"LabelSize": "Размер",
"LabelSleepTimer": "Таймер за изключване",
"LabelSleepTimerAlmostDoneChime": "Възпроизведи звуков сигнал малко преди да приключи",
"LabelSleepTimerAlmostDoneChimeHelp": "Възпроизведи звуков сигнал, 30 секунди преди изтичане на таймера за заспиване",
"LabelStart": "Старт",
"LabelStartTime": "Време на стартиране",
"LabelStatsBestDay": "Най-добър ден",
@@ -252,6 +260,7 @@
"LabelTag": "Таг",
"LabelTags": "Тагове",
"LabelTheme": "Тема",
"LabelThemeBlack": "Черен",
"LabelThemeDark": "Тъмна",
"LabelThemeLight": "Светла",
"LabelTimeRemaining": "{0} оставащи",
@@ -289,6 +298,7 @@
"MessageDownloading": "Сваляне...",
"MessageDownloadingEpisode": "Сваля епизод",
"MessageEpisodesQueuedForDownload": "{0} Епизод(и) са сложени за сваляне",
"MessageFailedToRefreshToken": "Неуспешно опресняване на токена, налага се да се впишете отново",
"MessageFeedURLWillBe": "Адресът на емисията ще бъде {0}",
"MessageFetching": "Извличане...",
"MessageFollowTheProjectOnGithub": "Последвайте проекта в Github",
@@ -296,6 +306,7 @@
"MessageItemMissing": "Елементът липсва и трябва да бъде поправен на сървъра. Обикновено един елемент се маркира като липсващ, защото пътищата към файловете не са достъпни.",
"MessageLoading": "Зарежда...",
"MessageLoadingServerData": "Зарежда данни от сървъра...",
"MessageLocalFolderDescription": "'Вътрешното хранилище на приложението' е достъпно само през това приложение. Това приложение поддържа само медийни файлове, изтеглени директно през него. Споделените папки за съхранение могат да се използват, за да се позволи на други приложения да имат достъп до медийните файлове, изтеглени чрез това приложение.",
"MessageMarkAsFinished": "Маркирай като завършено",
"MessageMediaLinkedToADifferentServer": "Медията е свързана със сървър на Audiobookshelf на различен адрес ({0}). Напредъкът ще бъде синхронизиран, когато се свържете с този сървърен адрес.",
"MessageMediaLinkedToADifferentUser": "Медията е свързана с този сървър, но е изтеглена от друг потребител. Напредъкът ще бъде синхронизиран само с потребителя, който я е изтеглил.",
@@ -308,12 +319,16 @@
"MessageNoItems": "Няма елементи",
"MessageNoItemsFound": "Няма намерени елементи",
"MessageNoListeningSessions": "Няма сесии за слушане",
"MessageNoLogs": "Няма логове",
"MessageNoMediaFolders": "Няма медии папки",
"MessageNoNetworkConnection": "Няма връзка с интернет",
"MessageNoPodcastsFound": "Няма намерени подкасти",
"MessageNoSeries": "Няма серии",
"MessageNoUpdatesWereNecessary": "Няма нужда от обновяване",
"MessageNoUserPlaylists": "Нямате създадени плейлисти",
"MessageOldServerAuthReLoginRequired": "За осигуряването на по-добра сигурност, автентикация беше подобрена във v.2.26.0. Всеки потребител ще трябва да се автентикира наново.",
"MessageOldServerAuthWarning": "Сървърът използва остаряла форма на автентикация",
"MessageOldServerAuthWarningHelp": "Този сървър е на версия, която е по-стара от v2.26.0. Във версия v2.26.0 беше добавена по-сигурна автентикация. Силно е препоръчително да обновите сървъра до последната версия. Аковече сте обновили сървъра, влезде наново, за да ползвате новата автентикация.",
"MessageOldServerConnectionWarning": "Конфигурацията за връзка със сървъра използва старо потребителско ID. Моля, изтрийте и добавете отново тази връзка със сървъра.",
"MessageOldServerConnectionWarningHelp": "Първоначално сте настроили връзката към този сървър преди миграцията на базата данни във версия 2.3.0, пусната през юни 2023 г. Бъдеща актуализация на сървъра ще премахне възможността за влизане с тази стара връзка. Моля, изтрийте съществуващата връзка със сървъра и се свържете отново (използвайки същия сървърен адрес и идентификационни данни). Ако имате изтеглена медия на това устройство, тя ще трябва да бъде изтеглена отново, за да се синхронизира със сървъра.",
"MessagePodcastSearchField": "Въведи какво да търся или RSS емисия адрес",
+5 -1
View File
@@ -203,6 +203,8 @@
"LabelProgress": "Progrés",
"LabelPubDate": "Data de publicació",
"LabelPublishYear": "Any de publicació",
"LabelRSSFeedPreventIndexing": "Evita la indexació",
"LabelRSSFeedSlug": "URL semàntic del canal RSS",
"LabelRandomly": "A l'atzar",
"LabelRead": "Llegit",
"LabelReadAgain": "Tornar a llegir",
@@ -210,7 +212,7 @@
"LabelRecentlyAdded": "Addicions recents",
"LabelRemoveFromPlaylist": "Eliminar de la llista de reproducció",
"LabelSelectADevice": "Seleccioneu un dispositiu",
"LabelSeries": "Sèries",
"LabelSeries": "Sèrie",
"LabelServerAddress": "Adreça del servidor",
"LabelShowAll": "Mostra-ho tot",
"LabelSize": "Mida",
@@ -269,6 +271,8 @@
"MessageNoUpdatesWereNecessary": "No calia cap actualització",
"MessageNoUserPlaylists": "No teniu cap llista de reproducció",
"MessagePodcastSearchField": "Introduïu el terme de cerca o l'URL del canal RSS",
"NoteRSSFeedPodcastAppsHttps": "Avís: la majoria d'aplicacions de pòdcast requereixen que l'URL del canal RSS utilitzi HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Avís: un o més dels vostres episodis no tenen data de publicació. Algunes aplicacions de pòdcast ho requereixen.",
"ToastItemMarkedAsFinishedFailed": "No s'ha pogut marcar com a finalitzat",
"ToastItemMarkedAsNotFinishedFailed": "No s'ha pogut marcar com a no finalitzat",
"ToastPlaylistCreateFailed": "No s'ha pogut crear la llista de reproducció",
+9 -3
View File
@@ -27,7 +27,7 @@
"ButtonLatest": "Nejnovější",
"ButtonLibrary": "Knihovna",
"ButtonLocalMedia": "Místní média",
"ButtonLogs": "Záznamy",
"ButtonLogs": "Logy",
"ButtonManageLocalFiles": "Spravovat místní soubory",
"ButtonMaskServerAddress": "Skrýt adresu serveru",
"ButtonNewFolder": "Nová složka",
@@ -146,6 +146,7 @@
"LabelEndOfChapter": "Konec kapitoly",
"LabelEndTime": "Do",
"LabelEpisode": "Epizoda",
"LabelExplicit": "Explicitně",
"LabelFeedURL": "URL kanálu",
"LabelFile": "Soubor",
"LabelFileBirthtime": "Čas vytvoření souboru",
@@ -220,6 +221,7 @@
"LabelPublishedDate": "Vydáno {0}",
"LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka",
"LabelRSSFeedCustomOwnerName": "Vlastní jméno vlastníka",
"LabelRSSFeedOpen": "RSS kanál otevřen",
"LabelRSSFeedPreventIndexing": "Zabránit indexování",
"LabelRSSFeedSlug": "Klíčové slovo kanálu RSS",
"LabelRandomly": "Náhodně",
@@ -296,6 +298,7 @@
"MessageDownloading": "Stahuje se...",
"MessageDownloadingEpisode": "Stahuji epizodu",
"MessageEpisodesQueuedForDownload": "{0} Epizody zařazené do fronty ke stažení",
"MessageFailedToRefreshToken": "Nepovedlo se obnovit token, je nutné se znovu přihlásit",
"MessageFeedURLWillBe": "URL zdroje bude {0}",
"MessageFetching": "Načítání...",
"MessageFollowTheProjectOnGithub": "Sledujte projekt na GitHubu",
@@ -316,20 +319,23 @@
"MessageNoItems": "Žádné položky",
"MessageNoItemsFound": "Nebyly nalezeny žádné položky",
"MessageNoListeningSessions": "Žádné poslechové relace",
"MessageNoLogs": "Žádné logy",
"MessageNoLogs": "Žádné záznamy událostí",
"MessageNoMediaFolders": "Žádné složky médií",
"MessageNoNetworkConnection": "Žádné připojení k síti",
"MessageNoPodcastsFound": "Nebyly nalezeny žádné podcasty",
"MessageNoSeries": "Žádné série",
"MessageNoUpdatesWereNecessary": "Nebyly nutné žádné aktualizace",
"MessageNoUserPlaylists": "Nemáte žádné seznamy skladeb",
"MessageOldServerAuthReLoginRequired": "V rámci aktualizace serveru v2.26.0 bylo vylepšeno zabezpečení autentizace. Všichni uživatelé se musí znovu přihlásit.",
"MessageOldServerAuthWarning": "Server používá zastaralou autentizaci",
"MessageOldServerAuthWarningHelp": "Server běží na verzi starší než v2.26.0. Bezpečnější způsob autentizace byl přidán ve verzi v2.26.0. Důrazně se doporučuje aktualizovat server na nejnovější verzi. Pokud jste již server aktualizovali, znovu se přihlašte pro použití nové autentizace.",
"MessageOldServerConnectionWarning": "Konfigurace připojení k serveru používá staré ID uživatele. Odstraňte a znovu přidejte toto připojení k serveru.",
"MessageOldServerConnectionWarningHelp": "Připojení k tomuto serveru jste původně nastavili před migrací databáze ve verzi 2.3.0 vydané v červnu 2023. Budoucí aktualizace serveru odstraní možnost přihlášení pomocí tohoto starého připojení. Odstraňte prosím stávající připojení k serveru a připojte se znovu (pomocí stejné adresy serveru a přihlašovacích údajů). Pokud máte v tomto zařízení stažená média, bude nutné je pro synchronizaci se serverem stáhnout znovu.",
"MessagePodcastSearchField": "Zadejte hledaný pojem pro RSS feed URL",
"MessageProgressSyncFailed": "Poslední pokus o nahlášení průběhu poslechu na server se nezdařil. Během přehrávání médií se budou požadavky na synchronizaci pokroku nadále pokoušet každých 15 až 60 sekund.",
"MessageReportBugsAndContribute": "Nahlašte chyby, vyžádejte si funkce a přispěte na",
"MessageSeriesAlreadyDownloaded": "Všechny knihy v této sérii jste již stáhli.",
"MessageSeriesDownloadConfirm": "Stáhnout chybějící {0} knihu(y) s {1} souborem(y), celkem {2} do složky {3}?",
"MessageSeriesDownloadConfirm": "Stáhnout {0} chybějících knih s {1} souborem/soubory, celkem {2}, do složky {3}?",
"MessageSeriesDownloadConfirmIos": "Stáhnout chybějící {0} knihu(y) s {1} souborem(y), celkem {2}?",
"MessageSocketConnectedOverMeteredCellular": "Socket připojen přes měřenou mobilní síť",
"MessageSocketConnectedOverMeteredWifi": "Socket připojen přes měřenou Wi-Fi",
+12
View File
@@ -6,6 +6,7 @@
"ButtonCancel": "Annuller",
"ButtonCancelTimer": "Stop timer",
"ButtonClearFilter": "Ryd filter",
"ButtonClearLogs": "Rens Logfiler",
"ButtonCloseFeed": "Luk feed",
"ButtonCollections": "Samlinger",
"ButtonConnect": "Forbind",
@@ -26,7 +27,9 @@
"ButtonLatest": "Seneste",
"ButtonLibrary": "Bibliotek",
"ButtonLocalMedia": "Lokalt medie",
"ButtonLogs": "Logfiler",
"ButtonManageLocalFiles": "Administrér Lokale Filer",
"ButtonMaskServerAddress": "Maskér server adresse",
"ButtonNewFolder": "Ny Folder",
"ButtonNextEpisode": "Næste Afsnit",
"ButtonOk": "Ok",
@@ -50,6 +53,7 @@
"ButtonStream": "Stream",
"ButtonSubmit": "Send",
"ButtonSwitchServerUser": "Skift Server/Bruger",
"ButtonUnmaskServerAddress": "Afmaskér server adresse",
"ButtonUserStats": "Bruger statitstik",
"ButtonYes": "Ja",
"HeaderAccount": "Konto",
@@ -142,6 +146,7 @@
"LabelEndOfChapter": "Slutningen af kapitel",
"LabelEndTime": "Sluttidspunkt",
"LabelEpisode": "Afsnit",
"LabelExplicit": "Eksplisit",
"LabelFeedURL": "Feed URL",
"LabelFile": "Fil",
"LabelFileBirthtime": "Oprettelsestidspunkt for fil",
@@ -216,6 +221,7 @@
"LabelPublishedDate": "Publiceret {0}",
"LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail",
"LabelRSSFeedCustomOwnerName": "Brugerdefineret ejerens navn",
"LabelRSSFeedOpen": "RSS-feed åbent",
"LabelRSSFeedPreventIndexing": "Forhindrer indeksering",
"LabelRSSFeedSlug": "RSS-feed-slug",
"LabelRandomly": "Tilfældigt",
@@ -237,6 +243,8 @@
"LabelShowAll": "Vis alle",
"LabelSize": "Størrelse",
"LabelSleepTimer": "Søvntimer",
"LabelSleepTimerAlmostDoneChime": "Afspil lyd når snart færdig",
"LabelSleepTimerAlmostDoneChimeHelp": "Afspil lyd når sove timeren har 30 sekunder tilbage",
"LabelStart": "Start",
"LabelStartTime": "Starttid",
"LabelStatsBestDay": "Bedste dag",
@@ -310,12 +318,16 @@
"MessageNoItems": "Ingen elementer",
"MessageNoItemsFound": "Ingen elementer fundet",
"MessageNoListeningSessions": "Ingen lyttesessioner",
"MessageNoLogs": "Ingen logfiler",
"MessageNoMediaFolders": "Ingen Mediemapper",
"MessageNoNetworkConnection": "Ingen Internetforbindelse",
"MessageNoPodcastsFound": "Ingen podcasts fundet",
"MessageNoSeries": "Ingen serier",
"MessageNoUpdatesWereNecessary": "Ingen opdateringer var nødvendige",
"MessageNoUserPlaylists": "Du har ingen afspilningslister",
"MessageOldServerAuthReLoginRequired": "Autentificering er blevet forbedret af sikkerhedsmæssige årsager i server v2.26.0. Alle brugere skal logge ind igen.",
"MessageOldServerAuthWarning": "Serveren bruger forældet autentificering",
"MessageOldServerAuthWarningHelp": "Denne server kører en version ældre end v2.26.0. Et mere sikkert autentificeringssystem blev tilføjet i v2.26.0. Det anbefales kraftigt at opdatere serveren til den nyeste version. Hvis du allerede har opdateret serveren, skal du logge ind igen for at bruge den nye autentificering.",
"MessageOldServerConnectionWarning": "Server forbindelseskonfiguration anvender et gammelt bruger ID. Fjern venligst og gentilføj denne server forbindelse.",
"MessageOldServerConnectionWarningHelp": "Du har oprindeligt sat forbindelsen til denne server op før databasemigreringen i 2.3.0, udgivet i juni 2023. En senere serveropdatering vil fjerne muligheden for at logge ind med genne gamle metode. Sørg venligst for at slette den eksisterende serverforbindelse og opret forbindelse igen (ved anvendelse af den samme server adresse og login oplysninger). Hvis du har downloaded noget medie på denne enhed vil dette være nødsaget til at bive downloaded igen efter serversynkronisering.",
"MessagePodcastSearchField": "Indtast søgeterm eller RSS URL",
+19 -13
View File
@@ -12,27 +12,27 @@
"ButtonConnect": "Verbinden",
"ButtonConnectToServer": "Mit Server verbinden",
"ButtonCreate": "Erstellen",
"ButtonCreateBookmark": "Erstelle Lesezeichen",
"ButtonCreateNewPlaylist": "Erstelle neue Wiedergabeliste",
"ButtonCreateBookmark": "Lesezeichen erstellen",
"ButtonCreateNewPlaylist": "Neue Wiedergabeliste erstellen",
"ButtonDelete": "Löschen",
"ButtonDeleteLocalEpisode": "Lösche lokale Episode",
"ButtonDeleteLocalFile": "Lösche lokale Datei",
"ButtonDeleteLocalItem": "Lösche lokales Element",
"ButtonDisableAutoTimer": "Deaktiviere automatischen Timer",
"ButtonDeleteLocalEpisode": "Lokale Episode löschen",
"ButtonDeleteLocalFile": "Lokale Datei löschen",
"ButtonDeleteLocalItem": "Lokales Element löschen",
"ButtonDisableAutoTimer": "Automatischen Timer deaktivieren",
"ButtonDisconnect": "Trennen",
"ButtonGoToWebClient": "Gehe zum Web Client",
"ButtonHistory": "Historie",
"ButtonGoToWebClient": "Gehe zum Webclient",
"ButtonHistory": "Verlauf",
"ButtonHome": "Startseite",
"ButtonIssues": "Probleme",
"ButtonLatest": "Neueste",
"ButtonLibrary": "Bibliothek",
"ButtonLocalMedia": "Lokale Medien",
"ButtonLogs": "Protokolle",
"ButtonManageLocalFiles": "Verwalte lokale Dateien",
"ButtonManageLocalFiles": "Lokale Dateien verwalten",
"ButtonMaskServerAddress": "Server Adresse verstecken",
"ButtonNewFolder": "Neuer Ordner",
"ButtonNextEpisode": "Nächste Episode",
"ButtonOk": "Einverstanden",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Feed öffnen",
"ButtonOverride": "Überschreiben",
"ButtonPause": "Pausieren",
@@ -52,7 +52,7 @@
"ButtonSetTimer": "Setze Timer",
"ButtonStream": "Streamen",
"ButtonSubmit": "Ok",
"ButtonSwitchServerUser": "Wechsle Server/Benutzer",
"ButtonSwitchServerUser": "Server/Benutzer wechseln",
"ButtonUnmaskServerAddress": "Server Adresse anzeigen",
"ButtonUserStats": "Nutzerstatistiken",
"ButtonYes": "Ja",
@@ -146,6 +146,7 @@
"LabelEndOfChapter": "Ende des Kapitels",
"LabelEndTime": "Endzeit",
"LabelEpisode": "Episode",
"LabelExplicit": "Explizit",
"LabelFeedURL": "Feed-URL",
"LabelFile": "Datei",
"LabelFileBirthtime": "Datei erstellt",
@@ -220,6 +221,7 @@
"LabelPublishedDate": "Veröffentlicht {0}",
"LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
"LabelRSSFeedCustomOwnerName": "Benutzerdefinierter Name des Eigentümers",
"LabelRSSFeedOpen": "RSS-Feed offen",
"LabelRSSFeedPreventIndexing": "Indizierung verhindern",
"LabelRSSFeedSlug": "RSS-Feed-Schlagwort",
"LabelRandomly": "Zufällig",
@@ -296,6 +298,7 @@
"MessageDownloading": "Wird heruntergeladen …",
"MessageDownloadingEpisode": "Episode herunterladen",
"MessageEpisodesQueuedForDownload": "{0} Episode(n) in der Warteschlange zum Herunterladen",
"MessageFailedToRefreshToken": "Aktualisierung des Tokens fehlgeschlagen, erneute Anmeldung erforderlich",
"MessageFeedURLWillBe": "Feed-URL wird {0} sein",
"MessageFetching": "Wird abgerufen …",
"MessageFollowTheProjectOnGithub": "Folge dem Projekt auf GitHub",
@@ -316,13 +319,16 @@
"MessageNoItems": "Keine Medien",
"MessageNoItemsFound": "Keine Medien gefunden",
"MessageNoListeningSessions": "Keine Hörsitzungen",
"MessageNoLogs": "Keine Protokolle vorhanden",
"MessageNoMediaFolders": "Keine Medien Ordner",
"MessageNoLogs": "Keine Protokolle",
"MessageNoMediaFolders": "Keine Medien-Ordner",
"MessageNoNetworkConnection": "Keine Netzwerkverbindung",
"MessageNoPodcastsFound": "Keine Podcasts gefunden",
"MessageNoSeries": "Keine Serie",
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
"MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden",
"MessageOldServerAuthReLoginRequired": "Für die verbesserung der Sicherheit wurde die Authentifizierung in der Version v2.26.0 verbessert. Alle Nutzer müssen sich neu einloggen.",
"MessageOldServerAuthWarning": "Der Server nutzt eine nicht mehr aktuelle Authentifizierung",
"MessageOldServerAuthWarningHelp": "Die Version dieses Servers ist älter als v2.26.0. Ein sichereres Authentifizierungssystem wurde in v2.26.0 hinzugefügt. Es wird dringend empfohlen, den Server auf die neuste Version zu aktualisieren. Solltest du den Server bereits aktualisiert haben, melde dich neu an, um das neue Authentifizierungssystem zu verwenden.",
"MessageOldServerConnectionWarning": "Diese Serververbindung nutzt eine alte Nutzer-ID (user ID). Bitte entferne diese Serververbindung und füge sie wieder neu hinzu.",
"MessageOldServerConnectionWarningHelp": "Du hast diese Serververbindung vor der Datenbankmigration in 2.3.0, veröffentlicht im Juni 2023, eingerichtet. Ein zukünftiges Serverupdate wird die Möglichkeit entfernen, sich mit dieser Verbindung anzumelden. Bitte lösche die existierende Serververbindung und melde dich neu an (mit der gleichen Serveradresse und Zugangsdaten). Wenn du irgendwelche Medien auf dieses Gerät heruntergeladen hast, musst du sie anschließend erneut herunterladen, um den Status zu synchronisieren.",
"MessagePodcastSearchField": "Suchbegriff oder RSS-Feed URL eingeben",
+6
View File
@@ -146,6 +146,7 @@
"LabelEndOfChapter": "End of Chapter",
"LabelEndTime": "End time",
"LabelEpisode": "Episode",
"LabelExplicit": "Explicit",
"LabelFeedURL": "Feed URL",
"LabelFile": "File",
"LabelFileBirthtime": "File Birthtime",
@@ -220,6 +221,7 @@
"LabelPublishedDate": "Published {0}",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedOpen": "RSS Feed Open",
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
"LabelRSSFeedSlug": "RSS Feed Slug",
"LabelRandomly": "Randomly",
@@ -296,6 +298,7 @@
"MessageDownloading": "Downloading...",
"MessageDownloadingEpisode": "Downloading episode",
"MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download",
"MessageFailedToRefreshToken": "Failed to refresh token, re-login required",
"MessageFeedURLWillBe": "Feed URL will be {0}",
"MessageFetching": "Fetching...",
"MessageFollowTheProjectOnGithub": "Follow the project on Github",
@@ -323,6 +326,9 @@
"MessageNoSeries": "No series",
"MessageNoUpdatesWereNecessary": "No updates were necessary",
"MessageNoUserPlaylists": "You have no playlists",
"MessageOldServerAuthReLoginRequired": "Authentication has been improved for security in server v2.26.0. All users are required to re-login.",
"MessageOldServerAuthWarning": "Server is using out-dated authentication",
"MessageOldServerAuthWarningHelp": "This server is running a version older than v2.26.0. A more secure authentication system was added in v2.26.0. It is strongly recommended to update the server to the latest version. If you have already updated the server, log in again to use the new authentication.",
"MessageOldServerConnectionWarning": "Server connection config is using an old user ID. Please delete and re-add this server connection.",
"MessageOldServerConnectionWarningHelp": "You originally set up the connection to this server prior to the database migration in 2.3.0, released June 2023. A future server update will remove the ability to sign in with this old connection. Please delete the existing server connection and connect again (using the same server address and credentials). If you have any downloaded media on this device, the media will need to be downloaded again to sync with the server.",
"MessagePodcastSearchField": "Enter search term or RSS feed URL",
+3 -2
View File
@@ -146,6 +146,7 @@
"LabelEndOfChapter": "Fin del capítulo",
"LabelEndTime": "Hora de finalización",
"LabelEpisode": "Episodio",
"LabelExplicit": "Explícito",
"LabelFeedURL": "URL del suministro",
"LabelFile": "Archivo",
"LabelFileBirthtime": "Archivo creado en",
@@ -220,7 +221,7 @@
"LabelPublishedDate": "Publicado {0}",
"LabelRSSFeedCustomOwnerEmail": "Correo electrónico de dueño personalizado",
"LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado",
"LabelRSSFeedPreventIndexing": "Prevenir indexado",
"LabelRSSFeedPreventIndexing": "Evitar indización",
"LabelRSSFeedSlug": "«Slug» de suministro RSS",
"LabelRandomly": "Aleatorio",
"LabelRead": "Leído",
@@ -338,7 +339,7 @@
"MessageSocketNotConnected": "Socket no conectado",
"NoteRSSFeedPodcastAppsHttps": "Atención: la mayoría de las aplicaciones de pódcast requieren que el URL del suministro RSS use HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Advertencia: 1 o más de sus episodios no tienen fecha de publicación. Algunas aplicaciones de podcast lo requieren.",
"ToastBookmarkCreateFailed": "Error al crear marcador",
"ToastBookmarkCreateFailed": "No se pudo crear el marcador",
"ToastBookmarkRemoveFailed": "Error al eliminar marcador",
"ToastBookmarkUpdateFailed": "Error al actualizar el marcador",
"ToastDownloadNotAllowedOnCellular": "Las descargas no están permitidas con datos móviles",
+177 -1
View File
@@ -1 +1,177 @@
{}
{
"ButtonAdd": "Lisa",
"ButtonAddNewServer": "Lisa uus server",
"ButtonAuthors": "Autorid",
"ButtonBack": "Tagasi",
"ButtonCancel": "Katkesta",
"ButtonCancelTimer": "Katkesta taimer",
"ButtonClearFilter": "Tühista filter",
"ButtonCollections": "Kollektsioonid",
"ButtonConnect": "Ühenda",
"ButtonConnectToServer": "Ühenda serveriga",
"ButtonCreate": "Loo uus",
"ButtonCreateBookmark": "Loo uus järjehoidja",
"ButtonCreateNewPlaylist": "Loo uus playlist",
"ButtonDelete": "Kustuta",
"ButtonDeleteLocalEpisode": "Kustuta lokaalne episood",
"ButtonDeleteLocalFile": "Kustuta lokaalne fail",
"ButtonDisableAutoTimer": "Lülita välja auto-taimer",
"ButtonDisconnect": "Ühenda lahti",
"ButtonHistory": "Ajalugu",
"ButtonIssues": "Probleemid",
"ButtonLatest": "Viimased",
"ButtonLibrary": "Raamatukogu",
"ButtonLocalMedia": "Lokaalne meedia",
"ButtonNewFolder": "Uus kaust",
"ButtonNextEpisode": "Järgmine episood",
"ButtonOk": "Ok",
"ButtonPause": "Paus",
"ButtonPlay": "Play",
"ButtonPlaylists": "Playlist",
"ButtonRead": "Loe",
"ButtonReadLess": "Loe vähem",
"ButtonReadMore": "Loe rohkem",
"ButtonRemove": "Kustuta",
"ButtonRemoveFromServer": "Kustuta serverist",
"ButtonSave": "Salvesta",
"ButtonSaveOrder": "Salvesta järjekord",
"ButtonSearch": "Otsi",
"ButtonSendEbookToDevice": "Saada e-raamat seadmele",
"ButtonSetTimer": "Säti taimer",
"ButtonSubmit": "Esita",
"ButtonUserStats": "Kasutaja statistika",
"ButtonYes": "Jah",
"HeaderAccount": "Konto",
"HeaderAndroidAutoSettings": "Android Auto sätted",
"HeaderChapters": "Peatükid",
"HeaderConnectionStatus": "Ühenduse staatus",
"HeaderDataSettings": "Andmete sätted",
"HeaderDetails": "Detailid",
"HeaderDownloads": "Allatõmmatud",
"HeaderEbookFiles": "E-raamatu failid",
"HeaderEpisodes": "Episoodid",
"HeaderEreaderSettings": "E-lugeja sätted",
"HeaderLatestEpisodes": "Viimased episoodid",
"HeaderLibraries": "Raamatukogud",
"HeaderLocalFolders": "Lokaalsed failid",
"HeaderLocalLibraryItems": "Lokaalse raamatukogu esemed",
"HeaderNewPlaylist": "Uus playlist",
"HeaderPlaylist": "Playlist",
"HeaderPlaylistItems": "Playlisti esemed",
"HeaderProgressSyncFailed": "Progressi sühnkronisatsioon ebaõnnestus",
"HeaderSelectDownloadLocation": "Vali allalaadimise kaust",
"HeaderSettings": "Seaded",
"HeaderSleepTimer": "Unetaimer",
"HeaderSleepTimerSettings": "Unetaimeri seaded",
"HeaderStatsRecentSessions": "Hiljutised sessioonid",
"HeaderTableOfContents": "Sisukord",
"HeaderUserInterfaceSettings": "Kasutajaliidese seaded",
"HeaderYourStats": "Sinu statistika",
"LabelAddToPlaylist": "Lisa playlisti",
"LabelAddedAt": "Lisatud",
"LabelAddedDate": "Lisatud {0}",
"LabelAll": "Kõik",
"LabelAllowSeekingOnMediaControls": "Luba positsiooniotsing meediateavituste juhtelementidel",
"LabelAlways": "Alati",
"LabelAndroidAutoBrowseLimitForGrouping": "Tähestikuline väljavõtmise limiit",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "Ära kasuta tähestikulises järjekorras järjestamist juhul kui kuvatavaid esemeid on vähem kui see arv",
"LabelAskConfirmation": "Küsi kinnitust",
"LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (eesnimi perekonnanimi)",
"LabelAuthorLastFirst": "Autor (perekonnanimi, eesnimi)",
"LabelAuthors": "Autorid",
"LabelAutoDownloadEpisodes": "Episoodide automaatne allalaadimine",
"LabelAutoRewindTime": "Keri aeg automaatselt tagasi",
"LabelAutoSleepTimer": "Automaatne unetaimer",
"LabelBooks": "Raamatud",
"LabelChapterTrack": "Peatüki heliriba",
"LabelChapters": "Peatükid",
"LabelComplete": "Valmis",
"LabelContinueBooks": "Jätka raamatuid",
"LabelContinueEpisodes": "Jätka episoode",
"LabelContinueListening": "Jätka kuulamist",
"LabelContinueReading": "Jätka lugemist",
"LabelDescription": "Kirjeldus",
"LabelDownload": "Lae alla",
"LabelDownloadUsingCellular": "Lae alla kasutades andmesidet",
"LabelDownloaded": "Allalaaditud",
"LabelDuration": "Kestvus",
"LabelEbook": "E-raamat",
"LabelEbooks": "E-raamatud",
"LabelEnable": "Luba",
"LabelEnd": "Lõpp",
"LabelEndOfChapter": "Peatükki lõpp",
"LabelEpisode": "Episood",
"LabelFile": "Fail",
"LabelFilename": "Faili nimi",
"LabelFinished": "Lõpetatud",
"LabelFolder": "Kaust",
"LabelFontBoldness": "Fondi paksus",
"LabelFontScale": "Fondi suurus",
"LabelGenre": "Žanr",
"LabelGenres": "Žanrid",
"LabelHapticFeedback": "Haptiline tagasiside",
"LabelHasEbook": "E-raamat olemas",
"LabelIncomplete": "Lõpetamata",
"LabelKeepScreenAwake": "Hoia ekraan ärkvel",
"LabelLanguage": "Keel",
"LabelLayoutAuto": "Auto",
"LabelLight": "Hele",
"LabelListenAgain": "Kuula uuesti",
"LabelLocalBooks": "Lokaalsed raamatud",
"LabelLocalPodcasts": "Lokaalsed podcastid",
"LabelLow": "Madal",
"LabelMediaType": "Meedia tüüp",
"LabelMedium": "Keskmine",
"LabelMissing": "Puudub",
"LabelMore": "Rohkem",
"LabelMoreInfo": "Rohkem infot",
"LabelName": "Nimi",
"LabelNavigateWithVolumeMirrored": "Peegeldatud",
"LabelNavigateWithVolumeWhilePlayingDisabled": "Väljalülitatud",
"LabelNavigateWithVolumeWhilePlayingEnabled": "Sisselülitatud",
"LabelNever": "Mitte kunagi",
"LabelNewestAuthors": "Uuemad autorid",
"LabelNewestEpisodes": "Uuemad episoodid",
"LabelNo": "Ei",
"LabelNotFinished": "Lõpetamata",
"LabelNotStarted": "Pole alustatud",
"LabelNumEpisodes": "{0} episoode",
"LabelNumEpisodesIncomplete": "{0} episoodi, {1} lõpetamata",
"LabelNumberOfEpisodes": "# episoode",
"LabelOff": "Väljalülitatud",
"LabelOn": "Sisselülitatud",
"LabelPassword": "Parool",
"LabelPlaybackDirect": "Otse",
"LabelPlaybackLocal": "Lokaalne",
"LabelPlaybackTranscode": "Transkodeeri",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcastid",
"LabelProgress": "Progress",
"LabelPubDate": "Publitseerimise kuupäev",
"LabelPublishYear": "Publitseerimise aasta",
"LabelPublishedDate": "Publitseeritud {0}",
"LabelRandomly": "Juhuslikus järjekorras",
"LabelRead": "Loetud läbi",
"LabelReadAgain": "Loe uuesti",
"LabelRecentlyAdded": "Hiljuti lisatud",
"LabelRemoveFromPlaylist": "Eemalda playlistist",
"LabelSeason": "Hooaeg",
"LabelSelectADevice": "Vali seade",
"LabelServerAddress": "Serveri aadres",
"LabelShowAll": "Näita kõik",
"LabelSize": "Suurus",
"LabelSleepTimer": "Unetaimer",
"LabelStart": "Alusta",
"LabelStatsBestDay": "Parim päev",
"LabelStatsDays": "Päevi",
"LabelStatsInARow": "järjest",
"LabelStatsMinutes": "minuteid",
"LabelThemeBlack": "Must",
"LabelThemeDark": "Pime",
"LabelThemeLight": "Hele",
"LabelTitle": "Pealkiri",
"LabelTotalSize": "Kogusuurus",
"LabelType": "Tüüp",
"LabelUnknown": "Teadmata"
}
+1
View File
@@ -6,6 +6,7 @@
"ButtonCancel": "Peruuta",
"ButtonCancelTimer": "Peruuta ajastin",
"ButtonClearFilter": "Poista suodatus",
"ButtonClearLogs": "Tyhjennä lokit",
"ButtonCloseFeed": "Sulje syöte",
"ButtonCollections": "Kokoelmat",
"ButtonConnect": "Yhdistä",
+15 -2
View File
@@ -6,6 +6,7 @@
"ButtonCancel": "Annuler",
"ButtonCancelTimer": "Annuler la minuterie",
"ButtonClearFilter": "Effacer le filtre",
"ButtonClearLogs": "Effacer les registres",
"ButtonCloseFeed": "Fermer le flux",
"ButtonCollections": "Collections",
"ButtonConnect": "Connecter",
@@ -26,7 +27,9 @@
"ButtonLatest": "Dernière version",
"ButtonLibrary": "Bibliothèque",
"ButtonLocalMedia": "Médias locaux",
"ButtonLogs": "Registres",
"ButtonManageLocalFiles": "Gérer les fichiers locaux",
"ButtonMaskServerAddress": "Masquer l'adresse du serveur",
"ButtonNewFolder": "Nouveau dossier",
"ButtonNextEpisode": "Prochain épisode",
"ButtonOk": "D'accord",
@@ -50,6 +53,7 @@
"ButtonStream": "Flux",
"ButtonSubmit": "Soumettre",
"ButtonSwitchServerUser": "Changer de serveur / utilisateur",
"ButtonUnmaskServerAddress": "Afficher l'adresse du serveur",
"ButtonUserStats": "Statistiques utilisateur",
"ButtonYes": "Oui",
"HeaderAccount": "Compte",
@@ -116,7 +120,7 @@
"LabelComplete": "Complet",
"LabelContinueBooks": "Continuer les Livres",
"LabelContinueEpisodes": "Continuer les épisodes",
"LabelContinueListening": "Continuer la lecture",
"LabelContinueListening": "Continuer l'écoute",
"LabelContinueReading": "Continuer la lecture",
"LabelContinueSeries": "Continuer les séries",
"LabelCustomTime": "Temps personalisé",
@@ -129,7 +133,7 @@
"LabelDisableVibrateOnReset": "Désactiver la vibration au redémarrage",
"LabelDisableVibrateOnResetHelp": "Lorsque le minuteur est redémarré, l'appareil vibre. Sélectionner pour désactiver les vibrations.",
"LabelDiscover": "Découvrir",
"LabelDownload": "Téléchargement",
"LabelDownload": "Télécharger",
"LabelDownloadUsingCellular": "Télécharger via les données mobiles",
"LabelDownloaded": "Téléchargé",
"LabelDuration": "Durée",
@@ -142,6 +146,7 @@
"LabelEndOfChapter": "Fin du chapitre",
"LabelEndTime": "Heure de fin",
"LabelEpisode": "Épisode",
"LabelExplicit": "Contenu explicite",
"LabelFeedURL": "URL du flux",
"LabelFile": "Fichier",
"LabelFileBirthtime": "Création du fichier",
@@ -216,6 +221,7 @@
"LabelPublishedDate": "Publié le {0}",
"LabelRSSFeedCustomOwnerEmail": "Courriel personnalisée du propriétaire",
"LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé",
"LabelRSSFeedOpen": "Flux RSS ouvert",
"LabelRSSFeedPreventIndexing": "Empêcher lindexation",
"LabelRSSFeedSlug": "Identifiant dURL du flux RSS",
"LabelRandomly": "Au hasard",
@@ -237,6 +243,8 @@
"LabelShowAll": "Tout afficher",
"LabelSize": "Taille",
"LabelSleepTimer": "Minuterie",
"LabelSleepTimerAlmostDoneChime": "Jouer un son quand c'est presque fini",
"LabelSleepTimerAlmostDoneChimeHelp": "Jouer un son quand il reste 30 s au minuteur de veille",
"LabelStart": "Démarrer",
"LabelStartTime": "Heure de démarrage",
"LabelStatsBestDay": "Meilleur jour",
@@ -290,6 +298,7 @@
"MessageDownloading": "Téléchargement…",
"MessageDownloadingEpisode": "Téléchargement de l’épisode",
"MessageEpisodesQueuedForDownload": "{0} épisode(s) mis en file pour téléchargement",
"MessageFailedToRefreshToken": "Échec de l'actualisation du jeton, reconnexion requise",
"MessageFeedURLWillBe": "lURL du flux sera {0}",
"MessageFetching": "Récupération…",
"MessageFollowTheProjectOnGithub": "Suivre le projet sur GitHub",
@@ -310,12 +319,16 @@
"MessageNoItems": "Aucun élément",
"MessageNoItemsFound": "Aucun élément trouvé",
"MessageNoListeningSessions": "Aucune session d’écoute en cours",
"MessageNoLogs": "Aucun registre",
"MessageNoMediaFolders": "Aucun dossier multimédia",
"MessageNoNetworkConnection": "Pas de connexion réseau",
"MessageNoPodcastsFound": "Aucun podcast trouvé",
"MessageNoSeries": "Aucune série",
"MessageNoUpdatesWereNecessary": "Aucune mise à jour n’était nécessaire",
"MessageNoUserPlaylists": "Vous navez aucune liste de lecture",
"MessageOldServerAuthReLoginRequired": "Lauthentification a été renforcée pour des raisons de sécurité dans la version 2.26.0 du serveur. Tous les utilisateurs doivent se reconnecter.",
"MessageOldServerAuthWarning": "Le serveur utilise un système dauthentification obsolète",
"MessageOldServerAuthWarningHelp": "Ce serveur utilise une version antérieure à la v2.26.0. Un système dauthentification plus sécurisé a été ajouté dans la version 2.26.0. Il est fortement recommandé de mettre à jour le serveur vers la dernière version. Si vous avez déjà effectué la mise à jour, reconnectez-vous pour utiliser le nouveau système dauthentification.",
"MessageOldServerConnectionWarning": "La configuration de connexion du serveur utilise un ancien identifiant utilisateur. Veuillez supprimer et ajouter cette connexion serveur.",
"MessageOldServerConnectionWarningHelp": "Vous avez initialement créé la connexion à ce serveur avant la migration de la base de données dans 2.3.0, publié en juin 2023. Une mise à jour future du serveur supprimera la possibilité de signer avec cette ancienne connexion. Veuillez supprimer la connexion existante du serveur et se connecter à nouveau (en utilisant la même adresse de serveur et les mêmes identifiants). Si vous avez des médias téléchargés sur cet appareil, les médias devront être téléchargés à nouveau pour synchroniser avec le serveur.",
"MessagePodcastSearchField": "Saisir un terme de recherche ou l'URL d'un flux RSS",
+7
View File
@@ -1,8 +1,12 @@
{
"ButtonAdd": "ઉમેરો",
"ButtonAddNewServer": "નવી સર્વર ઉમેરો",
"ButtonAuthors": "લેખકો",
"ButtonBack": "પાછા",
"ButtonCancel": "રદ કરો",
"ButtonCancelTimer": "ટાઈમર રદ કરો",
"ButtonClearFilter": "ફિલ્ટર જતુ કરો",
"ButtonClearLogs": "લોગ્સ સાફ કરો",
"ButtonCloseFeed": "ફીડ બંધ કરો",
"ButtonCollections": "સંગ્રહ",
"ButtonCreate": "બનાવો",
@@ -12,9 +16,12 @@
"ButtonLatest": "નવીનતમ",
"ButtonLibrary": "પુસ્તકાલય",
"ButtonOpenFeed": "ફીડ ખોલો",
"ButtonPause": "વિરામ",
"ButtonPlay": "ચલાવો",
"ButtonPlaylists": "પ્લેલિસ્ટ",
"ButtonRead": "વાંચો",
"ButtonReadLess": "ઓછું વાંચો",
"ButtonReadMore": "વધારે વાંચો",
"ButtonRemove": "કાઢી નાખો",
"ButtonSave": "સાચવો",
"ButtonSearch": "શોધો",
+322 -2
View File
@@ -6,6 +6,7 @@
"ButtonCancel": "ביטול",
"ButtonCancelTimer": "בטל טיימר",
"ButtonClearFilter": "נקה סינון",
"ButtonClearLogs": "נקה לוגים",
"ButtonCloseFeed": "סגור ערוץ",
"ButtonCollections": "אוספים",
"ButtonConnect": "התחבר",
@@ -25,9 +26,328 @@
"ButtonIssues": "בעיות",
"ButtonLatest": "אחרון",
"ButtonLibrary": "ספרייה",
"ButtonLocalMedia": "מדיה מקומית",
"ButtonLogs": "לוגים",
"ButtonManageLocalFiles": "נהל קבצים מקומיים",
"ButtonMaskServerAddress": "הסתר כתובת שרת",
"ButtonNewFolder": "תיקייה חדשה",
"ButtonNextEpisode": "הפרק הבא",
"ButtonOk": "אישור",
"ButtonOpenFeed": "פתח פיד",
"ButtonOverride": "עקוף",
"ButtonPause": "השהה",
"ButtonPlay": "נגן",
"ButtonPlayEpisode": "נגן פרק",
"ButtonPlaylists": "רשימת השמעה",
"ButtonPlaylists": "רשימות השמעה",
"ButtonRead": "קרא",
"ButtonReadLess": "קרא פחות",
"ButtonReadMore": "קרא עוד",
"ButtonRemove": "הסר",
"ButtonRemoveFromServer": "הסר מהשרת",
"ButtonSave": "שמור",
"ButtonSaveOrder": "שמור פקודה",
"ButtonSearch": "חיפוש",
"ButtonSendEbookToDevice": "שלח ספר אלקטרוני למכשיר",
"ButtonSeries": "סדרה",
"ButtonSetTimer": "הפעל טיימר",
"ButtonStream": "הזרם",
"ButtonSubmit": "שליחה",
"ButtonSwitchServerUser": "החלף שרת/משתמש",
"ButtonUnmaskServerAddress": "גלה כתובת שרת",
"ButtonUserStats": "נתוני משתמש",
"ButtonYes": "כן",
"HeaderAccount": "חשבון",
"HeaderAdvanced": "מתקדם",
"HeaderAndroidAutoSettings": "הגדרות Android Auto",
"HeaderAudioTracks": "רצועות קול",
"HeaderChapters": "פרקים",
"HeaderCollection": "אוסף",
"HeaderCollectionItems": "פרטי אוסף",
"HeaderConnectionStatus": "מצב החיבור",
"HeaderDataSettings": "הגדרות נתונים",
"HeaderDetails": "פרטים",
"HeaderDownloads": "הורדות",
"HeaderEbookFiles": "קבצי Ebook",
"HeaderEpisodes": "פרקים",
"HeaderEreaderSettings": "הגדרות קורא אלקטרוני",
"HeaderLatestEpisodes": "פרקים אחרונים",
"HeaderLibraries": "ספריות",
"HeaderLocalFolders": "תיקיות מקומיות",
"HeaderLocalLibraryItems": "פריטי ספרייה מקומית",
"HeaderNewPlaylist": "רשימת השמעה חדשה",
"HeaderOpenRSSFeed": "פתח ערוץ RSS",
"LabelAddedDate": "נוסף ב-{0}"
"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": "עוצמת השמע תתחיל לרדת כאשר נותרה פחות מדקה אחת בטיימר השינה. הפעל הגדרה זו כדי לא לדעוך.",
"LabelDisableAutoRewind": "השבתת הרצה אוטומטית אחורה",
"LabelDisableShakeToReset": "השבתת ניעור לאיפוס",
"LabelDisableShakeToResetHelp": "ניעור המכשיר בזמן שהטיימר פועל או תוך 2 דקות ממועד סופו יאפס את טיימר השינה. הפעל הגדרה זו כדי להשבית את הניעור לצורך איפוס.",
"LabelDisableVibrateOnReset": "השבתת רטט באיפוס",
"LabelDisableVibrateOnResetHelp": "כאשר טיימר השינה מתאפס, המכשיר ירטוט. הפעל הגדרה זו כדי לא לרטוט כאשר טיימר השינה מתאפס.",
"LabelDiscover": "גלה",
"LabelDownload": "הורדה",
"LabelDownloadUsingCellular": "הורדה באמצעות נתונים סלולריים",
"LabelDownloaded": "הורד",
"LabelDuration": "משך",
"LabelEbook": "ספר אלקטרוני",
"LabelEbooks": "ספרים אלקטרוניים",
"LabelEnable": "אפשר",
"LabelEnableMp3IndexSeeking": "אפשר חיפוש באינדקס mp3",
"LabelEnableMp3IndexSeekingHelp": "יש להפעיל הגדרה זו רק אם יש לכם קבצי mp3 שאינם מחפשים כראוי. חיפוש לא מדויק נובע ככל הנראה מקבצי MP3 בעלי קצב סיביות משתנה (VBR). הגדרה זו תכריח חיפוש אינדקס, שבו נבנה מיפוי זמן-לבייט בעת קריאת הקובץ. במקרים מסוימים, עם קבצי MP3 גדולים, יהיה עיכוב בחיפוש לקראת סוף הקובץ.",
"LabelEnd": "סוף",
"LabelEndOfChapter": "סוף הפרק",
"LabelEndTime": "זמן סיום",
"LabelEpisode": "פרק",
"LabelFeedURL": "כתובת ערוץ",
"LabelFile": "קובץ",
"LabelFileBirthtime": "זמן יצירת הקובץ",
"LabelFileModified": "קובץ נערך",
"LabelFilename": "שם קובץ",
"LabelFinished": "הושלם",
"LabelFolder": "תיקייה",
"LabelFontBoldness": "עובי פונט",
"LabelFontScale": "גודל פונט",
"LabelGenre": "סגנון",
"LabelGenres": "סגנונות",
"LabelHapticFeedback": "משוב מגע",
"LabelHasEbook": "קיים ספר אלקטרוני",
"LabelHasSupplementaryEbook": "קיים ספר אלקטרוני משלים",
"LabelHeavy": "כבד",
"LabelHigh": "גבוה",
"LabelHost": "מארח",
"LabelInProgress": "בתהליך",
"LabelIncomplete": "לא הושלם",
"LabelInternalAppStorage": "אחסון אפליקציה פנימי",
"LabelJumpBackwardsTime": "הרץ לאחור",
"LabelJumpForwardsTime": "הרץ קדימה",
"LabelKeepScreenAwake": "השאר מסך דלוק",
"LabelLanguage": "שפה",
"LabelLayout": "Layout",
"LabelLayoutAuto": "אוטומטי",
"LabelLayoutSinglePage": "עמוד יחיד",
"LabelLight": "בהיר",
"LabelLineSpacing": "מרווח שורה",
"LabelListenAgain": "האזן שוב",
"LabelLocalBooks": "ספרים מקומיים",
"LabelLocalPodcasts": "פודקאסטים מקומיים",
"LabelLockOrientation": "נעילת כיוון",
"LabelLockPlayer": "נעילת נגן",
"LabelLow": "נמוך",
"LabelMediaType": "סוג מדיה",
"LabelMedium": "בינוני",
"LabelMissing": "חסר",
"LabelMore": "עוד",
"LabelMoreInfo": "מידע נוסף",
"LabelName": "שם",
"LabelNarrator": "מספר",
"LabelNarrators": "מספרים",
"LabelNavigateWithVolume": "ניווט באמצעות כפתורי ווליום",
"LabelNavigateWithVolumeMirrored": "מראה",
"LabelNavigateWithVolumeWhilePlaying": "אפשר למקשי ווליום לנווט בזמן ניגון",
"LabelNavigateWithVolumeWhilePlayingDisabled": "כיבוי",
"LabelNavigateWithVolumeWhilePlayingEnabled": "הדלקה",
"LabelNever": "אף פעם",
"LabelNewestAuthors": "הסופרים האחרונים",
"LabelNewestEpisodes": "הפרקים החדשים ביותר",
"LabelNo": "לא",
"LabelNotFinished": "לא הושלם",
"LabelNotStarted": "לא התחיל",
"LabelNumEpisodes": "{0} פרקים",
"LabelNumEpisodesIncomplete": "{0} פרקים, {1} לא הושלם",
"LabelNumberOfEpisodes": "# פרקים",
"LabelOff": "כיבוי",
"LabelOn": "הדלקה",
"LabelPassword": "סיסמה",
"LabelPath": "נתיב",
"LabelPlaybackDirect": "ישיר",
"LabelPlaybackLocal": "מקומי",
"LabelPlaybackSpeed": "מהירות השמעה",
"LabelPlaybackTranscode": "קידוד מחדש",
"LabelPodcast": "פודקאסט",
"LabelPodcasts": "פודקאסטים",
"LabelPreventIndexing": "מנע רישום של הערוץ שלך על ידי ספריות אייטונס וגוגל פודקאסט",
"LabelProgress": "התקדמות",
"LabelPubDate": "תאריך פרסום",
"LabelPublishYear": "שנת הפרסום",
"LabelPublishedDate": "פורסם {0}",
"LabelRSSFeedCustomOwnerEmail": "אימייל בעלים מותאם אישית",
"LabelRSSFeedCustomOwnerName": "שם בעלים מותאם אישית",
"LabelRSSFeedPreventIndexing": "מנע רישום",
"LabelRSSFeedSlug": "Slug של ערוץ ה-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": "ברצף",
"LabelStatsItemsFinished": "פריטים שסיימת",
"LabelStatsMinutes": "דקות",
"LabelStatsMinutesListening": "דקות האזנה",
"LabelStatsWeekListening": "האזנה שבועית",
"LabelStreamingUsingCellular": "הזרמת באמצעות נתונים סלולריים",
"LabelTag": "תג",
"LabelTags": "תגיות",
"LabelTheme": "ערכת נושא",
"LabelThemeBlack": "שחור",
"LabelThemeDark": "כהה",
"LabelThemeLight": "בהיר",
"LabelTimeRemaining": "{0} נותרו",
"LabelTitle": "כותרת",
"LabelTotalSize": "סה״כ גודל",
"LabelTotalTrack": "סה״כ רצועות",
"LabelTracks": "רצועות",
"LabelType": "סוג",
"LabelUnknown": "לא ידוע",
"LabelUnlockPlayer": "בטל נעילת נגן",
"LabelUseBookshelfView": "שימוש בתצוגת מדף ספרים",
"LabelUser": "משתמש",
"LabelUsername": "שם משתמש",
"LabelVeryHigh": "גבוה מאוד",
"LabelVeryLow": "נמוך מאוד",
"LabelYearReviewHide": "הסתר סקירת שנה",
"LabelYearReviewShow": "הצג סקירת שנה",
"LabelYourBookmarks": "הסימניות שלך",
"LabelYourProgress": "ההתקדמות שלך",
"MessageAndroid10Downloads": "מכשירי אנדרואיד 10 או נמוך יותר ישתמשו באחסון היישום הפנימי להורדות.",
"MessageAttemptingServerConnection": "מנסה להתחבר לשרת...",
"MessageAudiobookshelfServerNotConnected": "לא מחובר לשרת Audiobookshelf",
"MessageAudiobookshelfServerRequired": "<strong>חשוב!</strong> אפליקציה זו מתוכננת לעבוד עם שרת Audiobookshelf שאת/ה או מישהו שאת/ה מכיר/ה מארח/ת. האפליקציה בפני עצמה לא מספקת שום תוכן.",
"MessageBookshelfEmpty": "ספריה ריקה",
"MessageConfirmDeleteLocalEpisode": "להסיר את הפרק המקומי \"{0}\" מהמכשיר שלך? הקובץ בשרת לא יושפע.",
"MessageConfirmDeleteLocalFiles": "להסיר קבצים מקומיים של פריט זה מהמכשיר שלך? הקבצים בשרת והתקדמותך לא יושפעו.",
"MessageConfirmDisableAutoTimer": "האם אתה בטוח שאתה רוצה להשבית את הטיימר האוטומי עד סוף היום? הטיימר יופעל מחדש לאחר הטיימר השינה הנוכחי יסתיים, או לאחר הפעלה מחדש של האפליקציה.",
"MessageConfirmDiscardProgress": "אתה בטוח שאתה רוצה לאפס את ההתקדמות שלך?",
"MessageConfirmDownloadUsingCellular": "אתה עומד להוריד באמצעות נתונים סלולריים. ספק הסלולר עשוי לחייב על שימוש בנתונים סלולריים, האם להמשיך?",
"MessageConfirmMarkAsFinished": "אתה בטוח שאתה רוצה לסמן את הפריט הזה כהושלם?",
"MessageConfirmRemoveBookmark": "האם להסיר את הסימניה?",
"MessageConfirmStreamingUsingCellular": "אתה עומד להזרים באמצעות נתונים סלולריים. ספק הסלולר עשוי לחייב על שימוש בנתונים סלולריים, האם להמשיך?",
"MessageDiscardProgress": "איפוס התקדמות",
"MessageDownloadCompleteProcessing": "ההורדה הושלמה. מעבד...",
"MessageDownloading": "מוריד...",
"MessageDownloadingEpisode": "מוריד פרק",
"MessageEpisodesQueuedForDownload": "{0} פרק/ים בתור להורדה",
"MessageFeedURLWillBe": "כתובת ה- URL של הערוץ תהיה {0}",
"MessageFetching": "מושך...",
"MessageFollowTheProjectOnGithub": "עקבו אחרי הפרוייקט ב-Github",
"MessageItemDownloadCompleteFailedToCreate": "הורדת הפריט הושלמה אך יצירת פריט בספריה נכשלה",
"MessageItemMissing": "הפריט חסר ונדרש תיקונו בשרת. בדרך כלל פריט מסומן כחסר כי נתיבי הקובץ הקיימים לא נגישים.",
"MessageLoading": "טוען...",
"MessageLoadingServerData": "טוען נתוני שרת...",
"MessageLocalFolderDescription": "\"אחסון אפליקציה פנימי\" זמין רק על ידי אפליקציה זו. אפליקציה זו תומכת רק במדיה שהורדה ישירות ע״י האפליקציה עצמה. ניתן להשתמש בתיקיות אחסון משותפות בכדי לאפשר ליישומים אחרים לגשת מדיה שהורדה ע״י אפליקציה זו.",
"MessageMarkAsFinished": "סמן כהושלם",
"MessageMediaLinkedToADifferentServer": "מדיה מקושרת לשרת Audiobookshelf בכתובת אחרת ({0}). ההתקדמות תהיה מסונכרנת כאשר היא האפליקציה תהיה מחוברת לכתובת שרת זו.",
"MessageMediaLinkedToADifferentUser": "מדיה מקושרת לשרת זה אבל הורדה ע״י משתמש אחר. התקדמות תסונכרן רק למשתמש שהוריד אותה.",
"MessageMediaLinkedToServer": "מקושר לשרת {0}",
"MessageMediaLinkedToThisServer": "המדיה שהורדה מקושרת לשרת זה",
"MessageMediaNotLinkedToServer": "מדיה לא מקושרת לשרת Audiobookshelf. התקדמות לא תהיה מסונכרנת.",
"MessageNoBookmarks": "אין סימניות",
"MessageNoChapters": "אין פרקים",
"MessageNoCollections": "אין אוספים",
"MessageNoItems": "אין פריטים",
"MessageNoItemsFound": "לא נמצאו פריטים",
"MessageNoListeningSessions": "אין הפעלות האזנה",
"MessageNoLogs": "אין לוגים",
"MessageNoMediaFolders": "אין תיקיות מדיה",
"MessageNoNetworkConnection": "אין חיבור לרשת",
"MessageNoPodcastsFound": "לא נמצאו פודקאסטים",
"MessageNoSeries": "אין סדרות",
"MessageNoUpdatesWereNecessary": "לא נדרש עדכון",
"MessageNoUserPlaylists": "אין לך רשימות השמעה",
"MessageOldServerConnectionWarning": "תצורת חיבור שרת משתמשת במזהה משתמש ישן. נא למחוק ולהתחבר מחדש לשרת זה.",
"MessageOldServerConnectionWarningHelp": "הגדרת במקור את החיבור לשרת זה לפני העברת מסד הנתונים בגרסה 2.3.0, שיצאה ביוני 2023. עדכון שרת עתידי יסיר את היכולת להתחבר באמצעות חיבור ישן זה. אנא מחק את חיבור השרת הקיים והתחבר שוב (תוך שימוש באותה כתובת שרת ופרטי גישה). אם הורדת מדיה כלשהי במכשיר זה, יהיה צורך להוריד אותה שוב כדי שתסתנכרן עם השרת.",
"MessagePodcastSearchField": "הזן מונח חיפוש או כתובת URL של ערוץ RSS",
"MessageProgressSyncFailed": "הניסיון האחרון לדווח על התקדמות ההאזנה שלך לשרת נכשל. בקשות סנכרון התקדמות ימשיכו להתבצע כל 15 שניות עד דקה בזמן שהמדיה מופעלת.",
"MessageReportBugsAndContribute": "דווח על באגים, בקש תכונות חדשות, ותרום ב-",
"MessageSeriesAlreadyDownloaded": "כבר הורדת את כל הספרים בסדרה זו.",
"MessageSeriesDownloadConfirm": "להוריד את הספר החסר {0} עם {1} קבצים ,סה״כ {2} לתיקיה {3}?",
"MessageSeriesDownloadConfirmIos": "להוריד את הספר החסר {0} עם {1} קבצים ,סה״כ {2}?",
"MessageSocketConnectedOverMeteredCellular": "מחובר באמצעות רשת סלולרית",
"MessageSocketConnectedOverMeteredWifi": "מחוברת באמצעות רשת WiFi מדודה",
"MessageSocketConnectedOverUnmeteredCellular": "מחובר באמצעות רשת סלולרית לא מדודה",
"MessageSocketConnectedOverUnmeteredWifi": "מחוברת באמצעות רשת WiFi לא מדודה",
"MessageSocketNotConnected": "לא מחובר",
"NoteRSSFeedPodcastAppsHttps": "אזהרה: רוב אפליקציות הפודקאסטים ידרשו שכתובת האתר של ערוץ ה-RSS תשתמש ב-HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "אזהרה: פרק אחד או יותר לא מכילים תאריך פרסום. חלק מיישומי הפודקאסט דורשים זאת.",
"ToastBookmarkCreateFailed": "יצירת סימניה נכשלה",
"ToastBookmarkRemoveFailed": "הסרת הסימניה נכשלה",
"ToastBookmarkUpdateFailed": "עדכון הסימניה נכשל",
"ToastDownloadNotAllowedOnCellular": "הורדה אינה מותרת באמצעות נתונים סלולריים",
"ToastItemMarkedAsFinishedFailed": "סימון כפריט שהושלם נכשל",
"ToastItemMarkedAsNotFinishedFailed": "סימון כפריט שלא הושלם נכשל",
"ToastPlaylistCreateFailed": "יצירת רשימת השמעה נכשלה",
"ToastPodcastCreateFailed": "יצירת הפודקאסט נכשלה",
"ToastPodcastCreateSuccess": "הפודקאסט נוצר בהצלחה",
"ToastRSSFeedCloseFailed": "סגירת ערוץ ה-RSS נכשלה",
"ToastRSSFeedCloseSuccess": "ערוץ ה-RSS נסגר בהצלחה",
"ToastStreamingNotAllowedOnCellular": "הזרמה אינה מותרת באמצעות נתונים סלולריים"
}
+18
View File
@@ -6,16 +6,34 @@
"ButtonCancel": "रद्द करें",
"ButtonCancelTimer": "टाइमर रद्द करें",
"ButtonClearFilter": "लागू फ़िल्टर साफ़ करें",
"ButtonClearLogs": "अभिलेख साफ़ करें",
"ButtonCloseFeed": "फ़ीड बंद करें",
"ButtonCollections": "संग्रह",
"ButtonConnect": "जोड़ना",
"ButtonConnectToServer": "सर्वर से संबद्ध स्थापित करें",
"ButtonCreate": "बनाएं",
"ButtonCreateBookmark": "बुकमार्क बनाएं",
"ButtonCreateNewPlaylist": "प्लेलिस्ट बनाएं",
"ButtonDelete": "हटाएं",
"ButtonDeleteLocalEpisode": "स्थानीय प्रकरण हटाएँ",
"ButtonDeleteLocalFile": "स्थानीय फ़ाइल हटाएँ",
"ButtonDeleteLocalItem": "स्थानीय वस्तु हटाएँ",
"ButtonDisableAutoTimer": "स्वचालित घड़ी अक्षम करें",
"ButtonDisconnect": "सर्वर संबद्ध तोड़े",
"ButtonGoToWebClient": "वेब क्लाइंट पर जाएँ",
"ButtonHistory": "इतिहास",
"ButtonHome": "घर",
"ButtonIssues": "समस्याएं",
"ButtonLatest": "नवीनतम",
"ButtonLibrary": "पुस्तकालय",
"ButtonLogs": "अभिलेख",
"ButtonManageLocalFiles": "स्थानीय फ़ाइल प्रबंधित करें",
"ButtonMaskServerAddress": "सर्वर पता नकाबपोश करें",
"ButtonNewFolder": "नया फ़ोल्डर",
"ButtonNextEpisode": "अगला एपिसोड",
"ButtonOk": "ठीक है",
"ButtonOpenFeed": "फ़ीड खोलें",
"ButtonOverride": "अवहेलना करें",
"ButtonPlay": "चलाएँ",
"ButtonPlaylists": "प्लेलिस्ट्स",
"ButtonRead": "पढ़ लिया",
+6
View File
@@ -6,6 +6,7 @@
"ButtonCancel": "Odustani",
"ButtonCancelTimer": "Poništi timer",
"ButtonClearFilter": "Poništi filter",
"ButtonClearLogs": "Izbriši zapisnike",
"ButtonCloseFeed": "Zatvori izvor",
"ButtonCollections": "Zbirke",
"ButtonConnect": "Povezivanje",
@@ -145,6 +146,7 @@
"LabelEndOfChapter": "Kraj poglavlja",
"LabelEndTime": "Vrijeme završetka",
"LabelEpisode": "Nastavak",
"LabelExplicit": "Eksplicitno",
"LabelFeedURL": "URL izvora",
"LabelFile": "Datoteka",
"LabelFileBirthtime": "Datoteka stvorena",
@@ -219,6 +221,7 @@
"LabelPublishedDate": "Objavljeno {0}",
"LabelRSSFeedCustomOwnerEmail": "Prilagođena adresa e-pošte vlasnika",
"LabelRSSFeedCustomOwnerName": "Prilagođeno ime vlasnika",
"LabelRSSFeedOpen": "RSS izvor otvoren",
"LabelRSSFeedPreventIndexing": "Onemogući indeksiranje",
"LabelRSSFeedSlug": "Slug RSS izvora",
"LabelRandomly": "Nasumično",
@@ -322,6 +325,9 @@
"MessageNoSeries": "Nema serijala",
"MessageNoUpdatesWereNecessary": "Ažuriranje nije bilo potrebno",
"MessageNoUserPlaylists": "Nemate popisa za izvođenje",
"MessageOldServerAuthReLoginRequired": "Sigurnost provjere autentičnosti poboljšana je u inačici poslužitelja v2.26.0. Svi se korisnici moraju iznova prijaviti.",
"MessageOldServerAuthWarning": "Poslužitelj upotrebljava zastarjeli način provjere autentičnosti",
"MessageOldServerAuthWarningHelp": "Ovaj poslužitelj starija je inačica od v2.26.0. Od inačice v2.26.0 uveden je sigurniji način provjere autentičnosti. Izričito preporučamo nadogradnju poslužitelja na noviju inačicu. Ukoliko ste već nadogradili poslužitelj, iznova se prijavite kako biste upotrebljavali novi način provjere autentičnosti.",
"MessageOldServerConnectionWarning": "Veza s poslužiteljem koristi se starim ID-jem korisnika. Molimo izbrišite i ponovno dodajte ovu vezu 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",
+6
View File
@@ -146,6 +146,7 @@
"LabelEndOfChapter": "Fejezet vége",
"LabelEndTime": "Befejezés ideje",
"LabelEpisode": "Epizód",
"LabelExplicit": "Szókimondó",
"LabelFeedURL": "Hírcsatorna URL",
"LabelFile": "Fájl",
"LabelFileBirthtime": "Fájl létrehozásának ideje",
@@ -220,6 +221,7 @@
"LabelPublishedDate": "Kiadva {0}",
"LabelRSSFeedCustomOwnerEmail": "Egyéni tulajdonos e-mail",
"LabelRSSFeedCustomOwnerName": "Egyéni tulajdonos neve",
"LabelRSSFeedOpen": "RSS-hírcsatorna nyitva",
"LabelRSSFeedPreventIndexing": "Indexelés megakadályozása",
"LabelRSSFeedSlug": "RSS hírcsatorna rövid cím",
"LabelRandomly": "Véletlenszerűen",
@@ -296,6 +298,7 @@
"MessageDownloading": "Letöltés...",
"MessageDownloadingEpisode": "Epizód letöltése",
"MessageEpisodesQueuedForDownload": "{0} epizód letöltésre vár",
"MessageFailedToRefreshToken": "A token frissítése nem sikerült, ismételt bejelentkezés szükséges",
"MessageFeedURLWillBe": "A hírcsatorna URL-je lesz: {0}",
"MessageFetching": "Lekérdezés...",
"MessageFollowTheProjectOnGithub": "Kövesse a projektet a GitHubon",
@@ -323,6 +326,9 @@
"MessageNoSeries": "Nincsenek sorozatok",
"MessageNoUpdatesWereNecessary": "Nem volt szükséges frissítés",
"MessageNoUserPlaylists": "Nincsenek lejátszási listái",
"MessageOldServerAuthReLoginRequired": "A szerver v2.26.0 verziójában a biztonság érdekében továbbfejlesztettük a hitelesítést. Minden felhasználónak újra be kell jelentkeznie.",
"MessageOldServerAuthWarning": "A szerver elavult hitelesítést használ",
"MessageOldServerAuthWarningHelp": "Ez a szerver a v2.26.0-nál régebbi verziót futtat. A v2.26.0-ban egy biztonságosabb hitelesítési rendszer került bevezetésre. Erősen ajánlott a szervert a legújabb verzióra frissíteni. Ha már frissítette a szervert, jelentkezzen be újra, hogy az új hitelesítést használhassa.",
"MessageOldServerConnectionWarning": "A kiszolgáló kapcsolat beállításai egy régi felhasználói azonosítót használnak. Kérjük, törölje és adja hozzá újra ezt a szerverkapcsolatot.",
"MessageOldServerConnectionWarningHelp": "Eredetileg a 2023 júniusában megjelent 2.3.0-s verziójú adatbázis-migráció előtt állította be a kapcsolatot ehhez a kiszolgálóhoz. Egy jövőbeli kiszolgálófrissítéssel megszűnik a lehetőség, hogy ezzel a régi kapcsolattal jelentkezzen be. Kérjük, törölje a meglévő szerverkapcsolatot, és csatlakozzon újra (ugyanazzal a szervercímmel és hitelesítő adatokkal). Ha ezen az eszközön bármilyen letöltött média van, a szerverrel való szinkronizáláshoz a médiát újra le kell tölteni.",
"MessagePodcastSearchField": "Adja meg a keresési kifejezést vagy az RSS hírcsatorna URL-címét",
+15 -10
View File
@@ -3,10 +3,10 @@
"ButtonAddNewServer": "Aggiungi un nuovo server",
"ButtonAuthors": "Autori",
"ButtonBack": "Indietro",
"ButtonCancel": "Cancella",
"ButtonCancel": "Annulla",
"ButtonCancelTimer": "Annulla il timer",
"ButtonClearFilter": "Elimina filtri",
"ButtonClearLogs": "Pulisci i Log",
"ButtonClearLogs": "Cancella i registri",
"ButtonCloseFeed": "Chiudi flusso",
"ButtonCollections": "Raccolte",
"ButtonConnect": "Connetti",
@@ -27,9 +27,9 @@
"ButtonLatest": "Ultimi",
"ButtonLibrary": "Biblioteca",
"ButtonLocalMedia": "Media locali",
"ButtonLogs": "Log",
"ButtonLogs": "Registri",
"ButtonManageLocalFiles": "Gestisci file locali",
"ButtonMaskServerAddress": "Nascondi Indirizzo Server",
"ButtonMaskServerAddress": "Nascondi l'indirizzo server",
"ButtonNewFolder": "Nuova cartella",
"ButtonNextEpisode": "Prossimo episodio",
"ButtonOk": "D'accordo",
@@ -53,7 +53,7 @@
"ButtonStream": "Stream",
"ButtonSubmit": "Invia",
"ButtonSwitchServerUser": "Cambia Server/Utente",
"ButtonUnmaskServerAddress": "Mostra Indirizzo Server",
"ButtonUnmaskServerAddress": "Mostra l'indirizzo server",
"ButtonUserStats": "Statistiche utente",
"ButtonYes": "Sì",
"HeaderAccount": "Account",
@@ -118,10 +118,10 @@
"LabelClosePlayer": "Chiudi player",
"LabelCollapseSeries": "Comprimi Serie",
"LabelComplete": "Completo",
"LabelContinueBooks": "Continua Libri",
"LabelContinueEpisodes": "Continua Episodi",
"LabelContinueListening": "Continua Ascolto",
"LabelContinueReading": "Continua Lettura",
"LabelContinueBooks": "Continua i libri",
"LabelContinueEpisodes": "Continua gli episodi",
"LabelContinueListening": "Continua l'ascolto",
"LabelContinueReading": "Continua la lettura",
"LabelContinueSeries": "Continua serie",
"LabelCustomTime": "Personalizza tempo",
"LabelDescription": "Descrizione",
@@ -146,6 +146,7 @@
"LabelEndOfChapter": "Fine Capitolo",
"LabelEndTime": "Tempo alla fine",
"LabelEpisode": "Episodio",
"LabelExplicit": "Esplicito",
"LabelFeedURL": "URL del flusso",
"LabelFile": "File",
"LabelFileBirthtime": "Data di creazione",
@@ -220,6 +221,7 @@
"LabelPublishedDate": "Pubblicati {0}",
"LabelRSSFeedCustomOwnerEmail": "E-mail del proprietario personalizzato",
"LabelRSSFeedCustomOwnerName": "Nome del proprietario personalizzato",
"LabelRSSFeedOpen": "Feed RSS aperto",
"LabelRSSFeedPreventIndexing": "Impedisci l'indicizzazione",
"LabelRSSFeedSlug": "Parole chiave del flusso RSS",
"LabelRandomly": "Casualmente",
@@ -316,13 +318,16 @@
"MessageNoItems": "Nessun oggetto",
"MessageNoItemsFound": "Nessun oggetto trovato",
"MessageNoListeningSessions": "Nessuna sessione di ascolto",
"MessageNoLogs": "Nessun Log",
"MessageNoLogs": "Nessun registro",
"MessageNoMediaFolders": "Nessuna cartella media",
"MessageNoNetworkConnection": "Nessuna connessione di rete",
"MessageNoPodcastsFound": "Nessun podcast trovato",
"MessageNoSeries": "Nessuna serie",
"MessageNoUpdatesWereNecessary": "Nessun aggiornamento necessario",
"MessageNoUserPlaylists": "Non hai nessuna Playlist",
"MessageOldServerAuthReLoginRequired": "L'autenticazione è stata migliorata per la sicurezza nel server v2.26.0. Tutti gli utenti sono tenuti a re-login.",
"MessageOldServerAuthWarning": "Server utilizza l'autenticazione obsoleta",
"MessageOldServerAuthWarningHelp": "Questo server esegue una versione precedente a v2.26.0. Un sistema di autenticazione più sicuro è stato aggiunto in v2.26.0. Si raccomanda vivamente di aggiornare il server alla versione più recente. Se hai già aggiornato il server, accedi di nuovo per utilizzare la nuova autenticazione.",
"MessageOldServerConnectionWarning": "La configurazione della connessione al server utilizza un vecchio ID utente. Elimina e aggiungi nuovamente questa connessione al server.",
"MessageOldServerConnectionWarningHelp": "Originariamente hai impostato la connessione a questo server prima della migrazione del database in 2.3.0, rilasciata a giugno 2023. Un futuro aggiornamento del server rimuoverà la possibilità di accedere con questa vecchia connessione. Elimina la connessione al server esistente e riconnettiti (utilizzando lo stesso indirizzo server e le stesse credenziali). Se hai scaricato dei contenuti multimediali su questo dispositivo, i contenuti multimediali dovranno essere scaricati di nuovo per la sincronizzazione con il server.",
"MessagePodcastSearchField": "Inserisci il termine di ricerca o l'URL del feed RSS",
+154 -4
View File
@@ -1,14 +1,164 @@
{
"ButtonAdd": "追加",
"ButtonAddNewServer": "サーバーを追加",
"ButtonAuthors": "作者",
"ButtonBack": "戻る",
"ButtonCancel": "キャンセル",
"ButtonCancelTimer": "キャンセルタイマー",
"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": "はい",
"ButtonPlay": "プレイ",
"ButtonRead": "野村",
"ButtonOpenFeed": "フィードを開く",
"ButtonOverride": "上書きする",
"ButtonPause": "一時停止",
"ButtonPlay": "再生",
"ButtonPlayEpisode": "エピソードを再生",
"ButtonPlaylists": "プレイリスト",
"ButtonRead": "読む",
"ButtonReadLess": "閉じる",
"ButtonReadMore": "もっと見る",
"ButtonRemove": "削除",
"ButtonRemoveFromServer": "サーバーから削除",
"ButtonSave": "保存",
"ButtonSaveOrder": "並び替えを保存",
"ButtonSearch": "検索",
"ButtonSeries": "シリーズ",
"ButtonSetTimer": "タイマーを設定",
"ButtonSubmit": "送信",
"ButtonSwitchServerUser": "サーバー/ユーザーの切り替え",
"ButtonUnmaskServerAddress": "サーバーアドレスを表示",
"ButtonUserStats": "ユーザーの記録",
"ButtonYes": "はい",
"HeaderAccount": "アカウント",
"HeaderAdvanced": "上級者向け",
"HeaderAndroidAutoSettings": "Android Autoの設定",
"HeaderAudioTracks": "オーディオトラック",
"HeaderChapters": "チャプター",
"HeaderCollection": "コレクション",
"HeaderCollectionItems": "コレクションの項目",
"HeaderConnectionStatus": "接続状況",
"HeaderDataSettings": "データ設定",
"HeaderDetails": "詳細",
"HeaderDownloads": "ダウンロード",
"HeaderEbookFiles": "電子書籍ファイル",
"HeaderEpisodes": "エピソード",
"HeaderEreaderSettings": "電子書籍リーダーの設定",
"HeaderLatestEpisodes": "最新のエピソード",
"HeaderLibraries": "ライブラリー",
"HeaderLocalFolders": "ローカルフォルダー",
"HeaderLocalLibraryItems": "ローカルライブラリーの項目",
"HeaderNewPlaylist": "新しいプレイリスト",
"HeaderOpenRSSFeed": "RSS Feedを開く",
"HeaderPlaylist": "プレイリスト",
"HeaderPlaylistItems": "プレイリストアイテム",
"HeaderProgressSyncFailed": "進行中の同期失敗",
"HeaderRSSFeed": "RSS Feed",
"HeaderRSSFeedGeneral": "RSS 詳細",
"HeaderSelectDownloadLocation": "保存場所の選択",
"HeaderSettings": "設定",
"HeaderSleepTimer": "スリープタイマー",
"HeaderSleepTimerSettings": "スリープタイマー設定",
"HeaderStatsMinutesListeningChart": "過去7日間の視聴時間(分)",
"HeaderUserInterfaceSettings": "UI設定",
"LabelAddToPlaylist": "プレイリストの追加",
"LabelAllowSeekingOnMediaControls": "メディア通知コントロールでの位置情報の取得を許可",
"LabelAskConfirmation": "確認を求める",
"LabelAuthor": "著者",
"LabelAuthorFirstLast": "著者(名 氏)",
"LabelAuthorLastFirst": "著者(氏 名)",
"LabelAuthors": "著者",
"LabelAutoDownloadEpisodes": "エピソードの自動ダウンロード",
"LabelAutoSleepTimer": "自動スリープタイマー",
"LabelBooks": "ほん",
"LabelChapterTrack": "チャプタートラック",
"LabelChapters": "チャプター",
"LabelClosePlayer": "プレイヤーを閉じる",
"LabelComplete": "完了",
"LabelContinueListening": "続きから聞く",
"LabelDescription": "説明",
"LabelDisableAudioFadeOut": "オーディオフェードアウトを無効",
"LabelDisableAudioFadeOutHelp": "スリープタイマーの残り時間が1分を切ると、音量が徐々に小さくなります。フェードアウトしないようにするには、この設定を有効にしてください。",
"LabelDisableAutoRewind": "自動巻き戻しを無効",
"LabelDisableShakeToResetHelp": "タイマー作動中、またはタイマー終了後2分以内に端末を振ると、スリープタイマーがリセットされます。この設定を有効にすると、振ってリセットする機能を無効にできます。",
"LabelDisableVibrateOnResetHelp": "スリープタイマーがリセットされると、デバイスが振動します。この設定を有効にすると、スリープタイマーがリセットされた際に振動しなくなります。",
"LabelDownload": "ダウンロード",
"LabelDownloadUsingCellular": "モバイル通信を使用してダウンロード",
"LabelDownloaded": "ダウンロード完了",
"LabelDuration": "長さ",
"LabelEbook": "Eブック",
"LabelEbooks": "Eブック",
"LabelEnable": "有効",
"LabelEnableMp3IndexSeeking": "mp3インデックス検索を有効化",
"LabelEnableMp3IndexSeekingHelp": "この設定は、正しくシークできないMP3ファイルがある場合にのみ有効にしてください。シークの不正確さは、可変ビットレート(VBR)のMP3ファイルが原因である可能性が高いです。この設定はインデックスシークを強制し、ファイルの読み取り時に時間-バイトマッピングを構築します。大きなMP3ファイルの場合、ファイルの終わり付近をシークする際に遅延が生じる場合があります。",
"LabelEndOfChapter": "チャプターの最後",
"LabelEndTime": "終了時間",
"LabelEpisode": "エピソード",
"LabelFeedURL": "Feed URL",
"LabelFile": "ファイル",
"LabelFilename": "ファイル名",
"LabelFinished": "完了",
"LabelFolder": "フォルダ",
"LabelFontBoldness": "フォントの太さ",
"LabelFontScale": "フォントサイズ",
"LabelGenre": "ジャンル",
"LabelGenres": "ジャンル",
"LabelHost": "ホスト",
"LabelInProgress": "進行中",
"LabelInternalAppStorage": "アプリ内ストレージ",
"LabelKeepScreenAwake": "画面をスリープにしない",
"LabelLanguage": "言語",
"LabelLayout": "レイアウト",
"LabelLayoutAuto": "自動",
"LabelLayoutSinglePage": "単ページ",
"LabelLight": "ライト",
"LabelLineSpacing": "行間",
"LabelListenAgain": "再度視聴",
"LabelLocalBooks": "ローカルブック",
"LabelLocalPodcasts": "ローカルポッドキャスト",
"LabelLockPlayer": "プレイヤーのロック",
"LabelMediaType": "メディアの種類",
"LabelMoreInfo": "追加情報",
"LabelName": "名",
"LabelNarrator": "ナレーター",
"LabelNarrators": "ナレーター",
"LabelNavigateWithVolume": "音量キーで操作",
"LabelNavigateWithVolumeWhilePlaying": "再生中に音量キーで操作を許可",
"LabelNavigateWithVolumeWhilePlayingDisabled": "オフ",
"LabelNavigateWithVolumeWhilePlayingEnabled": "オン",
"LabelNewestAuthors": "最新の著者",
"LabelNewestEpisodes": "最新エピソード",
"LabelNumEpisodes": "{0} エピソード",
"LabelNumEpisodesIncomplete": "{0} エピソード, {1} 未完了",
"LabelPassword": "パスワード",
"LabelPodcast": "ポッドキャスト"
"LabelPath": "パス",
"LabelPlaybackLocal": "ローカル",
"LabelPodcast": "ポッドキャスト",
"LabelPodcasts": "ポッドキャスト",
"LabelPreventIndexing": "フィードがiTunesおよびGoogleのポッドキャストディレクトリにインデックス登録されるのを防ぎます",
"LabelPublishYear": "公開年"
}
+25 -11
View File
@@ -6,12 +6,12 @@
"ButtonCancel": "Annuleren",
"ButtonCancelTimer": "Timer annuleren",
"ButtonClearFilter": "Filter verwijderen",
"ButtonClearLogs": "Log bestanden verwijderen",
"ButtonClearLogs": "Logbestanden verwijderen",
"ButtonCloseFeed": "Feed sluiten",
"ButtonCollections": "Collecties",
"ButtonConnect": "Verbinden",
"ButtonConnectToServer": "Verbind met server",
"ButtonCreate": "Creëer",
"ButtonCreate": "Aanmaken",
"ButtonCreateBookmark": "Creëer bladwijzer",
"ButtonCreateNewPlaylist": "Maak nieuwe afspeellijst",
"ButtonDelete": "Verwijder",
@@ -27,8 +27,9 @@
"ButtonLatest": "Meest recent",
"ButtonLibrary": "Bibliotheek",
"ButtonLocalMedia": "Lokale Media",
"ButtonLogs": "Log bestanden",
"ButtonLogs": "Logbestanden",
"ButtonManageLocalFiles": "Beheer Lokale Bestanden",
"ButtonMaskServerAddress": "Serveradres maskeren",
"ButtonNewFolder": "Nieuwe map",
"ButtonNextEpisode": "Volgende aflevering",
"ButtonOk": "Akkoord",
@@ -38,13 +39,13 @@
"ButtonPlay": "Afspelen",
"ButtonPlayEpisode": "Aflevering afspelen",
"ButtonPlaylists": "Afspeellijsten",
"ButtonRead": "Lees",
"ButtonRead": "Lezen",
"ButtonReadLess": "Minder lezen",
"ButtonReadMore": "Meer lezen",
"ButtonRemove": "Verwijder",
"ButtonRemoveFromServer": "Verwijder van server",
"ButtonSave": "Opslaan",
"ButtonSaveOrder": "Volgorde bewaren",
"ButtonSaveOrder": "Volgorde opslaan",
"ButtonSearch": "Zoeken",
"ButtonSendEbookToDevice": "Verzend Ebook naar Apparaat",
"ButtonSeries": "Series",
@@ -52,6 +53,7 @@
"ButtonStream": "Streamen",
"ButtonSubmit": "Indienen",
"ButtonSwitchServerUser": "Verwissel server/gebruiker",
"ButtonUnmaskServerAddress": "Ontmasker server adres",
"ButtonUserStats": "Gebruiker statistieken",
"ButtonYes": "Ja",
"HeaderAccount": "Account",
@@ -107,7 +109,7 @@
"LabelAutoDownloadEpisodes": "Afleveringen automatisch downloaden",
"LabelAutoRewindTime": "Automatisch Terugspoelen",
"LabelAutoSleepTimer": "Automatische sleep timer",
"LabelAutoSleepTimerAutoRewind": "Automatische slaap timer automatisch terugspoelen.",
"LabelAutoSleepTimerAutoRewind": "Automatische slaap timer automatisch terugspoelen",
"LabelAutoSleepTimerAutoRewindHelp": "Wanneer de automatische slaap timer afloop zal het item terugspoelen naar de vorige positie.",
"LabelAutoSleepTimerHelp": "Wanneer er tussen de opgegeven begin- en eindtijd media wordt afgespeeld, start er automatisch een slaaptimer.",
"LabelBooks": "Boeken",
@@ -135,7 +137,7 @@
"LabelDownloadUsingCellular": "Download met mobiele data",
"LabelDownloaded": "Gedownload",
"LabelDuration": "Duur",
"LabelEbook": "Ebook",
"LabelEbook": "E-boek",
"LabelEbooks": "Eboeken",
"LabelEnable": "Inschakelen",
"LabelEnableMp3IndexSeeking": "MP3-index zoeken inschakelen",
@@ -144,6 +146,7 @@
"LabelEndOfChapter": "Einde van het Hoofdstuk",
"LabelEndTime": "Eindtijd",
"LabelEpisode": "Aflevering",
"LabelExplicit": "Expliciet",
"LabelFeedURL": "Feed URL",
"LabelFile": "Bestand",
"LabelFileBirthtime": "Aanmaaktijd bestand",
@@ -151,10 +154,10 @@
"LabelFilename": "Bestandsnaam",
"LabelFinished": "Voltooid",
"LabelFolder": "Map",
"LabelFontBoldness": "Font Boldness",
"LabelFontBoldness": "Lettertype Dikte",
"LabelFontScale": "Lettertype schaal",
"LabelGenre": "Genre",
"LabelGenres": "Genres",
"LabelGenres": "Categorieën",
"LabelHapticFeedback": "Haptische feedback",
"LabelHasEbook": "Heeft Ebook",
"LabelHasSupplementaryEbook": "Heeft aanvullend Ebook",
@@ -218,6 +221,7 @@
"LabelPublishedDate": "{0} gepubliceerd",
"LabelRSSFeedCustomOwnerEmail": "Aangepast e-mailadres eigenaar",
"LabelRSSFeedCustomOwnerName": "Aangepaste naam eigenaar",
"LabelRSSFeedOpen": "RSS Feed open",
"LabelRSSFeedPreventIndexing": "Voorkom indexering",
"LabelRSSFeedSlug": "RSS-feed slug",
"LabelRandomly": "Willekeurig",
@@ -229,7 +233,7 @@
"LabelScaleElapsedTimeBySpeed": "Schaal verstreken tijd per snelheid",
"LabelSeason": "Seizoen",
"LabelSelectADevice": "Apparaat Selecteren",
"LabelSequenceAscending": "Sequentie opflopend",
"LabelSequenceAscending": "Sequentie Oplopend",
"LabelSequenceDescending": "Sequentie Aflopend",
"LabelSeries": "Serie",
"LabelServerAddress": "Server adres",
@@ -239,6 +243,8 @@
"LabelShowAll": "Toon alle",
"LabelSize": "Grootte",
"LabelSleepTimer": "Slaaptimer",
"LabelSleepTimerAlmostDoneChime": "Speel een belsignaal indien bijna klaar",
"LabelSleepTimerAlmostDoneChimeHelp": "Speel een belsignaal af wanneer slaaptimer nog 30 seconden over heeft",
"LabelStart": "Start",
"LabelStartTime": "Starttijd",
"LabelStatsBestDay": "Beste dag",
@@ -292,12 +298,15 @@
"MessageDownloading": "Downloaden...",
"MessageDownloadingEpisode": "Aflevering aan het dowloaden",
"MessageEpisodesQueuedForDownload": "{0} aflevering(en) in de rij om te downloaden",
"MessageFailedToRefreshToken": "Token vernieuwen mislukt, opnieuw inloggen vereist",
"MessageFeedURLWillBe": "Feed URL zal {0} zijn",
"MessageFetching": "Aan het ophalen...",
"MessageFollowTheProjectOnGithub": "Volg dit project op Github",
"MessageItemDownloadCompleteFailedToCreate": "Itemdownload voltooid, maar het aanmaken van een bibliotheekitem is mislukt",
"MessageItemMissing": "Item ontbreekt en moet opgelost worden op de server. Een item is typisch gemarkeerd als ontbrekend wanneer het file pad niet toegankelijk is.",
"MessageLoading": "Aan het laden...",
"MessageLoadingServerData": "Server data laden...",
"MessageLocalFolderDescription": "'Interne app-opslag' is alleen toegankelijk voor deze app. Deze app ondersteunt alleen media die rechtstreeks via de app is gedownload. Gedeelde opslagmappen kunnen worden gebruikt om andere apps toegang te geven tot media die door deze app zijn gedownload.",
"MessageMarkAsFinished": "Markeer als Voltooid",
"MessageMediaLinkedToADifferentServer": "Media is gekoppeld aan een Audiobookshelf-server op een ander adres ({0}). De voortgang wordt gesynchroniseerd wanneer er verbinding is met dit serveradres.",
"MessageMediaLinkedToADifferentUser": "Media is gekoppeld aan deze server, maar is gedownload door een andere gebruiker. De voortgang wordt alleen gesynchroniseerd met de gebruiker die het heeft gedownload.",
@@ -310,15 +319,20 @@
"MessageNoItems": "Geen onderdelen",
"MessageNoItemsFound": "Geen onderdelen gevonden",
"MessageNoListeningSessions": "Geen luistersessies",
"MessageNoLogs": "Geen log bestanden",
"MessageNoLogs": "Geen logbestanden",
"MessageNoMediaFolders": "Geen Media Folders",
"MessageNoNetworkConnection": "Geen netwerk connectie",
"MessageNoPodcastsFound": "Geen podcasts gevonden",
"MessageNoSeries": "Geen series",
"MessageNoUpdatesWereNecessary": "Geen bijwerkingen waren noodzakelijk",
"MessageNoUserPlaylists": "Je hebt geen afspeellijsten",
"MessageOldServerAuthReLoginRequired": "Authenticatie is verbeterd om de veiligheid te verhogen in server versie v2.26.0. Alle gebruikers moeten opnieuw inloggen.",
"MessageOldServerAuthWarning": "De server gebruikt een verouderde authenticatie versie",
"MessageOldServerAuthWarningHelp": "Deze server draait een versie ouder dan v2.26.0. Een veiliger authenticatie systeem is geïntroduceerd in v2.26.0. Het wordt met klem aangeraden om de server te updaten naar de laatste versie. Mocht de server al geüpdatet zijn, log dan opnieuw in om het nieuwe authenticatie mechanisme te gebruiken.",
"MessageOldServerConnectionWarning": "Server connectie configuratie gebruikt een oud user ID. Gelieve deze server connectie te verwijderen en opnieuw toe te voegen.",
"MessageOldServerConnectionWarningHelp": "U hebt de verbinding met deze server oorspronkelijk ingesteld vóór de databasemigratie in versie 2.3.0, uitgebracht in juni 2023. Een toekomstige serverupdate zal de mogelijkheid om in te loggen met deze oude verbinding verwijderen. Verwijder de bestaande serververbinding en maak opnieuw verbinding (met hetzelfde serveradres en dezelfde inloggegevens). Als u gedownloade media op dit apparaat hebt, moet u deze opnieuw downloaden om te synchroniseren met de server.",
"MessagePodcastSearchField": "Zoekterm of RSS feed URL invullen",
"MessageProgressSyncFailed": "De meest recente poging om je luistervoortgang aan de server te rapporteren is mislukt. Verzoeken om voortgangssynchronisatie worden nog steeds elke 15 seconden tot 1 minuut uitgevoerd terwijl de media wordt afgespeeld.",
"MessageReportBugsAndContribute": "Rapporteer bugs, vraag functionaliteiten aan en draag bij op",
"MessageSeriesAlreadyDownloaded": "Je hebt alle boeken in deze serie al gedownload.",
"MessageSeriesDownloadConfirm": "Ontbrekende {0} boek(en) met {1} bestand(en), in totaal {2}, downloaden naar map {3}?",
+81 -20
View File
@@ -1,7 +1,7 @@
{
"ButtonAdd": "Legg til",
"ButtonAddNewServer": "Legg til ny tjener",
"ButtonAuthors": "Forfatter",
"ButtonAuthors": "Forfattere",
"ButtonBack": "Tilbake",
"ButtonCancel": "Avbryt",
"ButtonCancelTimer": "Avbryt tidsur",
@@ -15,7 +15,7 @@
"ButtonCreateBookmark": "Opprett bokmerke",
"ButtonCreateNewPlaylist": "Opprett ny spilleliste",
"ButtonDelete": "Slett",
"ButtonDeleteLocalEpisode": "Slett episode på enheten",
"ButtonDeleteLocalEpisode": "Slett nedlastet episode",
"ButtonDeleteLocalFile": "Slett fil på enheten",
"ButtonDeleteLocalItem": "Slett element på enheten",
"ButtonDisableAutoTimer": "Skru av automatisk tidsur",
@@ -29,7 +29,7 @@
"ButtonLocalMedia": "Lokal media",
"ButtonLogs": "Logger",
"ButtonManageLocalFiles": "Administrer Lokale Filer",
"ButtonMaskServerAddress": "Skjul serveradresse",
"ButtonMaskServerAddress": "Skjul tjeneradresse",
"ButtonNewFolder": "Ny mappe",
"ButtonNextEpisode": "Neste episode",
"ButtonOk": "Ok",
@@ -53,7 +53,7 @@
"ButtonStream": "Strøm",
"ButtonSubmit": "Send inn",
"ButtonSwitchServerUser": "Bytt tjener/bruker",
"ButtonUnmaskServerAddress": "Vis serveradresse",
"ButtonUnmaskServerAddress": "Vis tjeneradresse",
"ButtonUserStats": "Brukerstatistikk",
"ButtonYes": "Ja",
"HeaderAccount": "Konto",
@@ -73,19 +73,20 @@
"HeaderLatestEpisodes": "Siste episoder",
"HeaderLibraries": "Biblioteker",
"HeaderLocalFolders": "Lokale Mapper",
"HeaderLocalLibraryItems": "Elementer i bibliotek på enheten",
"HeaderLocalLibraryItems": "Nedlastede elementer",
"HeaderNewPlaylist": "Ny spilleliste",
"HeaderOpenRSSFeed": "Åpne RSS Feed",
"HeaderPlaybackSettings": "Avspillingsinnstillinger",
"HeaderPlaylist": "Spilleliste",
"HeaderPlaylistItems": "Spillelisteelement",
"HeaderProgressSyncFailed": "Fremskritts-synkronisering feilet",
"HeaderRSSFeed": "RSS-informasjonskanal",
"HeaderRSSFeedGeneral": "RSS Detailer",
"HeaderRSSFeedIsOpen": "RSS Feed er åpen",
"HeaderSelectDownloadLocation": "Velg Nedlastningssted",
"HeaderSettings": "Innstillinger",
"HeaderSleepTimer": "Sove timer",
"HeaderSleepTimerSettings": "Innstillinger for Søvntidtaker",
"HeaderSleepTimerSettings": "Innstillinger for søvntidtaker",
"HeaderStatsMinutesListeningChart": "Minutter lyttet (siste 7 dagene)",
"HeaderStatsRecentSessions": "Siste sesjoner",
"HeaderTableOfContents": "Innholdsfortegnelse",
@@ -93,9 +94,13 @@
"HeaderYourStats": "Din statistikk",
"LabelAddToPlaylist": "Legg til i spilleliste",
"LabelAddedAt": "Lagt Til",
"LabelAddedDate": "La til {0}",
"LabelAll": "Alle",
"LabelAllowSeekingOnMediaControls": "Tillat posisjonsblafring i mediamerknadskontroller",
"LabelAlways": "Alltid",
"LabelAndroidAutoBrowseLimitForGrouping": "Alfabetisk nedtreksmeny-grense",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "Ikke bruk alfabetisk nedtrekksmeny når det er færre enn dette antallet elementer",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "Rekkefølge på bokserie",
"LabelAskConfirmation": "Spør om bekreftelse",
"LabelAuthor": "Forfatter",
"LabelAuthorFirstLast": "Forfatter (Fornavn Etternavn)",
@@ -105,7 +110,7 @@
"LabelAutoRewindTime": "Automatisk tilbakespolingstid",
"LabelAutoSleepTimer": "Automatisk søvntidsur",
"LabelAutoSleepTimerAutoRewind": "Automatisk tilbakespoling for automatisk søvntidsur",
"LabelAutoSleepTimerAutoRewindHelp": "Når automatisk søvntidsur fullføres vil avspilling av elementet automatisk spole tilbake posisjonen din.",
"LabelAutoSleepTimerAutoRewindHelp": "Når automatisk søvntidsur fullføres og du gjenopptar avspillingen vil posisjonen din automatisk spoles tilbake.",
"LabelAutoSleepTimerHelp": "Ved avspilling av media mellom angitt start og slutt vil automatisk søvntidsur starte automatisk.",
"LabelBooks": "Bøker",
"LabelChapterTrack": "Kapittelspor",
@@ -121,10 +126,10 @@
"LabelCustomTime": "Egendefinert tid",
"LabelDescription": "Beskrivelse",
"LabelDisableAudioFadeOut": "Skru av automatisk uttoning",
"LabelDisableAudioFadeOutHelp": "Lydstyrke vil minkes når det er mindre enn ett minutt igjen på søvntidsuret. Skru av dette for å ikke ha uttoning.",
"LabelDisableAudioFadeOutHelp": "Lydstyrke vil minkes når det er mindre enn ett minutt igjen på søvntidsuret. Aktiver denne innstillingen for å ikke ha uttoning.",
"LabelDisableAutoRewind": "Skru av automatisk tilbakespoling",
"LabelDisableShakeToReset": "Skru av risting for å tilbakestille",
"LabelDisableShakeToResetHelp": "Risting av enheten mens tidsuret kjører, eller opptil to minutter etter utløp av tidsuret tilbakestiller søvntidsuret. Skru på denne innstillinger for å skru av risting for å tilbakestille.",
"LabelDisableShakeToResetHelp": "Rister du enheten mens tidsuret kjører, eller opptil to minutter etter utløp av tidsuret, tilbakestillers søvntidsuret. Aktiver denne innstillinger for å skru av risting for å tilbakestille.",
"LabelDisableVibrateOnReset": "Skru av vibrering ved tilbakestilling",
"LabelDisableVibrateOnResetHelp": "Når søvntidsuret blir tilbakestilt vil enheten din vibrere. Skru på denne innstillingen for å skru av vibrering når tidsuret tilbakestilles.",
"LabelDiscover": "Oppdagelse",
@@ -140,6 +145,8 @@
"LabelEnd": "Slutt",
"LabelEndOfChapter": "Slutt på kapittel",
"LabelEndTime": "Slutt-tid",
"LabelEpisode": "Episode",
"LabelExplicit": "Eksplisitt",
"LabelFeedURL": "Feed Adresse",
"LabelFile": "Fil",
"LabelFileBirthtime": "Fil Opprettelsesdato",
@@ -151,63 +158,94 @@
"LabelFontScale": "Font størrelse",
"LabelGenre": "Sjanger",
"LabelGenres": "Sjangers",
"LabelHapticFeedback": "Føllighetsrespons",
"LabelHapticFeedback": "Taktil respons",
"LabelHasEbook": "Har e-bok",
"LabelHasSupplementaryEbook": "Har komplimentær e-bok",
"LabelHasSupplementaryEbook": "Har supplerende e-bok",
"LabelHeavy": "Tung",
"LabelHigh": "Høy",
"LabelHost": "Tjener",
"LabelInProgress": "I gang",
"LabelIncomplete": "Ufullstendig",
"LabelInternalAppStorage": "Programlager på enheten",
"LabelInternalAppStorage": "Intern lagringsplass",
"LabelJumpBackwardsTime": "Tilbakespolingstid",
"LabelJumpForwardsTime": "Foroverspolingstid",
"LabelKeepScreenAwake": "Hold skjerm våken",
"LabelLanguage": "Språk",
"LabelLayout": "Utseende",
"LabelLayoutAuto": "Automatisk",
"LabelLayoutSinglePage": "Enkeltside",
"LabelLight": "Lett",
"LabelLineSpacing": "Linjemellomrom",
"LabelListenAgain": "Lytt igjen",
"LabelLocalBooks": "Bøker på enheten",
"LabelLocalBooks": "Nedlastede bøker",
"LabelLocalPodcasts": "Nedlastede Podcaster",
"LabelLockOrientation": "Lås rotasjon",
"LabelLockPlayer": "Lås avspiller",
"LabelLow": "Lav",
"LabelMediaType": "Medie type",
"LabelMedium": "Middels",
"LabelMissing": "Mangler",
"LabelMore": "Mer",
"LabelMoreInfo": "Mer info",
"LabelName": "Navn",
"LabelNarrator": "Forteller",
"LabelNarrators": "Fortellere",
"LabelNavigateWithVolume": "Naviger med volum-knapper",
"LabelNavigateWithVolumeMirrored": "Speilet",
"LabelNavigateWithVolumeWhilePlaying": "Navigere med volumknapper under avspilling",
"LabelNavigateWithVolumeWhilePlayingDisabled": "Av",
"LabelNavigateWithVolumeWhilePlayingEnabled": "På",
"LabelNever": "Aldri",
"LabelNewestAuthors": "Nyeste forfattere",
"LabelNewestEpisodes": "Nyeste episoder",
"LabelNo": "Nei",
"LabelNotFinished": "Ikke fullført",
"LabelNotStarted": "Ikke startet",
"LabelNumEpisodes": "{0} episoder",
"LabelNumEpisodesIncomplete": "{0} episoder, {1} ufullstendige",
"LabelNumberOfEpisodes": "# episoder",
"LabelOff": "Av",
"LabelOn": "På",
"LabelPassword": "Passord",
"LabelPath": "Sti",
"LabelPlaybackDirect": "Direkte",
"LabelPlaybackLocal": "Lokal",
"LabelPlaybackSpeed": "Avspillingshastighet",
"LabelPlaybackTranscode": "Transkoding",
"LabelPlaybackTranscode": "Konvertering",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcaster",
"LabelPreventIndexing": "Forhindre at din feed fra å bli indeksert av iTunes og Google podcast kataloger",
"LabelProgress": "Framgang",
"LabelPubDate": "Publiseringsdato",
"LabelPublishYear": "Publikasjonsår",
"LabelPublishedDate": "Publisert {0}",
"LabelRSSFeedCustomOwnerEmail": "Tilpasset eier e-post",
"LabelRSSFeedCustomOwnerName": "Tilpasset eier Navn",
"LabelRSSFeedOpen": "RSS-strøm åpen",
"LabelRSSFeedPreventIndexing": "Forhindre indeksering",
"LabelRSSFeedSlug": "RSS-informasjonskanalunderadresse",
"LabelRandomly": "Tilfeldighet",
"LabelRead": "Les",
"LabelReadAgain": "Les igjen",
"LabelRecentSeries": "Nylige serier",
"LabelRecentlyAdded": "Nylig tillagt",
"LabelRemoveFromPlaylist": "Fjern fra spilleliste",
"LabelScaleElapsedTimeBySpeed": "Skaler forløpt tid etter hastighet",
"LabelSeason": "Sesong",
"LabelSelectADevice": "Velg en enhet",
"LabelSequenceAscending": "Stigende rekkefølge",
"LabelSequenceDescending": "Synkende rekkefølge",
"LabelSeries": "Serier",
"LabelServerAddress": "Tjener-adresse",
"LabelSetEbookAsPrimary": "Sett som primær",
"LabelSetEbookAsSupplementary": "Sett som supplerende",
"LabelShakeSensitivity": "Ristingsterskel",
"LabelShowAll": "Vis alt",
"LabelSize": "Størrelse",
"LabelSleepTimer": "Sove-timer",
"LabelSleepTimerAlmostDoneChime": "Spill en lyd når nesten ferdig",
"LabelSleepTimerAlmostDoneChimeHelp": "Spill av en lyd når søvnklokken har 30 sekunder igjen",
"LabelStart": "Start",
"LabelStartTime": "Start Tid",
"LabelStatsBestDay": "Beste dag",
"LabelStatsDailyAverage": "Daglig gjennomsnitt",
@@ -219,8 +257,10 @@
"LabelStatsMinutesListening": "Minutter lyttet",
"LabelStatsWeekListening": "Uker lyttet",
"LabelStreamingUsingCellular": "Strømmer via mobildata",
"LabelTag": "Merke",
"LabelTags": "Tagger",
"LabelTheme": "Tema",
"LabelThemeBlack": "Svart",
"LabelThemeDark": "Mørk",
"LabelThemeLight": "Lys",
"LabelTimeRemaining": "{0} gjennstående",
@@ -228,12 +268,16 @@
"LabelTotalSize": "Total størrelse",
"LabelTotalTrack": "Totalt antall spor",
"LabelTracks": "Spor",
"LabelType": "Type",
"LabelUnknown": "Ukjent",
"LabelUnlockPlayer": "Lås opp avspiller",
"LabelUseBookshelfView": "Bruk bokhyllevisning",
"LabelUser": "Bruker",
"LabelUsername": "Brukernavn",
"LabelVeryHigh": "Veldig høy",
"LabelVeryLow": "Veldig lav",
"LabelYearReviewHide": "Skjul oppsummering av året",
"LabelYearReviewShow": "Vis oppsummering av året",
"LabelYourBookmarks": "Dine bokmerker",
"LabelYourProgress": "Din fremgang",
"MessageAndroid10Downloads": "Android 10 og tidligere bruker lagringsplass på enheten for nedlastninger.",
@@ -243,13 +287,14 @@
"MessageBookshelfEmpty": "Tom bokhylle",
"MessageConfirmDeleteLocalEpisode": "Fjen episoden «{0}» fra enheten? Filen på tjeneren forblir som den er.",
"MessageConfirmDeleteLocalFiles": "Fjern dette elementets filer fra enheten? Filene på tjeneren og framdriften forblir som den er.",
"MessageConfirmDiscardProgress": "Tilbakestill framgang?",
"MessageConfirmDisableAutoTimer": "Er du sikker på at du vil slå av den automatiske tidsuret for resten av dagen? Tidsuret vil bli reaktivert på slutten av perioden til dette automatiske søvn-tidsuret, eller hvis du starter appen på nytt.",
"MessageConfirmDiscardProgress": "Er du sikker på at du vil tilbakestille fremdriften din?",
"MessageConfirmDownloadUsingCellular": "Last ned over mobildata selv om kostnader kan tilkomme?",
"MessageConfirmMarkAsFinished": "Merk element som fullført?",
"MessageConfirmRemoveBookmark": "Fjern bokmerke?",
"MessageConfirmMarkAsFinished": "Er du sikker på at du vil merke element som fullført?",
"MessageConfirmRemoveBookmark": "Er du sikker på at du vil fjerne bokmerket?",
"MessageConfirmStreamingUsingCellular": "Strøm via mobildata selv om kostnader kan tilkomme?",
"MessageDiscardProgress": "Forkast framdrift",
"MessageDownloadCompleteProcessing": "Behandler …",
"MessageDownloadCompleteProcessing": "Nedlasting fullført. Behandler …",
"MessageDownloading": "Laster ned …",
"MessageDownloadingEpisode": "Laster ned episode",
"MessageEpisodesQueuedForDownload": "{0} Episode(r) lagt til i kø for nedlasting",
@@ -257,28 +302,44 @@
"MessageFetching": "Henter...",
"MessageFollowTheProjectOnGithub": "Følg prosjektet på GitHub",
"MessageItemDownloadCompleteFailedToCreate": "Element nedlastet, men kunne ikke opprette bibliotekselement",
"MessageItemMissing": "Elementet mangler og må bli korrigert på tjeneren. Vanligvis blir et element markert som manglende hvis filens lokasjon ikke er tilgjengelig.",
"MessageLoading": "Laster...",
"MessageLoadingServerData": "Laster inn tjenerdata …",
"MessageLocalFolderDescription": "'Intern lagringsminne for app' er bare tilgjengelig for denne appen. Denne appen støtter bare medier lastet ned direkte gjennom appen. Mapper for delt lagringsminne kan bli brukt for å gi andre apper tilgang til media lastet ned av denne appen.",
"MessageMarkAsFinished": "Marker som Fullført",
"MessageMediaLinkedToADifferentServer": "Media er lenket til en Audiobookshelf-tjener på en annen adresse ({0}). Framdriften vil bli synkronisert når tilkoblet denne tjeneradressen.",
"MessageMediaLinkedToADifferentServer": "Media er lenket til en Audiobookshelf-tjener på en annen adresse ({0}). Framdriften vil bli synkronisert når du blir tilkoblet denne tjeneradressen.",
"MessageMediaLinkedToADifferentUser": "Media er lenket til denne tjeneren, men ble lastet ned av en annen bruker. Framdriften vil kun synkroniseres til brukeren som lastet den ned.",
"MessageMediaLinkedToServer": "Lenket til tjener {0}",
"MessageMediaLinkedToThisServer": "Nedlastet media er lenket til denne tjeneren",
"MessageMediaNotLinkedToServer": "Media er ikke lenket til en Audiobookshelf-tjener. Ingen fremdrift vil bli synkronisert.",
"MessageNoBookmarks": "Ingen bokmerker",
"MessageNoChapters": "Ingen kapitler",
"MessageNoCollections": "Ingen samlinger",
"MessageNoItems": "Ingen gjenstander",
"MessageNoItemsFound": "Ingen gjenstander funnet",
"MessageNoListeningSessions": "Ingen Lyttesesjoner",
"MessageNoLogs": "Ingen logger",
"MessageNoMediaFolders": "Ingen mediamapper",
"MessageNoNetworkConnection": "Ingen nettverkstilkobling",
"MessageNoPodcastsFound": "Ingen podcaster funnet",
"MessageNoSeries": "Ingen serier",
"MessageNoUpdatesWereNecessary": "Ingen oppdatering var nødvendig",
"MessageNoUserPlaylists": "Du har ingen spillelister",
"MessageOldServerAuthReLoginRequired": "Autentisering har blitt forbedret for sikkerhet i tjener-versjon v2.26.0. Alle brukere må logge inn igjen.",
"MessageOldServerAuthWarning": "Tjeneren bruker utdatert autentisering",
"MessageOldServerAuthWarningHelp": "Denne tjeneren kjører en versjon som er eldre enn v2.26.0. Et mer sikkert autentiseringssystem ble lagt til i v.2.26.0. Det anbefales sterkt å oppdatere tjeneren til siste versjon. Hvis du allerede har oppdatert tjeneren, logg inn igjen for å bruke den nye autentiseringen.",
"MessageOldServerConnectionWarning": "Konfigurasjonen til tjener-tilkoblingen bruker en gammel bruker ID. Vennligst slett og legg til igjen denne tjener-tilkoblingen.",
"MessageOldServerConnectionWarningHelp": "Du opprettet opprinnelig tilkoblingen til denne tjeneren før database-migreringen i versjon 2.3.0, som ble lansert i juni 2023. En fremtidig oppdatering av tjeneren vil fjerne muligheten for å logge inn med denne gamle tilkoblingen. Vennligst slett den eksisterende tjenertilkoblingen og koble til på nytt (bruk samme tjeneradresse og påloggingsinformasjon). Hvis du har nedlastede medier på denne enheten, må disse lastes ned på nytt for å synkronisere med tjeneren.",
"MessagePodcastSearchField": "Skriv inn søkeord eller URL til en RSS-strøm",
"MessageProgressSyncFailed": "Det siste forsøket på å rapportere lyttefremdriften til tjeneren mislyktes. Forsøk på å synkronisere fremdriften vil fortsette hvert 15. sekund til hvert minutt så lenge mediet spilles av.",
"MessageReportBugsAndContribute": "Rapporter feil, forespør funksjoner og tillegg og bidra på",
"MessageSeriesAlreadyDownloaded": "Du har allerede lastet ned alle bøkene i denne serien.",
"MessageSeriesDownloadConfirm": "Laste ned manglende {0} bok/bøker med {1} fil(er), totalt {2} til mappe {3}?",
"MessageSeriesDownloadConfirmIos": "Laste ned manglende {0} bok/bøker med {1} fil(er), totalt {2}?",
"MessageSocketConnectedOverMeteredCellular": "Socket tilkoblet over kvotebasert mobildata",
"MessageSocketConnectedOverMeteredWifi": "Socket tilkoblet over kvotebasert Wi-Fi",
"MessageSocketConnectedOverUnmeteredWifi": "Socket tilkoblet over ubegrenset Wi-Fi",
"MessageSocketConnectedOverUnmeteredCellular": "Tilkobling via ubegrenset mobildata",
"MessageSocketConnectedOverUnmeteredWifi": "Tilkoblet over ubegrenset Wi-Fi",
"MessageSocketNotConnected": "Socket ikke tilkoblet",
"NoteRSSFeedPodcastAppsHttps": "Advarsel! De fleste podcast applikasjoner trenger RSS feed URL som bruker HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Advarsel! 1 eller flere av episodene har ikke publikasjonsdato. Noen podcast applikasjoner trenger dette.",
+23 -4
View File
@@ -6,6 +6,7 @@
"ButtonCancel": "Anuluj",
"ButtonCancelTimer": "Anuluj wyłącznik",
"ButtonClearFilter": "Wyczyść filtr",
"ButtonClearLogs": "Wyczyść Logi",
"ButtonCloseFeed": "Zamknij kanał",
"ButtonCollections": "Kolekcje",
"ButtonConnect": "Połącz",
@@ -26,7 +27,9 @@
"ButtonLatest": "Aktualna wersja",
"ButtonLibrary": "Biblioteka",
"ButtonLocalMedia": "Media lokalne",
"ButtonLogs": "Logi",
"ButtonManageLocalFiles": "Zarządzaj lokalnymi plikami",
"ButtonMaskServerAddress": "Ukryj adres serwera",
"ButtonNewFolder": "Nowy folder",
"ButtonNextEpisode": "Następny odcinek",
"ButtonOk": "Ok",
@@ -50,6 +53,7 @@
"ButtonStream": "Strumieniuj",
"ButtonSubmit": "Zapisz",
"ButtonSwitchServerUser": "Przełącz serwer/użytkownika",
"ButtonUnmaskServerAddress": "Pokaż adres serwera",
"ButtonUserStats": "Statystyki użytkownika",
"ButtonYes": "Tak",
"HeaderAccount": "Konto",
@@ -94,6 +98,8 @@
"LabelAll": "Wszystkie",
"LabelAllowSeekingOnMediaControls": "Zezwól na wyszukiwanie pozycji na ekranie blokady",
"LabelAlways": "Zawsze",
"LabelAndroidAutoBrowseLimitForGrouping": "Limit podziału listy alfabetycznej",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "Nie używaj podziału alfabetycznego, jeśli liczba pozycji do pokazania jest mniejsza niż podana",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "Kolejność książek w serii",
"LabelAskConfirmation": "Pytaj o potwierdzenie",
"LabelAuthor": "Autor",
@@ -135,7 +141,7 @@
"LabelEbooks": "Ebooki",
"LabelEnable": "Włącz",
"LabelEnableMp3IndexSeeking": "Włącz wyszukiwanie indeksu mp3",
"LabelEnableMp3IndexSeekingHelp": "To ustawienie powinno być włączone tylko wtedy, gdy masz pliki mp3, które nie są poprawnie wyszukiwane. Niedokładne wyszukiwanie jest najprawdopodobniej spowodowane plikami MP3 o zmiennym birate (VBR). To ustawienie wymusi wyszukiwanie indeksu, w którym mapowanie czasu do bajtu jest kompilowane podczas odczytywania pliku. W niektórych przypadkach w przypadku dużych plików MP3 wystąpi opóźnienie podczas wyszukiwania w kierunku końca pliku.",
"LabelEnableMp3IndexSeekingHelp": "To ustawienie powinno być włączone tylko wtedy, gdy masz pliki mp3, które nie są poprawnie wyszukiwane. Niedokładne wyszukiwanie jest najprawdopodobniej spowodowane plikami MP3 o zmiennym bitrate (VBR). To ustawienie wymusi wyszukiwanie indeksu, w którym mapowanie czasu do bajtu jest kompilowane podczas odczytywania pliku. W niektórych przypadkach w przypadku dużych plików MP3 wystąpi opóźnienie podczas wyszukiwania w kierunku końca pliku.",
"LabelEnd": "Zakończ",
"LabelEndOfChapter": "Koniec rozdziału",
"LabelEndTime": "Koniec czasu",
@@ -211,9 +217,10 @@
"LabelProgress": "Postęp",
"LabelPubDate": "Data publikacji",
"LabelPublishYear": "Rok publikacji",
"LabelPublishedDate": "Opublikowane",
"LabelRSSFeedCustomOwnerEmail": "Własny email",
"LabelRSSFeedCustomOwnerName": "Własna nazwa",
"LabelPublishedDate": "Opublikowano {0}",
"LabelRSSFeedCustomOwnerEmail": "Email właściciela",
"LabelRSSFeedCustomOwnerName": "Nazwa właściciela",
"LabelRSSFeedOpen": "Otwarty Kanał RSS",
"LabelRSSFeedPreventIndexing": "Zapobiegaj indeksowaniu",
"LabelRSSFeedSlug": "Numer Kanału RSS",
"LabelRandomly": "Losowo",
@@ -235,6 +242,8 @@
"LabelShowAll": "Pokaż wszystko",
"LabelSize": "Rozmiar",
"LabelSleepTimer": "Wyłącznik czasowy",
"LabelSleepTimerAlmostDoneChime": "Odtwórz dźwięk tuż przed zakończeniem",
"LabelSleepTimerAlmostDoneChimeHelp": "Odtwórz dźwięk 30 sekund przed zakończeniem czasu uśpienia",
"LabelStart": "Rozpocznij",
"LabelStartTime": "Czas rozpoczęcia",
"LabelStatsBestDay": "Najlepszy dzień",
@@ -250,6 +259,7 @@
"LabelTag": "Znacznik",
"LabelTags": "Znaczniki",
"LabelTheme": "Motyw",
"LabelThemeBlack": "Czarny",
"LabelThemeDark": "Ciemny",
"LabelThemeLight": "Jasny",
"LabelTimeRemaining": "Pozostało {0}",
@@ -276,6 +286,7 @@
"MessageBookshelfEmpty": "Półka pusta",
"MessageConfirmDeleteLocalEpisode": "Usunąć lokalny odcinek \"{0}\" ze swojego urządzenia? Nie będzie to miało wpływu na plik na serwerze.",
"MessageConfirmDeleteLocalFiles": "Usunąć lokalne pliki tego elementu ze swojego urządzenia? Nie będzie to miało wpływu na pliki na serwerze i Twoje postępy.",
"MessageConfirmDisableAutoTimer": "Czy na pewno chcesz wyłączyć automatyczny timer na resztę dnia? Timer zostanie ponownie aktywowany po upływie czasu automatycznego uśpienia lub po ponownym uruchomieniu aplikacji..",
"MessageConfirmDiscardProgress": "Na pewno chcesz zresetować postęp?",
"MessageConfirmDownloadUsingCellular": "Zamierzasz pobrać poprzez dane komórkowe. Może to wpłynąć na wysokość rachunku. Czy chcesz kontynuować?",
"MessageConfirmMarkAsFinished": "Na pewno chcesz oznaczyć ten element jako ukończony?",
@@ -290,6 +301,7 @@
"MessageFetching": "Pobieranie...",
"MessageFollowTheProjectOnGithub": "Obserwuj projekt na Github'ie",
"MessageItemDownloadCompleteFailedToCreate": "Pobieranie elementu zostało zakończone, ale nie udało się utworzyć elementu biblioteki",
"MessageItemMissing": "Brakujący element, należy go naprawić na serwerze. Zazwyczaj element jest oznaczany jako brakujący, ponieważ ścieżki do plików nie są dostępne.",
"MessageLoading": "Ładowanie...",
"MessageLoadingServerData": "Ładowanie danych serwera...",
"MessageMarkAsFinished": "Oznacz jako ukończone",
@@ -304,13 +316,20 @@
"MessageNoItems": "Brak elementów",
"MessageNoItemsFound": "Nie znaleziono żadnych elementów",
"MessageNoListeningSessions": "Brak sesji słuchania",
"MessageNoLogs": "Brak logów",
"MessageNoMediaFolders": "Brak folderów z mediami",
"MessageNoNetworkConnection": "Brak połączenia sieciowego",
"MessageNoPodcastsFound": "Nie znaleziono podcastów",
"MessageNoSeries": "Brak serii",
"MessageNoUpdatesWereNecessary": "Brak wymaganych aktualizacji",
"MessageNoUserPlaylists": "Nie masz żadnych list odtwarzania",
"MessageOldServerAuthReLoginRequired": "Uwierzytelnianie zostało ulepszone pod kątem bezpieczeństwa w serwerze w wersji 2.26.0. Wszyscy użytkownicy muszą się ponownie zalogować.",
"MessageOldServerAuthWarning": "Serwer korzysta z przestarzałego uwierzytelniania",
"MessageOldServerAuthWarningHelp": "Ten serwer działa w wersji starszej niż 2.26.0. W wersji 2.26.0 dodano bezpieczniejszy system uwierzytelniania. Zdecydowanie zaleca się aktualizację serwera do najnowszej wersji. Jeżeli dokonałeś już aktualizacji serwera, zaloguj się ponownie, aby użyć nowego uwierzytelniania.",
"MessageOldServerConnectionWarning": "Konfiguracja połączenia z serwerem używa starego identyfikatora użytkownika. Usuń i dodaj ponownie to połączenie z serwerem.",
"MessageOldServerConnectionWarningHelp": "Połączenie z tym serwerem zostało pierwotnie skonfigurowane przed migracją bazy danych w wersji 2.3.0, wydanej w czerwcu 2023 r. Przyszła aktualizacja serwera usunie możliwość logowania się przy użyciu tego starego połączenia. Usuń istniejące połączenie z serwerem i połącz się ponownie (używając tego samego adresu serwera i danych logowania). Jeśli na tym urządzeniu znajdują się pobrane pliki multimedialne, konieczne będzie ich ponowne pobranie w celu synchronizacji z serwerem.",
"MessagePodcastSearchField": "Wprowadź wyszukiwane hasło lub adres URL kanału RSS",
"MessageProgressSyncFailed": "Ostatnia próba zgłoszenia postępów słuchania do serwera nie powiodła się. Żądania synchronizacji postępu będą wysyłane co 15 sekund do 1 minuty podczas odtwarzania multimediów.",
"MessageReportBugsAndContribute": "Zgłoś błędy, pomysły i pomóż rozwijać aplikację na",
"MessageSeriesAlreadyDownloaded": "Seria książek już pobrana.",
"MessageSeriesDownloadConfirm": "Pobrać serię?",
+359 -1
View File
@@ -1 +1,359 @@
{}
{
"ButtonAdd": "Adaugă",
"ButtonAddNewServer": "Adaugă server nou",
"ButtonAuthors": "Autori",
"ButtonBack": "Înapoi",
"ButtonCancel": "Anulează",
"ButtonCancelTimer": "Anulează timerul",
"ButtonClearFilter": "Șterge filtrul",
"ButtonClearLogs": "Șterge jurnalele",
"ButtonCloseFeed": "Închide sursa",
"ButtonCollections": "Colecții",
"ButtonConnect": "Conectează",
"ButtonConnectToServer": "Conectează la server",
"ButtonCreate": "Creează",
"ButtonCreateBookmark": "Creează semn de carte",
"ButtonCreateNewPlaylist": "Creează playlist nou",
"ButtonDelete": "Șterge",
"ButtonDeleteLocalEpisode": "Șterge episodul local",
"ButtonDeleteLocalFile": "Șterge fișierul local",
"ButtonDeleteLocalItem": "Șterge elementul local",
"ButtonDisableAutoTimer": "Dezactivează timerul automat",
"ButtonDisconnect": "Deconectează",
"ButtonGoToWebClient": "Deschide în browse",
"ButtonHistory": "Istoric",
"ButtonHome": "Acasă",
"ButtonIssues": "Erori",
"ButtonLatest": "Noutăți",
"ButtonLibrary": "Bibliotecă",
"ButtonLocalMedia": "Media locală",
"ButtonLogs": "Jurnale",
"ButtonManageLocalFiles": "Gestionare fișiere locale",
"ButtonMaskServerAddress": "Nu arăta adresa serverului",
"ButtonNewFolder": "Dosar nou",
"ButtonNextEpisode": "Episodul următor",
"ButtonOk": "OK",
"ButtonOpenFeed": "Vezi noutățile",
"ButtonOverride": "Înlocuiește",
"ButtonPause": "Pauză",
"ButtonPlay": "Redă",
"ButtonPlayEpisode": "Redă episodul",
"ButtonPlaylists": "Liste",
"ButtonRead": "Citește",
"ButtonReadLess": "Mai puțin",
"ButtonReadMore": "Afișează mai mult",
"ButtonRemove": "Elimină",
"ButtonRemoveFromServer": "Elimină de pe server",
"ButtonSave": "Salvează",
"ButtonSaveOrder": "Salvează ordinea",
"ButtonSearch": "Caută",
"ButtonSendEbookToDevice": "Trimite cartea pe dispozitiv",
"ButtonSeries": "Serii",
"ButtonSetTimer": "Setează limita",
"ButtonStream": "Redare online",
"ButtonSubmit": "Trimite",
"ButtonSwitchServerUser": "Schimbă serverul/utilizatorul",
"ButtonUnmaskServerAddress": "Afișează serverul",
"ButtonUserStats": "Activitatea mea",
"ButtonYes": "Da",
"HeaderAccount": "Cont",
"HeaderAdvanced": "Avansat",
"HeaderAndroidAutoSettings": "Setări Android Auto",
"HeaderAudioTracks": "Înregistrări audio",
"HeaderChapters": "Capitole",
"HeaderCollection": "Colecție",
"HeaderCollectionItems": "Conținutul colecției",
"HeaderConnectionStatus": "Starea conexiunii",
"HeaderDataSettings": "Configurare date",
"HeaderDetails": "Detalii",
"HeaderDownloads": "Descărcări",
"HeaderEbookFiles": "Ebook-uri",
"HeaderEpisodes": "Episoade",
"HeaderEreaderSettings": "Setări eReader",
"HeaderLatestEpisodes": "Episoade recente",
"HeaderLibraries": "Biblioteci",
"HeaderLocalFolders": "Dosare locale",
"HeaderLocalLibraryItems": "Conținut local din bibliotecă",
"HeaderNewPlaylist": "Listă nouă",
"HeaderOpenRSSFeed": "Deschide flux RSS",
"HeaderPlaybackSettings": "Setări redare",
"HeaderPlaylist": "Listă de redare",
"HeaderPlaylistItems": "Conținut listă",
"HeaderProgressSyncFailed": "Progresul nu a fost sincronizat",
"HeaderRSSFeed": "Sursă RSS",
"HeaderRSSFeedGeneral": "Date RSS",
"HeaderRSSFeedIsOpen": "RSS activ",
"HeaderSelectDownloadLocation": "Selectează locația descărcării",
"HeaderSettings": "Setări",
"HeaderSleepTimer": "Timer de somn",
"HeaderSleepTimerSettings": "Setări timer somn",
"HeaderStatsMinutesListeningChart": "Minute ascultate (ultimele 7 zile)",
"HeaderStatsRecentSessions": "Sesiuni recente",
"HeaderTableOfContents": "Cuprins",
"HeaderUserInterfaceSettings": "Setări interfață",
"HeaderYourStats": "Progresul tău",
"LabelAddToPlaylist": "Adaugă în listă",
"LabelAddedAt": "Adăugat la",
"LabelAddedDate": "Adăugat {0}",
"LabelAll": "Toate",
"LabelAllowSeekingOnMediaControls": "Permite derularea din controalele media",
"LabelAlways": "Întotdeauna",
"LabelAndroidAutoBrowseLimitForGrouping": "Limită pentru sortare alfabetică",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "Nu folosi listarea alfabetică dacă sunt mai puține elemente decât această valoare",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "Ordinea cărților din serie",
"LabelAskConfirmation": "Cere confirmare",
"LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Prenume Nume)",
"LabelAuthorLastFirst": "Autor (Nume, Prenume)",
"LabelAuthors": "Autori",
"LabelAutoDownloadEpisodes": "Descarcă automat episoadele",
"LabelAutoRewindTime": "Timp de revenire automată",
"LabelAutoSleepTimer": "Timer de somn automat",
"LabelAutoSleepTimerAutoRewind": "Somn automat cu revenire",
"LabelAutoSleepTimerAutoRewindHelp": "Când timerul de somn se termină, la reluarea redării vei fi mutat automat puțin înapoi.",
"LabelAutoSleepTimerHelp": "Când redai media între orele specificate, se va porni automat timerul de somn.",
"LabelBooks": "Cărți",
"LabelChapterTrack": "Marcaje capitole",
"LabelChapters": "Capitole",
"LabelClosePlayer": "Închide playerul",
"LabelCollapseSeries": "Restrânge seriile",
"LabelComplete": "Finalizat",
"LabelContinueBooks": "Continuă cărțile",
"LabelContinueEpisodes": "Continuă episoadele",
"LabelContinueListening": "Ascultă în continuare",
"LabelContinueReading": "Continuă lectura",
"LabelContinueSeries": "Continuă seria",
"LabelCustomTime": "Timp personalizat",
"LabelDescription": "Descriere",
"LabelDisableAudioFadeOut": "Nu estompa sunetul",
"LabelDisableAudioFadeOutHelp": "Volumul audio va scădea treptat când mai rămâne sub 1 minut din timerul de somn. Activează această opțiune pentru a dezactiva estomparea.",
"LabelDisableAutoRewind": "Dezactivează revenirea automată",
"LabelDisableShakeToReset": "Dezactivează resetarea prin scuturare",
"LabelDisableShakeToResetHelp": "Scuturarea dispozitivului în timp ce rulează timerul sau în 2 minute după expirarea lui va reseta timerul de somn. Activează această opțiune pentru a dezactiva resetarea prin scuturare.",
"LabelDisableVibrateOnReset": "Dezactivează vibrația la resetare",
"LabelDisableVibrateOnResetHelp": "La resetarea timerului de somn, dispozitivul va vibra. Activează această opțiune pentru a dezactiva vibrația.",
"LabelDiscover": "Descoperă",
"LabelDownload": "Descarcă",
"LabelDownloadUsingCellular": "Descarcă prin date mobile",
"LabelDownloaded": "Descărcat",
"LabelDuration": "Durată",
"LabelEbook": "Carte electronică",
"LabelEbooks": "Cărți electronice",
"LabelEnable": "Activează",
"LabelEnableMp3IndexSeeking": "Permite căutarea pe index în MP3",
"LabelEnableMp3IndexSeekingHelp": "Activează această opțiune doar dacă ai fișiere MP3 care nu permit derularea corectă. Problema apare de obicei la MP3-uri cu bitrate variabil (VBR). Această setare forțează căutarea pe index, construind o mapare timpbyte pe măsură ce fișierul este citit. La fișiere MP3 mari poate apărea o întârziere când derulezi spre final.",
"LabelEnd": "Sfârșit",
"LabelEndOfChapter": "Sfârșitul capitolului",
"LabelEndTime": "Oră de sfârșit",
"LabelEpisode": "Episod",
"LabelExplicit": "Explicit",
"LabelFeedURL": "URL flux",
"LabelFile": "Fișier",
"LabelFileBirthtime": "Data creării fișierului",
"LabelFileModified": "Fișier modificat",
"LabelFilename": "Nume fișier",
"LabelFinished": "Finalizat",
"LabelFolder": "Dosar",
"LabelFontBoldness": "Grosimea fontului",
"LabelFontScale": "Mărimea fontului",
"LabelGenre": "Gen",
"LabelGenres": "Genuri",
"LabelHapticFeedback": "Vibrație la atingere",
"LabelHasEbook": "Are carte electronică",
"LabelHasSupplementaryEbook": "Are carte electronică suplimentară",
"LabelHeavy": "Puternic",
"LabelHigh": "Ridicat",
"LabelHost": "Gazdă",
"LabelInProgress": "În desfășurare",
"LabelIncomplete": "Incomplet",
"LabelInternalAppStorage": "Stocare internă",
"LabelJumpBackwardsTime": "Derulare înapoi",
"LabelJumpForwardsTime": "Derulare înainte",
"LabelKeepScreenAwake": "Ține ecranul aprins",
"LabelLanguage": "Limbă",
"LabelLayout": "Aspect",
"LabelLayoutAuto": "Automat",
"LabelLayoutSinglePage": "Pagină unică",
"LabelLight": "Deschis",
"LabelLineSpacing": "Spațiere între rânduri",
"LabelListenAgain": "Ascultă din nou",
"LabelLocalBooks": "Cărți locale",
"LabelLocalPodcasts": "Podcasturi locale",
"LabelLockOrientation": "Blochează orientarea",
"LabelLockPlayer": "Blochează playerul",
"LabelLow": "Scăzut",
"LabelMediaType": "Tip media",
"LabelMedium": "Mediu",
"LabelMissing": "Lipsă",
"LabelMore": "Mai multe",
"LabelMoreInfo": "Mai multe informații",
"LabelName": "Nume",
"LabelNarrator": "Narator",
"LabelNarrators": "Naratori",
"LabelNavigateWithVolume": "Navighează cu tastele de volum",
"LabelNavigateWithVolumeMirrored": "Inversat",
"LabelNavigateWithVolumeWhilePlaying": "Permite navigarea cu tastele de volum în timpul redării",
"LabelNavigateWithVolumeWhilePlayingDisabled": "Oprit",
"LabelNavigateWithVolumeWhilePlayingEnabled": "Pornit",
"LabelNever": "Niciodată",
"LabelNewestAuthors": "Autori noi",
"LabelNewestEpisodes": "Episoade noi",
"LabelNo": "Nu",
"LabelNotFinished": "Nefinalizat",
"LabelNotStarted": "Neînceput",
"LabelNumEpisodes": "{0} episoade",
"LabelNumEpisodesIncomplete": "{0} episoade, {1} incomplete",
"LabelNumberOfEpisodes": "# de Episoade",
"LabelOff": "Oprit",
"LabelOn": "Pornit",
"LabelPassword": "Parolă",
"LabelPath": "Cale",
"LabelPlaybackDirect": "Direct",
"LabelPlaybackLocal": "Local",
"LabelPlaybackSpeed": "Viteză redare",
"LabelPlaybackTranscode": "Transcodare",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasturi",
"LabelPreventIndexing": "Împiedică indexarea fluxului în directoarele iTunes și Google Podcasts",
"LabelProgress": "Progres",
"LabelPubDate": "Data publicării",
"LabelPublishYear": "Anul publicării",
"LabelPublishedDate": "Publicat la {0}",
"LabelRSSFeedCustomOwnerEmail": "Email personalizat al proprietarului",
"LabelRSSFeedCustomOwnerName": "Nume personalizat al proprietarului",
"LabelRSSFeedOpen": "Flux RSS deschis",
"LabelRSSFeedPreventIndexing": "Previne indexarea",
"LabelRSSFeedSlug": "Identificator flux RSS",
"LabelRandomly": "Aleatoriu",
"LabelRead": "Citește",
"LabelReadAgain": "Citește din nou",
"LabelRecentSeries": "Serii recente",
"LabelRecentlyAdded": "Adăugate recent",
"LabelRemoveFromPlaylist": "Elimină din listă",
"LabelScaleElapsedTimeBySpeed": "Ajustează timpul scurs după viteză",
"LabelSeason": "Sezon",
"LabelSelectADevice": "Alege un dispozitiv",
"LabelSequenceAscending": "Ordine crescătoare",
"LabelSequenceDescending": "Ordine descrescătoare",
"LabelSeries": "Serii",
"LabelServerAddress": "Adresă server",
"LabelSetEbookAsPrimary": "Setează ca principală",
"LabelSetEbookAsSupplementary": "Setează ca suplimentară",
"LabelShakeSensitivity": "Sensibilitate la scuturare",
"LabelShowAll": "Afișează tot",
"LabelSize": "Dimensiune",
"LabelSleepTimer": "Timer de somn",
"LabelSleepTimerAlmostDoneChime": "Redă un sunet la final",
"LabelSleepTimerAlmostDoneChimeHelp": "Redă un sunet când mai rămân 30s din timerul de somn",
"LabelStart": "Pornește",
"LabelStartTime": "Timp de pornire",
"LabelStatsBestDay": "Ziua cea mai bună",
"LabelStatsDailyAverage": "Medie zilnică",
"LabelStatsDays": "Zile",
"LabelStatsDaysListened": "Zile ascultate",
"LabelStatsInARow": "la rând",
"LabelStatsItemsFinished": "Finalizate",
"LabelStatsMinutes": "minute",
"LabelStatsMinutesListening": "Minute ascultate",
"LabelStatsWeekListening": "Ascultare săptămânală",
"LabelStreamingUsingCellular": "Redare online prin date mobile",
"LabelTag": "Etichetă",
"LabelTags": "Etichete",
"LabelTheme": "Temă",
"LabelThemeBlack": "Negru",
"LabelThemeDark": "Întunecat",
"LabelThemeLight": "Deschis",
"LabelTimeRemaining": "{0} rămase",
"LabelTitle": "Titlu",
"LabelTotalSize": "Dimensiune totală",
"LabelTotalTrack": "Total audio",
"LabelTracks": "Fișiere audio",
"LabelType": "Tip",
"LabelUnknown": "Necunoscut",
"LabelUnlockPlayer": "Deblochează playerul",
"LabelUseBookshelfView": "Folosește vizualizare raft",
"LabelUser": "Utilizator",
"LabelUsername": "Nume utilizator",
"LabelVeryHigh": "Foarte ridicat",
"LabelVeryLow": "Foarte scăzut",
"LabelYearReviewHide": "Ascunde retrospectiva anului",
"LabelYearReviewShow": "Vezi retrospectiva anului",
"LabelYourBookmarks": "Semnele tale de carte",
"LabelYourProgress": "Progresul tău",
"MessageAndroid10Downloads": "Pe Android 10 și versiunile anterioare, descărcările vor fi stocate în memoria internă a aplicației.",
"MessageAttemptingServerConnection": "Se încearcă conectarea la server...",
"MessageAudiobookshelfServerNotConnected": "Serverul Audiobookshelf nu este conectat",
"MessageAudiobookshelfServerRequired": "<strong>Important!</strong> Această aplicație este proiectată să funcționeze cu un server Audiobookshelf găzduit de tine sau de cineva cunoscut. Aplicația nu oferă conținut propriu.",
"MessageBookshelfEmpty": "Raftul este gol",
"MessageConfirmDeleteLocalEpisode": "Ștergi episodul local „{0}” de pe dispozitiv? Fișierul de pe server nu va fi afectat.",
"MessageConfirmDeleteLocalFiles": "Ștergi fișierele locale ale acestui element de pe dispozitiv? Fișierele de pe server și progresul nu vor fi afectate.",
"MessageConfirmDisableAutoTimer": "Sigur vrei să dezactivezi timerul automat pentru restul zilei? Acesta va fi reactivat la finalul perioadei de somn automat sau când repornești aplicația.",
"MessageConfirmDiscardProgress": "Ești sigur că vrei să îți resetezi progresul?",
"MessageConfirmDownloadUsingCellular": "Urmează să descarci folosind date mobile. Acest lucru poate genera costuri suplimentare. Vrei să continui?",
"MessageConfirmMarkAsFinished": "Sigur vrei să marchezi acest element ca finalizat?",
"MessageConfirmRemoveBookmark": "Sigur vrei să ștergi semnul de carte?",
"MessageConfirmStreamingUsingCellular": "Urmează să redai online folosind date mobile. Acest lucru poate genera costuri suplimentare. Vrei să continui?",
"MessageDiscardProgress": "Resetează progresul",
"MessageDownloadCompleteProcessing": "Descărcare finalizată. Se procesează...",
"MessageDownloading": "Se descarcă...",
"MessageDownloadingEpisode": "Se descarcă episodul",
"MessageEpisodesQueuedForDownload": "{0} episod(e) în așteptare pentru descărcare",
"MessageFailedToRefreshToken": "Reîmprospătarea tokenului a eșuat, este necesară reconectarea",
"MessageFeedURLWillBe": "Adresa fluxului va fi {0}",
"MessageFetching": "Se preiau date…",
"MessageFollowTheProjectOnGithub": "Urmărește proiectul pe GitHub",
"MessageItemDownloadCompleteFailedToCreate": "Descărcarea s-a încheiat, dar nu s-a putut crea elementul în bibliotecă",
"MessageItemMissing": "Elementul lipsește și trebuie reparat pe server. De obicei este marcat ca lipsă deoarece căile fișierelor nu sunt accesibile.",
"MessageLoading": "Se încarcă…",
"MessageLoadingServerData": "Se încarcă datele de pe server…",
"MessageLocalFolderDescription": "'Internal App Storage' este accesibilă doar acestei aplicații. Aplicația acceptă doar media descărcată direct prin ea. Poți folosi dosarele de stocare partajată pentru ca alte aplicații să acceseze media descărcată de această aplicație.",
"MessageMarkAsFinished": "Marchează ca finalizat",
"MessageMediaLinkedToADifferentServer": "Conținutul este asociat unui server Audiobookshelf la o altă adresă ({0}). Progresul va fi sincronizat când te conectezi la acea adresă.",
"MessageMediaLinkedToADifferentUser": "Conținutul este asociat acestui server, dar a fost descărcat de un alt utilizator. Progresul se va sincroniza doar pentru utilizatorul care l-a descărcat.",
"MessageMediaLinkedToServer": "Asociat serverului {0}",
"MessageMediaLinkedToThisServer": "Conținutul descărcat este asociat acestui server",
"MessageMediaNotLinkedToServer": "Conținutul nu este asociat unui server Audiobookshelf. Progresul nu va fi sincronizat.",
"MessageNoBookmarks": "Fără semne de carte",
"MessageNoChapters": "Fără capitole",
"MessageNoCollections": "Fără colecții",
"MessageNoItems": "Niciun element",
"MessageNoItemsFound": "Nu s-au găsit elemente",
"MessageNoListeningSessions": "Nicio sesiune de ascultare",
"MessageNoLogs": "Fără jurnale",
"MessageNoMediaFolders": "Fără dosare media",
"MessageNoNetworkConnection": "Fără conexiune la rețea",
"MessageNoPodcastsFound": "Nu s-au găsit podcasturi",
"MessageNoSeries": "Fără volume",
"MessageNoUpdatesWereNecessary": "Nu au fost necesare actualizări",
"MessageNoUserPlaylists": "Nu ai nicio listă de redare",
"MessageOldServerAuthReLoginRequired": "Autentificarea a fost îmbunătățită pentru siguranță în versiunea de server v2.26.0. Toți utilizatorii trebuie să se reconecteze.",
"MessageOldServerAuthWarning": "Serverul folosește un sistem de autentificare învechit",
"MessageOldServerAuthWarningHelp": "Acest server rulează o versiune mai veche decât v2.26.0. În v2.26.0 a fost adăugat un sistem de autentificare mai sigur. Este recomandat să actualizezi serverul la cea mai nouă versiune. Dacă l-ai actualizat deja, autentifică-te din nou pentru a folosi noul sistem.",
"MessageOldServerConnectionWarning": "Configurația conexiunii la server folosește un ID de utilizator vechi. Șterge și adaugă din nou această conexiune la server.",
"MessageOldServerConnectionWarningHelp": "Inițial ai configurat conexiunea la acest server înainte de migrarea bazei de date din 2.3.0 (iunie 2023). O actualizare viitoare a serverului va elimina posibilitatea de autentificare cu această conexiune veche. Te rugăm să ștergi conexiunea existentă și să te reconectezi (folosind aceeași adresă de server și aceleași credențiale). Dacă ai conținut descărcat pe acest dispozitiv, va trebui descărcat din nou pentru a se sincroniza cu serverul.",
"MessagePodcastSearchField": "Introdu termenul de căutare sau URL-ul unui flux RSS",
"MessageProgressSyncFailed": "Ultima încercare de a raporta progresul de ascultare către server a eșuat. Cererile de sincronizare vor continua la fiecare 15 secunde 1 minut cât timp rulează redarea.",
"MessageReportBugsAndContribute": "Raportează erori, cere funcții noi și contribuie pe",
"MessageSeriesAlreadyDownloaded": "Toate volumele au fost deja descărcate.",
"MessageSeriesDownloadConfirm": "Descarci cele {0} cărți lipsă, cu {1} fișier(e), total {2}, în dosarul {3}?",
"MessageSeriesDownloadConfirmIos": "Descarci cele {0} cărți lipsă, cu {1} fișier(e), total {2}?",
"MessageSocketConnectedOverMeteredCellular": "Socket conectat prin date mobile cu trafic contorizat",
"MessageSocketConnectedOverMeteredWifi": "Socket conectat prin Wi-Fi cu trafic contorizat",
"MessageSocketConnectedOverUnmeteredCellular": "Socket conectat prin date mobile nelimitate",
"MessageSocketConnectedOverUnmeteredWifi": "Socket conectat prin Wi-Fi nelimitat",
"MessageSocketNotConnected": "Socket neconectat",
"NoteRSSFeedPodcastAppsHttps": "Atenționare: Majoritatea aplicațiilor de podcast cer ca URL-ul fluxului RSS să folosească HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Atenționare: unul sau mai multe episoade nu au data publicării (Pub Date). Unele aplicații de podcast o cer.",
"ToastBookmarkCreateFailed": "Nu s-a putut crea semnul de carte",
"ToastBookmarkRemoveFailed": "Nu s-a putut șterge semnul de carte",
"ToastBookmarkUpdateFailed": "Nu s-a putut actualiza semnul de carte",
"ToastDownloadNotAllowedOnCellular": "Descărcarea nu este permisă pe date mobile",
"ToastItemMarkedAsFinishedFailed": "Nu s-a putut marca drept finalizat",
"ToastItemMarkedAsNotFinishedFailed": "Nu s-a putut marca drept nefinalizat",
"ToastPlaylistCreateFailed": "Nu s-a putut crea lista de redare",
"ToastPodcastCreateFailed": "Nu s-a putut crea podcastul",
"ToastPodcastCreateSuccess": "Podcast creat cu succes",
"ToastRSSFeedCloseFailed": "Nu s-a putut închide fluxul RSS",
"ToastRSSFeedCloseSuccess": "Flux RSS închis",
"ToastStreamingNotAllowedOnCellular": "Redarea online nu este permisă pe date mobile"
}
+35 -29
View File
@@ -7,7 +7,7 @@
"ButtonCancelTimer": "Отмена таймера",
"ButtonClearFilter": "Очистить фильтр",
"ButtonClearLogs": "Очистить журнал",
"ButtonCloseFeed": "Закрыть канал",
"ButtonCloseFeed": "Закрыть ленту",
"ButtonCollections": "Коллекции",
"ButtonConnect": "Подключение",
"ButtonConnectToServer": "Подключение к серверу",
@@ -15,7 +15,7 @@
"ButtonCreateBookmark": "Создать закладку",
"ButtonCreateNewPlaylist": "Создать новый плейлист",
"ButtonDelete": "Удалить",
"ButtonDeleteLocalEpisode": "Удалить локальный эпизод",
"ButtonDeleteLocalEpisode": "Удалить выпуск локально",
"ButtonDeleteLocalFile": "Удалить локальный файл",
"ButtonDeleteLocalItem": "Удалить локальный элемент",
"ButtonDisableAutoTimer": "Отключить автоматический таймер",
@@ -31,13 +31,13 @@
"ButtonManageLocalFiles": "Управление локальными файлами",
"ButtonMaskServerAddress": "Скрывать адрес сервера",
"ButtonNewFolder": "Новая папка",
"ButtonNextEpisode": "Следующий эпизод",
"ButtonNextEpisode": "Следующий выпуск",
"ButtonOk": "Ок",
"ButtonOpenFeed": "Открыть канал",
"ButtonOpenFeed": "Открыть ленту",
"ButtonOverride": "Переопределить",
"ButtonPause": "Пауза",
"ButtonPlay": "Слушать",
"ButtonPlayEpisode": "Воспроизвести эпизод",
"ButtonPlayEpisode": "Воспроизвести выпуск",
"ButtonPlaylists": "Плейлисты",
"ButtonRead": "Читать",
"ButtonReadLess": "Читать меньше",
@@ -68,21 +68,21 @@
"HeaderDetails": "Подробности",
"HeaderDownloads": "Загрузки",
"HeaderEbookFiles": "Файлы e-книг",
"HeaderEpisodes": "Эпизоды",
"HeaderEpisodes": "Выпуски",
"HeaderEreaderSettings": "Настройки E-ридера",
"HeaderLatestEpisodes": "Последние эпизоды",
"HeaderLatestEpisodes": "Последние выпуски",
"HeaderLibraries": "Библиотеки",
"HeaderLocalFolders": "Локальные папки",
"HeaderLocalLibraryItems": "Элементы локальной библиотеки",
"HeaderNewPlaylist": "Новый плейлист",
"HeaderOpenRSSFeed": "Открыть RSS-канал",
"HeaderOpenRSSFeed": "Открыть RSS-ленту",
"HeaderPlaybackSettings": "Настройки воспроизведения",
"HeaderPlaylist": "Плейлист",
"HeaderPlaylistItems": "Элементы списка воспроизведения",
"HeaderProgressSyncFailed": "Синхронизация прогресса не удалась",
"HeaderRSSFeed": "RSS-канал",
"HeaderRSSFeed": "RSS-лента",
"HeaderRSSFeedGeneral": "Сведения о RSS",
"HeaderRSSFeedIsOpen": "RSS-канал открыт",
"HeaderRSSFeedIsOpen": "RSS-лента открыта",
"HeaderSelectDownloadLocation": "Выберите место загрузки",
"HeaderSettings": "Настройки",
"HeaderSleepTimer": "Таймер сна",
@@ -106,7 +106,7 @@
"LabelAuthorFirstLast": "Автор (Имя Фамилия)",
"LabelAuthorLastFirst": "Автор (Фамилия, Имя)",
"LabelAuthors": "Авторы",
"LabelAutoDownloadEpisodes": "Скачивать эпизоды автоматически",
"LabelAutoDownloadEpisodes": "Скачивать выпуски автоматически",
"LabelAutoRewindTime": "Время автоматической перемотки",
"LabelAutoSleepTimer": "Таймер сна",
"LabelAutoSleepTimerAutoRewind": "Перемотка назад таймера сна",
@@ -119,7 +119,7 @@
"LabelCollapseSeries": "Свернуть серии",
"LabelComplete": "Завершить",
"LabelContinueBooks": "Продолжить",
"LabelContinueEpisodes": "Продолжить эпизоды",
"LabelContinueEpisodes": "Продолжить выпуски",
"LabelContinueListening": "Продолжить слушать",
"LabelContinueReading": "Продолжить чтение",
"LabelContinueSeries": "Продолжить серии",
@@ -136,7 +136,7 @@
"LabelDownload": "Скачать",
"LabelDownloadUsingCellular": "Загрузка с помощью сотовой связи",
"LabelDownloaded": "Загружено",
"LabelDuration": "Продолжительность",
"LabelDuration": "Длительность",
"LabelEbook": "E-книга",
"LabelEbooks": "E-книги",
"LabelEnable": "Включить",
@@ -145,8 +145,9 @@
"LabelEnd": "Конец",
"LabelEndOfChapter": "Конец главы",
"LabelEndTime": "Время конца",
"LabelEpisode": "Эпизод",
"LabelFeedURL": "URL канала",
"LabelEpisode": "Выпуск",
"LabelExplicit": "18+",
"LabelFeedURL": "URL-адрес ленты",
"LabelFile": "Файл",
"LabelFileBirthtime": "Дата создания",
"LabelFileModified": "Дата модификации",
@@ -196,13 +197,13 @@
"LabelNavigateWithVolumeWhilePlayingEnabled": "Вкл.",
"LabelNever": "Никогда",
"LabelNewestAuthors": "Новые авторы",
"LabelNewestEpisodes": "Новые эпизоды",
"LabelNewestEpisodes": "Новые выпуски",
"LabelNo": "Нет",
"LabelNotFinished": "Не завершено",
"LabelNotStarted": "Не запущено",
"LabelNumEpisodes": "{0} эпизодов",
"LabelNumEpisodesIncomplete": "{0} эпизодов, {1} не завершено",
"LabelNumberOfEpisodes": "# из эпизодов",
"LabelNumEpisodes": "{0} выпусков",
"LabelNumEpisodesIncomplete": "{0} выпусков, {1} не завершено",
"LabelNumberOfEpisodes": "# из выпусков",
"LabelOff": "Выключить",
"LabelOn": "Включён",
"LabelPassword": "Пароль",
@@ -220,8 +221,9 @@
"LabelPublishedDate": "Опубликовано {0}",
"LabelRSSFeedCustomOwnerEmail": "Пользовательский Email владельца",
"LabelRSSFeedCustomOwnerName": "Пользовательское Имя владельца",
"LabelRSSFeedOpen": "Открыть RSS-ленту",
"LabelRSSFeedPreventIndexing": "Запретить индексирование",
"LabelRSSFeedSlug": "Встроить RSS-канал",
"LabelRSSFeedSlug": "Ключевое слово RSS-ленты",
"LabelRandomly": "Случайно",
"LabelRead": "Читать",
"LabelReadAgain": "Читать снова",
@@ -283,7 +285,7 @@
"MessageAudiobookshelfServerNotConnected": "Сервер Audiobookshelf не подключен",
"MessageAudiobookshelfServerRequired": "<strong>Важно!</strong> Это приложение предназначено для работы с сервером Audiobookshelf, который размещаете вы или кто-то из ваших знакомых. Это приложение не предоставляет никакого контента.",
"MessageBookshelfEmpty": "Книжная полка пуста",
"MessageConfirmDeleteLocalEpisode": "Удалить локальный эпизод \"{0}\" с Вашего устройства? Файл на сервере не будет затронут.",
"MessageConfirmDeleteLocalEpisode": "Удалить локальный выпуск \"{0}\" с вашего устройства? Файл на сервере не будет затронут.",
"MessageConfirmDeleteLocalFiles": "Удалить локальные файлы этого элемента с вашего устройства? Это не повлияет на файлы на сервере и ваш прогресс.",
"MessageConfirmDisableAutoTimer": "Вы уверены, что хотите отключить автоматический таймер до конца сегодняшнего дня? Таймер будет снова включен по истечении этого периода автоматического отключения или если вы перезапустите приложение.",
"MessageConfirmDiscardProgress": "Вы уверены, что хотите сбросить свой прогресс?",
@@ -294,9 +296,10 @@
"MessageDiscardProgress": "Отбросить прогресс",
"MessageDownloadCompleteProcessing": "Загрузка завершена. Обработка...",
"MessageDownloading": "Загрузка...",
"MessageDownloadingEpisode": "Эпизод скачивается",
"MessageEpisodesQueuedForDownload": "{0} Эпизод(ов) запланировано для закачки",
"MessageFeedURLWillBe": "URL канала будет {0}",
"MessageDownloadingEpisode": "Скачивание выпуска",
"MessageEpisodesQueuedForDownload": "{0} выпуск(ов) запланировано для скачивания",
"MessageFailedToRefreshToken": "Не удалось обновить токен, требуется повторный вход в систему",
"MessageFeedURLWillBe": "URL-адрес ленты будет {0}",
"MessageFetching": "Завершается...",
"MessageFollowTheProjectOnGithub": "Следите за проектом на GitHub",
"MessageItemDownloadCompleteFailedToCreate": "Загрузка элемента завершена, но не удалось создать элемент библиотеки",
@@ -323,9 +326,12 @@
"MessageNoSeries": "Нет серии",
"MessageNoUpdatesWereNecessary": "Обновления не требовались",
"MessageNoUserPlaylists": "У вас нет плейлистов",
"MessageOldServerAuthReLoginRequired": "В версии server 2.26.0 для обеспечения безопасности была улучшена аутентификация. Всем пользователям необходимо повторно войти в систему.",
"MessageOldServerAuthWarning": "Сервер использует устаревшую аутентификацию",
"MessageOldServerAuthWarningHelp": "Этот сервер работает под управлением версии старше чем 2.26.0. В версии 2.26.0 была добавлена более безопасная система аутентификации. Настоятельно рекомендуется обновить сервер до последней версии. Если вы уже обновили сервер, войдите в систему еще раз, чтобы использовать новую аутентификацию.",
"MessageOldServerConnectionWarning": "Конфигурация подключения к серверу использует старый идентификатор пользователя. Удалите и повторно добавьте это подключение к серверу.",
"MessageOldServerConnectionWarningHelp": "Первоначально вы настроили подключение к этому серверу до миграции базы данных в версии 2.3.0, выпущенной в июне 2023 года. Будущее обновление сервера уберет возможность входа с помощью этого старого подключения. Удалите существующее подключение к серверу и подключитесь снова (используя тот же адрес сервера и учетные данные). Если на этом устройстве есть загруженные медиафайлы, их необходимо будет загрузить снова для синхронизации с сервером.",
"MessagePodcastSearchField": "Введите поисковый запрос или URL-адрес RSS-канала",
"MessagePodcastSearchField": "Введите поисковый запрос или URL-адрес RSS-ленты",
"MessageProgressSyncFailed": "Последняя попытка сообщить о вашем прогрессе прослушивания на сервер не удалась. Запросы на синхронизацию прогресса будут продолжаться каждые 15 секунд в течении 1 минуты во время воспроизведения медиа.",
"MessageReportBugsAndContribute": "Сообщайте об ошибках, запрашивайте функции и вносите свой вклад на",
"MessageSeriesAlreadyDownloaded": "Вы уже скачали все книги из этой серии.",
@@ -336,8 +342,8 @@
"MessageSocketConnectedOverUnmeteredCellular": "Сокет подключен по безлимитной сотовой связи",
"MessageSocketConnectedOverUnmeteredWifi": "Сокет подключен через безлимитный Wi-Fi",
"MessageSocketNotConnected": "Сокет не подключен",
"NoteRSSFeedPodcastAppsHttps": "Предупреждение: Большинству приложений подкастов потребуется, чтобы URL-адрес RSS-канала использовал HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Предупреждение: 1 или более эпизодов не имеют даты публикации. Некоторые приложения для подкастов требуют этого.",
"NoteRSSFeedPodcastAppsHttps": "Предупреждение: Большинству приложений подкастов потребуется, чтобы URL-адрес RSS-ленты использовал HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Предупреждение: 1 или более выпусков не имеют даты публикации. Некоторые приложения для подкастов требуют этого.",
"ToastBookmarkCreateFailed": "Не удалось создать закладку",
"ToastBookmarkRemoveFailed": "Не удалось удалить закладку",
"ToastBookmarkUpdateFailed": "Не удалось обновить закладку",
@@ -347,7 +353,7 @@
"ToastPlaylistCreateFailed": "Не удалось создать плейлист",
"ToastPodcastCreateFailed": "Не удалось создать подкаст",
"ToastPodcastCreateSuccess": "Подкаст успешно создан",
"ToastRSSFeedCloseFailed": "Не удалось закрыть RSS-канал",
"ToastRSSFeedCloseSuccess": "RSS-канал закрыт",
"ToastRSSFeedCloseFailed": "Не удалось закрыть RSS-ленту",
"ToastRSSFeedCloseSuccess": "RSS-лента закрыта",
"ToastStreamingNotAllowedOnCellular": "Потоковая передача данных по сотовой сети запрещена"
}
+355 -1
View File
@@ -1 +1,355 @@
{}
{
"ButtonAdd": "Pridať",
"ButtonAddNewServer": "Pridať nový server",
"ButtonAuthors": "Autori",
"ButtonBack": "Späť",
"ButtonCancel": "Zrušiť",
"ButtonCancelTimer": "Zrušiť časovač",
"ButtonClearFilter": "Zrušiť filter",
"ButtonClearLogs": "Zmazať záznamy logovania",
"ButtonCloseFeed": "Zatvoriť zdroj",
"ButtonCollections": "Kolekcie",
"ButtonConnect": "Pripojiť",
"ButtonConnectToServer": "Pripojte sa k serveru",
"ButtonCreate": "Vytvoriť",
"ButtonCreateBookmark": "Vytvoriť záložku",
"ButtonCreateNewPlaylist": "Vytvoriť nový playlist",
"ButtonDelete": "Zmazať",
"ButtonDeleteLocalEpisode": "Odstrániť lokálnu epizódu",
"ButtonDeleteLocalFile": "Odstrániť lokálny súbor",
"ButtonDeleteLocalItem": "Odstrániť lokálnu položku",
"ButtonDisableAutoTimer": "Vypnúť automatický časovač",
"ButtonDisconnect": "Odpojiť",
"ButtonGoToWebClient": "Prejsť na webové rozhranie",
"ButtonHistory": "História",
"ButtonHome": "Domov",
"ButtonIssues": "Problémy",
"ButtonLatest": "Najnovšie",
"ButtonLibrary": "Knižnica",
"ButtonLocalMedia": "Lokálne médiá",
"ButtonLogs": "Záznamy logovania",
"ButtonManageLocalFiles": "Spravovať lokálne súbory",
"ButtonMaskServerAddress": "Maska adresy servera",
"ButtonNewFolder": "Nový priečinok",
"ButtonNextEpisode": "Ďalšia epizóda",
"ButtonOk": "OK",
"ButtonOpenFeed": "Otvoriť zdroj",
"ButtonOverride": "Uprednostniť",
"ButtonPause": "Pozastaviť",
"ButtonPlay": "Prehrať",
"ButtonPlayEpisode": "Prehrať epizódu",
"ButtonPlaylists": "Playlisty",
"ButtonRead": "Načítať",
"ButtonReadLess": "Načítať menej",
"ButtonReadMore": "Načítať viac",
"ButtonRemove": "Odstrániť",
"ButtonRemoveFromServer": "Odstrániť zo servera",
"ButtonSave": "Uložiť",
"ButtonSaveOrder": "Uložiť poradie",
"ButtonSearch": "Vyhľadať",
"ButtonSendEbookToDevice": "Poslať e-knihu do zariadenia",
"ButtonSeries": "Série",
"ButtonSetTimer": "Nastaviť časovač",
"ButtonStream": "Všeobecný",
"ButtonSubmit": "Odoslať",
"ButtonSwitchServerUser": "Zmeniť server/používateľa",
"ButtonUnmaskServerAddress": "Odmaskovať adresu servera",
"ButtonUserStats": "Štatistiky používateľov",
"ButtonYes": "Áno",
"HeaderAccount": "Účet",
"HeaderAdvanced": "Pokročilé",
"HeaderAndroidAutoSettings": "Nastavenia Android Auto",
"HeaderAudioTracks": "Zvukové stopy",
"HeaderChapters": "Kapitoly",
"HeaderCollection": "Zbierky",
"HeaderCollectionItems": "Položky zbierky",
"HeaderConnectionStatus": "Stav pripojenia",
"HeaderDataSettings": "Nastavenie dát",
"HeaderDetails": "Podrobnosti",
"HeaderDownloads": "Stiahnuté súbory",
"HeaderEbookFiles": "Súbory e-kníh",
"HeaderEpisodes": "Epizódy",
"HeaderEreaderSettings": "Nastavenia čítačky",
"HeaderLatestEpisodes": "Posledné epizódy",
"HeaderLibraries": "Knižnice",
"HeaderLocalFolders": "Lokálne priečinky",
"HeaderLocalLibraryItems": "Položky lokálnej knižnice",
"HeaderNewPlaylist": "Nový playlist",
"HeaderOpenRSSFeed": "Otvoriť RSS zdroj",
"HeaderPlaybackSettings": "Nastavenie prehrávania",
"HeaderPlaylist": "Playlist",
"HeaderPlaylistItems": "Položky playlistu",
"HeaderProgressSyncFailed": "Synchronizovanie aktuálneho stavu zlyhalo",
"HeaderRSSFeed": "RSS zdroj",
"HeaderRSSFeedGeneral": "Detaily RSS",
"HeaderRSSFeedIsOpen": "RSS zdroj je otvorený",
"HeaderSelectDownloadLocation": "Vyberte miesto na stiahnutie",
"HeaderSettings": "Nastavenia",
"HeaderSleepTimer": "Časovač spánku",
"HeaderSleepTimerSettings": "Nastavenia časovaču spánku",
"HeaderStatsMinutesListeningChart": "Vypočutých minút (za posledných 7 dní)",
"HeaderStatsRecentSessions": "Nedávne relácie",
"HeaderTableOfContents": "Obsah",
"HeaderUserInterfaceSettings": "Nastavenia používateľského rozhrania",
"HeaderYourStats": "Vaše štatistiky",
"LabelAddToPlaylist": "Pridať do playlistu",
"LabelAddedAt": "Pridané",
"LabelAddedDate": "Pridané {0}",
"LabelAll": "Všetko",
"LabelAllowSeekingOnMediaControls": "Povoliť zmenu pozície prehrávania v mediálnych notifikáciách",
"LabelAlways": "Vždy",
"LabelAndroidAutoBrowseLimitForGrouping": "Limit abecedného posunu",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "Nezobrazí možnosť abecedného posunu, ak je počet zobrazených položiek menší ako limit",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "Poradie kníh série",
"LabelAskConfirmation": "Požiadať o potvrdenie",
"LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Meno Priezvisko)",
"LabelAuthorLastFirst": "Autor (Priezvisko, Meno)",
"LabelAuthors": "Autori",
"LabelAutoDownloadEpisodes": "Automaticky sťahovať epizódy",
"LabelAutoRewindTime": "Automatické presunutie času",
"LabelAutoSleepTimer": "Auto časovač spánku",
"LabelAutoSleepTimerAutoRewind": "Automatické posunutie času pre Auto časovač spánku",
"LabelAutoSleepTimerAutoRewindHelp": "Keď Auto časovač spánku skončí, pred nasledujúcim prehratím položky sa prehrávanie automaticky posunie.",
"LabelAutoSleepTimerHelp": "keď sa prehrávanie začne vo vymedzenom časovom intervale, časovač spánku sa automaticky spustí tiež.",
"LabelBooks": "Knihy",
"LabelChapterTrack": "Zvuková stopa kapitoly",
"LabelChapters": "Kapitoly",
"LabelClosePlayer": "Zavrieť prehrávač",
"LabelCollapseSeries": "Zbaliť série",
"LabelComplete": "Hotovo",
"LabelContinueBooks": "Pokračovať v knihách",
"LabelContinueEpisodes": "Pokračovať v epizódach",
"LabelContinueListening": "Pokračovať v počúvaní",
"LabelContinueReading": "Pokračovať v čítaní",
"LabelContinueSeries": "Pokračovať v sérii",
"LabelCustomTime": "Vlastný čas",
"LabelDescription": "Popis",
"LabelDisableAudioFadeOut": "Vypnúť fade out",
"LabelDisableAudioFadeOutHelp": "Hlasitosť prehrávania sa automaticky začne znižovať, ak zostáva menej ako 1 minúta na časovači spánku. Povolením tejto voľby sa fade out efekt vypne.",
"LabelDisableAutoRewind": "Vypnúť automatické presúvanie",
"LabelDisableShakeToReset": "Vypnúť reštart potrasením",
"LabelDisableShakeToResetHelp": "Potrasením vášho zariadenia v prípade, ak časovač beží ALEBO na ňom zostáva menej ako 2 minúty, dôjde k rešttartovaniu časovača. Povolením tejto voľby reštart zatrasením vypnete.",
"LabelDisableVibrateOnReset": "Vypnúť vybrovanie pri reštarte",
"LabelDisableVibrateOnResetHelp": "Keď sa čas na časovači spánku uplynie, časovač spánku sa reštartuje a vaše zariadenie zavibruje. Povolením tejto voľby vybrovanie vypnete.",
"LabelDiscover": "Objaviť",
"LabelDownload": "Stiahnuť",
"LabelDownloadUsingCellular": "Stiahnuť cez mobilné dáta",
"LabelDownloaded": "Stiahnuté",
"LabelDuration": "Dĺžka",
"LabelEbook": "E-kniha",
"LabelEbooks": "E-knihy",
"LabelEnable": "Povoliť",
"LabelEnableMp3IndexSeeking": "Povoliť vyhľadávanie v MP3 indexe",
"LabelEnableMp3IndexSeekingHelp": "Táto voľba by mala byť zapnutá iba v prípade, ak vaše MP3 súbory nie sú vyhľadané správne. Príčinou nesprávneho vyhľadania je najčastejšie meniaci sa bitrate (VBR) MP3 súborov. Toto nastavenie vynúti vyhľadávanie na báze indexu, v ktorom je počas načítania súboru vytvorené time-to-byte mapovanie. V prípade niektorých veľkých MP3 súborov sa pri čítaní ku koncu súboru môže objaviť dlhšia pauza.",
"LabelEnd": "Ukončiť",
"LabelEndOfChapter": "Koniec kapitoly",
"LabelEndTime": "Čas ukončenia",
"LabelEpisode": "Epizóda",
"LabelExplicit": "Explicitný obsah",
"LabelFeedURL": "URL zdroja",
"LabelFile": "Súbor",
"LabelFileBirthtime": "Čas vytvorenia súboru",
"LabelFileModified": "Súbor zmenený",
"LabelFilename": "Názov súboru",
"LabelFinished": "Ukončené",
"LabelFolder": "Priečinok",
"LabelFontBoldness": "Hrúbka písma",
"LabelFontScale": "Veľkosť písma",
"LabelGenre": "Žáner",
"LabelGenres": "Žánre",
"LabelHapticFeedback": "Haptická spätná väzba",
"LabelHasEbook": "Má e-knihu",
"LabelHasSupplementaryEbook": "Má doplnkovú e-knihu",
"LabelHeavy": "Ťažký",
"LabelHigh": "Vysoký",
"LabelHost": "Host",
"LabelInProgress": "Prebieha",
"LabelIncomplete": "Nekompletné",
"LabelInternalAppStorage": "Interné úložisko aplikácie",
"LabelJumpBackwardsTime": "Časový skok dozadu",
"LabelJumpForwardsTime": "Časový skok dopredu",
"LabelKeepScreenAwake": "Ponechať displej zapnutý",
"LabelLanguage": "Jazyk",
"LabelLayout": "Rozloženie",
"LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Jedna stránka",
"LabelLight": "Svetlá",
"LabelLineSpacing": "Riadkovanie",
"LabelListenAgain": "Počúvať znova",
"LabelLocalBooks": "Lokálne knihy",
"LabelLocalPodcasts": "Lokálne podcasty",
"LabelLockOrientation": "Uzamknúť orientáciu",
"LabelLockPlayer": "Uzamknúť prehrávač",
"LabelLow": "Nízky",
"LabelMediaType": "Typ média",
"LabelMedium": "Stredný",
"LabelMissing": "Chýbajúce",
"LabelMore": "Viac",
"LabelMoreInfo": "Viac informácií",
"LabelName": "Meno",
"LabelNarrator": "Interpret",
"LabelNarrators": "Interpreti",
"LabelNavigateWithVolume": "Navigovať tlačítkami hlasitosti",
"LabelNavigateWithVolumeMirrored": "Zrkadlový",
"LabelNavigateWithVolumeWhilePlaying": "Povoliť navigáciu tlačítkami hlasitosti počas prehrávania",
"LabelNavigateWithVolumeWhilePlayingDisabled": "Vypnuté",
"LabelNavigateWithVolumeWhilePlayingEnabled": "Zapnuté",
"LabelNever": "Nikdy",
"LabelNewestAuthors": "Najnovší autori",
"LabelNewestEpisodes": "Najnovšie epizódy",
"LabelNo": "Nie",
"LabelNotFinished": "Nedokončené",
"LabelNotStarted": "Nezačaté",
"LabelNumEpisodes": "{0} epizód",
"LabelNumEpisodesIncomplete": "{0} epizód, {1} nekompletných",
"LabelNumberOfEpisodes": "# z epizód",
"LabelOff": "Vypnuté",
"LabelOn": "Zapnuté",
"LabelPassword": "Heslo",
"LabelPath": "Cesta",
"LabelPlaybackDirect": "Priamy",
"LabelPlaybackLocal": "Lokálny",
"LabelPlaybackSpeed": "Rýchlosť prehrávania",
"LabelPlaybackTranscode": "Transkódovať",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasty",
"LabelPreventIndexing": "Zabráni indexácii vašich zdrojov službami iTunes a Google podcast directories",
"LabelProgress": "Stav",
"LabelPubDate": "Dátum publikovania",
"LabelPublishYear": "Rok vydania",
"LabelPublishedDate": "Vydané {0}",
"LabelRSSFeedCustomOwnerEmail": "Vlastný e-mail vlastníka",
"LabelRSSFeedCustomOwnerName": "Vlastné meno vlastníka",
"LabelRSSFeedOpen": "RSS zdroj otvorený",
"LabelRSSFeedPreventIndexing": "Zakázať indexovanie",
"LabelRSSFeedSlug": "Slug RSS zdroja",
"LabelRandomly": "Náhodne",
"LabelRead": "Načítať",
"LabelReadAgain": "Čítať znova",
"LabelRecentSeries": "Posledné série",
"LabelRecentlyAdded": "Posledné pridané",
"LabelRemoveFromPlaylist": "Odstrániť z playlistu",
"LabelScaleElapsedTimeBySpeed": "Zmeniť ubehnutý čas podľa rýchlosti",
"LabelSeason": "Sezóna",
"LabelSelectADevice": "Vyberte zariadenie",
"LabelSequenceAscending": "Vzostupné poradie",
"LabelSequenceDescending": "Zostupné poradie",
"LabelSeries": "Série",
"LabelServerAddress": "Adresa servera",
"LabelSetEbookAsPrimary": "Nastaviť ako primárny",
"LabelSetEbookAsSupplementary": "Nastaviť ako doplnkový",
"LabelShakeSensitivity": "Citlivosť na trasenie",
"LabelShowAll": "Zobraziť všetko",
"LabelSize": "Veľkosť",
"LabelSleepTimer": "Časovač spánku",
"LabelSleepTimerAlmostDoneChime": "Zahrať akord pred dokončením",
"LabelSleepTimerAlmostDoneChimeHelp": "Zahrá akord, keď na časovači spánku zostáva 30 sekúnd",
"LabelStart": "Spustiť",
"LabelStartTime": "Čas spustenia",
"LabelStatsBestDay": "Najlepší deň",
"LabelStatsDailyAverage": "Denný priemer",
"LabelStatsDays": "Dní",
"LabelStatsDaysListened": "Dní počúvania",
"LabelStatsInARow": "v rade",
"LabelStatsItemsFinished": "Dokončených položiek",
"LabelStatsMinutes": "minút",
"LabelStatsMinutesListening": "Minút počúvania",
"LabelStatsWeekListening": "Týždňov počúvania",
"LabelStreamingUsingCellular": "Streamovanie na mobilných dátach",
"LabelTag": "Štítok",
"LabelTags": "Štítky",
"LabelTheme": "Téma",
"LabelThemeBlack": "Čierna",
"LabelThemeDark": "Tmavá",
"LabelThemeLight": "Svetlá",
"LabelTimeRemaining": "Zostáva {0}",
"LabelTitle": "Názov",
"LabelTotalSize": "Celková veľkosť",
"LabelTotalTrack": "Celková stopa",
"LabelTracks": "Stopy",
"LabelType": "Typ",
"LabelUnknown": "Neznámy",
"LabelUnlockPlayer": "Odomknúť prehrávač",
"LabelUseBookshelfView": "Použiť zobrazenie poličiek",
"LabelUser": "Používateľ",
"LabelUsername": "Prihlasovacie meno",
"LabelVeryHigh": "Veľmi vysoká",
"LabelVeryLow": "Veľmi nízka",
"LabelYearReviewHide": "Skryť rok v prehľade",
"LabelYearReviewShow": "Zobraziť rok v prehľade",
"LabelYourBookmarks": "Vaše záložky",
"LabelYourProgress": "Váš aktuálny stav",
"MessageAndroid10Downloads": "Android 10 a nižšie používajú na sťahovanie lokálny odkladací priestor aplikácie.",
"MessageAttemptingServerConnection": "Pokus o pripojenie servera...",
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf server nie je pripojený",
"MessageAudiobookshelfServerRequired": "<strong>Dôležité!</strong> Táto aplikácia je navrhnutá na fungovanie so Audiobookshelf serverom, ktorý patrí vám alebo niekomu inému. Táto aplikácia sama o sebe neposkytuje žiadny obsah.",
"MessageBookshelfEmpty": "Polička je prázdna",
"MessageConfirmDeleteLocalEpisode": "Želáte si odstrániť lokálnu epizódu \"{0}\" z vášho zariadenia? Súbor na serveri zostane nezmenený.",
"MessageConfirmDeleteLocalFiles": "Želáte si odstrániť z vášho zariadenia lokálne súbory patriace k tejto položke? Súbory na serveri a váš aktuálny stav počúvania zostanú nezmenené.",
"MessageConfirmDisableAutoTimer": "Ste si istí, že chcete vypnúť automatický časovač pre zvyšok dnešného dňa? Časovač sa znova zapne na konci nastavenej doby pre automatický časovač spánku alebo pri reštarte aplikácie.",
"MessageConfirmDiscardProgress": "Ste si istý, že chcete resetovať váš aktuálny stav počúvania?",
"MessageConfirmDownloadUsingCellular": "Chystáte sa sťahovať pri používaní mobilných dát. Toto môže mať za následok zvýšené poplatky u vášho poskytovateľ mobilnej siete. Chcete pokračovať?",
"MessageConfirmMarkAsFinished": "Ste si istý, že chcete označiť túto položku ako dokončenú?",
"MessageConfirmRemoveBookmark": "Ste si istý, že chcete odstrániť túto záložku?",
"MessageConfirmStreamingUsingCellular": "Chystáte sa streamovať pri používaní mobilných dát. To môže mať za následok zvýšené poplatky za využité dáta. Chcete pokračovať?",
"MessageDiscardProgress": "Odstrániť aktuálny stav počúvania",
"MessageDownloadCompleteProcessing": "Sťahovanie dokončené. Spracovávam...",
"MessageDownloading": "Sťahujem...",
"MessageDownloadingEpisode": "Sťahovanie epizódy",
"MessageEpisodesQueuedForDownload": "{0} epizód(a) v zozname na sťahovanie",
"MessageFeedURLWillBe": "URL zdroja bude {0}",
"MessageFetching": "Získavam...",
"MessageFollowTheProjectOnGithub": "Sledujte projekt na Github-e",
"MessageItemDownloadCompleteFailedToCreate": "Stiahnutie položky prebehlo úspešne, ale zlyhalo vytvorenie položky v knižnici",
"MessageItemMissing": "Položka neexistuje, chybu je nutné opraviť na serveri. Najčastejšou príčinou označenia položky za chýbajúcu je nedostupnosť jej umiestnenia.",
"MessageLoading": "Načítavam...",
"MessageLoadingServerData": "Načítavam údaje zo servera...",
"MessageLocalFolderDescription": "'Lokálny odkladací pristor aplikácie' je prístupný len cez aplikáciu. Aplikáciou je možné prehrávať iba médiá stiahnuté cez aplikáciu. Zdieľanie priečinkov aplikácie umožňuje iným aplikáciám pristupovať k médiám stiahnutým aplikáciou Audiobookshelf.",
"MessageMarkAsFinished": "Označiť ako dokončené",
"MessageMediaLinkedToADifferentServer": "Médiá majú prepojenie na Audiobookshelf server bežiaci na odlišnej adrese ({0}). Aktuálny stav počúvania bude synchronizovaný po pripojení k danému serveru.",
"MessageMediaLinkedToADifferentUser": "Média majú prepojenie na aktuálny server, ale boli stiahnuté odlišným používateľom. Stav počúvania bude synchronizovaný iba pre používateľa, ktorý ich stiahol.",
"MessageMediaLinkedToServer": "Prepojené na server {0}",
"MessageMediaLinkedToThisServer": "Stiahnuté médiá sú prepojené s týmto serverom",
"MessageMediaNotLinkedToServer": "Médiá nie sú prepojené na Audiobookshelf server. Stav počúvania nebude synchronizovaný.",
"MessageNoBookmarks": "Žiadne záložky",
"MessageNoChapters": "Žiadne kapitoly",
"MessageNoCollections": "Žiadne zbierky",
"MessageNoItems": "Žiadne položky",
"MessageNoItemsFound": "Žiadne položky neboli nájdené",
"MessageNoListeningSessions": "Žiadne relácie",
"MessageNoLogs": "Žiadne záznamy udalostí",
"MessageNoMediaFolders": "Žiadne priečinky médií",
"MessageNoNetworkConnection": "Žiadne sieťové pripojenie",
"MessageNoPodcastsFound": "Žiadne podcasty neboli nájdené",
"MessageNoSeries": "Žiadne série",
"MessageNoUpdatesWereNecessary": "Neboli potrebné žiadne aktualizácie",
"MessageNoUserPlaylists": "Nemáte žiadny playlist",
"MessageOldServerConnectionWarning": "Konfigurácia pripojenia na server používa staré ID používateľa. Prosím, odstráňte a nanovo pridajte dané pripojenie.",
"MessageOldServerConnectionWarningHelp": "Pripojenie na tento server bolo nakonfigurované pred migráciou databázy vo verzii 2.3.0, vydanej v júni 2023. V budúcej aktualizácii servera bude možnosť prihlasovania pomocou tohto starého pripojenia odstránená. Prosím, odstráňte aktuálne pripojenie na server a vytvorte nové (pomocou rovnakej adresy a prihlasovacích údajov). V prípade, ak máte na zariadení lokálne stiahnuté akékoľvek médiá, bude potrebné ich stiahnuť nanovo, aby bola možná serverová synchronizácia.",
"MessagePodcastSearchField": "Zadajte hľadaný výraz alebo URL RSS zdroja",
"MessageProgressSyncFailed": "Posledný pokus o odoslanie vášho stavu počúvania na server zlyhal. Aplikácia sa pokúsi odoslať váš stav každých 15 sekúnd až 1 minúty počas prehrávania média.",
"MessageReportBugsAndContribute": "Nahlásiť chyby, požiadavky na funkcie, a prispievať na",
"MessageSeriesAlreadyDownloaded": "Už ste si stiahli všetky knihy v tejto sérii.",
"MessageSeriesDownloadConfirm": "Stiahnuť chýbajúce: {0} knihu(-íh) obsahujúce {1} súbor(-ov), celkovo {2} do priečinka {3}?",
"MessageSeriesDownloadConfirmIos": "Stiahnuť chýbajúce: {0} knihu(-íh) s {1} súborom(-mi), celkovo {2}?",
"MessageSocketConnectedOverMeteredCellular": "Socket pripojený cez merané mobilné dáta",
"MessageSocketConnectedOverMeteredWifi": "Socket pripojený cez meranú wifi",
"MessageSocketConnectedOverUnmeteredCellular": "Socket pripojený cez nemerané mobilné dáta",
"MessageSocketConnectedOverUnmeteredWifi": "Socket pripojený cez nemeranú wifi",
"MessageSocketNotConnected": "Socket nie je pripojený",
"NoteRSSFeedPodcastAppsHttps": "Varovanie: Väčšina podcastových aplikácií požaduje URL RSS zdroja s HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Varovanie: 1 alebo viac vašich epizód neobsahuje infomáciu o dátum vydania. Niektoré podcastové ju vyžadujú.",
"ToastBookmarkCreateFailed": "Vytvorenie záložky zlyhalo",
"ToastBookmarkRemoveFailed": "Odstránenie záložky zlyhalo",
"ToastBookmarkUpdateFailed": "Aktualizácia záložky zlyhala",
"ToastDownloadNotAllowedOnCellular": "Sťahovanie cez mobilné dáta nie je povolené",
"ToastItemMarkedAsFinishedFailed": "Označenie za Dokončené zlyhalo",
"ToastItemMarkedAsNotFinishedFailed": "Označenie za Nedokončené zlyhalo",
"ToastPlaylistCreateFailed": "Vytvorenie playlistu zlyhalo",
"ToastPodcastCreateFailed": "Vytvorenie podcastu zlyhalo",
"ToastPodcastCreateSuccess": "Podcast bol vytvorený",
"ToastRSSFeedCloseFailed": "Odstránenie RSS zdroja zlyhalo",
"ToastRSSFeedCloseSuccess": "RSS zdroj bol odstránený",
"ToastStreamingNotAllowedOnCellular": "Streamovanie cez mobilné dáta nie je povolené"
}
+6
View File
@@ -146,6 +146,7 @@
"LabelEndOfChapter": "Konec poglavja",
"LabelEndTime": "Končni čas",
"LabelEpisode": "Epizoda",
"LabelExplicit": "Eksplicitno",
"LabelFeedURL": "URL vir",
"LabelFile": "Datoteka",
"LabelFileBirthtime": "Čas ustvarjanja datoteke",
@@ -220,6 +221,7 @@
"LabelPublishedDate": "Objavljeno {0}",
"LabelRSSFeedCustomOwnerEmail": "E-pošta lastnika po meri",
"LabelRSSFeedCustomOwnerName": "Ime lastnika po meri",
"LabelRSSFeedOpen": "RSS vir je odprt",
"LabelRSSFeedPreventIndexing": "Prepreči indeksiranje",
"LabelRSSFeedSlug": "Slug RSS vira",
"LabelRandomly": "Naključno",
@@ -296,6 +298,7 @@
"MessageDownloading": "Prenašanje...",
"MessageDownloadingEpisode": "Prenašam epizodo",
"MessageEpisodesQueuedForDownload": "{0} epizod v čakalni vrsti za prenos",
"MessageFailedToRefreshToken": "Osvežitev žetona ni uspela, potrebna je ponovna prijava",
"MessageFeedURLWillBe": "URL vira bo {0}",
"MessageFetching": "Pridobivam...",
"MessageFollowTheProjectOnGithub": "Sledite projektu na Githubu",
@@ -323,6 +326,9 @@
"MessageNoSeries": "Ni serij",
"MessageNoUpdatesWereNecessary": "Posodobitve niso bile potrebne",
"MessageNoUserPlaylists": "Nimate seznamov predvajanja",
"MessageOldServerAuthReLoginRequired": "Zaradi varnosti je bila v strežniku različice 2.26.0 izboljšana avtentikacija. Vsi uporabniki se morajo ponovno prijaviti.",
"MessageOldServerAuthWarning": "Strežnik uporablja zastarelo preverjanje avtentikacije",
"MessageOldServerAuthWarningHelp": "Ta strežnik uporablja različico, starejšo od v2.26.0. V različici v2.26.0 je bil dodan varnejši sistem preverjanja pristnosti. Zelo priporočljivo je, da strežnik posodobite na najnovejšo različico. Če ste strežnik že posodobili, se znova prijavite, da uporabite novo preverjanje pristnosti.",
"MessageOldServerConnectionWarning": "Nastavitev povezave s strežnikom uporablja stari ID uporabnika. Izbrišite in znova dodajte to povezavo s strežnikom.",
"MessageOldServerConnectionWarningHelp": "Povezavo s tem strežnikom ste prvotno nastavili pred selitvijo baze podatkov v različici 2.3.0, izdani junija 2023. Prihodnja posodobitev strežnika bo odstranila možnost prijave s to staro povezavo. Izbrišite obstoječo povezavo s strežnikom in se znova povežite (z istim naslovom strežnika in poverilnicami). Če imate v tej napravi prenesen medij, ga boste morali znova prenesti za sinhronizacijo s strežnikom.",
"MessagePodcastSearchField": "Vnesite iskalni izraz ali URL vira RSS",
+11 -6
View File
@@ -51,7 +51,7 @@
"ButtonSeries": "Serie",
"ButtonSetTimer": "Ställ in timer",
"ButtonStream": "Ström",
"ButtonSubmit": "Skicka",
"ButtonSubmit": "Spara",
"ButtonSwitchServerUser": "Byt Server/Användare",
"ButtonUnmaskServerAddress": "Visa server adress",
"ButtonUserStats": "Användarstatistik",
@@ -91,7 +91,7 @@
"HeaderStatsRecentSessions": "Senaste tillfällena",
"HeaderTableOfContents": "Innehållsförteckning",
"HeaderUserInterfaceSettings": "Användargränssnittsinställningar",
"HeaderYourStats": "Dina statistik",
"HeaderYourStats": "Din statistik",
"LabelAddToPlaylist": "Lägg till i en spellista",
"LabelAddedAt": "Tillagd vid",
"LabelAddedDate": "Adderad {0}",
@@ -146,10 +146,11 @@
"LabelEndOfChapter": "Slut av kapitel",
"LabelEndTime": "Slut tid",
"LabelEpisode": "Avsnitt",
"LabelExplicit": "Bestämd",
"LabelFeedURL": "URL-adress för flödet",
"LabelFile": "Fil",
"LabelFileBirthtime": "Tidpunkt, fil skapad",
"LabelFileModified": "Tidpunkt, fil ändrad",
"LabelFileBirthtime": "Tidpunkt, adderad",
"LabelFileModified": "Tidpunkt, ändrad",
"LabelFilename": "Filnamn",
"LabelFinished": "Avslutad",
"LabelFolder": "Mapp",
@@ -220,6 +221,7 @@
"LabelPublishedDate": "Publicerad {0}",
"LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post",
"LabelRSSFeedCustomOwnerName": "Anpassat ägarnamn",
"LabelRSSFeedOpen": "RSS-flöde öppet",
"LabelRSSFeedPreventIndexing": "Förhindra indexering",
"LabelRSSFeedSlug": "RSS-flödesslag",
"LabelRandomly": "Slumpartat",
@@ -265,7 +267,7 @@
"LabelTitle": "Titel",
"LabelTotalSize": "Total Storlek",
"LabelTotalTrack": "Total Spår",
"LabelTracks": "Spår",
"LabelTracks": "Ljudspår",
"LabelType": "Typ",
"LabelUnknown": "Okänd",
"LabelUnlockPlayer": "Lås upp spelare",
@@ -277,7 +279,7 @@
"LabelYearReviewHide": "Dölj årets sammanställning",
"LabelYearReviewShow": "Visa årets sammanställning",
"LabelYourBookmarks": "Dina bokmärken",
"LabelYourProgress": "Din framsteg",
"LabelYourProgress": "Framsteg",
"MessageAndroid10Downloads": "Android 10 och lägre använder intern applagring för nedladdningar.",
"MessageAttemptingServerConnection": "Försöker ansluta till server...",
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf-server är ej ansluten",
@@ -323,6 +325,9 @@
"MessageNoSeries": "Inga serier",
"MessageNoUpdatesWereNecessary": "Inga uppdateringar var nödvändiga",
"MessageNoUserPlaylists": "Du har inga spellistor",
"MessageOldServerAuthReLoginRequired": "Autentiseringen har förbättrats för ökad säkerhet i server v2.26.0. Alla användare måste logga in igen.",
"MessageOldServerAuthWarning": "Servern använder en gammal autentisering",
"MessageOldServerAuthWarningHelp": "Den här servern kör en version äldre än v2.26.0. Ett säkrare autentiseringssystem finns i v2.26.0. Det rekommenderas starkt att uppdatera servern till den senaste versionen. Om du redan har uppdaterat servern, logga in igen för att använda den nya autentiseringen.",
"MessageOldServerConnectionWarning": "Serveranslutningskonfigurationen använder ett gammalt användar-ID. Vänligen ta bort och lägg till denna serveranslutning igen.",
"MessageOldServerConnectionWarningHelp": "Du ställde ursprungligen in anslutningen till denna server före databasflytten i version 2.3.0, som släpptes i juni 2023. En framtida serveruppdatering kommer att ta bort möjligheten att logga in med denna gamla anslutning. Vänligen ta bort den befintliga serveranslutningen och anslut igen (med samma serveradress och inloggningsuppgifter). Om du har nedladdad media på den här enheten måste den laddas ner igen för att synkroniseras med servern.",
"MessagePodcastSearchField": "Skriv sökfrågan eller URL-adressen för RSS-flödet",
+6
View File
@@ -6,6 +6,7 @@
"ButtonCancel": "İptal",
"ButtonCancelTimer": "İptal Zamanlayıcısı",
"ButtonClearFilter": "Filtreyi Temizle",
"ButtonClearLogs": "Günlükleri Temizle",
"ButtonCloseFeed": "Akışı Kapat",
"ButtonCollections": "Koleksiyonlar",
"ButtonConnect": "Bağlan",
@@ -26,7 +27,9 @@
"ButtonLatest": "En yeni",
"ButtonLibrary": "Kütüphane",
"ButtonLocalMedia": "Yerel Medya",
"ButtonLogs": "Günlükler",
"ButtonManageLocalFiles": "Yerel Dosyaları Yönet",
"ButtonMaskServerAddress": "Sunucu adresini maskele",
"ButtonNewFolder": "Yeni Klasör",
"ButtonNextEpisode": "Sonraki Bölüm",
"ButtonOk": "Tamam",
@@ -50,6 +53,7 @@
"ButtonStream": "Yayın",
"ButtonSubmit": "Gönder",
"ButtonSwitchServerUser": "Sunucu/Kullanıcı Değiştir",
"ButtonUnmaskServerAddress": "Sunucu adresinin maskesini kaldır",
"ButtonUserStats": "Kullanıcı İstatistikleri",
"ButtonYes": "Evet",
"HeaderAccount": "Hesap",
@@ -142,6 +146,7 @@
"LabelEndOfChapter": "Bölüm Sonu",
"LabelEndTime": "Bitiş zamanı",
"LabelEpisode": "Bölüm",
"LabelExplicit": "Belirgin",
"LabelFeedURL": "Akış URLsi",
"LabelFile": "Dosya",
"LabelFileBirthtime": "Dosya Oluşum Zamanı",
@@ -202,6 +207,7 @@
"LabelOn": "Açık",
"LabelPassword": "Şifre",
"LabelPath": "Yol",
"LabelPlaybackDirect": "Doğrudan",
"LabelPlaybackLocal": "Yerel",
"LabelPlaybackSpeed": "Oynatma Hızı",
"LabelPlaybackTranscode": "Transcode",
+19 -13
View File
@@ -36,7 +36,7 @@
"ButtonOpenFeed": "Відкрити стрічку",
"ButtonOverride": "Перевизначити",
"ButtonPause": "Призупинити",
"ButtonPlay": "Слухати",
"ButtonPlay": "Відтворити",
"ButtonPlayEpisode": "Слухати епізод",
"ButtonPlaylists": "Списки відтворення",
"ButtonRead": "Читати",
@@ -112,7 +112,7 @@
"LabelAutoSleepTimerAutoRewind": "Автоперемотування автотаймеру вимкнення",
"LabelAutoSleepTimerAutoRewindHelp": "Коли сплине автотаймер вимкнення, відтворення знову автоматично перемотає доріжку.",
"LabelAutoSleepTimerHelp": "Таймер вимкнення автоматично ввімкнеться при відтворенні медіа між вказаним початковим та кінцевим часом.",
"LabelBooks": "Книги",
"LabelBooks": "Книг",
"LabelChapterTrack": "Прогрес глави",
"LabelChapters": "Глави",
"LabelClosePlayer": "Закрити програвач",
@@ -121,7 +121,7 @@
"LabelContinueBooks": "Продовжити книги",
"LabelContinueEpisodes": "Продовжити епізоди",
"LabelContinueListening": "Слухати далі",
"LabelContinueReading": "Читати далі",
"LabelContinueReading": "Продовжити читання",
"LabelContinueSeries": "Продовжити серії",
"LabelCustomTime": "Власний час",
"LabelDescription": "Опис",
@@ -133,7 +133,7 @@
"LabelDisableVibrateOnReset": "Вимкнути вібрацію при скиданні",
"LabelDisableVibrateOnResetHelp": "Коли таймер вимкнення буде скинуто, ваш пристрій завібрує. Увімкніть цей параметр, щоб не вібрувати при скиданні таймера.",
"LabelDiscover": "Огляд",
"LabelDownload": "Завантажити",
"LabelDownload": "Скачати",
"LabelDownloadUsingCellular": "Завантажувати через мобільний інтернет",
"LabelDownloaded": "Завантажено",
"LabelDuration": "Тривалість",
@@ -146,10 +146,11 @@
"LabelEndOfChapter": "Кінець глави",
"LabelEndTime": "Час завершення",
"LabelEpisode": "Епізод",
"LabelExplicit": "Відвертий",
"LabelFeedURL": "Адреса стрічки",
"LabelFile": "Файл",
"LabelFileBirthtime": "Дата створення",
"LabelFileModified": "Дата змінення",
"LabelFileBirthtime": "Дата створення файлу",
"LabelFileModified": "Дата зміни файлу",
"LabelFilename": "Ім'я файлу",
"LabelFinished": "Завершено",
"LabelFolder": "Тека",
@@ -220,6 +221,7 @@
"LabelPublishedDate": "Опубліковано {0}",
"LabelRSSFeedCustomOwnerEmail": "Користувацька електронна адреса власника",
"LabelRSSFeedCustomOwnerName": "Користувацьке ім'я власника",
"LabelRSSFeedOpen": "RSS-канал відкритий",
"LabelRSSFeedPreventIndexing": "Запобігати індексації",
"LabelRSSFeedSlug": "Назва RSS-каналу",
"LabelRandomly": "Випадково",
@@ -294,8 +296,9 @@
"MessageDiscardProgress": "Скинути прогрес",
"MessageDownloadCompleteProcessing": "Завантаження завершено. Обробка...",
"MessageDownloading": "Завантажується...",
"MessageDownloadingEpisode": "Завантаження епізоду",
"MessageEpisodesQueuedForDownload": "Епізодів у черзі завантаження: {0}",
"MessageDownloadingEpisode": "Скачування епізоду",
"MessageEpisodesQueuedForDownload": "{0} епізод(ів) у черзі на завантаження",
"MessageFailedToRefreshToken": "Не вдалося оновити токен, потрібен повторний увійти",
"MessageFeedURLWillBe": "URL-адреса каналу буде {0}",
"MessageFetching": "Отримання...",
"MessageFollowTheProjectOnGithub": "Слідкуйте за проєктом на GitHub",
@@ -304,7 +307,7 @@
"MessageLoading": "Завантаження...",
"MessageLoadingServerData": "Завантаження даних сервера...",
"MessageLocalFolderDescription": "«Внутрішня пам’ять програми» доступна лише цій програмі. Ця програма підтримує лише мультимедійні файли, завантажені безпосередньо через програму. Спільні папки для зберігання можна використовувати, щоб надати іншим програмам доступ до медіафайлів, завантажених цією програмою.",
"MessageMarkAsFinished": "Позначити завершеним",
"MessageMarkAsFinished": "Позначити як завершене",
"MessageMediaLinkedToADifferentServer": "Медіа пов'язане з Audiobookself-сервером за іншою адресою ({0}). Прогрес буде синхронізовано після підключення до вказаного сервера.",
"MessageMediaLinkedToADifferentUser": "Медіа пов'язане з цим сервером, але було завантажене іншим користувачем. Прогрес буде синхронізовано лише для користувача, що завантажив медіа.",
"MessageMediaLinkedToServer": "Пов'язано з сервером {0}",
@@ -316,13 +319,16 @@
"MessageNoItems": "Елементи відсутні",
"MessageNoItemsFound": "Елементів не знайдено",
"MessageNoListeningSessions": "Сеанси прослуховування відсутні",
"MessageNoLogs": "Без журналів",
"MessageNoLogs": "Немає журналу",
"MessageNoMediaFolders": "Теки медіа відсутні",
"MessageNoNetworkConnection": "Немає підключення до мережі",
"MessageNoPodcastsFound": "Подкастів не знайдено",
"MessageNoSeries": "Серії відсутні",
"MessageNoUpdatesWereNecessary": "Оновлень не потрібно",
"MessageNoUpdatesWereNecessary": "Оновлення не потрібні",
"MessageNoUserPlaylists": "У вас немає списків відтворення",
"MessageOldServerAuthReLoginRequired": "Автентифікацію покращено для безпеки на сервері версії 2.26.0. Усім користувачам потрібно повторно увійти в систему.",
"MessageOldServerAuthWarning": "Сервер використовує застарілу автентифікацію",
"MessageOldServerAuthWarningHelp": "Цей сервер використовує версію, старішу за v2.26.0. У версії 2.26.0 було додано безпечнішу систему автентифікації. Наполегливо рекомендується оновити сервер до останньої версії. Якщо ви вже оновили сервер, увійдіть знову, щоб використовувати нову автентифікацію.",
"MessageOldServerConnectionWarning": "Конфігурація підключення до сервера використовує старий ідентифікатор користувача. Видаліть і додайте це підключення до сервера заново.",
"MessageOldServerConnectionWarningHelp": "Ви спочатку налаштували з’єднання з цим сервером перед міграцією бази даних у версії 2.3.0, випущеній у червні 2023 року. Майбутнє оновлення сервера скасує можливість входу за допомогою цього старого з’єднання. Будь ласка, видаліть існуюче підключення до сервера та підключіться знову (використовуючи ту саму адресу сервера та облікові дані). Якщо на цьому пристрої є будь-які завантажені медіафайли, їх потрібно буде завантажити ще раз, щоб синхронізувати із сервером.",
"MessagePodcastSearchField": "Введіть пошуковий запит або URL RSS-стрічки",
@@ -336,8 +342,8 @@
"MessageSocketConnectedOverUnmeteredCellular": "Сокет під'єднано до безлімітної мобільної мережі",
"MessageSocketConnectedOverUnmeteredWifi": "Сокет під'єднано до безлімітної мережі Wi-Fi",
"MessageSocketNotConnected": "Сокет не під'єднано",
"NoteRSSFeedPodcastAppsHttps": "Попередження: Більшість додатків подкастів вимагатимуть використання протоколу HTTPS від RSS-каналу",
"NoteRSSFeedPodcastAppsPubDate": "Попередження: 1 або більше ваших епізодів не мають дати публікації. Деякі додатки подкастів вимагають це.",
"NoteRSSFeedPodcastAppsHttps": "Попередження: більшість додатків подкастів вимагають використання HTTPS для RSS-каналу",
"NoteRSSFeedPodcastAppsPubDate": "Попередження: один або більше ваших епізодів не мають дати публікації. Деякі додатки подкастів цього вимагають.",
"ToastBookmarkCreateFailed": "Не вдалося створити закладку",
"ToastBookmarkRemoveFailed": "Не вдалося видалити закладку",
"ToastBookmarkUpdateFailed": "Не вдалося оновити закладку",
+38 -5
View File
@@ -2,10 +2,11 @@
"ButtonAdd": "Thêm",
"ButtonAddNewServer": "Thêm Máy Chủ Mới",
"ButtonAuthors": "Tác Giả",
"ButtonBack": "Quay Lại",
"ButtonBack": "Quay lại",
"ButtonCancel": "Hủy",
"ButtonCancelTimer": "Hủy Bộ Đếm",
"ButtonClearFilter": "Xóa Bộ Lọc",
"ButtonClearLogs": "Xoá logs",
"ButtonCloseFeed": "Đóng Feed",
"ButtonCollections": "Bộ Sưu Tập",
"ButtonConnect": "Kết Nối",
@@ -19,21 +20,28 @@
"ButtonDeleteLocalItem": "Xóa mục địa phương",
"ButtonDisableAutoTimer": "Tắt Bộ Đếm Tự Động",
"ButtonDisconnect": "Ngắt Kết Nối",
"ButtonGoToWebClient": "Đi tới trang web",
"ButtonHistory": "Lịch Sử",
"ButtonHome": "Trang Chủ",
"ButtonIssues": "Vấn Đề",
"ButtonLatest": "Mới Nhất",
"ButtonLibrary": "Thư Viện",
"ButtonLocalMedia": "Phương Tiện Địa Phương",
"ButtonLogs": "Logs",
"ButtonManageLocalFiles": "Quản Lý Tập Tin Địa Phương",
"ButtonMaskServerAddress": "Ẩn địa chỉ máy chủ",
"ButtonNewFolder": "Thư Mục Mới",
"ButtonNextEpisode": "Tập Kế Tiếp",
"ButtonOk": "Chấp nhận",
"ButtonOpenFeed": "Mở Feed",
"ButtonOverride": "Bỏ Qua",
"ButtonPause": "Tạm Dừng",
"ButtonPlay": "Phát",
"ButtonPlayEpisode": "Phát",
"ButtonPlaylists": "Danh Sách Phát",
"ButtonRead": "Đọc",
"ButtonReadLess": "Thu gọn",
"ButtonReadMore": "Đọc thêm",
"ButtonRemove": "Xóa",
"ButtonRemoveFromServer": "Xóa khỏi Máy Chủ",
"ButtonSave": "Lưu",
@@ -45,15 +53,18 @@
"ButtonStream": "Phát Trực Tiếp",
"ButtonSubmit": "Gửi",
"ButtonSwitchServerUser": "Chuyển Đổi Máy Chủ/Người Dùng",
"ButtonUnmaskServerAddress": "Hiện địa chỉ máy chủ",
"ButtonUserStats": "Thống Kê Người Dùng",
"ButtonYes": "Có",
"HeaderAccount": "Tài Khoản",
"HeaderAdvanced": "Nâng Cao",
"HeaderAndroidAutoSettings": "Cài đặt Android Auto",
"HeaderAudioTracks": "Danh Sách Âm Thanh",
"HeaderChapters": "Chương",
"HeaderCollection": "Bộ Sưu Tập",
"HeaderCollectionItems": "Mục Bộ Sưu Tập",
"HeaderCollectionItems": "Danh sách Bộ Sưu Tập",
"HeaderConnectionStatus": "Trạng Thái Kết Nối",
"HeaderDataSettings": "Cài đặt dữ liệu",
"HeaderDetails": "Chi Tiết",
"HeaderDownloads": "Tải Xuống",
"HeaderEbookFiles": "Tập Tin Ebook",
@@ -68,6 +79,7 @@
"HeaderPlaybackSettings": "Cài Đặt Phát Lại",
"HeaderPlaylist": "Danh Sách Phát",
"HeaderPlaylistItems": "Các Mục Danh Sách Phát",
"HeaderProgressSyncFailed": "Lỗi đồng bộ hoá",
"HeaderRSSFeedGeneral": "Thông Tin Chi Tiết về RSS",
"HeaderRSSFeedIsOpen": "RSS Feed đã được Mở",
"HeaderSelectDownloadLocation": "Chọn Vị Trí Tải Xuống",
@@ -78,16 +90,20 @@
"HeaderStatsRecentSessions": "Các Phiên Gần Đây",
"HeaderTableOfContents": "Mục Lục",
"HeaderUserInterfaceSettings": "Cài Đặt Giao Diện Người Dùng",
"HeaderYourStats": "Thống Kê của Bạn",
"HeaderYourStats": "Thống Kê Của Bạn",
"LabelAddToPlaylist": "Thêm vào Danh Sách Phát",
"LabelAddedAt": "Đã Thêm Vào",
"LabelAddedDate": "Đã thêm {0}",
"LabelAll": "Tất Cả",
"LabelAllowSeekingOnMediaControls": "Cho phép tìm kiếm vị trí trên các điều khiển phương tiện thông báo",
"LabelAlways": "Luôn luôn",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "Không dùng danh sách sổ xuống theo thứ tự chữ cái khi số mục hiển thị ít hơn số lượng này",
"LabelAskConfirmation": "Yêu cầu xác nhận",
"LabelAuthor": "Tác Giả",
"LabelAuthorFirstLast": "Tác Giả (Tên Đầu Tiên, Họ)",
"LabelAuthorLastFirst": "Tác Giả (Họ, Tên Đầu Tiên)",
"LabelAuthors": "Tác Giả",
"LabelAutoDownloadEpisodes": "Tự Động Tải Xuống Các Tập Phim",
"LabelAutoDownloadEpisodes": "Tự Động Tải Xuống Các Tập",
"LabelAutoRewindTime": "Thời Gian Tự Động Lùi",
"LabelAutoSleepTimer": "Bộ Đếm Thời Gian Ngủ Tự Động",
"LabelAutoSleepTimerAutoRewind": "Lùi Tự Động Bộ Đếm Thời Gian Ngủ",
@@ -115,8 +131,10 @@
"LabelDisableVibrateOnResetHelp": "Khi bộ đếm thời gian ngủ được đặt lại, thiết bị của bạn sẽ rung. Bật cài đặt này để không rung khi bộ đếm thời gian ngủ được đặt lại.",
"LabelDiscover": "Khám Phá",
"LabelDownload": "Tải Xuống",
"LabelDownloadUsingCellular": "Tải xuống với dữ liệu di động",
"LabelDownloaded": "Đã Tải Xuống",
"LabelDuration": "Thời Lượng",
"LabelEbooks": "",
"LabelEnable": "Bật",
"LabelEnableMp3IndexSeeking": "Bật Tìm Kiếm Chỉ Mục Mp3",
"LabelEnableMp3IndexSeekingHelp": "Cài đặt này chỉ nên được bật nếu bạn có các tập tin mp3 không tìm kiếm đúng cách. Việc tìm kiếm không chính xác chủ yếu là do các tập tin MP3 biến đổi biến (VBR). Cài đặt này sẽ buộc tìm kiếm chỉ mục, trong đó một bản đồ thời gian-đến-byte được xây dựng khi tập tin được đọc. Trong một số trường hợp với các tập tin MP3 lớn, có thể có một sự trễ khi tìm kiếm về phía cuối của tập tin.",
@@ -131,6 +149,7 @@
"LabelFilename": "Tên Tập Tin",
"LabelFinished": "Hoàn Thành",
"LabelFolder": "Thư Mục",
"LabelFontBoldness": "Chữ đậm",
"LabelFontScale": "Tỷ Lệ Phông Chữ",
"LabelGenre": "Thể Loại",
"LabelGenres": "Thể Loại",
@@ -145,6 +164,7 @@
"LabelInternalAppStorage": "Bộ Nhớ Ứng Dụng Nội Bộ",
"LabelJumpBackwardsTime": "Nhảy Lùi Thời Gian",
"LabelJumpForwardsTime": "Nhảy Chuyển Tiếp Thời Gian",
"LabelKeepScreenAwake": "Không khoá màn hình",
"LabelLanguage": "Ngôn Ngữ",
"LabelLayout": "Bố Cục",
"LabelLayoutAuto": "Tự Động",
@@ -159,27 +179,40 @@
"LabelLow": "Thấp",
"LabelMediaType": "Loại Phương Tiện",
"LabelMedium": "Trung Bình",
"LabelMissing": "",
"LabelMore": "Thêm",
"LabelMoreInfo": "Thông Tin Thêm",
"LabelName": "Tên",
"LabelNarrator": "Người Đọc",
"LabelNarrators": "Người Đọc",
"LabelNavigateWithVolume": "Điều hướng bằng nút âm thanh",
"LabelNavigateWithVolumeWhilePlaying": "Cho phép sử dụng nút âm lượng để điều hướng",
"LabelNavigateWithVolumeWhilePlayingDisabled": "Không cho phép sử dụng nút âm lượng để điều hướng trong lúc phát",
"LabelNavigateWithVolumeWhilePlayingEnabled": "Cho phép sử dụng nút âm lượng để điều hướng trong lúc phát",
"LabelNever": "Không bao giờ",
"LabelNewestAuthors": "Tác Giả Mới Nhất",
"LabelNewestEpisodes": "Các Tập Phim Mới Nhất",
"LabelNo": "Không",
"LabelNotFinished": "Chưa Hoàn Thành",
"LabelNotStarted": "Chưa Bắt Đầu",
"LabelNumEpisodes": "{0} tập",
"LabelNumEpisodesIncomplete": "{0} tập, {1} chưa hoàn thành",
"LabelNumberOfEpisodes": "Số lượng tập",
"LabelOff": "Tắt",
"LabelOn": "Bật",
"LabelPassword": "Mật Khẩu",
"LabelPath": "Đường Dẫn",
"LabelPlaybackDirect": "Trực Tiếp",
"LabelPlaybackLocal": "Địa Phương",
"LabelPlaybackSpeed": "Tốc Độ Phát Lại",
"LabelPlaybackTranscode": "Chuyển Đổi",
"LabelPreventIndexing": "Ngăn Chặn Feed của bạn được lập chỉ mục bởi iTunes và Google podcast directories",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPreventIndexing": "Ngăn Chặn Feed của bạn bị truy xuất bởi thư mục iTunes và Google podcast",
"LabelProgress": "Tiến Độ",
"LabelPubDate": "Ngày Xuất Bản",
"LabelPublishYear": "Năm Xuất Bản",
"LabelPublishedDate": "Xuất bản vào {0}",
"LabelRSSFeedCustomOwnerEmail": "Email Chủ Sở Hữu Tùy Chỉnh",
"LabelRSSFeedCustomOwnerName": "Tên Chủ Sở Hữu Tùy Chỉnh",
"LabelRSSFeedPreventIndexing": "Ngăn Chặn Lập Chỉ Mục",
+14 -8
View File
@@ -29,7 +29,7 @@
"ButtonLocalMedia": "本地媒体",
"ButtonLogs": "日志",
"ButtonManageLocalFiles": "管理本地文件",
"ButtonMaskServerAddress": "屏蔽服务器地址",
"ButtonMaskServerAddress": "隐藏服务器地址",
"ButtonNewFolder": "新建文件夹",
"ButtonNextEpisode": "下一集",
"ButtonOk": "确定",
@@ -40,7 +40,7 @@
"ButtonPlayEpisode": "播放剧集",
"ButtonPlaylists": "播放列表",
"ButtonRead": "读取",
"ButtonReadLess": "阅读较少",
"ButtonReadLess": "收起",
"ButtonReadMore": "阅读更多",
"ButtonRemove": "移除",
"ButtonRemoveFromServer": "从服务器中删除",
@@ -53,7 +53,7 @@
"ButtonStream": "串流播放",
"ButtonSubmit": "提交",
"ButtonSwitchServerUser": "切换服务器/用户",
"ButtonUnmaskServerAddress": "取消屏蔽服务器地址",
"ButtonUnmaskServerAddress": "显示服务器地址",
"ButtonUserStats": "用户统计信息",
"ButtonYes": "确定",
"HeaderAccount": "帐户",
@@ -81,7 +81,7 @@
"HeaderPlaylistItems": "播放列表项目",
"HeaderProgressSyncFailed": "进度同步失败",
"HeaderRSSFeed": "RSS 源",
"HeaderRSSFeedGeneral": "RSS 源概述",
"HeaderRSSFeedGeneral": "RSS 详细信息",
"HeaderRSSFeedIsOpen": "RSS 源已打开",
"HeaderSelectDownloadLocation": "选择下载位置",
"HeaderSettings": "设置",
@@ -146,6 +146,7 @@
"LabelEndOfChapter": "章节结束",
"LabelEndTime": "结束时间",
"LabelEpisode": "剧集",
"LabelExplicit": "含成人内容",
"LabelFeedURL": "源 URL",
"LabelFile": "文件",
"LabelFileBirthtime": "文件创建时间",
@@ -220,6 +221,7 @@
"LabelPublishedDate": "已发布 {0}",
"LabelRSSFeedCustomOwnerEmail": "自定义所有者电子邮件",
"LabelRSSFeedCustomOwnerName": "自定义所有者名称",
"LabelRSSFeedOpen": "打开 RSS 源",
"LabelRSSFeedPreventIndexing": "防止索引",
"LabelRSSFeedSlug": "RSS 源段",
"LabelRandomly": "随机",
@@ -245,11 +247,11 @@
"LabelSleepTimerAlmostDoneChimeHelp": "当睡眠定时器还剩30秒时播放铃声",
"LabelStart": "开始",
"LabelStartTime": "开始时间",
"LabelStatsBestDay": "最好的一天",
"LabelStatsBestDay": "单日最高",
"LabelStatsDailyAverage": "每日平均值",
"LabelStatsDays": "",
"LabelStatsDays": "连续",
"LabelStatsDaysListened": "收听天数",
"LabelStatsInARow": "在一行",
"LabelStatsInARow": "",
"LabelStatsItemsFinished": "已完成的项目",
"LabelStatsMinutes": "分钟",
"LabelStatsMinutesListening": "收听分钟数",
@@ -296,6 +298,7 @@
"MessageDownloading": "下载中...",
"MessageDownloadingEpisode": "正在下载剧集",
"MessageEpisodesQueuedForDownload": "{0} 个剧集排队等待下载",
"MessageFailedToRefreshToken": "无法刷新令牌,需要重新登录",
"MessageFeedURLWillBe": "源 URL 将改为 {0}",
"MessageFetching": "正在获取...",
"MessageFollowTheProjectOnGithub": "在 GitHub 上关注此项目",
@@ -323,11 +326,14 @@
"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": "报告错误、请求功能和贡献在",
"MessageReportBugsAndContribute": "反馈问题, 建议功能或参与贡献, 请访问",
"MessageSeriesAlreadyDownloaded": "你已下载此系列的所有书籍.",
"MessageSeriesDownloadConfirm": "将缺失的 {0} 本图书和 {1} 个文件 (共 {2} 个) 下载至文件夹 {3}?",
"MessageSeriesDownloadConfirmIos": "是否下载缺失的 {0} 本书和 {1} 个文件,总计 {2} 个?",