Compare commits

..
238 Commits
Author SHA1 Message Date
advplyr 12025ab59a Version bump 0.14.0-beta 2026-08-04 17:26:09 -05:00
advplyrandGitHub f66840e8e4 Merge pull request #1948 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2026-08-04 17:17:03 -05:00
violawang0401-cloudandHosted Weblate 4c3e098785 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hant/
2026-08-04 23:59:01 +02:00
violawang0401-cloudandHosted Weblate 441086e773 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 94.7% (361 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hant/
2026-08-04 23:59:00 +02:00
FiendFEARingandHosted Weblate 1408a4c3ff Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2026-08-04 23:59:00 +02:00
fe67a221ac Render a higher resolution cover in the Android notification background (#1917)
* Render a higher resolution bitmap in the notification background art

Previously only METADATA_KEY_ALBUM_ART_URI/ART_URI were set, leaving notification/lock-screen/Android Auto consumers to resolve the cover art themselves. Likely due to internal scaling, a lower quality bitmap was loaded.

This commit adds a shared resolveUriAsBitmap() helper (extracted from AbMediaDescriptionAdapter.kt) and uses it to:
  - Resolve and cache a bitmap on PlaybackSession once per
  session (synchronously for local covers, asynchronously for server covers), then set it on METADATA_KEY_ALBUM_ART/ART once available, falling back to the URI keys until it resolves.
  - Reuse the same helper in AbMediaDescriptionAdapter so the notification icon is resolved consistently.

  PlayerNotificationService tracks the resolution Job and cancels/replaces it on each new playback session, invalidating the media session metadata once the bitmap is ready.

* Remove the size override in Glide

The override was not needed. It wasn't in the original code, so I'll remove it for consistency.

* Update notification icon to reuse cover bitmap if already loaded on session

---------

Co-authored-by: advplyr <advplyr@protonmail.com>
2026-08-04 16:58:47 -05:00
advplyr f087cbed6f Update notification icon to reuse cover bitmap if already loaded on session 2026-08-04 16:57:00 -05:00
9820db0ef6 fix(android-auto): run server connection check off the main thread (#1920)
Android Auto browsing could freeze and crash the app (phone app "closes",
Android Auto shows a generic connection error) whenever the Capacitor
webview had not yet established the server connection.

Root cause: onLoadChildren runs on the media browser service main thread.
It calls loadAndroidAutoItems -> checkSetValidServerConnectionConfig, which
used runBlocking to ping every saved server config and authorize the user.
That blocked the main thread on network I/O. Android Auto (plus Assistant)
re-request the browse root every ~2s, and every onGetRoot flags a reload
while disconnected, so the blocking work was triggered repeatedly and
re-entrantly, producing an ANR.

Changes:
- checkSetValidServerConnectionConfig now runs on a dedicated
  Dispatchers.IO scope instead of runBlocking, so pings/authorize never
  block the caller. The callback contract is unchanged; downstream Android
  Auto callbacks already ran off the main thread.
- loadAndroidAutoItems coalesces overlapping calls into a single in-flight
  load and fires all queued callbacks on completion, so the browse-root
  polling storm can no longer spawn concurrent loads racing shared state.
- Fix latent bug in checkResetServerItems: server config id was compared
  with !== (reference identity) instead of != (value), causing spurious
  cache resets that fed the reload storm.

Co-authored-by: databoy2k <18686442+databoy2k@users.noreply.github.com>
2026-08-04 16:26:09 -05:00
advplyrandGitHub 9664f79b1f Merge pull request #1947 from hypnotoad08/fix/finished-book-restarts
Restart finished local media from the beginning
2026-08-03 17:36:59 -05:00
advplyr 533396b855 Android clear current time on local media progress when marking as finished 2026-08-03 17:32:19 -05:00
advplyrandGitHub 005a051cd0 Merge pull request #1906 from jucor/fix/audiotrack-nullable-mimetype
Make AudioTrack.mimeType nullable to prevent Android playback stall (fixes #1905)
2026-08-03 17:06:18 -05:00
hypnotoad08 f06168fdc2 Restart finished local media from the beginning 2026-08-02 20:15:20 -04:00
advplyrandGitHub 26c6b9149b Merge pull request #1938 from cosandr/fix-304
Remember sort & filter options for podcast episodes
2026-08-02 17:49:02 -05:00
advplyr 8b70d02332 Fix getUserSetting incorrectly returning null for all falsy values 2026-08-02 17:43:45 -05:00
advplyrandGitHub 4370b19e12 Merge pull request #1879 from OguzIronCode/fix/search-result-limit
Remove hardcoded search result limit of 5
2026-08-02 17:28:34 -05:00
advplyrandGitHub f708e4dc07 Merge pull request #1862 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2026-08-02 07:28:09 -05:00
Vladimir MakeevandHosted Weblate 4bb809c1b9 Translated using Weblate (Russian)
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2026-08-01 22:54:17 +00:00
Igor DobračaandHosted Weblate ccf57bb016 Translated using Weblate (Croatian)
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2026-08-01 22:54:11 +00:00
Pavel MiniutkaandHosted Weblate 65b2a1a9a4 Translated using Weblate (Belarusian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-08-01 22:54:06 +00:00
ZarakkasandHosted Weblate 8e6ef8e718 Translated using Weblate (German)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2026-08-01 22:54:04 +00:00
Mona LisaandHosted Weblate cbea6229b5 Translated using Weblate (Swedish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2026-08-01 22:54:02 +00:00
Axel BjörnssonandHosted Weblate 05205eb646 Translated using Weblate (Icelandic)
Currently translated at 2.6% (10 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/is/
2026-08-01 22:54:00 +00:00
Mona LisaandHosted Weblate 32fc866d8a Translated using Weblate (Swedish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2026-08-01 22:53:57 +00:00
Mona LisaandHosted Weblate da089338dc Translated using Weblate (Swedish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2026-08-01 22:53:55 +00:00
FiendFEARingandHosted Weblate e19f47b291 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (374 of 374 strings)

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

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2026-08-01 22:53:51 +00:00
FiendFEARingandHosted Weblate 3db3bbd756 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2026-08-01 22:53:48 +00:00
FiendFEARingandHosted Weblate 24ab13e27e Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2026-08-01 22:53:46 +00:00
FiendFEARingandHosted Weblate 195d1d1ef4 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2026-08-01 22:53:45 +00:00
burghy86andHosted Weblate 5383e91249 Translated using Weblate (Italian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2026-08-01 22:53:44 +00:00
தமிழ்நேரம்andHosted Weblate c02e45b703 Translated using Weblate (Tamil)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ta/
2026-08-01 22:53:41 +00:00
தமிழ்நேரம்andHosted Weblate 991dd997ee Translated using Weblate (Tamil)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ta/
2026-08-01 22:53:37 +00:00
GabrielandHosted Weblate daddd5c3c2 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2026-08-01 22:53:34 +00:00
GabrielandHosted Weblate 8b77fae668 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2026-08-01 22:53:33 +00:00
advplyrandHosted Weblate bba15dacf8 Added translation using Weblate (Tamil) 2026-08-01 22:53:32 +00:00
Hans Henrik JuhlandHosted Weblate bfaeb6ea42 Translated using Weblate (Danish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2026-08-01 22:53:31 +00:00
Hans Henrik JuhlandHosted Weblate 5f66f65d6f Translated using Weblate (Danish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2026-08-01 22:53:30 +00:00
Mads Kvist MøllerandHosted Weblate 8b9098c877 Translated using Weblate (Danish)
Currently translated at 99.4% (372 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2026-08-01 22:53:27 +00:00
no1taandHosted Weblate 7044925791 Translated using Weblate (Greek)
Currently translated at 72.1% (270 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/el/
2026-08-01 22:53:23 +00:00
advplyrandHosted Weblate bcc292b33a Added translation using Weblate (Uzbek) 2026-08-01 22:53:22 +00:00
Pavel MiniutkaandHosted Weblate ae08206c3e Translated using Weblate (Belarusian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-08-01 22:53:20 +00:00
EteranlKandHosted Weblate 91315dd9b5 Translated using Weblate (Arabic)
Currently translated at 99.4% (372 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2026-08-01 22:53:18 +00:00
advplyrandHosted Weblate 8a253e7679 Added translation using Weblate (Latvian) 2026-08-01 22:53:17 +00:00
advplyrandGitHub 04e9c8a329 Merge pull request #1869 from AndrolGenhald/reduce-data-usage
Send less data to server with progress update.
2026-08-01 17:53:00 -05:00
advplyr 1d163df773 Update iOS to send partial playback session on local syncs 2026-08-01 17:47:35 -05:00
advplyrandGitHub 4cfa22004c Merge pull request #1861 from lkiesow/landscape-player
Player view in landscape mode
2026-07-31 18:06:31 -04:00
advplyr e0747e3f19 Fix chapter track overflow and switch to percentage width 2026-07-31 17:00:35 -05:00
advplyr b14e7b7357 Merge branch 'master' into landscape-player 2026-07-31 16:42:19 -05:00
advplyr 0123df99d2 Improve logging around progress sync 2026-07-31 16:38:15 -05:00
advplyrandGitHub 4a48cb0a9f Merge pull request #1848 from Kyrluckechuck/fix/seek-to-exact-time-rather-than-floored-second
Seek to millisecond rather than flooring to second
2026-07-31 17:18:55 -04:00
advplyrandGitHub 425ef4c3b6 Merge pull request #1937 from nichwall/download-overhaul-again
Android download rewrite, try 2
2026-07-30 18:36:32 -04:00
advplyr e5ef38295d Update download notification cancel to clear download queue in UI 2026-07-30 17:31:23 -05:00
advplyr b084e54dcc Update download notification id to 11 2026-07-30 17:08:24 -05:00
advplyr d3e55cabf4 Version guard download notification channel API 26+ 2026-07-30 16:51:48 -05:00
advplyr 2cc9ff5a41 Add POST_NOTIFICATION to manifest and start foreground service with type 2026-07-29 17:07:27 -05:00
Andrei Costescu 9562e8f252 Fix ascending bug 2026-07-21 21:05:27 +02:00
Andrei Costescu 7726192008 Align with codebase 2026-07-21 20:53:58 +02:00
Nicholas Wallace f8424c03fd Increase download chunk size to 512 KB 2026-07-20 20:05:18 -07:00
Nicholas Wallace 58fbfca645 Add translation strings for download dialogs and notification 2026-07-20 20:01:02 -07:00
Nicholas Wallace 6282fd1f95 Remove deleted migration call 2026-07-20 19:54:47 -07:00
Nicholas Wallace 245df5d934 Remove unnecessary migration, autoformat 2026-07-20 19:06:35 -07:00
Nicholas Wallace dc6184f41d Remove old and slow simplePath from SAF, also autoformatted 2026-07-20 18:59:19 -07:00
Nicholas Wallace b3c3950c55 Check if completed file exists in shared storage before downloading again 2026-07-19 22:31:32 -07:00
Nicholas Wallace 034a134319 Remove retry stubs 2026-07-19 17:54:46 -07:00
Nicholas Wallace 8142d626c0 Ensure token is not part of download URL 2026-07-19 10:24:12 -07:00
Nicholas Wallace 8b20730b6f Cleaning up retry logic and unused return types 2026-07-19 10:24:01 -07:00
Nicholas Wallace d27f431f6f Comment cleanup 2026-07-19 09:23:31 -07:00
Nicholas Wallace 0df0a206e7 Delete partial files after 24 hours 2026-07-19 09:16:35 -07:00
Nicholas Wallace 6aeb31b591 Add support for background download and resumption 2026-07-18 22:56:12 -07:00
Nicholas Wallace eb2483d039 Initial rewrite of 1587 by codex 2026-07-18 17:32:37 -07:00
Andrei Costescu 67fbeeeb6c Remember sort & filter options for podcast episodes 2026-07-18 18:07:14 +02:00
photown 5099e39f7b Remove the size override in Glide
The override was not needed. It wasn't in the original code, so I'll remove it for consistency.
2026-07-05 23:32:28 -04:00
photown 34710dbddc Render a higher resolution bitmap in the notification background art
Previously only METADATA_KEY_ALBUM_ART_URI/ART_URI were set, leaving notification/lock-screen/Android Auto consumers to resolve the cover art themselves. Likely due to internal scaling, a lower quality bitmap was loaded.

This commit adds a shared resolveUriAsBitmap() helper (extracted from AbMediaDescriptionAdapter.kt) and uses it to:
  - Resolve and cache a bitmap on PlaybackSession once per
  session (synchronously for local covers, asynchronously for server covers), then set it on METADATA_KEY_ALBUM_ART/ART once available, falling back to the URI keys until it resolves.
  - Reuse the same helper in AbMediaDescriptionAdapter so the notification icon is resolved consistently.

  PlayerNotificationService tracks the resolution Job and cancels/replaces it on each new playback session, invalidating the media session metadata once the bitmap is ready.
2026-07-05 23:32:28 -04:00
Julien Cornebise 4798644bea Make AudioTrack.mimeType nullable to prevent Android playback stall
The POST /api/items/<id>/play/<episodeId> response inlines every episode
of a podcast with an audioTrack per episode. Server-side, audioTrack.mimeType
is derived from audioFile.mimeType, which is null when the row was created
without a successful audio probe (direct DB inserts, import scripts,
partial probe failures).

The Jackson deserializer hits one null mimeType among the inlined
episodes, throws MissingKotlinParameterException, and the entire
play-session response fails to deserialize. ExoPlayer is never called,
the UI silently stalls.

Make mimeType nullable on the data class to tolerate this. ExoPlayer's
MediaItem.Builder.setMimeType already accepts nullable. Defensively
coerce null to empty string for Cast's MediaInfo.Builder.setContentType,
whose Java signature is non-null.

Fixes #1905
2026-06-23 15:58:18 +01:00
OguzHAN bd0cbf8bf0 Remove hardcoded search result limit of 5
The search API call had a hardcoded limit=5 parameter which restricted
search results to only 5 items regardless of how many matches exist.
Removing this limit allows the server's default pagination to apply,
showing all relevant search results to the user.

Fixes #1872
2026-05-25 21:12:38 +03:00
AndrolGenhald f562a1a5dc Send less data to server with progress update. 2026-05-13 21:18:05 -05:00
advplyrandGitHub 185cba16eb Merge pull request #1868 from pavel-miniutka/master
Belarusian language option
2026-05-13 16:32:34 -05:00
pavel-miniutka 547d086228 Belarusian language option 2026-05-13 09:11:27 +03:00
advplyr 3a2d5647d4 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2026-05-12 08:22:20 -05:00
advplyr 0a67c9b802 Update gh issue templates for 0.13.0 2026-05-12 08:22:15 -05:00
advplyr 0254afd5d8 iOS version bump 0.13.0 2026-05-10 15:18:30 -05:00
advplyr 853546f3d2 Fix cover image sampling not working when webview strips origin header, instead fetch image from server with CapacitorHttp 2026-05-10 15:15:13 -05:00
advplyr b8ac073247 Version bump 0.13.0-beta 2026-05-10 14:29:52 -05:00
advplyr 56cb4df31a Cleanup unnecessary logs 2026-05-10 14:23:17 -05:00
advplyr 7285b0bf26 iOS update RealmSwift to 10.54.6 2026-05-10 14:12:58 -05:00
advplyrandGitHub 3fdc2e6dcf Merge pull request #1864 from advplyr/fix_open_session_progress_sync
Fix external progress sync for open sessions
2026-05-10 11:24:38 -05:00
advplyr ed2f9d2e45 Prevent focus and socket reconnect from both fetching server media progress 2026-05-10 11:12:17 -05:00
advplyr 68dafbba75 Sync local media open in player when socket reconnects after 30s+ 2026-05-10 11:05:17 -05:00
Lars Kiesow 7d17367c1e Adjust title and author size in landscape mode 2026-05-10 00:55:53 +02:00
advplyr 8be08702c1 Fix regain focus check server progress for local media items open in player & sync more recent 2026-05-09 16:48:55 -05:00
Lars Kiesow 91bc56e1e1 Player view in landscape mode
Rotating your phone to landscape mode and opening the player, the view
wasn't using the available space very efficiently. Especially the cover
image is not very helpful being pushed to a minimal size.

This patch redesigned the view in landscape mode, pushing the cover
image to the left half of the screen while the controls are moved to the
right half of the screen.
2026-05-07 02:05:04 +02:00
advplyrandGitHub 2c2e77f6cd Merge pull request #1824 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2026-05-05 15:55:32 -05:00
LvanAlphenandHosted Weblate 12d02645fd Translated using Weblate (Dutch)
Currently translated at 100.0% (374 of 374 strings)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-12-12 23:12:54 +00:00
advplyr bab95530c8 Update WrappingMarquee to use textContent 2025-12-12 17:12:39 -06:00
Dani Horon 68afba45bc feat: implement download queue on iOS 2025-12-10 17:55:43 +02:00
eagleeyetom 1a52f19b32 Android cast: stabilize MediaSession volume handling; simplify Google Cast volume 2025-11-27 00:32:33 +01:00
advplyr aaef0a7e8d Update github issue templates with 0.11.0 2025-11-25 17:28:10 -06:00
advplyr 02d9023f9d Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2025-11-24 14:52:04 -06:00
advplyr 59a27cdd4d Add Turkish language option #1734 2025-11-24 14:51:59 -06:00
108 changed files with 4982 additions and 2262 deletions
+4 -4
View File
@@ -72,10 +72,10 @@ body:
description: Please ensure your app is up to date. *If you are using a 3rd-party app, please reach out to them directly.* description: Please ensure your app is up to date. *If you are using a 3rd-party app, please reach out to them directly.*
multiple: true multiple: true
options: options:
- 'Android App - 0.10.1' - 'Android App - 0.13.0'
- 'iOS App - 0.10.1' - 'iOS App - 0.13.0'
- 'Android App - 0.10.0' - 'Android App - 0.12.0'
- 'iOS App - 0.10.0' - 'iOS App - 0.12.0'
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
+4 -4
View File
@@ -43,10 +43,10 @@ body:
description: Please ensure your app is up to date. *If you are using a 3rd-party app, please reach out to them directly.* description: Please ensure your app is up to date. *If you are using a 3rd-party app, please reach out to them directly.*
multiple: true multiple: true
options: options:
- 'Android App - 0.10.1' - 'Android App - 0.13.0'
- 'iOS App - 0.10.1' - 'iOS App - 0.13.0'
- 'Android App - 0.10.0' - 'Android App - 0.12.0'
- 'iOS App - 0.10.0' - 'iOS App - 0.12.0'
validations: validations:
required: true required: true
- type: textarea - type: textarea
+3 -2
View File
@@ -36,8 +36,8 @@ android {
applicationId "com.audiobookshelf.app" applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 115 versionCode 118
versionName "0.11.0-beta" versionName "0.14.0-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
manifestPlaceholders = [ manifestPlaceholders = [
"appAuthRedirectScheme": "com.audiobookshelf.app" "appAuthRedirectScheme": "com.audiobookshelf.app"
@@ -92,6 +92,7 @@ dependencies {
implementation project(':capacitor-cordova-android-plugins') implementation project(':capacitor-cordova-android-plugins')
implementation "androidx.core:core-ktx:$androidx_core_ktx_version" implementation "androidx.core:core-ktx:$androidx_core_ktx_version"
implementation "androidx.work:work-runtime-ktx:2.9.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
+8
View File
@@ -6,12 +6,14 @@
<!-- Permissions --> <!-- Permissions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" /> android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:allowBackup="true" android:allowBackup="true"
@@ -98,6 +100,12 @@
</intent-filter> </intent-filter>
</service> </service>
<service
android:name=".services.DownloadService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider" android:authorities="${applicationId}.fileprovider"
@@ -40,9 +40,6 @@ class MainActivity : BridgeActivity() {
val storage = SimpleStorage(this) val storage = SimpleStorage(this)
val REQUEST_PERMISSIONS = 1 val REQUEST_PERMISSIONS = 1
var PERMISSIONS_ALL = arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE
)
public override fun onCreate(savedInstanceState: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
DbManager.initialize(applicationContext) DbManager.initialize(applicationContext)
@@ -98,11 +95,20 @@ class MainActivity : BridgeActivity() {
} }
} }
val permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) requestNeededPermissions()
if (permission != PackageManager.PERMISSION_GRANTED) { }
ActivityCompat.requestPermissions(this,
PERMISSIONS_ALL, private fun requestNeededPermissions() {
REQUEST_PERMISSIONS) val needed = mutableListOf<String>()
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
needed.add(Manifest.permission.READ_EXTERNAL_STORAGE)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
needed.add(Manifest.permission.POST_NOTIFICATIONS)
}
if (needed.isNotEmpty()) {
ActivityCompat.requestPermissions(this, needed.toTypedArray(), REQUEST_PERMISSIONS)
} }
} }
@@ -10,7 +10,7 @@ data class AudioTrack(
var duration: Double, var duration: Double,
var title: String, var title: String,
var contentUrl: String, var contentUrl: String,
var mimeType: String, var mimeType: String?,
var metadata: FileMetadata?, var metadata: FileMetadata?,
var isLocal: Boolean, var isLocal: Boolean,
var localFileId: String?, var localFileId: String?,
@@ -1,74 +1,102 @@
package com.audiobookshelf.app.data package com.audiobookshelf.app.data
import android.content.Context import android.content.Context
import android.net.Uri
import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.MediaDescriptionCompat
import android.util.Log import android.util.Log
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo import com.fasterxml.jackson.annotation.JsonTypeInfo
import java.io.File
enum class LockOrientationSetting { enum class LockOrientationSetting {
NONE, PORTRAIT, LANDSCAPE NONE,
PORTRAIT,
LANDSCAPE
} }
enum class HapticFeedbackSetting { enum class HapticFeedbackSetting {
OFF, LIGHT, MEDIUM, HEAVY OFF,
LIGHT,
MEDIUM,
HEAVY
} }
enum class ShakeSensitivitySetting { enum class ShakeSensitivitySetting {
VERY_LOW, LOW, MEDIUM, HIGH, VERY_HIGH VERY_LOW,
LOW,
MEDIUM,
HIGH,
VERY_HIGH
} }
enum class DownloadUsingCellularSetting { enum class DownloadUsingCellularSetting {
ASK, ALWAYS, NEVER ASK,
ALWAYS,
NEVER
} }
enum class StreamingUsingCellularSetting { enum class StreamingUsingCellularSetting {
ASK, ALWAYS, NEVER ASK,
ALWAYS,
NEVER
} }
enum class AndroidAutoBrowseSeriesSequenceOrderSetting { enum class AndroidAutoBrowseSeriesSequenceOrderSetting {
ASC, DESC ASC,
DESC
} }
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class ServerConnectionConfig( data class ServerConnectionConfig(
var id:String, var id: String,
var index:Int, var index: Int,
var name:String, var name: String,
var address:String, var address: String,
// version added after 0.9.81-beta // version added after 0.9.81-beta
var version:String?, var version: String?,
var userId:String, var userId: String,
var username:String, var username: String,
var token:String, var token: String,
var customHeaders:Map<String, String>? var customHeaders: Map<String, String>?
) )
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class LocalFile( data class LocalFile(
var id:String, var id: String,
var filename:String?, var filename: String?,
var contentUrl:String, var contentUrl: String,
var basePath:String, var basePath: String,
var absolutePath:String, var absolutePath: String,
var simplePath:String, var mimeType: String?,
var mimeType:String?, var size: Long
var size:Long
) { ) {
@JsonIgnore @JsonIgnore
fun isAudioFile():Boolean { fun exists(ctx: Context): Boolean {
if (contentUrl.startsWith("content:")) {
return try {
ctx.contentResolver.openFileDescriptor(Uri.parse(contentUrl), "r")?.use { true } ?: false
} catch (e: Exception) {
Log.w("LocalFile", "Cannot access SAF file $contentUrl", e)
false
}
}
return File(absolutePath).exists()
}
@JsonIgnore
fun isAudioFile(): Boolean {
if (mimeType == "application/octet-stream") return true if (mimeType == "application/octet-stream") return true
if (mimeType == "video/mp4") return true if (mimeType == "video/mp4") return true
return mimeType?.startsWith("audio") == true return mimeType?.startsWith("audio") == true
} }
@JsonIgnore @JsonIgnore
fun isEBookFile():Boolean { fun isEBookFile(): Boolean {
return getEBookFormat() != null return getEBookFormat() != null
} }
@JsonIgnore @JsonIgnore
fun getEBookFormat():String? { fun getEBookFormat(): String? {
if (mimeType == "application/epub+zip") return "epub" if (mimeType == "application/epub+zip") return "epub"
if (mimeType == "application/pdf") return "pdf" if (mimeType == "application/pdf") return "pdf"
if (mimeType == "application/x-mobipocket-ebook") return "mobi" if (mimeType == "application/x-mobipocket-ebook") return "mobi"
@@ -81,118 +109,124 @@ data class LocalFile(
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class LocalFolder( data class LocalFolder(
var id:String, var id: String,
var name:String, var name: String,
var contentUrl:String, var contentUrl: String,
var basePath:String, var basePath: String,
var absolutePath:String, var absolutePath: String,
var simplePath:String, var storageType: String,
var storageType:String, var mediaType: String
var mediaType:String
) )
@JsonTypeInfo(use= JsonTypeInfo.Id.DEDUCTION) @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes( @JsonSubTypes(JsonSubTypes.Type(LibraryItem::class), JsonSubTypes.Type(LocalLibraryItem::class))
JsonSubTypes.Type(LibraryItem::class), open class LibraryItemWrapper(var id: String) {
JsonSubTypes.Type(LocalLibraryItem::class)
)
open class LibraryItemWrapper(var id:String) {
@JsonIgnore @JsonIgnore
open fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat { return MediaDescriptionCompat.Builder().build() } open fun getMediaDescription(
progress: MediaProgressWrapper?,
ctx: Context
): MediaDescriptionCompat {
return MediaDescriptionCompat.Builder().build()
}
} }
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class DeviceInfo( data class DeviceInfo(
var deviceId:String, var deviceId: String,
var manufacturer:String, var manufacturer: String,
var model:String, var model: String,
var sdkVersion:Int, var sdkVersion: Int,
var clientVersion: String var clientVersion: String
) )
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class PlayItemRequestPayload( data class PlayItemRequestPayload(
var mediaPlayer:String, var mediaPlayer: String,
var forceDirectPlay:Boolean, var forceDirectPlay: Boolean,
var forceTranscode:Boolean, var forceTranscode: Boolean,
var deviceInfo:DeviceInfo var deviceInfo: DeviceInfo
) )
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class DeviceSettings( data class DeviceSettings(
var disableAutoRewind:Boolean, var disableAutoRewind: Boolean,
var enableAltView:Boolean, var enableAltView: Boolean,
var allowSeekingOnMediaControls:Boolean, var allowSeekingOnMediaControls: Boolean,
var jumpBackwardsTime:Int, var jumpBackwardsTime: Int,
var jumpForwardTime:Int, var jumpForwardTime: Int,
var enableMp3IndexSeeking:Boolean, var enableMp3IndexSeeking: Boolean,
var disableShakeToResetSleepTimer:Boolean, var disableShakeToResetSleepTimer: Boolean,
var shakeSensitivity: ShakeSensitivitySetting, var shakeSensitivity: ShakeSensitivitySetting,
var lockOrientation: LockOrientationSetting, var lockOrientation: LockOrientationSetting,
var hapticFeedback: HapticFeedbackSetting, var hapticFeedback: HapticFeedbackSetting,
var autoSleepTimer: Boolean, var autoSleepTimer: Boolean,
var autoSleepTimerStartTime: String, var autoSleepTimerStartTime: String,
var autoSleepTimerEndTime: String, var autoSleepTimerEndTime: String,
var autoSleepTimerAutoRewind: Boolean, var autoSleepTimerAutoRewind: Boolean,
var autoSleepTimerAutoRewindTime: Long, //Time in milliseconds var autoSleepTimerAutoRewindTime: Long, // Time in milliseconds
var sleepTimerLength: Long, // Time in milliseconds var sleepTimerLength: Long, // Time in milliseconds
var disableSleepTimerFadeOut: Boolean, var disableSleepTimerFadeOut: Boolean,
var disableSleepTimerResetFeedback: Boolean, var disableSleepTimerResetFeedback: Boolean,
var enableSleepTimerAlmostDoneChime: Boolean, var enableSleepTimerAlmostDoneChime: Boolean,
var languageCode: String, var languageCode: String,
var downloadUsingCellular: DownloadUsingCellularSetting, var downloadUsingCellular: DownloadUsingCellularSetting,
var streamingUsingCellular: StreamingUsingCellularSetting, var streamingUsingCellular: StreamingUsingCellularSetting,
var androidAutoBrowseLimitForGrouping: Int, var androidAutoBrowseLimitForGrouping: Int,
var androidAutoBrowseSeriesSequenceOrder: AndroidAutoBrowseSeriesSequenceOrderSetting var androidAutoBrowseSeriesSequenceOrder: AndroidAutoBrowseSeriesSequenceOrderSetting
) { ) {
companion object { companion object {
// Static method to get default device settings // Static method to get default device settings
fun default():DeviceSettings { fun default(): DeviceSettings {
return DeviceSettings( return DeviceSettings(
disableAutoRewind = false, disableAutoRewind = false,
enableAltView = true, enableAltView = true,
allowSeekingOnMediaControls = false, allowSeekingOnMediaControls = false,
jumpBackwardsTime = 10, jumpBackwardsTime = 10,
jumpForwardTime = 10, jumpForwardTime = 10,
enableMp3IndexSeeking = false, enableMp3IndexSeeking = false,
disableShakeToResetSleepTimer = false, disableShakeToResetSleepTimer = false,
shakeSensitivity = ShakeSensitivitySetting.MEDIUM, shakeSensitivity = ShakeSensitivitySetting.MEDIUM,
lockOrientation = LockOrientationSetting.NONE, lockOrientation = LockOrientationSetting.NONE,
hapticFeedback = HapticFeedbackSetting.LIGHT, hapticFeedback = HapticFeedbackSetting.LIGHT,
autoSleepTimer = false, autoSleepTimer = false,
autoSleepTimerStartTime = "22:00", autoSleepTimerStartTime = "22:00",
autoSleepTimerEndTime = "06:00", autoSleepTimerEndTime = "06:00",
sleepTimerLength = 900000L, // 15 minutes sleepTimerLength = 900000L, // 15 minutes
autoSleepTimerAutoRewind = false, autoSleepTimerAutoRewind = false,
autoSleepTimerAutoRewindTime = 300000L, // 5 minutes autoSleepTimerAutoRewindTime = 300000L, // 5 minutes
disableSleepTimerFadeOut = false, disableSleepTimerFadeOut = false,
disableSleepTimerResetFeedback = false, disableSleepTimerResetFeedback = false,
enableSleepTimerAlmostDoneChime = false, enableSleepTimerAlmostDoneChime = false,
languageCode = "en-us", languageCode = "en-us",
downloadUsingCellular = DownloadUsingCellularSetting.ALWAYS, downloadUsingCellular = DownloadUsingCellularSetting.ALWAYS,
streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS, streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS,
androidAutoBrowseLimitForGrouping = 100, androidAutoBrowseLimitForGrouping = 100,
androidAutoBrowseSeriesSequenceOrder = AndroidAutoBrowseSeriesSequenceOrderSetting.ASC androidAutoBrowseSeriesSequenceOrder = AndroidAutoBrowseSeriesSequenceOrderSetting.ASC
) )
} }
} }
@get:JsonIgnore @get:JsonIgnore
val jumpBackwardsTimeMs get() = jumpBackwardsTime * 1000L val jumpBackwardsTimeMs
get() = jumpBackwardsTime * 1000L
@get:JsonIgnore @get:JsonIgnore
val jumpForwardTimeMs get() = jumpForwardTime * 1000L val jumpForwardTimeMs
get() = jumpForwardTime * 1000L
@get:JsonIgnore @get:JsonIgnore
val autoSleepTimerStartHour get() = autoSleepTimerStartTime.split(":")[0].toInt() val autoSleepTimerStartHour
get() = autoSleepTimerStartTime.split(":")[0].toInt()
@get:JsonIgnore @get:JsonIgnore
val autoSleepTimerStartMinute get() = autoSleepTimerStartTime.split(":")[1].toInt() val autoSleepTimerStartMinute
get() = autoSleepTimerStartTime.split(":")[1].toInt()
@get:JsonIgnore @get:JsonIgnore
val autoSleepTimerEndHour get() = autoSleepTimerEndTime.split(":")[0].toInt() val autoSleepTimerEndHour
get() = autoSleepTimerEndTime.split(":")[0].toInt()
@get:JsonIgnore @get:JsonIgnore
val autoSleepTimerEndMinute get() = autoSleepTimerEndTime.split(":")[1].toInt() val autoSleepTimerEndMinute
get() = autoSleepTimerEndTime.split(":")[1].toInt()
@JsonIgnore @JsonIgnore
fun getShakeThresholdGravity() : Float { // Used in ShakeDetector fun getShakeThresholdGravity(): Float { // Used in ShakeDetector
return if (shakeSensitivity == ShakeSensitivitySetting.VERY_HIGH) 1.1f return if (shakeSensitivity == ShakeSensitivitySetting.VERY_HIGH) 1.1f
else if (shakeSensitivity == ShakeSensitivitySetting.HIGH) 1.3f else if (shakeSensitivity == ShakeSensitivitySetting.HIGH) 1.3f
else if (shakeSensitivity == ShakeSensitivitySetting.MEDIUM) 1.5f else if (shakeSensitivity == ShakeSensitivitySetting.MEDIUM) 1.5f
@@ -206,10 +240,10 @@ data class DeviceSettings(
} }
data class DeviceData( data class DeviceData(
var serverConnectionConfigs:MutableList<ServerConnectionConfig>, var serverConnectionConfigs: MutableList<ServerConnectionConfig>,
var lastServerConnectionConfigId:String?, var lastServerConnectionConfigId: String?,
var deviceSettings: DeviceSettings?, var deviceSettings: DeviceSettings?,
var lastPlaybackSession: PlaybackSession? var lastPlaybackSession: PlaybackSession?
) { ) {
@JsonIgnore @JsonIgnore
fun getLastServerConnectionConfig(): ServerConnectionConfig? { fun getLastServerConnectionConfig(): ServerConnectionConfig? {
@@ -218,4 +252,3 @@ data class DeviceData(
} }
} }
} }
@@ -1,15 +0,0 @@
package com.audiobookshelf.app.data
data class FolderScanResult(
var itemsAdded:Int,
var itemsUpdated:Int,
var itemsRemoved:Int,
var itemsUpToDate:Int,
val localFolder:LocalFolder,
val localLibraryItems:List<LocalLibraryItem>,
)
data class LocalLibraryItemScanResult(
val updated:Boolean,
val localLibraryItem:LocalLibraryItem,
)
@@ -80,7 +80,7 @@ class LocalLibraryItem(
} }
@JsonIgnore @JsonIgnore
fun hasTracks(episode:PodcastEpisode?): Boolean { fun hasTracks(ctx: Context, episode:PodcastEpisode?): Boolean {
var audioTracks = media.getAudioTracks() as MutableList<AudioTrack> var audioTracks = media.getAudioTracks() as MutableList<AudioTrack>
if (episode != null) { // Get podcast episode audio track if (episode != null) { // Get podcast episode audio track
episode.audioTrack?.let { at -> mutableListOf(at) }?.let { tracks -> audioTracks = tracks } episode.audioTrack?.let { at -> mutableListOf(at) }?.let { tracks -> audioTracks = tracks }
@@ -91,15 +91,18 @@ class LocalLibraryItem(
if (it.metadata === null) { if (it.metadata === null) {
return false return false
} }
// Check that file exists if (!trackExists(ctx, it.contentUrl, it.metadata!!.path)) {
val file = File(it.metadata!!.path)
if (!file.exists()) {
return false return false
} }
} }
return true return true
} }
@JsonIgnore
private fun trackExists(ctx: Context, contentUrl: String?, path: String): Boolean {
return LocalFile("", null, contentUrl ?: "", "", path, null, 0).exists(ctx)
}
@JsonIgnore @JsonIgnore
fun getPlaybackSession(episode:PodcastEpisode?, deviceInfo:DeviceInfo):PlaybackSession { fun getPlaybackSession(episode:PodcastEpisode?, deviceInfo:DeviceInfo):PlaybackSession {
val localEpisodeId = episode?.id val localEpisodeId = episode?.id
@@ -108,7 +111,7 @@ class LocalLibraryItem(
// Get current progress for local media // Get current progress for local media
val mediaProgressId = if (localEpisodeId.isNullOrEmpty()) id else "$id-$localEpisodeId" val mediaProgressId = if (localEpisodeId.isNullOrEmpty()) id else "$id-$localEpisodeId"
val mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(mediaProgressId) val mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(mediaProgressId)
val currentTime = mediaProgress?.currentTime ?: 0.0 val currentTime = if (mediaProgress?.isFinished == true) 0.0 else mediaProgress?.currentTime ?: 0.0
val mediaMetadata = media.metadata val mediaMetadata = media.metadata
@@ -14,7 +14,6 @@ data class LocalMediaItem(
var mediaType: String, var mediaType: String,
var folderId: String, var folderId: String,
var contentUrl: String, var contentUrl: String,
var simplePath: String,
var basePath: String, var basePath: String,
var absolutePath: String, var absolutePath: String,
var audioTracks: MutableList<AudioTrack>, var audioTracks: MutableList<AudioTrack>,
@@ -46,6 +46,10 @@ class LocalMediaProgress(
progress = if (finished) 1.0 else 0.0 progress = if (finished) 1.0 else 0.0
} }
if (finished) {
currentTime = 0.0
}
isFinished = finished isFinished = finished
lastUpdate = System.currentTimeMillis() lastUpdate = System.currentTimeMillis()
finishedAt = if (isFinished) lastUpdate else null finishedAt = if (isFinished) lastUpdate else null
@@ -1,6 +1,7 @@
package com.audiobookshelf.app.data package com.audiobookshelf.app.data
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageDecoder import android.graphics.ImageDecoder
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@@ -20,6 +21,9 @@ import com.google.android.exoplayer2.MediaMetadata
import com.google.android.gms.cast.MediaInfo import com.google.android.gms.cast.MediaInfo
import com.google.android.gms.cast.MediaQueueItem import com.google.android.gms.cast.MediaQueueItem
import com.google.android.gms.common.images.WebImage import com.google.android.gms.common.images.WebImage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
class PlaybackSession( class PlaybackSession(
@@ -202,6 +206,13 @@ class PlaybackSession(
return Uri.parse("$serverAddress${audioTrack.contentUrl}?token=${DeviceManager.token}") return Uri.parse("$serverAddress${audioTrack.contentUrl}?token=${DeviceManager.token}")
} }
/** Bitmap for the cover art, once resolved */
@JsonIgnore
private var resolvedCoverBitmap: Bitmap? = null
/**
* Builds the current session metadata, including the cover art bitmap if it has already been resolved.
*/
@JsonIgnore @JsonIgnore
fun getMediaMetadataCompat(ctx: Context): MediaMetadataCompat { fun getMediaMetadataCompat(ctx: Context): MediaMetadataCompat {
val coverUri = getCoverUri(ctx) val coverUri = getCoverUri(ctx)
@@ -217,16 +228,41 @@ class PlaybackSession(
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, displayAuthor) .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, displayAuthor)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, displayAuthor) .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, displayAuthor)
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, coverUri.toString())
.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, coverUri.toString())
.putString( .putString(
MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,
coverUri.toString() coverUri.toString()
) )
// Local covers get bitmap if (resolvedCoverBitmap != null) {
metadataBuilder
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, resolvedCoverBitmap)
.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, resolvedCoverBitmap)
} else {
metadataBuilder
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, coverUri.toString())
.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, coverUri.toString())
}
return metadataBuilder.build()
}
/**
* Resolves the cover art bitmap (local covers are decoded synchronously, server-side covers
* are fetched asynchronously) and calls `onArtResolved` once it's available
*
* Returns the Job for the async fetch, or `null` if the bitmap was resolved synchronously.
*/
@JsonIgnore
fun resolveCoverBitmapAsync(
ctx: Context,
coroutineScope: CoroutineScope,
onArtResolved: () -> Unit
): Job? {
val coverUri = getCoverUri(ctx)
// Local covers get bitmap synchronously, no async fetch needed
if (localLibraryItem?.coverContentUrl != null) { if (localLibraryItem?.coverContentUrl != null) {
val bitmap = resolvedCoverBitmap =
if (Build.VERSION.SDK_INT < 28) { if (Build.VERSION.SDK_INT < 28) {
MediaStore.Images.Media.getBitmap(ctx.contentResolver, coverUri) MediaStore.Images.Media.getBitmap(ctx.contentResolver, coverUri)
} else { } else {
@@ -234,11 +270,18 @@ class PlaybackSession(
ImageDecoder.createSource(ctx.contentResolver, coverUri) ImageDecoder.createSource(ctx.contentResolver, coverUri)
ImageDecoder.decodeBitmap(source) ImageDecoder.decodeBitmap(source)
} }
metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap) onArtResolved()
metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmap) return null
} }
return metadataBuilder.build() // Server-side cover: resolve the art bitmap async
return coroutineScope.launch {
val bitmap = resolveUriAsBitmap(ctx, coverUri)
bitmap?.let {
resolvedCoverBitmap = it
onArtResolved()
}
}
} }
@JsonIgnore @JsonIgnore
@@ -332,7 +375,7 @@ class PlaybackSession(
MediaInfo.Builder(mediaUri.toString()) MediaInfo.Builder(mediaUri.toString())
.apply { .apply {
setContentUrl(mediaUri.toString()) setContentUrl(mediaUri.toString())
setContentType(audioTrack.mimeType) setContentType(audioTrack.mimeType ?: "")
setMetadata(castMetadata) setMetadata(castMetadata)
setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
} }
@@ -7,570 +7,306 @@ import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.file.* import com.anggrayudi.storage.file.*
import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.models.DownloadItem import com.audiobookshelf.app.models.DownloadItem
import com.fasterxml.jackson.core.json.JsonReadFeature import com.audiobookshelf.app.models.DownloadItemPart
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import java.io.File import java.io.File
class FolderScanner(var ctx: Context) { /** Creates local-library records from a completed download manifest. */
class FolderScanner(private val ctx: Context) {
private val tag = "FolderScanner" private val tag = "FolderScanner"
private var jacksonMapper =
jacksonObjectMapper()
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
data class DownloadItemScanResult( data class DownloadItemScanResult(
val localLibraryItem: LocalLibraryItem, val localLibraryItem: LocalLibraryItem,
var localMediaProgress: LocalMediaProgress? var localMediaProgress: LocalMediaProgress?
) )
private fun getLocalLibraryItemId(mediaItemId: String): String { private fun localLibraryItemId(mediaItemId: String) =
return "local_" + DeviceManager.getBase64Id(mediaItemId) "local_${DeviceManager.getBase64Id(mediaItemId)}"
}
private fun scanInternalDownloadItem( private fun createLocalFile(
downloadItem: DownloadItem, part: DownloadItemPart,
cb: (DownloadItemScanResult?) -> Unit externalFile: DocumentFile? = null
) { ): LocalFile? {
val localLibraryItemId = "local_${downloadItem.libraryItemId}" if (part.isInternalStorage) {
val file = File(part.finalDestinationPath)
var localEpisodeId: String? = null if (!file.exists()) return null
var localLibraryItem: LocalLibraryItem? return LocalFile(
if (downloadItem.mediaType == "book") { DeviceManager.getBase64Id(file.name),
localLibraryItem = file.name,
LocalLibraryItem( Uri.fromFile(file).toString(),
localLibraryItemId, file.getBasePath(ctx),
downloadItem.localFolder.id, file.absolutePath,
downloadItem.itemFolderPath, file.mimeType,
downloadItem.itemFolderPath, file.length()
"", )
false,
downloadItem.mediaType,
downloadItem.media.getLocalCopy(),
mutableListOf(),
null,
null,
true,
downloadItem.serverConnectionConfigId,
downloadItem.serverAddress,
downloadItem.serverUserId,
downloadItem.libraryItemId
)
} else {
// Lookup or create podcast local library item
localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
if (localLibraryItem == null) {
Log.d(
tag,
"[FolderScanner] Podcast local library item not created yet for ${downloadItem.media.metadata.title}"
)
localLibraryItem =
LocalLibraryItem(
localLibraryItemId,
downloadItem.localFolder.id,
downloadItem.itemFolderPath,
downloadItem.itemFolderPath,
"",
false,
downloadItem.mediaType,
downloadItem.media.getLocalCopy(),
mutableListOf(),
null,
null,
true,
downloadItem.serverConnectionConfigId,
downloadItem.serverAddress,
downloadItem.serverUserId,
downloadItem.libraryItemId
)
}
} }
val audioTracks: MutableList<AudioTrack> = mutableListOf() part.completedDestinationUri?.let { contentUrl ->
var foundEBookFile = false val uri = Uri.parse(contentUrl)
val size =
downloadItem.downloadItemParts.forEach { downloadItemPart -> try {
Log.d( ctx.contentResolver.openFileDescriptor(uri, "r")?.use { descriptor ->
tag, descriptor.statSize.coerceAtLeast(0L)
"Scan internal storage item with finalDestinationUri=${downloadItemPart.finalDestinationUri}" }
?: 0L
} catch (e: Exception) {
Log.e(tag, "Could not open completed SAF file: $contentUrl", e)
return null
}
// Android 10 DownloadsProvider may not reconstruct a DocumentFile for an audio URI even
// though the URI remains readable. Keep the URI as the authoritative local-file location.
return LocalFile(
DeviceManager.getBase64Id(contentUrl),
part.filename,
contentUrl,
part.localFolderName,
part.finalDestinationPath,
mimeTypeFor(part),
size
) )
}
val file = File(downloadItemPart.finalDestinationPath) // Do not reconstruct a DocumentFile from an absolute path: on Android 10 that becomes a
Log.d(tag, "Scan internal storage item created file ${file.name}") // file:// URI, which DocumentsContract rejects. The caller resolves this from the persisted
// SAF tree grant instead.
val document = externalFile
if (document == null || !document.exists()) {
Log.e(tag, "Could not resolve downloaded SAF file: ${part.finalDestinationPath}")
return null
}
return LocalFile(
DeviceManager.getBase64Id(document.id),
document.name,
document.uri.toString(),
document.getBasePath(ctx),
document.getAbsolutePath(ctx),
document.mimeType,
document.length()
)
}
if (file == null) { private fun newLocalLibraryItem(
Log.e( id: String,
tag, downloadItem: DownloadItem,
"scanInternalDownloadItem: Null docFile for path ${downloadItemPart.finalDestinationPath}" basePath: String,
) absolutePath: String,
} else { contentUrl: String
if (downloadItemPart.audioTrack != null) { ) =
val audioTrackFromServer = downloadItemPart.audioTrack LocalLibraryItem(
Log.d( id,
tag, downloadItem.localFolder.id,
"scanInternalDownloadItem: Audio Track from Server index = ${audioTrackFromServer.index}" basePath,
absolutePath,
contentUrl,
false,
downloadItem.mediaType,
downloadItem.media.getLocalCopy(),
mutableListOf(),
null,
null,
true,
downloadItem.serverConnectionConfigId,
downloadItem.serverAddress,
downloadItem.serverUserId,
downloadItem.libraryItemId
) )
val localFileId = DeviceManager.getBase64Id(file.name) private fun scanParts(
Log.d(tag, "Scan internal file localFileId=$localFileId") item: DownloadItem,
val localFile = localItem: LocalLibraryItem,
LocalFile( externalFolder: DocumentFile? = null,
localFileId, callback: (DownloadItemScanResult?) -> Unit
file.name, ) {
downloadItemPart.finalDestinationUri.toString(), val tracks = mutableListOf<AudioTrack>()
file.getBasePath(ctx), var foundEbook = false
file.absolutePath, var localEpisodeId: String? = null
file.getSimplePath(ctx),
file.mimeType,
file.length()
)
localLibraryItem.localFiles.add(localFile)
val trackFileMetadata = item.downloadItemParts.forEach { part ->
val externalFile =
if (part.isInternalStorage) {
null
} else {
part.completedDestinationUri?.let { DocumentFileCompat.fromUri(ctx, Uri.parse(it)) }
?: resolveExternalFile(externalFolder, part)
}
Log.d(tag, "Resolve part ${part.filename}: externalFile=${externalFile?.uri}")
val localFile = createLocalFile(part, externalFile) ?: return@forEach
when {
part.audioTrack != null -> {
val serverTrack = part.audioTrack
localItem.localFiles.removeAll { it.id == localFile.id }
localItem.localFiles.add(localFile)
val metadata =
FileMetadata( FileMetadata(
file.name, localFile.filename ?: "",
file.extension, File(localFile.filename ?: "").extension,
file.absolutePath, localFile.absolutePath,
file.getBasePath(ctx), localFile.basePath,
file.length() localFile.size
) )
// Create new audio track
val track = val track =
AudioTrack( AudioTrack(
audioTrackFromServer.index, serverTrack.index,
audioTrackFromServer.startOffset, serverTrack.startOffset,
audioTrackFromServer.duration, serverTrack.duration,
localFile.filename ?: "", localFile.filename ?: "",
localFile.contentUrl, localFile.contentUrl,
localFile.mimeType ?: "", localFile.mimeType ?: "",
trackFileMetadata, metadata,
true, true,
localFileId, localFile.id,
audioTrackFromServer.index serverTrack.index
) )
audioTracks.add(track) tracks.add(track)
Log.d(tag, "Added local audio track ${track.contentUrl} (${track.metadata?.path})")
Log.d( part.episode?.let { episode ->
tag, val podcast = localItem.media as Podcast
"scanInternalDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}" localEpisodeId = podcast.addEpisode(track, episode).id
)
// Add podcast episodes to library
downloadItemPart.episode?.let { podcastEpisode ->
val podcast = localLibraryItem.media as Podcast
val newEpisode = podcast.addEpisode(track, podcastEpisode)
localEpisodeId = newEpisode.id
Log.d(
tag,
"scanInternalDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}"
)
} }
} else if (downloadItemPart.ebookFile != null) { }
foundEBookFile = true part.ebookFile != null -> {
Log.d(tag, "scanInternalDownloadItem: Ebook file found with mimetype=${file.mimeType}") foundEbook = true
val localFileId = DeviceManager.getBase64Id(file.name) localItem.localFiles.removeAll { it.id == localFile.id }
val localFile = localItem.localFiles.add(localFile)
LocalFile( (localItem.media as Book).ebookFile =
localFileId,
file.name,
Uri.fromFile(file).toString(),
file.getBasePath(ctx),
file.absolutePath,
file.getSimplePath(ctx),
file.mimeType,
file.length()
)
localLibraryItem.localFiles.add(localFile)
val ebookFile =
EBookFile( EBookFile(
downloadItemPart.ebookFile.ino, part.ebookFile.ino,
downloadItemPart.ebookFile.metadata, part.ebookFile.metadata,
downloadItemPart.ebookFile.ebookFormat, part.ebookFile.ebookFormat,
true, true,
localFileId, localFile.id,
localFile.contentUrl localFile.contentUrl
) )
(localLibraryItem.media as Book).ebookFile = ebookFile }
Log.d(tag, "scanInternalDownloadItem: Ebook file added to lli ${localFile.contentUrl}") else -> {
} else { localItem.coverAbsolutePath = localFile.absolutePath
val localFileId = DeviceManager.getBase64Id(file.name) localItem.coverContentUrl = localFile.contentUrl
val localFile = localItem.localFiles.removeAll { it.id == localFile.id }
LocalFile( localItem.localFiles.add(localFile)
localFileId,
file.name,
Uri.fromFile(file).toString(),
file.getBasePath(ctx),
file.absolutePath,
file.getSimplePath(ctx),
file.mimeType,
file.length()
)
localLibraryItem.coverAbsolutePath = localFile.absolutePath
localLibraryItem.coverContentUrl = localFile.contentUrl
localLibraryItem.localFiles.add(localFile)
} }
} }
} }
if (audioTracks.isEmpty() && !foundEBookFile) { if (tracks.isEmpty() && !foundEbook) {
Log.d( callback(null)
tag, return
"scanDownloadItem did not find any audio tracks or ebook file in folder for ${downloadItem.itemFolderPath}"
)
return cb(null)
} }
if (item.mediaType == "book") {
// For books sort audio tracks then set tracks.sortBy { it.index }
if (downloadItem.mediaType == "book") { var expectedIndex = 1
audioTracks.sortBy { it.index } var offset = 0.0
tracks.forEach { track ->
var indexCheck = 1 track.index = expectedIndex++
var startOffset = 0.0 track.startOffset = offset
audioTracks.forEach { audioTrack -> offset += track.duration
if (audioTrack.index != indexCheck || audioTrack.startOffset != startOffset) {
audioTrack.index = indexCheck
audioTrack.startOffset = startOffset
}
indexCheck++
startOffset += audioTrack.duration
} }
localItem.media.setAudioTracks(tracks)
localLibraryItem.media.setAudioTracks(audioTracks)
} }
val downloadItemScanResult = DownloadItemScanResult(localLibraryItem, null) val result = DownloadItemScanResult(localItem, null)
item.userMediaProgress?.let { progress ->
// If library item had media progress then make local media progress and save val progressId =
downloadItem.userMediaProgress?.let { mediaProgress -> if (item.episodeId.isNullOrEmpty()) localItem.id
val localMediaProgressId = else "${localItem.id}-$localEpisodeId"
if (downloadItem.episodeId.isNullOrEmpty()) localLibraryItemId result.localMediaProgress =
else "$localLibraryItemId-$localEpisodeId"
val newLocalMediaProgress =
LocalMediaProgress( LocalMediaProgress(
id = localMediaProgressId, progressId,
localLibraryItemId = localLibraryItemId, localItem.id,
localEpisodeId = localEpisodeId, localEpisodeId,
duration = mediaProgress.duration, progress.duration,
progress = mediaProgress.progress, progress.progress,
currentTime = mediaProgress.currentTime, progress.currentTime,
isFinished = mediaProgress.isFinished, progress.isFinished,
ebookLocation = mediaProgress.ebookLocation, progress.ebookLocation,
ebookProgress = mediaProgress.ebookProgress, progress.ebookProgress,
lastUpdate = mediaProgress.lastUpdate, progress.lastUpdate,
startedAt = mediaProgress.startedAt, progress.startedAt,
finishedAt = mediaProgress.finishedAt, progress.finishedAt,
serverConnectionConfigId = downloadItem.serverConnectionConfigId, item.serverConnectionConfigId,
serverAddress = downloadItem.serverAddress, item.serverAddress,
serverUserId = downloadItem.serverUserId, item.serverUserId,
libraryItemId = downloadItem.libraryItemId, item.libraryItemId,
episodeId = downloadItem.episodeId item.episodeId
) )
Log.d( DeviceManager.dbManager.saveLocalMediaProgress(result.localMediaProgress!!)
tag,
"scanLibraryItemFolder: Saving local media progress ${newLocalMediaProgress.id} at progress ${newLocalMediaProgress.progress}"
)
DeviceManager.dbManager.saveLocalMediaProgress(newLocalMediaProgress)
downloadItemScanResult.localMediaProgress = newLocalMediaProgress
} }
DeviceManager.dbManager.saveLocalLibraryItem(localItem)
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem) callback(result)
cb(downloadItemScanResult)
} }
// Scan item after download and create local library item private fun findFolderByPath(root: DocumentFile, subPath: String): DocumentFile? {
fun scanDownloadItem(downloadItem: DownloadItem, cb: (DownloadItemScanResult?) -> Unit) { if (subPath.isBlank()) return root
// If downloading to internal storage handle separately var current = root
if (downloadItem.isInternalStorage) { subPath.split('/').filter { it.isNotBlank() }.forEach { segment ->
scanInternalDownloadItem(downloadItem, cb) if (segment == "." || segment == "..") return null
current = current.findFile(segment) ?: return null
}
return current
}
/**
* DownloadsProvider on Android 10 may expose an audio document without its extension through
* DocumentFile.findFile(). Match the manifest first, then match the provider-normalized base
* filename. MIME type and server-reported size are unreliable for Opus on this platform.
*/
private fun resolveExternalFile(folder: DocumentFile?, part: DownloadItemPart): DocumentFile? {
if (folder == null) return null
folder.findFile(part.filename)?.let {
return it
}
val expectedBaseName = part.filename.substringBeforeLast('.')
return folder.listFiles().firstOrNull { document ->
document.name == part.filename ||
document.fullName == part.filename ||
(part.audioTrack != null &&
document.isFile &&
(document.name ?: "").substringBeforeLast('.') == expectedBaseName) ||
(part.audioTrack != null &&
document.isFile &&
document.fullName.substringBeforeLast('.') == expectedBaseName)
}
}
private fun mimeTypeFor(part: DownloadItemPart): String? {
return part.audioTrack?.mimeType
?: when (part.ebookFile?.ebookFormat?.lowercase()) {
"epub" -> "application/epub+zip"
"pdf" -> "application/pdf"
else -> "image/jpeg"
}
}
fun scanDownloadItem(item: DownloadItem, callback: (DownloadItemScanResult?) -> Unit) {
if (item.isInternalStorage) {
val id = "local_${item.libraryItemId}"
val localItem =
DeviceManager.dbManager.getLocalLibraryItem(id)
?: newLocalLibraryItem(id, item, item.itemFolderPath, item.itemFolderPath, "")
scanParts(item, localItem, callback = callback)
return return
} }
val folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl)) val root = DocumentFileCompat.fromUri(ctx, Uri.parse(item.localFolder.contentUrl))
val foldersFound = folderDf?.search(true, DocumentFileType.FOLDER) ?: mutableListOf() if (root == null) {
Log.e(tag, "Invalid SAF root: ${item.localFolder.contentUrl}")
var itemFolderId = "" callback(null)
var itemFolderUrl = "" return
var itemFolderBasePath = ""
var itemFolderAbsolutePath = ""
foldersFound.forEach {
// e.g. absolute path is "storage/emulated/0/Audiobooks/Orson Scott Card/Enders Game"
// and itemSubfolder is "Orson Scott Card/Enders Game"
if (it.getAbsolutePath(ctx).endsWith(downloadItem.itemSubfolder)) {
itemFolderId = it.id
itemFolderUrl = it.uri.toString()
itemFolderBasePath = it.getBasePath(ctx)
itemFolderAbsolutePath = it.getAbsolutePath(ctx)
}
} }
val itemFolder = findFolderByPath(root, item.itemSubfolder)
if (itemFolderUrl == "") { if (itemFolder == null) {
Log.d(tag, "scanDownloadItem failed to find media folder") Log.e(tag, "SAF item folder not found: ${item.itemSubfolder}")
return cb(null) callback(null)
return
} }
val df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(itemFolderUrl)) val id = localLibraryItemId(itemFolder.id)
val localItem =
if (df == null) { DeviceManager.dbManager.getLocalLibraryItem(id)
Log.e(tag, "Folder Doc File Invalid ${downloadItem.itemFolderPath}") ?: newLocalLibraryItem(
return cb(null) id,
} item,
itemFolder.getBasePath(ctx),
val localLibraryItemId = getLocalLibraryItemId(itemFolderId) itemFolder.getAbsolutePath(ctx),
Log.d( itemFolder.uri.toString()
tag, )
"scanDownloadItem starting for ${downloadItem.itemFolderPath} | ${df.uri} | Item Folder Id:$itemFolderId | LLI Id:$localLibraryItemId" scanParts(item, localItem, itemFolder, callback)
)
// Search for files in media item folder
// m4b files showing as mimeType application/octet-stream on Android 10 and earlier see #154
val filesFound =
df.search(
false,
DocumentFileType.FILE,
arrayOf("audio/*", "image/*", "video/mp4", "application/*")
)
Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}")
var localEpisodeId: String? = null
var localLibraryItem: LocalLibraryItem?
if (downloadItem.mediaType == "book") {
localLibraryItem =
LocalLibraryItem(
localLibraryItemId,
downloadItem.localFolder.id,
itemFolderBasePath,
itemFolderAbsolutePath,
itemFolderUrl,
false,
downloadItem.mediaType,
downloadItem.media.getLocalCopy(),
mutableListOf(),
null,
null,
true,
downloadItem.serverConnectionConfigId,
downloadItem.serverAddress,
downloadItem.serverUserId,
downloadItem.libraryItemId
)
} else {
// Lookup or create podcast local library item
localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
if (localLibraryItem == null) {
Log.d(
tag,
"[FolderScanner] Podcast local library item not created yet for ${downloadItem.media.metadata.title}"
)
localLibraryItem =
LocalLibraryItem(
localLibraryItemId,
downloadItem.localFolder.id,
itemFolderBasePath,
itemFolderAbsolutePath,
itemFolderUrl,
false,
downloadItem.mediaType,
downloadItem.media.getLocalCopy(),
mutableListOf(),
null,
null,
true,
downloadItem.serverConnectionConfigId,
downloadItem.serverAddress,
downloadItem.serverUserId,
downloadItem.libraryItemId
)
}
}
val audioTracks: MutableList<AudioTrack> = mutableListOf()
var foundEBookFile = false
filesFound.forEach { docFile ->
val itemPart =
downloadItem.downloadItemParts.find { itemPart -> itemPart.filename == docFile.name }
if (itemPart == null) {
if (downloadItem.mediaType == "book"
) { // for books every download item should be a file found
Log.e(
tag,
"scanDownloadItem: Item part not found for doc file ${docFile.name} | ${docFile.getAbsolutePath(ctx)} | ${docFile.uri}"
)
}
} else if (itemPart.audioTrack != null) { // Is audio track
val audioTrackFromServer = itemPart.audioTrack
Log.d(
tag,
"scanDownloadItem: Audio Track from Server index = ${audioTrackFromServer.index}"
)
val localFileId = DeviceManager.getBase64Id(docFile.id)
val localFile =
LocalFile(
localFileId,
docFile.name,
docFile.uri.toString(),
docFile.getBasePath(ctx),
docFile.getAbsolutePath(ctx),
docFile.getSimplePath(ctx),
docFile.mimeType,
docFile.length()
)
localLibraryItem.localFiles.add(localFile)
// Create new audio track
val trackFileMetadata =
FileMetadata(
docFile.name ?: "",
docFile.extension ?: "",
docFile.getAbsolutePath(ctx),
docFile.getBasePath(ctx),
docFile.length()
)
val track =
AudioTrack(
audioTrackFromServer.index,
audioTrackFromServer.startOffset,
audioTrackFromServer.duration,
localFile.filename ?: "",
localFile.contentUrl,
localFile.mimeType ?: "",
trackFileMetadata,
true,
localFileId,
audioTrackFromServer.index
)
audioTracks.add(track)
Log.d(
tag,
"scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}"
)
// Add podcast episodes to library
itemPart.episode?.let { podcastEpisode ->
val podcast = localLibraryItem.media as Podcast
val newEpisode = podcast.addEpisode(track, podcastEpisode)
localEpisodeId = newEpisode.id
Log.d(
tag,
"scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}"
)
}
} else if (itemPart.ebookFile != null) { // Ebook
foundEBookFile = true
Log.d(tag, "scanDownloadItem: Ebook file found with mimetype=${docFile.mimeType}")
val localFileId = DeviceManager.getBase64Id(docFile.id)
val localFile =
LocalFile(
localFileId,
docFile.name,
docFile.uri.toString(),
docFile.getBasePath(ctx),
docFile.getAbsolutePath(ctx),
docFile.getSimplePath(ctx),
docFile.mimeType,
docFile.length()
)
localLibraryItem.localFiles.add(localFile)
val ebookFile =
EBookFile(
itemPart.ebookFile.ino,
itemPart.ebookFile.metadata,
itemPart.ebookFile.ebookFormat,
true,
localFileId,
localFile.contentUrl
)
(localLibraryItem.media as Book).ebookFile = ebookFile
Log.d(tag, "scanDownloadItem: Ebook file added to lli ${localFile.contentUrl}")
} else { // Cover image
val localFileId = DeviceManager.getBase64Id(docFile.id)
val localFile =
LocalFile(
localFileId,
docFile.name,
docFile.uri.toString(),
docFile.getBasePath(ctx),
docFile.getAbsolutePath(ctx),
docFile.getSimplePath(ctx),
docFile.mimeType,
docFile.length()
)
localLibraryItem.coverAbsolutePath = localFile.absolutePath
localLibraryItem.coverContentUrl = localFile.contentUrl
localLibraryItem.localFiles.add(localFile)
}
}
if (audioTracks.isEmpty() && !foundEBookFile) {
Log.d(
tag,
"scanDownloadItem did not find any audio tracks or ebook file in folder for ${downloadItem.itemFolderPath}"
)
return cb(null)
}
// For books sort audio tracks then set
if (downloadItem.mediaType == "book") {
audioTracks.sortBy { it.index }
var indexCheck = 1
var startOffset = 0.0
audioTracks.forEach { audioTrack ->
if (audioTrack.index != indexCheck || audioTrack.startOffset != startOffset) {
audioTrack.index = indexCheck
audioTrack.startOffset = startOffset
}
indexCheck++
startOffset += audioTrack.duration
}
localLibraryItem.media.setAudioTracks(audioTracks)
}
val downloadItemScanResult = DownloadItemScanResult(localLibraryItem, null)
// If library item had media progress then make local media progress and save
downloadItem.userMediaProgress?.let { mediaProgress ->
val localMediaProgressId =
if (downloadItem.episodeId.isNullOrEmpty()) localLibraryItemId
else "$localLibraryItemId-$localEpisodeId"
val newLocalMediaProgress =
LocalMediaProgress(
id = localMediaProgressId,
localLibraryItemId = localLibraryItemId,
localEpisodeId = localEpisodeId,
duration = mediaProgress.duration,
progress = mediaProgress.progress,
currentTime = mediaProgress.currentTime,
isFinished = mediaProgress.isFinished,
ebookLocation = mediaProgress.ebookLocation,
ebookProgress = mediaProgress.ebookProgress,
lastUpdate = mediaProgress.lastUpdate,
startedAt = mediaProgress.startedAt,
finishedAt = mediaProgress.finishedAt,
serverConnectionConfigId = downloadItem.serverConnectionConfigId,
serverAddress = downloadItem.serverAddress,
serverUserId = downloadItem.serverUserId,
libraryItemId = downloadItem.libraryItemId,
episodeId = downloadItem.episodeId
)
Log.d(
tag,
"scanLibraryItemFolder: Saving local media progress ${newLocalMediaProgress.id} at progress ${newLocalMediaProgress.progress}"
)
DeviceManager.dbManager.saveLocalMediaProgress(newLocalMediaProgress)
downloadItemScanResult.localMediaProgress = newLocalMediaProgress
}
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
cb(downloadItemScanResult)
} }
} }
@@ -7,7 +7,6 @@ import com.audiobookshelf.app.models.DownloadItem
import com.audiobookshelf.app.plugins.AbsLog import com.audiobookshelf.app.plugins.AbsLog
import com.audiobookshelf.app.plugins.AbsLogger import com.audiobookshelf.app.plugins.AbsLogger
import io.paperdb.Paper import io.paperdb.Paper
import java.io.File
class DbManager { class DbManager {
val tag = "DbManager" val tag = "DbManager"
@@ -148,7 +147,7 @@ class DbManager {
} }
// Make sure all local file ids still exist // Make sure all local file ids still exist
fun cleanLocalLibraryItems() { fun cleanLocalLibraryItems(context: Context) {
val localLibraryItems = getLocalLibraryItems() val localLibraryItems = getLocalLibraryItems()
localLibraryItems.forEach { lli -> localLibraryItems.forEach { lli ->
@@ -157,15 +156,15 @@ class DbManager {
// Check local files // Check local files
lli.localFiles = lli.localFiles =
lli.localFiles.filter { localFile -> lli.localFiles.filter { localFile ->
val file = File(localFile.absolutePath) val exists = localFile.exists(context)
if (!file.exists()) { if (!exists) {
Log.d( Log.d(
tag, tag,
"cleanLocalLibraryItems: Local file ${localFile.absolutePath} was removed from library item ${lli.media.metadata.title}" "cleanLocalLibraryItems: Local file ${localFile.absolutePath} was removed from library item ${lli.media.metadata.title}"
) )
hasUpdates = true hasUpdates = true
} }
file.exists() exists
} as } as
MutableList<LocalFile> MutableList<LocalFile>
@@ -203,9 +202,11 @@ class DbManager {
// Check cover still there // Check cover still there
lli.coverAbsolutePath?.let { lli.coverAbsolutePath?.let {
val coverFile = File(it) val coverExists =
lli.localFiles.any { localFile ->
if (!coverFile.exists()) { localFile.absolutePath == it && localFile.exists(context)
}
if (!coverExists) {
Log.d( Log.d(
tag, tag,
"cleanLocalLibraryItems: Cover $it was removed from library item ${lli.media.metadata.title}" "cleanLocalLibraryItems: Cover $it was removed from library item ${lli.media.metadata.title}"
@@ -290,15 +291,13 @@ class DbManager {
return sessions return sessions
} }
fun saveLog(log:AbsLog) { fun saveLog(log: AbsLog) {
Paper.book("log").write(log.id, log) Paper.book("log").write(log.id, log)
} }
fun getAllLogs() : List<AbsLog> { fun getAllLogs(): List<AbsLog> {
val logs:MutableList<AbsLog> = mutableListOf() val logs: MutableList<AbsLog> = mutableListOf()
Paper.book("log").allKeys.forEach { logId -> Paper.book("log").allKeys.forEach { logId ->
Paper.book("log").read<AbsLog>(logId)?.let { Paper.book("log").read<AbsLog>(logId)?.let { logs.add(it) }
logs.add(it)
}
} }
return logs.sortedBy { it.timestamp } return logs.sortedBy { it.timestamp }
} }
@@ -317,7 +316,10 @@ class DbManager {
} }
} }
if (logsRemoved > 0) { if (logsRemoved > 0) {
AbsLogger.info("DbManager", "cleanLogs: Removed $logsRemoved logs older than $numberOfHoursToKeep hours") AbsLogger.info(
"DbManager",
"cleanLogs: Removed $logsRemoved logs older than $numberOfHoursToKeep hours"
)
} }
} }
} }
@@ -1,16 +1,10 @@
package com.audiobookshelf.app.managers package com.audiobookshelf.app.managers
import android.app.DownloadManager import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.StatFs
import android.util.Log import android.util.Log
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.callback.FileCallback
import com.anggrayudi.storage.file.DocumentFileCompat
import com.anggrayudi.storage.file.MimeType
import com.anggrayudi.storage.file.getAbsolutePath
import com.anggrayudi.storage.file.moveFileTo
import com.anggrayudi.storage.media.FileDescription
import com.audiobookshelf.app.MainActivity
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.device.FolderScanner import com.audiobookshelf.app.device.FolderScanner
import com.audiobookshelf.app.models.DownloadItem import com.audiobookshelf.app.models.DownloadItem
@@ -19,41 +13,44 @@ import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.JSObject import com.getcapacitor.JSObject
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileInputStream
import java.util.* import java.util.concurrent.ConcurrentHashMap
import kotlin.math.max
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.Call
/** Manages download items and their parts. */ /** Manages the process-owned queue for app-managed downloads. */
class DownloadItemManager( class DownloadItemManager(
var downloadManager: DownloadManager, private val folderScanner: FolderScanner,
private var folderScanner: FolderScanner, private val context: Context,
var mainActivity: MainActivity,
private var clientEventEmitter: DownloadEventEmitter private var clientEventEmitter: DownloadEventEmitter
) { ) {
val tag = "DownloadItemManager" private val tag = "DownloadItemManager"
private val maxSimultaneousDownloads = 3 private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var jacksonMapper = private val activeCalls = ConcurrentHashMap<String, Call>()
private val safFolderLocks = ConcurrentHashMap<String, Any>()
private val reservations = mutableMapOf<String, Long>()
private val lastPersistTime = mutableMapOf<String, Long>()
private var watcherRunning = false
private val jacksonMapper =
jacksonObjectMapper() jacksonObjectMapper()
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) .enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
enum class DownloadCheckStatus { var downloadItemQueue: MutableList<DownloadItem> = mutableListOf()
InProgress, private set
Successful, var currentDownloadItemParts: MutableList<DownloadItemPart> = mutableListOf()
Failed private set
}
var downloadItemQueue: MutableList<DownloadItem> =
mutableListOf() // All pending and downloading items
var currentDownloadItemParts: MutableList<DownloadItemPart> =
mutableListOf() // Item parts currently being downloaded
interface DownloadEventEmitter { interface DownloadEventEmitter {
fun onDownloadItem(downloadItem: DownloadItem) fun onDownloadItem(downloadItem: DownloadItem)
fun onDownloadItemPartUpdate(downloadItemPart: DownloadItemPart) fun onDownloadItemPartUpdate(downloadItemPart: DownloadItemPart)
fun onDownloadItemComplete(jsobj: JSObject) fun onDownloadItemComplete(jsobj: JSObject)
fun onQueueChanged(hasWork: Boolean)
} }
interface InternalProgressCallback { interface InternalProgressCallback {
@@ -61,323 +58,455 @@ class DownloadItemManager(
fun onComplete(failed: Boolean) fun onComplete(failed: Boolean)
} }
companion object { init {
var isDownloading: Boolean = false IncompleteDownloadCleanup.cleanupExpired(context)
} }
/** Adds a download item to the queue and starts processing the queue. */ @Synchronized
fun addDownloadItem(downloadItem: DownloadItem) { fun setEventEmitter(eventEmitter: DownloadEventEmitter) {
DeviceManager.dbManager.saveDownloadItem(downloadItem) clientEventEmitter = eventEmitter
Log.i(tag, "Add download item ${downloadItem.media.metadata.title}") downloadItemQueue.forEach(clientEventEmitter::onDownloadItem)
notifyQueueChanged()
}
@Synchronized
fun restoreQueue() {
if (downloadItemQueue.isNotEmpty()) return
DeviceManager.dbManager.getDownloadItems().forEach { item ->
if (item.isDownloadFinished) {
downloadItemQueue.add(item)
checkDownloadItemFinished(item)
return@forEach
}
item.downloadItemParts.forEach { part ->
if (part.moved) return@forEach
if (item.terminalFailureAt != null && part.failed) return@forEach
part.downloadId = null
part.isMoving = false
part.failed = false
part.completed = false
part.waitingForSpace = false
part.bytesDownloaded = File(part.destinationPath).takeIf(File::exists)?.length() ?: 0L
}
downloadItemQueue.add(item)
if (item.terminalFailureAt != null) IncompleteDownloadCleanup.schedule(context, item)
clientEventEmitter.onDownloadItem(item)
}
checkUpdateDownloadQueue()
notifyQueueChanged()
}
@Synchronized
fun addDownloadItem(downloadItem: DownloadItem) {
val existingItem = downloadItemQueue.find { it.id == downloadItem.id }
if (existingItem != null) {
if (existingItem.terminalFailureAt != null) {
retryDownloadItem(existingItem)
checkUpdateDownloadQueue()
notifyQueueChanged()
}
return
}
persist(downloadItem, force = true)
downloadItemQueue.add(downloadItem) downloadItemQueue.add(downloadItem)
clientEventEmitter.onDownloadItem(downloadItem) clientEventEmitter.onDownloadItem(downloadItem)
checkUpdateDownloadQueue() checkUpdateDownloadQueue()
notifyQueueChanged()
} }
/** Checks and updates the download queue. */ private fun retryDownloadItem(item: DownloadItem) {
item.terminalFailureAt = null
IncompleteDownloadCleanup.cancel(context, item.id)
item.downloadItemParts.filter { it.failed }.forEach { part ->
part.failed = false
part.completed = false
part.isMoving = false
part.downloadId = null
part.retryCount = 0
}
persist(item, force = true)
}
@Synchronized
fun cancelAll() {
activeCalls.values.forEach(Call::cancel)
activeCalls.clear()
downloadItemQueue.forEach { item ->
item.downloadItemParts.forEach { part -> File(part.destinationPath).delete() }
DeviceManager.dbManager.removeDownloadItem(item.id)
}
currentDownloadItemParts.clear()
reservations.clear()
downloadItemQueue.clear()
notifyQueueChanged()
}
@Synchronized
fun hasWork(): Boolean =
downloadItemQueue.any { item ->
item.downloadItemParts.any { part ->
(!part.completed && !part.failed) || part.isMoving
}
}
@Synchronized
private fun checkUpdateDownloadQueue() { private fun checkUpdateDownloadQueue() {
for (downloadItem in downloadItemQueue) { downloadItemQueue.toList().forEach { item ->
val numPartsToGet = maxSimultaneousDownloads - currentDownloadItemParts.size val slots = MAX_SIMULTANEOUS_DOWNLOADS - currentDownloadItemParts.size
val nextDownloadItemParts = downloadItem.getNextDownloadItemParts(numPartsToGet) if (slots <= 0) return@forEach
Log.d( item.getNextDownloadItemParts(slots).forEach { part ->
tag, val existingFile = findSharedStorageFile(part)
"checkUpdateDownloadQueue: numPartsToGet=$numPartsToGet, nextDownloadItemParts=${nextDownloadItemParts.size}" if (existingFile != null) {
) part.bytesDownloaded = existingFile.length()
part.progress = 100L
if (nextDownloadItemParts.isNotEmpty()) { part.completedDestinationUri = existingFile.uri.toString()
processDownloadItemParts(nextDownloadItemParts) File(part.destinationPath).delete()
} completePart(item, part)
clientEventEmitter.onDownloadItemPartUpdate(part)
if (currentDownloadItemParts.size >= maxSimultaneousDownloads) { } else if (tryReserve(part)) startDownload(item, part)
break else {
part.waitingForSpace = true
part.lastUpdateTime = System.currentTimeMillis()
persist(item)
clientEventEmitter.onDownloadItemPartUpdate(part)
}
} }
} }
if (hasWork()) startWatchingDownloads() else notifyQueueChanged()
if (currentDownloadItemParts.isNotEmpty()) startWatchingDownloads()
} }
/** Processes the download item parts. */ private fun startDownload(item: DownloadItem, part: DownloadItemPart) {
private fun processDownloadItemParts(nextDownloadItemParts: List<DownloadItemPart>) { val stagingFile = File(part.destinationPath)
nextDownloadItemParts.forEach { stagingFile.parentFile?.mkdirs()
if (it.isInternalStorage) { part.downloadId = APP_MANAGED_DOWNLOAD_ID
startInternalDownload(it) part.waitingForSpace = false
} else { part.lastUpdateTime = System.currentTimeMillis()
startExternalDownload(it) currentDownloadItemParts.add(part)
} persist(item, force = true)
} val activeConfig = DeviceManager.serverConnectionConfig
val token =
if (activeConfig?.id == item.serverConnectionConfigId) activeConfig.token
else
DeviceManager.getServerConnectionConfig(item.serverConnectionConfigId)?.token
?: DeviceManager.token
activeCalls[part.id] =
InternalDownloadManager(
stagingFile,
part.fileSize,
object : InternalProgressCallback {
override fun onProgress(totalBytesWritten: Long, progress: Long) {
synchronized(this@DownloadItemManager) {
if (part !in currentDownloadItemParts) return
part.bytesDownloaded = totalBytesWritten
part.progress = progress
part.lastUpdateTime = System.currentTimeMillis()
persist(item)
}
}
override fun onComplete(failed: Boolean) {
synchronized(this@DownloadItemManager) {
if (part !in currentDownloadItemParts) return
part.failed = failed
part.completed = !failed
part.lastUpdateTime = System.currentTimeMillis()
activeCalls.remove(part.id)
persist(item, force = true)
}
}
},
{ hasAvailableSpace(part) }
)
.download(serverUrl(item, part), token)
} }
/** Starts an internal download. */ @Synchronized
private fun startInternalDownload(downloadItemPart: DownloadItemPart) {
val file = File(downloadItemPart.finalDestinationPath)
file.parentFile?.mkdirs()
val fileOutputStream = FileOutputStream(downloadItemPart.finalDestinationPath)
val internalProgressCallback =
object : InternalProgressCallback {
override fun onProgress(totalBytesWritten: Long, progress: Long) {
downloadItemPart.bytesDownloaded = totalBytesWritten
downloadItemPart.progress = progress
}
override fun onComplete(failed: Boolean) {
downloadItemPart.failed = failed
downloadItemPart.completed = true
}
}
Log.d(
tag,
"Start internal download to destination path ${downloadItemPart.finalDestinationPath} from ${downloadItemPart.serverUrl}"
)
InternalDownloadManager(fileOutputStream, internalProgressCallback)
.download(downloadItemPart.serverUrl)
downloadItemPart.downloadId = 1
currentDownloadItemParts.add(downloadItemPart)
}
/** Starts an external download. */
private fun startExternalDownload(downloadItemPart: DownloadItemPart) {
val dlRequest = downloadItemPart.getDownloadRequest()
val downloadId = downloadManager.enqueue(dlRequest)
downloadItemPart.downloadId = downloadId
Log.d(tag, "checkUpdateDownloadQueue: Starting download item part, downloadId=$downloadId")
currentDownloadItemParts.add(downloadItemPart)
}
/** Starts watching the downloads. */
private fun startWatchingDownloads() { private fun startWatchingDownloads() {
if (isDownloading) return // Already watching if (watcherRunning) return
watcherRunning = true
GlobalScope.launch(Dispatchers.IO) { scope.launch {
Log.d(tag, "Starting watching downloads") while (true) {
isDownloading = true val activeParts =
synchronized(this@DownloadItemManager) { currentDownloadItemParts.toList() }
while (currentDownloadItemParts.isNotEmpty()) { activeParts.forEach(::handlePartUpdate)
val itemParts = currentDownloadItemParts.filter { !it.isMoving } synchronized(this@DownloadItemManager) {
for (downloadItemPart in itemParts) {
if (downloadItemPart.isInternalStorage) {
handleInternalDownloadPart(downloadItemPart)
} else {
handleExternalDownloadPart(downloadItemPart)
}
}
delay(500)
if (currentDownloadItemParts.size < maxSimultaneousDownloads) {
checkUpdateDownloadQueue() checkUpdateDownloadQueue()
} if (!hasWork()) {
} watcherRunning = false
notifyQueueChanged()
Log.d(tag, "Finished watching downloads") return@launch
isDownloading = false
}
}
/** Handles an internal download part. */
private fun handleInternalDownloadPart(downloadItemPart: DownloadItemPart) {
clientEventEmitter.onDownloadItemPartUpdate(downloadItemPart)
if (downloadItemPart.completed) {
val downloadItem = downloadItemQueue.find { it.id == downloadItemPart.downloadItemId }
downloadItem?.let { checkDownloadItemFinished(it) }
currentDownloadItemParts.remove(downloadItemPart)
}
}
/** Handles an external download part. */
private fun handleExternalDownloadPart(downloadItemPart: DownloadItemPart) {
val downloadCheckStatus = checkDownloadItemPart(downloadItemPart)
clientEventEmitter.onDownloadItemPartUpdate(downloadItemPart)
// Will move to final destination, remove current item parts, and check if download item is
// finished
handleDownloadItemPartCheck(downloadCheckStatus, downloadItemPart)
}
/** Checks the status of a download item part. */
private fun checkDownloadItemPart(downloadItemPart: DownloadItemPart): DownloadCheckStatus {
val downloadId = downloadItemPart.downloadId ?: return DownloadCheckStatus.Failed
val query = DownloadManager.Query().setFilterById(downloadId)
downloadManager.query(query).use {
if (it.moveToFirst()) {
val bytesColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
val statusColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_STATUS)
val bytesDownloadedColumnIndex =
it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
val totalBytes = if (bytesColumnIndex >= 0) it.getInt(bytesColumnIndex) else 0
val downloadStatus = if (statusColumnIndex >= 0) it.getInt(statusColumnIndex) else 0
val bytesDownloadedSoFar =
if (bytesDownloadedColumnIndex >= 0) it.getLong(bytesDownloadedColumnIndex) else 0
Log.d(
tag,
"checkDownloads Download ${downloadItemPart.filename} bytes $totalBytes | bytes dled $bytesDownloadedSoFar | downloadStatus $downloadStatus"
)
return when (downloadStatus) {
DownloadManager.STATUS_SUCCESSFUL -> {
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Successful")
downloadItemPart.completed = true
downloadItemPart.progress = 1
downloadItemPart.bytesDownloaded = bytesDownloadedSoFar
DownloadCheckStatus.Successful
}
DownloadManager.STATUS_FAILED -> {
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Failed")
downloadItemPart.completed = true
downloadItemPart.failed = true
DownloadCheckStatus.Failed
}
else -> {
val percentProgress =
if (totalBytes > 0) ((bytesDownloadedSoFar * 100L) / totalBytes) else 0
Log.d(
tag,
"checkDownloads Download ${downloadItemPart.filename} Progress = $percentProgress%"
)
downloadItemPart.progress = percentProgress
downloadItemPart.bytesDownloaded = bytesDownloadedSoFar
DownloadCheckStatus.InProgress
} }
} }
} else { delay(WATCH_INTERVAL_MS)
Log.d(tag, "Download ${downloadItemPart.filename} not found in dlmanager")
downloadItemPart.completed = true
downloadItemPart.failed = true
return DownloadCheckStatus.Failed
} }
} }
} }
/** Handles the result of a download item part check. */ private fun handlePartUpdate(part: DownloadItemPart) {
private fun handleDownloadItemPartCheck( clientEventEmitter.onDownloadItemPartUpdate(part)
downloadCheckStatus: DownloadCheckStatus, val item =
downloadItemPart: DownloadItemPart synchronized(this) { downloadItemQueue.find { it.id == part.downloadItemId } }
) { ?: run {
val downloadItem = downloadItemQueue.find { it.id == downloadItemPart.downloadItemId } removeActivePart(part)
if (downloadItem == null) { return
Log.e( }
tag, if (!part.completed && !part.failed) {
"Download item part finished but download item not found ${downloadItemPart.filename}" val lastUpdate = part.lastUpdateTime ?: return
) if (System.currentTimeMillis() - lastUpdate > STALL_TIMEOUT_MS) {
currentDownloadItemParts.remove(downloadItemPart) Log.w(tag, "Download stalled: ${part.filename}")
} else if (downloadCheckStatus == DownloadCheckStatus.Successful) { activeCalls.remove(part.id)?.cancel()
moveDownloadedFile(downloadItem, downloadItemPart) failOrRetry(item, part, "Download stalled")
} else if (downloadCheckStatus != DownloadCheckStatus.InProgress) { }
checkDownloadItemFinished(downloadItem) return
currentDownloadItemParts.remove(downloadItemPart) }
if (part.failed) {
failOrRetry(item, part, "Transfer failed")
return
}
if (part.isInternalStorage) finalizeInternalFile(item, part) else moveDownloadedFile(item, part)
}
@Synchronized
private fun failOrRetry(item: DownloadItem, part: DownloadItemPart, reason: String) {
removeActivePart(part)
part.retryCount += 1
reservations.remove(part.destinationPath)
if (part.retryCount > MAX_RETRIES) {
Log.e(tag, "$reason after $MAX_RETRIES retries: ${part.filename}")
part.failed = true
part.completed = false
part.downloadId = null
item.terminalFailureAt = item.terminalFailureAt ?: System.currentTimeMillis()
persist(item, force = true)
IncompleteDownloadCleanup.schedule(context, item)
notifyQueueChanged()
return
}
part.failed = false
part.completed = false
part.downloadId = null
part.isMoving = false
persist(item, force = true)
}
private fun finalizeInternalFile(item: DownloadItem, part: DownloadItemPart) {
if (part.moved || part.isMoving) return
part.isMoving = true
val stagingFile = File(part.destinationPath)
val finalFile = File(part.finalDestinationPath)
finalFile.parentFile?.mkdirs()
val backup = File(finalFile.parentFile, ".${finalFile.name}.abs-backup")
try {
if (backup.exists() && !backup.delete()) throw IllegalStateException("Could not clear backup")
if (finalFile.exists() && !finalFile.renameTo(backup))
throw IllegalStateException("Could not protect existing file")
if (!stagingFile.renameTo(finalFile)) {
if (backup.exists()) backup.renameTo(finalFile)
throw IllegalStateException("Could not finalize internal staging file")
}
backup.delete()
completePart(item, part)
} catch (e: Exception) {
part.isMoving = false
part.failed = true
failOrRetry(item, part, e.message ?: "Internal finalization failed")
} }
} }
/** Moves the downloaded file to its final destination. */ private fun moveDownloadedFile(item: DownloadItem, part: DownloadItemPart) {
private fun moveDownloadedFile(downloadItem: DownloadItem, downloadItemPart: DownloadItemPart) { if (part.moved || part.isMoving) return
val file = DocumentFileCompat.fromUri(mainActivity, downloadItemPart.destinationUri) val root =
Log.d(tag, "DOWNLOAD: DESTINATION URI ${downloadItemPart.destinationUri}") DocumentFile.fromTreeUri(context, Uri.parse(part.localFolderUrl))
?: return failFinalization(item, part, "Could not resolve SAF destination")
part.isMoving = true
persist(item, force = true)
scope.launch {
try {
if (!hasAvailableSpace(part))
throw IllegalStateException("Insufficient storage for SAF copy")
val folderKey = "${root.uri}/${part.finalDestinationSubfolder}"
val folderLock = safFolderLocks.computeIfAbsent(folderKey) { Any() }
val folder =
synchronized(folderLock) { getOrCreateFolder(root, part.finalDestinationSubfolder) }
?: throw IllegalStateException("Could not create SAF destination folder")
val temporaryName = ".${part.filename}.${part.id.hashCode()}.part"
folder.findFile(temporaryName)?.delete()
val temporary =
folder.createFile(mimeTypeFor(part), temporaryName)
?: throw IllegalStateException("Could not create SAF temporary file")
val staging = File(part.destinationPath)
FileInputStream(staging).use { input ->
context.contentResolver.openOutputStream(temporary.uri, "w")?.use { input.copyTo(it) }
?: throw IllegalStateException("Could not open SAF output stream")
}
if (temporary.length() != staging.length())
throw IllegalStateException("SAF copy size mismatch")
val existing = folder.findFile(part.filename)
if (existing != null && !existing.delete())
throw IllegalStateException("Could not replace existing file")
if (!temporary.renameTo(part.filename))
throw IllegalStateException("Could not finalize SAF temporary file")
val destination =
folder.findFile(part.filename)
?: throw IllegalStateException("Could not reopen finalized SAF file")
if (destination.length() != staging.length())
throw IllegalStateException("SAF final size mismatch")
if (!staging.delete()) Log.w(tag, "Could not remove staging file ${staging.name}")
part.completedDestinationUri = destination.uri.toString()
completePart(item, part)
} catch (e: Exception) {
failFinalization(item, part, "SAF copy failed: ${e.message}")
}
}
}
val fcb = @Synchronized
object : FileCallback() { private fun failFinalization(item: DownloadItem, part: DownloadItemPart, message: String) {
override fun onPrepare() { Log.e(tag, message)
Log.d(tag, "DOWNLOAD: PREPARING MOVE FILE") part.isMoving = false
} part.failed = true
failOrRetry(item, part, message)
}
override fun onFailed(errorCode: ErrorCode) { @Synchronized
Log.e(tag, "DOWNLOAD: FAILED TO MOVE FILE $errorCode") private fun completePart(item: DownloadItem, part: DownloadItemPart) {
downloadItemPart.failed = true part.moved = true
downloadItemPart.isMoving = false part.completed = true
file?.delete() part.failed = false
checkDownloadItemFinished(downloadItem) part.isMoving = false
currentDownloadItemParts.remove(downloadItemPart) reservations.remove(part.destinationPath)
} removeActivePart(part)
persist(item, force = true)
checkDownloadItemFinished(item)
}
override fun onCompleted(result: Any) { private fun checkDownloadItemFinished(item: DownloadItem) {
Log.d(tag, "DOWNLOAD: FILE MOVE COMPLETED") if (!item.isDownloadFinished) return
val resultDocFile = result as DocumentFile scope.launch {
Log.d( folderScanner.scanDownloadItem(item) { scanResult ->
tag, val event =
"DOWNLOAD: COMPLETED FILE INFO (name=${resultDocFile.name}) ${resultDocFile.getAbsolutePath(mainActivity)}" JSObject().apply {
) put("libraryItemId", item.id)
put("localFolderId", item.localFolder.id)
// Rename to fix appended .mp3 on m4b/m4a files scanResult?.localLibraryItem?.let {
// REF: https://github.com/anggrayudi/SimpleStorage/issues/94 put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(it)))
val docNameLowerCase = resultDocFile.name?.lowercase(Locale.getDefault()) ?: "" }
if (docNameLowerCase.endsWith(".m4b.mp3") || docNameLowerCase.endsWith(".m4a.mp3") scanResult?.localMediaProgress?.let {
) { put("localMediaProgress", JSObject(jacksonMapper.writeValueAsString(it)))
resultDocFile.renameTo(downloadItemPart.filename) }
} }
clientEventEmitter.onDownloadItemComplete(event)
downloadItemPart.moved = true synchronized(this@DownloadItemManager) {
downloadItemPart.isMoving = false downloadItemQueue.remove(item)
checkDownloadItemFinished(downloadItem) DeviceManager.dbManager.removeDownloadItem(item.id)
currentDownloadItemParts.remove(downloadItemPart) notifyQueueChanged()
} }
} }
val localFolderFile =
DocumentFileCompat.fromUri(mainActivity, Uri.parse(downloadItemPart.localFolderUrl))
if (localFolderFile == null) {
// Failed
downloadItemPart.failed = true
Log.e(tag, "Local Folder File from uri is null")
checkDownloadItemFinished(downloadItem)
currentDownloadItemParts.remove(downloadItemPart)
} else {
downloadItemPart.isMoving = true
val mimetype = if (downloadItemPart.audioTrack != null) MimeType.AUDIO else MimeType.IMAGE
val fileDescription =
FileDescription(
downloadItemPart.filename,
downloadItemPart.finalDestinationSubfolder,
mimetype
)
file?.moveFileTo(mainActivity, localFolderFile, fileDescription, fcb)
} }
} }
/** Checks if a download item is finished and processes it. */ private fun tryReserve(part: DownloadItemPart): Boolean {
private fun checkDownloadItemFinished(downloadItem: DownloadItem) { if (part.fileSize <= 0L && currentDownloadItemParts.any { it.fileSize <= 0L }) return false
if (downloadItem.isDownloadFinished) { val staging = File(part.destinationPath)
Log.i(tag, "Download Item finished ${downloadItem.media.metadata.title}") staging.parentFile?.mkdirs()
val expectedSize = if (part.fileSize > 0L) part.fileSize else UNKNOWN_PART_RESERVATION_BYTES
val remaining =
(expectedSize - (staging.takeIf(File::exists)?.length() ?: 0L)).coerceAtLeast(0L)
val required = if (part.isInternalStorage) remaining else remaining + expectedSize
val key = storageKey(staging)
val fs = statFsFor(staging)
val headroom = max(MIN_FREE_SPACE_BYTES, fs.totalBytes / 20L)
val alreadyReserved = reservations.filterKeys { storageKey(File(it)) == key }.values.sum()
if (fs.availableBytes - alreadyReserved < required + headroom) return false
reservations[part.destinationPath] = required
return true
}
GlobalScope.launch(Dispatchers.IO) { private fun hasAvailableSpace(part: DownloadItemPart): Boolean {
folderScanner.scanDownloadItem(downloadItem) { downloadItemScanResult -> val staging = File(part.destinationPath)
Log.d( val fs = statFsFor(staging)
tag, return fs.availableBytes >= max(MIN_FREE_SPACE_BYTES, fs.totalBytes / 20L)
"Item download complete ${downloadItem.itemTitle} | local library item id: ${downloadItemScanResult?.localLibraryItem?.id}" }
)
val jsobj = private fun statFsFor(staging: File): StatFs {
JSObject().apply { var directory = staging.parentFile ?: context.filesDir
put("libraryItemId", downloadItem.id) directory.mkdirs()
put("localFolderId", downloadItem.localFolder.id) while (!directory.exists()) directory = directory.parentFile ?: context.filesDir
return StatFs(directory.absolutePath)
}
downloadItemScanResult?.localLibraryItem?.let { localLibraryItem -> private fun storageKey(file: File): String =
put( if (file.absolutePath.startsWith(context.filesDir.absolutePath)) "internal"
"localLibraryItem", else "external"
JSObject(jacksonMapper.writeValueAsString(localLibraryItem))
) @Synchronized
} private fun removeActivePart(part: DownloadItemPart) {
downloadItemScanResult?.localMediaProgress?.let { localMediaProgress -> activeCalls.remove(part.id)
put( currentDownloadItemParts.remove(part)
"localMediaProgress", }
JSObject(jacksonMapper.writeValueAsString(localMediaProgress))
) private fun persist(item: DownloadItem, force: Boolean = false) {
} val now = System.currentTimeMillis()
if (!force && now - (lastPersistTime[item.id] ?: 0L) < PERSIST_INTERVAL_MS) return
lastPersistTime[item.id] = now
DeviceManager.dbManager.saveDownloadItem(item)
}
private fun notifyQueueChanged() {
clientEventEmitter.onQueueChanged(hasWork())
}
fun destroy() {
activeCalls.values.forEach(Call::cancel)
activeCalls.clear()
scope.cancel()
}
private fun getOrCreateFolder(root: DocumentFile, relativePath: String): DocumentFile? {
var current = root
relativePath.split('/').filter { it.isNotBlank() }.forEach { segment ->
if (segment == "." || segment == "..") return null
current = current.findFile(segment) ?: current.createDirectory(segment) ?: return null
}
return current
}
private fun findSharedStorageFile(part: DownloadItemPart): DocumentFile? {
if (part.isInternalStorage) return null
val root = DocumentFile.fromTreeUri(context, Uri.parse(part.localFolderUrl)) ?: return null
var folder = root
part.finalDestinationSubfolder.split('/').filter { it.isNotBlank() }.forEach { segment ->
if (segment == "." || segment == "..") return null
folder = folder.findFile(segment) ?: return null
}
val file = folder.findFile(part.filename) ?: return null
if (!file.isFile) return null
if (part.fileSize > 0L && file.length() != part.fileSize) return null
if (part.fileSize <= 0L && file.length() <= 0L) return null
return file
}
private fun mimeTypeFor(part: DownloadItemPart): String =
part.audioTrack?.mimeType
?: when (part.ebookFile?.ebookFormat?.lowercase()) {
"epub" -> "application/epub+zip"
"pdf" -> "application/pdf"
else -> "image/jpeg"
} }
launch(Dispatchers.Main) { private fun serverUrl(item: DownloadItem, part: DownloadItemPart): String {
clientEventEmitter.onDownloadItemComplete(jsobj) val rawCover = if (part.serverPath.endsWith("/cover")) "?raw=1" else ""
downloadItemQueue.remove(downloadItem) return "${item.serverAddress}${part.serverPath}$rawCover"
DeviceManager.dbManager.removeDownloadItem(downloadItem.id) }
}
} private companion object {
} const val APP_MANAGED_DOWNLOAD_ID = -1L
} const val MAX_SIMULTANEOUS_DOWNLOADS = 3
const val WATCH_INTERVAL_MS = 1_000L
const val STALL_TIMEOUT_MS = 60_000L
const val MAX_RETRIES = 5
const val PERSIST_INTERVAL_MS = 2_000L
const val MIN_FREE_SPACE_BYTES = 100L * 1024L * 1024L
const val UNKNOWN_PART_RESERVATION_BYTES = 100L * 1024L * 1024L
} }
} }
@@ -0,0 +1,94 @@
package com.audiobookshelf.app.managers
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.models.DownloadItem
import java.io.File
import java.util.concurrent.TimeUnit
/** Removes terminally failed downloads after their retention window elapses. */
object IncompleteDownloadCleanup {
private const val tag = "IncompleteDownloadCleanup"
private const val RETENTION_MS = 24L * 60L * 60L * 1000L
private const val WORK_PREFIX = "incomplete-download-"
fun schedule(context: Context, item: DownloadItem) {
val failedAt = item.terminalFailureAt ?: return
val delay = (failedAt + RETENTION_MS - System.currentTimeMillis()).coerceAtLeast(0L)
val request = OneTimeWorkRequestBuilder<IncompleteDownloadCleanupWorker>()
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
WORK_PREFIX + item.id, ExistingWorkPolicy.REPLACE, request)
}
fun cancel(context: Context, itemId: String) {
WorkManager.getInstance(context).cancelUniqueWork(WORK_PREFIX + itemId)
}
/** Removes failures retained longer than 24 hours when scheduled work did not run. */
fun cleanupExpired(context: Context) {
val now = System.currentTimeMillis()
DeviceManager.dbManager.getDownloadItems()
.filter { item -> isEligible(item, now) }
.forEach { item ->
deleteItem(context, item)
}
}
private fun isEligible(item: DownloadItem, now: Long): Boolean {
val failedAt = item.terminalFailureAt ?: return false
if (now - failedAt < RETENTION_MS) return false
return item.downloadItemParts.all { part ->
part.moved || (part.failed && !part.isMoving)
}
}
private fun deleteItem(context: Context, item: DownloadItem) {
item.downloadItemParts.forEach { part ->
deleteAppOwnedFile(context, File(part.destinationPath))
if (part.isInternalStorage && part.moved) {
deleteAppOwnedFile(context, File(part.finalDestinationPath))
} else if (!part.isInternalStorage && part.moved) {
part.completedDestinationUri?.let { uriString ->
try {
DocumentFile.fromSingleUri(context, Uri.parse(uriString))?.delete()
} catch (e: Exception) {
Log.w(tag, "Could not delete expired SAF document for ${part.filename}", e)
}
}
}
}
DeviceManager.dbManager.removeDownloadItem(item.id)
cancel(context, item.id)
Log.i(tag, "Deleted terminally failed download item ${item.id}")
}
private fun deleteAppOwnedFile(context: Context, file: File) {
val path = file.absolutePath
val internal = context.filesDir.absolutePath
val external = context.getExternalFilesDir(null)?.absolutePath
if (path.startsWith(internal) || (external != null && path.startsWith(external))) {
if (file.exists() && !file.delete()) Log.w(tag, "Could not delete expired staging file $path")
file.parentFile?.takeIf { it.isDirectory && it.list()?.isEmpty() == true }?.delete()
} else {
Log.w(tag, "Refusing to delete non-app-owned path $path")
}
}
}
class IncompleteDownloadCleanupWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
DbManager.initialize(applicationContext)
IncompleteDownloadCleanup.cleanupExpired(applicationContext)
return Result.success()
}
}
@@ -1,114 +1,135 @@
package com.audiobookshelf.app.managers package com.audiobookshelf.app.managers
import android.util.Log import android.util.Log
import java.io.* import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import okhttp3.* import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
/** /** Streams a download into an app-owned staging file. */
* Manages the internal download process.
*
* @property outputStream The output stream to write the downloaded data.
* @property progressCallback The callback to report download progress.
*/
class InternalDownloadManager( class InternalDownloadManager(
private val outputStream: FileOutputStream, private val destinationFile: File,
private val progressCallback: DownloadItemManager.InternalProgressCallback private val expectedSize: Long,
) : AutoCloseable { private val progressCallback: DownloadItemManager.InternalProgressCallback,
private val hasAvailableSpace: () -> Boolean
) {
private val tag = "InternalDownloadManager" private val tag = "InternalDownloadManager"
private val client: OkHttpClient =
OkHttpClient.Builder().connectTimeout(30, TimeUnit.SECONDS).build()
private val writer = BinaryFileWriter(outputStream, progressCallback)
/** /**
* Downloads a file from the given URL. * Starts or resumes a download.
* *
* @param url The URL to download the file from. * @param url download URL
* @throws IOException If an I/O error occurs. * @param token access token sent in the Authorization header
* @return active call, used to cancel a stalled transfer
*/ */
@Throws(IOException::class) fun download(url: String, token: String): Call {
fun download(url: String) { destinationFile.parentFile?.mkdirs()
val request: Request = Request.Builder().url(url).addHeader("Accept-Encoding", "identity").build() val existingBytes = destinationFile.takeIf { it.exists() }?.length() ?: 0L
client.newCall(request) val request =
.enqueue( Request.Builder()
object : Callback { .url(url)
override fun onFailure(call: Call, e: IOException) { .addHeader("Accept-Encoding", "identity")
Log.e(tag, "Download URL $url FAILED", e) .addHeader("Authorization", "Bearer $token")
progressCallback.onComplete(true) .apply { if (existingBytes > 0L) header("Range", "bytes=$existingBytes-") }
} .build()
val call = client.newCall(request)
call.enqueue(
object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e(tag, "Download URL failed", e)
progressCallback.onComplete(true)
}
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
response.body?.let { responseBody -> response.use {
val length: Long = response.header("Content-Length")?.toLongOrNull() ?: 0L try {
writer.write(responseBody.byteStream(), length) if (response.code == 416 && expectedSize > 0L && existingBytes == expectedSize
) {
progressCallback.onProgress(existingBytes, 100L)
progressCallback.onComplete(false)
return
}
val append =
existingBytes > 0L &&
response.code == 206 &&
hasExpectedRange(response, existingBytes)
if (existingBytes > 0L && !append && response.code != 200) {
Log.e(
tag,
"Invalid resume response ${response.code} for offset $existingBytes"
)
progressCallback.onComplete(true)
return
}
if (!response.isSuccessful || response.body == null) {
Log.e(tag, "Download HTTP failure ${response.code}")
progressCallback.onComplete(true)
return
}
val startingBytes = if (append) existingBytes else 0L
val responseLength = response.body!!.contentLength()
val totalLength =
if (expectedSize > 0L) expectedSize
else if (responseLength >= 0L) startingBytes + responseLength else 0L
FileOutputStream(destinationFile, append).use { output ->
response.body!!.byteStream().use { input ->
val buffer = ByteArray(CHUNK_SIZE)
var totalBytes = startingBytes
while (true) {
val read = input.read(buffer)
if (read < 0) break
if (!hasAvailableSpace())
throw IOException("Download paused to preserve free storage")
output.write(buffer, 0, read)
totalBytes += read
val progress =
if (totalLength > 0L) (totalBytes * 100L) / totalLength else 0L
progressCallback.onProgress(totalBytes, progress.coerceAtMost(100L))
} }
?: run {
Log.e(tag, "Response doesn't contain a file")
progressCallback.onComplete(true)
}
} }
} }
)
if (expectedSize > 0L && destinationFile.length() != expectedSize) {
Log.e(
tag,
"Downloaded size ${destinationFile.length()} did not match $expectedSize"
)
progressCallback.onComplete(true)
} else {
progressCallback.onComplete(false)
}
} catch (e: IOException) {
Log.e(tag, "Could not write staging file", e)
progressCallback.onComplete(true)
}
}
}
}
)
return call
} }
/** private fun hasExpectedRange(response: Response, offset: Long): Boolean {
* Closes the download manager and releases resources. val range = response.header("Content-Range") ?: return false
* val match = CONTENT_RANGE.matchEntire(range) ?: return false
* @throws Exception If an error occurs during closing. return match.groupValues[1].toLongOrNull() == offset &&
*/ match.groupValues[2].toLongOrNull()?.let { it >= offset } == true
@Throws(Exception::class) }
override fun close() {
writer.close() private companion object {
} const val CHUNK_SIZE = 512 * 1024 // 512 KB
} val CONTENT_RANGE = Regex("bytes (\\d+)-(\\d+)/(?:\\d+|\\*)")
val client =
/** OkHttpClient.Builder()
* Writes binary data to an output stream. .connectTimeout(30, TimeUnit.SECONDS)
* .readTimeout(60, TimeUnit.SECONDS)
* @property outputStream The output stream to write the data to. .writeTimeout(60, TimeUnit.SECONDS)
* @property progressCallback The callback to report write progress. .build()
*/
class BinaryFileWriter(
private val outputStream: OutputStream,
private val progressCallback: DownloadItemManager.InternalProgressCallback
) : AutoCloseable {
/**
* Writes data from the input stream to the output stream.
*
* @param inputStream The input stream to read the data from.
* @param length The total length of the data to be written.
* @return The total number of bytes written.
* @throws IOException If an I/O error occurs.
*/
@Throws(IOException::class)
fun write(inputStream: InputStream, length: Long): Long {
BufferedInputStream(inputStream).use { input ->
val dataBuffer = ByteArray(CHUNK_SIZE)
var totalBytes: Long = 0
var readBytes: Int
while (input.read(dataBuffer).also { readBytes = it } != -1) {
totalBytes += readBytes
outputStream.write(dataBuffer, 0, readBytes)
progressCallback.onProgress(totalBytes, (totalBytes * 100L) / length)
}
progressCallback.onComplete(false)
return totalBytes
}
}
/**
* Closes the writer and releases resources.
*
* @throws IOException If an error occurs during closing.
*/
@Throws(IOException::class)
override fun close() {
outputStream.close()
}
companion object {
private const val CHUNK_SIZE = 8192 // Increased chunk size for better performance
} }
} }
@@ -527,4 +527,18 @@ constructor(private val playerNotificationService: PlayerNotificationService) {
checkAutoSleepTimer() checkAutoSleepTimer()
} }
/**
* Sends the current sleep timer state to the client.
* Called when app resumes from background to sync UI state.
*/
fun sendCurrentSleepTimerState() {
if (sleepTimerRunning) {
val timeRemaining = getSleepTimerTimeRemainingSeconds(getPlaybackSpeed())
playerNotificationService.clientEventEmitter?.onSleepTimerSet(timeRemaining, isAutoSleepTimer)
} else {
// No timer running - send 0 to clear any stale UI state
playerNotificationService.clientEventEmitter?.onSleepTimerSet(0, false)
}
}
} }
@@ -9,8 +9,11 @@ import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.server.ApiHandler import com.audiobookshelf.app.server.ApiHandler
import com.getcapacitor.JSObject import com.getcapacitor.JSObject
import java.util.* import java.util.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.launch
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import kotlin.coroutines.resume import kotlin.coroutines.resume
@@ -46,6 +49,16 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
var userSettingsPlaybackRate:Float? = null var userSettingsPlaybackRate:Float? = null
// Android Auto browses on the media browser service main thread, so the server pings
// and authorize call must not run there or they block it and trigger an ANR
private val androidAutoIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// Android Auto re-requests the browse root every ~2s and multiple clients do so at once,
// so overlapping loads are coalesced into one to avoid racing on the shared server state
private var isLoadingAndroidAutoItems = false
private val pendingAndroidAutoLoadCallbacks = mutableListOf<() -> Unit>()
private val androidAutoLoadLock = Any()
fun getIsLibrary(id:String) : Boolean { fun getIsLibrary(id:String) : Boolean {
return serverLibraries.find { it.id == id } != null return serverLibraries.find { it.id == id } != null
} }
@@ -136,7 +149,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
// and reset any server data already set // and reset any server data already set
val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig() val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig()
if (!DeviceManager.isConnectedToServer || !DeviceManager.checkConnectivity(ctx) || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) { if (!DeviceManager.isConnectedToServer || !DeviceManager.checkConnectivity(ctx) || serverConnConfig == null || serverConnConfig.id != serverConfigIdUsed) {
podcastEpisodeLibraryItemMap = mutableMapOf() podcastEpisodeLibraryItemMap = mutableMapOf()
serverLibraries = listOf() serverLibraries = listOf()
serverLibraryItems = mutableListOf() serverLibraryItems = mutableListOf()
@@ -730,7 +743,10 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
return mediaProgress return mediaProgress
} }
private fun checkSetValidServerConnectionConfig(cb: (Boolean) -> Unit) = runBlocking { // Runs on [androidAutoIOScope] rather than blocking the caller: this is reached from
// onLoadChildren on the media browser service main thread, and the pings/authorize below
// are network calls, so runBlocking here caused ANRs while browsing in Android Auto
private fun checkSetValidServerConnectionConfig(cb: (Boolean) -> Unit) = androidAutoIOScope.launch {
Log.d(tag, "checkSetValidServerConnectionConfig | serverConfigIdUsed=$serverConfigIdUsed | lastServerConnectionConfigId=${DeviceManager.deviceData.lastServerConnectionConfigId}") Log.d(tag, "checkSetValidServerConnectionConfig | serverConfigIdUsed=$serverConfigIdUsed | lastServerConnectionConfigId=${DeviceManager.deviceData.lastServerConnectionConfigId}")
coroutineScope { coroutineScope {
@@ -835,6 +851,26 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
fun loadAndroidAutoItems(cb: () -> Unit) { fun loadAndroidAutoItems(cb: () -> Unit) {
Log.d(tag, "Load android auto items") Log.d(tag, "Load android auto items")
// Coalesce overlapping calls into a single load, every caller still gets its callback
synchronized(androidAutoLoadLock) {
pendingAndroidAutoLoadCallbacks.add(cb)
if (isLoadingAndroidAutoItems) {
Log.d(tag, "loadAndroidAutoItems: Load already in progress, queued callback")
return
}
isLoadingAndroidAutoItems = true
}
val onLoadFinished = {
val callbacks: List<() -> Unit>
synchronized(androidAutoLoadLock) {
isLoadingAndroidAutoItems = false
callbacks = pendingAndroidAutoLoadCallbacks.toList()
pendingAndroidAutoLoadCallbacks.clear()
}
callbacks.forEach { it() }
}
// Check if any valid server connection if not use locally downloaded books // Check if any valid server connection if not use locally downloaded books
checkSetValidServerConnectionConfig { isConnected -> checkSetValidServerConnectionConfig { isConnected ->
if (isConnected) { if (isConnected) {
@@ -844,14 +880,14 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
loadLibraries { libraries -> loadLibraries { libraries ->
if (libraries.isEmpty()) { if (libraries.isEmpty()) {
Log.w(tag, "No libraries returned from server request") Log.w(tag, "No libraries returned from server request")
cb() onLoadFinished()
} else { } else {
cb() // Fully loaded onLoadFinished() // Fully loaded
} }
} }
} else { // Not connected to server } else { // Not connected to server
Log.d(tag, "loadAndroidAutoItems: Not connected to server") Log.d(tag, "loadAndroidAutoItems: Not connected to server")
cb() onLoadFinished()
} }
} }
} }
@@ -6,34 +6,37 @@ import com.audiobookshelf.app.data.MediaType
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
data class DownloadItem( data class DownloadItem(
val id: String, val id: String,
val libraryItemId:String, val libraryItemId: String,
val episodeId:String?, val episodeId: String?,
val userMediaProgress: MediaProgress?, val userMediaProgress: MediaProgress?,
val serverConnectionConfigId:String, val serverConnectionConfigId: String,
val serverAddress:String, val serverAddress: String,
val serverUserId:String, val serverUserId: String,
val mediaType: String, val mediaType: String,
val itemFolderPath:String, val itemFolderPath: String,
val localFolder: LocalFolder, val localFolder: LocalFolder,
val itemTitle: String, val itemTitle: String,
val itemSubfolder: String, val itemSubfolder: String,
val media: MediaType, val media: MediaType,
val downloadItemParts: MutableList<DownloadItemPart> val downloadItemParts: MutableList<DownloadItemPart>,
@JsonIgnore var terminalFailureAt: Long? = null
) { ) {
@get:JsonIgnore @get:JsonIgnore
val isInternalStorage get() = localFolder.id.startsWith("internal-") val isInternalStorage
get() = localFolder.id.startsWith("internal-")
@get:JsonIgnore @get:JsonIgnore
val isDownloadFinished get() = !downloadItemParts.any { !it.completed || it.isMoving } val isDownloadFinished
get() = !downloadItemParts.any { !it.completed || it.isMoving || it.failed }
@JsonIgnore @JsonIgnore
fun getNextDownloadItemParts(limit:Int): MutableList<DownloadItemPart> { fun getNextDownloadItemParts(limit: Int): MutableList<DownloadItemPart> {
val itemParts = mutableListOf<DownloadItemPart>() val itemParts = mutableListOf<DownloadItemPart>()
if (limit == 0) return itemParts if (limit == 0) return itemParts
for (it in downloadItemParts) { for (it in downloadItemParts) {
if (!it.completed && it.downloadId == null) { if (!it.completed && !it.failed && it.downloadId == null) {
itemParts.add(it) itemParts.add(it)
if (itemParts.size >= limit) break if (itemParts.size >= limit) break
} }
@@ -1,6 +1,5 @@
package com.audiobookshelf.app.models package com.audiobookshelf.app.models
import android.app.DownloadManager
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import com.audiobookshelf.app.data.AudioTrack import com.audiobookshelf.app.data.AudioTrack
@@ -16,6 +15,7 @@ data class DownloadItemPart(
val downloadItemId: String, val downloadItemId: String,
val filename: String, val filename: String,
val fileSize: Long, val fileSize: Long,
@JsonIgnore val destinationPath: String,
val finalDestinationPath:String, val finalDestinationPath:String,
val serverPath: String, val serverPath: String,
val localFolderName: String, val localFolderName: String,
@@ -31,28 +31,29 @@ data class DownloadItemPart(
@JsonIgnore val uri: Uri, @JsonIgnore val uri: Uri,
@JsonIgnore val destinationUri: Uri, @JsonIgnore val destinationUri: Uri,
@JsonIgnore val finalDestinationUri: Uri, @JsonIgnore val finalDestinationUri: Uri,
@JsonIgnore var completedDestinationUri: String?,
val finalDestinationSubfolder: String, val finalDestinationSubfolder: String,
var downloadId: Long?, var downloadId: Long?,
@JsonIgnore var lastUpdateTime: Long?,
var progress: Long, var progress: Long,
var bytesDownloaded: Long var bytesDownloaded: Long,
@JsonIgnore var retryCount: Int = 0,
@JsonIgnore var waitingForSpace: Boolean = false
) { ) {
companion object { companion object {
fun make(downloadItemId:String, filename:String, fileSize: Long, destinationFile: File, finalDestinationFile: File, subfolder:String, serverPath:String, localFolder: LocalFolder, ebookFile: EBookFile?, audioTrack: AudioTrack?, episode: PodcastEpisode?) :DownloadItemPart { fun make(downloadItemId:String, filename:String, fileSize: Long, destinationFile: File, finalDestinationFile: File, subfolder:String, serverPath:String, localFolder: LocalFolder, ebookFile: EBookFile?, audioTrack: AudioTrack?, episode: PodcastEpisode?) :DownloadItemPart {
val destinationUri = Uri.fromFile(destinationFile) val destinationUri = Uri.fromFile(destinationFile)
val finalDestinationUri = Uri.fromFile(finalDestinationFile) val finalDestinationUri = Uri.fromFile(finalDestinationFile)
val rawCover = if (serverPath.endsWith("/cover")) "?raw=1" else ""
val downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}$rawCover")
var downloadUrl = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}" Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Server Path $serverPath")
if (serverPath.endsWith("/cover")) {
downloadUrl += "&raw=1" // Download raw cover image
}
val downloadUri = Uri.parse(downloadUrl)
Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Download URI $downloadUri")
return DownloadItemPart( return DownloadItemPart(
id = DeviceManager.getBase64Id(finalDestinationFile.absolutePath), id = DeviceManager.getBase64Id(finalDestinationFile.absolutePath),
downloadItemId, downloadItemId,
filename = filename, filename = filename,
fileSize = fileSize, fileSize = fileSize,
destinationPath = destinationFile.absolutePath,
finalDestinationPath = finalDestinationFile.absolutePath, finalDestinationPath = finalDestinationFile.absolutePath,
serverPath = serverPath, serverPath = serverPath,
localFolderName = localFolder.name, localFolderName = localFolder.name,
@@ -68,8 +69,10 @@ data class DownloadItemPart(
uri = downloadUri, uri = downloadUri,
destinationUri = destinationUri, destinationUri = destinationUri,
finalDestinationUri = finalDestinationUri, finalDestinationUri = finalDestinationUri,
completedDestinationUri = null,
finalDestinationSubfolder = subfolder, finalDestinationSubfolder = subfolder,
downloadId = null, downloadId = null,
lastUpdateTime = null,
progress = 0, progress = 0,
bytesDownloaded = 0 bytesDownloaded = 0
) )
@@ -79,16 +82,4 @@ data class DownloadItemPart(
@get:JsonIgnore @get:JsonIgnore
val isInternalStorage get() = localFolderId.startsWith("internal-") val isInternalStorage get() = localFolderId.startsWith("internal-")
@get:JsonIgnore
val serverUrl get() = uri.toString()
@JsonIgnore
fun getDownloadRequest(): DownloadManager.Request {
val dlRequest = DownloadManager.Request(uri)
dlRequest.setTitle(filename)
dlRequest.setDescription("Downloading to $localFolderName with filename $filename")
dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
dlRequest.setDestinationUri(destinationUri)
return dlRequest
}
} }
@@ -6,10 +6,8 @@ import android.graphics.ImageDecoder
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaControllerCompat import android.support.v4.media.session.MediaControllerCompat
import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.R
import com.bumptech.glide.Glide
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.PlayerNotificationManager import com.google.android.exoplayer2.ui.PlayerNotificationManager
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -36,7 +34,9 @@ class AbMediaDescriptionAdapter (private val controller: MediaControllerCompat,
): Bitmap? { ): Bitmap? {
val albumArtUri = controller.metadata.description.iconUri val albumArtUri = controller.metadata.description.iconUri
val albumBitmap = controller.metadata.description.iconBitmap val albumBitmap = controller.metadata.description.iconBitmap
?: controller.metadata.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART)
// Reuse bitmap from queue navigator (local) or PlaybackSession.resolveCoverBitmapAsync (streaming)
// For local cover images, bitmap is set in PlayerNotificationService TimelineQueueNavigator.getMediaDescription // For local cover images, bitmap is set in PlayerNotificationService TimelineQueueNavigator.getMediaDescription
if (albumBitmap != null) { if (albumBitmap != null) {
return albumBitmap return albumBitmap
@@ -59,7 +59,7 @@ class AbMediaDescriptionAdapter (private val controller: MediaControllerCompat,
} else { } else {
serviceScope.launch { serviceScope.launch {
currentBitmap = albumArtUri?.let { currentBitmap = albumArtUri?.let {
resolveUriAsBitmap(it) resolveUriAsBitmap(playerNotificationService, it)
} }
currentBitmap?.let { callback.onBitmap(it) } currentBitmap?.let { callback.onBitmap(it) }
} }
@@ -69,26 +69,4 @@ class AbMediaDescriptionAdapter (private val controller: MediaControllerCompat,
currentBitmap currentBitmap
} }
} }
private suspend fun resolveUriAsBitmap(uri: Uri): Bitmap? {
return withContext(Dispatchers.IO) {
try {
Glide.with(playerNotificationService)
.asBitmap()
.load(uri)
.placeholder(R.drawable.icon)
.error(R.drawable.icon)
.submit()
.get()
} catch (e: Exception) {
e.printStackTrace()
Glide.with(playerNotificationService)
.asBitmap()
.load(Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon))
.submit()
.get()
}
}
}
} }
@@ -3,6 +3,7 @@ package com.audiobookshelf.app.player
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import kotlin.math.roundToInt
import android.view.Surface import android.view.Surface
import android.view.SurfaceHolder import android.view.SurfaceHolder
import android.view.SurfaceView import android.view.SurfaceView
@@ -53,6 +54,10 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
private var myCurrentTrackGroups: TrackGroupArray private var myCurrentTrackGroups: TrackGroupArray
private var myCurrentTrackSelections: TrackSelectionArray private var myCurrentTrackSelections: TrackSelectionArray
// Cache for pending volume to handle async setVolume
private var pendingVolume: Int? = null
private var pendingVolumeSetAt: Long = 0L
var currentMediaItems:List<MediaItem> = mutableListOf() var currentMediaItems:List<MediaItem> = mutableListOf()
var remoteMediaClient:RemoteMediaClient? = null var remoteMediaClient:RemoteMediaClient? = null
var sessionAvailabilityListener:SessionAvailabilityListener? = null var sessionAvailabilityListener:SessionAvailabilityListener? = null
@@ -864,12 +869,24 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
} }
override fun getDeviceVolume(): Int { override fun getDeviceVolume(): Int {
// Short-lived cache without even-value quantization
val now = System.currentTimeMillis()
if (pendingVolume != null && (now - pendingVolumeSetAt) < 100) {
return pendingVolume!!
}
val session = castContext.sessionManager.currentCastSession val session = castContext.sessionManager.currentCastSession
return if (session != null && !session.isMute) { val vol = if (session != null && !session.isMute) {
(session.volume * 100).toInt() (session.volume * 100).roundToInt().coerceIn(0, 100)
} else { } else {
0 0
} }
if (pendingVolume != null && vol == pendingVolume) {
pendingVolume = null
}
return vol
} }
override fun isDeviceMuted(): Boolean { override fun isDeviceMuted(): Boolean {
@@ -881,22 +898,28 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
val session = castContext.sessionManager.currentCastSession val session = castContext.sessionManager.currentCastSession
try { try {
val clamped = volume.coerceIn(0, 100) val clamped = volume.coerceIn(0, 100)
session?.volume = clamped / 100.0 val target = (clamped / 100.0).let { kotlin.math.round(it * 100.0) / 100.0 }
// Cache for 100ms to return in getDeviceVolume during async period
pendingVolume = clamped
pendingVolumeSetAt = System.currentTimeMillis()
session?.setVolume(target)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "Failed to set cast device volume", e) Log.e(tag, "Failed to set cast device volume", e)
} }
} }
override fun increaseDeviceVolume() { override fun increaseDeviceVolume() {
val currentVolume = getDeviceVolume() val current = getDeviceVolume()
val newVolume = (currentVolume + 1).coerceAtMost(100) val next = current + 1
setDeviceVolume(newVolume) setDeviceVolume(next.coerceAtMost(100))
} }
override fun decreaseDeviceVolume() { override fun decreaseDeviceVolume() {
val currentVolume = getDeviceVolume() val current = getDeviceVolume()
val newVolume = (currentVolume - 1).coerceAtLeast(0) val next = current - 1
setDeviceVolume(newVolume) setDeviceVolume(next.coerceAtLeast(0))
} }
override fun setDeviceMuted(muted: Boolean) { override fun setDeviceMuted(muted: Boolean) {
@@ -0,0 +1,36 @@
package com.audiobookshelf.app.player
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.util.Log
import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.R
import com.bumptech.glide.Glide
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
private const val TAG = "CoverImageLoader"
/** Loads [uri] as a bitmap via Glide, falling back to the app icon if the load fails. */
suspend fun resolveUriAsBitmap(context: Context, uri: Uri): Bitmap? {
return withContext(Dispatchers.IO) {
try {
Glide.with(context)
.asBitmap()
.load(uri)
.placeholder(R.drawable.icon)
.error(R.drawable.icon)
.submit()
.get()
} catch (e: Exception) {
Log.e(TAG, "Failed to load cover bitmap for uri: $uri", e)
Glide.with(context)
.asBitmap()
.load(Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon))
.submit()
.get()
}
}
}
@@ -54,6 +54,11 @@ import com.google.android.exoplayer2.ui.PlayerNotificationManager
import com.google.android.exoplayer2.upstream.* import com.google.android.exoplayer2.upstream.*
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
const val SLEEP_TIMER_WAKE_UP_EXPIRATION = 120000L // 2m const val SLEEP_TIMER_WAKE_UP_EXPIRATION = 120000L // 2m
@@ -116,6 +121,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
var currentPlaybackSession: PlaybackSession? = null var currentPlaybackSession: PlaybackSession? = null
private var initialPlaybackRate: Float? = null private var initialPlaybackRate: Float? = null
private val metadataScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var metadataArtJob: Job? = null
private var isAndroidAuto = false private var isAndroidAuto = false
// The following are used for the shake detection // The following are used for the shake detection
@@ -198,6 +206,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
castPlayer?.release() castPlayer?.release()
mediaSession.release() mediaSession.release()
mediaProgressSyncer.reset() mediaProgressSyncer.reset()
metadataScope.cancel()
super.onDestroy() super.onDestroy()
} }
@@ -295,6 +304,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
transportControls = mediaController.transportControls transportControls = mediaController.transportControls
mediaSessionConnector = MediaSessionConnector(mediaSession) mediaSessionConnector = MediaSessionConnector(mediaSession)
// Without this, the connector's default metadata provider rebuilds metadata from the
// player's own state on media item transitions/timeline changes, dropping the cover art
// bitmap that PlaybackSession.resolveCoverBitmapAsync resolves separately.
mediaSessionConnector.setMediaMetadataProvider { _ ->
currentPlaybackSession?.getMediaMetadataCompat(ctx) ?: MediaMetadataCompat.Builder().build()
}
val queueNavigator: TimelineQueueNavigator = val queueNavigator: TimelineQueueNavigator =
object : TimelineQueueNavigator(mediaSession) { object : TimelineQueueNavigator(mediaSession) {
override fun getSupportedQueueNavigatorActions(player: Player): Long { override fun getSupportedQueueNavigatorActions(player: Player): Long {
@@ -430,8 +445,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
isClosed = false isClosed = false
val metadata = playbackSession.getMediaMetadataCompat(ctx)
mediaSession.setMetadata(metadata)
val mediaItems = playbackSession.getMediaItems(ctx) val mediaItems = playbackSession.getMediaItems(ctx)
val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f
initialPlaybackRate = playbackRate initialPlaybackRate = playbackRate
@@ -459,6 +472,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
playbackSession playbackSession
) // Save playback session to use when app is closed ) // Save playback session to use when app is closed
metadataArtJob?.cancel()
metadataArtJob =
playbackSession.resolveCoverBitmapAsync(ctx, metadataScope) {
mediaSessionConnector.invalidateMediaSessionMetadata()
}
mediaSessionConnector.invalidateMediaSessionMetadata()
AbsLogger.info("PlayerNotificationService", "preparePlayer: Started playback session for item ${currentPlaybackSession?.mediaItemId}. MediaPlayer ${currentPlaybackSession?.mediaPlayer}") AbsLogger.info("PlayerNotificationService", "preparePlayer: Started playback session for item ${currentPlaybackSession?.mediaItemId}. MediaPlayer ${currentPlaybackSession?.mediaPlayer}")
// Notify client // Notify client
clientEventEmitter?.onPlaybackSession(playbackSession) clientEventEmitter?.onPlaybackSession(playbackSession)
@@ -480,6 +500,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
val dataSourceFactory = DefaultDataSource.Factory(ctx) val dataSourceFactory = DefaultDataSource.Factory(ctx)
val extractorsFactory = DefaultExtractorsFactory() val extractorsFactory = DefaultExtractorsFactory()
extractorsFactory.setConstantBitrateSeekingEnabled(true)
if (DeviceManager.deviceData.deviceSettings?.enableMp3IndexSeeking == true) { if (DeviceManager.deviceData.deviceSettings?.enableMp3IndexSeeking == true) {
// @see // @see
// https://exoplayer.dev/troubleshooting.html#why-is-seeking-inaccurate-in-some-mp3-files // https://exoplayer.dev/troubleshooting.html#why-is-seeking-inaccurate-in-some-mp3-files
@@ -494,6 +516,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
val dataSourceFactory = DefaultHttpDataSource.Factory() val dataSourceFactory = DefaultHttpDataSource.Factory()
val extractorsFactory = DefaultExtractorsFactory() val extractorsFactory = DefaultExtractorsFactory()
extractorsFactory.setConstantBitrateSeekingEnabled(true)
if (DeviceManager.deviceData.deviceSettings?.enableMp3IndexSeeking == true) { if (DeviceManager.deviceData.deviceSettings?.enableMp3IndexSeeking == true) {
// @see // @see
// https://exoplayer.dev/troubleshooting.html#why-is-seeking-inaccurate-in-some-mp3-files // https://exoplayer.dev/troubleshooting.html#why-is-seeking-inaccurate-in-some-mp3-files
@@ -732,32 +756,21 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
} }
private fun setMediaSessionToCastVolume() { private fun setMediaSessionToCastVolume() {
val currentVol = try { castPlayer?.getDeviceVolume() ?: 0 } catch (e: Exception) { 0 } val currentVol = try { castPlayer?.getDeviceVolume() ?: 0 } catch (_: Exception) { 0 }
val provider = object : VolumeProviderCompat(VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE, 100, currentVol) { val provider = object : VolumeProviderCompat(VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE, 100, currentVol) {
override fun onSetVolumeTo(volume: Int) { override fun onSetVolumeTo(volume: Int) {
// Clamp, update UI immediately, then send to device
val clamped = volume.coerceIn(0, 100) val clamped = volume.coerceIn(0, 100)
try { setCurrentVolume(clamped)
castPlayer?.setDeviceVolume(clamped) try { castPlayer?.setDeviceVolume(clamped) } catch (_: Exception) {}
val actual = castPlayer?.getDeviceVolume() ?: clamped
setCurrentVolume(actual)
} catch (_: Exception) {}
} }
override fun onAdjustVolume(direction: Int) { override fun onAdjustVolume(direction: Int) {
val current = try { castPlayer?.getDeviceVolume() ?: currentVolume } catch (_: Exception) { currentVolume } // Use Android-provided step (1, 0, +1). Clamp, update UI immediately, then send.
val step = if (direction > 0) 1 else if (direction < 0) -1 else 0 val current = currentVolume
if (step == 0) return val target = (current + direction).coerceIn(0, 100)
var target = (current + step).coerceIn(0, 100) setCurrentVolume(target)
try { try { castPlayer?.setDeviceVolume(target) } catch (_: Exception) {}
castPlayer?.setDeviceVolume(target)
var actual = castPlayer?.getDeviceVolume() ?: current
// If device ignored tiny step, try nudging up to 3 steps total.
if (actual == current) {
target = (current + step * 3).coerceIn(0, 100)
castPlayer?.setDeviceVolume(target)
actual = castPlayer?.getDeviceVolume() ?: current
}
setCurrentVolume(actual)
} catch (_: Exception) {}
} }
} }
remoteVolumeProvider = provider remoteVolumeProvider = provider
@@ -33,6 +33,9 @@ class AbsAudioPlayer : Plugin() {
private var isCastAvailable:Boolean = false private var isCastAvailable:Boolean = false
// Track foreground state to avoid flooding WebView with events while backgrounded
private var isInForeground: Boolean = true
override fun load() { override fun load() {
mainActivity = (activity as MainActivity) mainActivity = (activity as MainActivity)
apiHandler = ApiHandler(mainActivity) apiHandler = ApiHandler(mainActivity)
@@ -60,6 +63,8 @@ class AbsAudioPlayer : Plugin() {
} }
override fun onMetadata(metadata: PlaybackMetadata) { override fun onMetadata(metadata: PlaybackMetadata) {
// Skip frequent metadata updates when app is backgrounded to prevent event queue buildup
if (!isInForeground) return
notifyListeners("onMetadata", JSObject(jacksonMapper.writeValueAsString(metadata))) notifyListeners("onMetadata", JSObject(jacksonMapper.writeValueAsString(metadata)))
} }
@@ -68,6 +73,8 @@ class AbsAudioPlayer : Plugin() {
} }
override fun onSleepTimerSet(sleepTimeRemaining: Int, isAutoSleepTimer:Boolean) { override fun onSleepTimerSet(sleepTimeRemaining: Int, isAutoSleepTimer:Boolean) {
// Skip sleep timer updates when app is backgrounded to prevent event queue buildup
if (!isInForeground) return
val ret = JSObject() val ret = JSObject()
ret.put("value", sleepTimeRemaining) ret.put("value", sleepTimeRemaining)
ret.put("isAuto", isAutoSleepTimer) ret.put("isAuto", isAutoSleepTimer)
@@ -75,6 +82,8 @@ class AbsAudioPlayer : Plugin() {
} }
override fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress) { override fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress) {
// Skip progress updates when app is backgrounded to prevent event queue buildup
if (!isInForeground) return
notifyListeners("onLocalMediaProgressUpdate", JSObject(jacksonMapper.writeValueAsString(localMediaProgress))) notifyListeners("onLocalMediaProgressUpdate", JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
} }
@@ -118,6 +127,27 @@ class AbsAudioPlayer : Plugin() {
notifyListeners(evtName, ret) notifyListeners(evtName, ret)
} }
override fun handleOnPause() {
super.handleOnPause()
isInForeground = false
}
override fun handleOnResume() {
super.handleOnResume()
isInForeground = true
// Send current state to UI after resume to sync up (with small delay to let WebView fully resume)
if (::playerNotificationService.isInitialized && playerNotificationService.currentPlaybackSession != null) {
Handler(Looper.getMainLooper()).postDelayed({
playerNotificationService.sendClientMetadata(PlayerState.READY)
playerNotificationService.sleepTimerManager.sendCurrentSleepTimerState()
playerNotificationService.mediaProgressSyncer.currentLocalMediaProgress?.let {
playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it)
}
}, 100)
}
}
private fun initCastManager() { private fun initCastManager() {
val googleApi = GoogleApiAvailability.getInstance() val googleApi = GoogleApiAvailability.getInstance()
val statusCode = googleApi.isGooglePlayServicesAvailable(mainActivity) val statusCode = googleApi.isGooglePlayServicesAvailable(mainActivity)
@@ -199,7 +229,7 @@ class AbsAudioPlayer : Plugin() {
return call.resolve(JSObject("{\"error\":\"Podcast episode not found\"}")) return call.resolve(JSObject("{\"error\":\"Podcast episode not found\"}"))
} }
} }
if (!it.hasTracks(episode)) { if (!it.hasTracks(mainActivity, episode)) {
return call.resolve(JSObject("{\"error\":\"No audio files found on device. Download book again to fix.\"}")) return call.resolve(JSObject("{\"error\":\"No audio files found on device. Download book again to fix.\"}"))
} }
@@ -295,10 +325,10 @@ class AbsAudioPlayer : Plugin() {
@PluginMethod @PluginMethod
fun seek(call: PluginCall) { fun seek(call: PluginCall) {
val time:Int = call.getInt("value", 0) ?: 0 // Value in seconds val time: Double = call.getDouble("value", 0.0) ?: 0.0 // Value in seconds, fractional
Log.d(tag, "seek action to $time") Log.d(tag, "seek action to $time")
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
playerNotificationService.seekPlayer(time * 1000L) // convert to ms playerNotificationService.seekPlayer((time * 1000L).toLong())
call.resolve() call.resolve()
} }
} }
@@ -40,7 +40,7 @@ class AbsDatabase : Plugin() {
secureStorage = SecureStorage(mainActivity) secureStorage = SecureStorage(mainActivity)
DeviceManager.dbManager.cleanLocalMediaProgress() DeviceManager.dbManager.cleanLocalMediaProgress()
DeviceManager.dbManager.cleanLocalLibraryItems() DeviceManager.dbManager.cleanLocalLibraryItems(mainActivity)
DeviceManager.dbManager.cleanLogs() DeviceManager.dbManager.cleanLogs()
} }
@@ -119,7 +119,7 @@ class AbsDatabase : Plugin() {
@PluginMethod @PluginMethod
fun setCurrentServerConnectionConfig(call:PluginCall) { fun setCurrentServerConnectionConfig(call:PluginCall) {
Log.d(tag, "setCurrentServerConnectionConfig ${call.data}") Log.d(tag, "setCurrentServerConnectionConfig called")
val serverConfigPayload = jacksonMapper.readValue<ServerConnConfigPayload>(call.data.toString()) val serverConfigPayload = jacksonMapper.readValue<ServerConnConfigPayload>(call.data.toString())
var serverConnectionConfig = DeviceManager.deviceData.serverConnectionConfigs.find { it.id == serverConfigPayload.id } var serverConnectionConfig = DeviceManager.deviceData.serverConnectionConfigs.find { it.id == serverConfigPayload.id }
@@ -558,7 +558,7 @@ class AbsDatabase : Plugin() {
@PluginMethod @PluginMethod
fun updateDeviceSettings(call:PluginCall) { // Returns device data fun updateDeviceSettings(call:PluginCall) { // Returns device data
Log.d(tag, "updateDeviceSettings ${call.data}") Log.d(tag, "updateDeviceSettings called")
val newDeviceSettings = jacksonMapper.readValue<DeviceSettings>(call.data.toString()) val newDeviceSettings = jacksonMapper.readValue<DeviceSettings>(call.data.toString())
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
@@ -576,7 +576,7 @@ class AbsDatabase : Plugin() {
@PluginMethod @PluginMethod
fun getMediaItemHistory(call:PluginCall) { // Returns device data fun getMediaItemHistory(call:PluginCall) { // Returns device data
Log.d(tag, "getMediaItemHistory ${call.data}") Log.d(tag, "getMediaItemHistory called")
val mediaId = call.getString("mediaId") ?: "" val mediaId = call.getString("mediaId") ?: ""
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
@@ -1,17 +1,15 @@
package com.audiobookshelf.app.plugins package com.audiobookshelf.app.plugins
import android.app.DownloadManager
import android.content.Context
import android.os.Environment import android.os.Environment
import android.util.Log import android.util.Log
import com.audiobookshelf.app.MainActivity import com.audiobookshelf.app.MainActivity
import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.device.FolderScanner
import com.audiobookshelf.app.models.DownloadItem import com.audiobookshelf.app.models.DownloadItem
import com.audiobookshelf.app.models.DownloadItemPart import com.audiobookshelf.app.models.DownloadItemPart
import com.audiobookshelf.app.server.ApiHandler import com.audiobookshelf.app.server.ApiHandler
import com.audiobookshelf.app.managers.DownloadItemManager import com.audiobookshelf.app.managers.DownloadItemManager
import com.audiobookshelf.app.services.DownloadServiceHost
import com.fasterxml.jackson.core.json.JsonReadFeature import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.JSObject import com.getcapacitor.JSObject
@@ -27,9 +25,7 @@ class AbsDownloader : Plugin() {
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
lateinit var mainActivity: MainActivity lateinit var mainActivity: MainActivity
lateinit var downloadManager: DownloadManager
lateinit var apiHandler: ApiHandler lateinit var apiHandler: ApiHandler
lateinit var folderScanner: FolderScanner
lateinit var downloadItemManager: DownloadItemManager lateinit var downloadItemManager: DownloadItemManager
private val clientEventEmitter = (object : DownloadItemManager.DownloadEventEmitter { private val clientEventEmitter = (object : DownloadItemManager.DownloadEventEmitter {
@@ -42,14 +38,44 @@ class AbsDownloader : Plugin() {
override fun onDownloadItemComplete(jsobj:JSObject) { override fun onDownloadItemComplete(jsobj:JSObject) {
notifyListeners("onItemDownloadComplete", jsobj) notifyListeners("onItemDownloadComplete", jsobj)
} }
override fun onQueueChanged(hasWork: Boolean) {
notifyListeners("onQueueChanged", JSObject().put("hasWork", hasWork))
}
}) })
override fun load() { override fun load() {
mainActivity = (activity as MainActivity) mainActivity = (activity as MainActivity)
downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
folderScanner = FolderScanner(mainActivity)
apiHandler = ApiHandler(mainActivity) apiHandler = ApiHandler(mainActivity)
downloadItemManager = DownloadItemManager(downloadManager, folderScanner, mainActivity, clientEventEmitter) downloadItemManager = DownloadServiceHost.ensure(mainActivity)
DownloadServiceHost.attachBridge(mainActivity, clientEventEmitter)
}
override fun handleOnDestroy() {
DownloadServiceHost.detachBridge()
super.handleOnDestroy()
}
@PluginMethod
fun setDownloadNotificationStrings(call: PluginCall) {
DownloadServiceHost.setNotificationStrings(
mainActivity,
call.getString("preparing") ?: "Preparing downloads",
call.getString("downloadingFile") ?: "Downloading {0}",
call.getString("waitingForStorage") ?: "Waiting for available storage",
call.getString("downloads") ?: "Downloads",
call.getString("cancel") ?: "Cancel")
call.resolve()
}
/** Replays restored queue items when the frontend subscribes to download events. */
@PluginMethod(returnType = PluginMethod.RETURN_NONE)
override fun addListener(call: PluginCall) {
super.addListener(call)
if (call.getString("eventName") == "onDownloadItem" && ::downloadItemManager.isInitialized) {
downloadItemManager.downloadItemQueue.forEach { item ->
notifyListeners("onDownloadItem", JSObject(jacksonMapper.writeValueAsString(item)))
}
}
} }
@PluginMethod @PluginMethod
@@ -79,7 +105,7 @@ class AbsDownloader : Plugin() {
if (localFolder == null && localFolderId.startsWith("internal-")) { if (localFolder == null && localFolderId.startsWith("internal-")) {
Log.d(tag, "Creating new App Storage internal LocalFolder $localFolderId") Log.d(tag, "Creating new App Storage internal LocalFolder $localFolderId")
localFolder = LocalFolder(localFolderId, "Internal App Storage", "", "", "", "", "internal", libraryItem.mediaType) localFolder = LocalFolder(localFolderId, "Internal App Storage", "", "", "", "internal", libraryItem.mediaType)
DeviceManager.dbManager.saveLocalFolder(localFolder) DeviceManager.dbManager.saveLocalFolder(localFolder)
} }
@@ -132,7 +158,13 @@ class AbsDownloader : Plugin() {
private fun startLibraryItemDownload(libraryItem: LibraryItem, localFolder: LocalFolder, episode:PodcastEpisode?) { private fun startLibraryItemDownload(libraryItem: LibraryItem, localFolder: LocalFolder, episode:PodcastEpisode?) {
val isInternal = localFolder.id.startsWith("internal-") val isInternal = localFolder.id.startsWith("internal-")
val tempFolderPath = if (isInternal) "${mainActivity.filesDir}/downloads/${libraryItem.id}" else mainActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) val finalInternalFolderPath = "${mainActivity.filesDir}/downloads/${libraryItem.id}"
val tempFolderPath =
if (isInternal) {
"${mainActivity.filesDir}/download-staging/${libraryItem.id}"
} else {
"${mainActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) ?: mainActivity.filesDir}/download-staging/${libraryItem.id}"
}
Log.d(tag, "downloadCacheDirectory=$tempFolderPath") Log.d(tag, "downloadCacheDirectory=$tempFolderPath")
@@ -143,7 +175,7 @@ class AbsDownloader : Plugin() {
val tracks = libraryItem.media.getAudioTracks() val tracks = libraryItem.media.getAudioTracks()
Log.d(tag, "Starting library item download with ${tracks.size} tracks") Log.d(tag, "Starting library item download with ${tracks.size} tracks")
val itemSubfolder = "$bookAuthor/$bookTitle" val itemSubfolder = "$bookAuthor/$bookTitle"
val itemFolderPath = if (isInternal) "$tempFolderPath" else "${localFolder.absolutePath}/$itemSubfolder" val itemFolderPath = if (isInternal) finalInternalFolderPath else "${localFolder.absolutePath}/$itemSubfolder"
val downloadItem = DownloadItem(libraryItem.id, libraryItem.id, null, libraryItem.userMediaProgress,DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, itemSubfolder, libraryItem.media, mutableListOf()) val downloadItem = DownloadItem(libraryItem.id, libraryItem.id, null, libraryItem.userMediaProgress,DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, itemSubfolder, libraryItem.media, mutableListOf())
val book = libraryItem.media as Book val book = libraryItem.media as Book
@@ -152,17 +184,7 @@ class AbsDownloader : Plugin() {
val serverPath = "/api/items/${libraryItem.id}/file/${ebookFile.ino}/download" val serverPath = "/api/items/${libraryItem.id}/file/${ebookFile.ino}/download"
val destinationFilename = getFilenameFromRelPath(ebookFile.metadata?.relPath ?: "") val destinationFilename = getFilenameFromRelPath(ebookFile.metadata?.relPath ?: "")
val finalDestinationFile = File("$itemFolderPath/$destinationFilename") val finalDestinationFile = File("$itemFolderPath/$destinationFilename")
val destinationFile = File("$tempFolderPath/$destinationFilename") val destinationFile = File("$tempFolderPath/$destinationFilename.part")
if (destinationFile.exists()) {
Log.d(tag, "TEMP ebook file already exists, removing it from ${destinationFile.absolutePath}")
destinationFile.delete()
}
if (finalDestinationFile.exists()) {
Log.d(tag, "ebook file already exists, removing it from ${finalDestinationFile.absolutePath}")
finalDestinationFile.delete()
}
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,ebookFile,null,null) val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,ebookFile,null,null)
downloadItem.downloadItemParts.add(downloadItemPart) downloadItem.downloadItemParts.add(downloadItemPart)
@@ -181,17 +203,7 @@ class AbsDownloader : Plugin() {
Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioTrack.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName $destinationFilename") Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioTrack.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName $destinationFilename")
val finalDestinationFile = File("$itemFolderPath/$destinationFilename") val finalDestinationFile = File("$itemFolderPath/$destinationFilename")
val destinationFile = File("$tempFolderPath/$destinationFilename") val destinationFile = File("$tempFolderPath/$destinationFilename.part")
if (destinationFile.exists()) {
Log.d(tag, "TEMP Audio file already exists, removing it from ${destinationFile.absolutePath}")
destinationFile.delete()
}
if (finalDestinationFile.exists()) {
Log.d(tag, "Audio file already exists, removing it from ${finalDestinationFile.absolutePath}")
finalDestinationFile.delete()
}
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,audioTrack,null) val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,audioTrack,null)
downloadItem.downloadItemParts.add(downloadItemPart) downloadItem.downloadItemParts.add(downloadItemPart)
@@ -205,24 +217,14 @@ class AbsDownloader : Plugin() {
val serverPath = "/api/items/${libraryItem.id}/cover" val serverPath = "/api/items/${libraryItem.id}/cover"
val destinationFilename = "cover-${libraryItem.id}.jpg" val destinationFilename = "cover-${libraryItem.id}.jpg"
val destinationFile = File("$tempFolderPath/$destinationFilename") val destinationFile = File("$tempFolderPath/$destinationFilename.part")
val finalDestinationFile = File("$itemFolderPath/$destinationFilename") val finalDestinationFile = File("$itemFolderPath/$destinationFilename")
if (destinationFile.exists()) {
Log.d(tag, "TEMP Audio file already exists, removing it from ${destinationFile.absolutePath}")
destinationFile.delete()
}
if (finalDestinationFile.exists()) {
Log.d(tag, "Cover already exists, removing it from ${finalDestinationFile.absolutePath}")
finalDestinationFile.delete()
}
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, coverFileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,null,null) val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, coverFileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,null,null)
downloadItem.downloadItemParts.add(downloadItemPart) downloadItem.downloadItemParts.add(downloadItemPart)
} }
downloadItemManager.addDownloadItem(downloadItem) DownloadServiceHost.enqueue(mainActivity, downloadItem)
} }
} else { } else {
// Podcast episode download // Podcast episode download
@@ -233,7 +235,7 @@ class AbsDownloader : Plugin() {
val fileSize = audioTrack?.metadata?.size ?: 0 val fileSize = audioTrack?.metadata?.size ?: 0
Log.d(tag, "Starting podcast episode download") Log.d(tag, "Starting podcast episode download")
val itemFolderPath = if (isInternal) "$tempFolderPath" else "${localFolder.absolutePath}/$podcastTitle" val itemFolderPath = if (isInternal) finalInternalFolderPath else "${localFolder.absolutePath}/$podcastTitle"
val downloadItemId = "${libraryItem.id}-${episode?.id}" val downloadItemId = "${libraryItem.id}-${episode?.id}"
val downloadItem = DownloadItem(downloadItemId, libraryItem.id, episode?.id, libraryItem.userMediaProgress, DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, podcastTitle, podcastTitle, libraryItem.media, mutableListOf()) val downloadItem = DownloadItem(downloadItemId, libraryItem.id, episode?.id, libraryItem.userMediaProgress, DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, podcastTitle, podcastTitle, libraryItem.media, mutableListOf())
@@ -241,13 +243,8 @@ class AbsDownloader : Plugin() {
var destinationFilename = getFilenameFromRelPath(audioTrack?.relPath ?: "") var destinationFilename = getFilenameFromRelPath(audioTrack?.relPath ?: "")
Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioTrack?.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName $destinationFilename") Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioTrack?.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName $destinationFilename")
var destinationFile = File("$tempFolderPath/$destinationFilename") var destinationFile = File("$tempFolderPath/$destinationFilename.part")
var finalDestinationFile = File("$itemFolderPath/$destinationFilename") var finalDestinationFile = File("$itemFolderPath/$destinationFilename")
if (finalDestinationFile.exists()) {
Log.d(tag, "Audio file already exists, removing it from ${finalDestinationFile.absolutePath}")
finalDestinationFile.delete()
}
var downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,fileSize, destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,audioTrack,episode) var downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,fileSize, destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,audioTrack,episode)
downloadItem.downloadItemParts.add(downloadItemPart) downloadItem.downloadItemParts.add(downloadItemPart)
@@ -258,18 +255,14 @@ class AbsDownloader : Plugin() {
serverPath = "/api/items/${libraryItem.id}/cover" serverPath = "/api/items/${libraryItem.id}/cover"
destinationFilename = "cover.jpg" destinationFilename = "cover.jpg"
destinationFile = File("$tempFolderPath/$destinationFilename") destinationFile = File("$tempFolderPath/$destinationFilename.part")
finalDestinationFile = File("$itemFolderPath/$destinationFilename") finalDestinationFile = File("$itemFolderPath/$destinationFilename")
if (finalDestinationFile.exists()) { downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,coverFileSize,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null,null)
Log.d(tag, "Podcast cover already exists - not downloading cover again") downloadItem.downloadItemParts.add(downloadItemPart)
} else {
downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,coverFileSize,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null,null)
downloadItem.downloadItemParts.add(downloadItemPart)
}
} }
downloadItemManager.addDownloadItem(downloadItem) DownloadServiceHost.enqueue(mainActivity, downloadItem)
} }
} }
} }
@@ -1,6 +1,7 @@
package com.audiobookshelf.app.plugins package com.audiobookshelf.app.plugins
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
@@ -23,40 +24,62 @@ import java.io.File
class AbsFileSystem : Plugin() { class AbsFileSystem : Plugin() {
private val TAG = "AbsFileSystem" private val TAG = "AbsFileSystem"
private val tag = "AbsFileSystem" private val tag = "AbsFileSystem"
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) private var jacksonMapper =
jacksonObjectMapper()
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
lateinit var mainActivity: MainActivity lateinit var mainActivity: MainActivity
override fun load() { override fun load() {
mainActivity = (activity as MainActivity) mainActivity = (activity as MainActivity)
mainActivity.storage.storageAccessCallback = object : StorageAccessCallback { mainActivity.storage.storageAccessCallback =
override fun onRootPathNotSelected( object : StorageAccessCallback {
requestCode: Int, override fun onRootPathNotSelected(
rootPath: String, requestCode: Int,
uri: Uri, rootPath: String,
selectedStorageType: StorageType, uri: Uri,
expectedStorageType: StorageType selectedStorageType: StorageType,
) { expectedStorageType: StorageType
Log.d(TAG, "STORAGE ACCESS CALLBACK") ) {
} Log.d(TAG, "STORAGE ACCESS CALLBACK")
}
override fun onCanceledByUser(requestCode: Int) { override fun onCanceledByUser(requestCode: Int) {
Log.d(TAG, "STORAGE ACCESS CALLBACK") Log.d(TAG, "STORAGE ACCESS CALLBACK")
} }
override fun onExpectedStorageNotSelected(requestCode: Int, selectedFolder: DocumentFile, selectedStorageType: StorageType, expectedBasePath: String, expectedStorageType: StorageType) { override fun onExpectedStorageNotSelected(
Log.d(TAG, "STORAGE ACCESS CALLBACK") requestCode: Int,
} selectedFolder: DocumentFile,
selectedStorageType: StorageType,
expectedBasePath: String,
expectedStorageType: StorageType
) {
Log.d(TAG, "STORAGE ACCESS CALLBACK")
}
override fun onStoragePermissionDenied(requestCode: Int) { override fun onStoragePermissionDenied(requestCode: Int) {
Log.d(TAG, "STORAGE ACCESS CALLBACK") Log.d(TAG, "STORAGE ACCESS CALLBACK")
} }
override fun onRootPathPermissionGranted(requestCode: Int, root: DocumentFile) { override fun onRootPathPermissionGranted(requestCode: Int, root: DocumentFile) {
Log.d(TAG, "STORAGE ACCESS CALLBACK") Log.d(TAG, "STORAGE ACCESS CALLBACK")
} }
} }
}
@PluginMethod
fun setFolderPickerStrings(call: PluginCall) {
mainActivity.getSharedPreferences(FOLDER_PICKER_PREFERENCES, Context.MODE_PRIVATE)
.edit()
.putString(KEY_WRITE_ACCESS_REQUIRED, call.getString("writeAccessRequired"))
.putString(KEY_ALLOW, call.getString("allow"))
.putString(KEY_CANCEL, call.getString("cancel"))
.putString(KEY_ACCESS_DENIED, call.getString("accessDenied"))
.putString(KEY_PERMISSION_DENIED, call.getString("permissionDenied"))
.apply()
call.resolve()
} }
@PluginMethod @PluginMethod
@@ -65,60 +88,74 @@ class AbsFileSystem : Plugin() {
val REQUEST_CODE_SELECT_FOLDER = 6 val REQUEST_CODE_SELECT_FOLDER = 6
val REQUEST_CODE_SDCARD_ACCESS = 7 val REQUEST_CODE_SDCARD_ACCESS = 7
mainActivity.storage.folderPickerCallback = object : FolderPickerCallback { mainActivity.storage.folderPickerCallback =
override fun onFolderSelected(requestCode: Int, folder: DocumentFile) { object : FolderPickerCallback {
Log.d(TAG, "ON FOLDER SELECTED ${folder.uri} ${folder.name}") override fun onFolderSelected(requestCode: Int, folder: DocumentFile) {
val absolutePath = folder.getAbsolutePath(activity) Log.d(TAG, "ON FOLDER SELECTED ${folder.uri} ${folder.name}")
val storageType = folder.getStorageType(activity) val absolutePath = folder.getAbsolutePath(activity)
val simplePath = folder.getSimplePath(activity) val storageType = folder.getStorageType(activity)
val basePath = folder.getBasePath(activity) val basePath = folder.getBasePath(activity)
val folderId = android.util.Base64.encodeToString(folder.id.toByteArray(), android.util.Base64.DEFAULT) val folderId =
android.util.Base64.encodeToString(
folder.id.toByteArray(),
android.util.Base64.DEFAULT
)
val localFolder = LocalFolder(folderId, folder.name ?: "", folder.uri.toString(),basePath,absolutePath, simplePath, storageType.toString(), mediaType) val localFolder =
LocalFolder(
folderId,
folder.name ?: "",
folder.uri.toString(),
basePath,
absolutePath,
storageType.toString(),
mediaType
)
DeviceManager.dbManager.saveLocalFolder(localFolder) DeviceManager.dbManager.saveLocalFolder(localFolder)
call.resolve(JSObject(jacksonMapper.writeValueAsString(localFolder))) call.resolve(JSObject(jacksonMapper.writeValueAsString(localFolder)))
} }
override fun onStorageAccessDenied( override fun onStorageAccessDenied(
requestCode: Int, requestCode: Int,
folder: DocumentFile?, folder: DocumentFile?,
storageType: StorageType, storageType: StorageType,
storageId: String storageId: String
) { ) {
Log.e(tag, "Storage Access Denied ${folder?.getAbsolutePath(mainActivity)}") Log.e(tag, "Storage Access Denied ${folder?.getAbsolutePath(mainActivity)}")
val jsobj = JSObject() val jsobj = JSObject()
if (requestCode == REQUEST_CODE_SELECT_FOLDER) { if (requestCode == REQUEST_CODE_SELECT_FOLDER) {
val builder: AlertDialog.Builder = AlertDialog.Builder(mainActivity) val builder: AlertDialog.Builder = AlertDialog.Builder(mainActivity)
builder.setMessage( builder.setMessage(folderPickerString(KEY_WRITE_ACCESS_REQUIRED, DEFAULT_WRITE_ACCESS_REQUIRED))
"You have no write access to this storage, thus selecting this folder is useless." + builder.setNegativeButton(folderPickerString(KEY_CANCEL, DEFAULT_CANCEL)) { _, _ ->
"\nWould you like to grant access to this folder?") run {
builder.setNegativeButton("Dont Allow") { _, _ -> jsobj.put("error", folderPickerString(KEY_ACCESS_DENIED, DEFAULT_ACCESS_DENIED))
run { call.resolve(jsobj)
jsobj.put("error", "User Canceled, Access Denied") }
call.resolve(jsobj) }
builder.setPositiveButton(folderPickerString(KEY_ALLOW, DEFAULT_ALLOW)) { _, _ ->
mainActivity.storageHelper.requestStorageAccess(
REQUEST_CODE_SDCARD_ACCESS,
initialPath = FileFullPath(mainActivity, storageId, "")
)
}
builder.show()
} else {
Log.d(TAG, "STORAGE ACCESS DENIED $requestCode")
jsobj.put("error", folderPickerString(KEY_ACCESS_DENIED, DEFAULT_ACCESS_DENIED))
call.resolve(jsobj)
}
}
override fun onStoragePermissionDenied(requestCode: Int) {
Log.d(TAG, "STORAGE PERMISSION DENIED $requestCode")
val jsobj = JSObject()
jsobj.put("error", folderPickerString(KEY_PERMISSION_DENIED, DEFAULT_PERMISSION_DENIED))
call.resolve(jsobj)
}
} }
}
builder.setPositiveButton("Allow.") { _, _ -> mainActivity.storageHelper.requestStorageAccess(REQUEST_CODE_SDCARD_ACCESS, initialPath = FileFullPath(mainActivity, storageId, "")) }
builder.show()
} else {
Log.d(TAG, "STORAGE ACCESS DENIED $requestCode")
jsobj.put("error", "Access Denied")
call.resolve(jsobj)
}
}
override fun onStoragePermissionDenied(requestCode: Int) {
Log.d(TAG, "STORAGE PERMISSION DENIED $requestCode")
val jsobj = JSObject()
jsobj.put("error", "Permission Denied")
call.resolve(jsobj)
}
}
mainActivity.storage.openFolderPicker(REQUEST_CODE_SELECT_FOLDER) mainActivity.storage.openFolderPicker(REQUEST_CODE_SELECT_FOLDER)
} }
@@ -152,7 +189,7 @@ class AbsFileSystem : Plugin() {
val folderUrl = call.data.getString("folderUrl", "").toString() val folderUrl = call.data.getString("folderUrl", "").toString()
Log.d(TAG, "Check Folder Permissions for $folderUrl") Log.d(TAG, "Check Folder Permissions for $folderUrl")
val hasAccess = SimpleStorage.hasStorageAccess(context,folderUrl,true) val hasAccess = SimpleStorage.hasStorageAccess(context, folderUrl, true)
val jsobj = JSObject() val jsobj = JSObject()
jsobj.put("value", hasAccess) jsobj.put("value", hasAccess)
@@ -196,26 +233,29 @@ class AbsFileSystem : Plugin() {
if (localLibraryItem?.folderId?.startsWith("internal-") == true) { if (localLibraryItem?.folderId?.startsWith("internal-") == true) {
Log.d(tag, "Deleting internal library item at absolutePath $absolutePath") Log.d(tag, "Deleting internal library item at absolutePath $absolutePath")
val file = File(absolutePath) val file = File(absolutePath)
success = if (file.exists()) { success =
file.deleteRecursively() if (file.exists()) {
} else { file.deleteRecursively()
true } else {
} true
}
} else { } else {
var subfolderPathToDelete = "" var subfolderPathToDelete = ""
localLibraryItem?.folderId?.let { folderId -> localLibraryItem?.folderId?.let { folderId ->
val folder = DeviceManager.dbManager.getLocalFolder(folderId) val folder = DeviceManager.dbManager.getLocalFolder(folderId)
folder?.absolutePath?.let { folderPath -> folder?.absolutePath?.let { folderPath ->
val splitAbsolutePath = absolutePath.split("/") val splitAbsolutePath = absolutePath.split("/")
val fullSubDir = splitAbsolutePath.subList(0, splitAbsolutePath.size - 1).joinToString("/") val fullSubDir =
splitAbsolutePath.subList(0, splitAbsolutePath.size - 1).joinToString("/")
if (fullSubDir != folderPath) { if (fullSubDir != folderPath) {
val subdirHasAnItem = DeviceManager.dbManager.getLocalLibraryItems().any { _localLibraryItem -> val subdirHasAnItem =
if (_localLibraryItem.id == localLibraryItemId) { DeviceManager.dbManager.getLocalLibraryItems().any { _localLibraryItem ->
false if (_localLibraryItem.id == localLibraryItemId) {
} else { false
_localLibraryItem.absolutePath.startsWith(fullSubDir) } else {
} _localLibraryItem.absolutePath.startsWith(fullSubDir)
} }
}
subfolderPathToDelete = if (subdirHasAnItem) "" else fullSubDir subfolderPathToDelete = if (subdirHasAnItem) "" else fullSubDir
} }
} }
@@ -266,4 +306,23 @@ class AbsFileSystem : Plugin() {
call.resolve(JSObject("{\"success\":false}")) call.resolve(JSObject("{\"success\":false}"))
} }
} }
private fun folderPickerString(key: String, defaultValue: String): String =
mainActivity.getSharedPreferences(FOLDER_PICKER_PREFERENCES, Context.MODE_PRIVATE)
.getString(key, defaultValue) ?: defaultValue
private companion object {
const val FOLDER_PICKER_PREFERENCES = "folder_picker"
const val KEY_WRITE_ACCESS_REQUIRED = "write_access_required"
const val KEY_ALLOW = "allow"
const val KEY_CANCEL = "cancel"
const val KEY_ACCESS_DENIED = "access_denied"
const val KEY_PERMISSION_DENIED = "permission_denied"
const val DEFAULT_WRITE_ACCESS_REQUIRED =
"You do not have write access to this folder. Would you like to grant access?"
const val DEFAULT_ALLOW = "Allow"
const val DEFAULT_CANCEL = "Cancel"
const val DEFAULT_ACCESS_DENIED = "Access denied"
const val DEFAULT_PERMISSION_DENIED = "Permission denied"
}
} }
@@ -17,6 +17,7 @@ import com.audiobookshelf.app.plugins.AbsLogger
import com.audiobookshelf.app.managers.SecureStorage import com.audiobookshelf.app.managers.SecureStorage
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.core.json.JsonReadFeature import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.getcapacitor.JSArray import com.getcapacitor.JSArray
@@ -48,7 +49,6 @@ class ApiHandler(var ctx:Context) {
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
private var secureStorage = SecureStorage(ctx) private var secureStorage = SecureStorage(ctx)
data class LocalSessionsSyncRequestPayload(val sessions:List<PlaybackSession>, val deviceInfo:DeviceInfo)
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class LocalSessionSyncResult(val id:String, val success:Boolean, val progressSynced:Boolean?, val error:String?) data class LocalSessionSyncResult(val id:String, val success:Boolean, val progressSynced:Boolean?, val error:String?)
data class LocalSessionsSyncResponsePayload(val results:List<LocalSessionSyncResult>) data class LocalSessionsSyncResponsePayload(val results:List<LocalSessionSyncResult>)
@@ -652,10 +652,29 @@ class ApiHandler(var ctx:Context) {
} }
} }
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: (Boolean, String?) -> Unit) { private fun createPartialPlaybackSession(playbackSession: PlaybackSession): ObjectNode {
val payload = JSObject(jacksonMapper.writeValueAsString(playbackSession)) val json = jacksonMapper.createObjectNode()
json.put("id", playbackSession.id)
json.put("userId", playbackSession.userId)
json.put("libraryItemId", playbackSession.libraryItemId)
json.put("episodeId", playbackSession.episodeId)
json.put("mediaType", playbackSession.mediaType)
json.put("displayTitle", playbackSession.displayTitle)
json.put("displayAuthor", playbackSession.displayAuthor)
json.put("duration", playbackSession.duration)
json.put("playMethod", playbackSession.playMethod)
json.put("startedAt", playbackSession.startedAt)
json.put("updatedAt", playbackSession.updatedAt)
json.put("timeListening", playbackSession.timeListening)
json.put("currentTime", playbackSession.currentTime)
json.put("mediaPlayer", playbackSession.mediaPlayer)
return json
}
postRequest("/api/session/local", payload, null) { fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: (Boolean, String?) -> Unit) {
val partialSession = createPartialPlaybackSession(playbackSession)
partialSession.set<ObjectNode>("deviceInfo", jacksonMapper.valueToTree(playbackSession.deviceInfo))
postRequest("/api/session/local", JSObject(partialSession.toString()), null) {
if (!it.getString("error").isNullOrEmpty()) { if (!it.getString("error").isNullOrEmpty()) {
cb(false, it.getString("error")) cb(false, it.getString("error"))
} else { } else {
@@ -748,10 +767,12 @@ class ApiHandler(var ctx:Context) {
val deviceId = Settings.Secure.getString(ctx.contentResolver, Settings.Secure.ANDROID_ID) val deviceId = Settings.Secure.getString(ctx.contentResolver, Settings.Secure.ANDROID_ID)
val deviceInfo = DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME) val deviceInfo = DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
val payload = JSObject(jacksonMapper.writeValueAsString(LocalSessionsSyncRequestPayload(playbackSessions, deviceInfo))) val json = jacksonMapper.createObjectNode();
json.putArray("sessions").addAll(playbackSessions.map(::createPartialPlaybackSession))
json.set<ObjectNode>("deviceInfo", jacksonMapper.valueToTree(deviceInfo))
AbsLogger.info("ApiHandler", "sendSyncLocalSessions: Sending ${playbackSessions.size} saved local playback sessions to server (${DeviceManager.serverConnectionConfigName})") AbsLogger.info("ApiHandler", "sendSyncLocalSessions: Sending ${playbackSessions.size} saved local playback sessions to server (${DeviceManager.serverConnectionConfigName})")
postRequest("/api/session/local-all", payload, null) { postRequest("/api/session/local-all", JSObject(json.toString()), null) {
if (!it.getString("error").isNullOrEmpty()) { if (!it.getString("error").isNullOrEmpty()) {
AbsLogger.error("ApiHandler", "sendSyncLocalSessions: Failed to sync local sessions. (${it.getString("error")})") AbsLogger.error("ApiHandler", "sendSyncLocalSessions: Failed to sync local sessions. (${it.getString("error")})")
cb(false, it.getString("error")) cb(false, it.getString("error"))
@@ -0,0 +1,102 @@
package com.audiobookshelf.app.services
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.audiobookshelf.app.R
import com.audiobookshelf.app.models.DownloadItemPart
/** Android-owned foreground lifecycle for transfers that must outlive the WebView and Activity. */
class DownloadService : Service() {
override fun onCreate() {
super.onCreate()
createChannel()
startForegroundWithType(DownloadServiceHost.notificationStrings(this).preparing)
DownloadServiceHost.attachService(this)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_CANCEL -> DownloadServiceHost.cancelAll(this)
else -> {
startForegroundWithType(DownloadServiceHost.notificationStrings(this).preparing)
DownloadServiceHost.ensure(this)
}
}
return START_STICKY
}
override fun onDestroy() {
DownloadServiceHost.detachService(this)
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? = null
fun onPartUpdate(part: DownloadItemPart) {
val strings = DownloadServiceHost.notificationStrings(this)
val text =
if (part.waitingForSpace) strings.waitingForStorage
else strings.downloadingFile.replace("{0}", part.filename)
val progress = part.progress.coerceIn(0L, 100L).toInt()
val notification = notification(text, progress, part.fileSize > 0L)
(getSystemService(NOTIFICATION_SERVICE) as NotificationManager).notify(NOTIFICATION_ID, notification)
}
fun onQueueChanged(hasWork: Boolean) {
if (!hasWork) {
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
}
private fun startForegroundWithType(text: String) {
val notification = notification(text)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} else {
startForeground(NOTIFICATION_ID, notification)
}
}
private fun notification(text: String, progress: Int = 0, determinate: Boolean = false): Notification {
val cancelIntent = PendingIntent.getService(
this, 1, Intent(this, DownloadService::class.java).setAction(ACTION_CANCEL), pendingIntentFlags())
return NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.icon)
.setContentTitle(DownloadServiceHost.notificationStrings(this).downloads)
.setContentText(text)
.setOnlyAlertOnce(true)
.setOngoing(true)
.setProgress(100, progress, !determinate)
.addAction(0, DownloadServiceHost.notificationStrings(this).cancel, cancelIntent)
.build()
}
private fun createChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(
NotificationChannel(
CHANNEL_ID,
DownloadServiceHost.notificationStrings(this).downloads,
NotificationManager.IMPORTANCE_LOW))
}
private fun pendingIntentFlags(): Int = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
companion object {
private const val CHANNEL_ID = "downloads"
private const val NOTIFICATION_ID = 11
private const val ACTION_CANCEL = "com.audiobookshelf.app.download.CANCEL"
fun intent(context: Context) = Intent(context, DownloadService::class.java)
}
}
@@ -0,0 +1,147 @@
package com.audiobookshelf.app.services
import android.content.Context
import androidx.core.content.ContextCompat
import com.audiobookshelf.app.device.FolderScanner
import com.audiobookshelf.app.managers.DbManager
import com.audiobookshelf.app.managers.DownloadItemManager
import com.audiobookshelf.app.models.DownloadItem
import com.getcapacitor.JSObject
import java.util.Collections
/** Shared process owner used by the foreground service and the Capacitor bridge. */
object DownloadServiceHost {
data class NotificationStrings(
val preparing: String,
val downloadingFile: String,
val waitingForStorage: String,
val downloads: String,
val cancel: String
)
private var manager: DownloadItemManager? = null
private var bridgeEmitter: DownloadItemManager.DownloadEventEmitter = NoopEmitter
private var service: DownloadService? = null
@Volatile private var bridgeReady = false
private val deferredCompletions = Collections.synchronizedList(mutableListOf<JSObject>())
@Synchronized
fun ensure(context: Context): DownloadItemManager {
if (manager == null) {
val appContext = context.applicationContext
DbManager.initialize(appContext)
manager = DownloadItemManager(FolderScanner(appContext), appContext, ForwardingEmitter)
manager!!.restoreQueue()
}
return manager!!
}
/** Attaches the frontend after restored queue items have been emitted. */
@Synchronized
fun attachBridge(context: Context, emitter: DownloadItemManager.DownloadEventEmitter) {
bridgeReady = false
bridgeEmitter = emitter
val queue = ensure(context)
queue.setEventEmitter(ForwardingEmitter)
bridgeReady = true
val completions = synchronized(deferredCompletions) {
deferredCompletions.toList().also { deferredCompletions.clear() }
}
completions.forEach(bridgeEmitter::onDownloadItemComplete)
if (queue.hasWork()) startService(context)
}
@Synchronized
fun detachBridge() {
bridgeReady = false
bridgeEmitter = NoopEmitter
}
@Synchronized
fun enqueue(context: Context, item: DownloadItem) {
ensure(context).addDownloadItem(item)
startService(context)
}
@Synchronized
fun cancelAll(context: Context) { ensure(context).cancelAll() }
fun setNotificationStrings(
context: Context,
preparing: String,
downloadingFile: String,
waitingForStorage: String,
downloads: String,
cancel: String
) {
context.getSharedPreferences(NOTIFICATION_PREFERENCES, Context.MODE_PRIVATE)
.edit()
.putString(KEY_PREPARING, preparing)
.putString(KEY_DOWNLOADING_FILE, downloadingFile)
.putString(KEY_WAITING_FOR_STORAGE, waitingForStorage)
.putString(KEY_DOWNLOADS, downloads)
.putString(KEY_CANCEL, cancel)
.apply()
}
fun notificationStrings(context: Context): NotificationStrings {
val preferences = context.getSharedPreferences(NOTIFICATION_PREFERENCES, Context.MODE_PRIVATE)
return NotificationStrings(
preferences.getString(KEY_PREPARING, DEFAULT_PREPARING) ?: DEFAULT_PREPARING,
preferences.getString(KEY_DOWNLOADING_FILE, DEFAULT_DOWNLOADING_FILE)
?: DEFAULT_DOWNLOADING_FILE,
preferences.getString(KEY_WAITING_FOR_STORAGE, DEFAULT_WAITING_FOR_STORAGE)
?: DEFAULT_WAITING_FOR_STORAGE,
preferences.getString(KEY_DOWNLOADS, DEFAULT_DOWNLOADS) ?: DEFAULT_DOWNLOADS,
preferences.getString(KEY_CANCEL, DEFAULT_CANCEL) ?: DEFAULT_CANCEL)
}
@Synchronized
fun attachService(downloadService: DownloadService) {
service = downloadService
service?.onQueueChanged(ensure(downloadService).hasWork())
}
@Synchronized
fun detachService(downloadService: DownloadService) {
if (service === downloadService) service = null
}
private fun startService(context: Context) {
ContextCompat.startForegroundService(context, DownloadService.intent(context))
}
private object ForwardingEmitter : DownloadItemManager.DownloadEventEmitter {
override fun onDownloadItem(downloadItem: DownloadItem) { bridgeEmitter.onDownloadItem(downloadItem) }
override fun onDownloadItemPartUpdate(downloadItemPart: com.audiobookshelf.app.models.DownloadItemPart) {
if (bridgeReady) bridgeEmitter.onDownloadItemPartUpdate(downloadItemPart)
service?.onPartUpdate(downloadItemPart)
}
override fun onDownloadItemComplete(jsobj: JSObject) {
if (bridgeReady) bridgeEmitter.onDownloadItemComplete(jsobj) else deferredCompletions.add(jsobj)
}
override fun onQueueChanged(hasWork: Boolean) {
bridgeEmitter.onQueueChanged(hasWork)
service?.onQueueChanged(hasWork)
}
}
private object NoopEmitter : DownloadItemManager.DownloadEventEmitter {
override fun onDownloadItem(downloadItem: DownloadItem) = Unit
override fun onDownloadItemPartUpdate(downloadItemPart: com.audiobookshelf.app.models.DownloadItemPart) = Unit
override fun onDownloadItemComplete(jsobj: JSObject) = Unit
override fun onQueueChanged(hasWork: Boolean) = Unit
}
private const val NOTIFICATION_PREFERENCES = "download_notifications"
private const val KEY_PREPARING = "preparing"
private const val KEY_DOWNLOADING_FILE = "downloading_file"
private const val KEY_WAITING_FOR_STORAGE = "waiting_for_storage"
private const val KEY_DOWNLOADS = "downloads"
private const val KEY_CANCEL = "cancel"
private const val DEFAULT_PREPARING = "Preparing downloads"
private const val DEFAULT_DOWNLOADING_FILE = "Downloading {0}"
private const val DEFAULT_WAITING_FOR_STORAGE = "Waiting for available storage"
private const val DEFAULT_DOWNLOADS = "Downloads"
private const val DEFAULT_CANCEL = "Cancel"
}
+13 -11
View File
@@ -3,7 +3,7 @@ class WrappingMarquee {
#scrollSpeed = 30 #scrollSpeed = 30
/** /**
* @param {HTMLElement} el * @param {HTMLElement} el
*/ */
constructor(el) { constructor(el) {
this.el = el this.el = el
@@ -21,8 +21,8 @@ class WrappingMarquee {
/** /**
* Transparent gradient mask shown when text is scrolling left and overflowing right * Transparent gradient mask shown when text is scrolling left and overflowing right
* *
* @param {boolean} showLeft * @param {boolean} showLeft
*/ */
setMask(showLeft) { setMask(showLeft) {
if (!this.el) return if (!this.el) return
@@ -39,11 +39,11 @@ class WrappingMarquee {
this.setMask(true) this.setMask(true)
let textScrollAmount = this.el.scrollWidth let textScrollAmount = this.el.scrollWidth
this.pEl.innerHTML = this.innerText + '&nbsp;'.repeat(15) this.pEl.textContent = this.innerText + '\u00A0'.repeat(15) // \u00A0 = non-breaking space
let totalScrollAmount = this.el.scrollWidth let totalScrollAmount = this.el.scrollWidth
let scrollDuration = totalScrollAmount * this.#scrollSpeed let scrollDuration = totalScrollAmount * this.#scrollSpeed
this.pEl.innerHTML = this.pEl.innerHTML + this.innerText this.pEl.textContent = this.pEl.textContent + this.innerText
let done = false let done = false
let start, previousTimeStamp let start, previousTimeStamp
@@ -55,13 +55,14 @@ class WrappingMarquee {
const elapsed = timeStamp - start const elapsed = timeStamp - start
if (this.isScrolling && previousTimeStamp !== timeStamp) { if (this.isScrolling && previousTimeStamp !== timeStamp) {
const amountToMove = Math.min(elapsed / scrollDuration * totalScrollAmount, totalScrollAmount) const amountToMove = Math.min((elapsed / scrollDuration) * totalScrollAmount, totalScrollAmount)
this.pEl.style.transform = `translateX(-${amountToMove}px)` this.pEl.style.transform = `translateX(-${amountToMove}px)`
if (amountToMove === totalScrollAmount) done = true if (amountToMove === totalScrollAmount) done = true
if (amountToMove > textScrollAmount) this.setMask(false) if (amountToMove > textScrollAmount) this.setMask(false)
} }
if (!this.isScrolling || done) { // canceled or done if (!this.isScrolling || done) {
// canceled or done
this.isScrolling = false this.isScrolling = false
this.pEl.style.transform = 'translateX(0px)' this.pEl.style.transform = 'translateX(0px)'
this.pEl.innerText = this.innerText this.pEl.innerText = this.innerText
@@ -69,7 +70,8 @@ class WrappingMarquee {
if (done) { if (done) {
this.startTimer() this.startTimer()
} }
} else if (elapsed < scrollDuration) { // step } else if (elapsed < scrollDuration) {
// step
previousTimeStamp = timeStamp previousTimeStamp = timeStamp
this.animationId = window.requestAnimationFrame(step) this.animationId = window.requestAnimationFrame(step)
} }
@@ -94,8 +96,8 @@ class WrappingMarquee {
/** /**
* Initialize and start marquee if text overflows container * Initialize and start marquee if text overflows container
* resets the marquee if already active * resets the marquee if already active
* *
* @param {string} innerText * @param {string} innerText
*/ */
init(innerText) { init(innerText) {
if (!this.el || !this.pEl) return if (!this.el || !this.pEl) return
@@ -114,4 +116,4 @@ class WrappingMarquee {
} }
} }
} }
export default WrappingMarquee export default WrappingMarquee
+154 -58
View File
@@ -1,20 +1,31 @@
<template> <template>
<!-- Main container - only shows when playback session exists, applies fullscreen/platform classes -->
<div v-if="playbackSession" id="streamContainer" class="fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none" :class="{ fullscreen: showFullscreen, 'ios-player': $platform === 'ios', 'web-player': $platform === 'web' }"> <div v-if="playbackSession" id="streamContainer" class="fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none" :class="{ fullscreen: showFullscreen, 'ios-player': $platform === 'ios', 'web-player': $platform === 'web' }">
<!-- Fullscreen overlays: colored background and menu overlays -->
<div v-if="showFullscreen" class="w-full h-full z-10 absolute top-0 left-0 pointer-events-auto" :style="{ backgroundColor: coverRgb }"> <div v-if="showFullscreen" class="w-full h-full z-10 absolute top-0 left-0 pointer-events-auto" :style="{ backgroundColor: coverRgb }">
<!-- Background gradient for player -->
<div class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: var(--gradient-audio-player)" /> <div class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: var(--gradient-audio-player)" />
<!-- Collapse button - minimizes player -->
<div class="top-4 left-4 absolute cursor-pointer"> <div class="top-4 left-4 absolute cursor-pointer">
<span class="material-symbols text-5xl" :class="{ 'text-black text-opacity-75': coverBgIsLight && theme !== 'black' }" @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>
<!-- Cast button - Chromecast toggle -->
<div v-show="showCastBtn" class="top-6 right-16 absolute cursor-pointer"> <div v-show="showCastBtn" class="top-6 right-16 absolute cursor-pointer">
<span class="material-symbols text-3xl" :class="coverBgIsLight && theme !== 'black' ? '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>
<!-- Player options menu button -->
<div class="top-6 right-4 absolute cursor-pointer"> <div class="top-6 right-4 absolute cursor-pointer">
<span class="material-symbols text-3xl" :class="{ 'text-black text-opacity-75': coverBgIsLight && theme !== 'black' }" @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> </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 && theme !== 'black' }" style="font-size: 10px">{{ isDirectPlayMethod ? $strings.LabelPlaybackDirect : isLocalPlayMethod ? $strings.LabelPlaybackLocal : $strings.LabelPlaybackTranscode }}</p>
<!-- Playback method indicator (Direct/Local/Transcode) -->
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75 z-50" :class="{ 'text-black text-opacity-75': coverBgIsLight && theme !== 'black' }" style="font-size: 10px">{{ isDirectPlayMethod ? $strings.LabelPlaybackDirect : isLocalPlayMethod ? $strings.LabelPlaybackLocal : $strings.LabelPlaybackTranscode }}</p>
</div> </div>
<!-- Overall book progress bar -->
<div v-if="playerSettings.useChapterTrack && playerSettings.useTotalTrack && showFullscreen" class="absolute total-track w-full z-30 px-6"> <div v-if="playerSettings.useChapterTrack && playerSettings.useTotalTrack && showFullscreen" class="absolute total-track w-full z-30 px-6">
<div class="flex"> <div class="flex">
<p class="font-mono text-fg" style="font-size: 0.8rem">{{ currentTimePretty }}</p> <p class="font-mono text-fg" style="font-size: 0.8rem">{{ currentTimePretty }}</p>
@@ -22,7 +33,7 @@
<p class="font-mono text-fg" style="font-size: 0.8rem">{{ totalTimeRemainingPretty }}</p> <p class="font-mono text-fg" style="font-size: 0.8rem">{{ totalTimeRemainingPretty }}</p>
</div> </div>
<div class="w-full"> <div class="w-full">
<div class="h-1 w-full bg-track/50 relative rounded-full"> <div class="h-1 w-full bg-track/50 relative rounded-full overflow-hidden">
<div ref="totalReadyTrack" class="h-full bg-track-buffered absolute top-0 left-0 pointer-events-none rounded-full" /> <div ref="totalReadyTrack" class="h-full bg-track-buffered absolute top-0 left-0 pointer-events-none rounded-full" />
<div ref="totalBufferedTrack" class="h-full bg-track absolute top-0 left-0 pointer-events-none rounded-full" /> <div ref="totalBufferedTrack" class="h-full bg-track absolute top-0 left-0 pointer-events-none rounded-full" />
<div ref="totalPlayedTrack" class="h-full bg-track-cursor absolute top-0 left-0 pointer-events-none rounded-full" /> <div ref="totalPlayedTrack" class="h-full bg-track-cursor absolute top-0 left-0 pointer-events-none rounded-full" />
@@ -30,16 +41,19 @@
</div> </div>
</div> </div>
<!-- Book cover wrapper -->
<div class="cover-wrapper absolute z-30 pointer-events-auto" @click="clickContainer"> <div class="cover-wrapper absolute z-30 pointer-events-auto" @click="clickContainer">
<div class="w-full h-full flex justify-center"> <div class="w-full h-full flex justify-center">
<covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" ref="cover" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" raw @imageLoaded="coverImageLoaded" /> <covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" ref="cover" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" raw @imageLoaded="coverImageLoaded" />
</div> </div>
<!-- Sync failed indicator - shows error icon if progress sync failed -->
<div v-if="syncStatus === $constants.SyncStatus.FAILED" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-30" @click.stop="showSyncsFailedDialog"> <div v-if="syncStatus === $constants.SyncStatus.FAILED" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-30" @click.stop="showSyncsFailedDialog">
<span class="material-symbols text-error text-3xl">error</span> <span class="material-symbols text-error text-3xl">error</span>
</div> </div>
</div> </div>
<!-- Title and author text -->
<div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor"> <div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor">
<div ref="titlewrapper" class="overflow-hidden relative"> <div ref="titlewrapper" class="overflow-hidden relative">
<p class="title-text whitespace-nowrap"></p> <p class="title-text whitespace-nowrap"></p>
@@ -48,6 +62,7 @@
</div> </div>
<div id="playerContent" class="playerContainer w-full z-20 absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" :style="{ backgroundColor: showFullscreen ? '' : coverRgb }" @click="clickContainer"> <div id="playerContent" class="playerContainer w-full z-20 absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" :style="{ backgroundColor: showFullscreen ? '' : coverRgb }" @click="clickContainer">
<!-- Top controls bar - fullscreen only: bookmarks, speed, sleep timer, chapters -->
<div v-if="showFullscreen" class="absolute bottom-4 left-0 right-0 w-full pb-4 pt-2 mx-auto px-6" style="max-width: 414px"> <div v-if="showFullscreen" class="absolute bottom-4 left-0 right-0 w-full pb-4 pt-2 mx-auto px-6" style="max-width: 414px">
<div class="flex items-center justify-between pointer-events-auto"> <div class="flex items-center justify-between pointer-events-auto">
<span v-if="!isPodcast && serverLibraryItemId && socketConnected" class="material-symbols text-3xl text-fg-muted cursor-pointer" :class="{ fill: bookmarks.length }" @click="$emit('showBookmarks')">bookmark</span> <span v-if="!isPodcast && serverLibraryItemId && socketConnected" class="material-symbols text-3xl text-fg-muted cursor-pointer" :class="{ fill: bookmarks.length }" @click="$emit('showBookmarks')">bookmark</span>
@@ -67,28 +82,36 @@
</div> </div>
<div v-else class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: var(--gradient-minimized-audio-player)" /> <div v-else class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: var(--gradient-minimized-audio-player)" />
<!-- Playback controls - jump buttons, play/pause, chapter navigation -->
<div id="playerControls" class="absolute right-0 bottom-0 mx-auto" style="max-width: 414px"> <div id="playerControls" class="absolute right-0 bottom-0 mx-auto" style="max-width: 414px">
<div class="flex items-center max-w-full" :class="playerSettings.lockUi ? 'justify-center' : 'justify-between'"> <div class="flex items-center max-w-full" :class="playerSettings.lockUi ? 'justify-center' : 'justify-between'">
<span v-show="showFullscreen && !playerSettings.lockUi" class="material-symbols next-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span> <span v-show="showFullscreen && !playerSettings.lockUi" class="material-symbols next-icon text-fg cursor-pointer" :class="showLoadingState ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
<span v-show="!playerSettings.lockUi" class="material-symbols jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">{{ jumpBackwardsIcon }}</span> <div v-show="!playerSettings.lockUi" class="jump-icon text-fg cursor-pointer flex flex-col items-center" :class="showLoadingState ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">
<span class="material-symbols text-3xl leading-none">replay</span>
<span v-if="showFullscreen" class="jump-label text-[10px] font-semibold leading-tight">{{ jumpBackwardsLabel }}</span>
</div>
<div class="play-btn cursor-pointer shadow-sm flex items-center justify-center rounded-full text-primary mx-4 relative overflow-hidden" :style="{ backgroundColor: coverRgb }" :class="{ 'animate-spin': seekLoading }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick"> <div class="play-btn cursor-pointer shadow-sm flex items-center justify-center rounded-full text-primary mx-4 relative overflow-hidden" :style="{ backgroundColor: coverRgb }" :class="{ 'animate-spin': seekLoading }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
<div v-if="!coverBgIsLight" class="absolute top-0 left-0 w-full h-full bg-white bg-opacity-20 pointer-events-none" /> <div v-if="!coverBgIsLight" class="absolute top-0 left-0 w-full h-full bg-white bg-opacity-20 pointer-events-none" />
<span v-if="!isLoading" class="material-symbols fill" :class="{ 'text-white': coverRgb && !coverBgIsLight }">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span> <span v-if="!showLoadingState" class="material-symbols fill" :class="{ 'text-white': coverRgb && !coverBgIsLight }">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span>
<widgets-spinner-icon v-else class="h-8 w-8" /> <widgets-spinner-icon v-else class="h-8 w-8" />
</div> </div>
<span v-show="!playerSettings.lockUi" class="material-symbols jump-icon text-fg cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">{{ jumpForwardIcon }}</span> <div v-show="!playerSettings.lockUi" class="jump-icon text-fg cursor-pointer flex flex-col items-center" :class="showLoadingState ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">
<span v-show="showFullscreen && !playerSettings.lockUi" class="material-symbols next-icon text-fg cursor-pointer" :class="nextChapter && !isLoading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span> <span class="material-symbols text-3xl leading-none">forward_media</span>
<span v-if="showFullscreen" class="jump-label text-[10px] font-semibold leading-tight">{{ jumpForwardLabel }}</span>
</div>
<span v-show="showFullscreen && !playerSettings.lockUi" class="material-symbols next-icon text-fg cursor-pointer" :class="nextChapter && !showLoadingState ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
</div> </div>
</div> </div>
<!-- Chapter progress bar -->
<div id="playerTrack" class="absolute left-0 w-full px-6"> <div id="playerTrack" class="absolute left-0 w-full px-6">
<div class="flex pointer-events-none"> <div class="flex pointer-events-none">
<p class="font-mono text-fg" style="font-size: 0.8rem" ref="currentTimestamp">0:00</p> <p class="font-mono text-fg" style="font-size: 0.8rem" ref="currentTimestamp">0:00</p>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="font-mono text-fg" style="font-size: 0.8rem">{{ timeRemainingPretty }}</p> <p class="font-mono text-fg" style="font-size: 0.8rem">{{ timeRemainingPretty }}</p>
</div> </div>
<div ref="track" class="h-1.5 w-full bg-track/50 relative rounded-full" :class="{ 'animate-pulse': isLoading }" @click.stop> <div ref="track" class="h-1.5 w-full bg-track/50 relative rounded-full overflow-hidden" :class="{ 'animate-pulse': showLoadingState }" @click.stop>
<div ref="readyTrack" class="h-full bg-track-buffered absolute top-0 left-0 rounded-full pointer-events-none" /> <div ref="readyTrack" class="h-full bg-track-buffered absolute top-0 left-0 rounded-full pointer-events-none" />
<div ref="bufferedTrack" class="h-full bg-track absolute top-0 left-0 rounded-full pointer-events-none" /> <div ref="bufferedTrack" class="h-full bg-track absolute top-0 left-0 rounded-full pointer-events-none" />
<div ref="playedTrack" class="h-full bg-track-cursor absolute top-0 left-0 rounded-full pointer-events-none" /> <div ref="playedTrack" class="h-full bg-track-cursor absolute top-0 left-0 rounded-full pointer-events-none" />
@@ -99,7 +122,9 @@
</div> </div>
</div> </div>
<!-- Chapters modal - lists chapters for navigation -->
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" :playback-rate="currentPlaybackRate" @select="selectChapter" /> <modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" :playback-rate="currentPlaybackRate" @select="selectChapter" />
<!-- More menu dialog - player settings and options -->
<modals-dialog v-model="showMoreMenuDialog" :items="menuItems" width="80vw" @action="clickMenuAction" /> <modals-dialog v-model="showMoreMenuDialog" :items="menuItems" width="80vw" @action="clickMenuAction" />
</div> </div>
</template> </template>
@@ -108,8 +133,9 @@
import { Capacitor } from '@capacitor/core' import { Capacitor } from '@capacitor/core'
import { AbsAudioPlayer } from '@/plugins/capacitor' import { AbsAudioPlayer } from '@/plugins/capacitor'
import { Dialog } from '@capacitor/dialog' import { Dialog } from '@capacitor/dialog'
import { FastAverageColor } from 'fast-average-color' import { getAverageColorFromCoverUrl } from '@/utils/coverAverageColor'
import WrappingMarquee from '@/assets/WrappingMarquee.js' import WrappingMarquee from '@/assets/WrappingMarquee.js'
import jumpLabelMixin from '@/mixins/jumpLabel'
export default { export default {
props: { props: {
@@ -121,6 +147,7 @@ export default {
sleepTimeRemaining: Number, sleepTimeRemaining: Number,
serverLibraryItemId: String serverLibraryItemId: String
}, },
mixins: [jumpLabelMixin],
data() { data() {
return { return {
windowHeight: 0, windowHeight: 0,
@@ -149,6 +176,7 @@ export default {
lockUi: false lockUi: false
}, },
isLoading: false, isLoading: false,
isCheckingServerProgress: false,
isDraggingCursor: false, isDraggingCursor: false,
draggingTouchStartX: 0, draggingTouchStartX: 0,
draggingTouchStartTime: 0, draggingTouchStartTime: 0,
@@ -221,11 +249,11 @@ export default {
return items return items
}, },
jumpForwardIcon() { jumpForwardLabel() {
return this.$store.getters['globals/getJumpForwardIcon'](this.jumpForwardTime) return this.getJumpLabel(this.jumpForwardTime)
}, },
jumpBackwardsIcon() { jumpBackwardsLabel() {
return this.$store.getters['globals/getJumpBackwardsIcon'](this.jumpBackwardsTime) return this.getJumpLabel(this.jumpBackwardsTime)
}, },
jumpForwardTime() { jumpForwardTime() {
return this.$store.getters['getJumpForwardTime'] return this.$store.getters['getJumpForwardTime']
@@ -262,6 +290,9 @@ export default {
return 190 * heightScale return 190 * heightScale
} }
}, },
showLoadingState() {
return this.isLoading || this.isCheckingServerProgress
},
showCastBtn() { showCastBtn() {
return this.$store.state.isCastAvailable return this.$store.state.isCastAvailable
}, },
@@ -400,17 +431,14 @@ export default {
}, },
async coverImageLoaded(fullCoverUrl) { async coverImageLoaded(fullCoverUrl) {
if (!fullCoverUrl) return if (!fullCoverUrl) return
const avg = await getAverageColorFromCoverUrl(this, fullCoverUrl)
const fac = new FastAverageColor() if (!avg) {
fac this.coverRgb = 'rgb(55, 56, 56)'
.getColorAsync(fullCoverUrl) this.coverBgIsLight = false
.then((color) => { } else {
this.coverRgb = color.rgba this.coverRgb = avg.rgba
this.coverBgIsLight = color.isLight this.coverBgIsLight = avg.isLight
}) }
.catch((e) => {
console.log(e)
})
}, },
clickTitleAndAuthor() { clickTitleAndAuthor() {
if (!this.showFullscreen) return if (!this.showFullscreen) return
@@ -440,9 +468,8 @@ export default {
this.showFullscreen = true this.showFullscreen = true
if (this.titleMarquee) this.titleMarquee.reset() if (this.titleMarquee) this.titleMarquee.reset()
// Update track for total time bar if useChapterTrack is set
this.$nextTick(() => { this.$nextTick(() => {
this.updateTrack() this.measureAndUpdateTrackWidth()
}) })
}, },
collapseFullscreen() { collapseFullscreen() {
@@ -453,13 +480,13 @@ export default {
}, },
async jumpNextChapter() { async jumpNextChapter() {
await this.$hapticsImpact() await this.$hapticsImpact()
if (this.isLoading) return if (this.showLoadingState) return
if (!this.nextChapter) return if (!this.nextChapter) return
this.seek(this.nextChapter.start) this.seek(this.nextChapter.start)
}, },
async jumpChapterStart() { async jumpChapterStart() {
await this.$hapticsImpact() await this.$hapticsImpact()
if (this.isLoading) return if (this.showLoadingState) return
if (!this.currentChapter) { if (!this.currentChapter) {
return this.restart() return this.restart()
} }
@@ -489,12 +516,12 @@ export default {
}, },
async jumpBackwards() { async jumpBackwards() {
await this.$hapticsImpact() await this.$hapticsImpact()
if (this.isLoading) return if (this.showLoadingState) return
AbsAudioPlayer.seekBackward({ value: this.jumpBackwardsTime }) AbsAudioPlayer.seekBackward({ value: this.jumpBackwardsTime })
}, },
async jumpForward() { async jumpForward() {
await this.$hapticsImpact() await this.$hapticsImpact()
if (this.isLoading) return if (this.showLoadingState) return
AbsAudioPlayer.seekForward({ value: this.jumpForwardTime }) AbsAudioPlayer.seekForward({ value: this.jumpForwardTime })
}, },
setStreamReady() { setStreamReady() {
@@ -522,13 +549,17 @@ export default {
this.updateReadyTrack() this.updateReadyTrack()
}, },
updateReadyTrack() { updateReadyTrack() {
if (!this.$refs.readyTrack) return
if (this.playerSettings.useChapterTrack) { if (this.playerSettings.useChapterTrack) {
if (this.$refs.totalReadyTrack) { if (this.$refs.totalReadyTrack && this.trackWidth) {
this.$refs.totalReadyTrack.style.width = this.readyTrackWidth + 'px' const readyPercent = (this.readyTrackWidth / this.trackWidth) * 100
this.$refs.totalReadyTrack.style.width = readyPercent + '%'
} }
this.$refs.readyTrack.style.width = this.trackWidth + 'px' this.$refs.readyTrack.style.width = '100%'
} else { } else if (this.trackWidth) {
this.$refs.readyTrack.style.width = this.readyTrackWidth + 'px' const readyPercent = (this.readyTrackWidth / this.trackWidth) * 100
this.$refs.readyTrack.style.width = readyPercent + '%'
} }
}, },
updateTimestamp() { updateTimestamp() {
@@ -580,25 +611,27 @@ export default {
bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration)) bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration))
} }
const ptWidth = Math.round(percentDone * this.trackWidth) const playedPercent = percentDone * 100
const bufferedPercentWidth = bufferedPercent * 100
if (this.$refs.playedTrack) { if (this.$refs.playedTrack) {
this.$refs.playedTrack.style.width = ptWidth + 'px' this.$refs.playedTrack.style.width = playedPercent + '%'
} }
if (this.$refs.bufferedTrack) { if (this.$refs.bufferedTrack) {
this.$refs.bufferedTrack.style.width = Math.round(bufferedPercent * this.trackWidth) + 'px' this.$refs.bufferedTrack.style.width = bufferedPercentWidth + '%'
} }
if (this.$refs.trackCursor) { if (this.$refs.trackCursor && this.$refs.track) {
this.$refs.trackCursor.style.left = ptWidth - 14 + 'px' const trackWidth = this.$refs.track.clientWidth
this.$refs.trackCursor.style.left = Math.round(percentDone * trackWidth) - 14 + 'px'
} }
if (this.playerSettings.useChapterTrack) { if (this.playerSettings.useChapterTrack) {
if (this.$refs.totalPlayedTrack) this.$refs.totalPlayedTrack.style.width = Math.round(totalPercentDone * this.trackWidth) + 'px' if (this.$refs.totalPlayedTrack) this.$refs.totalPlayedTrack.style.width = totalPercentDone * 100 + '%'
if (this.$refs.totalBufferedTrack) this.$refs.totalBufferedTrack.style.width = Math.round(totalBufferedPercent * this.trackWidth) + 'px' if (this.$refs.totalBufferedTrack) this.$refs.totalBufferedTrack.style.width = totalBufferedPercent * 100 + '%'
} }
}, },
seek(time) { seek(time) {
if (this.isLoading) return if (this.showLoadingState) return
if (this.seekLoading) { if (this.seekLoading) {
console.error('Already seek loading', this.seekedTime) console.error('Already seek loading', this.seekedTime)
return return
@@ -607,12 +640,12 @@ export default {
this.seekedTime = time this.seekedTime = time
this.seekLoading = true this.seekLoading = true
AbsAudioPlayer.seek({ value: Math.floor(time) }) // Pass fractional seconds so seeks to non-integer chapter starts don't truncate
AbsAudioPlayer.seek({ value: time })
if (this.$refs.playedTrack) { if (this.$refs.playedTrack) {
const perc = time / this.totalDuration const perc = time / this.totalDuration
const ptWidth = Math.round(perc * this.trackWidth) this.$refs.playedTrack.style.width = perc * 100 + '%'
this.$refs.playedTrack.style.width = ptWidth + 'px'
this.$refs.playedTrack.classList.remove('bg-gray-200') this.$refs.playedTrack.classList.remove('bg-gray-200')
this.$refs.playedTrack.classList.add('bg-yellow-300') this.$refs.playedTrack.classList.add('bg-yellow-300')
@@ -630,11 +663,14 @@ export default {
}, },
async playPauseClick() { async playPauseClick() {
await this.$hapticsImpact() await this.$hapticsImpact()
if (this.isLoading) return if (this.showLoadingState) return
this.isPlaying = !!((await AbsAudioPlayer.playPause()) || {}).playing this.isPlaying = !!((await AbsAudioPlayer.playPause()) || {}).playing
this.isEnded = false this.isEnded = false
}, },
setIsCheckingServerProgress(value) {
this.isCheckingServerProgress = !!value
},
play() { play() {
AbsAudioPlayer.playPlayer() AbsAudioPlayer.playPlayer()
this.startPlayInterval() this.startPlayInterval()
@@ -718,7 +754,8 @@ export default {
maxTime = minTime + duration maxTime = minTime + duration
} }
const timePerPixel = duration / this.trackWidth const trackWidth = this.$refs.track?.clientWidth || this.trackWidth
const timePerPixel = duration / trackWidth
const newTime = this.draggingTouchStartTime + timePerPixel * distanceMoved const newTime = this.draggingTouchStartTime + timePerPixel * distanceMoved
this.draggingCurrentTime = Math.min(maxTime, Math.max(minTime, newTime)) this.draggingCurrentTime = Math.min(maxTime, Math.max(minTime, newTime))
@@ -856,11 +893,7 @@ export default {
this.titleMarquee = new WrappingMarquee(this.$refs.titlewrapper) this.titleMarquee = new WrappingMarquee(this.$refs.titlewrapper)
this.titleMarquee.init(this.title) this.titleMarquee.init(this.title)
if (this.$refs.track) { this.measureAndUpdateTrackWidth()
this.trackWidth = this.$refs.track.clientWidth
} else {
console.error('Track not loaded', this.$refs)
}
}) })
}, },
onPlaybackClosed() { onPlaybackClosed() {
@@ -906,15 +939,26 @@ export default {
} }
} }
// playerContainer width transition is 150ms, remeasure after layout settles
await new Promise((resolve) => setTimeout(resolve, 200))
this.refreshUI()
this.isRefreshingUI = false this.isRefreshingUI = false
}, },
measureAndUpdateTrackWidth() {
if (!this.$refs.track) {
console.error('Track not loaded', this.$refs)
return
}
this.trackWidth = this.$refs.track.clientWidth
this.updateTrack()
this.updateReadyTrack()
},
refreshUI() { refreshUI() {
this.updateScreenSize() this.updateScreenSize()
if (this.$refs.track) { this.$nextTick(() => {
this.trackWidth = this.$refs.track.clientWidth this.measureAndUpdateTrackWidth()
this.updateTrack() })
this.updateReadyTrack()
}
}, },
updateScreenSize() { updateScreenSize() {
setTimeout(() => { setTimeout(() => {
@@ -1005,6 +1049,12 @@ export default {
.fullscreen .playerContainer { .fullscreen .playerContainer {
height: 200px; height: 200px;
} }
@media (orientation: landscape) {
.fullscreen .playerContainer {
width: 50%;
left: 50%;
}
}
#playerContent { #playerContent {
box-shadow: 0px -8px 8px #11111155; box-shadow: 0px -8px 8px #11111155;
} }
@@ -1016,6 +1066,7 @@ export default {
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1); transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
transition-property: margin; transition-property: margin;
bottom: 35px; bottom: 35px;
max-width: 100%;
} }
.fullscreen #playerTrack { .fullscreen #playerTrack {
bottom: unset; bottom: unset;
@@ -1032,13 +1083,45 @@ export default {
border-radius: 3px; border-radius: 3px;
overflow: hidden; overflow: hidden;
} }
@media (orientation: landscape) {
.fullscreen .cover-wrapper {
left: 25px !important;
top: 50px;
width: calc(50% - 25px) !important;
height: calc(100% - 85px) !important;
}
.fullscreen .cover-wrapper > div > div {
width: 100% !important;
height: 100% !important;
max-width: 100% !important;
max-height: 100% !important;
}
.fullscreen .cover-wrapper > div > div > div {
background-color: transparent;
}
.fullscreen .cover-wrapper img {
object-fit: contain;
}
}
.total-track { .total-track {
bottom: 215px; bottom: 215px;
left: 0; left: 0;
right: 0; right: 0;
} }
@media (orientation: landscape) {
.fullscreen .total-track {
left: 50%;
width: 50%;
}
}
@media (orientation: landscape) {
.fullscreen .title-author-texts {
left: 50% !important;
width: 50% !important;
}
}
.title-author-texts { .title-author-texts {
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1); transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
transition-property: left, bottom, width, height; transition-property: left, bottom, width, height;
@@ -1055,12 +1138,22 @@ export default {
font-size: 0.85rem; font-size: 0.85rem;
line-height: 1.5; line-height: 1.5;
} }
@media (orientation: landscape) {
.fullscreen .title-author-texts .title-text {
font-size: larger !important;
}
}
.title-author-texts .author-text { .title-author-texts .author-text {
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1); transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
transition-property: font-size; transition-property: font-size;
font-size: 0.75rem; font-size: 0.75rem;
line-height: 1.2; line-height: 1.2;
} }
@media (orientation: landscape) {
.fullscreen .title-author-texts .author-text {
font-size: inherit !important;
}
}
.fullscreen .title-author-texts { .fullscreen .title-author-texts {
bottom: calc(50% - var(--cover-image-height) / 2 + 50px); bottom: calc(50% - var(--cover-image-height) / 2 + 50px);
@@ -1091,6 +1184,9 @@ export default {
margin: 0px 0px; margin: 0px 0px;
font-size: 1.6rem; font-size: 1.6rem;
} }
#playerControls .jump-label {
margin-top: 2px;
}
#playerControls .play-btn { #playerControls .play-btn {
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1); transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
transition-property: padding, margin, height, width, min-width, min-height; transition-property: padding, margin, height, width, min-width, min-height;
+126 -40
View File
@@ -48,6 +48,9 @@ export default {
}, },
isIos() { isIos() {
return this.$platform === 'ios' return this.$platform === 'ios'
},
currentPlaybackSession() {
return this.$store.state.currentPlaybackSession
} }
}, },
methods: { methods: {
@@ -226,7 +229,7 @@ export default {
console.log('Already streaming item', startTime) console.log('Already streaming item', startTime)
if (startTime !== undefined && startTime !== null) { if (startTime !== undefined && startTime !== null) {
// seek to start time // seek to start time
AbsAudioPlayer.seek({ value: Math.floor(startTime) }) AbsAudioPlayer.seek({ value: startTime })
} else if (this.$refs.audioPlayer) { } else if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.play() this.$refs.audioPlayer.play()
} }
@@ -304,48 +307,129 @@ export default {
this.$refs.audioPlayer?.seek(currentTime) this.$refs.audioPlayer?.seek(currentTime)
}, },
/** /**
* When device gains focus then refresh the timestamps in the audio player * Fetch the current user's media progress from the server for a given library item / episode.
* Returns the server media progress object, or null if the request fails, times out, or the
* response doesn't match the requested library item.
*
* The audio player's loading state is shown while the request is in flight so the user
* doesn't tap play before we have a chance to update the timestamps. The request timeout
* is 7 seconds so a slow/unresponsive server doesn't block the user for long.
*/ */
deviceFocused(hasFocus) { async getServerMediaProgressForCurrentSession() {
if (!this.$store.state.currentPlaybackSession) return if (!this.$store.state.user.user || !this.$store.state.networkConnected) return null
const libraryItemId = this.currentPlaybackSession?.libraryItemId
const episodeId = this.currentPlaybackSession?.episodeId
if (!libraryItemId) return null
if (hasFocus) { if (this.$refs.audioPlayer?.isCheckingServerProgress) {
console.log('[AudioPlayerContainer] getServerMediaProgressForCurrentSession: already checking server progress')
return null
}
const url = episodeId ? `/api/me/progress/${libraryItemId}/${episodeId}` : `/api/me/progress/${libraryItemId}`
this.$refs.audioPlayer?.setIsCheckingServerProgress(true)
try {
const data = await this.$nativeHttp.get(url, { connectTimeout: 7000, readTimeout: 7000 })
if (!data || data.libraryItemId !== libraryItemId) return null
return data
} catch (error) {
console.error('[AudioPlayerContainer] Failed to get server media progress', error)
return null
} finally {
this.$refs.audioPlayer?.setIsCheckingServerProgress(false)
}
},
getLocalMediaProgressForCurrentSession() {
if (!this.currentPlaybackSession) return null
return this.$store.getters['globals/getLocalMediaProgressById'](this.currentPlaybackSession.localLibraryItem?.id, this.currentPlaybackSession.localEpisodeId)
},
/**
* Sync the server media progress with the local media progress
*/
async syncServerMediaProgressWithLocalMediaProgress(localMediaProgressId, serverMediaProgress) {
try {
const newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress({
localMediaProgressId,
mediaProgress: serverMediaProgress
})
if (newLocalMediaProgress?.id) {
this.$store.commit('globals/updateLocalMediaProgress', newLocalMediaProgress)
}
} catch (error) {
console.error('[AudioPlayerContainer] Failed to sync server progress with local media progress', error)
}
},
/**
* Check if the server media progress is more recent than the local media progress and sync if so
*/
async checkSyncServerProgressWithLocalProgress(localMediaProgress) {
if (!localMediaProgress) return
console.log('[AudioPlayerContainer] checkSyncServerProgressWithLocalProgress: checking server media progress for local media item open in player')
const serverMediaProgress = await this.getServerMediaProgressForCurrentSession()
if (!serverMediaProgress?.lastUpdate || serverMediaProgress.lastUpdate <= localMediaProgress.lastUpdate) return
console.log('[AudioPlayerContainer] checkSyncServerProgressWithLocalProgress: server progress is more recent than local progress. Server current time:', serverMediaProgress.currentTime, 'vs local', localMediaProgress.currentTime, `(server lastUpdate=${serverMediaProgress.lastUpdate} > local lastUpdate=${localMediaProgress.lastUpdate})`)
if (!this.$refs.audioPlayer?.isPlaying && serverMediaProgress.currentTime !== localMediaProgress.currentTime) {
// Use seek() so the native audio player's current session is updated
this.$refs.audioPlayer.seek(serverMediaProgress.currentTime)
}
await this.syncServerMediaProgressWithLocalMediaProgress(localMediaProgress.id, serverMediaProgress)
},
/**
* When socket is reconnected after a delay, if a local media item is open in the player (paused)
* we fetch the server media progress and sync it if it is more recent than the local progress
*
* If there is no socket connection we may have missed external progress updates
*/
async socketReconnected() {
if (!this.currentPlaybackSession) return
// dont update timestamps if player is playing
if (this.$refs.audioPlayer?.isPlaying) return
if (this.$refs.audioPlayer.isLocalPlayMethod) {
const localMediaProgress = this.getLocalMediaProgressForCurrentSession()
if (!localMediaProgress) {
console.error('[AudioPlayerContainer] socket reconnected: Local media progress not found')
return
}
await this.checkSyncServerProgressWithLocalProgress(localMediaProgress)
}
},
/**
* When device re-gains focus then refresh the timestamps in the audio player
* if local item is open then fetch the server media progress and update if more recent
*/
async deviceFocused(hasFocus) {
if (!this.currentPlaybackSession || !hasFocus) return
// dont update timestamps if player is playing
if (this.$refs.audioPlayer?.isPlaying) return
if (this.$refs.audioPlayer.isLocalPlayMethod) {
const localMediaProgress = this.getLocalMediaProgressForCurrentSession()
if (!localMediaProgress) {
console.error('[AudioPlayerContainer] device visibility: Local media progress not found')
return
}
console.log('[AudioPlayerContainer] device visibility: found local media progress', localMediaProgress.currentTime, 'last time in player is', this.currentTime)
this.$refs.audioPlayer.currentTime = localMediaProgress.currentTime
this.$refs.audioPlayer.timeupdate()
await this.checkSyncServerProgressWithLocalProgress(localMediaProgress)
} else {
// server item so fetch server media progress and update player time
console.log('[AudioPlayerContainer] device visibility: checking server media progress for server media item open in player')
const data = await this.getServerMediaProgressForCurrentSession()
if (!data) return
if (!this.$refs.audioPlayer?.isPlaying) { if (!this.$refs.audioPlayer?.isPlaying) {
const playbackSession = this.$store.state.currentPlaybackSession console.log('[AudioPlayerContainer] device visibility: got server media progress', data.currentTime, 'last time in player is', this.currentTime)
if (this.$refs.audioPlayer.isLocalPlayMethod) { // Only seek if the difference is greater than 1 second
const localLibraryItemId = playbackSession.localLibraryItem?.id if (Math.abs(data.currentTime - this.currentTime) > 1) {
const localEpisodeId = playbackSession.localEpisodeId // Use seek() so the native audio player's current session is updated
if (!localLibraryItemId) { this.$refs.audioPlayer.seek(data.currentTime)
console.error('[AudioPlayerContainer] device visibility: no local library item for session', JSON.stringify(playbackSession))
return
}
const localMediaProgress = this.$store.state.globals.localMediaProgress.find((mp) => {
if (localEpisodeId) return mp.localEpisodeId === localEpisodeId
return mp.localLibraryItemId === localLibraryItemId
})
if (localMediaProgress) {
console.log('[AudioPlayerContainer] device visibility: found local media progress', localMediaProgress.currentTime, 'last time in player is', this.currentTime)
this.$refs.audioPlayer.currentTime = localMediaProgress.currentTime
this.$refs.audioPlayer.timeupdate()
} else {
console.error('[AudioPlayerContainer] device visibility: Local media progress not found')
}
} else {
const libraryItemId = playbackSession.libraryItemId
const episodeId = playbackSession.episodeId
const url = episodeId ? `/api/me/progress/${libraryItemId}/${episodeId}` : `/api/me/progress/${libraryItemId}`
this.$nativeHttp
.get(url)
.then((data) => {
if (!this.$refs.audioPlayer?.isPlaying && data.libraryItemId === libraryItemId) {
console.log('[AudioPlayerContainer] device visibility: got server media progress', data.currentTime, 'last time in player is', this.currentTime)
this.$refs.audioPlayer.currentTime = data.currentTime
this.$refs.audioPlayer.timeupdate()
}
})
.catch((error) => {
console.error('[AudioPlayerContainer] device visibility: Failed to get progress', error)
})
} }
} }
} }
@@ -368,6 +452,7 @@ export default {
this.$eventBus.$on('user-settings', this.settingsUpdated) this.$eventBus.$on('user-settings', this.settingsUpdated)
this.$eventBus.$on('playback-time-update', this.playbackTimeUpdate) this.$eventBus.$on('playback-time-update', this.playbackTimeUpdate)
this.$eventBus.$on('device-focus-update', this.deviceFocused) this.$eventBus.$on('device-focus-update', this.deviceFocused)
this.$eventBus.$on('socket-reconnected', this.socketReconnected)
}, },
beforeDestroy() { beforeDestroy() {
this.onLocalMediaProgressUpdateListener?.remove() this.onLocalMediaProgressUpdateListener?.remove()
@@ -383,6 +468,7 @@ export default {
this.$eventBus.$off('user-settings', this.settingsUpdated) this.$eventBus.$off('user-settings', this.settingsUpdated)
this.$eventBus.$off('playback-time-update', this.playbackTimeUpdate) this.$eventBus.$off('playback-time-update', this.playbackTimeUpdate)
this.$eventBus.$off('device-focus-update', this.deviceFocused) this.$eventBus.$off('device-focus-update', this.deviceFocused)
this.$eventBus.$off('socket-reconnected', this.socketReconnected)
} }
} }
</script> </script>
+1 -4
View File
@@ -3,10 +3,7 @@
<div class="absolute top-0 left-0 w-full h-full bg-black transition-opacity duration-200" :class="show ? 'bg-opacity-60 pointer-events-auto' : 'bg-opacity-0'" @click="clickBackground" /> <div class="absolute top-0 left-0 w-full h-full bg-black transition-opacity duration-200" :class="show ? 'bg-opacity-60 pointer-events-auto' : 'bg-opacity-0'" @click="clickBackground" />
<div class="absolute top-0 right-0 w-64 h-full bg-bg transform transition-transform py-6 pointer-events-auto" :class="show ? '' : 'translate-x-64'" @click.stop> <div class="absolute top-0 right-0 w-64 h-full bg-bg transform transition-transform py-6 pointer-events-auto" :class="show ? '' : 'translate-x-64'" @click.stop>
<div class="px-6 mb-4"> <div class="px-6 mb-4">
<p v-if="user" class="text-base"> <p v-if="user" class="text-base" v-html="$getString('HeaderWelcome', [username])" />
Welcome,
<strong>{{ username }}</strong>
</p>
</div> </div>
<div class="w-full overflow-y-auto"> <div class="w-full overflow-y-auto">
+69
View File
@@ -0,0 +1,69 @@
<template>
<div class="flex h-full px-1 overflow-hidden">
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div class="grow px-2 audiobookSearchCardContent">
<p class="truncate text-sm">{{ title }}</p>
<p v-if="subtitle" class="truncate text-xs text-gray-300">{{ subtitle }}</p>
<p class="text-xs text-gray-200 truncate">{{ $getString('LabelByAuthor', [authorName]) }}</p>
</div>
</div>
</template>
<script>
export default {
props: {
episode: {
type: Object,
default: () => {}
},
libraryItem: {
type: Object,
default: () => {}
}
},
data() {
return {}
},
computed: {
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
coverWidth() {
if (this.bookCoverAspectRatio === 1) return 50 * 1.2
return 50
},
media() {
return this.libraryItem ? this.libraryItem.media || {} : {}
},
mediaMetadata() {
return this.media.metadata || {}
},
mediaType() {
return this.libraryItem ? this.libraryItem.mediaType : null
},
isPodcast() {
return this.mediaType === 'podcast'
},
title() {
return this.episode?.title || 'No Title'
},
subtitle() {
return this.mediaMetadata.title
},
authorName() {
return this.mediaMetadata.author
}
},
methods: {},
mounted() {}
}
</script>
<style>
.audiobookSearchCardContent {
width: calc(100% - 80px);
display: flex;
flex-direction: column;
justify-content: center;
}
</style>
+6 -43
View File
@@ -1,16 +1,10 @@
<template> <template>
<div class="flex h-full px-1 overflow-hidden"> <div class="flex h-full px-1 overflow-hidden">
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div class="flex-grow px-2 audiobookSearchCardContent"> <div class="grow px-2 audiobookSearchCardContent">
<p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p> <p class="truncate text-sm">{{ title }}</p>
<p v-else class="truncate text-sm" v-html="matchHtml" /> <p v-if="subtitle" class="truncate text-xs text-gray-300">{{ subtitle }}</p>
<p class="text-xs text-gray-200 truncate">{{ $getString('LabelByAuthor', [authorName]) }}</p>
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-fg-muted">{{ matchHtml }}</p>
<p v-if="matchKey !== 'authors'" class="text-xs text-fg truncate">by {{ authorName }}</p>
<p v-else class="truncate text-xs text-fg" v-html="matchHtml" />
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin' || matchKey === 'episode' || matchKey === 'narrators'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
</div> </div>
</div> </div>
</template> </template>
@@ -22,9 +16,7 @@ export default {
type: Object, type: Object,
default: () => {} default: () => {}
}, },
search: String, search: String
matchKey: String,
matchText: String
}, },
data() { data() {
return {} return {}
@@ -58,35 +50,6 @@ export default {
authorName() { authorName() {
if (this.isPodcast) return this.mediaMetadata.author if (this.isPodcast) return this.mediaMetadata.author
return this.mediaMetadata.authorName return this.mediaMetadata.authorName
},
matchHtml() {
if (!this.matchText || !this.search) return ''
if (this.matchKey === 'subtitle') return ''
var matchSplit = this.matchText.toLowerCase().split(this.search.toLowerCase().trim())
if (matchSplit.length < 2) return ''
var html = ''
var totalLenSoFar = 0
for (let i = 0; i < matchSplit.length - 1; i++) {
var indexOf = matchSplit[i].length
var firstPart = this.matchText.substr(totalLenSoFar, indexOf)
var actualWasThere = this.matchText.substr(totalLenSoFar + indexOf, this.search.length)
totalLenSoFar += indexOf + this.search.length
html += `${firstPart}<strong class="text-warning">${actualWasThere}</strong>`
}
var lastPart = this.matchText.substr(totalLenSoFar)
html += lastPart
if (this.matchKey === 'episode') return `<p class="truncate">Episode: ${html}</p>`
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
if (this.matchKey === 'subtitle') return `<p class="truncate">${html}</p>`
if (this.matchKey === 'authors') return `by ${html}`
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
if (this.matchKey === 'series') return `<p class="truncate">Series: ${html}</p>`
if (this.matchKey === 'narrators') return `<p class="truncate">Narrator: ${html}</p>`
return `${html}`
} }
}, },
methods: {}, methods: {},
@@ -101,4 +64,4 @@ export default {
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
} }
</style> </style>
+6 -5
View File
@@ -483,8 +483,8 @@ export default {
await this.$hapticsImpact() await this.$hapticsImpact()
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: `Remove this server config?` message: this.$strings.MessageConfirmDeleteServerConfig
}) })
if (value) { if (value) {
this.processing = true this.processing = true
@@ -843,7 +843,7 @@ export default {
async setUserAndConnection({ user, userDefaultLibraryId, serverSettings, ereaderDevices }) { async setUserAndConnection({ user, userDefaultLibraryId, serverSettings, ereaderDevices }) {
if (!user) return if (!user) return
console.log('Successfully logged in', JSON.stringify(user)) console.log('Successfully logged in: ' + user.username)
this.$store.commit('setServerSettings', serverSettings) this.$store.commit('setServerSettings', serverSettings)
this.$store.commit('libraries/setEReaderDevices', ereaderDevices) this.$store.commit('libraries/setEReaderDevices', ereaderDevices)
@@ -951,8 +951,9 @@ export default {
init() { init() {
if (this.$route.query.serverConnectionConfigId) { if (this.$route.query.serverConnectionConfigId) {
// Handle force re-login for servers using new JWT auth but still using an old token OR refresh token failed // Handle force re-login for servers using new JWT auth but still using an old token OR refresh token failed
this.serverConfig = this.serverConnectionConfigs.find((scc) => scc.id === this.$route.query.serverConnectionConfigId) const _serverConfig = this.serverConnectionConfigs.find((scc) => scc.id === this.$route.query.serverConnectionConfigId)
if (this.serverConfig) { if (_serverConfig) {
this.serverConfig = { ..._serverConfig }
this.setForceRelogin(this.$route.query.error) this.setForceRelogin(this.$route.query.error)
return return
} else { } else {
+4 -4
View File
@@ -303,7 +303,7 @@ export default {
// Show confirm if item has progress since it will reset // Show confirm if item has progress since it will reset
if (this.userItemProgress && this.userItemProgress.progress > 0 && !this.userIsFinished) { if (this.userItemProgress && this.userItemProgress.progress > 0 && !this.userIsFinished) {
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: this.$strings.MessageConfirmMarkAsFinished message: this.$strings.MessageConfirmMarkAsFinished
}) })
if (!value) return if (!value) return
@@ -369,7 +369,7 @@ export default {
await this.$hapticsImpact() await this.$hapticsImpact()
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: this.$strings.MessageConfirmDiscardProgress message: this.$strings.MessageConfirmDiscardProgress
}) })
if (value) { if (value) {
@@ -407,7 +407,7 @@ export default {
} }
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: this.$getString('MessageConfirmDeleteLocalEpisode', [localFile.basePath]) message: this.$getString('MessageConfirmDeleteLocalEpisode', [localFile.basePath])
}) })
if (value) { if (value) {
@@ -432,7 +432,7 @@ export default {
await this.$hapticsImpact() await this.$hapticsImpact()
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: this.$strings.MessageConfirmDeleteLocalFiles message: this.$strings.MessageConfirmDeleteLocalFiles
}) })
if (value) { if (value) {
+13 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<modals-modal v-model="show" width="90%"> <modals-modal v-model="show" width="90%">
<div class="w-full h-full bg-primary rounded-lg border border-fg/20"> <div class="w-full h-full bg-primary rounded-lg border border-fg/20">
<ul class="w-full rounded-lg text-base" role="listbox" aria-labelledby="listbox-label"> <ul class="w-full rounded-lg text-base max-h-[70vh] overflow-y-auto overscroll-contain" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in items"> <template v-for="item in items">
<li :key="item.value" class="text-fg select-none relative py-4 pr-9 cursor-pointer" :class="item.value === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedOption(item.value)"> <li :key="item.value" class="text-fg select-none relative py-4 pr-9 cursor-pointer" :class="item.value === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedOption(item.value)">
<div class="flex items-center"> <div class="flex items-center">
@@ -64,6 +64,18 @@ export default {
text: this.$strings.LabelFileModified, text: this.$strings.LabelFileModified,
value: 'mtimeMs' value: 'mtimeMs'
}, },
{
text: this.$strings.LabelLibrarySortByProgress,
value: 'progress'
},
{
text: this.$strings.LabelLibrarySortByProgressStarted,
value: 'progress.createdAt'
},
{
text: this.$strings.LabelLibrarySortByProgressFinished,
value: 'progress.finishedAt'
},
{ {
text: this.$strings.LabelRandomly, text: this.$strings.LabelRandomly,
value: 'random' value: 'random'
+1 -1
View File
@@ -108,7 +108,7 @@ export default {
async cancelSleepTimer() { async cancelSleepTimer() {
if (this.isAuto) { if (this.isAuto) {
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: this.$strings.MessageConfirmDisableAutoTimer message: this.$strings.MessageConfirmDisableAutoTimer
}) })
if (!value) return if (!value) return
+25 -8
View File
@@ -79,9 +79,6 @@ export default {
episodesCopy: [], episodesCopy: [],
showFiltersModal: false, showFiltersModal: false,
showSortModal: false, showSortModal: false,
sortKey: 'publishedAt',
sortDesc: true,
filterKey: 'incomplete',
fetchingRSSFeed: false, fetchingRSSFeed: false,
podcastFeedEpisodes: [], podcastFeedEpisodes: [],
showPodcastEpisodeFeed: false, showPodcastEpisodeFeed: false,
@@ -223,13 +220,34 @@ export default {
if (!this.sortKey) return '' if (!this.sortKey) return ''
const _sel = this.episodeSortItems.find((i) => i.value === this.sortKey) const _sel = this.episodeSortItems.find((i) => i.value === this.sortKey)
return _sel?.text || '' return _sel?.text || ''
},
filterKey() {
return this.$store.getters['user/getUserSetting']('podcastEpisodesFilterBy') || 'incomplete'
},
sortKey: {
get() {
return this.$store.getters['user/getUserSetting']('podcastEpisodesOrderBy') || 'publishedAt'
},
set(val) {
this.$store.dispatch('user/updateUserSettings', { podcastEpisodesOrderBy: val })
}
},
sortDesc: {
get() {
const desc = this.$store.getters['user/getUserSetting']('podcastEpisodesOrderDesc')
if (desc == null) return this.mediaMetadata.type === 'episodic'
return desc
},
set(val) {
this.$store.dispatch('user/updateUserSettings', { podcastEpisodesOrderDesc: val })
}
} }
}, },
methods: { methods: {
async clearDownloadQueue() { async clearDownloadQueue() {
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: `Are you sure you want to clear episode download queue?` message: this.$strings.MessageConfirmDeleteEpisodeDownloadQueue
}) })
if (value) { if (value) {
@@ -277,8 +295,8 @@ export default {
this.$store.commit('globals/setShowPlaylistsAddCreateModal', true) this.$store.commit('globals/setShowPlaylistsAddCreateModal', true)
}, },
setFilter(filter) { setFilter(filter) {
this.filterKey = filter
this.showFiltersModal = false this.showFiltersModal = false
this.$store.dispatch('user/updateUserSettings', { podcastEpisodesFilterBy: filter })
}, },
showFilters() { showFilters() {
this.showFiltersModal = true this.showFiltersModal = true
@@ -291,7 +309,6 @@ export default {
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, episode.id) return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, episode.id)
}, },
init() { init() {
this.sortDesc = this.mediaMetadata.type === 'episodic'
this.episodesCopy = this.episodes.map((ep) => { this.episodesCopy = this.episodes.map((ep) => {
return { ...ep } return { ...ep }
}) })
@@ -316,7 +333,7 @@ export default {
}, },
mounted() { mounted() {
if (this.$route.query['episodefilter'] === 'downloaded') { if (this.$route.query['episodefilter'] === 'downloaded') {
this.filterKey = 'downloaded' this.$store.dispatch('user/updateUserSettings', { podcastEpisodesFilterBy: 'downloaded' })
} }
this.$socket.$on('episode_download_queued', this.episodeDownloadQueued) this.$socket.$on('episode_download_queued', this.episodeDownloadQueued)
this.$socket.$on('episode_download_started', this.episodeDownloadStarted) this.$socket.$on('episode_download_started', this.episodeDownloadStarted)
@@ -12,7 +12,8 @@ export default {
return { return {
downloadItemListener: null, downloadItemListener: null,
completeListener: null, completeListener: null,
itemPartUpdateListener: null itemPartUpdateListener: null,
queueChangedListener: null
} }
}, },
computed: { computed: {
@@ -76,17 +77,22 @@ export default {
}, },
onDownloadItemPartUpdate(itemPart) { onDownloadItemPartUpdate(itemPart) {
this.$store.commit('globals/updateDownloadItemPart', itemPart) this.$store.commit('globals/updateDownloadItemPart', itemPart)
},
onQueueChanged(data) {
if (!data.hasWork) this.$store.commit('globals/clearItemDownloads')
} }
}, },
async mounted() { async mounted() {
this.downloadItemListener = await AbsDownloader.addListener('onDownloadItem', (data) => this.onDownloadItem(data)) this.downloadItemListener = await AbsDownloader.addListener('onDownloadItem', (data) => this.onDownloadItem(data))
this.itemPartUpdateListener = await AbsDownloader.addListener('onDownloadItemPartUpdate', (data) => this.onDownloadItemPartUpdate(data)) this.itemPartUpdateListener = await AbsDownloader.addListener('onDownloadItemPartUpdate', (data) => this.onDownloadItemPartUpdate(data))
this.queueChangedListener = await AbsDownloader.addListener('onQueueChanged', (data) => this.onQueueChanged(data))
this.completeListener = await AbsDownloader.addListener('onItemDownloadComplete', (data) => this.onItemDownloadComplete(data)) this.completeListener = await AbsDownloader.addListener('onItemDownloadComplete', (data) => this.onItemDownloadComplete(data))
}, },
beforeDestroy() { beforeDestroy() {
this.downloadItemListener?.remove() this.downloadItemListener?.remove()
this.completeListener?.remove() this.completeListener?.remove()
this.itemPartUpdateListener?.remove() this.itemPartUpdateListener?.remove()
this.queueChangedListener?.remove()
} }
} }
</script> </script>
+4 -4
View File
@@ -740,12 +740,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons; ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 41; CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_TEAM = 7UFJ7D8V6A; DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.11.0; MARKETING_VERSION = 0.13.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev; PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -764,12 +764,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons; ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 41; CURRENT_PROJECT_VERSION = 43;
DEVELOPMENT_TEAM = 7UFJ7D8V6A; DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.11.0; MARKETING_VERSION = 0.13.0;
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app; PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
+99 -33
View File
@@ -28,32 +28,70 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
private let progressStatusQueue = DispatchQueue(label: "progress-status-queue", attributes: .concurrent) private let progressStatusQueue = DispatchQueue(label: "progress-status-queue", attributes: .concurrent)
private var downloadItemProgress = [String: DownloadItem]() private var downloadItemProgress = [String: DownloadItem]()
private var monitoringProgressTimer: Timer? private var monitoringProgressTimer: Timer?
// Download queue management
private let downloadQueueLock = NSLock()
private var pendingDownloadTasks: [DownloadItemPartTask] = []
private var activeDownloadTasks: Set<String> = [] // Track active task IDs
private let maxConcurrentDownloads = 3
// MARK: - Download Queue Management
private func startNextDownloadInQueue() {
downloadQueueLock.lock()
defer { downloadQueueLock.unlock() }
// Start downloads up to the max concurrent limit
while activeDownloadTasks.count < maxConcurrentDownloads && !pendingDownloadTasks.isEmpty {
let nextTask = pendingDownloadTasks.removeFirst()
activeDownloadTasks.insert(nextTask.partId)
AbsLogger.info(message: "Starting download for \(nextTask.filename) (\(activeDownloadTasks.count)/\(maxConcurrentDownloads) active, \(pendingDownloadTasks.count) pending)")
nextTask.task.resume()
}
}
private func markDownloadTaskCompleted(_ taskId: String) {
downloadQueueLock.lock()
activeDownloadTasks.remove(taskId)
downloadQueueLock.unlock()
// Try to start the next download
startNextDownloadInQueue()
}
// MARK: - Progress handling // MARK: - Progress handling
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in handleDownloadTaskUpdate(downloadTask: downloadTask) { downloadItem, downloadItemPart in
let realm = try Realm() let realm = try Realm()
let partId = downloadItemPart.id
// Get fresh reference to the object in this realm
guard let liveDownloadItemPart = realm.object(ofType: DownloadItemPart.self, forPrimaryKey: partId) else {
throw LibraryItemDownloadError.downloadItemPartNotFound
}
try realm.write { try realm.write {
downloadItemPart.bytesDownloaded = downloadItemPart.fileSize liveDownloadItemPart.bytesDownloaded = liveDownloadItemPart.fileSize
downloadItemPart.progress = 100 liveDownloadItemPart.progress = 100
downloadItemPart.completed = true liveDownloadItemPart.completed = true
} }
do { do {
// Move the downloaded file into place // Move the downloaded file into place
guard let destinationUrl = downloadItemPart.destinationURL else { guard let destinationUrl = liveDownloadItemPart.destinationURL else {
throw LibraryItemDownloadError.downloadItemPartDestinationUrlNotDefined throw LibraryItemDownloadError.downloadItemPartDestinationUrlNotDefined
} }
try? FileManager.default.removeItem(at: destinationUrl) try? FileManager.default.removeItem(at: destinationUrl)
try FileManager.default.moveItem(at: location, to: destinationUrl) try FileManager.default.moveItem(at: location, to: destinationUrl)
try realm.write { try realm.write {
downloadItemPart.moved = true liveDownloadItemPart.moved = true
} }
} catch { } catch {
try realm.write { try realm.write {
downloadItemPart.failed = true liveDownloadItemPart.failed = true
} }
throw error throw error
} }
@@ -63,13 +101,26 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
handleDownloadTaskUpdate(downloadTask: task) { downloadItem, downloadItemPart in handleDownloadTaskUpdate(downloadTask: task) { downloadItem, downloadItemPart in
if let error = error { if let error = error {
try Realm().write { let realm = try Realm()
downloadItemPart.completed = true let partId = downloadItemPart.id
downloadItemPart.failed = true
// Get fresh reference to the object in this realm
guard let liveDownloadItemPart = realm.object(ofType: DownloadItemPart.self, forPrimaryKey: partId) else {
throw LibraryItemDownloadError.downloadItemPartNotFound
}
try realm.write {
liveDownloadItemPart.completed = true
liveDownloadItemPart.failed = true
} }
throw error throw error
} }
} }
// Mark this task as completed and start the next download in queue
if let taskId = task.taskDescription {
markDownloadTaskCompleted(taskId)
}
} }
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
@@ -79,9 +130,17 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
// Only update the progress if we received accurate progress data // Only update the progress if we received accurate progress data
if percentDownloaded >= 0.0 && percentDownloaded <= 100.0 { if percentDownloaded >= 0.0 && percentDownloaded <= 100.0 {
try Realm().write { let realm = try Realm()
downloadItemPart.bytesDownloaded = Double(totalBytesWritten) let partId = downloadItemPart.id
downloadItemPart.progress = percentDownloaded
// Get fresh reference to the object in this realm
guard let liveDownloadItemPart = realm.object(ofType: DownloadItemPart.self, forPrimaryKey: partId) else {
throw LibraryItemDownloadError.downloadItemPartNotFound
}
try realm.write {
liveDownloadItemPart.bytesDownloaded = Double(totalBytesWritten)
liveDownloadItemPart.progress = percentDownloaded
} }
} }
} }
@@ -307,32 +366,37 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
// Persist in the database before status start coming in // Persist in the database before status start coming in
try Database.shared.saveDownloadItem(downloadItem) try Database.shared.saveDownloadItem(downloadItem)
// Start all the downloads // Add all tasks to the download queue
for task in tasks { downloadQueueLock.lock()
task.task.resume() pendingDownloadTasks.append(contentsOf: tasks)
} downloadQueueLock.unlock()
AbsLogger.info(message: "Added \(tasks.count) tasks to download queue. Starting downloads...")
// Start downloading (up to maxConcurrentDownloads at a time)
startNextDownloadInQueue()
} }
private func startLibraryItemTrackDownload(downloadItemId: String, item: LibraryItem, position: Int, track: AudioTrack, episode: PodcastEpisode?) throws -> DownloadItemPartTask { private func startLibraryItemTrackDownload(downloadItemId: String, item: LibraryItem, position: Int, track: AudioTrack, episode: PodcastEpisode?) throws -> DownloadItemPartTask {
AbsLogger.info(message: "TRACK \(track.contentUrl!)") AbsLogger.info(message: "TRACK \(track.contentUrl!)")
// If we don't name metadata, then we can't proceed // If we don't name metadata, then we can't proceed
guard let filename = track.metadata?.filename else { guard let filename = track.metadata?.filename else {
throw LibraryItemDownloadError.noMetadata throw LibraryItemDownloadError.noMetadata
} }
let serverUrl = urlForTrack(item: item, track: track) let serverUrl = urlForTrack(item: item, track: track)
let itemDirectory = try createLibraryItemFileDirectory(item: item) let itemDirectory = try createLibraryItemFileDirectory(item: item)
let localUrl = "\(itemDirectory)/\(filename)" let localUrl = "\(itemDirectory)/\(filename)"
let task = session.downloadTask(with: serverUrl) let task = session.downloadTask(with: serverUrl)
let part = DownloadItemPart(downloadItemId: downloadItemId, filename: filename, destination: localUrl, itemTitle: track.title ?? "Unknown", serverPath: Store.serverConfig!.address, audioTrack: track, episode: episode, ebookFile: nil, size: track.metadata?.size ?? 0) let part = DownloadItemPart(downloadItemId: downloadItemId, filename: filename, destination: localUrl, itemTitle: track.title ?? "Unknown", serverPath: Store.serverConfig!.address, audioTrack: track, episode: episode, ebookFile: nil, size: track.metadata?.size ?? 0)
// Store the id on the task so the download item can be pulled from the database later // Store the id on the task so the download item can be pulled from the database later
task.taskDescription = part.id task.taskDescription = part.id
return DownloadItemPartTask(part: part, task: task) return DownloadItemPartTask(part: part, task: task, partId: part.id, filename: filename)
} }
private func startLibraryItemEbookDownload(downloadItemId: String, item: LibraryItem, ebookFile: EBookFile) throws -> DownloadItemPartTask { private func startLibraryItemEbookDownload(downloadItemId: String, item: LibraryItem, ebookFile: EBookFile) throws -> DownloadItemPartTask {
@@ -340,14 +404,14 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
let serverPath = "/api/items/\(item.id)/file/\(ebookFile.ino)/download" let serverPath = "/api/items/\(item.id)/file/\(ebookFile.ino)/download"
let itemDirectory = try createLibraryItemFileDirectory(item: item) let itemDirectory = try createLibraryItemFileDirectory(item: item)
let localUrl = "\(itemDirectory)/\(filename)" let localUrl = "\(itemDirectory)/\(filename)"
let part = DownloadItemPart(downloadItemId: downloadItemId, filename: filename, destination: localUrl, itemTitle: filename, serverPath: serverPath, audioTrack: nil, episode: nil, ebookFile: ebookFile, size: ebookFile.metadata?.size ?? 0) let part = DownloadItemPart(downloadItemId: downloadItemId, filename: filename, destination: localUrl, itemTitle: filename, serverPath: serverPath, audioTrack: nil, episode: nil, ebookFile: ebookFile, size: ebookFile.metadata?.size ?? 0)
let task = session.downloadTask(with: part.downloadURL!) let task = session.downloadTask(with: part.downloadURL!)
// Store the id on the task so the download item can be pulled from the database later // Store the id on the task so the download item can be pulled from the database later
task.taskDescription = part.id task.taskDescription = part.id
return DownloadItemPartTask(part: part, task: task) return DownloadItemPartTask(part: part, task: task, partId: part.id, filename: filename)
} }
private func startLibraryItemCoverDownload(downloadItemId: String, item: LibraryItem) throws -> DownloadItemPartTask { private func startLibraryItemCoverDownload(downloadItemId: String, item: LibraryItem) throws -> DownloadItemPartTask {
@@ -355,19 +419,19 @@ public class AbsDownloader: CAPPlugin, CAPBridgedPlugin, URLSessionDownloadDeleg
let serverPath = "/api/items/\(item.id)/cover" let serverPath = "/api/items/\(item.id)/cover"
let itemDirectory = try createLibraryItemFileDirectory(item: item) let itemDirectory = try createLibraryItemFileDirectory(item: item)
let localUrl = "\(itemDirectory)/\(filename)" let localUrl = "\(itemDirectory)/\(filename)"
// Find library file to get cover size // Find library file to get cover size
let coverLibraryFile = item.libraryFiles.first(where: { let coverLibraryFile = item.libraryFiles.first(where: {
$0.metadata?.path == item.media?.coverPath $0.metadata?.path == item.media?.coverPath
}) })
let part = DownloadItemPart(downloadItemId: downloadItemId, filename: filename, destination: localUrl, itemTitle: "cover", serverPath: serverPath, audioTrack: nil, episode: nil, ebookFile: nil, size: coverLibraryFile?.metadata?.size ?? 0) let part = DownloadItemPart(downloadItemId: downloadItemId, filename: filename, destination: localUrl, itemTitle: "cover", serverPath: serverPath, audioTrack: nil, episode: nil, ebookFile: nil, size: coverLibraryFile?.metadata?.size ?? 0)
let task = session.downloadTask(with: part.downloadURL!) let task = session.downloadTask(with: part.downloadURL!)
// Store the id on the task so the download item can be pulled from the database later // Store the id on the task so the download item can be pulled from the database later
task.taskDescription = part.id task.taskDescription = part.id
return DownloadItemPartTask(part: part, task: task) return DownloadItemPartTask(part: part, task: task, partId: part.id, filename: filename)
} }
private func urlForTrack(item: LibraryItem, track: AudioTrack) -> URL { private func urlForTrack(item: LibraryItem, track: AudioTrack) -> URL {
@@ -431,6 +495,8 @@ typealias DownloadProgressHandler = (_ downloadItem: DownloadItem, _ downloadIte
struct DownloadItemPartTask { struct DownloadItemPartTask {
let part: DownloadItemPart let part: DownloadItemPart
let task: URLSessionDownloadTask let task: URLSessionDownloadTask
let partId: String // Cache the ID to avoid cross-thread Realm access
let filename: String // Cache the filename to avoid cross-thread Realm access
} }
enum LibraryItemDownloadError: String, Error { enum LibraryItemDownloadError: String, Error {
+20 -20
View File
@@ -25,11 +25,11 @@ PODS:
- Capacitor - Capacitor
- CordovaPlugins (6.2.1): - CordovaPlugins (6.2.1):
- CapacitorCordova - CapacitorCordova
- Realm (10.54.4): - Realm (10.54.6):
- Realm/Headers (= 10.54.4) - Realm/Headers (= 10.54.6)
- Realm/Headers (10.54.4) - Realm/Headers (10.54.6)
- RealmSwift (10.54.4): - RealmSwift (10.54.6):
- Realm (= 10.54.4) - Realm (= 10.54.6)
- WebnativellcCapacitorFilesharer (7.0.4): - WebnativellcCapacitorFilesharer (7.0.4):
- Capacitor - Capacitor
@@ -89,22 +89,22 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
Capacitor: 106e7a4205f4618d582b886a975657c61179138d Capacitor: 03bc7cbdde6a629a8b910a9d7d78c3cc7ed09ea7
CapacitorApp: d63334c052278caf5d81585d80b21905c6f93f39 CapacitorApp: febecbb9582cb353aed037e18ec765141f880fe9
CapacitorBrowser: 081852cf532acf77b9d2953f3a88fe5b9711fb06 CapacitorBrowser: 6299776d496e968505464884d565992faa20444a
CapacitorClipboard: b98aead5dc7ec595547fc2c5d75bacd2ae3338bc CapacitorClipboard: 70bfdb42b877b320a6e511ab94fa7a6a55d57ecb
CapacitorCommunityKeepAwake: 00dfd8fa3cca0df003c9a3e2cd7bee678aeec68b CapacitorCommunityKeepAwake: ae762ce29b53147d28cfcaae5273cd1db0c38fc4
CapacitorCommunityVolumeButtons: 8a0443a202ed659688d85f4d44d66f42f62f2b56 CapacitorCommunityVolumeButtons: 1b84f7abf29cd9476cef9e8979b2854a64d2eed5
CapacitorCordova: 5967b9ba03915ef1d585469d6e31f31dc49be96f CapacitorCordova: 5967b9ba03915ef1d585469d6e31f31dc49be96f
CapacitorDialog: 9b934329026b2b0ffa56939bb06df3c67541a2ab CapacitorDialog: 0e09f242f6c3f5e82e4dc76b20f2a056be57a579
CapacitorHaptics: 70e47470fa1a6bd6338cd102552e3846b7f9a1b3 CapacitorHaptics: 1f1e17041f435d8ead9ff2a34edd592c6aa6a8d6
CapacitorNetwork: 07ec4c69c1bb696f41c23e00d31bda1bbb221bba CapacitorNetwork: 15cb4385f0913a8ceb5e9a4d7af1ec554bdb8de8
CapacitorPreferences: cbf154e5e5519b7f5ab33817a334dda1e98387f9 CapacitorPreferences: 6c98117d4d7508034a4af9db64d6b26fc75d7b94
CapacitorStatusBar: 275cbf2f4dfc00388f519ef80c7ec22edda342c9 CapacitorStatusBar: 6e7af040d8fc4dd655999819625cae9c2d74c36f
CordovaPlugins: 5a72a85b45469e68556bb172409f1b6d57b27236 CordovaPlugins: 2ecbba09775516c41764dbf78ade612427311b7e
Realm: 8b5cda39a41f17a1734da2f39c6004eb8745587a Realm: b1b3bc68162fa242132eb7eefbf91d7c40f36a85
RealmSwift: 0b4f808fed6898f1f6c26f501f740efd80dff0b4 RealmSwift: 456cfd82a4f23dff8e3456980999331ab69bbf3e
WebnativellcCapacitorFilesharer: 10b111373d4dc49608935600dcbcc14605258c73 WebnativellcCapacitorFilesharer: e3a5930240633db3335040251d66aac6762ff111
PODFILE CHECKSUM: 498821c0cfa2508609567fa95d7244c01cbef538 PODFILE CHECKSUM: 498821c0cfa2508609567fa95d7244c01cbef538
+44 -4
View File
@@ -511,7 +511,8 @@ class ApiClient {
public static func reportLocalPlaybackProgress(_ session: PlaybackSession) async -> Bool { public static func reportLocalPlaybackProgress(_ session: PlaybackSession) async -> Bool {
return await withCheckedContinuation { continuation in return await withCheckedContinuation { continuation in
postResourceWithTokenRefresh(endpoint: "api/session/local", parameters: session) { success in let payload = PartialPlaybackSessionSyncPayload(from: session, includeDeviceInfo: true)
postResourceWithTokenRefresh(endpoint: "api/session/local", parameters: payload) { success in
continuation.resume(returning: success) continuation.resume(returning: success)
} }
} }
@@ -519,7 +520,10 @@ class ApiClient {
public static func reportAllLocalPlaybackSessions(_ sessions: [PlaybackSession]) async -> Bool { public static func reportAllLocalPlaybackSessions(_ sessions: [PlaybackSession]) async -> Bool {
return await withCheckedContinuation { continuation in return await withCheckedContinuation { continuation in
let payload = LocalPlaybackSessionSyncAllPayload(sessions: sessions, deviceInfo: sessions.first?.deviceInfo) let payload = LocalPlaybackSessionSyncAllPayload(
sessions: sessions.map { PartialPlaybackSessionSyncPayload(from: $0, includeDeviceInfo: false) },
deviceInfo: sessions.first?.deviceInfo
)
postResourceWithTokenRefresh(endpoint: "api/session/local-all", parameters: payload) { success in postResourceWithTokenRefresh(endpoint: "api/session/local-all", parameters: payload) { success in
continuation.resume(returning: success) continuation.resume(returning: success)
} }
@@ -668,8 +672,44 @@ struct LocalMediaProgressSyncResultsPayload: Codable {
var numLocalProgressUpdates: Int? var numLocalProgressUpdates: Int?
} }
struct LocalPlaybackSessionSyncAllPayload: Codable { struct PartialPlaybackSessionSyncPayload: Encodable {
var sessions: [PlaybackSession] let id: String
let userId: String?
let libraryItemId: String?
let episodeId: String?
let mediaType: String
let displayTitle: String?
let displayAuthor: String?
let duration: Double
let playMethod: Int
let startedAt: Double?
let updatedAt: Double?
let timeListening: Double
let currentTime: Double
let mediaPlayer: String
let deviceInfo: [String: String?]?
init(from session: PlaybackSession, includeDeviceInfo: Bool) {
id = session.id
userId = session.userId
libraryItemId = session.libraryItemId
episodeId = session.episodeId
mediaType = session.mediaType
displayTitle = session.displayTitle
displayAuthor = session.displayAuthor
duration = session.duration
playMethod = session.playMethod
startedAt = session.startedAt
updatedAt = session.updatedAt
timeListening = session.timeListening
currentTime = session.currentTime
mediaPlayer = session.mediaPlayer
deviceInfo = includeDeviceInfo ? session.deviceInfo : nil
}
}
struct LocalPlaybackSessionSyncAllPayload: Encodable {
var sessions: [PartialPlaybackSessionSyncPayload]
var deviceInfo: [String: String?]? var deviceInfo: [String: String?]?
} }
+41 -13
View File
@@ -24,6 +24,7 @@ export default {
inittingLibraries: false, inittingLibraries: false,
hasMounted: false, hasMounted: false,
disconnectTime: 0, disconnectTime: 0,
socketDisconnectedTime: 0,
timeLostFocus: 0, timeLostFocus: 0,
currentLang: null currentLang: null
} }
@@ -44,7 +45,7 @@ export default {
} else { } else {
var timeSinceDisconnect = Date.now() - this.disconnectTime var timeSinceDisconnect = Date.now() - this.disconnectTime
if (timeSinceDisconnect > 5000) { if (timeSinceDisconnect > 5000) {
console.log('Time since disconnect was', timeSinceDisconnect, 'sync with server') console.log('[default] Time since disconnect was', timeSinceDisconnect, 'sync with server')
setTimeout(() => { setTimeout(() => {
this.syncLocalSessions(false) this.syncLocalSessions(false)
}, 4000) }, 4000)
@@ -55,6 +56,28 @@ export default {
this.disconnectTime = Date.now() this.disconnectTime = Date.now()
} }
} }
},
socketConnected: {
handler(newVal, oldVal) {
if (!this.hasMounted) {
// watcher runs before mount, handling libraries/connection should be handled in mount
return
}
if (newVal) {
// if we havent been receiving socket events then external progress updates may have been missed
const timeSinceDisconnect = Date.now() - this.socketDisconnectedTime
if (timeSinceDisconnect > 30000 && this.isPlayerOpen) {
console.log('[default] socket reconnected after ' + timeSinceDisconnect + 'ms and player is open, triggering server media progress sync')
// used for triggering a server media progress sync if local media item is open in player
this.$eventBus.$emit('socket-reconnected')
} else {
console.log('[default] socket reconnected after ' + timeSinceDisconnect + 'ms')
}
} else {
console.log('[default] socket disconnected')
this.socketDisconnectedTime = Date.now()
}
}
} }
}, },
computed: { computed: {
@@ -67,6 +90,9 @@ export default {
networkConnected() { networkConnected() {
return this.$store.state.networkConnected return this.$store.state.networkConnected
}, },
socketConnected() {
return this.$store.state.socketConnected
},
user() { user() {
return this.$store.state.user.user return this.$store.state.user.user
}, },
@@ -218,9 +244,9 @@ export default {
AbsLogger.info({ tag: 'default', message: 'Calling syncLocalSessions' }) AbsLogger.info({ tag: 'default', message: 'Calling syncLocalSessions' })
const response = await this.$db.syncLocalSessionsWithServer(isFirstSync) const response = await this.$db.syncLocalSessionsWithServer(isFirstSync)
if (response?.error) { if (response?.error) {
console.error('[default] Failed to sync local sessions', response.error) await AbsLogger.error({ tag: 'default', message: `syncLocalSessions: Failed to sync local sessions: ${response.error}` })
} else { } else {
console.log('[default] Successfully synced local sessions') await AbsLogger.info({ tag: 'default', message: 'syncLocalSessions: Successfully synced local sessions' })
// Reload local media progresses // Reload local media progresses
await this.$store.dispatch('globals/loadLocalMediaProgress') await this.$store.dispatch('globals/loadLocalMediaProgress')
} }
@@ -233,11 +259,13 @@ export default {
async userMediaProgressUpdated(payload) { async userMediaProgressUpdated(payload) {
const prog = payload.data // MediaProgress const prog = payload.data // MediaProgress
await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: Received updated media progress for current user from socket event. Media item id ${payload.id}` }) await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: Received updated media progress for current user from socket event. Media item id ${payload.id}` })
const mediaProgressId = payload.id
const itemLabel = `${prog.libraryItemId}${prog.episodeId ? ` episodeId: ${prog.episodeId}` : ''}`
// Check if this media item is currently open in the player, paused, and this progress update is coming from a different session // Check if this media item is currently open in the player, paused, and this progress update is coming from a different session
const isMediaOpenInPlayer = this.$store.getters['getIsMediaStreaming'](prog.libraryItemId, prog.episodeId) const isMediaOpenInPlayer = this.$store.getters['getIsMediaStreaming'](prog.libraryItemId, prog.episodeId)
if (isMediaOpenInPlayer && this.$store.getters['getCurrentPlaybackSessionId'] !== payload.sessionId && !this.$store.state.playerIsPlaying) { if (isMediaOpenInPlayer && this.$store.getters['getCurrentPlaybackSessionId'] !== payload.sessionId && !this.$store.state.playerIsPlaying) {
await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: Item is currently open in player, paused and this progress update is coming from a different session. Updating playback time to ${payload.data.currentTime}` }) await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: Updating paused player playback time to ${payload.data.currentTime} (${itemLabel}, mediaProgressId: ${mediaProgressId})` })
this.$eventBus.$emit('playback-time-update', payload.data.currentTime) this.$eventBus.$emit('playback-time-update', payload.data.currentTime)
} }
@@ -248,17 +276,17 @@ export default {
// Progress update is more recent then local progress // Progress update is more recent then local progress
if (localProg && localProg.lastUpdate < prog.lastUpdate) { if (localProg && localProg.lastUpdate < prog.lastUpdate) {
if (localProg.currentTime == prog.currentTime && localProg.isFinished == prog.isFinished) { if (localProg.currentTime == prog.currentTime && localProg.isFinished == prog.isFinished) {
await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: server lastUpdate is more recent but progress is up-to-date (libraryItemId: ${prog.libraryItemId}${prog.episodeId ? ` episodeId: ${prog.episodeId}` : ''})` }) await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: server lastUpdate is more recent but progress is up-to-date (${itemLabel}, mediaProgressId: ${mediaProgressId}, server lastUpdate=${prog.lastUpdate} > local lastUpdate=${localProg.lastUpdate})` })
return return
} }
// Server progress is more up-to-date // Server progress is more up-to-date
await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: syncing progress from server with local item for "${prog.libraryItemId}" ${prog.episodeId ? `episode ${prog.episodeId}` : ''} | server lastUpdate=${prog.lastUpdate} > local lastUpdate=${localProg.lastUpdate}` }) await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: Syncing server progress to local (${itemLabel}, mediaProgressId: ${mediaProgressId}, server lastUpdate=${prog.lastUpdate} > local lastUpdate=${localProg.lastUpdate})` })
const payload = { const syncPayload = {
localMediaProgressId: localProg.id, localMediaProgressId: localProg.id,
mediaProgress: prog mediaProgress: prog
} }
newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress(payload) newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress(syncPayload)
} else if (!localProg) { } else if (!localProg) {
// Check if local library item exists // Check if local library item exists
// local media progress may not exist yet if it hasn't been played // local media progress may not exist yet if it hasn't been played
@@ -270,20 +298,20 @@ export default {
const localEpisode = lliEpisodes.find((ep) => ep.serverEpisodeId === prog.episodeId) const localEpisode = lliEpisodes.find((ep) => ep.serverEpisodeId === prog.episodeId)
if (localEpisode) { if (localEpisode) {
// Add new local media progress // Add new local media progress
const payload = { const syncPayload = {
localLibraryItemId: localLibraryItem.id, localLibraryItemId: localLibraryItem.id,
localEpisodeId: localEpisode.id, localEpisodeId: localEpisode.id,
mediaProgress: prog mediaProgress: prog
} }
newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress(payload) newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress(syncPayload)
} }
} else { } else {
// Add new local media progress // Add new local media progress
const payload = { const syncPayload = {
localLibraryItemId: localLibraryItem.id, localLibraryItemId: localLibraryItem.id,
mediaProgress: prog mediaProgress: prog
} }
newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress(payload) newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress(syncPayload)
} }
} else { } else {
console.log(`[default] userMediaProgressUpdate no local media progress or lli found for this server item ${prog.id}`) console.log(`[default] userMediaProgressUpdate no local media progress or lli found for this server item ${prog.id}`)
@@ -291,7 +319,7 @@ export default {
} }
if (newLocalMediaProgress?.id) { if (newLocalMediaProgress?.id) {
await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: local media progress updated for ${newLocalMediaProgress.id}` }) await AbsLogger.info({ tag: 'default', message: `userMediaProgressUpdate: Local media progress updated (${itemLabel}, localId: ${newLocalMediaProgress.id})` })
this.$store.commit('globals/updateLocalMediaProgress', newLocalMediaProgress) this.$store.commit('globals/updateLocalMediaProgress', newLocalMediaProgress)
} }
}, },
+1 -1
View File
@@ -33,7 +33,7 @@ export default {
this.$strings.MessageConfirmStreamingUsingCellular this.$strings.MessageConfirmStreamingUsingCellular
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message message
}) })
return value return value
+12
View File
@@ -0,0 +1,12 @@
export default {
methods: {
getJumpLabel(seconds) {
const val = Number(seconds)
if (isNaN(val)) return ''
const useMinutes = val >= 120 // keep 60s as seconds
const key = useMinutes ? 'UnitMinutesShort' : 'UnitSecondsShort'
const unitValue = useMinutes ? val / 60 : val
return this.$getString(key, [this.$formatNumber(unitValue)])
}
}
}
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf-app", "name": "audiobookshelf-app",
"version": "0.11.0-beta", "version": "0.14.0-beta",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf-app", "name": "audiobookshelf-app",
"version": "0.11.0-beta", "version": "0.14.0-beta",
"dependencies": { "dependencies": {
"@capacitor-community/keep-awake": "^7.0.0", "@capacitor-community/keep-awake": "^7.0.0",
"@capacitor-community/volume-buttons": "^7.0.0", "@capacitor-community/volume-buttons": "^7.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-app", "name": "audiobookshelf-app",
"version": "0.11.0-beta", "version": "0.14.0-beta",
"author": "advplyr", "author": "advplyr",
"scripts": { "scripts": {
"dev": "nuxt --hostname 0.0.0.0 --port 1337", "dev": "nuxt --hostname 0.0.0.0 --port 1337",
+1 -1
View File
@@ -148,7 +148,7 @@ export default {
// Show confirmation dialog and start downloading if user chooses so // Show confirmation dialog and start downloading if user chooses so
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: startDownloadMessage message: startDownloadMessage
}) })
if (value) { if (value) {
+1 -1
View File
@@ -11,7 +11,7 @@
<span v-else class="font-semibold text-fg">{{ Math.round(itemPart.progress) }}%</span> <span v-else class="font-semibold text-fg">{{ Math.round(itemPart.progress) }}%</span>
</div> </div>
<div class="flex-grow px-2"> <div class="flex-grow px-2">
<p class="truncate">{{ itemPart.filename }}</p> <p class="break-all">{{ itemPart.filename }}</p>
</div> </div>
</div> </div>
+4 -4
View File
@@ -346,7 +346,7 @@ export default {
} }
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: this.$getString('MessageConfirmDeleteLocalEpisode', [localFile.basePath]) message: this.$getString('MessageConfirmDeleteLocalEpisode', [localFile.basePath])
}) })
if (value) { if (value) {
@@ -514,7 +514,7 @@ export default {
await this.$hapticsImpact() await this.$hapticsImpact()
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: this.$strings.MessageConfirmDiscardProgress message: this.$strings.MessageConfirmDiscardProgress
}) })
if (value) { if (value) {
@@ -575,8 +575,8 @@ export default {
await this.$hapticsImpact() await this.$hapticsImpact()
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: `Are you sure you want to delete episode "${this.title}" from the server?\nWarning: This will delete the audio file.` message: this.$getString('MessageConfirmDeleteServerEpisode', [this.title])
}) })
if (value) { if (value) {
+14 -17
View File
@@ -13,7 +13,7 @@
</div> </div>
</div> </div>
<div class="relative" @click="showFullscreenCover = true"> <div class="relative" @click="showFullscreenCover = true">
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" no-bg raw @imageLoaded="coverImageLoaded" /> <covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" no-bg raw />
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1 z-10 box-shadow-progressbar" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * progressPercent + 'px' }"></div> <div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1 z-10 box-shadow-progressbar" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * progressPercent + 'px' }"></div>
</div> </div>
</div> </div>
@@ -172,7 +172,7 @@
<script> <script>
import { Dialog } from '@capacitor/dialog' import { Dialog } from '@capacitor/dialog'
import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor' import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor'
import { FastAverageColor } from 'fast-average-color' import { getAverageColorFromCoverUrl } from '@/utils/coverAverageColor'
import cellularPermissionHelpers from '@/mixins/cellularPermissionHelpers' import cellularPermissionHelpers from '@/mixins/cellularPermissionHelpers'
export default { export default {
@@ -496,17 +496,10 @@ export default {
}, },
async coverImageLoaded(fullCoverUrl) { async coverImageLoaded(fullCoverUrl) {
if (!fullCoverUrl) return if (!fullCoverUrl) return
const avg = await getAverageColorFromCoverUrl(this, fullCoverUrl)
const fac = new FastAverageColor() if (!avg) return
fac this.coverRgb = avg.rgba
.getColorAsync(fullCoverUrl) this.coverBgIsLight = avg.isLight
.then((color) => {
this.coverRgb = color.rgba
this.coverBgIsLight = color.isLight
})
.catch((e) => {
console.log(e)
})
}, },
moreButtonPress() { moreButtonPress() {
this.showMoreMenu = true this.showMoreMenu = true
@@ -535,7 +528,11 @@ export default {
if (this.isPodcast) { if (this.isPodcast) {
this.episodes.sort((a, b) => { this.episodes.sort((a, b) => {
return String(b.publishedAt).localeCompare(String(a.publishedAt), undefined, { numeric: true, sensitivity: 'base' }) if (this.podcastType === 'serial') {
return String(a.publishedAt).localeCompare(String(b.publishedAt), undefined, { numeric: true, sensitivity: 'base' })
} else {
return String(b.publishedAt).localeCompare(String(a.publishedAt), undefined, { numeric: true, sensitivity: 'base' })
}
}) })
let episode = this.episodes.find((ep) => { let episode = this.episodes.find((ep) => {
@@ -584,8 +581,8 @@ export default {
// If start time and is not already streaming then ask for confirmation // If start time and is not already streaming then ask for confirmation
if (startTime !== null && startTime !== undefined && !this.$store.getters['getIsMediaStreaming'](libraryItemId, null)) { if (startTime !== null && startTime !== undefined && !this.$store.getters['getIsMediaStreaming'](libraryItemId, null)) {
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: `Start playback for "${this.title}" at ${this.$secondsToTimestamp(startTime)}?` message: this.$getString('MessageConfirmPlaybackTime', [this.title, this.$secondsToTimestamp(startTime)])
}) })
if (!value) return if (!value) return
} }
@@ -673,7 +670,7 @@ export default {
} }
} }
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: startDownloadMessage message: startDownloadMessage
}) })
if (value) { if (value) {
+1 -1
View File
@@ -100,7 +100,7 @@ export default {
deleteMessage = `Are you sure you want to remove this folder and ${this.localLibraryItems.length} items? (does not delete anything in your file system)` deleteMessage = `Are you sure you want to remove this folder and ${this.localLibraryItems.length} items? (does not delete anything in your file system)`
} }
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: deleteMessage message: deleteMessage
}) })
if (value) { if (value) {
+1 -1
View File
@@ -22,7 +22,7 @@
</div> </div>
<div v-if="!isAndroid10OrBelow || overrideFolderRestriction" class="flex border-t border-fg/10 my-4 py-4"> <div v-if="!isAndroid10OrBelow || overrideFolderRestriction" class="flex border-t border-fg/10 my-4 py-4">
<div class="flex-grow pr-1"> <div class="flex-grow pr-1">
<ui-dropdown v-model="newFolderMediaType" placeholder="Select media type" :items="mediaTypeItems" /> <ui-dropdown v-model="newFolderMediaType" :placeholder="$strings.LabelSelectMediaType" :items="mediaTypeItems" />
</div> </div>
<ui-btn small class="w-28" color="success" @click="selectFolder">{{ $strings.ButtonNewFolder }}</ui-btn> <ui-btn small class="w-28" color="success" @click="selectFolder">{{ $strings.ButtonNewFolder }}</ui-btn>
</div> </div>
+3 -3
View File
@@ -369,7 +369,7 @@ export default {
confirmMessage += ' The file on the server will be unaffected.' confirmMessage += ' The file on the server will be unaffected.'
} }
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: confirmMessage message: confirmMessage
}) })
if (value) { if (value) {
@@ -395,7 +395,7 @@ export default {
confirmMessage += ' The file on the server will be unaffected.' confirmMessage += ' The file on the server will be unaffected.'
} }
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: confirmMessage message: confirmMessage
}) })
if (value) { if (value) {
@@ -412,7 +412,7 @@ export default {
confirmMessage += ' The files on the server and your progress will be unaffected.' confirmMessage += ' The files on the server and your progress will be unaffected.'
} }
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: this.$strings.HeaderConfirm,
message: confirmMessage message: confirmMessage
}) })
if (value) { if (value) {
+18 -6
View File
@@ -14,16 +14,25 @@
<template v-for="item in bookResults"> <template v-for="item in bookResults">
<div :key="item.libraryItem.id" class="w-full h-16 py-1"> <div :key="item.libraryItem.id" class="w-full h-16 py-1">
<nuxt-link :to="`/item/${item.libraryItem.id}`"> <nuxt-link :to="`/item/${item.libraryItem.id}`">
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" /> <cards-item-search-card :library-item="item.libraryItem" :search="lastSearch" />
</nuxt-link> </nuxt-link>
</div> </div>
</template> </template>
<p v-if="podcastResults.length" class="uppercase text-xs text-fg-muted my-1 px-1 font-semibold">{{ $strings.LabelPodcasts }}</p> <p v-if="podcastResults.length" class="font-semibold text-sm mb-1 mt-2">{{ $strings.LabelPodcasts }}</p>
<template v-for="item in podcastResults"> <template v-for="item in podcastResults">
<div :key="item.libraryItem.id" class="text-fg select-none relative py-1"> <div :key="item.libraryItem.id" class="text-fg select-none relative py-1">
<nuxt-link :to="`/item/${item.libraryItem.id}`"> <nuxt-link :to="`/item/${item.libraryItem.id}`">
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" /> <cards-item-search-card :library-item="item.libraryItem" :search="lastSearch" />
</nuxt-link>
</div>
</template>
<p v-if="episodeResults.length" class="font-semibold text-sm mb-1 mt-2">{{ $strings.HeaderEpisodes }}</p>
<template v-for="item in episodeResults">
<div :key="item.libraryItem.recentEpisode.id" class="text-fg select-none relative py-1">
<nuxt-link :to="`/item/${item.libraryItem.id}/${item.libraryItem.recentEpisode.id}`">
<cards-episode-search-card :episode="item.libraryItem.recentEpisode" :library-item="item.libraryItem" />
</nuxt-link> </nuxt-link>
</div> </div>
</template> </template>
@@ -77,6 +86,7 @@ export default {
isFetching: false, isFetching: false,
bookResults: [], bookResults: [],
podcastResults: [], podcastResults: [],
episodeResults: [],
seriesResults: [], seriesResults: [],
authorResults: [], authorResults: [],
narratorResults: [], narratorResults: [],
@@ -91,7 +101,7 @@ export default {
return this.$store.getters['libraries/getBookCoverAspectRatio'] return this.$store.getters['libraries/getBookCoverAspectRatio']
}, },
totalResults() { totalResults() {
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.podcastResults.length + this.narratorResults.length + this.tagResults.length return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.podcastResults.length + this.narratorResults.length + this.tagResults.length + this.episodeResults.length
} }
}, },
methods: { methods: {
@@ -104,6 +114,7 @@ export default {
if (!this.lastSearch) { if (!this.lastSearch) {
this.bookResults = [] this.bookResults = []
this.podcastResults = [] this.podcastResults = []
this.episodeResults = []
this.seriesResults = [] this.seriesResults = []
this.authorResults = [] this.authorResults = []
this.narratorResults = [] this.narratorResults = []
@@ -111,7 +122,7 @@ export default {
return return
} }
this.isFetching = true this.isFetching = true
const results = await this.$nativeHttp.get(`/api/libraries/${this.currentLibraryId}/search?q=${value}&limit=5`).catch((error) => { const results = await this.$nativeHttp.get(`/api/libraries/${this.currentLibraryId}/search?q=${value}`).catch((error) => {
console.error('Search error', error) console.error('Search error', error)
return null return null
}) })
@@ -125,6 +136,7 @@ export default {
this.bookResults = results?.book || [] this.bookResults = results?.book || []
this.podcastResults = results?.podcast || [] this.podcastResults = results?.podcast || []
this.episodeResults = results?.episodes || []
this.seriesResults = results?.series || [] this.seriesResults = results?.series || []
this.authorResults = results?.authors || [] this.authorResults = results?.authors || []
this.narratorResults = results?.narrators || [] this.narratorResults = results?.narrators || []
@@ -160,4 +172,4 @@ export default {
height: calc(100% - 108px); height: calc(100% - 108px);
max-height: calc(100% - 108px); max-height: calc(100% - 108px);
} }
</style> </style>
+61 -42
View File
@@ -36,24 +36,24 @@
<!-- Playback settings --> <!-- Playback settings -->
<p class="uppercase text-xs font-semibold text-fg-muted mb-2 mt-10">{{ $strings.HeaderPlaybackSettings }}</p> <p class="uppercase text-xs font-semibold text-fg-muted mb-2 mt-10">{{ $strings.HeaderPlaybackSettings }}</p>
<div class="py-3 flex items-center">
<p class="pr-4 w-36">{{ $strings.LabelJumpBackwardsTime }}</p>
<div @click.stop="showJumpBackwardsOptions">
<ui-text-input :value="jumpBackwardsOption" readonly append-icon="expand_more" style="width: 145px; max-width: 145px" />
</div>
</div>
<div class="py-3 flex items-center">
<p class="pr-4 w-36">{{ $strings.LabelJumpForwardsTime }}</p>
<div @click.stop="showJumpForwardOptions">
<ui-text-input :value="jumpForwardOption" readonly append-icon="expand_more" style="width: 145px; max-width: 145px" />
</div>
</div>
<div class="flex items-center py-3"> <div class="flex items-center py-3">
<div class="w-10 flex justify-center" @click="toggleDisableAutoRewind"> <div class="w-10 flex justify-center" @click="toggleDisableAutoRewind">
<ui-toggle-switch v-model="settings.disableAutoRewind" @input="saveSettings" /> <ui-toggle-switch v-model="settings.disableAutoRewind" @input="saveSettings" />
</div> </div>
<p class="pl-4">{{ $strings.LabelDisableAutoRewind }}</p> <p class="pl-4">{{ $strings.LabelDisableAutoRewind }}</p>
</div> </div>
<div class="flex items-center py-3">
<div class="w-10 flex justify-center" @click="toggleJumpBackwards">
<span class="material-symbols text-4xl">{{ currentJumpBackwardsTimeIcon }}</span>
</div>
<p class="pl-4">{{ $strings.LabelJumpBackwardsTime }}</p>
</div>
<div class="flex items-center py-3">
<div class="w-10 flex justify-center" @click="toggleJumpForward">
<span class="material-symbols text-4xl">{{ currentJumpForwardTimeIcon }}</span>
</div>
<p class="pl-4">{{ $strings.LabelJumpForwardsTime }}</p>
</div>
<div v-if="!isiOS" class="flex items-center py-3"> <div v-if="!isiOS" class="flex items-center py-3">
<div class="w-10 flex justify-center" @click="toggleEnableMp3IndexSeeking"> <div class="w-10 flex justify-center" @click="toggleEnableMp3IndexSeeking">
<ui-toggle-switch v-model="settings.enableMp3IndexSeeking" @input="saveSettings" /> <ui-toggle-switch v-model="settings.enableMp3IndexSeeking" @input="saveSettings" />
@@ -179,7 +179,7 @@
<ui-loading-indicator /> <ui-loading-indicator />
</div> </div>
<modals-dialog v-model="showMoreMenuDialog" :items="moreMenuItems" @action="clickMenuAction" /> <modals-dialog v-model="showMoreMenuDialog" :items="moreMenuItems" :selected="moreMenuSelected" @action="clickMenuAction" />
<modals-sleep-timer-length-modal v-model="showSleepTimerLengthModal" @change="sleepTimerLengthModalSelection" /> <modals-sleep-timer-length-modal v-model="showSleepTimerLengthModal" @change="sleepTimerLengthModalSelection" />
<modals-auto-sleep-timer-rewind-length-modal v-model="showAutoSleepTimerRewindLengthModal" @change="showAutoSleepTimerRewindLengthModalSelection" /> <modals-auto-sleep-timer-rewind-length-modal v-model="showAutoSleepTimerRewindLengthModal" @change="showAutoSleepTimerRewindLengthModalSelection" />
</div> </div>
@@ -187,8 +187,10 @@
<script> <script>
import { Dialog } from '@capacitor/dialog' import { Dialog } from '@capacitor/dialog'
import jumpLabelMixin from '@/mixins/jumpLabel'
export default { export default {
mixins: [jumpLabelMixin],
data() { data() {
return { return {
loading: false, loading: false,
@@ -352,15 +354,21 @@ export default {
isiOS() { isiOS() {
return this.$platform === 'ios' return this.$platform === 'ios'
}, },
jumpForwardItems() { jumpForwardSecondsOptions() {
return this.$store.state.globals.jumpForwardItems || [] return this.$store.state.globals.jumpForwardSecondsOptions || []
}, },
jumpBackwardsItems() { jumpBackwardsSecondsOptions() {
return this.$store.state.globals.jumpBackwardsItems || [] return this.$store.state.globals.jumpBackwardsSecondsOptions || []
}, },
languageOptionItems() { languageOptionItems() {
return this.$languageCodeOptions || [] return this.$languageCodeOptions || []
}, },
jumpForwardOption() {
return this.getJumpLabel(this.settings.jumpForwardTime)
},
jumpBackwardsOption() {
return this.getJumpLabel(this.settings.jumpBackwardsTime)
},
themeOptionItems() { themeOptionItems() {
return [ return [
{ {
@@ -377,20 +385,6 @@ export default {
} }
] ]
}, },
currentJumpForwardTimeIcon() {
return this.jumpForwardItems[this.currentJumpForwardTimeIndex].icon
},
currentJumpForwardTimeIndex() {
var index = this.jumpForwardItems.findIndex((jfi) => jfi.value === this.settings.jumpForwardTime)
return index >= 0 ? index : 1
},
currentJumpBackwardsTimeIcon() {
return this.jumpBackwardsItems[this.currentJumpBackwardsTimeIndex].icon
},
currentJumpBackwardsTimeIndex() {
var index = this.jumpBackwardsItems.findIndex((jfi) => jfi.value === this.settings.jumpBackwardsTime)
return index >= 0 ? index : 1
},
shakeSensitivityOption() { shakeSensitivityOption() {
const item = this.shakeSensitivityItems.find((i) => i.value === this.settings.shakeSensitivity) const item = this.shakeSensitivityItems.find((i) => i.value === this.settings.shakeSensitivity)
return item?.text || 'Error' return item?.text || 'Error'
@@ -434,7 +428,29 @@ export default {
else if (this.moreMenuSetting === 'downloadUsingCellular') return this.downloadUsingCellularItems else if (this.moreMenuSetting === 'downloadUsingCellular') return this.downloadUsingCellularItems
else if (this.moreMenuSetting === 'streamingUsingCellular') return this.streamingUsingCellularItems else if (this.moreMenuSetting === 'streamingUsingCellular') return this.streamingUsingCellularItems
else if (this.moreMenuSetting === 'androidAutoBrowseSeriesSequenceOrder') return this.androidAutoBrowseSeriesSequenceOrderItems else if (this.moreMenuSetting === 'androidAutoBrowseSeriesSequenceOrder') return this.androidAutoBrowseSeriesSequenceOrderItems
else if (this.moreMenuSetting === 'jumpForward')
return this.jumpForwardSecondsOptions.map((value) => ({
text: this.getJumpLabel(value),
value: value
}))
else if (this.moreMenuSetting === 'jumpBackwards')
return this.jumpBackwardsSecondsOptions.map((value) => ({
text: this.getJumpLabel(value),
value: value
}))
return [] return []
},
moreMenuSelected() {
if (this.moreMenuSetting === 'jumpForward') return this.settings.jumpForwardTime
if (this.moreMenuSetting === 'jumpBackwards') return this.settings.jumpBackwardsTime
if (this.moreMenuSetting === 'language') return this.settings.languageCode
if (this.moreMenuSetting === 'theme') return this.theme
if (this.moreMenuSetting === 'downloadUsingCellular') return this.settings.downloadUsingCellular
if (this.moreMenuSetting === 'streamingUsingCellular') return this.settings.streamingUsingCellular
if (this.moreMenuSetting === 'androidAutoBrowseSeriesSequenceOrder') return this.settings.androidAutoBrowseSeriesSequenceOrder
if (this.moreMenuSetting === 'shakeSensitivity') return this.settings.shakeSensitivity
if (this.moreMenuSetting === 'hapticFeedback') return this.settings.hapticFeedback
return null
} }
}, },
methods: { methods: {
@@ -468,6 +484,14 @@ export default {
this.moreMenuSetting = 'theme' this.moreMenuSetting = 'theme'
this.showMoreMenuDialog = true this.showMoreMenuDialog = true
}, },
showJumpForwardOptions() {
this.moreMenuSetting = 'jumpForward'
this.showMoreMenuDialog = true
},
showJumpBackwardsOptions() {
this.moreMenuSetting = 'jumpBackwards'
this.showMoreMenuDialog = true
},
showDownloadUsingCellularOptions() { showDownloadUsingCellularOptions() {
this.moreMenuSetting = 'downloadUsingCellular' this.moreMenuSetting = 'downloadUsingCellular'
this.showMoreMenuDialog = true this.showMoreMenuDialog = true
@@ -503,6 +527,12 @@ export default {
} else if (this.moreMenuSetting === 'androidAutoBrowseSeriesSequenceOrder') { } else if (this.moreMenuSetting === 'androidAutoBrowseSeriesSequenceOrder') {
this.settings.androidAutoBrowseSeriesSequenceOrder = action this.settings.androidAutoBrowseSeriesSequenceOrder = action
this.saveSettings() this.saveSettings()
} else if (this.moreMenuSetting === 'jumpForward') {
this.settings.jumpForwardTime = action
this.saveSettings()
} else if (this.moreMenuSetting === 'jumpBackwards') {
this.settings.jumpBackwardsTime = action
this.saveSettings()
} }
}, },
saveTheme(theme) { saveTheme(theme) {
@@ -598,17 +628,6 @@ export default {
this.$setOrientationLock(this.settings.lockOrientation) this.$setOrientationLock(this.settings.lockOrientation)
this.saveSettings() this.saveSettings()
}, },
toggleJumpForward() {
var next = (this.currentJumpForwardTimeIndex + 1) % 3
this.settings.jumpForwardTime = this.jumpForwardItems[next].value
this.saveSettings()
},
toggleJumpBackwards() {
var next = (this.currentJumpBackwardsTimeIndex + 4) % 3
if (next > 2) return
this.settings.jumpBackwardsTime = this.jumpBackwardsItems[next].value
this.saveSettings()
},
async saveSettings() { async saveSettings() {
await this.$hapticsImpact() await this.$hapticsImpact()
const updatedDeviceData = await this.$db.updateDeviceSettings({ ...this.settings }) const updatedDeviceData = await this.$db.updateDeviceSettings({ ...this.settings })
+2 -4
View File
@@ -51,7 +51,7 @@ class AbsDatabaseWeb extends WebPlugin {
ssc.customHeaders = serverConnectionConfig.customHeaders || {} ssc.customHeaders = serverConnectionConfig.customHeaders || {}
if (serverConnectionConfig.refreshToken) { if (serverConnectionConfig.refreshToken) {
console.log('[AbsDatabase] Updating refresh token...', serverConnectionConfig.refreshToken) console.log('[AbsDatabase] Updating refresh token...')
// Only using local storage for web version that is only used for testing // Only using local storage for web version that is only used for testing
localStorage.setItem(`refresh_token_${ssc.id}`, serverConnectionConfig.refreshToken) localStorage.setItem(`refresh_token_${ssc.id}`, serverConnectionConfig.refreshToken)
} }
@@ -71,7 +71,7 @@ class AbsDatabaseWeb extends WebPlugin {
} }
if (serverConnectionConfig.refreshToken) { if (serverConnectionConfig.refreshToken) {
console.log('[AbsDatabase] Setting refresh token...', serverConnectionConfig.refreshToken) console.log('[AbsDatabase] Setting refresh token...')
// Only using local storage for web version that is only used for testing // Only using local storage for web version that is only used for testing
localStorage.setItem(`refresh_token_${ssc.id}`, serverConnectionConfig.refreshToken) localStorage.setItem(`refresh_token_${ssc.id}`, serverConnectionConfig.refreshToken)
} }
@@ -119,7 +119,6 @@ class AbsDatabaseWeb extends WebPlugin {
name: 'Audiobooks', name: 'Audiobooks',
contentUrl: 'test', contentUrl: 'test',
absolutePath: '/audiobooks', absolutePath: '/audiobooks',
simplePath: 'audiobooks',
storageType: 'primary', storageType: 'primary',
mediaType: 'book' mediaType: 'book'
} }
@@ -196,7 +195,6 @@ class AbsDatabaseWeb extends WebPlugin {
filename: 'lf1.mp3', filename: 'lf1.mp3',
contentUrl: 'test', contentUrl: 'test',
absolutePath: 'test', absolutePath: 'test',
simplePath: 'test',
mimeType: 'audio/mpeg', mimeType: 'audio/mpeg',
size: 39048290 size: 39048290
} }
+1 -2
View File
@@ -128,13 +128,12 @@ export default ({ app, store }, inject) => {
// Listen for token refresh events from native app // Listen for token refresh events from native app
AbsDatabase.addListener('onTokenRefresh', (data) => { AbsDatabase.addListener('onTokenRefresh', (data) => {
console.log('[db] onTokenRefresh', data)
store.commit('user/setAccessToken', data.accessToken) store.commit('user/setAccessToken', data.accessToken)
}) })
// Listen for token refresh failure events from native app // Listen for token refresh failure events from native app
AbsDatabase.addListener('onTokenRefreshFailure', async (data) => { AbsDatabase.addListener('onTokenRefreshFailure', async (data) => {
console.log('[db] onTokenRefreshFailure', data) console.log('[db] onTokenRefreshFailure')
// Clear store and redirect to login page // Clear store and redirect to login page
await store.dispatch('user/logout') await store.dispatch('user/logout')
if (window.location.pathname !== '/connect') { if (window.location.pathname !== '/connect') {
+25 -1
View File
@@ -1,11 +1,15 @@
import Vue from 'vue' import Vue from 'vue'
import { Capacitor } from '@capacitor/core'
import { AbsDownloader, AbsFileSystem } from '@/plugins/capacitor'
import enUsStrings from '../strings/en-us.json' import enUsStrings from '../strings/en-us.json'
const defaultCode = 'en-us' const defaultCode = 'en-us'
let $localStore = null let $localStore = null
const languageCodeMap = { const languageCodeMap = {
be: { label: 'Беларуская', dateFnsLocale: 'be' },
bn: { label: 'বাংলা', dateFnsLocale: 'bn' }, bn: { label: 'বাংলা', dateFnsLocale: 'bn' },
bg: { label: 'Български', dateFnsLocale: 'bg' },
ca: { label: 'Català', dateFnsLocale: 'ca' }, ca: { label: 'Català', dateFnsLocale: 'ca' },
cs: { label: 'Čeština', dateFnsLocale: 'cs' }, cs: { label: 'Čeština', dateFnsLocale: 'cs' },
da: { label: 'Dansk', dateFnsLocale: 'da' }, da: { label: 'Dansk', dateFnsLocale: 'da' },
@@ -25,6 +29,7 @@ const languageCodeMap = {
ru: { label: 'Русский', dateFnsLocale: 'ru' }, ru: { label: 'Русский', dateFnsLocale: 'ru' },
sl: { label: 'Slovenščina', dateFnsLocale: 'sl' }, sl: { label: 'Slovenščina', dateFnsLocale: 'sl' },
sv: { label: 'Svenska', dateFnsLocale: 'sv' }, sv: { label: 'Svenska', dateFnsLocale: 'sv' },
tr: { label: 'Türkçe', dateFnsLocale: 'tr' },
uk: { label: 'Українська', dateFnsLocale: 'uk' }, uk: { label: 'Українська', dateFnsLocale: 'uk' },
'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' }, 'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' },
'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' } 'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' }
@@ -38,6 +43,24 @@ function supplant(str, subs) {
}) })
} }
function syncDownloadNotificationStrings() {
if (Capacitor.getPlatform() !== 'android') return
AbsDownloader.setDownloadNotificationStrings({
preparing: Vue.prototype.$strings.MessagePreparingDownloads,
downloadingFile: Vue.prototype.$strings.MessageDownloadingFile,
waitingForStorage: Vue.prototype.$strings.MessageWaitingForAvailableStorage,
downloads: Vue.prototype.$strings.HeaderDownloads,
cancel: Vue.prototype.$strings.ButtonCancel
}).catch((error) => console.warn('Failed to update download notification strings', error))
AbsFileSystem.setFolderPickerStrings({
writeAccessRequired: Vue.prototype.$strings.MessageStorageWriteAccessRequired,
allow: Vue.prototype.$strings.ButtonAllow,
cancel: Vue.prototype.$strings.ButtonCancel,
accessDenied: Vue.prototype.$strings.MessageStorageAccessDenied,
permissionDenied: Vue.prototype.$strings.MessageStoragePermissionDenied
}).catch((error) => console.warn('Failed to update folder picker strings', error))
}
Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map((code) => { Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map((code) => {
return { return {
text: languageCodeMap[code].label, text: languageCodeMap[code].label,
@@ -105,6 +128,7 @@ async function loadi18n(code) {
} }
Vue.prototype.$setDateFnsLocale(languageCodeMap[code].dateFnsLocale) Vue.prototype.$setDateFnsLocale(languageCodeMap[code].dateFnsLocale)
syncDownloadNotificationStrings()
this.$eventBus.$emit('change-lang', code) this.$eventBus.$emit('change-lang', code)
return true return true
@@ -142,5 +166,5 @@ async function initialize() {
export default ({ app, store }, inject) => { export default ({ app, store }, inject) => {
$localStore = app.$localStore $localStore = app.$localStore
initialize() initialize().finally(syncDownloadNotificationStrings)
} }
+2 -2
View File
@@ -314,8 +314,8 @@ export default ({ store, app }, inject) => {
} }
if (!canGoBack) { if (!canGoBack) {
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Confirm', title: eventBus.$strings.HeaderConfirm,
message: `Did you want to exit the app?` message: eventBus.$strings.MessageConfirmAppExit
}) })
if (value) { if (value) {
App.exitApp() App.exitApp()
+4 -2
View File
@@ -43,7 +43,8 @@ export default function ({ store, $db, $socket }, inject) {
} }
if (res.status >= 400) { if (res.status >= 400) {
console.error(`[nativeHttp] ${res.status} status for url "${url}"`) console.error(`[nativeHttp] ${res.status} status for url "${url}"`)
throw new Error(res.data) const message = typeof res.data === 'string' ? res.data : `HTTP ${res.status}`
throw new Error(message)
} }
return res.data return res.data
}) })
@@ -100,7 +101,8 @@ export default function ({ store, $db, $socket }, inject) {
if (retryResponse.status >= 400) { if (retryResponse.status >= 400) {
console.error(`[nativeHttp] Retry request failed with status ${retryResponse.status}`) console.error(`[nativeHttp] Retry request failed with status ${retryResponse.status}`)
throw new Error(retryResponse.data) const message = typeof retryResponse.data === 'string' ? retryResponse.data : `HTTP ${retryResponse.status}`
throw new Error(message)
} }
return retryResponse.data return retryResponse.data
+1 -1
View File
@@ -110,7 +110,7 @@ class ServerSocket extends EventEmitter {
} }
onAuthFailed(data) { onAuthFailed(data) {
console.log('[SOCKET] Auth failed', data) console.log('[SOCKET] Auth failed: ' + (data?.message || 'Unknown reason'))
this.isAuthenticated = false this.isAuthenticated = false
} }
+6 -37
View File
@@ -5,34 +5,8 @@ export const state = () => ({
series: null, series: null,
localMediaProgress: [], localMediaProgress: [],
lastSearch: null, lastSearch: null,
jumpForwardItems: [ jumpForwardSecondsOptions: [5, 10, 15, 30, 60, 120, 300],
{ jumpBackwardsSecondsOptions: [5, 10, 15, 30, 60, 120, 300],
icon: 'forward_5',
value: 5
},
{
icon: 'forward_10',
value: 10
},
{
icon: 'forward_30',
value: 30
}
],
jumpBackwardsItems: [
{
icon: 'replay_5',
value: 5
},
{
icon: 'replay_10',
value: 10
},
{
icon: 'replay_30',
value: 30
}
],
libraryIcons: ['database', 'audiobookshelf', 'books-1', 'books-2', 'book-1', 'microphone-1', 'microphone-3', 'radio', 'podcast', 'rss', 'headphones', 'music', 'file-picture', 'rocket', 'power', 'star', 'heart'], libraryIcons: ['database', 'audiobookshelf', 'books-1', 'books-2', 'book-1', 'microphone-1', 'microphone-3', 'radio', 'podcast', 'rss', 'headphones', 'music', 'file-picture', 'rocket', 'power', 'star', 'heart'],
selectedPlaylistItems: [], selectedPlaylistItems: [],
showPlaylistsAddCreateModal: false, showPlaylistsAddCreateModal: false,
@@ -111,15 +85,7 @@ export const getters = {
if (episodeId != null && lmp.episodeId != episodeId) return false if (episodeId != null && lmp.episodeId != episodeId) return false
return lmp.libraryItemId == libraryItemId return lmp.libraryItemId == libraryItemId
}) })
}, }
getJumpForwardIcon: (state) => (jumpForwardTime) => {
const item = state.jumpForwardItems.find((i) => i.value == jumpForwardTime)
return item ? item.icon : 'forward_10'
},
getJumpBackwardsIcon: (state) => (jumpBackwardsTime) => {
const item = state.jumpBackwardsItems.find((i) => i.value == jumpBackwardsTime)
return item ? item.icon : 'replay_10'
}
} }
export const actions = { export const actions = {
@@ -169,6 +135,9 @@ export const mutations = {
removeItemDownload(state, id) { removeItemDownload(state, id) {
state.itemDownloads = state.itemDownloads.filter((i) => i.id != id) state.itemDownloads = state.itemDownloads.filter((i) => i.id != id)
}, },
clearItemDownloads(state) {
state.itemDownloads = []
},
setBookshelfListView(state, val) { setBookshelfListView(state, val) {
state.bookshelfListView = val state.bookshelfListView = val
}, },
+5 -3
View File
@@ -12,7 +12,10 @@ export const state = () => ({
mobileFilterBy: 'all', mobileFilterBy: 'all',
playbackRate: 1, playbackRate: 1,
collapseSeries: false, collapseSeries: false,
collapseBookSeries: false collapseBookSeries: false,
podcastEpisodesOrderBy: 'publishedAt',
podcastEpisodesOrderDesc: null,
podcastEpisodesFilterBy: 'incomplete'
} }
}) })
@@ -45,7 +48,7 @@ export const getters = {
return state.user.bookmarks.filter((bm) => bm.libraryItemId === libraryItemId) return state.user.bookmarks.filter((bm) => bm.libraryItemId === libraryItemId)
}, },
getUserSetting: (state) => (key) => { getUserSetting: (state) => (key) => {
return state.settings?.[key] || null return state.settings?.[key] ?? null
}, },
getUserCanUpdate: (state) => { getUserCanUpdate: (state) => {
return !!state.user?.permissions?.update return !!state.user?.permissions?.update
@@ -231,7 +234,6 @@ export const mutations = {
state.user = user state.user = user
}, },
setAccessToken(state, accessToken) { setAccessToken(state, accessToken) {
console.log('[user] setAccessToken', accessToken)
state.accessToken = accessToken state.accessToken = accessToken
}, },
removeMediaProgress(state, id) { removeMediaProgress(state, id) {
+23 -1
View File
@@ -63,6 +63,7 @@
"HeaderChapters": "الفصول", "HeaderChapters": "الفصول",
"HeaderCollection": "مجموعة", "HeaderCollection": "مجموعة",
"HeaderCollectionItems": "عناصر المجموعة", "HeaderCollectionItems": "عناصر المجموعة",
"HeaderConfirm": "تأكيد",
"HeaderConnectionStatus": "حالة الاتصال", "HeaderConnectionStatus": "حالة الاتصال",
"HeaderDataSettings": "إعدادات البيانات", "HeaderDataSettings": "إعدادات البيانات",
"HeaderDetails": "التفاصيل", "HeaderDetails": "التفاصيل",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "الجلسات الأخيرة", "HeaderStatsRecentSessions": "الجلسات الأخيرة",
"HeaderTableOfContents": "جدول المحتويات", "HeaderTableOfContents": "جدول المحتويات",
"HeaderUserInterfaceSettings": "إعدادات واجهة المستخدم", "HeaderUserInterfaceSettings": "إعدادات واجهة المستخدم",
"HeaderWelcome": "مرحبا, <strong>{0}</strong>",
"HeaderYourStats": "إحصائياتك", "HeaderYourStats": "إحصائياتك",
"LabelAddToPlaylist": "أضف إلى قائمة التشغيل", "LabelAddToPlaylist": "أضف إلى قائمة التشغيل",
"LabelAddedAt": "أضيفت على", "LabelAddedAt": "أضيفت على",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "عندما ينتهي مؤقت النوم التلقائي، فإن تشغيل العنصر مرة أخرى سيؤدي إلى الإرجاع التلقائي.", "LabelAutoSleepTimerAutoRewindHelp": "عندما ينتهي مؤقت النوم التلقائي، فإن تشغيل العنصر مرة أخرى سيؤدي إلى الإرجاع التلقائي.",
"LabelAutoSleepTimerHelp": "عند تشغيل الوسائط بين أوقات البداية والنهاية المحددة، سيبدأ مؤقت النوم تلقائيًا.", "LabelAutoSleepTimerHelp": "عند تشغيل الوسائط بين أوقات البداية والنهاية المحددة، سيبدأ مؤقت النوم تلقائيًا.",
"LabelBooks": "الكتب", "LabelBooks": "الكتب",
"LabelByAuthor": "بواسطة {0}",
"LabelChapterTrack": "مسار الفصل", "LabelChapterTrack": "مسار الفصل",
"LabelChapters": "الفصول", "LabelChapters": "الفصول",
"LabelClosePlayer": "إغلاق المشغل", "LabelClosePlayer": "إغلاق المشغل",
@@ -146,6 +149,7 @@
"LabelEndOfChapter": "نهاية الفصل", "LabelEndOfChapter": "نهاية الفصل",
"LabelEndTime": "وقت النهاية", "LabelEndTime": "وقت النهاية",
"LabelEpisode": "الحلقة", "LabelEpisode": "الحلقة",
"LabelExplicit": "محتوى صريح",
"LabelFeedURL": "عنوان التغذية", "LabelFeedURL": "عنوان التغذية",
"LabelFile": "الملف", "LabelFile": "الملف",
"LabelFileBirthtime": "وقت انشاء الملف", "LabelFileBirthtime": "وقت انشاء الملف",
@@ -154,6 +158,9 @@
"LabelFinished": "المنجزة", "LabelFinished": "المنجزة",
"LabelFolder": "المجلد", "LabelFolder": "المجلد",
"LabelFontBoldness": "تعريض الخط", "LabelFontBoldness": "تعريض الخط",
"LabelFontFamily": "عائلة الخطوط",
"LabelFontFamilySans": "سانس",
"LabelFontFamilySerif": "سارف",
"LabelFontScale": "نطاق الخط", "LabelFontScale": "نطاق الخط",
"LabelGenre": "التصنيف", "LabelGenre": "التصنيف",
"LabelGenres": "التصانيف", "LabelGenres": "التصانيف",
@@ -173,6 +180,9 @@
"LabelLayout": "التنسيق", "LabelLayout": "التنسيق",
"LabelLayoutAuto": "تلقائي", "LabelLayoutAuto": "تلقائي",
"LabelLayoutSinglePage": "صفحة واحدة", "LabelLayoutSinglePage": "صفحة واحدة",
"LabelLibrarySortByProgress": "المرحلة: الأحدث",
"LabelLibrarySortByProgressFinished": "المرحلة: تم الانتهاء",
"LabelLibrarySortByProgressStarted": "المرحلة: تم البدء",
"LabelLight": "فاتح", "LabelLight": "فاتح",
"LabelLineSpacing": "تباعد الأسطر", "LabelLineSpacing": "تباعد الأسطر",
"LabelListenAgain": "الاستماع مجدداً", "LabelListenAgain": "الاستماع مجدداً",
@@ -232,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "تعديل الوقت المنصرم حسب السرعة", "LabelScaleElapsedTimeBySpeed": "تعديل الوقت المنصرم حسب السرعة",
"LabelSeason": "الموسم", "LabelSeason": "الموسم",
"LabelSelectADevice": "اختيار جهاز", "LabelSelectADevice": "اختيار جهاز",
"LabelSelectMediaType": "اختيار نوع الوسائط",
"LabelSequenceAscending": "تسلسل تصاعدي", "LabelSequenceAscending": "تسلسل تصاعدي",
"LabelSequenceDescending": "تسلسل تنازلي", "LabelSequenceDescending": "تسلسل تنازلي",
"LabelSeries": "المسلسلات", "LabelSeries": "المسلسلات",
@@ -284,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "خادم Audiobookshelf غير متصل", "MessageAudiobookshelfServerNotConnected": "خادم Audiobookshelf غير متصل",
"MessageAudiobookshelfServerRequired": "<strong>مهم!</strong> هذا التطبيق مصمم للعمل مع خادم Audiobookshelf تستضيفه أنت أو شخص تعرفه. هذا التطبيق لا يقدم أي محتوى.", "MessageAudiobookshelfServerRequired": "<strong>مهم!</strong> هذا التطبيق مصمم للعمل مع خادم Audiobookshelf تستضيفه أنت أو شخص تعرفه. هذا التطبيق لا يقدم أي محتوى.",
"MessageBookshelfEmpty": "الرف فارغ", "MessageBookshelfEmpty": "الرف فارغ",
"MessageConfirmAppExit": "هل تريد الخروج من التطبيق؟",
"MessageConfirmDeleteEpisodeDownloadQueue": "هل أنت متأكد من تصفية قائمة انتظار تنزيل الحلقة؟",
"MessageConfirmDeleteLocalEpisode": "هل تريد بالتأكيد إزالة الحلقة المحلية \"{0}\" من جهازك؟ لن يتأثر الملف الموجود على الخادم.", "MessageConfirmDeleteLocalEpisode": "هل تريد بالتأكيد إزالة الحلقة المحلية \"{0}\" من جهازك؟ لن يتأثر الملف الموجود على الخادم.",
"MessageConfirmDeleteLocalFiles": "هل تريد بالتأكيد إزالة الملفات المحلية لهذا العنصر من جهازك؟ الملفات الموجودة على الخادم وتقدمك لن يتأثرا.", "MessageConfirmDeleteLocalFiles": "هل تريد بالتأكيد إزالة الملفات المحلية لهذا العنصر من جهازك؟ الملفات الموجودة على الخادم وتقدمك لن يتأثرا.",
"MessageConfirmDeleteServerConfig": "إزالة إعدادات هذا الخادم؟",
"MessageConfirmDeleteServerEpisode": "هل أنت متأكد من حذف الحلقة \"{0}\" من الخادم؟\nتحذير: سيتم حذف الملف الصوتي.",
"MessageConfirmDisableAutoTimer": "هل أنت متأكد أنك تريد تعطيل المؤقت التلقائي لبقية اليوم؟ سيتم إعادة تفعيل المؤقت في نهاية فترة مؤقت النوم التلقائي هذه، أو إذا قمت بإعادة تشغيل التطبيق.", "MessageConfirmDisableAutoTimer": "هل أنت متأكد أنك تريد تعطيل المؤقت التلقائي لبقية اليوم؟ سيتم إعادة تفعيل المؤقت في نهاية فترة مؤقت النوم التلقائي هذه، أو إذا قمت بإعادة تشغيل التطبيق.",
"MessageConfirmDiscardProgress": "هل أنت متأكد أنك تريد إعادة تعيين تقدمك؟", "MessageConfirmDiscardProgress": "هل أنت متأكد أنك تريد إعادة تعيين تقدمك؟",
"MessageConfirmDownloadUsingCellular": "أنت على وشك التنزيل باستخدام بيانات الجوال. قد يشمل ذلك رسوم استخدام بيانات من شركة الاتصالات. هل ترغب في المتابعة؟", "MessageConfirmDownloadUsingCellular": "أنت على وشك التنزيل باستخدام بيانات الجوال. قد يشمل ذلك رسوم استخدام بيانات من شركة الاتصالات. هل ترغب في المتابعة؟",
"MessageConfirmMarkAsFinished": "هل أنت متأكد أنك تريد وضع علامة \"مكتمل\" على هذا العنصر؟", "MessageConfirmMarkAsFinished": "هل أنت متأكد أنك تريد وضع علامة \"مكتمل\" على هذا العنصر؟",
"MessageConfirmPlaybackTime": "هل تريد بدء التشغيل لـ \"{0}\" في الساعة {1}؟",
"MessageConfirmRemoveBookmark": "هل أنت متأكد أنك تريد إزالة العلامة المرجعية؟", "MessageConfirmRemoveBookmark": "هل أنت متأكد أنك تريد إزالة العلامة المرجعية؟",
"MessageConfirmStreamingUsingCellular": "أنت على وشك البث باستخدام بيانات الجوال. قد يشمل ذلك رسوم استخدام بيانات من شركة الاتصالات. هل ترغب في المتابعة؟", "MessageConfirmStreamingUsingCellular": "أنت على وشك البث باستخدام بيانات الجوال. قد يشمل ذلك رسوم استخدام بيانات من شركة الاتصالات. هل ترغب في المتابعة؟",
"MessageDiscardProgress": "إلغاء التقدم", "MessageDiscardProgress": "إلغاء التقدم",
@@ -297,6 +313,7 @@
"MessageDownloading": "جاري التنزيل...", "MessageDownloading": "جاري التنزيل...",
"MessageDownloadingEpisode": "جاري تنزيل الحلقة", "MessageDownloadingEpisode": "جاري تنزيل الحلقة",
"MessageEpisodesQueuedForDownload": "تمت إضافة {0} حلقة (حلقات) إلى قائمة انتظار التنزيل", "MessageEpisodesQueuedForDownload": "تمت إضافة {0} حلقة (حلقات) إلى قائمة انتظار التنزيل",
"MessageFailedToRefreshToken": "فشل في تحديث الرمز، إعادة تسجيل الدخول مطلوب",
"MessageFeedURLWillBe": "سيكون رابط التغذية هو {0}", "MessageFeedURLWillBe": "سيكون رابط التغذية هو {0}",
"MessageFetching": "جاري الجلب...", "MessageFetching": "جاري الجلب...",
"MessageFollowTheProjectOnGithub": "تابع المشروع على Github", "MessageFollowTheProjectOnGithub": "تابع المشروع على Github",
@@ -324,6 +341,9 @@
"MessageNoSeries": "لا توجد سلاسل", "MessageNoSeries": "لا توجد سلاسل",
"MessageNoUpdatesWereNecessary": "لا حاجة لأي تحديثات", "MessageNoUpdatesWereNecessary": "لا حاجة لأي تحديثات",
"MessageNoUserPlaylists": "ليس لديك أي قوائم تشغيل", "MessageNoUserPlaylists": "ليس لديك أي قوائم تشغيل",
"MessageOldServerAuthReLoginRequired": "تم تحسين المصادقة في الخادم v2.26.0 لضمان الأمان. يُطلب من جميع المستخدمين إعادة تسجيل الدخول.",
"MessageOldServerAuthWarning": "الخادم يستخدم مصادقة قديمة",
"MessageOldServerAuthWarningHelp": "الخادم يعمل على إصدار أقدم من النسخة (2.26.0)، نظام مصادقة أكثر أماناً أضيف في النسخة (2.26.0). يوصى بشدة تحديث الخادم إلى آخر نسخة. إذا قمت بالفعل بتحديث الخادم قم بإعادة تسجيل الدخول لاستخدام المصادقة الجديدة.",
"MessageOldServerConnectionWarning": "تكوين اتصال الخادم يستخدم معرّف مستخدم قديم. يرجى حذف اتصال الخادم هذا وإعادة إضافته.", "MessageOldServerConnectionWarning": "تكوين اتصال الخادم يستخدم معرّف مستخدم قديم. يرجى حذف اتصال الخادم هذا وإعادة إضافته.",
"MessageOldServerConnectionWarningHelp": "لقد قمتَ بإعداد الاتصال بهذا الخادم قبل ترحيل قاعدة البيانات في الإصدار 2.3.0، الصادر في يونيو 2023. سيؤدي تحديث الخادم المستقبلي إلى إلغاء إمكانية تسجيل الدخول باستخدام هذا الاتصال القديم. يُرجى حذف اتصال الخادم الحالي والاتصال مرة أخرى (باستخدام نفس عنوان الخادم وبيانات الاعتماد). إذا كانت لديك أي وسائط مُنزّلة على هذا الجهاز، فيجب تنزيلها مرة أخرى لمزامنتها مع الخادم.", "MessageOldServerConnectionWarningHelp": "لقد قمتَ بإعداد الاتصال بهذا الخادم قبل ترحيل قاعدة البيانات في الإصدار 2.3.0، الصادر في يونيو 2023. سيؤدي تحديث الخادم المستقبلي إلى إلغاء إمكانية تسجيل الدخول باستخدام هذا الاتصال القديم. يُرجى حذف اتصال الخادم الحالي والاتصال مرة أخرى (باستخدام نفس عنوان الخادم وبيانات الاعتماد). إذا كانت لديك أي وسائط مُنزّلة على هذا الجهاز، فيجب تنزيلها مرة أخرى لمزامنتها مع الخادم.",
"MessagePodcastSearchField": "أدخل مصطلح البحث أو عنوان URL الخاص بتغذية RSS", "MessagePodcastSearchField": "أدخل مصطلح البحث أو عنوان URL الخاص بتغذية RSS",
@@ -350,5 +370,7 @@
"ToastPodcastCreateSuccess": "تم إنشاء البودكاست بنجاح", "ToastPodcastCreateSuccess": "تم إنشاء البودكاست بنجاح",
"ToastRSSFeedCloseFailed": "فشل إغلاق مغذّي RSS", "ToastRSSFeedCloseFailed": "فشل إغلاق مغذّي RSS",
"ToastRSSFeedCloseSuccess": "تم إغلاق مغذّي RSS", "ToastRSSFeedCloseSuccess": "تم إغلاق مغذّي RSS",
"ToastStreamingNotAllowedOnCellular": "لا يُسمح بالبث عبر البيانات الخلوية" "ToastStreamingNotAllowedOnCellular": "لا يُسمح بالبث عبر البيانات الخلوية",
"UnitMinutesShort": "{0}د",
"UnitSecondsShort": "{0}ث"
} }
+116 -99
View File
@@ -11,11 +11,11 @@
"ButtonCollections": "Калекцыі", "ButtonCollections": "Калекцыі",
"ButtonConnect": "Падлучыцца", "ButtonConnect": "Падлучыцца",
"ButtonConnectToServer": "Падлучыцца да сервера", "ButtonConnectToServer": "Падлучыцца да сервера",
"ButtonCreate": "Ствараць", "ButtonCreate": "Стварыць",
"ButtonCreateBookmark": "Стварыць закладку", "ButtonCreateBookmark": "Стварыць закладку",
"ButtonCreateNewPlaylist": "Стварыць новы спіс прайгравання", "ButtonCreateNewPlaylist": "Стварыць новы плэй-ліст",
"ButtonDelete": "Выдаліць", "ButtonDelete": "Выдаліць",
"ButtonDeleteLocalEpisode": "Выдаліць лакальны эпізод", "ButtonDeleteLocalEpisode": "Выдаліць лакальны выпуск",
"ButtonDeleteLocalFile": "Выдаліць лакальны файл", "ButtonDeleteLocalFile": "Выдаліць лакальны файл",
"ButtonDeleteLocalItem": "Выдаліць лакальны элемент", "ButtonDeleteLocalItem": "Выдаліць лакальны элемент",
"ButtonDisableAutoTimer": "Адключыць аўтаматычны таймер", "ButtonDisableAutoTimer": "Адключыць аўтаматычны таймер",
@@ -30,15 +30,15 @@
"ButtonLogs": "Журналы", "ButtonLogs": "Журналы",
"ButtonManageLocalFiles": "Кіраванне лакальнымі файламі", "ButtonManageLocalFiles": "Кіраванне лакальнымі файламі",
"ButtonMaskServerAddress": "Схаваць адрас сервера", "ButtonMaskServerAddress": "Схаваць адрас сервера",
"ButtonNewFolder": "Новая тэчка", "ButtonNewFolder": "Новая папка",
"ButtonNextEpisode": "Наступны эпізод", "ButtonNextEpisode": "Наступны выпуск",
"ButtonOk": "Добра", "ButtonOk": "ОК",
"ButtonOpenFeed": "Адкрыць стужку", "ButtonOpenFeed": "Адкрыць стужку",
"ButtonOverride": "Пераазначыць", "ButtonOverride": "Пераазначыць",
"ButtonPause": аўза", "ButtonPause": рыпыніць",
"ButtonPlay": "Прайграць", "ButtonPlay": "Прайграць",
"ButtonPlayEpisode": "Прайграць эпізод", "ButtonPlayEpisode": "Прайграць выпуск",
"ButtonPlaylists": "Спісы прайгравання", "ButtonPlaylists": "Плэй-лісты",
"ButtonRead": "Чытаць", "ButtonRead": "Чытаць",
"ButtonReadLess": "Чытаць менш", "ButtonReadLess": "Чытаць менш",
"ButtonReadMore": "Чытаць больш", "ButtonReadMore": "Чытаць больш",
@@ -50,40 +50,41 @@
"ButtonSendEbookToDevice": "Адправіць электронную кнігу на прыладу", "ButtonSendEbookToDevice": "Адправіць электронную кнігу на прыладу",
"ButtonSeries": "Серыі", "ButtonSeries": "Серыі",
"ButtonSetTimer": "Устанавіць таймер", "ButtonSetTimer": "Устанавіць таймер",
"ButtonStream": "Трансляцыя", "ButtonStream": "Трансліраваць",
"ButtonSubmit": "Адправіць", "ButtonSubmit": "Адправіць",
"ButtonSwitchServerUser": "Змяніць Сервер/Карыстальніка", "ButtonSwitchServerUser": "Змяніць сервер/карыстальніка",
"ButtonUnmaskServerAddress": "Паказаць адрас сервера", "ButtonUnmaskServerAddress": "Паказаць адрас сервера",
"ButtonUserStats": "Статыстыка карыстальніка", "ButtonUserStats": "Статыстыка карыстальніка",
"ButtonYes": "Так", "ButtonYes": "Так",
"HeaderAccount": "Уліковы запіс", "HeaderAccount": "Уліковы запіс",
"HeaderAdvanced": "Дадаткова", "HeaderAdvanced": "Дадаткова",
"HeaderAndroidAutoSettings": "Налады Android Auto", "HeaderAndroidAutoSettings": "Налады Android Auto",
"HeaderAudioTracks": "Аўдыядарожкі", "HeaderAudioTracks": "Аўдыятрэкі",
"HeaderChapters": "Раздзелы", "HeaderChapters": "Раздзелы",
"HeaderCollection": "Калекцыя", "HeaderCollection": "Калекцыя",
"HeaderCollectionItems": "Элементы калекцыі", "HeaderCollectionItems": "Элементы калекцыі",
"HeaderConfirm": "Пацвердзіць",
"HeaderConnectionStatus": "Стан злучэння", "HeaderConnectionStatus": "Стан злучэння",
"HeaderDataSettings": "Налады дадзеных", "HeaderDataSettings": "Налады даных",
"HeaderDetails": "Падрабязнасці", "HeaderDetails": "Падрабязнасці",
"HeaderDownloads": "Спампоўкі", "HeaderDownloads": "Спампоўкі",
"HeaderEbookFiles": "Файлы электронных кніг", "HeaderEbookFiles": "Файлы электронных кніг",
"HeaderEpisodes": "Эпізоды", "HeaderEpisodes": "Выпускі",
"HeaderEreaderSettings": "Налады прылады для чытання", "HeaderEreaderSettings": "Налады прылады для чытання",
"HeaderLatestEpisodes": "Апошнія эпізоды", "HeaderLatestEpisodes": "Апошнія выпускі",
"HeaderLibraries": "Бібліятэкі", "HeaderLibraries": "Бібліятэкі",
"HeaderLocalFolders": "Лакальныя тэчкі", "HeaderLocalFolders": "Лакальныя папкі",
"HeaderLocalLibraryItems": "Элементы лакальнай бібліятэкі", "HeaderLocalLibraryItems": "Элементы лакальнай бібліятэкі",
"HeaderNewPlaylist": "Новы спіс прайгравання", "HeaderNewPlaylist": "Новы плэй-ліст",
"HeaderOpenRSSFeed": "Адкрыць RSS-стужку", "HeaderOpenRSSFeed": "Адкрыць RSS-стужку",
"HeaderPlaybackSettings": "Налады прайгравання", "HeaderPlaybackSettings": "Налады прайгравання",
"HeaderPlaylist": "Спіс прайгравання", "HeaderPlaylist": "Плэй-ліст",
"HeaderPlaylistItems": "Элементы спіса прайгравання", "HeaderPlaylistItems": "Элементы плэй-ліста",
"HeaderProgressSyncFailed": "Не ўдалося сінхранізаваць прагрэс", "HeaderProgressSyncFailed": "Не ўдалося сінхранізаваць прагрэс",
"HeaderRSSFeed": "RSS-стужка", "HeaderRSSFeed": "RSS-стужка",
"HeaderRSSFeedGeneral": "Падрабязнасці RSS", "HeaderRSSFeedGeneral": "Падрабязнасці RSS",
"HeaderRSSFeedIsOpen": "RSS-стужка адкрытая", "HeaderRSSFeedIsOpen": "RSS-стужка адкрытая",
"HeaderSelectDownloadLocation": "Абраць месцазнаходжання для спампоўкі", "HeaderSelectDownloadLocation": "Выберыце размяшчэнне для спампоўкі",
"HeaderSettings": "Налады", "HeaderSettings": "Налады",
"HeaderSleepTimer": "Таймер сну", "HeaderSleepTimer": "Таймер сну",
"HeaderSleepTimerSettings": "Налады таймера сну", "HeaderSleepTimerSettings": "Налады таймера сну",
@@ -91,8 +92,9 @@
"HeaderStatsRecentSessions": "Апошнія сеансы", "HeaderStatsRecentSessions": "Апошнія сеансы",
"HeaderTableOfContents": "Змест", "HeaderTableOfContents": "Змест",
"HeaderUserInterfaceSettings": "Налады інтэрфейсу карыстальніка", "HeaderUserInterfaceSettings": "Налады інтэрфейсу карыстальніка",
"HeaderWelcome": "Вітаем, <strong>{0}</strong>",
"HeaderYourStats": "Ваша статыстыка", "HeaderYourStats": "Ваша статыстыка",
"LabelAddToPlaylist": "Дадаць у спіс прайгравання", "LabelAddToPlaylist": "Дадаць у плэй-ліст",
"LabelAddedAt": "Дата дабаўлення", "LabelAddedAt": "Дата дабаўлення",
"LabelAddedDate": "Дададзена {0}", "LabelAddedDate": "Дададзена {0}",
"LabelAll": "Усе", "LabelAll": "Усе",
@@ -101,72 +103,76 @@
"LabelAndroidAutoBrowseLimitForGrouping": "Ліміт алфавітнага выпадальнага спісу", "LabelAndroidAutoBrowseLimitForGrouping": "Ліміт алфавітнага выпадальнага спісу",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "Не выкарыстоўваць алфавітны выпадальны спіс, калі элементаў для паказу менш за гэтую колькасць", "LabelAndroidAutoBrowseLimitForGroupingHelp": "Не выкарыстоўваць алфавітны выпадальны спіс, калі элементаў для паказу менш за гэтую колькасць",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "Парадак кніг у серыі", "LabelAndroidAutoBrowseSeriesSequenceOrder": "Парадак кніг у серыі",
"LabelAskConfirmation": "Запытваць пацверджанне", "LabelAskConfirmation": "Запытваць пацвярджэнне",
"LabelAuthor": "Аўтар", "LabelAuthor": "Аўтар",
"LabelAuthorFirstLast": "Аўтар (Імя Прозвішча)", "LabelAuthorFirstLast": "Аўтар (імя, прозвішча)",
"LabelAuthorLastFirst": "Аўтар (Прозвішча, Імя)", "LabelAuthorLastFirst": "Аўтар (прозвішча, імя)",
"LabelAuthors": "Аўтары", "LabelAuthors": "Аўтары",
"LabelAutoDownloadEpisodes": "Аўтаматычнае спампаванне эпізодаў", "LabelAutoDownloadEpisodes": "Аўтаматычна спампоўваць выпускі",
"LabelAutoRewindTime": "Час аўтаматычнай перамоткі назад", "LabelAutoRewindTime": "Час аўтаматычнай перамоткі назад",
"LabelAutoSleepTimer": "Аўтаматычны таймер сну", "LabelAutoSleepTimer": "Аўтаматычны таймер сну",
"LabelAutoSleepTimerAutoRewind": "Аўтаматычная перамотка таймера сну", "LabelAutoSleepTimerAutoRewind": "Аўтаматычная перамотка таймера сну",
"LabelAutoSleepTimerAutoRewindHelp": "Калі таймер аўтаматычнага сну скончыцца, паўторнае прайграванне элемента аўтаматычна перамотвае вашу пазіцыю.", "LabelAutoSleepTimerAutoRewindHelp": "Пасля заканчэння аўтаматычнага таймера сну паўторнае прайграванне элемента аўтаматычна перамотвае вашу пазіцыю.",
"LabelAutoSleepTimerHelp": "Калі медыя прайграецца ў вызначаны час паміж пачаткам і канцом, таймер сну будзе запушчаны аўтаматычна.", "LabelAutoSleepTimerHelp": "Калі медыя прайграецца ў вызначаны час паміж пачаткам і канцом, таймер сну будзе запушчаны аўтаматычна.",
"LabelBooks": "Кнігі", "LabelBooks": "Кнігі",
"LabelChapterTrack": "Дарожка раздзела", "LabelByAuthor": " {0}",
"LabelChapterTrack": "Трэк раздзела",
"LabelChapters": "Раздзелы", "LabelChapters": "Раздзелы",
"LabelClosePlayer": "Зачыніць прайгравальнік", "LabelClosePlayer": "Закрыць прайгравальнік",
"LabelCollapseSeries": "Згарнуць серыі", "LabelCollapseSeries": "Згарнуць серыі",
"LabelComplete": "Завершана", "LabelComplete": "Завяршыць",
"LabelContinueBooks": "Працягнуць кнігі", "LabelContinueBooks": "Працягнуць кнігі",
"LabelContinueEpisodes": "Працягнуць эпізоды", "LabelContinueEpisodes": "Працягнуць выпускі",
"LabelContinueListening": "Працягваць слухаць", "LabelContinueListening": "Працяг праслухоўвання",
"LabelContinueReading": "Працягнуць чытанне", "LabelContinueReading": "Працягнуць чытанне",
"LabelContinueSeries": "Працягнуць серыі", "LabelContinueSeries": "Працягнуць серыі",
"LabelCustomTime": "Свой час", "LabelCustomTime": "Свой час",
"LabelDescription": "Апісанне", "LabelDescription": "Апісанне",
"LabelDisableAudioFadeOut": "Адключыць змяншэнне гучнасці", "LabelDisableAudioFadeOut": "Адключыць змяншэнне гучнасці",
"LabelDisableAudioFadeOutHelp": "Гучнасць аўдыё пачне змяншацца, калі на таймеры сну застанецца менш за 1 хвіліну. Уключыце гэтую наладу, каб пазбегнуць змяншэння гучнасці.", "LabelDisableAudioFadeOutHelp": "Гучнасць гуку пачне змяншацца, калі на таймеры сну застанецца менш за 1 хвіліну. Уключыце гэтую наладу, каб пазбегнуць змяншэння гучнасці.",
"LabelDisableAutoRewind": "Адключыць аўтаматычную перамотку назад", "LabelDisableAutoRewind": "Адключыць аўтаматычную перамотку назад",
"LabelDisableShakeToReset": "Адключыць скід страсеннем", "LabelDisableShakeToReset": "Адключыць скід устрэсваннем",
"LabelDisableShakeToResetHelp": "Калі патрасці прыладу падчас працы таймера АБО на працягу 2 хвілін пасля заканчэння таймера, таймер сну скідваецца. Уключыце гэтую наладу, каб адключыць скід страсеннем.", "LabelDisableShakeToResetHelp": "Калі патрэсці прыладу падчас працы таймера АБО на працягу 2 хвілін пасля заканчэння таймера, таймер сну скідваецца. Уключыце гэтую наладу, каб адключыць скід устрэсваннем.",
"LabelDisableVibrateOnReset": "Адключыць вібрацыю пры скідзе", "LabelDisableVibrateOnReset": "Адключыць вібрацыю пры скідзе",
"LabelDisableVibrateOnResetHelp": "Калі таймер сну скідваецца, ваша прылада будзе вібраваць. Уключыце гэтую наладу, каб адключыць вібрацыю пры скідзе таймера сну.", "LabelDisableVibrateOnResetHelp": "Калі таймер сну скідваецца, ваша прылада будзе вібраваць. Уключыце гэтую наладу, каб адключыць вібрацыю пры скідзе таймера сну.",
"LabelDiscover": "Знайсці", "LabelDiscover": "Знаходкі",
"LabelDownload": "Спампаваць", "LabelDownload": "Спампаваць",
"LabelDownloadUsingCellular": "Спампоўка праз мабільны інтэрнэт", "LabelDownloadUsingCellular": "Спампоўваць праз мабільны інтэрнэт",
"LabelDownloaded": "Спампавана", "LabelDownloaded": "Спампавана",
"LabelDuration": "Працягласць", "LabelDuration": "Працягласць",
"LabelEbook": "Электронная кніга", "LabelEbook": "Электронная кніга",
"LabelEbooks": "Электронныя кнігі", "LabelEbooks": "Электронныя кнігі",
"LabelEnable": "Уключыць", "LabelEnable": "Уключыць",
"LabelEnableMp3IndexSeeking": "Уключыць індэксаванне для перамоткі MP3", "LabelEnableMp3IndexSeeking": "Уключыць пошук па індэксе mp3",
"LabelEnableMp3IndexSeekingHelp": "Гэтую наладу варта ўключаць толькі, калі ў вас ёсць MP3-файлы, якія няправільна перамотваюцца. Хутчэй за ўсё, гэта выклікана файламі з зменным бітрэйтам (VBR). Гэтая налада прымусова ўключае індэксаванне для перамоткі, пры якім падчас чытання файла ствараецца адпаведнасць паміж часам і байтамі. У некаторых выпадках з вялікімі MP3-файламі можа быць затрымка пры перемотцы да канца файла.", "LabelEnableMp3IndexSeekingHelp": "Гэтую наладу варта ўключаць толькі, калі ў вас ёсць MP3-файлы, якія няправільна перамотваюцца. Хутчэй за ўсё, гэта выклікана файламі са зменным бітрэйтам (VBR). Гэтая налада прымусова ўключае індэксаванне для перамоткі, пры якім падчас чытання файла ствараецца адпаведнасць паміж часам і байтамі. У некаторых выпадках з вялікімі MP3-файламі можа быць затрымка пры перемотцы да канца файла.",
"LabelEnd": "Канец", "LabelEnd": "Канец",
"LabelEndOfChapter": "Канец раздзела", "LabelEndOfChapter": "Канец раздзела",
"LabelEndTime": "Час заканчэння", "LabelEndTime": "Час заканчэння",
"LabelEpisode": "Эпізод", "LabelEpisode": "Выпуск",
"LabelExplicit": "Відверты", "LabelExplicit": "Непрыстойнае",
"LabelFeedURL": "URL стужкі", "LabelFeedURL": "URL стужкі",
"LabelFile": "Файл", "LabelFile": "Файл",
"LabelFileBirthtime": "Час стварэння файла", "LabelFileBirthtime": "Час стварэння файла",
"LabelFileModified": "Час змянення файла", "LabelFileModified": "Час змянення файла",
"LabelFilename": "Імя файла", "LabelFilename": "Назва файла",
"LabelFinished": "Скончана", "LabelFinished": "Завершана",
"LabelFolder": "Тэчка", "LabelFolder": "Папка",
"LabelFontBoldness": "Таўшчыня шрыфта", "LabelFontBoldness": "Таўшчыня шрыфта",
"LabelFontFamily": "Сямейства шрыфтоў",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Памер шрыфту", "LabelFontScale": "Памер шрыфту",
"LabelGenre": "Жанр", "LabelGenre": "Жанр",
"LabelGenres": "Жанры", "LabelGenres": "Жанры",
"LabelHapticFeedback": "Тактыльная зваротная сувязь", "LabelHapticFeedback": "Тактыльная зваротная сувязь",
"LabelHasEbook": "Мае электронную кнігу", "LabelHasEbook": "Мае электронную кнігу",
"LabelHasSupplementaryEbook": "Мае дадатковую электронную кнігу", "LabelHasSupplementaryEbook": "Мае дадатковую электронную кнігу",
"LabelHeavy": "Моцна", "LabelHeavy": "Моцная",
"LabelHigh": "Высокі", "LabelHigh": "Высокая",
"LabelHost": "Хост", "LabelHost": "Хост",
"LabelInProgress": "У працэсе", "LabelInProgress": "У працэсе",
"LabelIncomplete": "Незавершана", "LabelIncomplete": "Незавершана",
"LabelInternalAppStorage": "Унутраная памяць прыкладання", "LabelInternalAppStorage": "Унутраная памяць праграмы",
"LabelJumpBackwardsTime": "Час пераходу назад", "LabelJumpBackwardsTime": "Час пераходу назад",
"LabelJumpForwardsTime": "Час пераходу наперад", "LabelJumpForwardsTime": "Час пераходу наперад",
"LabelKeepScreenAwake": "Трымаць экран уключаным", "LabelKeepScreenAwake": "Трымаць экран уключаным",
@@ -174,22 +180,25 @@
"LabelLayout": "Знешні выгляд", "LabelLayout": "Знешні выгляд",
"LabelLayoutAuto": "Аўто", "LabelLayoutAuto": "Аўто",
"LabelLayoutSinglePage": "Аднабаковы", "LabelLayoutSinglePage": "Аднабаковы",
"LabelLight": "Святло", "LabelLibrarySortByProgress": "Прагрэс: апошняе абнаўленне",
"LabelLibrarySortByProgressFinished": "Прагрэс: завершана",
"LabelLibrarySortByProgressStarted": "Прагрэс: пачата",
"LabelLight": "Лёгкая",
"LabelLineSpacing": "Міжрадковы інтэрвал", "LabelLineSpacing": "Міжрадковы інтэрвал",
"LabelListenAgain": "Паслухаць зноў", "LabelListenAgain": "Паслухаць зноў",
"LabelLocalBooks": "Лакальныя кнігі", "LabelLocalBooks": "Лакальныя кнігі",
"LabelLocalPodcasts": "Лакальныя падкасты", "LabelLocalPodcasts": "Лакальныя падкасты",
"LabelLockOrientation": "Зафіксаваць арыентацыю", "LabelLockOrientation": "Зафіксаваць арыентацыю",
"LabelLockPlayer": "Заблакаваць прайгравальнік", "LabelLockPlayer": "Заблакіраваць прайгравальнік",
"LabelLow": "Нізкі", "LabelLow": "Нізкая",
"LabelMediaType": "Тып медыя", "LabelMediaType": "Тып медыя",
"LabelMedium": "Сярэдні", "LabelMedium": "Сярэдняя",
"LabelMissing": "Адсутнічае", "LabelMissing": "Адсутнічае",
"LabelMore": "Больш", "LabelMore": "Больш",
"LabelMoreInfo": "Больш інфармацыі", "LabelMoreInfo": "Больш інфармацыі",
"LabelName": "Імя", "LabelName": "Назва",
"LabelNarrator": "Чытальнік", "LabelNarrator": "Дыктар",
"LabelNarrators": "Чытальнікі", "LabelNarrators": "Дыктары",
"LabelNavigateWithVolume": "Навігацыя з дапамогай клавіш гучнасці", "LabelNavigateWithVolume": "Навігацыя з дапамогай клавіш гучнасці",
"LabelNavigateWithVolumeMirrored": "Люстраваны", "LabelNavigateWithVolumeMirrored": "Люстраваны",
"LabelNavigateWithVolumeWhilePlaying": "Дазволіць клавішам гучнасці кіраваць навігацыяй падчас прайгравання", "LabelNavigateWithVolumeWhilePlaying": "Дазволіць клавішам гучнасці кіраваць навігацыяй падчас прайгравання",
@@ -197,13 +206,13 @@
"LabelNavigateWithVolumeWhilePlayingEnabled": "Уключана", "LabelNavigateWithVolumeWhilePlayingEnabled": "Уключана",
"LabelNever": "Ніколі", "LabelNever": "Ніколі",
"LabelNewestAuthors": "Новыя аўтары", "LabelNewestAuthors": "Новыя аўтары",
"LabelNewestEpisodes": "Новыя эпізоды", "LabelNewestEpisodes": "Найноўшыя выпускі",
"LabelNo": "Не", "LabelNo": "Не",
"LabelNotFinished": "Не скончана", "LabelNotFinished": "Незавершана",
"LabelNotStarted": "Не пачата", "LabelNotStarted": "Не пачата",
"LabelNumEpisodes": "{0} эпізодаў", "LabelNumEpisodes": "{0} выпускаў",
"LabelNumEpisodesIncomplete": "{0} эпізодаў, {1} незавершаных", "LabelNumEpisodesIncomplete": "{0} выпускаў, {1} незавершаных",
"LabelNumberOfEpisodes": "# з эпізодаў", "LabelNumberOfEpisodes": "Колькасць выпускаў",
"LabelOff": "Выключына", "LabelOff": "Выключына",
"LabelOn": "Уключана", "LabelOn": "Уключана",
"LabelPassword": "Пароль", "LabelPassword": "Пароль",
@@ -229,30 +238,31 @@
"LabelReadAgain": "Чытаць зноў", "LabelReadAgain": "Чытаць зноў",
"LabelRecentSeries": "Апошнія серыі", "LabelRecentSeries": "Апошнія серыі",
"LabelRecentlyAdded": "Нядаўна дададзеныя", "LabelRecentlyAdded": "Нядаўна дададзеныя",
"LabelRemoveFromPlaylist": "Выдаліць з спіса прайгравання", "LabelRemoveFromPlaylist": "Выдаліць з плэй-ліста",
"LabelScaleElapsedTimeBySpeed": "Прыстасаваць мінулы час да хуткасці", "LabelScaleElapsedTimeBySpeed": "Маштабаваць мінулы час па хуткасці",
"LabelSeason": "Сезон", "LabelSeason": "Сезон",
"LabelSelectADevice": "Выбраць прыладу", "LabelSelectADevice": "Выбраць прыладу",
"LabelSequenceAscending": "Паслядоўнасць па ўзрастанні", "LabelSelectMediaType": "Выберыце тып медыя",
"LabelSequenceDescending": "Паслядоўнасць па змяншэнні", "LabelSequenceAscending": "Па ўзрастанні",
"LabelSequenceDescending": "Па ўбыванні",
"LabelSeries": "Серыі", "LabelSeries": "Серыі",
"LabelServerAddress": "Адрас сервера", "LabelServerAddress": "Адрас сервера",
"LabelSetEbookAsPrimary": "Зрабіць асноўным", "LabelSetEbookAsPrimary": "Зрабіць асноўным",
"LabelSetEbookAsSupplementary": "Зрабіць дадатковым", "LabelSetEbookAsSupplementary": "Зрабіць дадатковым",
"LabelShakeSensitivity": "Чуйнасць да трасення", "LabelShakeSensitivity": "Адчувальнасць да ўстрэсвання",
"LabelShowAll": "Паказаць усё", "LabelShowAll": "Паказаць усё",
"LabelSize": "Памер", "LabelSize": "Памер",
"LabelSleepTimer": "Таймер сну", "LabelSleepTimer": "Таймер сну",
"LabelSleepTimerAlmostDoneChime": "Праіграйце звонак, калі амаль скончана", "LabelSleepTimerAlmostDoneChime": "Гукавы сігнал, калі амаль завершаны",
"LabelSleepTimerAlmostDoneChimeHelp": амякчыць звонак, калі на таймеры сну застанецца 30 секунд", "LabelSleepTimerAlmostDoneChimeHelp": райграць званок, калі да канца таймера сну застанецца 30 секунд",
"LabelStart": "Пачаць", "LabelStart": "Пачатак",
"LabelStartTime": "Час пачатку", "LabelStartTime": "Час пачатку",
"LabelStatsBestDay": "Лепшы дзень", "LabelStatsBestDay": "Найлепшы дзень",
"LabelStatsDailyAverage": "У сярэднім за дзень", "LabelStatsDailyAverage": "У сярэднім за дзень",
"LabelStatsDays": "Дзён", "LabelStatsDays": "Дзён",
"LabelStatsDaysListened": "Дзён праслухана", "LabelStatsDaysListened": "Дзён праслухана",
"LabelStatsInARow": "без перапынку", "LabelStatsInARow": "без перапынку",
"LabelStatsItemsFinished": "Скончаныя элементы", "LabelStatsItemsFinished": "Элементаў завершана",
"LabelStatsMinutes": "хвілін", "LabelStatsMinutes": "хвілін",
"LabelStatsMinutesListening": "Хвілін праслухоўвання", "LabelStatsMinutesListening": "Хвілін праслухоўвання",
"LabelStatsWeekListening": "Праслухана за тыдзень", "LabelStatsWeekListening": "Праслухана за тыдзень",
@@ -260,44 +270,49 @@
"LabelTag": "Метка", "LabelTag": "Метка",
"LabelTags": "Меткі", "LabelTags": "Меткі",
"LabelTheme": "Тэма", "LabelTheme": "Тэма",
"LabelThemeBlack": "Чорны", "LabelThemeBlack": "Чорная",
"LabelThemeDark": "Цёмная", "LabelThemeDark": "Цёмная",
"LabelThemeLight": "Светлая", "LabelThemeLight": "Светлая",
"LabelTimeRemaining": "Засталося {0}", "LabelTimeRemaining": "Засталося {0}",
"LabelTitle": "Назва", "LabelTitle": "Загаловак",
"LabelTotalSize": "Агульны памер", "LabelTotalSize": "Агульны памер",
"LabelTotalTrack": "Агульная колькасць дарожак", "LabelTotalTrack": "Усяго трэкаў",
"LabelTracks": "Дарожкі", "LabelTracks": "Трэкі",
"LabelType": "Тып", "LabelType": "Тып",
"LabelUnknown": "Невядома", "LabelUnknown": "Невядома",
"LabelUnlockPlayer": "Разблакаваць прайгравальнік", "LabelUnlockPlayer": "Разблакіраваць прайгравальнік",
"LabelUseBookshelfView": "Выкарыстоўваць выгляд кніжнай паліцы", "LabelUseBookshelfView": "Паказваць кніжныя паліцы",
"LabelUser": "Карыстальнік", "LabelUser": "Карыстальнік",
"LabelUsername": "Імя карыстальніка", "LabelUsername": "Імя карыстальніка",
"LabelVeryHigh": "Вельмі высокі", "LabelVeryHigh": "Вельмі высокая",
"LabelVeryLow": "Вельмі нізкі", "LabelVeryLow": "Вельмі нізкая",
"LabelYearReviewHide": "Схаваць вынікі года", "LabelYearReviewHide": "Схаваць вынікі года",
"LabelYearReviewShow": "Азнаёміцца з вынікамі года", "LabelYearReviewShow": "Азнаёміцца з вынікамі года",
"LabelYourBookmarks": "Вашы закладкі", "LabelYourBookmarks": "Вашы закладкі",
"LabelYourProgress": "Ваш прагрэс", "LabelYourProgress": "Ваш прагрэс",
"MessageAndroid10Downloads": "На Android 10 і ніжэй для спампоўкі будзе выкарыстоўвацца ўнутраная памяць прыкладання.", "MessageAndroid10Downloads": "На Android 10 і ніжэй для спамповак будзе выкарыстоўвацца ўнутраная памяць праграмы.",
"MessageAttemptingServerConnection": "Спроба злучэння з серверам...", "MessageAttemptingServerConnection": "Спроба злучэння з серверам...",
"MessageAudiobookshelfServerNotConnected": "Сервер Audiobookshelf не падлучаны", "MessageAudiobookshelfServerNotConnected": "Сервер Audiobookshelf не падлучаны",
"MessageAudiobookshelfServerRequired": "<strong>Важна!</strong> Гэта праграма прызначана для працы з серверам Audiobookshelf, які вы ці хтосьці іншы абслугоўваеце. Праграма не змяшчае ўласнага кантэнту.", "MessageAudiobookshelfServerRequired": "<strong>Важна!</strong> Гэта праграма прызначана для працы з серверам Audiobookshelf, які вы ці хтосьці іншы абслугоўваеце. Праграма не змяшчае ўласнага кантэнту.",
"MessageBookshelfEmpty": "Кніжная паліца пустая", "MessageBookshelfEmpty": "Кніжная паліца пустая",
"MessageConfirmDeleteLocalEpisode": "Выдаліць лакальны эпізод \"{0}\" з вашай прылады? Файл на серверы застанецца нязменным.", "MessageConfirmAppExit": "Вы хацелі выйсці з праграмы?",
"MessageConfirmDeleteLocalFiles": "Выдаліць лакальныя файлы гэтага элемента з вашай прылады? Файлы на серверы і ваш прагрэс застануцца нязменнымі.", "MessageConfirmDeleteEpisodeDownloadQueue": "Вы ўпэўнены, што хочаце ачысціць чаргу спампоўвання выпускаў?",
"MessageConfirmDisableAutoTimer": "Вы ўпэўнены, што жадаеце адключыць аўтаматычны таймер на сённяшні дзень? Таймер будзе зноў уключаны па заканчэнні гэтага перыяду аўтаматычнага таймера сну або пры перазапуску праграмы.", "MessageConfirmDeleteLocalEpisode": "Выдаліць лакальны выпуск \"{0}\" з прылады? Файл на серверы гэта не закране.",
"MessageConfirmDiscardProgress": "Вы ўпэўнены, што жадаеце скінуць свой прагрэс?", "MessageConfirmDeleteLocalFiles": "Выдаліць лакальныя файлы гэтага элемента з прылады? Файлы на серверы і прагрэс застануцца нязменнымі.",
"MessageConfirmDownloadUsingCellular": "Вы збіраецеся спампаваць, выкарыстоўваючы мабільны інтэрнэт. Гэта можа прывесці да дадатковых плат ад аператара. Вы жадаеце працягнуць?", "MessageConfirmDeleteServerConfig": "Выдаліць гэту канфігурацыю сервера?",
"MessageConfirmMarkAsFinished": "Вы ўпэўнены, што жадаеце пазначыць гэты элемент як скончаны?", "MessageConfirmDeleteServerEpisode": "Вы ўпэўнены, што хочаце выдаліць выпуск \"{0}\" з сервера?\nУвага: будзе выдалены аўдыяфайл.",
"MessageConfirmRemoveBookmark": "Вы ўпэўнены, што жадаеце выдаліць закладку?", "MessageConfirmDisableAutoTimer": "Вы ўпэўнены, што хочаце адключыць аўтаматычны таймер на сённяшні дзень? Таймер будзе зноў уключаны па заканчэнні перыяду гэтага аўтаматычнага таймера сну або пры перазапуску праграмы.",
"MessageConfirmStreamingUsingCellular": "Вы збіраецеся трансляваць кантэнт праз мабільны інтэрнэт. Гэта можа прывесці да дадатковых платаў ад вашага аператара. Вы жадаеце працягнуць?", "MessageConfirmDiscardProgress": "Вы ўпэўнены, што хочаце скінуць прагрэс?",
"MessageConfirmDownloadUsingCellular": "Вы збіраецеся спампаваць праз мабільны інтэрнэт. Гэта можа прывесці да дадатковых плат ад аператара. Хочаце працягнуць?",
"MessageConfirmMarkAsFinished": "Вы ўпэўнены, што хочаце пазначыць гэты элемент як завершаны?",
"MessageConfirmPlaybackTime": "Пачаць прайграванне \"{0}\" у {1}?",
"MessageConfirmRemoveBookmark": "Вы ўпэўнены, што хочаце выдаліць закладку?",
"MessageConfirmStreamingUsingCellular": "Вы збіраецеся трансляваць кантэнт праз мабільны інтэрнэт. Гэта можа прывесці да дадатковых плат ад аператара. Хочаце працягнуць?",
"MessageDiscardProgress": "Скінуць прагрэс", "MessageDiscardProgress": "Скінуць прагрэс",
"MessageDownloadCompleteProcessing": "Спампоўка завершана. Апрацоўка...", "MessageDownloadCompleteProcessing": "Спампоўванне завершана. Апрацоўка...",
"MessageDownloading": "Спампоўванне...", "MessageDownloading": "Спампоўванне...",
"MessageDownloadingEpisode": "Спампоўка эпізоду", "MessageDownloadingEpisode": "Спампоўванне выпуску",
"MessageEpisodesQueuedForDownload": "{0} эпізод(аў) у чарзе для спампоўкі", "MessageEpisodesQueuedForDownload": "{0} выпуск(-аў) у чарзе спампоўвання",
"MessageFailedToRefreshToken": "Не ўдалося абнавіць токен, патрабуецца паўторны ўваход", "MessageFailedToRefreshToken": "Не ўдалося абнавіць токен, патрабуецца паўторны ўваход",
"MessageFeedURLWillBe": "URL стужкі будзе {0}", "MessageFeedURLWillBe": "URL стужкі будзе {0}",
"MessageFetching": "Атрыманне...", "MessageFetching": "Атрыманне...",
@@ -306,8 +321,8 @@
"MessageItemMissing": "Элемент адсутнічае і патрабуе выпраўлення на серверы. Звычайна элемент пазначаецца як адсутны, таму што файлавыя шляхі недаступныя.", "MessageItemMissing": "Элемент адсутнічае і патрабуе выпраўлення на серверы. Звычайна элемент пазначаецца як адсутны, таму што файлавыя шляхі недаступныя.",
"MessageLoading": "Загрузка...", "MessageLoading": "Загрузка...",
"MessageLoadingServerData": "Загрузка даных з сервера...", "MessageLoadingServerData": "Загрузка даных з сервера...",
"MessageLocalFolderDescription": "\"Унутраная памяць прыкладання\" даступная толькі для гэтага прыкладання. Гэта прыкладанне працуе толькі з медыя, якія былі спампаваныя непасрэдна праз яго. Каб іншыя прыкладанні маглі атрымаць доступ да медыя, спампаванага гэтым прыкладаннем, можна выкарыстоўваць агульныя тэчкі.", "MessageLocalFolderDescription": "\"Унутраная памяць праграмы\" даступная толькі для гэтай праграмы. Гэта праграма працуе толькі з медыя, якія былі спампаваныя непасрэдна праз яго. Каб іншыя праграмы маглі атрымаць доступ да медыя, спампаванага гэтай праграмай, можна выкарыстоўваць абагуленыя папкі.",
"MessageMarkAsFinished": "Пазначыць як скончана", "MessageMarkAsFinished": "Пазначыць як завершаную",
"MessageMediaLinkedToADifferentServer": "Медыя звязаны з серверам Audiobookshelf па іншым адрасе ({0}). Сінхранізацыя прагрэсу адбудзецца пры падключэнні да гэтага адрасу сервера.", "MessageMediaLinkedToADifferentServer": "Медыя звязаны з серверам Audiobookshelf па іншым адрасе ({0}). Сінхранізацыя прагрэсу адбудзецца пры падключэнні да гэтага адрасу сервера.",
"MessageMediaLinkedToADifferentUser": "Медыя звязаны з гэтым серверам, але быў спампаваны іншым карыстальнікам. Прагрэс будзе сінхранізавацца толькі для карыстальніка, які яго спампаваў.", "MessageMediaLinkedToADifferentUser": "Медыя звязаны з гэтым серверам, але быў спампаваны іншым карыстальнікам. Прагрэс будзе сінхранізавацца толькі для карыстальніка, які яго спампаваў.",
"MessageMediaLinkedToServer": "Звязана з серверам {0}", "MessageMediaLinkedToServer": "Звязана з серверам {0}",
@@ -320,12 +335,12 @@
"MessageNoItemsFound": "Элементы не знойдзены", "MessageNoItemsFound": "Элементы не знойдзены",
"MessageNoListeningSessions": "Няма сеансаў праслухоўвання", "MessageNoListeningSessions": "Няма сеансаў праслухоўвання",
"MessageNoLogs": "Няма журналаў", "MessageNoLogs": "Няма журналаў",
"MessageNoMediaFolders": "Няма тэчак з медыя", "MessageNoMediaFolders": "Няма папак з медыя",
"MessageNoNetworkConnection": "Няма падлучэння да сеткі", "MessageNoNetworkConnection": "Няма падлучэння да сеткі",
"MessageNoPodcastsFound": "Падкасты не знойдзены", "MessageNoPodcastsFound": "Падкасты не знойдзены",
"MessageNoSeries": "Няма серый", "MessageNoSeries": "Няма серый",
"MessageNoUpdatesWereNecessary": "Абнаўленні не патрабаваліся", "MessageNoUpdatesWereNecessary": "Абнаўленні не патрабаваліся",
"MessageNoUserPlaylists": "У вас няма спісаў прайгравання", "MessageNoUserPlaylists": "У вас няма плэй-лістоў",
"MessageOldServerAuthReLoginRequired": "Аўтэнтыфікацыя была палепшана для бяспекі ў серверы v2.26.0. Усім карыстальнікам патрабуецца паўторна ўвайсці ў сістэму.", "MessageOldServerAuthReLoginRequired": "Аўтэнтыфікацыя была палепшана для бяспекі ў серверы v2.26.0. Усім карыстальнікам патрабуецца паўторна ўвайсці ў сістэму.",
"MessageOldServerAuthWarning": "Сервер выкарыстоўвае састарэлую аўтэнтыфікацыю", "MessageOldServerAuthWarning": "Сервер выкарыстоўвае састарэлую аўтэнтыфікацыю",
"MessageOldServerAuthWarningHelp": "Гэты сервер працуе пад версіяй, старэйшай за 2.26.0. У версіі 2.26.0 была дададзена больш бяспечная сістэма аўтэнтыфікацыі. Настойліва рэкамендуецца абнавіць сервер да апошняй версіі. Калі вы ўжо абнавілі сервер, увайдзіце зноў, каб выкарыстоўваць новую аўтэнтыфікацыю.", "MessageOldServerAuthWarningHelp": "Гэты сервер працуе пад версіяй, старэйшай за 2.26.0. У версіі 2.26.0 была дададзена больш бяспечная сістэма аўтэнтыфікацыі. Настойліва рэкамендуецца абнавіць сервер да апошняй версіі. Калі вы ўжо абнавілі сервер, увайдзіце зноў, каб выкарыстоўваць новую аўтэнтыфікацыю.",
@@ -333,9 +348,9 @@
"MessageOldServerConnectionWarningHelp": "Вы першапачаткова наладзілі падключэнне да гэтага сервера да міграцыі базы даных у версіі 2.3.0, якая выйшла ў чэрвені 2023 года. У будучым абнаўленні сервера магчымасць уваходу праз гэтае старое падключэнне будзе выдалена. Калі ласка, выдаліце існуючае падключэнне да сервера і падключыцеся зноў (выкарыстоўваючы той жа адрас сервера і ўліковыя даныя). Калі на гэтай прыладзе ёсць спампаваныя медыяфайлы, іх трэба будзе спампаваць зноў для сінхранізацыі з серверам.", "MessageOldServerConnectionWarningHelp": "Вы першапачаткова наладзілі падключэнне да гэтага сервера да міграцыі базы даных у версіі 2.3.0, якая выйшла ў чэрвені 2023 года. У будучым абнаўленні сервера магчымасць уваходу праз гэтае старое падключэнне будзе выдалена. Калі ласка, выдаліце існуючае падключэнне да сервера і падключыцеся зноў (выкарыстоўваючы той жа адрас сервера і ўліковыя даныя). Калі на гэтай прыладзе ёсць спампаваныя медыяфайлы, іх трэба будзе спампаваць зноў для сінхранізацыі з серверам.",
"MessagePodcastSearchField": "Увядзіце пошукавы запыт або URL RSS-стужкі", "MessagePodcastSearchField": "Увядзіце пошукавы запыт або URL RSS-стужкі",
"MessageProgressSyncFailed": "Апошняя спроба адправіць ваш прагрэс праслухоўвання на сервер не ўдалася. Спробы сінхранізаваць прагрэс будуць працягвацца кожныя 15 секунд на працягу 1 хвіліны падчас прайгравання медыяфайла.", "MessageProgressSyncFailed": "Апошняя спроба адправіць ваш прагрэс праслухоўвання на сервер не ўдалася. Спробы сінхранізаваць прагрэс будуць працягвацца кожныя 15 секунд на працягу 1 хвіліны падчас прайгравання медыяфайла.",
"MessageReportBugsAndContribute": "Паведамляйце пра памылкі, прапануйце новыя функцыі і ўдзельнічайце на", "MessageReportBugsAndContribute": "Паведамляйце пра памылкі, прапануйце функцыі і ўносьце свой уклад на",
"MessageSeriesAlreadyDownloaded": "Вы ўжо спампавалі ўсе кнігі з гэтай серыі.", "MessageSeriesAlreadyDownloaded": "Вы ўжо спампавалі ўсе кнігі з гэтай серыі.",
"MessageSeriesDownloadConfirm": "Спампаваць адсутныя {0} кніг(і) з {1} файл(амі), агульным памерам {2}, у тэчку {3}?", "MessageSeriesDownloadConfirm": "Спампаваць адсутныя {0} кніг(і) з {1} файлам(-і), агульным памерам {2}, у папку {3}?",
"MessageSeriesDownloadConfirmIos": "Спампаваць адсутныя {0} кніг(і) з {1} файл(амі), агульным памерам {2}?", "MessageSeriesDownloadConfirmIos": "Спампаваць адсутныя {0} кніг(і) з {1} файл(амі), агульным памерам {2}?",
"MessageSocketConnectedOverMeteredCellular": "Сокет падлучаны праз мабільны інтэрнэт з абмежаваным трафікам", "MessageSocketConnectedOverMeteredCellular": "Сокет падлучаны праз мабільны інтэрнэт з абмежаваным трафікам",
"MessageSocketConnectedOverMeteredWifi": "Сокет падлучаны праз Wi-Fi з абмежаваным трафікам", "MessageSocketConnectedOverMeteredWifi": "Сокет падлучаны праз Wi-Fi з абмежаваным трафікам",
@@ -343,17 +358,19 @@
"MessageSocketConnectedOverUnmeteredWifi": "Сокет падлучаны праз Wi-Fi без абмежавання трафіку", "MessageSocketConnectedOverUnmeteredWifi": "Сокет падлучаны праз Wi-Fi без абмежавання трафіку",
"MessageSocketNotConnected": "Сокет не падлучаны", "MessageSocketNotConnected": "Сокет не падлучаны",
"NoteRSSFeedPodcastAppsHttps": "Папярэджанне: большасць праграм для падкастаў патрабуюць, каб URL RSS-стужкі выкарыстоўваў HTTPS", "NoteRSSFeedPodcastAppsHttps": "Папярэджанне: большасць праграм для падкастаў патрабуюць, каб URL RSS-стужкі выкарыстоўваў HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Папярэджанне: адзін ці больш вашых эпізодаў не маюць даты публікацыі. Некаторыя праграмы для падкастаў патрабуюць гэтага.", "NoteRSSFeedPodcastAppsPubDate": "Папярэджанне: адзін ці больш выпускаў не маюць даты публікацыі. Некаторыя праграмы для падкастаў патрабуюць гэтага.",
"ToastBookmarkCreateFailed": "Не ўдалося стварыць закладку", "ToastBookmarkCreateFailed": "Не ўдалося стварыць закладку",
"ToastBookmarkRemoveFailed": "Не ўдалося выдаліць закладку", "ToastBookmarkRemoveFailed": "Не ўдалося выдаліць закладку",
"ToastBookmarkUpdateFailed": "Не ўдалося абнавіць закладку", "ToastBookmarkUpdateFailed": "Не ўдалося абнавіць закладку",
"ToastDownloadNotAllowedOnCellular": "Спампоўванне забаронена праз мабільны інтэрнэт", "ToastDownloadNotAllowedOnCellular": "Спампоўванне забаронена праз мабільны інтэрнэт",
"ToastItemMarkedAsFinishedFailed": "Не ўдалося пазначыць як Скончана", "ToastItemMarkedAsFinishedFailed": "Не ўдалося пазначыць як завершаны",
"ToastItemMarkedAsNotFinishedFailed": "Не ўдалося пазначыць як Незавершанае", "ToastItemMarkedAsNotFinishedFailed": "Не ўдалося пазначыць як незавершаны",
"ToastPlaylistCreateFailed": "Не ўдалося стварыць спіс прайгравання", "ToastPlaylistCreateFailed": "Не ўдалося стварыць плэй-ліст",
"ToastPodcastCreateFailed": "Не ўдалося стварыць падкаст", "ToastPodcastCreateFailed": "Не ўдалося стварыць падкаст",
"ToastPodcastCreateSuccess": "Падкаст паспяхова створаны", "ToastPodcastCreateSuccess": "Падкаст паспяхова створаны",
"ToastRSSFeedCloseFailed": "Не ўдалося закрыць RSS-стужку", "ToastRSSFeedCloseFailed": "Не ўдалося закрыць RSS-стужку",
"ToastRSSFeedCloseSuccess": "RSS-стужка закрыта", "ToastRSSFeedCloseSuccess": "RSS-стужка закрыта",
"ToastStreamingNotAllowedOnCellular": "Патокавая перадача праз мабільную сувязь забаронена" "ToastStreamingNotAllowedOnCellular": "Патокавая перадача праз мабільную сувязь забаронена",
"UnitMinutesShort": "{0} хв",
"UnitSecondsShort": "{0} с"
} }
+18 -1
View File
@@ -63,6 +63,7 @@
"HeaderChapters": "Глави", "HeaderChapters": "Глави",
"HeaderCollection": "Колекция", "HeaderCollection": "Колекция",
"HeaderCollectionItems": "Елемент в колекция", "HeaderCollectionItems": "Елемент в колекция",
"HeaderConfirm": "Потвърди",
"HeaderConnectionStatus": "Състояние на връзката", "HeaderConnectionStatus": "Състояние на връзката",
"HeaderDataSettings": "Настройки на данните", "HeaderDataSettings": "Настройки на данните",
"HeaderDetails": "Детайли", "HeaderDetails": "Детайли",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Последни сесии", "HeaderStatsRecentSessions": "Последни сесии",
"HeaderTableOfContents": "Съдържание", "HeaderTableOfContents": "Съдържание",
"HeaderUserInterfaceSettings": "Настройки на потребителския интерфейс", "HeaderUserInterfaceSettings": "Настройки на потребителския интерфейс",
"HeaderWelcome": "Добре дошли, <strong>{0}</strong>",
"HeaderYourStats": "Вашата статистика", "HeaderYourStats": "Вашата статистика",
"LabelAddToPlaylist": "Добави в плейлист", "LabelAddToPlaylist": "Добави в плейлист",
"LabelAddedAt": "Добавено в", "LabelAddedAt": "Добавено в",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Когато автоматичният таймер на изключване изтече, повторното пускане на елемента автоматично ще върне позицията ви назад.", "LabelAutoSleepTimerAutoRewindHelp": "Когато автоматичният таймер на изключване изтече, повторното пускане на елемента автоматично ще върне позицията ви назад.",
"LabelAutoSleepTimerHelp": "Когато се възпроизвеждате медия между посочените начален и краен час, автоматично ще стартира таймер за изключване.", "LabelAutoSleepTimerHelp": "Когато се възпроизвеждате медия между посочените начален и краен час, автоматично ще стартира таймер за изключване.",
"LabelBooks": "Книги", "LabelBooks": "Книги",
"LabelByAuthor": "от {0}",
"LabelChapterTrack": "Трак на глава", "LabelChapterTrack": "Трак на глава",
"LabelChapters": "Глави", "LabelChapters": "Глави",
"LabelClosePlayer": "Затвори плейъра", "LabelClosePlayer": "Затвори плейъра",
@@ -155,6 +158,9 @@
"LabelFinished": "Дата на приключване", "LabelFinished": "Дата на приключване",
"LabelFolder": "Папка", "LabelFolder": "Папка",
"LabelFontBoldness": "Дебелина на шрифта", "LabelFontBoldness": "Дебелина на шрифта",
"LabelFontFamily": "Шрифт",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Мащаб на шрифта", "LabelFontScale": "Мащаб на шрифта",
"LabelGenre": "Жанр", "LabelGenre": "Жанр",
"LabelGenres": "Жанрове", "LabelGenres": "Жанрове",
@@ -174,6 +180,9 @@
"LabelLayout": "Оформление", "LabelLayout": "Оформление",
"LabelLayoutAuto": "Авто", "LabelLayoutAuto": "Авто",
"LabelLayoutSinglePage": "Единична страница", "LabelLayoutSinglePage": "Единична страница",
"LabelLibrarySortByProgress": "Прогрес: Последно обновление",
"LabelLibrarySortByProgressFinished": "Прогрес: Приключено",
"LabelLibrarySortByProgressStarted": "Прогрес: Започнато",
"LabelLight": "Светло", "LabelLight": "Светло",
"LabelLineSpacing": "Междуредие", "LabelLineSpacing": "Междуредие",
"LabelListenAgain": "Слушай отново", "LabelListenAgain": "Слушай отново",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Скалируй изминалото време спрямо скоростта", "LabelScaleElapsedTimeBySpeed": "Скалируй изминалото време спрямо скоростта",
"LabelSeason": "Сезон", "LabelSeason": "Сезон",
"LabelSelectADevice": "Избери устройство", "LabelSelectADevice": "Избери устройство",
"LabelSelectMediaType": "Изберете тип на медията",
"LabelSequenceAscending": "Възходяща последователност", "LabelSequenceAscending": "Възходяща последователност",
"LabelSequenceDescending": "Низходяща последователност", "LabelSequenceDescending": "Низходяща последователност",
"LabelSeries": "От сериите", "LabelSeries": "От сериите",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf сървър не е включен", "MessageAudiobookshelfServerNotConnected": "Audiobookshelf сървър не е включен",
"MessageAudiobookshelfServerRequired": "<strong>ВАЖНО!</strong> Това приложение е създадено да работи с Audiobookshelf сървър, който вие или някой, когото познавате, хоства. Това приложение не предоставя никакво съдържание.", "MessageAudiobookshelfServerRequired": "<strong>ВАЖНО!</strong> Това приложение е създадено да работи с Audiobookshelf сървър, който вие или някой, когото познавате, хоства. Това приложение не предоставя никакво съдържание.",
"MessageBookshelfEmpty": "Библиотеката е празна", "MessageBookshelfEmpty": "Библиотеката е празна",
"MessageConfirmAppExit": "Искате ли да излезете от апликацията?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Сигурни ли сте, че искате да изчистите опашката за сваляне на епизоди?",
"MessageConfirmDeleteLocalEpisode": "Премахни \"{0}\" епизода от устройството? Файловете на сървъра няма да бъдат премахнати.", "MessageConfirmDeleteLocalEpisode": "Премахни \"{0}\" епизода от устройството? Файловете на сървъра няма да бъдат премахнати.",
"MessageConfirmDeleteLocalFiles": "Да премахна ли локалните файлове на този елемент от вашето устройство? Файловете на сървъра и вашият напредък няма да бъдат засегнати.", "MessageConfirmDeleteLocalFiles": "Да премахна ли локалните файлове на този елемент от вашето устройство? Файловете на сървъра и вашият напредък няма да бъдат засегнати.",
"MessageConfirmDeleteServerConfig": "Премахнете тази сървърна настройка?",
"MessageConfirmDeleteServerEpisode": "Сигурни ли сте че искате да изтрийете епизод \"{0}\" от сървъра?\nПредупреждение: Това ще изтрийе и аудио файла.",
"MessageConfirmDisableAutoTimer": "Сигурни ли сте, че искате да деактивирате автоматичния таймер за остатъка от днешния ден? Таймерът ще бъде активиран отново в края на този период на автоматичния таймер за изключване или ако рестартирате приложението.", "MessageConfirmDisableAutoTimer": "Сигурни ли сте, че искате да деактивирате автоматичния таймер за остатъка от днешния ден? Таймерът ще бъде активиран отново в края на този период на автоматичния таймер за изключване или ако рестартирате приложението.",
"MessageConfirmDiscardProgress": "Сигурни ли сте, че искате да нулирате напредъка си?", "MessageConfirmDiscardProgress": "Сигурни ли сте, че искате да нулирате напредъка си?",
"MessageConfirmDownloadUsingCellular": "Ще изтегляте, използвайки мобилни данни. Това може да включва такси за данни от оператора. Искате ли да продължите?", "MessageConfirmDownloadUsingCellular": "Ще изтегляте, използвайки мобилни данни. Това може да включва такси за данни от оператора. Искате ли да продължите?",
"MessageConfirmMarkAsFinished": "Сигурни ли сте, че искате да маркирате този елемент като завършен?", "MessageConfirmMarkAsFinished": "Сигурни ли сте, че искате да маркирате този елемент като завършен?",
"MessageConfirmPlaybackTime": "Започни възпроизвеждане на \"{0}\" в {1}?",
"MessageConfirmRemoveBookmark": "Сигурни ли сте, че искате да премахнете отметката?", "MessageConfirmRemoveBookmark": "Сигурни ли сте, че искате да премахнете отметката?",
"MessageConfirmStreamingUsingCellular": "Ще стриймвате, използвайки мобилни данни. Това може да включва такси за данни от оператора. Искате ли да продължите?", "MessageConfirmStreamingUsingCellular": "Ще стриймвате, използвайки мобилни данни. Това може да включва такси за данни от оператора. Искате ли да продължите?",
"MessageDiscardProgress": "Нулирай прогреса", "MessageDiscardProgress": "Нулирай прогреса",
@@ -355,5 +370,7 @@
"ToastPodcastCreateSuccess": "Подкаст успешно създаден", "ToastPodcastCreateSuccess": "Подкаст успешно създаден",
"ToastRSSFeedCloseFailed": "Неуспешно затваряне на RSS емисията", "ToastRSSFeedCloseFailed": "Неуспешно затваряне на RSS емисията",
"ToastRSSFeedCloseSuccess": "RSS емисията е затворена", "ToastRSSFeedCloseSuccess": "RSS емисията е затворена",
"ToastStreamingNotAllowedOnCellular": "Стриймването не е разрешено чрез мобилни данни" "ToastStreamingNotAllowedOnCellular": "Стриймването не е разрешено чрез мобилни данни",
"UnitMinutesShort": "{0}мин",
"UnitSecondsShort": "{0}сек"
} }
+18 -1
View File
@@ -63,6 +63,7 @@
"HeaderChapters": "Capítols", "HeaderChapters": "Capítols",
"HeaderCollection": "Col·lecció", "HeaderCollection": "Col·lecció",
"HeaderCollectionItems": "Elements de la col·lecció", "HeaderCollectionItems": "Elements de la col·lecció",
"HeaderConfirm": "Confirma",
"HeaderConnectionStatus": "Estat de la connexió", "HeaderConnectionStatus": "Estat de la connexió",
"HeaderDataSettings": "Paràmetres de dades", "HeaderDataSettings": "Paràmetres de dades",
"HeaderDetails": "Detalls", "HeaderDetails": "Detalls",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Sessions recents", "HeaderStatsRecentSessions": "Sessions recents",
"HeaderTableOfContents": "Sumari", "HeaderTableOfContents": "Sumari",
"HeaderUserInterfaceSettings": "Paràmetres de la interfície d'usuari", "HeaderUserInterfaceSettings": "Paràmetres de la interfície d'usuari",
"HeaderWelcome": "Benvingut, <strong>{0}</strong>",
"HeaderYourStats": "Les vostres estadístiques", "HeaderYourStats": "Les vostres estadístiques",
"LabelAddToPlaylist": "Afegeix a la llista de reproducció", "LabelAddToPlaylist": "Afegeix a la llista de reproducció",
"LabelAddedAt": "Afegit a", "LabelAddedAt": "Afegit a",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Quan el temporitzador d'auto sleep acaba, reproduir l'element altra vegada farà que es rebobine automàticament a la teua posició.", "LabelAutoSleepTimerAutoRewindHelp": "Quan el temporitzador d'auto sleep acaba, reproduir l'element altra vegada farà que es rebobine automàticament a la teua posició.",
"LabelAutoSleepTimerHelp": "Quan es reprodueix entre hores especificades, el temporitzador s'activarà automàticament.", "LabelAutoSleepTimerHelp": "Quan es reprodueix entre hores especificades, el temporitzador s'activarà automàticament.",
"LabelBooks": "Llibres", "LabelBooks": "Llibres",
"LabelByAuthor": "per {0}",
"LabelChapterTrack": "Seguiment de capítol", "LabelChapterTrack": "Seguiment de capítol",
"LabelChapters": "Capítols", "LabelChapters": "Capítols",
"LabelClosePlayer": "Tanca el reproductor", "LabelClosePlayer": "Tanca el reproductor",
@@ -126,11 +129,12 @@
"LabelCustomTime": "Temps personalitzat", "LabelCustomTime": "Temps personalitzat",
"LabelDescription": "Descripció", "LabelDescription": "Descripció",
"LabelDisableAudioFadeOut": "Desactivar desaparició d'àudio", "LabelDisableAudioFadeOut": "Desactivar desaparició d'àudio",
"LabelDisableAudioFadeOutHelp": "El volum de l'audio començara a reduïr-se quan quede menys d'1 minut del temporitzador d'anar a dormir. Habilita este ajustament per a no reduïr el volum.", "LabelDisableAudioFadeOutHelp": "El volum de l'audio començara a reduïr-se quan quedi menys d'1 minut del temporitzador d'anar a dormir. Habilita aquest ajustament per a no reduïr el volum.",
"LabelDisableAutoRewind": "Desactivar rebobinatge automàtic", "LabelDisableAutoRewind": "Desactivar rebobinatge automàtic",
"LabelDisableShakeToReset": "Desactivar sacsejar per reiniciar", "LabelDisableShakeToReset": "Desactivar sacsejar per reiniciar",
"LabelDisableShakeToResetHelp": "Si sacseges el teu dispositiu quan el temporitzador és actiu o durant els 2 minuts després que hagi expirat, el temporitzador es reinicia. Activa aquesta opció per evitar-ho.", "LabelDisableShakeToResetHelp": "Si sacseges el teu dispositiu quan el temporitzador és actiu o durant els 2 minuts després que hagi expirat, el temporitzador es reinicia. Activa aquesta opció per evitar-ho.",
"LabelDisableVibrateOnReset": "Desactivar la vibració al reiniciar", "LabelDisableVibrateOnReset": "Desactivar la vibració al reiniciar",
"LabelDisableVibrateOnResetHelp": "Quan el temporitzador d'apagat es reinicia, el dispositiu vibra. Activa aquesta opció perquè no vibri quan es reiniciï el temporitzador.",
"LabelDiscover": "Descobrir", "LabelDiscover": "Descobrir",
"LabelDownload": "Descarregar", "LabelDownload": "Descarregar",
"LabelDownloadUsingCellular": "Descarregar utilitzant dades mòbils", "LabelDownloadUsingCellular": "Descarregar utilitzant dades mòbils",
@@ -144,6 +148,7 @@
"LabelEndOfChapter": "Final del capítol", "LabelEndOfChapter": "Final del capítol",
"LabelEndTime": "Hora de finalització", "LabelEndTime": "Hora de finalització",
"LabelEpisode": "Episodi", "LabelEpisode": "Episodi",
"LabelExplicit": "Explícit",
"LabelFeedURL": "Font URL", "LabelFeedURL": "Font URL",
"LabelFile": "Fitxer", "LabelFile": "Fitxer",
"LabelFileBirthtime": "Fitxer creat a", "LabelFileBirthtime": "Fitxer creat a",
@@ -152,22 +157,31 @@
"LabelFinished": "Acabat", "LabelFinished": "Acabat",
"LabelFolder": "Carpeta", "LabelFolder": "Carpeta",
"LabelFontBoldness": "Gruix de la lletra", "LabelFontBoldness": "Gruix de la lletra",
"LabelFontFamily": "Família tipogràfica",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Escala de la lletra", "LabelFontScale": "Escala de la lletra",
"LabelGenre": "Gènere", "LabelGenre": "Gènere",
"LabelGenres": "Gèneres", "LabelGenres": "Gèneres",
"LabelHapticFeedback": "Resposta hàptica", "LabelHapticFeedback": "Resposta hàptica",
"LabelHasEbook": "Té llibre electrònic", "LabelHasEbook": "Té llibre electrònic",
"LabelHasSupplementaryEbook": "Té llibre electrònic suplementari", "LabelHasSupplementaryEbook": "Té llibre electrònic suplementari",
"LabelHeavy": "Pesat",
"LabelHigh": "Alt", "LabelHigh": "Alt",
"LabelHost": "Amfitrió", "LabelHost": "Amfitrió",
"LabelInProgress": "En procés", "LabelInProgress": "En procés",
"LabelIncomplete": "Incomplet", "LabelIncomplete": "Incomplet",
"LabelInternalAppStorage": "Emmagatzematge intern de l'aplicació", "LabelInternalAppStorage": "Emmagatzematge intern de l'aplicació",
"LabelJumpBackwardsTime": "Saltar enrere", "LabelJumpBackwardsTime": "Saltar enrere",
"LabelJumpForwardsTime": "Salt endavant en el temps",
"LabelKeepScreenAwake": "Manté la pantalla encesa",
"LabelLanguage": "Llengua", "LabelLanguage": "Llengua",
"LabelLayout": "Disposició", "LabelLayout": "Disposició",
"LabelLayoutAuto": "Automàtic", "LabelLayoutAuto": "Automàtic",
"LabelLayoutSinglePage": "Pàgina única", "LabelLayoutSinglePage": "Pàgina única",
"LabelLibrarySortByProgress": "Progrés: Última actualització",
"LabelLibrarySortByProgressFinished": "Progrés: Finalitzat",
"LabelLibrarySortByProgressStarted": "Progrés: Començat",
"LabelLight": "Clar", "LabelLight": "Clar",
"LabelLineSpacing": "Interlineat", "LabelLineSpacing": "Interlineat",
"LabelListenAgain": "Escoltar de nou", "LabelListenAgain": "Escoltar de nou",
@@ -175,14 +189,17 @@
"LabelLocalPodcasts": "Pòdcasts locals", "LabelLocalPodcasts": "Pòdcasts locals",
"LabelLockOrientation": "Bloca l'orientació", "LabelLockOrientation": "Bloca l'orientació",
"LabelLockPlayer": "Bloca el reproductor", "LabelLockPlayer": "Bloca el reproductor",
"LabelLow": "Baix",
"LabelMediaType": "Tipus de mitjà", "LabelMediaType": "Tipus de mitjà",
"LabelMedium": "Mitjà", "LabelMedium": "Mitjà",
"LabelMissing": "Falta",
"LabelMore": "Més", "LabelMore": "Més",
"LabelMoreInfo": "Més informació", "LabelMoreInfo": "Més informació",
"LabelName": "Nom", "LabelName": "Nom",
"LabelNarrator": "Narrador", "LabelNarrator": "Narrador",
"LabelNarrators": "Narradors", "LabelNarrators": "Narradors",
"LabelNavigateWithVolume": "Navega amb les tecles de volum", "LabelNavigateWithVolume": "Navega amb les tecles de volum",
"LabelNavigateWithVolumeMirrored": "Emmirallat",
"LabelNavigateWithVolumeWhilePlayingDisabled": "⭘", "LabelNavigateWithVolumeWhilePlayingDisabled": "⭘",
"LabelNavigateWithVolumeWhilePlayingEnabled": "", "LabelNavigateWithVolumeWhilePlayingEnabled": "",
"LabelNever": "Mai", "LabelNever": "Mai",
+19 -2
View File
@@ -63,6 +63,7 @@
"HeaderChapters": "Kapitoly", "HeaderChapters": "Kapitoly",
"HeaderCollection": "Kolekce", "HeaderCollection": "Kolekce",
"HeaderCollectionItems": "Položky kolekce", "HeaderCollectionItems": "Položky kolekce",
"HeaderConfirm": "Potvrzení",
"HeaderConnectionStatus": "Stav připojení", "HeaderConnectionStatus": "Stav připojení",
"HeaderDataSettings": "Nastavení dat", "HeaderDataSettings": "Nastavení dat",
"HeaderDetails": "Podrobnosti", "HeaderDetails": "Podrobnosti",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Poslední sezení", "HeaderStatsRecentSessions": "Poslední sezení",
"HeaderTableOfContents": "Obsah", "HeaderTableOfContents": "Obsah",
"HeaderUserInterfaceSettings": "Nastavení uživatelského rozhraní", "HeaderUserInterfaceSettings": "Nastavení uživatelského rozhraní",
"HeaderWelcome": "Vítejte, <strong>{0}</strong>",
"HeaderYourStats": "Vaše statistiky", "HeaderYourStats": "Vaše statistiky",
"LabelAddToPlaylist": "Přidat do seznamu skladeb", "LabelAddToPlaylist": "Přidat do seznamu skladeb",
"LabelAddedAt": "Přidáno v", "LabelAddedAt": "Přidáno v",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Když dojde k uspání automatickým časovačem spánku, pozice přehrávání je posunuta zpět o vybraný čas.", "LabelAutoSleepTimerAutoRewindHelp": "Když dojde k uspání automatickým časovačem spánku, pozice přehrávání je posunuta zpět o vybraný čas.",
"LabelAutoSleepTimerHelp": "Během přehrávání média v časovém rozmezí \"Od\" a \"Do\" se automaticky spustí časovač spánku.", "LabelAutoSleepTimerHelp": "Během přehrávání média v časovém rozmezí \"Od\" a \"Do\" se automaticky spustí časovač spánku.",
"LabelBooks": "Knihy", "LabelBooks": "Knihy",
"LabelByAuthor": "od {0}",
"LabelChapterTrack": "Stopa kapitoly", "LabelChapterTrack": "Stopa kapitoly",
"LabelChapters": "Kapitoly", "LabelChapters": "Kapitoly",
"LabelClosePlayer": "Zavřít přehrávač", "LabelClosePlayer": "Zavřít přehrávač",
@@ -155,7 +158,10 @@
"LabelFinished": "Dokončeno", "LabelFinished": "Dokončeno",
"LabelFolder": "Složka", "LabelFolder": "Složka",
"LabelFontBoldness": "Výraznost písma", "LabelFontBoldness": "Výraznost písma",
"LabelFontScale": "Měřítko písma", "LabelFontFamily": "Rodina písem",
"LabelFontFamilySans": "Bezpatkové",
"LabelFontFamilySerif": "Patkové",
"LabelFontScale": "Velikost písma",
"LabelGenre": "Žánr", "LabelGenre": "Žánr",
"LabelGenres": "Žánry", "LabelGenres": "Žánry",
"LabelHapticFeedback": "Haptická odezva", "LabelHapticFeedback": "Haptická odezva",
@@ -174,6 +180,9 @@
"LabelLayout": "Rozvržení", "LabelLayout": "Rozvržení",
"LabelLayoutAuto": "Automatické", "LabelLayoutAuto": "Automatické",
"LabelLayoutSinglePage": "Jedna stránka", "LabelLayoutSinglePage": "Jedna stránka",
"LabelLibrarySortByProgress": "Pokrok: naposledy aktualizováno",
"LabelLibrarySortByProgressFinished": "Pokrok: dokončeno",
"LabelLibrarySortByProgressStarted": "Pokrok: začato",
"LabelLight": "Slabá", "LabelLight": "Slabá",
"LabelLineSpacing": "Řádkování", "LabelLineSpacing": "Řádkování",
"LabelListenAgain": "Poslouchat znovu", "LabelListenAgain": "Poslouchat znovu",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Škálovat uplynulý čas podle rychlosti", "LabelScaleElapsedTimeBySpeed": "Škálovat uplynulý čas podle rychlosti",
"LabelSeason": "Sezóna", "LabelSeason": "Sezóna",
"LabelSelectADevice": "Vyberte zařízení", "LabelSelectADevice": "Vyberte zařízení",
"LabelSelectMediaType": "Vyberte typ média",
"LabelSequenceAscending": "Řadit vzestupně", "LabelSequenceAscending": "Řadit vzestupně",
"LabelSequenceDescending": "Řadit sestupně", "LabelSequenceDescending": "Řadit sestupně",
"LabelSeries": "Série", "LabelSeries": "Série",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Server Audiobookshelf není připojen", "MessageAudiobookshelfServerNotConnected": "Server Audiobookshelf není připojen",
"MessageAudiobookshelfServerRequired": "<strong>Důležité!</strong> Tato aplikace je navržena, aby pracovala se serverem Audiobookshelf, který hostuje buď vy, nebo někdo, koho znáte. Tato aplikace neposkytuje žádný obsah.", "MessageAudiobookshelfServerRequired": "<strong>Důležité!</strong> Tato aplikace je navržena, aby pracovala se serverem Audiobookshelf, který hostuje buď vy, nebo někdo, koho znáte. Tato aplikace neposkytuje žádný obsah.",
"MessageBookshelfEmpty": "Knihovna je prázdná", "MessageBookshelfEmpty": "Knihovna je prázdná",
"MessageConfirmAppExit": "Opravdu chcete opustit aplikaci?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Přejete si skutečně vymazat frontu stahování epizod?",
"MessageConfirmDeleteLocalEpisode": "Odebrat místní epizodu „{0}“ ze zařízení? Soubor na serveru zůstane nezměněný.", "MessageConfirmDeleteLocalEpisode": "Odebrat místní epizodu „{0}“ ze zařízení? Soubor na serveru zůstane nezměněný.",
"MessageConfirmDeleteLocalFiles": "Odebrat místní soubory této položky ze zařízení? Soubory na serveru a váš pokrok nebudou ovlivněny.", "MessageConfirmDeleteLocalFiles": "Odebrat místní soubory této položky ze zařízení? Soubory na serveru a váš pokrok nebudou ovlivněny.",
"MessageConfirmDeleteServerConfig": "Odebrat tuto konfiguraci serveru?",
"MessageConfirmDeleteServerEpisode": "Přejete si skutečně odstranit epizodu \"{0}\" z serveru?\nUpozornění: Toto odstraní zvukový soubor.",
"MessageConfirmDisableAutoTimer": "Určitě chcete vypnout automatický časovač pro zbytek dnešního dne? Časovač bude opět aktivován po uběhnutí doby automatického spánku nebo po restartování aplikace.", "MessageConfirmDisableAutoTimer": "Určitě chcete vypnout automatický časovač pro zbytek dnešního dne? Časovač bude opět aktivován po uběhnutí doby automatického spánku nebo po restartování aplikace.",
"MessageConfirmDiscardProgress": "Opravdu chcete zahodit svůj pokrok?", "MessageConfirmDiscardProgress": "Opravdu chcete zahodit svůj pokrok?",
"MessageConfirmDownloadUsingCellular": "Chystáte se stahovat přes mobilní data. Toto může zahrnovat poplatky za mobilní data. Chcete pokračovat?", "MessageConfirmDownloadUsingCellular": "Chystáte se stahovat přes mobilní data. Toto může zahrnovat poplatky za mobilní data. Chcete pokračovat?",
"MessageConfirmMarkAsFinished": "Opravdu chcete tuto položku označit jako dokončenou?", "MessageConfirmMarkAsFinished": "Opravdu chcete tuto položku označit jako dokončenou?",
"MessageConfirmPlaybackTime": "Spustit přehrávání pro \"{0}\" v {1}?",
"MessageConfirmRemoveBookmark": "Opravdu chcete odebrat záložku?", "MessageConfirmRemoveBookmark": "Opravdu chcete odebrat záložku?",
"MessageConfirmStreamingUsingCellular": "Chystáte se přehrávat přes mobilní data. Toto může zahrnovat poplatky za mobilní data. Chcete pokračovat?", "MessageConfirmStreamingUsingCellular": "Chystáte se přehrávat přes mobilní data. Toto může zahrnovat poplatky za mobilní data. Chcete pokračovat?",
"MessageDiscardProgress": "Zahodit pokrok", "MessageDiscardProgress": "Zahodit pokrok",
@@ -355,5 +370,7 @@
"ToastPodcastCreateSuccess": "Podcast byl úspěšně vytvořen", "ToastPodcastCreateSuccess": "Podcast byl úspěšně vytvořen",
"ToastRSSFeedCloseFailed": "Nepodařilo se zavřít RSS kanál", "ToastRSSFeedCloseFailed": "Nepodařilo se zavřít RSS kanál",
"ToastRSSFeedCloseSuccess": "RSS kanál uzavřen", "ToastRSSFeedCloseSuccess": "RSS kanál uzavřen",
"ToastStreamingNotAllowedOnCellular": "Přehrávání přes mobilní data není povoleno" "ToastStreamingNotAllowedOnCellular": "Přehrávání přes mobilní data není povoleno",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+52 -34
View File
@@ -62,39 +62,41 @@
"HeaderAudioTracks": "Lydspor", "HeaderAudioTracks": "Lydspor",
"HeaderChapters": "Kapitler", "HeaderChapters": "Kapitler",
"HeaderCollection": "Samling", "HeaderCollection": "Samling",
"HeaderCollectionItems": "Samlingselementer", "HeaderCollectionItems": "Bøger i samlingen",
"HeaderConfirm": "Bekræft",
"HeaderConnectionStatus": "Forbindelsesstatus", "HeaderConnectionStatus": "Forbindelsesstatus",
"HeaderDataSettings": "Data indstillinger", "HeaderDataSettings": "Data indstillinger",
"HeaderDetails": "Detaljer", "HeaderDetails": "Detaljer",
"HeaderDownloads": "Downloads", "HeaderDownloads": "Downloads",
"HeaderEbookFiles": "E-bogsfiler", "HeaderEbookFiles": "E-bogsfiler",
"HeaderEpisodes": "Episoder", "HeaderEpisodes": "Afsnit",
"HeaderEreaderSettings": "E-læser Indstillinger", "HeaderEreaderSettings": "Indstillinger for e-bogslæsere",
"HeaderLatestEpisodes": "Seneste episoder", "HeaderLatestEpisodes": "Nyeste afsnit",
"HeaderLibraries": "Biblioteker", "HeaderLibraries": "Biblioteker",
"HeaderLocalFolders": "Lokale mapper", "HeaderLocalFolders": "Lokale mapper",
"HeaderLocalLibraryItems": "Lokale biblioteks elementer", "HeaderLocalLibraryItems": "Lokale biblioteks elementer",
"HeaderNewPlaylist": "Ny afspilningsliste", "HeaderNewPlaylist": "Ny afspilningsliste",
"HeaderOpenRSSFeed": "Åbn RSS Feed", "HeaderOpenRSSFeed": "Åbn RSS-feed",
"HeaderPlaybackSettings": "Afspilnings indstillinger", "HeaderPlaybackSettings": "Afspilnings indstillinger",
"HeaderPlaylist": "Afspilningsliste", "HeaderPlaylist": "Afspilningsliste",
"HeaderPlaylistItems": "Afspilningsliste Elementer", "HeaderPlaylistItems": "Elementer i afspilningslisten",
"HeaderProgressSyncFailed": "Fremskridt synkronisering fejlede", "HeaderProgressSyncFailed": "Fremskridt synkronisering fejlede",
"HeaderRSSFeed": "RSS Feed", "HeaderRSSFeed": "RSS Feed",
"HeaderRSSFeedGeneral": "RSS Detaljer", "HeaderRSSFeedGeneral": "RSS-detaljer",
"HeaderRSSFeedIsOpen": "RSS Feed er Åben", "HeaderRSSFeedIsOpen": "RSS-feedet er åbent",
"HeaderSelectDownloadLocation": "Vælg Download Placering", "HeaderSelectDownloadLocation": "Vælg Download Placering",
"HeaderSettings": "Indstillinger", "HeaderSettings": "Indstillinger",
"HeaderSleepTimer": "Søvntimer", "HeaderSleepTimer": "Sovefunktion",
"HeaderSleepTimerSettings": "Sleep Timer instillinger", "HeaderSleepTimerSettings": "Sleep Timer instillinger",
"HeaderStatsMinutesListeningChart": "Minutter Lyttet (sidste 7 dage)", "HeaderStatsMinutesListeningChart": "Minutter lyttet (sidste 7 dage)",
"HeaderStatsRecentSessions": "Seneste Sessions", "HeaderStatsRecentSessions": "Seneste sessioner",
"HeaderTableOfContents": "Indholdsfortegnelse", "HeaderTableOfContents": "Indholdsfortegnelse",
"HeaderUserInterfaceSettings": "Indstillinger for brugergrænseflade", "HeaderUserInterfaceSettings": "Indstillinger for brugergrænseflade",
"HeaderYourStats": "Dine Statistikker", "HeaderWelcome": "Velkommen, <strong>{0}</strong>",
"LabelAddToPlaylist": "Tilføj til Afspilningsliste", "HeaderYourStats": "Din statistik",
"LabelAddToPlaylist": "Tilføj til afspilningsliste",
"LabelAddedAt": "Tilføjet", "LabelAddedAt": "Tilføjet",
"LabelAddedDate": "Tilføjet {0}", "LabelAddedDate": "Tilføjede {0}",
"LabelAll": "Alle", "LabelAll": "Alle",
"LabelAllowSeekingOnMediaControls": "Tillad positionssøgning på mediemeddelelseskontroller", "LabelAllowSeekingOnMediaControls": "Tillad positionssøgning på mediemeddelelseskontroller",
"LabelAlways": "Altid", "LabelAlways": "Altid",
@@ -106,23 +108,24 @@
"LabelAuthorFirstLast": "Forfatter (Fornavn Efternavn)", "LabelAuthorFirstLast": "Forfatter (Fornavn Efternavn)",
"LabelAuthorLastFirst": "Forfatter (Efternavn, Fornavn)", "LabelAuthorLastFirst": "Forfatter (Efternavn, Fornavn)",
"LabelAuthors": "Forfattere", "LabelAuthors": "Forfattere",
"LabelAutoDownloadEpisodes": "Auto Download Episoder", "LabelAutoDownloadEpisodes": "Hent automatisk afsnit",
"LabelAutoRewindTime": "Automatisk tilbagespolingstid", "LabelAutoRewindTime": "Automatisk tilbagespolingstid",
"LabelAutoSleepTimer": "Auto sleep timer", "LabelAutoSleepTimer": "Auto søvn-timer",
"LabelAutoSleepTimerAutoRewind": "Automatisk sleep timer automatisk tilbagespoling", "LabelAutoSleepTimerAutoRewind": "Automatisk sleep timer automatisk tilbagespoling",
"LabelAutoSleepTimerAutoRewindHelp": "Når den automatiske sleep-timer er færdig, vil genafspilning af elementet automatisk spole din position tilbage.", "LabelAutoSleepTimerAutoRewindHelp": "Når den automatiske sleep-timer er færdig, vil genafspilning af elementet automatisk spole din position tilbage.",
"LabelAutoSleepTimerHelp": "Når der afspilles medie mellem de angivne start- og sluttidspunkter, starter en sleep-timer automatisk.", "LabelAutoSleepTimerHelp": "Når der afspilles medie mellem de angivne start- og sluttidspunkter, starter en sleep-timer automatisk.",
"LabelBooks": "Bøger", "LabelBooks": "Bøger",
"LabelByAuthor": "af {0}",
"LabelChapterTrack": "Kapitel spor", "LabelChapterTrack": "Kapitel spor",
"LabelChapters": "Kapitler", "LabelChapters": "Kapitler",
"LabelClosePlayer": "Luk afspiller", "LabelClosePlayer": "Luk afspiller",
"LabelCollapseSeries": "Fold Serier Sammen", "LabelCollapseSeries": "Fold serier sammen",
"LabelComplete": "Fuldfør", "LabelComplete": "Afsluttet",
"LabelContinueBooks": "Fortsæt Bøger", "LabelContinueBooks": "Fortsæt Bøger",
"LabelContinueEpisodes": "Fortsæt Episoder", "LabelContinueEpisodes": "Fortsæt Episoder",
"LabelContinueListening": "Fortsæt med at lytte", "LabelContinueListening": "Fortsæt med at lytte",
"LabelContinueReading": "Fortsæt med at læse", "LabelContinueReading": "Fortsæt med at læse",
"LabelContinueSeries": "Fortsæt Serien", "LabelContinueSeries": "Fortsæt serien",
"LabelCustomTime": "Brugerdefineret tid", "LabelCustomTime": "Brugerdefineret tid",
"LabelDescription": "Beskrivelse", "LabelDescription": "Beskrivelse",
"LabelDisableAudioFadeOut": "Deaktiver lydudtoning", "LabelDisableAudioFadeOut": "Deaktiver lydudtoning",
@@ -133,7 +136,7 @@
"LabelDisableVibrateOnReset": "Deaktiver vibrering ved nulstilling", "LabelDisableVibrateOnReset": "Deaktiver vibrering ved nulstilling",
"LabelDisableVibrateOnResetHelp": "Når sleep-timeren nulstilles, vil din enhed vibrere. Aktiver denne indstilling for ikke at vibrere, når sleep-timeren nulstilles.", "LabelDisableVibrateOnResetHelp": "Når sleep-timeren nulstilles, vil din enhed vibrere. Aktiver denne indstilling for ikke at vibrere, når sleep-timeren nulstilles.",
"LabelDiscover": "Opdag", "LabelDiscover": "Opdag",
"LabelDownload": "Download", "LabelDownload": "Hent",
"LabelDownloadUsingCellular": "Download med mobildata", "LabelDownloadUsingCellular": "Download med mobildata",
"LabelDownloaded": "Downloadet", "LabelDownloaded": "Downloadet",
"LabelDuration": "Varighed", "LabelDuration": "Varighed",
@@ -146,8 +149,8 @@
"LabelEndOfChapter": "Slutningen af kapitel", "LabelEndOfChapter": "Slutningen af kapitel",
"LabelEndTime": "Sluttidspunkt", "LabelEndTime": "Sluttidspunkt",
"LabelEpisode": "Afsnit", "LabelEpisode": "Afsnit",
"LabelExplicit": "Eksplisit", "LabelExplicit": "Voksenindhold",
"LabelFeedURL": "Feed URL", "LabelFeedURL": "Feed-URL",
"LabelFile": "Fil", "LabelFile": "Fil",
"LabelFileBirthtime": "Oprettelsestidspunkt for fil", "LabelFileBirthtime": "Oprettelsestidspunkt for fil",
"LabelFileModified": "Fil ændret", "LabelFileModified": "Fil ændret",
@@ -155,6 +158,9 @@
"LabelFinished": "Færdig", "LabelFinished": "Færdig",
"LabelFolder": "Mappe", "LabelFolder": "Mappe",
"LabelFontBoldness": "Skrift tykkelse", "LabelFontBoldness": "Skrift tykkelse",
"LabelFontFamily": "Skrifttypefamilie",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Skriftstørrelse", "LabelFontScale": "Skriftstørrelse",
"LabelGenre": "Genre", "LabelGenre": "Genre",
"LabelGenres": "Genrer", "LabelGenres": "Genrer",
@@ -171,12 +177,15 @@
"LabelJumpForwardsTime": "Spring frem tid", "LabelJumpForwardsTime": "Spring frem tid",
"LabelKeepScreenAwake": "Hold skærm aktiv", "LabelKeepScreenAwake": "Hold skærm aktiv",
"LabelLanguage": "Sprog", "LabelLanguage": "Sprog",
"LabelLayout": "Layout", "LabelLayout": "Udseende",
"LabelLayoutAuto": "Automatisk", "LabelLayoutAuto": "Automatisk",
"LabelLayoutSinglePage": "Enkel Side", "LabelLayoutSinglePage": "Enkel Side",
"LabelLibrarySortByProgress": "Fremgang: Sidst opdateret",
"LabelLibrarySortByProgressFinished": "Fremgang: Afsluttet",
"LabelLibrarySortByProgressStarted": "Fremgang: Startet",
"LabelLight": "Lys", "LabelLight": "Lys",
"LabelLineSpacing": "Linjeafstand", "LabelLineSpacing": "Linjeafstand",
"LabelListenAgain": "Lyt Igen", "LabelListenAgain": "Lyt igen",
"LabelLocalBooks": "Lokale Bøger", "LabelLocalBooks": "Lokale Bøger",
"LabelLocalPodcasts": "Lokale Podcasts", "LabelLocalPodcasts": "Lokale Podcasts",
"LabelLockOrientation": "Lås Orientering", "LabelLockOrientation": "Lås Orientering",
@@ -188,8 +197,8 @@
"LabelMore": "Mere", "LabelMore": "Mere",
"LabelMoreInfo": "Mere info", "LabelMoreInfo": "Mere info",
"LabelName": "Navn", "LabelName": "Navn",
"LabelNarrator": "Fortæller", "LabelNarrator": "Oplæser",
"LabelNarrators": "Fortællere", "LabelNarrators": "Oplæsere",
"LabelNavigateWithVolume": "Naviger med lydstyrketasterne", "LabelNavigateWithVolume": "Naviger med lydstyrketasterne",
"LabelNavigateWithVolumeMirrored": "Spejlet", "LabelNavigateWithVolumeMirrored": "Spejlet",
"LabelNavigateWithVolumeWhilePlaying": "Tillad navigering med lydstyrketasterne , mens du spiller", "LabelNavigateWithVolumeWhilePlaying": "Tillad navigering med lydstyrketasterne , mens du spiller",
@@ -197,7 +206,7 @@
"LabelNavigateWithVolumeWhilePlayingEnabled": "Tændt", "LabelNavigateWithVolumeWhilePlayingEnabled": "Tændt",
"LabelNever": "Aldrig", "LabelNever": "Aldrig",
"LabelNewestAuthors": "Nyeste Forfattere", "LabelNewestAuthors": "Nyeste Forfattere",
"LabelNewestEpisodes": "Nyeste Episoder", "LabelNewestEpisodes": "Nyeste afsnit",
"LabelNo": "Nej", "LabelNo": "Nej",
"LabelNotFinished": "Ikke færdig", "LabelNotFinished": "Ikke færdig",
"LabelNotStarted": "Ikke påbegyndt", "LabelNotStarted": "Ikke påbegyndt",
@@ -214,25 +223,26 @@
"LabelPlaybackTranscode": "Transcode", "LabelPlaybackTranscode": "Transcode",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcast", "LabelPodcasts": "Podcast",
"LabelPreventIndexing": "Forhindrer, at dit feed bliver indekseret af iTunes og Google podcastkataloger", "LabelPreventIndexing": "Forhindrer at dit feed bliver indekseret af iTunes- og Google-podcastkataloger",
"LabelProgress": "Fremskridt", "LabelProgress": "Fremskridt",
"LabelPubDate": "Udgivelsesdato", "LabelPubDate": "Udgivelsesdato",
"LabelPublishYear": "Udgivelsesår", "LabelPublishYear": "Udgivelsesår",
"LabelPublishedDate": "Publiceret {0}", "LabelPublishedDate": "Udgivet {0}",
"LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail", "LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail",
"LabelRSSFeedCustomOwnerName": "Brugerdefineret ejerens navn", "LabelRSSFeedCustomOwnerName": "Brugerdefineret ejerens navn",
"LabelRSSFeedOpen": "RSS-feed åbent", "LabelRSSFeedOpen": "RSS-feed åbent",
"LabelRSSFeedPreventIndexing": "Forhindrer indeksering", "LabelRSSFeedPreventIndexing": "Forhindr indeksering",
"LabelRSSFeedSlug": "RSS-feed-slug", "LabelRSSFeedSlug": "RSS-feed-slug",
"LabelRandomly": "Tilfældigt", "LabelRandomly": "Tilfældigt",
"LabelRead": "Læst", "LabelRead": "Læst",
"LabelReadAgain": "Læs Igen", "LabelReadAgain": "Læs igen",
"LabelRecentSeries": "Seneste serier", "LabelRecentSeries": "Seneste serier",
"LabelRecentlyAdded": "Nyligt Tilføjet", "LabelRecentlyAdded": "Nyligt Tilføjet",
"LabelRemoveFromPlaylist": "Fjern fra Playliste", "LabelRemoveFromPlaylist": "Fjern fra Playliste",
"LabelScaleElapsedTimeBySpeed": "Skalér Forløbet Tid med Hastighed", "LabelScaleElapsedTimeBySpeed": "Skalér Forløbet Tid med Hastighed",
"LabelSeason": "Sæson", "LabelSeason": "Sæson",
"LabelSelectADevice": "Vælg en Enhed", "LabelSelectADevice": "Vælg en Enhed",
"LabelSelectMediaType": "Vælg medietype",
"LabelSequenceAscending": "Sekvens Stigende", "LabelSequenceAscending": "Sekvens Stigende",
"LabelSequenceDescending": "Sekvens Faldende", "LabelSequenceDescending": "Sekvens Faldende",
"LabelSeries": "Serie", "LabelSeries": "Serie",
@@ -252,7 +262,7 @@
"LabelStatsDays": "Dage", "LabelStatsDays": "Dage",
"LabelStatsDaysListened": "Dage hørt", "LabelStatsDaysListened": "Dage hørt",
"LabelStatsInARow": "i træk", "LabelStatsInARow": "i træk",
"LabelStatsItemsFinished": "Elementer færdige", "LabelStatsItemsFinished": "færdiglæste bøger",
"LabelStatsMinutes": "minutter", "LabelStatsMinutes": "minutter",
"LabelStatsMinutesListening": "Minutter hørt", "LabelStatsMinutesListening": "Minutter hørt",
"LabelStatsWeekListening": "Ugens lytning", "LabelStatsWeekListening": "Ugens lytning",
@@ -283,14 +293,19 @@
"MessageAndroid10Downloads": "Android 10 og mindre vil bruge intern lagerplads til downloads.", "MessageAndroid10Downloads": "Android 10 og mindre vil bruge intern lagerplads til downloads.",
"MessageAttemptingServerConnection": "Prøver at tilgå server...", "MessageAttemptingServerConnection": "Prøver at tilgå server...",
"MessageAudiobookshelfServerNotConnected": "Audiobookshel server er ikke tilsluttet", "MessageAudiobookshelfServerNotConnected": "Audiobookshel server er ikke tilsluttet",
"MessageAudiobookshelfServerRequired": "<strong>Vigtig</strong> Denne app er designet til at fungere med en Audiobookshelf server, som du eller en du kender hoster. Denne app levere ikke noget indhold.", "MessageAudiobookshelfServerRequired": "<strong>Vigtigt!</strong> Denne app er designet til at fungere med en Audiobookshelf-server, som du eller en du kender hoster. Denne app leverer ikke noget indhold.",
"MessageBookshelfEmpty": "Bogreol Tom", "MessageBookshelfEmpty": "Bogreol Tom",
"MessageConfirmAppExit": "Ville du lukke appen?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Er du sikker på, at du vil rydde downloadkøen for episoder?",
"MessageConfirmDeleteLocalEpisode": "Slet lokal episode \"{0}\" fra din enhed? Filen på serveren vil ikke blive påvirket.", "MessageConfirmDeleteLocalEpisode": "Slet lokal episode \"{0}\" fra din enhed? Filen på serveren vil ikke blive påvirket.",
"MessageConfirmDeleteLocalFiles": "Slet lokale filer af denne type fra din enhed? Filerne på serveren og din fremgang vil ikke blive påvirket.", "MessageConfirmDeleteLocalFiles": "Slet lokale filer af denne type fra din enhed? Filerne på serveren og din fremgang vil ikke blive påvirket.",
"MessageConfirmDeleteServerConfig": "Fjern denne serverkonfiguration?",
"MessageConfirmDeleteServerEpisode": "Er du sikker på at du vil slette episode \"{0}\" fra serveren?\nAdvarsel: Dette sletter lydfilen.",
"MessageConfirmDisableAutoTimer": "Er du sikker på at du vil slå auto-timer fra resten af dagen? Timeren vil blive slået til ved slutningen af auto-sleep timer perioden eller appen genstartes.", "MessageConfirmDisableAutoTimer": "Er du sikker på at du vil slå auto-timer fra resten af dagen? Timeren vil blive slået til ved slutningen af auto-sleep timer perioden eller appen genstartes.",
"MessageConfirmDiscardProgress": "Er du sikker på at du vil nulstille din fremgang?", "MessageConfirmDiscardProgress": "Er du sikker på at du vil nulstille din fremgang?",
"MessageConfirmDownloadUsingCellular": "Du er ved at downloade via mobildata. Dette kan medføre ekstra omkostninger fra din teleoperatør. Vil du fortsætte?", "MessageConfirmDownloadUsingCellular": "Du er ved at downloade via mobildata. Dette kan medføre ekstra omkostninger fra din teleoperatør. Vil du fortsætte?",
"MessageConfirmMarkAsFinished": "Er du sikker på at du vil markere dette element færdig?", "MessageConfirmMarkAsFinished": "Er du sikker på at du vil markere dette element færdig?",
"MessageConfirmPlaybackTime": "Start afspilning af \"{0}\" på {1}?",
"MessageConfirmRemoveBookmark": "Er du sikker på at du vil fjerne bogmærke?", "MessageConfirmRemoveBookmark": "Er du sikker på at du vil fjerne bogmærke?",
"MessageConfirmStreamingUsingCellular": "Du er ved at streame via mobildata. Dette kan medføre ekstra omkostninger fra din teleoperatør. Vil du fortsætte?", "MessageConfirmStreamingUsingCellular": "Du er ved at streame via mobildata. Dette kan medføre ekstra omkostninger fra din teleoperatør. Vil du fortsætte?",
"MessageDiscardProgress": "Kassér fremgang", "MessageDiscardProgress": "Kassér fremgang",
@@ -298,6 +313,7 @@
"MessageDownloading": "Downloader...", "MessageDownloading": "Downloader...",
"MessageDownloadingEpisode": "Downloader episode", "MessageDownloadingEpisode": "Downloader episode",
"MessageEpisodesQueuedForDownload": "{0} episoder er sat i kø til download", "MessageEpisodesQueuedForDownload": "{0} episoder er sat i kø til download",
"MessageFailedToRefreshToken": "Mislykkedes med at opdatere token, nyt loginforsøg nødvendigt",
"MessageFeedURLWillBe": "Feed-URL vil være {0}", "MessageFeedURLWillBe": "Feed-URL vil være {0}",
"MessageFetching": "Henter...", "MessageFetching": "Henter...",
"MessageFollowTheProjectOnGithub": "Følg projektet på GitHub", "MessageFollowTheProjectOnGithub": "Følg projektet på GitHub",
@@ -354,5 +370,7 @@
"ToastPodcastCreateSuccess": "Podcast oprettet med succes", "ToastPodcastCreateSuccess": "Podcast oprettet med succes",
"ToastRSSFeedCloseFailed": "Mislykkedes lukning af RSS-feed", "ToastRSSFeedCloseFailed": "Mislykkedes lukning af RSS-feed",
"ToastRSSFeedCloseSuccess": "RSS-feed lukket", "ToastRSSFeedCloseSuccess": "RSS-feed lukket",
"ToastStreamingNotAllowedOnCellular": "Det er ikke tillad at streame over mobildata" "ToastStreamingNotAllowedOnCellular": "Det er ikke tillad at streame over mobildata",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+21 -4
View File
@@ -63,6 +63,7 @@
"HeaderChapters": "Kapitel", "HeaderChapters": "Kapitel",
"HeaderCollection": "Sammlung", "HeaderCollection": "Sammlung",
"HeaderCollectionItems": "Sammlungseinträge", "HeaderCollectionItems": "Sammlungseinträge",
"HeaderConfirm": "Bestätigen",
"HeaderConnectionStatus": "Verbindungsstatus", "HeaderConnectionStatus": "Verbindungsstatus",
"HeaderDataSettings": "Daten-Einstellungen", "HeaderDataSettings": "Daten-Einstellungen",
"HeaderDetails": "Details", "HeaderDetails": "Details",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Neueste Ereignisse", "HeaderStatsRecentSessions": "Neueste Ereignisse",
"HeaderTableOfContents": "Inhaltsverzeichnis", "HeaderTableOfContents": "Inhaltsverzeichnis",
"HeaderUserInterfaceSettings": "Einstellungen der Benutzeroberfläche", "HeaderUserInterfaceSettings": "Einstellungen der Benutzeroberfläche",
"HeaderWelcome": "Willkommen, <strong>{0}</strong>",
"HeaderYourStats": "Eigene Statistiken", "HeaderYourStats": "Eigene Statistiken",
"LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen", "LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen",
"LabelAddedAt": "Hinzugefügt am", "LabelAddedAt": "Hinzugefügt am",
@@ -113,13 +115,14 @@
"LabelAutoSleepTimerAutoRewindHelp": "Wenn die Schlummerfunktion abgelaufen ist, wird bei der erneuten Wiedergabe des Titels die Position automatisch zurückgespult.", "LabelAutoSleepTimerAutoRewindHelp": "Wenn die Schlummerfunktion abgelaufen ist, wird bei der erneuten Wiedergabe des Titels die Position automatisch zurückgespult.",
"LabelAutoSleepTimerHelp": "Bei der Wiedergabe von Medien zwischen der angegebenen Start- und Endzeit wird automatisch eine Schlummerfunktion gestartet.", "LabelAutoSleepTimerHelp": "Bei der Wiedergabe von Medien zwischen der angegebenen Start- und Endzeit wird automatisch eine Schlummerfunktion gestartet.",
"LabelBooks": "Bücher", "LabelBooks": "Bücher",
"LabelByAuthor": "von {0}",
"LabelChapterTrack": "Kapitelspur", "LabelChapterTrack": "Kapitelspur",
"LabelChapters": "Kapitel", "LabelChapters": "Kapitel",
"LabelClosePlayer": "Player schließen", "LabelClosePlayer": "Player schließen",
"LabelCollapseSeries": "Serien einklappen", "LabelCollapseSeries": "Serien einklappen",
"LabelComplete": "Vollständig", "LabelComplete": "Vollständig",
"LabelContinueBooks": "Bücher fortfahren", "LabelContinueBooks": "Bücher fortsetzen",
"LabelContinueEpisodes": "Episoden fortfahren", "LabelContinueEpisodes": "Episoden fortsetzen",
"LabelContinueListening": "Weiterhören", "LabelContinueListening": "Weiterhören",
"LabelContinueReading": "Weiterlesen", "LabelContinueReading": "Weiterlesen",
"LabelContinueSeries": "Serie fortsetzen", "LabelContinueSeries": "Serie fortsetzen",
@@ -140,7 +143,7 @@
"LabelEbook": "E-Buch", "LabelEbook": "E-Buch",
"LabelEbooks": "E-Bücher", "LabelEbooks": "E-Bücher",
"LabelEnable": "Aktivieren", "LabelEnable": "Aktivieren",
"LabelEnableMp3IndexSeeking": "MP3-Indexsuche", "LabelEnableMp3IndexSeeking": "MP3-Indexsuche aktivieren",
"LabelEnableMp3IndexSeekingHelp": "Diese Einstellung sollte nur aktiviert werden, wenn du MP3-Dateien hast, bei denen das Navigieren (Seeking) nicht korrekt funktioniert. Ungenaues Navigieren ist höchstwahrscheinlich auf MP3-Dateien mit variabler Bitrate (VBR) zurückzuführen. Diese Einstellung erzwingt das Index-Navigieren (Index Seeking), bei dem eine Zeit-zu-Byte-Zuordnung erstellt wird, während die Datei gelesen wird. In einigen Fällen kann es bei großen MP3-Dateien zu einer Verzögerung kommen, wenn gegen Ende der Datei navigiert wird.", "LabelEnableMp3IndexSeekingHelp": "Diese Einstellung sollte nur aktiviert werden, wenn du MP3-Dateien hast, bei denen das Navigieren (Seeking) nicht korrekt funktioniert. Ungenaues Navigieren ist höchstwahrscheinlich auf MP3-Dateien mit variabler Bitrate (VBR) zurückzuführen. Diese Einstellung erzwingt das Index-Navigieren (Index Seeking), bei dem eine Zeit-zu-Byte-Zuordnung erstellt wird, während die Datei gelesen wird. In einigen Fällen kann es bei großen MP3-Dateien zu einer Verzögerung kommen, wenn gegen Ende der Datei navigiert wird.",
"LabelEnd": "Ende", "LabelEnd": "Ende",
"LabelEndOfChapter": "Ende des Kapitels", "LabelEndOfChapter": "Ende des Kapitels",
@@ -155,6 +158,9 @@
"LabelFinished": "Beendet", "LabelFinished": "Beendet",
"LabelFolder": "Ordner", "LabelFolder": "Ordner",
"LabelFontBoldness": "Schriftstärke", "LabelFontBoldness": "Schriftstärke",
"LabelFontFamily": "Schriftfamilie",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serifen",
"LabelFontScale": "Schriftgröße", "LabelFontScale": "Schriftgröße",
"LabelGenre": "Kategorie", "LabelGenre": "Kategorie",
"LabelGenres": "Kategorien", "LabelGenres": "Kategorien",
@@ -174,6 +180,9 @@
"LabelLayout": "Ansicht", "LabelLayout": "Ansicht",
"LabelLayoutAuto": "Auto", "LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Eine Seite", "LabelLayoutSinglePage": "Eine Seite",
"LabelLibrarySortByProgress": "Fortschritt: Letzte Aktualisierung",
"LabelLibrarySortByProgressFinished": "Fortschritt: Beendet",
"LabelLibrarySortByProgressStarted": "Fortschritt: Gestartet",
"LabelLight": "Leicht", "LabelLight": "Leicht",
"LabelLineSpacing": "Zeilenabstand", "LabelLineSpacing": "Zeilenabstand",
"LabelListenAgain": "Erneut anhören", "LabelListenAgain": "Erneut anhören",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Vergangene Zeit anhand der Geschwindigkeit skalieren", "LabelScaleElapsedTimeBySpeed": "Vergangene Zeit anhand der Geschwindigkeit skalieren",
"LabelSeason": "Staffel", "LabelSeason": "Staffel",
"LabelSelectADevice": "Wähle ein Gerät", "LabelSelectADevice": "Wähle ein Gerät",
"LabelSelectMediaType": "Wähle die Art des Mediums",
"LabelSequenceAscending": "Reihenfolge aufsteigend", "LabelSequenceAscending": "Reihenfolge aufsteigend",
"LabelSequenceDescending": "Reihenfolge absteigend", "LabelSequenceDescending": "Reihenfolge absteigend",
"LabelSeries": "Serien", "LabelSeries": "Serien",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf Server nicht verbunden", "MessageAudiobookshelfServerNotConnected": "Audiobookshelf Server nicht verbunden",
"MessageAudiobookshelfServerRequired": "<strong>Wichtig!</strong> Diese App wurde entwickelt, um mit einem Audiobookshelf-Server zu arbeiten, den du oder jemand, den du kennst, hostet. Diese App stellt keine Inhalte zur Verfügung.", "MessageAudiobookshelfServerRequired": "<strong>Wichtig!</strong> Diese App wurde entwickelt, um mit einem Audiobookshelf-Server zu arbeiten, den du oder jemand, den du kennst, hostet. Diese App stellt keine Inhalte zur Verfügung.",
"MessageBookshelfEmpty": "Bücherregal leer", "MessageBookshelfEmpty": "Bücherregal leer",
"MessageConfirmAppExit": "Möchten Sie die App schließen?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Sind Sie sicher, dass Sie die Warteschlange für Epsioden-Downloads leeren wollen?",
"MessageConfirmDeleteLocalEpisode": "Soll die lokale Episode \"{0}\" von deinem Gerät entfernt werden? Die Datei auf dem Server bleibt davon unberührt.", "MessageConfirmDeleteLocalEpisode": "Soll die lokale Episode \"{0}\" von deinem Gerät entfernt werden? Die Datei auf dem Server bleibt davon unberührt.",
"MessageConfirmDeleteLocalFiles": "Sollen lokale Dateien dieses Elements von deinem Gerät entfernt werden? Die Dateien auf dem Server und Ihr Fortschritt bleiben davon unberührt.", "MessageConfirmDeleteLocalFiles": "Sollen lokale Dateien dieses Elements von deinem Gerät entfernt werden? Die Dateien auf dem Server und Ihr Fortschritt bleiben davon unberührt.",
"MessageConfirmDeleteServerConfig": "Diese Server-Konfiguration entfernen?",
"MessageConfirmDeleteServerEpisode": "Sind Sie sicher, dass Sie die Episode \"{0}\" vom Server löschen wollen?\nWarnung: Damit wird die Audio-Datei gelöscht.",
"MessageConfirmDisableAutoTimer": "Bist du sicher, dass du den automatischen Timer für den Rest des Tages deaktivieren möchtest? Der Timer wird am Ende dieser automatischen Schlaf-Timer-Periode oder beim Neustart der App wieder aktiviert.", "MessageConfirmDisableAutoTimer": "Bist du sicher, dass du den automatischen Timer für den Rest des Tages deaktivieren möchtest? Der Timer wird am Ende dieser automatischen Schlaf-Timer-Periode oder beim Neustart der App wieder aktiviert.",
"MessageConfirmDiscardProgress": "Bist du sicher, dass du deinen Fortschritt zurücksetzen willst?", "MessageConfirmDiscardProgress": "Bist du sicher, dass du deinen Fortschritt zurücksetzen willst?",
"MessageConfirmDownloadUsingCellular": "Du bist dabei, über mobile Daten herunterzuladen. Dies kann zu Gebühren deines Mobilfunkanbieters führen. Möchtest du fortfahren?", "MessageConfirmDownloadUsingCellular": "Du bist dabei, über mobile Daten herunterzuladen. Dies kann zu Gebühren deines Mobilfunkanbieters führen. Möchtest du fortfahren?",
"MessageConfirmMarkAsFinished": "Bist du sicher, dass du diesen Artikel als beendet markieren willst?", "MessageConfirmMarkAsFinished": "Bist du sicher, dass du diesen Artikel als beendet markieren willst?",
"MessageConfirmPlaybackTime": "Wiedergabe von \"{0}\" bei {1} starten?",
"MessageConfirmRemoveBookmark": "Bist du sicher, dass du das Lesezeichen entfernen willst?", "MessageConfirmRemoveBookmark": "Bist du sicher, dass du das Lesezeichen entfernen willst?",
"MessageConfirmStreamingUsingCellular": "Du bist dabei, über mobile Daten zu streamen. Dies kann zu Gebühren deines Mobilfunkanbieters führen. Möchtest du fortfahren?", "MessageConfirmStreamingUsingCellular": "Du bist dabei, über mobile Daten zu streamen. Dies kann zu Gebühren deines Mobilfunkanbieters führen. Möchtest du fortfahren?",
"MessageDiscardProgress": "Fortschritt verwerfen", "MessageDiscardProgress": "Fortschritt verwerfen",
@@ -355,5 +370,7 @@
"ToastPodcastCreateSuccess": "Podcast erstellt", "ToastPodcastCreateSuccess": "Podcast erstellt",
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden", "ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen", "ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
"ToastStreamingNotAllowedOnCellular": "Das Streamen über mobile Daten ist nicht erlaubt" "ToastStreamingNotAllowedOnCellular": "Das Streamen über mobile Daten ist nicht erlaubt",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+272
View File
@@ -0,0 +1,272 @@
{
"ButtonAdd": "Προσθήκη",
"ButtonAddNewServer": "Προσθήκη Νέου Διακομιστή",
"ButtonAuthors": "Συγγραφείς",
"ButtonBack": "Πίσω",
"ButtonCancel": "Ακύρωση",
"ButtonCancelTimer": "Ακύρωση Χρονόμετρου",
"ButtonClearFilter": "Διαγραφή Φίλτρου",
"ButtonClearLogs": "Διαγραφή Αρχείων Καταγραφής",
"ButtonCloseFeed": "Κλείσιμο Τροφοδοσίας",
"ButtonCollections": "Συλλογές",
"ButtonConnect": "Σύνδεση",
"ButtonConnectToServer": "Σύνδεση σε Διακομιστή",
"ButtonCreate": "Δημιουργία",
"ButtonCreateBookmark": "Δημιουργία Σελιδοδείκτη",
"ButtonCreateNewPlaylist": "Δημιουργία Νέας Λίστας Αναπαραγωγής",
"ButtonDelete": "Διαγραφή",
"ButtonDeleteLocalEpisode": "Διαγραφή τοπικού επεισοδίου",
"ButtonDeleteLocalFile": "Διαγραφή τοπικού αρχείου",
"ButtonDeleteLocalItem": "Διαγραφή τοπικού αντικειμένου",
"ButtonDisableAutoTimer": "Απενεργοποίηση Αυτόματου Χρονόμετρου",
"ButtonDisconnect": "Αποσύνδεση Σύνδσης",
"ButtonGoToWebClient": "Μετάβαση στον Web Client",
"ButtonHistory": "Ιστορικό",
"ButtonHome": "Αρχική",
"ButtonIssues": "Θέματα",
"ButtonLatest": "Τελευταία",
"ButtonLibrary": "Βιβλιοθήκη",
"ButtonLocalMedia": "Τοπικά Μέσα",
"ButtonLogs": "Αρχεία Καταγραφής",
"ButtonManageLocalFiles": "Διαχείριση Τοπικών Αρχείων",
"ButtonMaskServerAddress": "Απόκρυψη διέθυνσης διακομιστή",
"ButtonNewFolder": "Νέος Φάκελος",
"ButtonNextEpisode": "Επόμενο Επεισόδιο",
"ButtonOk": "Εντάξει",
"ButtonOpenFeed": "Άνοιγμα Τροφοδοσίας",
"ButtonOverride": "Παράκαμψη",
"ButtonPause": "Παύση",
"ButtonPlay": "Αναπαραγωγή",
"ButtonPlayEpisode": "Αναπαραγωγή Επεισοδίου",
"ButtonPlaylists": "Λίστες Αναπαραγωγής",
"ButtonRead": "Ανάγνωση",
"ButtonReadLess": "Ανάγνωση λιγότερων",
"ButtonReadMore": "Διάβασε περισσότερα",
"ButtonRemove": "Αφαίρεση",
"ButtonRemoveFromServer": "Αφαίρεση απ'τον Διακομιστή",
"ButtonSave": "Αποθήκευση",
"ButtonSaveOrder": "Αποθήκευση Ταξινόμησης",
"ButtonSearch": "Αναζήτηση",
"ButtonSendEbookToDevice": "Αποστολή Ebook σε Συσκευή",
"ButtonSeries": "Σειρά",
"ButtonSetTimer": "Ορισμός Χρονοδιακόπτη",
"ButtonStream": "Μετάδοση",
"ButtonSubmit": "Υποβολή",
"ButtonSwitchServerUser": "Αλλαγή Διακομιστή/Χρήστη",
"ButtonUnmaskServerAddress": "Αφαίρεση απόκρυψης διακομιστή",
"ButtonUserStats": "Στατιστικά Χρήστη",
"ButtonYes": "Ναι",
"HeaderAccount": "Λογαριασμός",
"HeaderAdvanced": "Για Προχωρημένους",
"HeaderAndroidAutoSettings": "Αυτόματες Ρυθμίσεις Διαχειριστή",
"HeaderAudioTracks": "Κομμάτια Ήχου",
"HeaderChapters": "Κεφάλαια",
"HeaderCollection": "Συλλογή",
"HeaderCollectionItems": "Αντικείμενα Συλλογής",
"HeaderConfirm": "Επιβεβαίωση",
"HeaderConnectionStatus": "Στατιστικά Σύνδεσης",
"HeaderDataSettings": "Ρυθμίσεις Δεδομένων",
"HeaderDetails": "Λεπτομέρειες",
"HeaderDownloads": "Λήψεις",
"HeaderEbookFiles": "Αρχεία Ebook",
"HeaderEpisodes": "Επεισόδια",
"HeaderEreaderSettings": "Ρυθμίσεις Ereader",
"HeaderLatestEpisodes": "Τελευταία Επεισόδια",
"HeaderLibraries": "Βιβλιοθήκες",
"HeaderLocalFolders": "Τοπικοί Φάκελοι",
"HeaderLocalLibraryItems": "Αντικείμενα Τοπικής Βιβλιοθήκης",
"HeaderNewPlaylist": "Νέα Λίστα Αναπαραγωγής",
"HeaderOpenRSSFeed": "Άνοιγμα Τροφοδοσίας RSS",
"HeaderPlaybackSettings": "Ρυθμίσεις Αναπαραγωγής",
"HeaderPlaylist": "Λίστα Αναπαραγωγής",
"HeaderPlaylistItems": "Αντικείμενα Λίστας Αναπαραγωγής",
"HeaderProgressSyncFailed": "Αποτυχία Συγχρονισμού Προόδου",
"HeaderRSSFeed": "Τροφοδοσία RSS",
"HeaderRSSFeedGeneral": "Λεπτομέρειες RSS",
"HeaderRSSFeedIsOpen": "Η Τροφοδοσία RSS είναι Ανοιχτή",
"HeaderSelectDownloadLocation": "Επιλογή Τοποθεσίας Λήψης",
"HeaderSettings": "Ρυθμίσεις",
"HeaderSleepTimer": "Χρονοδιακόπτης Ύπνου",
"HeaderSleepTimerSettings": "Ρυθμίσεις Χρονοδιακόπτη Ύπνου",
"HeaderStatsMinutesListeningChart": "Λεπτά Ακρόασης (τελευταίες 7 ημέρες)",
"HeaderStatsRecentSessions": "Πρόσφατες Συνεδρίες",
"HeaderTableOfContents": "Πίνακας Περιεχομένων",
"HeaderUserInterfaceSettings": "Ρυθμίσεις Διεπαφής Χρήστη",
"HeaderWelcome": "Καλωσήρθες, <strong>{0}</strong>",
"HeaderYourStats": "Τα Στατιστικά Σας",
"LabelAddToPlaylist": "Προσθήκη στην Λίστα Αναπαραγωγής",
"LabelAddedAt": "Προστέθηκε Στις",
"LabelAddedDate": "Προστέθηκε {0}",
"LabelAll": "Όλα",
"LabelAllowSeekingOnMediaControls": "Να επιτρέπεται η αλλαγή θέσης από τα χειριστήρια ειδοποιήσεων πολυμέσων",
"LabelAlways": "Πάντα",
"LabelAndroidAutoBrowseLimitForGrouping": "Όριο λίστας κατά αλφαβητική σειρά",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "Μην χρησιμοποιείτε την αλφαβητική λίστα περιορισμού όταν υπάρχουν λιγότερα από αυτά τα στοιχεία για εμφάνιση",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "Ταξινόμηση βιβλίων σειράς",
"LabelAskConfirmation": "Ερώτηση για επιβεβαίωση",
"LabelAuthor": "Συγγραφέας",
"LabelAuthorFirstLast": "Συγγραφέας (Όνομα Επώνυμο)",
"LabelAuthorLastFirst": "Συγγραφέας (Επώνυμο, Όνομα)",
"LabelAuthors": "Συγγραφείς",
"LabelAutoDownloadEpisodes": "Αυτόματο Κατέβασμα Επεισοδίων",
"LabelAutoRewindTime": "Χρόνος αυτόματης επαναφοράς",
"LabelAutoSleepTimer": "Αυτόματος Χρονοδιακόπτης Ύπνου",
"LabelAutoSleepTimerAutoRewind": "Αυτόματη επαναφορά κατά τον χρονοδιακόπτη ύπνου",
"LabelAutoSleepTimerAutoRewindHelp": "Όταν ολοκληρωθεί ο αυτόματος χρονοδιακόπτης ύπνου, η επαναφορά της αναπαραγωγής θα γυρίζει αυτόματα τη θέση σας πίσω.",
"LabelAutoSleepTimerHelp": "Όταν αναπαράγεται περιεχόμενο μεταξύ των καθορισμένων χρόνων έναρξης και λήξης, ο χρονοδιακόπτης ύπνου θα ξεκινά αυτόματα.",
"LabelBooks": "Βιβλία",
"LabelByAuthor": "κατά {0}",
"LabelChapterTrack": "Κομμάτι Κεφαλαίου",
"LabelChapters": "Κεφάλαια",
"LabelClosePlayer": "Κλείσιμο αναπαραγωγής",
"LabelCollapseSeries": "Σύμπτυξη Σειράς",
"LabelComplete": "Ολοκλήρωση",
"LabelContinueBooks": "Συνέχεια Βιβλίων",
"LabelContinueEpisodes": "Συνέχεια Επεισοδίων",
"LabelContinueListening": "Συνέχεια Ακρόασης",
"LabelContinueReading": "Συνέχεια Ανάγνωσης",
"LabelContinueSeries": "Συνέχεια Σειράς",
"LabelCustomTime": "Προσαρμοσμένη ώρα",
"LabelDescription": "Περιγραφή",
"LabelDisableAudioFadeOut": "Απενεργοποίηση σταδιακής μείωσης ήχου",
"LabelDisableAudioFadeOutHelp": "Η ένταση του ήχου θα αρχίσει να μειώνεται όταν απομένει λιγότερο από 1 λεπτό στον χρονοδιακόπτη ύπνου. Ενεργοποιήστε αυτή τη ρύθμιση για να μην γίνεται σταδιακή μείωση.",
"LabelDisableAutoRewind": "Απενεργοποίηση αυτόματης επαναφοράς",
"LabelDisableShakeToReset": "Απενεργοποιήστε το κούνημα για επαναφορά",
"LabelDisableShakeToResetHelp": "Αν κουνήσετε τη συσκευή σας ενώ λειτουργεί ο χρονοδιακόπτης Ή εντός 2 λεπτών από τη λήξη του χρονοδιακόπτη, θα επαναφέρετε τον χρονοδιακόπτη ύπνου. Ενεργοποιήστε αυτήν τη ρύθμιση για να απενεργοποιήσετε το κούνημα για επαναφορά.",
"LabelDisableVibrateOnReset": "Απενεργοποίηση δόνησης κατά την επαναφορά",
"LabelDisableVibrateOnResetHelp": "Όταν γίνει επαναφορά του χρονοδιακόπτη ύπνου, η συσκευή σας θα δονηθεί. Ενεργοποιήστε αυτήν τη ρύθμιση για να μην δονείται κατά την επαναφορά του χρονοδιακόπτη ύπνου.",
"LabelDiscover": "Ανακάλυψη",
"LabelDownload": "Λήψη",
"LabelDownloadUsingCellular": "Λήψη με χρήση δεδομένων",
"LabelDownloaded": "Ληφθέντα",
"LabelDuration": "Διάρκεια",
"LabelEbook": "Ηλεκτρονικό Βιβλίο/ Ebook",
"LabelEbooks": "Ηλεκτρονικά Βιβλία / Ebooks",
"LabelEnable": "Ενεργοποίηση",
"LabelEnableMp3IndexSeeking": "Ενεργοποίηση αναζήτησης σε mp3 ευρετήριο",
"LabelEnableMp3IndexSeekingHelp": "Αυτή η ρύθμιση θα πρέπει να είναι ενεργοποιημένη μόνο εάν έχετε αρχεία mp3 που δεν έχει γίνει σωστή αναζήτηση. Η ανακριβής αναζήτηση πιθανότατα οφείλεται σε αρχεία MP3 με μεταβλητό ρυθμό μετάδοσης bit (VBR). Αυτή η ρύθμιση θα επιβάλει την αναζήτηση ευρετηρίου, στην οποία δημιουργείται μια αντιστοίχιση χρόνου-byte καθώς διαβάζεται το αρχείο. Σε ορισμένες περιπτώσεις, με μεγάλα αρχεία MP3, θα υπάρχει καθυστέρηση κατά την αναζήτηση προς το τέλος του αρχείου.",
"LabelEnd": "Τέλος",
"LabelEndOfChapter": "Τέλος Κεφαλαίου",
"LabelEndTime": "Ώρα λήξης",
"LabelEpisode": "Επεισόδιο",
"LabelExplicit": "Άσεμνο περιεχόμενο",
"LabelFeedURL": "URL ροής",
"LabelFile": "Αρχείο",
"LabelFileBirthtime": "Ώρα δημιουργίας αρχείου",
"LabelFileModified": "Τροποποιημένο αρχείο",
"LabelFilename": "Όνομα Αρχείου",
"LabelFinished": "Ολοκληρώθηκε",
"LabelFolder": "Φάκελος",
"LabelFontBoldness": "Ένταση γραμματοσειράς",
"LabelFontFamily": "Οικογένεια Γραμματοσειράς",
"LabelFontScale": "Μέγεθος γραμματοσειράς",
"LabelGenre": "Λογοτεχνικό Είδος",
"LabelGenres": "Λογοτεχνικά Είδη",
"LabelHapticFeedback": "Απτική ανατροφοδότηση",
"LabelHasEbook": "Έχει ebook",
"LabelHasSupplementaryEbook": "Διαθέτει συμπληρωματικό ηλεκτρονικό βιβλίο",
"LabelHeavy": "Έντονη",
"LabelHigh": "Υψηλή",
"LabelHost": "Διακομιστής",
"LabelInProgress": "Σε Εξέλιξη",
"LabelIncomplete": "Ελειπής",
"LabelInternalAppStorage": "Εσωτερικός χώρος αποθήκευσης εφαρμογών",
"LabelJumpBackwardsTime": "Πήγαινε πίσω το χρόνο",
"LabelJumpForwardsTime": "Πήγαινε μπρος το χρόνο",
"LabelKeepScreenAwake": "Να παραμείνει η οθόνη ενεργή",
"LabelLanguage": "Γλώσσα",
"LabelLayout": "Διάταξη",
"LabelLayoutAuto": "Αυτόματα",
"LabelLayoutSinglePage": "Μονή Σελίδα",
"LabelLibrarySortByProgress": "Πρόοδος: Τελευταία ενημέρωση",
"LabelLibrarySortByProgressFinished": "Πρόοδος: Ολοκληρώθηκε",
"LabelLibrarySortByProgressStarted": "Πρόοδος: Έναρξη",
"LabelLight": "Ανοιχτό",
"LabelLineSpacing": "Διάστημα γραμμών",
"LabelListenAgain": "Επανάληψη Ακρόασης",
"LabelLocalBooks": "Τοπικά Βιβλία",
"LabelLocalPodcasts": "Τοπικά Podcasts",
"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": "Ταχύτητα Αναπαραγωγής",
"LabelProgress": "Πρόοδος",
"LabelPubDate": "Ημερομηνία έκδοσης",
"LabelPublishYear": "Χρονολογία Έκδοσης",
"LabelPublishedDate": "Εκδόθηκε {0}",
"LabelRandomly": "Τυχαία",
"LabelRead": "Ανάγνωση",
"LabelReadAgain": "Ανάγνωση Ξανά",
"LabelRecentSeries": "Πρόσφατη Σειρά",
"LabelRecentlyAdded": "Προστέθηκαν Πρόσφατα",
"LabelRemoveFromPlaylist": "Αφαίρεση από την Λίστα Αναπαραγωγής",
"LabelSelectADevice": "Επιλογή συσκευής",
"LabelSeries": "Σειρά",
"LabelServerAddress": "Διεύθυνση Διακομιστή",
"LabelSetEbookAsPrimary": "Ορισμός ως πρωτεύων",
"LabelShowAll": "Εμφάνιση Όλων",
"LabelSize": "Μέγεθος",
"LabelSleepTimer": "Χρονοδιακόπτης Ύπνου",
"LabelStart": "Έναρξη",
"LabelStatsBestDay": "Καλύτερη Ημέρα",
"LabelStatsDailyAverage": "Ημερήσιος Μέσος Όρος",
"LabelStatsDays": "Ημέρες",
"LabelStatsDaysListened": "Ημέρες Ακρόασης",
"LabelStatsInARow": "Σε σειρά",
"LabelStatsItemsFinished": "Ολοκληρωμένα Αντικείμενα",
"LabelStatsMinutes": "λεπτά",
"LabelStatsMinutesListening": "Λεπτά Ακρόασης",
"LabelStatsWeekListening": "Εβδομαδιαία Ακρόαση",
"LabelTheme": "Θέμα",
"LabelThemeBlack": "Μαύρο",
"LabelThemeDark": "Σκοτεινό",
"LabelThemeLight": "Φωτεινό",
"LabelTimeRemaining": "{0} απομένουν",
"LabelTitle": "Τίτλος",
"LabelTotalSize": "Συνολικό Μέγεθος",
"LabelTracks": "Κομμάτια",
"LabelType": "Τύπος",
"LabelUnknown": "Άγνωστο",
"LabelUseBookshelfView": "Χρήση προβολής βιβλιοθήκης",
"LabelUser": "Χρήστης",
"LabelUsername": "Όνομα Χρήστη",
"LabelVeryHigh": "Πολύ Ψηλά",
"LabelVeryLow": "Πολύ Χαμηλά",
"LabelYourProgress": "Η Πρόοδος Σας",
"MessageDownloadCompleteProcessing": "Η λήψη ολοκληρώθηκε. Επεξεργασία...",
"MessageDownloading": "Λήψη...",
"MessageDownloadingEpisode": "Λήψη επεισοδίου",
"MessageLoading": "Φόρτωση...",
"MessageMarkAsFinished": "Σήμανση ως Ολοκληρωμένο",
"MessageNoItemsFound": "Δεν βρέθηκαν αντικείμενα",
"MessageNoNetworkConnection": "Δεν υπάρχει σύνδεση δικτύου",
"MessageNoUserPlaylists": "Δεν έχετε λίστες αναπαραγωγής"
}
+22 -1
View File
@@ -1,6 +1,7 @@
{ {
"ButtonAdd": "Add", "ButtonAdd": "Add",
"ButtonAddNewServer": "Add New Server", "ButtonAddNewServer": "Add New Server",
"ButtonAllow": "Allow",
"ButtonAuthors": "Authors", "ButtonAuthors": "Authors",
"ButtonBack": "Back", "ButtonBack": "Back",
"ButtonCancel": "Cancel", "ButtonCancel": "Cancel",
@@ -63,6 +64,7 @@
"HeaderChapters": "Chapters", "HeaderChapters": "Chapters",
"HeaderCollection": "Collection", "HeaderCollection": "Collection",
"HeaderCollectionItems": "Collection Items", "HeaderCollectionItems": "Collection Items",
"HeaderConfirm": "Confirm",
"HeaderConnectionStatus": "Connection Status", "HeaderConnectionStatus": "Connection Status",
"HeaderDataSettings": "Data Settings", "HeaderDataSettings": "Data Settings",
"HeaderDetails": "Details", "HeaderDetails": "Details",
@@ -91,6 +93,7 @@
"HeaderStatsRecentSessions": "Recent Sessions", "HeaderStatsRecentSessions": "Recent Sessions",
"HeaderTableOfContents": "Table of Contents", "HeaderTableOfContents": "Table of Contents",
"HeaderUserInterfaceSettings": "User Interface Settings", "HeaderUserInterfaceSettings": "User Interface Settings",
"HeaderWelcome": "Welcome, <strong>{0}</strong>",
"HeaderYourStats": "Your Stats", "HeaderYourStats": "Your Stats",
"LabelAddToPlaylist": "Add to Playlist", "LabelAddToPlaylist": "Add to Playlist",
"LabelAddedAt": "Added At", "LabelAddedAt": "Added At",
@@ -113,6 +116,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "When the auto sleep timer finishes, playing the item again will automatically rewind your position.", "LabelAutoSleepTimerAutoRewindHelp": "When the auto sleep timer finishes, playing the item again will automatically rewind your position.",
"LabelAutoSleepTimerHelp": "When playing media between the specified start and end times a sleep timer will automatically start.", "LabelAutoSleepTimerHelp": "When playing media between the specified start and end times a sleep timer will automatically start.",
"LabelBooks": "Books", "LabelBooks": "Books",
"LabelByAuthor": "by {0}",
"LabelChapterTrack": "Chapter Track", "LabelChapterTrack": "Chapter Track",
"LabelChapters": "Chapters", "LabelChapters": "Chapters",
"LabelClosePlayer": "Close player", "LabelClosePlayer": "Close player",
@@ -177,6 +181,9 @@
"LabelLayout": "Layout", "LabelLayout": "Layout",
"LabelLayoutAuto": "Auto", "LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Single page", "LabelLayoutSinglePage": "Single page",
"LabelLibrarySortByProgress": "Progress: Last Updated",
"LabelLibrarySortByProgressFinished": "Progress: Finished",
"LabelLibrarySortByProgressStarted": "Progress: Started",
"LabelLight": "Light", "LabelLight": "Light",
"LabelLineSpacing": "Line spacing", "LabelLineSpacing": "Line spacing",
"LabelListenAgain": "Listen Again", "LabelListenAgain": "Listen Again",
@@ -236,6 +243,7 @@
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed", "LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
"LabelSeason": "Season", "LabelSeason": "Season",
"LabelSelectADevice": "Select a device", "LabelSelectADevice": "Select a device",
"LabelSelectMediaType": "Select media type",
"LabelSequenceAscending": "Sequence Ascending", "LabelSequenceAscending": "Sequence Ascending",
"LabelSequenceDescending": "Sequence Descending", "LabelSequenceDescending": "Sequence Descending",
"LabelSeries": "Series", "LabelSeries": "Series",
@@ -288,18 +296,24 @@
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf server not connected", "MessageAudiobookshelfServerNotConnected": "Audiobookshelf server not connected",
"MessageAudiobookshelfServerRequired": "<strong>Important!</strong> This app is designed to work with an Audiobookshelf server that you or someone you know is hosting. This app does not provide any content.", "MessageAudiobookshelfServerRequired": "<strong>Important!</strong> This app is designed to work with an Audiobookshelf server that you or someone you know is hosting. This app does not provide any content.",
"MessageBookshelfEmpty": "Bookshelf empty", "MessageBookshelfEmpty": "Bookshelf empty",
"MessageConfirmAppExit": "Did you want to exit the app?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Are you sure you want to clear episode download queue?",
"MessageConfirmDeleteLocalEpisode": "Remove local episode \"{0}\" from your device? The file on the server will be unaffected.", "MessageConfirmDeleteLocalEpisode": "Remove local episode \"{0}\" from your device? The file on the server will be unaffected.",
"MessageConfirmDeleteLocalFiles": "Remove local files of this item from your device? The files on the server and your progress will be unaffected.", "MessageConfirmDeleteLocalFiles": "Remove local files of this item from your device? The files on the server and your progress will be unaffected.",
"MessageConfirmDeleteServerConfig": "Remove this server config?",
"MessageConfirmDeleteServerEpisode": "Are you sure you want to delete episode \"{0}\" from the server?\nWarning: This will delete the audio file.",
"MessageConfirmDisableAutoTimer": "Are you sure you want to disable the auto timer for the rest of today? The timer will be re-enabled at the end of this auto-sleep timer period, or if you restart the app.", "MessageConfirmDisableAutoTimer": "Are you sure you want to disable the auto timer for the rest of today? The timer will be re-enabled at the end of this auto-sleep timer period, or if you restart the app.",
"MessageConfirmDiscardProgress": "Are you sure you want to reset your progress?", "MessageConfirmDiscardProgress": "Are you sure you want to reset your progress?",
"MessageConfirmDownloadUsingCellular": "You are about to download using cellular data. This may include carrier data charges. Do you wish to continue?", "MessageConfirmDownloadUsingCellular": "You are about to download using cellular data. This may include carrier data charges. Do you wish to continue?",
"MessageConfirmMarkAsFinished": "Are you sure you want to mark this item as finished?", "MessageConfirmMarkAsFinished": "Are you sure you want to mark this item as finished?",
"MessageConfirmPlaybackTime": "Start playback for \"{0}\" at {1}?",
"MessageConfirmRemoveBookmark": "Are you sure you want to remove bookmark?", "MessageConfirmRemoveBookmark": "Are you sure you want to remove bookmark?",
"MessageConfirmStreamingUsingCellular": "You are about to stream using cellular data. This may include carrier data charges. Do you wish to continue?", "MessageConfirmStreamingUsingCellular": "You are about to stream using cellular data. This may include carrier data charges. Do you wish to continue?",
"MessageDiscardProgress": "Discard Progress", "MessageDiscardProgress": "Discard Progress",
"MessageDownloadCompleteProcessing": "Download complete. Processing...", "MessageDownloadCompleteProcessing": "Download complete. Processing...",
"MessageDownloading": "Downloading...", "MessageDownloading": "Downloading...",
"MessageDownloadingEpisode": "Downloading episode", "MessageDownloadingEpisode": "Downloading episode",
"MessageDownloadingFile": "Downloading {0}",
"MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download", "MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download",
"MessageFailedToRefreshToken": "Failed to refresh token, re-login required", "MessageFailedToRefreshToken": "Failed to refresh token, re-login required",
"MessageFeedURLWillBe": "Feed URL will be {0}", "MessageFeedURLWillBe": "Feed URL will be {0}",
@@ -335,6 +349,7 @@
"MessageOldServerConnectionWarning": "Server connection config is using an old user ID. Please delete and re-add this server connection.", "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.", "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", "MessagePodcastSearchField": "Enter search term or RSS feed URL",
"MessagePreparingDownloads": "Preparing downloads",
"MessageProgressSyncFailed": "The most recent attempt to report your listening progress to the server has failed. Progress sync requests will continue to be attempted every 15 seconds to 1 minute while media is playing.", "MessageProgressSyncFailed": "The most recent attempt to report your listening progress to the server has failed. Progress sync requests will continue to be attempted every 15 seconds to 1 minute while media is playing.",
"MessageReportBugsAndContribute": "Report bugs, request features, and contribute on", "MessageReportBugsAndContribute": "Report bugs, request features, and contribute on",
"MessageSeriesAlreadyDownloaded": "You have already downloaded all books in this series.", "MessageSeriesAlreadyDownloaded": "You have already downloaded all books in this series.",
@@ -345,6 +360,10 @@
"MessageSocketConnectedOverUnmeteredCellular": "Socket connected over unmetered cellular", "MessageSocketConnectedOverUnmeteredCellular": "Socket connected over unmetered cellular",
"MessageSocketConnectedOverUnmeteredWifi": "Socket connected over unmetered wifi", "MessageSocketConnectedOverUnmeteredWifi": "Socket connected over unmetered wifi",
"MessageSocketNotConnected": "Socket not connected", "MessageSocketNotConnected": "Socket not connected",
"MessageStorageAccessDenied": "Access denied",
"MessageStoragePermissionDenied": "Permission denied",
"MessageStorageWriteAccessRequired": "You do not have write access to this folder. Would you like to grant access?",
"MessageWaitingForAvailableStorage": "Waiting for available storage",
"NoteRSSFeedPodcastAppsHttps": "Warning: Most podcast apps will require the RSS feed URL is using HTTPS", "NoteRSSFeedPodcastAppsHttps": "Warning: Most podcast apps will require the RSS feed URL is using HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.", "NoteRSSFeedPodcastAppsPubDate": "Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.",
"ToastBookmarkCreateFailed": "Failed to create bookmark", "ToastBookmarkCreateFailed": "Failed to create bookmark",
@@ -358,5 +377,7 @@
"ToastPodcastCreateSuccess": "Podcast created successfully", "ToastPodcastCreateSuccess": "Podcast created successfully",
"ToastRSSFeedCloseFailed": "Failed to close RSS feed", "ToastRSSFeedCloseFailed": "Failed to close RSS feed",
"ToastRSSFeedCloseSuccess": "RSS feed closed", "ToastRSSFeedCloseSuccess": "RSS feed closed",
"ToastStreamingNotAllowedOnCellular": "Streaming is not allowed on cellular data" "ToastStreamingNotAllowedOnCellular": "Streaming is not allowed on cellular data",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+85 -63
View File
@@ -5,7 +5,7 @@
"ButtonBack": "Atrás", "ButtonBack": "Atrás",
"ButtonCancel": "Cancelar", "ButtonCancel": "Cancelar",
"ButtonCancelTimer": "Cancelar temporizador", "ButtonCancelTimer": "Cancelar temporizador",
"ButtonClearFilter": "Quitar filtros", "ButtonClearFilter": "Vaciar filtro",
"ButtonClearLogs": "Vaciar registros", "ButtonClearLogs": "Vaciar registros",
"ButtonCloseFeed": "Cerrar suministro", "ButtonCloseFeed": "Cerrar suministro",
"ButtonCollections": "Colecciones", "ButtonCollections": "Colecciones",
@@ -18,16 +18,16 @@
"ButtonDeleteLocalEpisode": "Eliminar episodio local", "ButtonDeleteLocalEpisode": "Eliminar episodio local",
"ButtonDeleteLocalFile": "Eliminar archivo local", "ButtonDeleteLocalFile": "Eliminar archivo local",
"ButtonDeleteLocalItem": "Eliminar elemento local", "ButtonDeleteLocalItem": "Eliminar elemento local",
"ButtonDisableAutoTimer": "Desactivar temporizador automático", "ButtonDisableAutoTimer": "Inhabilitar temporizador automático",
"ButtonDisconnect": "Desconectar", "ButtonDisconnect": "Desconectar",
"ButtonGoToWebClient": "Ir al cliente web", "ButtonGoToWebClient": "Ir al cliente web",
"ButtonHistory": "Historial", "ButtonHistory": "Historial",
"ButtonHome": "Inicio", "ButtonHome": "Inicio",
"ButtonIssues": "Problemas", "ButtonIssues": "Incidencias",
"ButtonLatest": "Más recientes", "ButtonLatest": "Más recientes",
"ButtonLibrary": "Biblioteca", "ButtonLibrary": "Biblioteca",
"ButtonLocalMedia": "Medios locales", "ButtonLocalMedia": "Medios locales",
"ButtonLogs": "Registros", "ButtonLogs": "Bitácoras",
"ButtonManageLocalFiles": "Gestionar archivos locales", "ButtonManageLocalFiles": "Gestionar archivos locales",
"ButtonMaskServerAddress": "Enmascarar dirección de servidor", "ButtonMaskServerAddress": "Enmascarar dirección de servidor",
"ButtonNewFolder": "Carpeta nueva", "ButtonNewFolder": "Carpeta nueva",
@@ -49,102 +49,105 @@
"ButtonSearch": "Buscar", "ButtonSearch": "Buscar",
"ButtonSendEbookToDevice": "Enviar libro al dispositivo", "ButtonSendEbookToDevice": "Enviar libro al dispositivo",
"ButtonSeries": "Series", "ButtonSeries": "Series",
"ButtonSetTimer": "Ajustar el temporizador", "ButtonSetTimer": "Ajustar cronómetro",
"ButtonStream": "En directo", "ButtonStream": "En directo",
"ButtonSubmit": "Enviar", "ButtonSubmit": "Entregar",
"ButtonSwitchServerUser": "Cambiar servidor/usuario", "ButtonSwitchServerUser": "Cambiar servidor/usuario",
"ButtonUnmaskServerAddress": "Desenmascarar dirección de servidor", "ButtonUnmaskServerAddress": "Desenmascarar dirección de servidor",
"ButtonUserStats": "Estadísticas de usuario", "ButtonUserStats": "Estadísticas de usuario",
"ButtonYes": "Sí", "ButtonYes": "Sí",
"HeaderAccount": "Cuenta", "HeaderAccount": "Cuenta",
"HeaderAdvanced": "Avanzado", "HeaderAdvanced": "Avanzado",
"HeaderAndroidAutoSettings": "Configuración de Android Auto", "HeaderAndroidAutoSettings": "Ajustes automáticos de Android",
"HeaderAudioTracks": "Pistas de audio", "HeaderAudioTracks": "Pistas de Audio",
"HeaderChapters": "Capítulos", "HeaderChapters": "Capítulos",
"HeaderCollection": "Colección", "HeaderCollection": "Colección",
"HeaderCollectionItems": "Elementos en la colección", "HeaderCollectionItems": "Elementos de colección",
"HeaderConnectionStatus": "Estado de la conexión", "HeaderConfirm": "Confirmar",
"HeaderDataSettings": "Configuración de datos", "HeaderConnectionStatus": "Estado de conexión",
"HeaderDataSettings": "Ajustes de datos",
"HeaderDetails": "Detalles", "HeaderDetails": "Detalles",
"HeaderDownloads": "Descargas", "HeaderDownloads": "Descargas",
"HeaderEbookFiles": "Archivos de libros digitales", "HeaderEbookFiles": "Archivos de libros digitales",
"HeaderEpisodes": "Episodios", "HeaderEpisodes": "Episodios",
"HeaderEreaderSettings": "Configuración del lector", "HeaderEreaderSettings": "Ajustes del Lector-e",
"HeaderLatestEpisodes": "Episodios más recientes", "HeaderLatestEpisodes": "Episodios más recientes",
"HeaderLibraries": "Bibliotecas", "HeaderLibraries": "Bibliotecas",
"HeaderLocalFolders": "Carpetas locales", "HeaderLocalFolders": "Carpetas locales",
"HeaderLocalLibraryItems": "Elementos de la biblioteca local", "HeaderLocalLibraryItems": "Elementos de Biblioteca Local",
"HeaderNewPlaylist": "Nueva lista de reproducción", "HeaderNewPlaylist": "Nueva lista de reproducción",
"HeaderOpenRSSFeed": "Abrir suministro RSS", "HeaderOpenRSSFeed": "Abrir suministro RSS",
"HeaderPlaybackSettings": "Configuración de reproducción", "HeaderPlaybackSettings": "Ajustes de Reproducción",
"HeaderPlaylist": "Lista de reproducción", "HeaderPlaylist": "Lista de reproducción",
"HeaderPlaylistItems": "Elementos de lista de reproducción", "HeaderPlaylistItems": "Elementos de lista de reproducción",
"HeaderProgressSyncFailed": "Falló la sincronización del progreso", "HeaderProgressSyncFailed": "Progreso de sincronización incorrecto",
"HeaderRSSFeed": "Suministro RSS", "HeaderRSSFeed": "Suministro RSS",
"HeaderRSSFeedGeneral": "Detalles de RSS", "HeaderRSSFeedGeneral": "Detalles de RSS",
"HeaderRSSFeedIsOpen": "El suministro RSS está abierto", "HeaderRSSFeedIsOpen": "El suministro RSS está abierto",
"HeaderSelectDownloadLocation": "Seleccionar ubicación de descarga", "HeaderSelectDownloadLocation": "Seleccionar lugar de descarga",
"HeaderSettings": "Configuración", "HeaderSettings": "Ajustes",
"HeaderSleepTimer": "Temporizador de apagado", "HeaderSleepTimer": "Cronómetro de dormida",
"HeaderSleepTimerSettings": "Ajustes del temporizador para dormir", "HeaderSleepTimerSettings": "Ajustes del temporizador para dormida",
"HeaderStatsMinutesListeningChart": "Minutos escuchando (últimos 7 días)", "HeaderStatsMinutesListeningChart": "Minutos escuchando (últimos 7 días)",
"HeaderStatsRecentSessions": "Sesiones recientes", "HeaderStatsRecentSessions": "Sesiones recientes",
"HeaderTableOfContents": "Sumario", "HeaderTableOfContents": "Sumario",
"HeaderUserInterfaceSettings": "Configuración de interfaz de usuario", "HeaderUserInterfaceSettings": "Ajustes de interfaz de usuario",
"HeaderWelcome": "Bienvenido/a, <strong>{0}</strong>",
"HeaderYourStats": "Sus estadísticas", "HeaderYourStats": "Sus estadísticas",
"LabelAddToPlaylist": "Añadir a lista de reproducción", "LabelAddToPlaylist": "Añadir a lista de reproducción",
"LabelAddedAt": "Añadido", "LabelAddedAt": "Añadido en",
"LabelAddedDate": "{0} Añadido", "LabelAddedDate": "Añadido {0}",
"LabelAll": "Todos", "LabelAll": "Todos",
"LabelAllowSeekingOnMediaControls": "Permitir la búsqueda de posición en los controles de notificación de medios", "LabelAllowSeekingOnMediaControls": "Concede la búsqueda de posición en los controles de notificación de medios",
"LabelAlways": "Siempre", "LabelAlways": "Siempre",
"LabelAndroidAutoBrowseLimitForGrouping": "Limite del despliegue alfabético", "LabelAndroidAutoBrowseLimitForGrouping": "Límite del despliegue alfabético",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "No utilice el despliegue alfabético cuando haya menos de esta cantidad de elementos para mostrar", "LabelAndroidAutoBrowseLimitForGroupingHelp": "No utilice el despliegue alfabético cuando haya menos de esta cantidad de elementos para mostrar",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "Orden de libros de Series", "LabelAndroidAutoBrowseSeriesSequenceOrder": "Ordenación de libros de Series",
"LabelAskConfirmation": "Pedir confirmación", "LabelAskConfirmation": "Pedir confirmación",
"LabelAuthor": "Autor", "LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Nombre Apellido)", "LabelAuthorFirstLast": "Autor (Nombre Apellido)",
"LabelAuthorLastFirst": "Autor (Apellido, Nombre)", "LabelAuthorLastFirst": "Autor (Apellido, Nombre)",
"LabelAuthors": "Autores", "LabelAuthors": "Autores",
"LabelAutoDownloadEpisodes": "Descargar episodios automáticamente", "LabelAutoDownloadEpisodes": "AutoDescargar episodios",
"LabelAutoRewindTime": "Tiempo de rebobinado automático", "LabelAutoRewindTime": "Tiempo de autorebobinado",
"LabelAutoSleepTimer": "Temporizador de apagado automático", "LabelAutoSleepTimer": "Cronómetro de autoapagado",
"LabelAutoSleepTimerAutoRewind": "Temporizador de apagado automático con rebobinado automático", "LabelAutoSleepTimerAutoRewind": "Cronómetro de autoapagado con autorebobinado",
"LabelAutoSleepTimerAutoRewindHelp": "Cuando el temporizador de auto apagado finaliza, reproducir el elemento nuevamente rebobinará automáticamente tu posición.", "LabelAutoSleepTimerAutoRewindHelp": "Cuando el cronómetro de autoapagado finaliza, reproducir el elemento nuevamente autorebobinará tu posición.",
"LabelAutoSleepTimerHelp": "Cuando se reproduce contenido multimedia entre las horas de inicio y finalización especificadas, se activará automáticamente un temporizador de apagado.", "LabelAutoSleepTimerHelp": "Cuando se reproduce contenido multimedia entre las horas de inicio y finalización especificadas, se activará automáticamente un temporizador de apagado.",
"LabelBooks": "Libros", "LabelBooks": "Libros",
"LabelByAuthor": "por {0}",
"LabelChapterTrack": "Seguimiento de Capítulo", "LabelChapterTrack": "Seguimiento de Capítulo",
"LabelChapters": "Capítulos", "LabelChapters": "Capítulos",
"LabelClosePlayer": "Cerrar reproductor", "LabelClosePlayer": "Cerrar reproductor",
"LabelCollapseSeries": "Colapsar serie", "LabelCollapseSeries": "Colapsar Series",
"LabelComplete": "Completo", "LabelComplete": "Completo",
"LabelContinueBooks": "Continuar libros", "LabelContinueBooks": "Continuar libros",
"LabelContinueEpisodes": "Continuar episodios", "LabelContinueEpisodes": "Continuar Episodios",
"LabelContinueListening": "Seguir escuchando", "LabelContinueListening": "Seguir Escuchando",
"LabelContinueReading": "Continuar leyendo", "LabelContinueReading": "Continuar leyendo",
"LabelContinueSeries": "Continuar series", "LabelContinueSeries": "Continuar series",
"LabelCustomTime": "Tiempo personalizado", "LabelCustomTime": "Tiempo personalizado",
"LabelDescription": "Descripción", "LabelDescription": "Descripción",
"LabelDisableAudioFadeOut": "Desactivar el fundido de audio", "LabelDisableAudioFadeOut": "Desactivar el fundido de audio",
"LabelDisableAudioFadeOutHelp": "El volumen de audio comenzará a disminuir cuando quede menos de 1 minuto en el temporizador de apagado. Habilite este ajuste para que no se desvanezca.", "LabelDisableAudioFadeOutHelp": "El volumen de audio comenzará a disminuir cuando quede menos de 1 minuto en el temporizador de dormida. Habilite este ajuste para que no se desvanezca.",
"LabelDisableAutoRewind": "Desactivar rebobinado automático", "LabelDisableAutoRewind": "Desactivar autorebobinado",
"LabelDisableShakeToReset": "Desactivar agitar para reiniciar", "LabelDisableShakeToReset": "Desactivar agitar para restablecer",
"LabelDisableShakeToResetHelp": "Si agitas el dispositivo mientras el temporizador está en marcha o en los 2 minutos siguientes a la expiración del temporizador, éste se reiniciará. Habilita esta configuración para desactivar el restablecimiento al agitar.", "LabelDisableShakeToResetHelp": "Si agitas el dispositivo mientras el temporizador está en marcha o en los 2 minutos siguientes a la caducidad del temporizador, éste se reiniciará. Habilita estos ajustes para desactivar el restablecimiento al agitar.",
"LabelDisableVibrateOnReset": "Desactivar vibración al reiniciar", "LabelDisableVibrateOnReset": "Desactivar vibración al reiniciar",
"LabelDisableVibrateOnResetHelp": "Cuando el temporizador de apagado se reinicia, el dispositivo vibra. Activa esta opción para que no vibre cuando se reinicie el temporizador.", "LabelDisableVibrateOnResetHelp": "Cuando el temporizador de apagado se reinicia, el dispositivo vibra. Activa esta opción para que no vibre cuando se duerme el temporizador.",
"LabelDiscover": "Descubrir", "LabelDiscover": "Descubrir",
"LabelDownload": "Descargar", "LabelDownload": "Descargar",
"LabelDownloadUsingCellular": "Descargar usando el móvil", "LabelDownloadUsingCellular": "Descargar usando el móvil",
"LabelDownloaded": "Descargado", "LabelDownloaded": "Descargado",
"LabelDuration": "Duración", "LabelDuration": "Duración",
"LabelEbook": "Libro electrónico", "LabelEbook": "Libro-e",
"LabelEbooks": "Libros electrónicos", "LabelEbooks": "Libros-e",
"LabelEnable": "Activar", "LabelEnable": "Activar",
"LabelEnableMp3IndexSeeking": "Activar la búsqueda de índices mp3", "LabelEnableMp3IndexSeeking": "Activar la búsqueda de índices mp3",
"LabelEnableMp3IndexSeekingHelp": "Esta configuración solo debe activarse si tiene archivos MP3 en los que no se puede buscar correctamente. La búsqueda inexacta probablemente se deba a archivos MP3 con tasas de bits variables (VBR). Esta configuración forzará la búsqueda de índice, en la que se construye una asignación de tiempo a bytes mientras se lee el archivo. En algunos casos, con archivos MP3 grandes, puede haber un retraso al buscar hacia el final del archivo.", "LabelEnableMp3IndexSeekingHelp": "Estos ajustes solo estaría activado si tiene archivos mp3 en los que no se puede buscar correctamente. La búsqueda inexacta probablemente se deba a archivos mp3 con tasas de los bit variables (VBR). Este ajuste forzará la búsqueda de índice, en la que se construye una asignación de tiempo a bytes mientras se lee el archivo. En algunos casos, con archivos mp3 grandes, puede haber un retraso al buscar hacia el final del archivo.",
"LabelEnd": "Fin", "LabelEnd": "Fin",
"LabelEndOfChapter": "Fin del capítulo", "LabelEndOfChapter": "Fin del capítulo",
"LabelEndTime": "Hora de finalización", "LabelEndTime": "Tiempo de finalización",
"LabelEpisode": "Episodio", "LabelEpisode": "Episodio",
"LabelExplicit": "Explícito", "LabelExplicit": "Explícito",
"LabelFeedURL": "URL del suministro", "LabelFeedURL": "URL del suministro",
@@ -152,35 +155,41 @@
"LabelFileBirthtime": "Archivo creado en", "LabelFileBirthtime": "Archivo creado en",
"LabelFileModified": "Archivo modificado", "LabelFileModified": "Archivo modificado",
"LabelFilename": "Nombre del archivo", "LabelFilename": "Nombre del archivo",
"LabelFinished": "Terminado", "LabelFinished": "Finalizado",
"LabelFolder": "Carpeta", "LabelFolder": "Carpeta",
"LabelFontBoldness": "Peso tipográfico", "LabelFontBoldness": "Tipográfico sin Negrita",
"LabelFontFamily": "Familia tipográfica",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Escala de letra", "LabelFontScale": "Escala de letra",
"LabelGenre": "Género", "LabelGenre": "Género",
"LabelGenres": "Géneros", "LabelGenres": "Géneros",
"LabelHapticFeedback": "Respuesta háptica", "LabelHapticFeedback": "Comentario háptico",
"LabelHasEbook": "Tiene un libro", "LabelHasEbook": "Tiene libro-e",
"LabelHasSupplementaryEbook": "Tiene un libro complementario", "LabelHasSupplementaryEbook": "Tiene un libro-e suplementario",
"LabelHeavy": "Pesado", "LabelHeavy": "Denso",
"LabelHigh": "Alto", "LabelHigh": "Alto",
"LabelHost": "Anfitrión", "LabelHost": "Anfitrión",
"LabelInProgress": "En proceso", "LabelInProgress": "En proceso",
"LabelIncomplete": "Incompleto", "LabelIncomplete": "Incompleto",
"LabelInternalAppStorage": "Almacenamiento interno de aplicaciones", "LabelInternalAppStorage": "Almacenamiento Interno de App",
"LabelJumpBackwardsTime": "Saltar atrás en el tiempo", "LabelJumpBackwardsTime": "Volver en el tiempo",
"LabelJumpForwardsTime": "Salto adelante en el tiempo", "LabelJumpForwardsTime": "Avanzar en el tiempo",
"LabelKeepScreenAwake": "Mantener la pantalla encendida", "LabelKeepScreenAwake": "Mantener la pantalla encendida",
"LabelLanguage": "Idioma", "LabelLanguage": "Idioma",
"LabelLayout": "Disposición", "LabelLayout": "Disposición",
"LabelLayoutAuto": "Automático", "LabelLayoutAuto": "Automático",
"LabelLayoutSinglePage": "Página única", "LabelLayoutSinglePage": "Página única",
"LabelLibrarySortByProgress": "Progreso: Último actualizado",
"LabelLibrarySortByProgressFinished": "Progreso: Finalizado",
"LabelLibrarySortByProgressStarted": "Progreso: Iniciado",
"LabelLight": "Claro", "LabelLight": "Claro",
"LabelLineSpacing": "Interlineado", "LabelLineSpacing": "Interlineado",
"LabelListenAgain": "Volver a escuchar", "LabelListenAgain": "Volver a escuchar",
"LabelLocalBooks": "Libros Locales", "LabelLocalBooks": "Libros Locales",
"LabelLocalPodcasts": "Pódcast locales", "LabelLocalPodcasts": "Pódcast locales",
"LabelLockOrientation": "Bloquear orientación", "LabelLockOrientation": "Bloquear orientación",
"LabelLockPlayer": "Bloquear el reproductor", "LabelLockPlayer": "Bloquear reproductor",
"LabelLow": "Bajo", "LabelLow": "Bajo",
"LabelMediaType": "Tipo de multimedia", "LabelMediaType": "Tipo de multimedia",
"LabelMedium": "Medio", "LabelMedium": "Medio",
@@ -191,7 +200,7 @@
"LabelNarrator": "Narrador", "LabelNarrator": "Narrador",
"LabelNarrators": "Narradores", "LabelNarrators": "Narradores",
"LabelNavigateWithVolume": "Navegar con las teclas de volumen", "LabelNavigateWithVolume": "Navegar con las teclas de volumen",
"LabelNavigateWithVolumeMirrored": "Espejo", "LabelNavigateWithVolumeMirrored": "Espejado",
"LabelNavigateWithVolumeWhilePlaying": "También puede utilizar los botones de volumen para desplazarse durante la reproducción", "LabelNavigateWithVolumeWhilePlaying": "También puede utilizar los botones de volumen para desplazarse durante la reproducción",
"LabelNavigateWithVolumeWhilePlayingDisabled": "Apagado", "LabelNavigateWithVolumeWhilePlayingDisabled": "Apagado",
"LabelNavigateWithVolumeWhilePlayingEnabled": "Encender", "LabelNavigateWithVolumeWhilePlayingEnabled": "Encender",
@@ -199,11 +208,11 @@
"LabelNewestAuthors": "Autores más nuevos", "LabelNewestAuthors": "Autores más nuevos",
"LabelNewestEpisodes": "Episodios más nuevos", "LabelNewestEpisodes": "Episodios más nuevos",
"LabelNo": "No", "LabelNo": "No",
"LabelNotFinished": "No terminado", "LabelNotFinished": "No finalizado",
"LabelNotStarted": "Sin iniciar", "LabelNotStarted": "Sin iniciar",
"LabelNumEpisodes": "{0} episodios", "LabelNumEpisodes": "{0} episodios",
"LabelNumEpisodesIncomplete": "{0} episodios, {1} incompletos", "LabelNumEpisodesIncomplete": "{0} episodios, {1} incompletos",
"LabelNumberOfEpisodes": "N.º de episodios", "LabelNumberOfEpisodes": "Nº de episodios",
"LabelOff": "Apagado", "LabelOff": "Apagado",
"LabelOn": "Encendido", "LabelOn": "Encendido",
"LabelPassword": "Contraseña", "LabelPassword": "Contraseña",
@@ -214,15 +223,16 @@
"LabelPlaybackTranscode": "Transcodificar", "LabelPlaybackTranscode": "Transcodificar",
"LabelPodcast": "Pódcast", "LabelPodcast": "Pódcast",
"LabelPodcasts": "Pódcast", "LabelPodcasts": "Pódcast",
"LabelPreventIndexing": "Evite que los directorios de pódcast de iTunes y Google indicen su suministro", "LabelPreventIndexing": "Evite que los directorios de pódcast de iTunes y Google indexen su suministro",
"LabelProgress": "Progreso", "LabelProgress": "Progreso",
"LabelPubDate": "Fecha de publicación", "LabelPubDate": "Fecha de publicación",
"LabelPublishYear": "Año de publicación", "LabelPublishYear": "Año de publicación",
"LabelPublishedDate": "Publicado {0}", "LabelPublishedDate": "Publicado {0}",
"LabelRSSFeedCustomOwnerEmail": "Correo electrónico de dueño personalizado", "LabelRSSFeedCustomOwnerEmail": "Correo-e de propietario personalizado",
"LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado", "LabelRSSFeedCustomOwnerName": "Nombre de propietario personalizado",
"LabelRSSFeedOpen": "Fuente RSS Abierta",
"LabelRSSFeedPreventIndexing": "Evitar indización", "LabelRSSFeedPreventIndexing": "Evitar indización",
"LabelRSSFeedSlug": "«Slug» de suministro RSS", "LabelRSSFeedSlug": "Ficha de suministro RSS",
"LabelRandomly": "Aleatorio", "LabelRandomly": "Aleatorio",
"LabelRead": "Leído", "LabelRead": "Leído",
"LabelReadAgain": "Volver a leer", "LabelReadAgain": "Volver a leer",
@@ -232,16 +242,17 @@
"LabelScaleElapsedTimeBySpeed": "Escala el tiempo transcurrido según la velocidad", "LabelScaleElapsedTimeBySpeed": "Escala el tiempo transcurrido según la velocidad",
"LabelSeason": "Temporada", "LabelSeason": "Temporada",
"LabelSelectADevice": "Seleccione un dispositivo", "LabelSelectADevice": "Seleccione un dispositivo",
"LabelSelectMediaType": "Seleccionar tipo de medio",
"LabelSequenceAscending": "Secuencia ascendente", "LabelSequenceAscending": "Secuencia ascendente",
"LabelSequenceDescending": "Secuencia descendente", "LabelSequenceDescending": "Secuencia descendente",
"LabelSeries": "Serie", "LabelSeries": "Series",
"LabelServerAddress": "Dirección del servidor", "LabelServerAddress": "Dirección del servidor",
"LabelSetEbookAsPrimary": "Establecer como primario", "LabelSetEbookAsPrimary": "Establecer como primario",
"LabelSetEbookAsSupplementary": "Establecer como suplementario", "LabelSetEbookAsSupplementary": "Establecer como suplementario",
"LabelShakeSensitivity": "Sensibilidad a la sacudida", "LabelShakeSensitivity": "Sensibilidad a la sacudida",
"LabelShowAll": "Mostrar todo", "LabelShowAll": "Mostrar todo",
"LabelSize": "Tamaño", "LabelSize": "Tamaño",
"LabelSleepTimer": "Temporizador de apagado", "LabelSleepTimer": "Temporizador de dormida",
"LabelSleepTimerAlmostDoneChime": "Timbrar casi al terminar", "LabelSleepTimerAlmostDoneChime": "Timbrar casi al terminar",
"LabelSleepTimerAlmostDoneChimeHelp": "Reproducir una señal acústica cuando queden 30 segundos en el temporizador", "LabelSleepTimerAlmostDoneChimeHelp": "Reproducir una señal acústica cuando queden 30 segundos en el temporizador",
"LabelStart": "Iniciar", "LabelStart": "Iniciar",
@@ -282,14 +293,19 @@
"MessageAndroid10Downloads": "Android 10 e inferiores utilizarán el almacenamiento interno de aplicaciones para las descargas.", "MessageAndroid10Downloads": "Android 10 e inferiores utilizarán el almacenamiento interno de aplicaciones para las descargas.",
"MessageAttemptingServerConnection": "Intentando conectar con el servidor...", "MessageAttemptingServerConnection": "Intentando conectar con el servidor...",
"MessageAudiobookshelfServerNotConnected": "Servidor de Audiobookshelf no conectado", "MessageAudiobookshelfServerNotConnected": "Servidor de Audiobookshelf no conectado",
"MessageAudiobookshelfServerRequired": "<strong>¡Importante!</strong> Esta aplicación está diseñada para trabajar con un servidor Audiobookshelf que usted o alguien que usted conoce es el anfitrión. Esta aplicación no proporciona ningún contenido.", "MessageAudiobookshelfServerRequired": "<strong>¡Importante!</strong> Esta aplicación está diseñada para trabajar con un servidor Audiobookshelf que o alguien que conozcas es el host. Esta aplicación no proporciona ningún contenido.",
"MessageBookshelfEmpty": "Estantería vacía", "MessageBookshelfEmpty": "Estantería vacía",
"MessageConfirmAppExit": "¿Desea salir de la aplicación?",
"MessageConfirmDeleteEpisodeDownloadQueue": "¿Seguro que desea vaciar la cola de descarga del episodio?",
"MessageConfirmDeleteLocalEpisode": "¿Quiere eliminar el episodio local «{0}» del dispositivo? El archivo en el servidor no se verá afectado.", "MessageConfirmDeleteLocalEpisode": "¿Quiere eliminar el episodio local «{0}» del dispositivo? El archivo en el servidor no se verá afectado.",
"MessageConfirmDeleteLocalFiles": "¿Quiere quitar los archivos locales de este elemento del dispositivo? Los archivos del servidor y su progreso no se verán afectados.", "MessageConfirmDeleteLocalFiles": "¿Quiere quitar los archivos locales de este elemento del dispositivo? Los archivos del servidor y su progreso no se verán afectados.",
"MessageConfirmDeleteServerConfig": "¿Quito esta configuración del servidor?",
"MessageConfirmDeleteServerEpisode": "¿Seguro que desea eliminar el episodio «{0}» desde el servidor?\nAdvertencia: esto eliminará el archivo de sonido.",
"MessageConfirmDisableAutoTimer": "¿Confirma que quiere desactivar el temporizador automático por el resto del día? El temporizador volverá a activarse al final de este periodo de temporizador de apagado automático, o si reinicia la aplicación.", "MessageConfirmDisableAutoTimer": "¿Confirma que quiere desactivar el temporizador automático por el resto del día? El temporizador volverá a activarse al final de este periodo de temporizador de apagado automático, o si reinicia la aplicación.",
"MessageConfirmDiscardProgress": "¿Confirma que quiere restablecer su progreso?", "MessageConfirmDiscardProgress": "¿Confirma que quiere restablecer su progreso?",
"MessageConfirmDownloadUsingCellular": "Está a punto de efectuar una descarga con datos móviles. Esto puede conllevar tarifas de datos de la operadora. ¿Quiere continuar?", "MessageConfirmDownloadUsingCellular": "Está a punto de efectuar una descarga con datos móviles. Esto puede conllevar tarifas de datos de la operadora. ¿Quiere continuar?",
"MessageConfirmMarkAsFinished": "¿Confirma que quiere marcar este elemento como terminado?", "MessageConfirmMarkAsFinished": "¿Confirma que quiere marcar este elemento como terminado?",
"MessageConfirmPlaybackTime": "¿Inicio la reproducción para «{0}» en {1}?",
"MessageConfirmRemoveBookmark": "¿Confirma que quiere quitar el marcador?", "MessageConfirmRemoveBookmark": "¿Confirma que quiere quitar el marcador?",
"MessageConfirmStreamingUsingCellular": "Está a punto de hacer una transmisión con datos móviles. Esto puede conllevar tarifas de datos de la operadora. ¿Quiere continuar?", "MessageConfirmStreamingUsingCellular": "Está a punto de hacer una transmisión con datos móviles. Esto puede conllevar tarifas de datos de la operadora. ¿Quiere continuar?",
"MessageDiscardProgress": "Descartar progreso", "MessageDiscardProgress": "Descartar progreso",
@@ -297,6 +313,7 @@
"MessageDownloading": "Descargando...", "MessageDownloading": "Descargando...",
"MessageDownloadingEpisode": "Descargando episodio", "MessageDownloadingEpisode": "Descargando episodio",
"MessageEpisodesQueuedForDownload": "{0} episodio(s) en cola para descargar", "MessageEpisodesQueuedForDownload": "{0} episodio(s) en cola para descargar",
"MessageFailedToRefreshToken": "No se pudo actualizar el token, se requiere reiniciar la sesión",
"MessageFeedURLWillBe": "El URL del suministro será {0}", "MessageFeedURLWillBe": "El URL del suministro será {0}",
"MessageFetching": "Recuperando...", "MessageFetching": "Recuperando...",
"MessageFollowTheProjectOnGithub": "Siga el proyecto en GitHub", "MessageFollowTheProjectOnGithub": "Siga el proyecto en GitHub",
@@ -324,6 +341,9 @@
"MessageNoSeries": "Ninguna serie", "MessageNoSeries": "Ninguna serie",
"MessageNoUpdatesWereNecessary": "No fue necesario actualizar", "MessageNoUpdatesWereNecessary": "No fue necesario actualizar",
"MessageNoUserPlaylists": "No tiene ninguna lista de reproducción", "MessageNoUserPlaylists": "No tiene ninguna lista de reproducción",
"MessageOldServerAuthReLoginRequired": "La autenticación se ha mejorado para seguridad en la versión v2.26.0 del servidor. Todos los usuarios deberán reiniciar sesión.",
"MessageOldServerAuthWarning": "El servidor está utilizando un método de autenticación anticuado",
"MessageOldServerAuthWarningHelp": "Este servidor está ejecutando una versión más antigua que la v2.26.0. Ha sido añadido un sistema de autenticación más seguro en v2.26.0. Se recomienda encarecidamente que actualice el servidor a la última versión. Si ya tiene el servidor actualizado, acceda de nuevo para utilizar la autenticación nueva.",
"MessageOldServerConnectionWarning": "La configuración de la conexión al servidor utiliza un identificador de usuario antiguo. Elimine y vuelva a añadir esta conexión al servidor.", "MessageOldServerConnectionWarning": "La configuración de la conexión al servidor utiliza un identificador de usuario antiguo. Elimine y vuelva a añadir esta conexión al servidor.",
"MessageOldServerConnectionWarningHelp": "Usted configuró originalmente la conexión a este servidor antes de la migración de la base de datos en la versión 2.3.0, publicada en junio de 2023. Una futura actualización del servidor eliminará la posibilidad de iniciar sesión con esta conexión antigua. Por favor, elimine la conexión existente al servidor y conéctese de nuevo (utilizando la misma dirección del servidor y las mismas credenciales). Si tiene algún medio descargado en este dispositivo, será necesario descargarlo de nuevo para sincronizarlo con el servidor.", "MessageOldServerConnectionWarningHelp": "Usted configuró originalmente la conexión a este servidor antes de la migración de la base de datos en la versión 2.3.0, publicada en junio de 2023. Una futura actualización del servidor eliminará la posibilidad de iniciar sesión con esta conexión antigua. Por favor, elimine la conexión existente al servidor y conéctese de nuevo (utilizando la misma dirección del servidor y las mismas credenciales). Si tiene algún medio descargado en este dispositivo, será necesario descargarlo de nuevo para sincronizarlo con el servidor.",
"MessagePodcastSearchField": "Introduzca el término de búsqueda o el URL del suministro RSS", "MessagePodcastSearchField": "Introduzca el término de búsqueda o el URL del suministro RSS",
@@ -350,5 +370,7 @@
"ToastPodcastCreateSuccess": "Se creó el pódcast correctamente", "ToastPodcastCreateSuccess": "Se creó el pódcast correctamente",
"ToastRSSFeedCloseFailed": "Error al cerrar el suministro RSS", "ToastRSSFeedCloseFailed": "Error al cerrar el suministro RSS",
"ToastRSSFeedCloseSuccess": "Suministro RSS cerrado", "ToastRSSFeedCloseSuccess": "Suministro RSS cerrado",
"ToastStreamingNotAllowedOnCellular": "El streaming no está permitido con datos móviles" "ToastStreamingNotAllowedOnCellular": "El streaming no está permitido con datos móviles",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+18 -1
View File
@@ -63,6 +63,7 @@
"HeaderChapters": "Luvut", "HeaderChapters": "Luvut",
"HeaderCollection": "Kokoelma", "HeaderCollection": "Kokoelma",
"HeaderCollectionItems": "Kokoelman osat", "HeaderCollectionItems": "Kokoelman osat",
"HeaderConfirm": "Vahvista",
"HeaderConnectionStatus": "Kokoelman tila", "HeaderConnectionStatus": "Kokoelman tila",
"HeaderDataSettings": "Tietojen asetukset", "HeaderDataSettings": "Tietojen asetukset",
"HeaderDetails": "Yksityiskohdat", "HeaderDetails": "Yksityiskohdat",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Viimeaikaiset istunnot", "HeaderStatsRecentSessions": "Viimeaikaiset istunnot",
"HeaderTableOfContents": "Sisällysluettelo", "HeaderTableOfContents": "Sisällysluettelo",
"HeaderUserInterfaceSettings": "Käyttöliittymän asetukset", "HeaderUserInterfaceSettings": "Käyttöliittymän asetukset",
"HeaderWelcome": "Tervetuloa, <strong>{0</strong>",
"HeaderYourStats": "Tilastosi", "HeaderYourStats": "Tilastosi",
"LabelAddToPlaylist": "Lisää soittolistaan", "LabelAddToPlaylist": "Lisää soittolistaan",
"LabelAddedAt": "Lisätty listalle", "LabelAddedAt": "Lisätty listalle",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Kun automaattinen uniajastin päättyy, ensi kerralla toistetaan osa kuunenltua uudelleen.", "LabelAutoSleepTimerAutoRewindHelp": "Kun automaattinen uniajastin päättyy, ensi kerralla toistetaan osa kuunenltua uudelleen.",
"LabelAutoSleepTimerHelp": "Toiston alkaessa tällä aikävälillä unikytkin kytketään automaattisesti käyttöön.", "LabelAutoSleepTimerHelp": "Toiston alkaessa tällä aikävälillä unikytkin kytketään automaattisesti käyttöön.",
"LabelBooks": "Kirjat", "LabelBooks": "Kirjat",
"LabelByAuthor": "Tekijältä: {0}",
"LabelChapterTrack": "Luvun raita", "LabelChapterTrack": "Luvun raita",
"LabelChapters": "Luvut", "LabelChapters": "Luvut",
"LabelClosePlayer": "Sulje soitin", "LabelClosePlayer": "Sulje soitin",
@@ -155,6 +158,9 @@
"LabelFinished": "Valmis", "LabelFinished": "Valmis",
"LabelFolder": "Kansio", "LabelFolder": "Kansio",
"LabelFontBoldness": "Kirjasintyyppien lihavointi", "LabelFontBoldness": "Kirjasintyyppien lihavointi",
"LabelFontFamily": "Fonttiperhe",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Kirjasintyyppien skaalautuminen", "LabelFontScale": "Kirjasintyyppien skaalautuminen",
"LabelGenre": "Lajityyppi", "LabelGenre": "Lajityyppi",
"LabelGenres": "Lajityypit", "LabelGenres": "Lajityypit",
@@ -174,6 +180,9 @@
"LabelLayout": "Asettelu", "LabelLayout": "Asettelu",
"LabelLayoutAuto": "Automaattinen", "LabelLayoutAuto": "Automaattinen",
"LabelLayoutSinglePage": "Yksi sivu", "LabelLayoutSinglePage": "Yksi sivu",
"LabelLibrarySortByProgress": "Edistyminen: Viimeksi päivitetty",
"LabelLibrarySortByProgressFinished": "Edistyminen: Valmis",
"LabelLibrarySortByProgressStarted": "Edistyminen: Aloitettu",
"LabelLight": "Vaalea", "LabelLight": "Vaalea",
"LabelLineSpacing": "Riviväli", "LabelLineSpacing": "Riviväli",
"LabelListenAgain": "Kuuntele uudelleen", "LabelListenAgain": "Kuuntele uudelleen",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Skaalaa kulunut aika nopeuden mukaan", "LabelScaleElapsedTimeBySpeed": "Skaalaa kulunut aika nopeuden mukaan",
"LabelSeason": "Kausi", "LabelSeason": "Kausi",
"LabelSelectADevice": "Valitse laite", "LabelSelectADevice": "Valitse laite",
"LabelSelectMediaType": "Valitse mediatyyppi",
"LabelSequenceAscending": "Nouseva sekvenssi", "LabelSequenceAscending": "Nouseva sekvenssi",
"LabelSequenceDescending": "Laskeva sekvenssi", "LabelSequenceDescending": "Laskeva sekvenssi",
"LabelSeries": "Sarja", "LabelSeries": "Sarja",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf palvelinta ei ole yhdistetty", "MessageAudiobookshelfServerNotConnected": "Audiobookshelf palvelinta ei ole yhdistetty",
"MessageAudiobookshelfServerRequired": "<strong>Tärkeää!</strong> Tämä sovellus on suunniteltu toimimaan sinun tai tuttavasi Audiobookshelf-palvelimen kanssa. Tämä sovellus ei tarjoa sisältöä.", "MessageAudiobookshelfServerRequired": "<strong>Tärkeää!</strong> Tämä sovellus on suunniteltu toimimaan sinun tai tuttavasi Audiobookshelf-palvelimen kanssa. Tämä sovellus ei tarjoa sisältöä.",
"MessageBookshelfEmpty": "Kirjahylly on tyhjä", "MessageBookshelfEmpty": "Kirjahylly on tyhjä",
"MessageConfirmAppExit": "Haluatko poistua sovelluksesta?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Haluatko varmasti tyhjentää jaksojen latausjonon?",
"MessageConfirmDeleteLocalEpisode": "Poista jakso \"{0}\" laitteeltasi? Tämä ei vaikuta palvelimella olevaan tiedostoon.", "MessageConfirmDeleteLocalEpisode": "Poista jakso \"{0}\" laitteeltasi? Tämä ei vaikuta palvelimella olevaan tiedostoon.",
"MessageConfirmDeleteLocalFiles": "Poista tämän kohteen tiedostot laitteestasi? Tämä ei vaikuta palvelimella oleviin tiedostoihin tai edistymiseesi.", "MessageConfirmDeleteLocalFiles": "Poista tämän kohteen tiedostot laitteestasi? Tämä ei vaikuta palvelimella oleviin tiedostoihin tai edistymiseesi.",
"MessageConfirmDeleteServerConfig": "Poistetaanko palvelimen konfiguraatio?",
"MessageConfirmDeleteServerEpisode": "Haluatko varmasti poistaa jakson \"{0}\" palvelimelta?\nVaroitus: Tämä poistaa äänitiedoston.",
"MessageConfirmDisableAutoTimer": "Oletko varma, että haluat poistaa automaattisen ajastimen käytöstä loppupäivän ajaksi? Ajastin otetaan uudelleen käyttöön tämän automaattisen uniajastimen jakson lopussa tai jos käynnistät sovelluksen uudelleen.", "MessageConfirmDisableAutoTimer": "Oletko varma, että haluat poistaa automaattisen ajastimen käytöstä loppupäivän ajaksi? Ajastin otetaan uudelleen käyttöön tämän automaattisen uniajastimen jakson lopussa tai jos käynnistät sovelluksen uudelleen.",
"MessageConfirmDiscardProgress": "Haluatko varmasti nollata edistymisesi?", "MessageConfirmDiscardProgress": "Haluatko varmasti nollata edistymisesi?",
"MessageConfirmDownloadUsingCellular": "Olet lataamassa mobiilidatan avulla. Tämä voi aiheuttaa operaattorin datamaksuja. Haluatko jatkaa?", "MessageConfirmDownloadUsingCellular": "Olet lataamassa mobiilidatan avulla. Tämä voi aiheuttaa operaattorin datamaksuja. Haluatko jatkaa?",
"MessageConfirmMarkAsFinished": "Haluatko varmasti merkitä tämän kohteen valmiiksi?", "MessageConfirmMarkAsFinished": "Haluatko varmasti merkitä tämän kohteen valmiiksi?",
"MessageConfirmPlaybackTime": "Toistetaanko \"{0}\" kohdasta {1}?",
"MessageConfirmRemoveBookmark": "Haluatko varmasti poistaa kirjanmerkin?", "MessageConfirmRemoveBookmark": "Haluatko varmasti poistaa kirjanmerkin?",
"MessageConfirmStreamingUsingCellular": "Olet suoratoistamassa matkapuhelindatan avulla. Tämä voi aiheuttaa operaattorin datamaksuja. Haluatko jatkaa?", "MessageConfirmStreamingUsingCellular": "Olet suoratoistamassa matkapuhelindatan avulla. Tämä voi aiheuttaa operaattorin datamaksuja. Haluatko jatkaa?",
"MessageDiscardProgress": "Hylkää edistyminen", "MessageDiscardProgress": "Hylkää edistyminen",
@@ -355,5 +370,7 @@
"ToastPodcastCreateSuccess": "Podcastin luominen onnistui", "ToastPodcastCreateSuccess": "Podcastin luominen onnistui",
"ToastRSSFeedCloseFailed": "RSS syötteen sulkeminen epäonnistui", "ToastRSSFeedCloseFailed": "RSS syötteen sulkeminen epäonnistui",
"ToastRSSFeedCloseSuccess": "RSS syöte suljettu", "ToastRSSFeedCloseSuccess": "RSS syöte suljettu",
"ToastStreamingNotAllowedOnCellular": "Suoratoisto ei ole sallittu mobiilidatalla" "ToastStreamingNotAllowedOnCellular": "Suoratoisto ei ole sallittu mobiilidatalla",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+20 -3
View File
@@ -63,6 +63,7 @@
"HeaderChapters": "Chapitres", "HeaderChapters": "Chapitres",
"HeaderCollection": "Collection", "HeaderCollection": "Collection",
"HeaderCollectionItems": "Entrées de la collection", "HeaderCollectionItems": "Entrées de la collection",
"HeaderConfirm": "Confirmer",
"HeaderConnectionStatus": "Status de connexion", "HeaderConnectionStatus": "Status de connexion",
"HeaderDataSettings": "Paramètres de données", "HeaderDataSettings": "Paramètres de données",
"HeaderDetails": "Détails", "HeaderDetails": "Détails",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Sessions récentes", "HeaderStatsRecentSessions": "Sessions récentes",
"HeaderTableOfContents": "Table des matières", "HeaderTableOfContents": "Table des matières",
"HeaderUserInterfaceSettings": "Paramètres de l'interface", "HeaderUserInterfaceSettings": "Paramètres de l'interface",
"HeaderWelcome": "Bienvenue, <strong>{0}</strong>",
"HeaderYourStats": "Vos statistiques", "HeaderYourStats": "Vos statistiques",
"LabelAddToPlaylist": "Ajouter à la liste de lecture", "LabelAddToPlaylist": "Ajouter à la liste de lecture",
"LabelAddedAt": "Date dajout", "LabelAddedAt": "Date dajout",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Lorsque le minuteur nocturne se termine, relire l'élément fera un saut en arrière.", "LabelAutoSleepTimerAutoRewindHelp": "Lorsque le minuteur nocturne se termine, relire l'élément fera un saut en arrière.",
"LabelAutoSleepTimerHelp": "Lorsqu'un éléments est lu entre l'heure de début et de fin, un minuteur nocturne se lance automatiquement.", "LabelAutoSleepTimerHelp": "Lorsqu'un éléments est lu entre l'heure de début et de fin, un minuteur nocturne se lance automatiquement.",
"LabelBooks": "Livres", "LabelBooks": "Livres",
"LabelByAuthor": "de {0}",
"LabelChapterTrack": "Piste des chapitres", "LabelChapterTrack": "Piste des chapitres",
"LabelChapters": "Chapitres", "LabelChapters": "Chapitres",
"LabelClosePlayer": "Fermer le lecteur", "LabelClosePlayer": "Fermer le lecteur",
@@ -155,7 +158,10 @@
"LabelFinished": "Fini(e)", "LabelFinished": "Fini(e)",
"LabelFolder": "Dossier", "LabelFolder": "Dossier",
"LabelFontBoldness": "Graisse de la police", "LabelFontBoldness": "Graisse de la police",
"LabelFontScale": "Taille de la police de caractère", "LabelFontFamily": "Famille de caractères",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Avec empattements",
"LabelFontScale": "Taille de la police",
"LabelGenre": "Genre", "LabelGenre": "Genre",
"LabelGenres": "Genres", "LabelGenres": "Genres",
"LabelHapticFeedback": "Retour haptique", "LabelHapticFeedback": "Retour haptique",
@@ -174,7 +180,10 @@
"LabelLayout": "Mise en page", "LabelLayout": "Mise en page",
"LabelLayoutAuto": "Automatique", "LabelLayoutAuto": "Automatique",
"LabelLayoutSinglePage": "Page seule", "LabelLayoutSinglePage": "Page seule",
"LabelLight": "Léger", "LabelLibrarySortByProgress": "Progression : Mise à jour",
"LabelLibrarySortByProgressFinished": "Progression : Terminé",
"LabelLibrarySortByProgressStarted": "Progression : En cours",
"LabelLight": "Clair",
"LabelLineSpacing": "Interligne", "LabelLineSpacing": "Interligne",
"LabelListenAgain": "Écouter à nouveau", "LabelListenAgain": "Écouter à nouveau",
"LabelLocalBooks": "Livres locaux", "LabelLocalBooks": "Livres locaux",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Traduire le temps restant en fonction de la vitesse de lecture", "LabelScaleElapsedTimeBySpeed": "Traduire le temps restant en fonction de la vitesse de lecture",
"LabelSeason": "Saison", "LabelSeason": "Saison",
"LabelSelectADevice": "Sélectionner un Appareil", "LabelSelectADevice": "Sélectionner un Appareil",
"LabelSelectMediaType": "Sélectionnez le type de media",
"LabelSequenceAscending": "Séquence ascendante", "LabelSequenceAscending": "Séquence ascendante",
"LabelSequenceDescending": "Séquence descendante", "LabelSequenceDescending": "Séquence descendante",
"LabelSeries": "Séries", "LabelSeries": "Séries",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Serveur Audiobookshelf non connecté", "MessageAudiobookshelfServerNotConnected": "Serveur Audiobookshelf non connecté",
"MessageAudiobookshelfServerRequired": "<strong>Important!</strong> Cette application est faite pour fonctionner avec un serveur Audiobookshelf. L'application ne fournit aucun contenu.", "MessageAudiobookshelfServerRequired": "<strong>Important!</strong> Cette application est faite pour fonctionner avec un serveur Audiobookshelf. L'application ne fournit aucun contenu.",
"MessageBookshelfEmpty": "Bibliothèque vide", "MessageBookshelfEmpty": "Bibliothèque vide",
"MessageConfirmAppExit": "Voulez-vous quitter l'application?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Voulez-vous vraiment vider la file d'attente de téléchargement des épisodes?",
"MessageConfirmDeleteLocalEpisode": "Retirer l’épisode local « {0} » de votre appareil? Le fichier sur le serveur ne sera pas affecté.", "MessageConfirmDeleteLocalEpisode": "Retirer l’épisode local « {0} » de votre appareil? Le fichier sur le serveur ne sera pas affecté.",
"MessageConfirmDeleteLocalFiles": "Supprimer les fichiers locaux de cet élément de votre appareil ? Les fichiers sur le serveur ainsi que votre progression ne serons pas affectés.", "MessageConfirmDeleteLocalFiles": "Supprimer les fichiers locaux de cet élément de votre appareil ? Les fichiers sur le serveur ainsi que votre progression ne serons pas affectés.",
"MessageConfirmDeleteServerConfig": "Supprimer la configuration de ce serveur?",
"MessageConfirmDeleteServerEpisode": "Voulez-vous vraiment supprimer l'épisode \"{0}\" du serveur?\nAttention : le fichier audio sera supprimé.",
"MessageConfirmDisableAutoTimer": "Êtes-vous sûr·e de vouloir désactiver le minuteur automatique pour le reste de la journée? Le minuteur sera réactivé à la fin de cette période de décompte automatique, ou si vous redémarrez l'application.", "MessageConfirmDisableAutoTimer": "Êtes-vous sûr·e de vouloir désactiver le minuteur automatique pour le reste de la journée? Le minuteur sera réactivé à la fin de cette période de décompte automatique, ou si vous redémarrez l'application.",
"MessageConfirmDiscardProgress": "Êtes-vous sûr·e de vouloir supprimer votre progression?", "MessageConfirmDiscardProgress": "Êtes-vous sûr·e de vouloir supprimer votre progression?",
"MessageConfirmDownloadUsingCellular": "Vous êtes sur le point deffectuer un téléchargement en utilisant des données mobiles.Il se peut que des frais de données soient facturés par votre opérateur. Souhaitez-vous continuer?", "MessageConfirmDownloadUsingCellular": "Vous êtes sur le point deffectuer un téléchargement en utilisant des données mobiles.Il se peut que des frais de données soient facturés par votre opérateur. Souhaitez-vous continuer?",
"MessageConfirmMarkAsFinished": "Êtes-vous sûr·e de vouloir marquer cette élement comme terminé ?", "MessageConfirmMarkAsFinished": "Êtes-vous sûr·e de vouloir marquer cette élement comme terminé ?",
"MessageConfirmPlaybackTime": "Lancer la lecture de \"{0}\" à {1}?",
"MessageConfirmRemoveBookmark": "Êtes-vous sûr·e de vouloir supprimer le marque-page?", "MessageConfirmRemoveBookmark": "Êtes-vous sûr·e de vouloir supprimer le marque-page?",
"MessageConfirmStreamingUsingCellular": "Vous êtes sur le point de diffuser du contenu en utilisant des données mobiles. Il se peut que des frais de données soient facturés par votre opérateur. Souhaitez-vous continuer?", "MessageConfirmStreamingUsingCellular": "Vous êtes sur le point de diffuser du contenu en utilisant des données mobiles. Il se peut que des frais de données soient facturés par votre opérateur. Souhaitez-vous continuer?",
"MessageDiscardProgress": "Supprimer la progression", "MessageDiscardProgress": "Supprimer la progression",
@@ -355,5 +370,7 @@
"ToastPodcastCreateSuccess": "Podcast créé avec succès", "ToastPodcastCreateSuccess": "Podcast créé avec succès",
"ToastRSSFeedCloseFailed": "Échec de la fermeture du flux RSS", "ToastRSSFeedCloseFailed": "Échec de la fermeture du flux RSS",
"ToastRSSFeedCloseSuccess": "Flux RSS fermé", "ToastRSSFeedCloseSuccess": "Flux RSS fermé",
"ToastStreamingNotAllowedOnCellular": "La diffusion sur les données mobile nest pas autorisée" "ToastStreamingNotAllowedOnCellular": "La diffusion sur les données mobile nest pas autorisée",
"UnitMinutesShort": "{0}min",
"UnitSecondsShort": "{0}sec"
} }
+3
View File
@@ -174,6 +174,9 @@
"LabelLayout": "Layout", "LabelLayout": "Layout",
"LabelLayoutAuto": "אוטומטי", "LabelLayoutAuto": "אוטומטי",
"LabelLayoutSinglePage": "עמוד יחיד", "LabelLayoutSinglePage": "עמוד יחיד",
"LabelLibrarySortByProgress": "התקדמות: עודכן לאחרונה",
"LabelLibrarySortByProgressFinished": "התקדמות: הושלם",
"LabelLibrarySortByProgressStarted": "התקדמות: הותחל",
"LabelLight": "בהיר", "LabelLight": "בהיר",
"LabelLineSpacing": "מרווח שורה", "LabelLineSpacing": "מרווח שורה",
"LabelListenAgain": "האזן שוב", "LabelListenAgain": "האזן שוב",
+25 -1
View File
@@ -1,6 +1,7 @@
{ {
"ButtonAdd": "Dodaj", "ButtonAdd": "Dodaj",
"ButtonAddNewServer": "Dodaj novi poslužitelj", "ButtonAddNewServer": "Dodaj novi poslužitelj",
"ButtonAllow": "Dopusti",
"ButtonAuthors": "Autori", "ButtonAuthors": "Autori",
"ButtonBack": "Natrag", "ButtonBack": "Natrag",
"ButtonCancel": "Odustani", "ButtonCancel": "Odustani",
@@ -63,6 +64,7 @@
"HeaderChapters": "Poglavlja", "HeaderChapters": "Poglavlja",
"HeaderCollection": "Zbirka", "HeaderCollection": "Zbirka",
"HeaderCollectionItems": "Stavke u zbirci", "HeaderCollectionItems": "Stavke u zbirci",
"HeaderConfirm": "Potvrdi",
"HeaderConnectionStatus": "Stanje veze", "HeaderConnectionStatus": "Stanje veze",
"HeaderDataSettings": "Podatkovne postavke", "HeaderDataSettings": "Podatkovne postavke",
"HeaderDetails": "Pojedinosti", "HeaderDetails": "Pojedinosti",
@@ -91,6 +93,7 @@
"HeaderStatsRecentSessions": "Nedavne sesije", "HeaderStatsRecentSessions": "Nedavne sesije",
"HeaderTableOfContents": "Sadržaj", "HeaderTableOfContents": "Sadržaj",
"HeaderUserInterfaceSettings": "Postavke korisničkog sučelja", "HeaderUserInterfaceSettings": "Postavke korisničkog sučelja",
"HeaderWelcome": "Dobrodošao, <strong>{0}</strong>",
"HeaderYourStats": "Vaša statistika", "HeaderYourStats": "Vaša statistika",
"LabelAddToPlaylist": "Dodaj na popis za izvođenje", "LabelAddToPlaylist": "Dodaj na popis za izvođenje",
"LabelAddedAt": "Dodano", "LabelAddedAt": "Dodano",
@@ -113,6 +116,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Kada automatsk timer za spavanje završi, ponovna reprodukcija stavke će se automatski vratiti na zadani položaj.", "LabelAutoSleepTimerAutoRewindHelp": "Kada automatsk timer za spavanje završi, ponovna reprodukcija stavke će se automatski vratiti na zadani položaj.",
"LabelAutoSleepTimerHelp": "Automatski timer za spavanje uključit će se u zadano vrijeme prilikom izvođenja medija.", "LabelAutoSleepTimerHelp": "Automatski timer za spavanje uključit će se u zadano vrijeme prilikom izvođenja medija.",
"LabelBooks": "Knjige", "LabelBooks": "Knjige",
"LabelByAuthor": "autor: {0}",
"LabelChapterTrack": "Zapis poglavlja", "LabelChapterTrack": "Zapis poglavlja",
"LabelChapters": "Poglavlja", "LabelChapters": "Poglavlja",
"LabelClosePlayer": "Zatvori reproduktor", "LabelClosePlayer": "Zatvori reproduktor",
@@ -155,6 +159,9 @@
"LabelFinished": "Završeno", "LabelFinished": "Završeno",
"LabelFolder": "Mapa", "LabelFolder": "Mapa",
"LabelFontBoldness": "Debljina slova", "LabelFontBoldness": "Debljina slova",
"LabelFontFamily": "Skup pisma",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Veličina slova", "LabelFontScale": "Veličina slova",
"LabelGenre": "Žanr", "LabelGenre": "Žanr",
"LabelGenres": "Žanrovi", "LabelGenres": "Žanrovi",
@@ -174,6 +181,9 @@
"LabelLayout": "Prikaz", "LabelLayout": "Prikaz",
"LabelLayoutAuto": "Automatski", "LabelLayoutAuto": "Automatski",
"LabelLayoutSinglePage": "Jedna stranica", "LabelLayoutSinglePage": "Jedna stranica",
"LabelLibrarySortByProgress": "Napredak: Zadnje ažuriranje",
"LabelLibrarySortByProgressFinished": "Napredak: Završeno",
"LabelLibrarySortByProgressStarted": "Napredak: Započeto",
"LabelLight": "Lagano", "LabelLight": "Lagano",
"LabelLineSpacing": "Razmak između redaka", "LabelLineSpacing": "Razmak između redaka",
"LabelListenAgain": "Ponovno poslušaj", "LabelListenAgain": "Ponovno poslušaj",
@@ -233,6 +243,7 @@
"LabelScaleElapsedTimeBySpeed": "Prilagodi preostalo vrijeme brzini izvođenja", "LabelScaleElapsedTimeBySpeed": "Prilagodi preostalo vrijeme brzini izvođenja",
"LabelSeason": "Sezona", "LabelSeason": "Sezona",
"LabelSelectADevice": "Odabir uređaja", "LabelSelectADevice": "Odabir uređaja",
"LabelSelectMediaType": "Odaberi vrstu medija",
"LabelSequenceAscending": "Redoslijed uzlazno", "LabelSequenceAscending": "Redoslijed uzlazno",
"LabelSequenceDescending": "Redoslijed silazno", "LabelSequenceDescending": "Redoslijed silazno",
"LabelSeries": "Serijal", "LabelSeries": "Serijal",
@@ -285,18 +296,24 @@
"MessageAudiobookshelfServerNotConnected": "Poslužitelj Audiobookshelf nije povezan", "MessageAudiobookshelfServerNotConnected": "Poslužitelj Audiobookshelf nije povezan",
"MessageAudiobookshelfServerRequired": "<strong>Važno!</strong> Ova je aplikacija predviđena za rad s poslužiteljem Audiobookshelf na vašem poslužitelju kojim upravljate vi ili netko koga znate. Ova aplikacija ne nudi nikakav sadržaj.", "MessageAudiobookshelfServerRequired": "<strong>Važno!</strong> Ova je aplikacija predviđena za rad s poslužiteljem Audiobookshelf na vašem poslužitelju kojim upravljate vi ili netko koga znate. Ova aplikacija ne nudi nikakav sadržaj.",
"MessageBookshelfEmpty": "Polica s knjigama je prazna", "MessageBookshelfEmpty": "Polica s knjigama je prazna",
"MessageConfirmAppExit": "Želiš izaći iz aplikacije?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Sigurno želiš izbrisati red čekanja za preuzimanje epizode?",
"MessageConfirmDeleteLocalEpisode": "Želite li ukloniti preuzeti nastavak \"{0}\" sa svog uređaja? Ovo ne utječe na datoteku na poslužitelju.", "MessageConfirmDeleteLocalEpisode": "Želite li ukloniti preuzeti nastavak \"{0}\" sa svog uređaja? Ovo ne utječe na datoteku na poslužitelju.",
"MessageConfirmDeleteLocalFiles": "Želite li ukloniti preuzete datoteke ove stavke sa svog uređaja? Ovo ne utječe na datoteke na poslužitelju niti na vaš napredak.", "MessageConfirmDeleteLocalFiles": "Želite li ukloniti preuzete datoteke ove stavke sa svog uređaja? Ovo ne utječe na datoteke na poslužitelju niti na vaš napredak.",
"MessageConfirmDeleteServerConfig": "Ukloniti ovu konfiguraciju poslužitelja?",
"MessageConfirmDeleteServerEpisode": "Sigurno želiš izbrisati epizodu \"{0}\" sa servera?\nUpozorenje: Ovim će se izbrisati audio datoteka.",
"MessageConfirmDisableAutoTimer": "Sigurno želite onemogućiti automatski timer do kraja dana? Timer će se ponovno aktivirati po isteku trenutnog automatskog timera ili ako ponovno pokrenete aplikaciju.", "MessageConfirmDisableAutoTimer": "Sigurno želite onemogućiti automatski timer do kraja dana? Timer će se ponovno aktivirati po isteku trenutnog automatskog timera ili ako ponovno pokrenete aplikaciju.",
"MessageConfirmDiscardProgress": "Sigurno želite resetirati napredak?", "MessageConfirmDiscardProgress": "Sigurno želite resetirati napredak?",
"MessageConfirmDownloadUsingCellular": "Spremate se pokrenuti preuzimanje koristeći mobilne podatke. Ovo može uzrokovati dodatne troškove. Želite li nastaviti?", "MessageConfirmDownloadUsingCellular": "Spremate se pokrenuti preuzimanje koristeći mobilne podatke. Ovo može uzrokovati dodatne troškove. Želite li nastaviti?",
"MessageConfirmMarkAsFinished": "Sigurno želite označiti ovu stavku kao završenu?", "MessageConfirmMarkAsFinished": "Sigurno želite označiti ovu stavku kao završenu?",
"MessageConfirmPlaybackTime": "Započeti reprodukciju za \"{0}\" u {1}?",
"MessageConfirmRemoveBookmark": "Sigurno želite ukloniti knjižnu oznaku?", "MessageConfirmRemoveBookmark": "Sigurno želite ukloniti knjižnu oznaku?",
"MessageConfirmStreamingUsingCellular": "Pokrenut ćete strujanje korištenjem mobilnih podataka. Ovo može uzrokovati dodatne troškove. Želite li nastaviti?", "MessageConfirmStreamingUsingCellular": "Pokrenut ćete strujanje korištenjem mobilnih podataka. Ovo može uzrokovati dodatne troškove. Želite li nastaviti?",
"MessageDiscardProgress": "Zanemari napredak", "MessageDiscardProgress": "Zanemari napredak",
"MessageDownloadCompleteProcessing": "Preuzimanje dovršeno. Obrađujem...", "MessageDownloadCompleteProcessing": "Preuzimanje dovršeno. Obrađujem...",
"MessageDownloading": "Preuzimanje...", "MessageDownloading": "Preuzimanje...",
"MessageDownloadingEpisode": "Preuzimam nastavak", "MessageDownloadingEpisode": "Preuzimam nastavak",
"MessageDownloadingFile": "Preuzimanje {0}",
"MessageEpisodesQueuedForDownload": "{0} nastavak(a) u redu za preuzimanje", "MessageEpisodesQueuedForDownload": "{0} nastavak(a) u redu za preuzimanje",
"MessageFailedToRefreshToken": "Osvježavanje tokena nije uspjelo, potrebna je ponovna prijava", "MessageFailedToRefreshToken": "Osvježavanje tokena nije uspjelo, potrebna je ponovna prijava",
"MessageFeedURLWillBe": "URL izvora bit će {0}", "MessageFeedURLWillBe": "URL izvora bit će {0}",
@@ -332,6 +349,7 @@
"MessageOldServerConnectionWarning": "Veza s poslužiteljem koristi se starim ID-jem korisnika. Molimo izbrišite i ponovno dodajte ovu vezu s poslužiteljem.", "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.", "MessageOldServerConnectionWarningHelp": "Vezu s ovim poslužiteljem postavili ste prije migracije baze podataka u inačici 2.3.0 objavljenoj u lipnju 2023. Nadolazeće ažuriranje poslužitelja onemogućit će prijavu korištenjem ove stare veze. Molimo izbrišite postojeću vezu s poslužiteljem i ponovno se povežite (korištenjem iste adrese poslužitelja i korisničkih podataka). Ukoliko imate preuzetih medijskih datoteka na ovom uređaju, morat ćete ih iznova preuzeti da bi se sinkronizirali s poslužiteljem.",
"MessagePodcastSearchField": "Upišite izraz za pretraživanje ili URL RSS izvora", "MessagePodcastSearchField": "Upišite izraz za pretraživanje ili URL RSS izvora",
"MessagePreparingDownloads": "Pripremanje preuzimanja",
"MessageProgressSyncFailed": "Zadnji pokušaj slanja napretka slušanja poslužitelju nije uspio. Nastavit ćemo s pokušajima slanja napretka svakih 15 do 60 sekundi dok god traje izvođenje medijske datoteke.", "MessageProgressSyncFailed": "Zadnji pokušaj slanja napretka slušanja poslužitelju nije uspio. Nastavit ćemo s pokušajima slanja napretka svakih 15 do 60 sekundi dok god traje izvođenje medijske datoteke.",
"MessageReportBugsAndContribute": "Prijavite pogreške, zatražite funkcije i doprinesite na", "MessageReportBugsAndContribute": "Prijavite pogreške, zatražite funkcije i doprinesite na",
"MessageSeriesAlreadyDownloaded": "Već ste preuzeli sve knjige ovog serijala.", "MessageSeriesAlreadyDownloaded": "Već ste preuzeli sve knjige ovog serijala.",
@@ -342,6 +360,10 @@
"MessageSocketConnectedOverUnmeteredCellular": "Socket je povezan preko mobilne mreže kod koje se ne prati potrošnja", "MessageSocketConnectedOverUnmeteredCellular": "Socket je povezan preko mobilne mreže kod koje se ne prati potrošnja",
"MessageSocketConnectedOverUnmeteredWifi": "Socket je povezan preko Wi-Fi mreže na kojoj se ne prati potrošnja", "MessageSocketConnectedOverUnmeteredWifi": "Socket je povezan preko Wi-Fi mreže na kojoj se ne prati potrošnja",
"MessageSocketNotConnected": "Socket nije povezan", "MessageSocketNotConnected": "Socket nije povezan",
"MessageStorageAccessDenied": "Pristup zabranjen",
"MessageStoragePermissionDenied": "Dozvola odbijena",
"MessageStorageWriteAccessRequired": "Nemate pristup za pisanje u ovu mapu. Želite li odobriti pristup?",
"MessageWaitingForAvailableStorage": "Čekanje dostupne pohrane",
"NoteRSSFeedPodcastAppsHttps": "Pripazite: Većina aplikacija za podcaste iziskuje URL RSS izvora koji se koristi HTTPS protokolom", "NoteRSSFeedPodcastAppsHttps": "Pripazite: Većina aplikacija za podcaste iziskuje URL RSS izvora koji se koristi HTTPS protokolom",
"NoteRSSFeedPodcastAppsPubDate": "Upozorenje: jedan ili više vaših nastavaka nemaju datum objavljivanja. To je obavezno kod nekih aplikacija za podcaste.", "NoteRSSFeedPodcastAppsPubDate": "Upozorenje: jedan ili više vaših nastavaka nemaju datum objavljivanja. To je obavezno kod nekih aplikacija za podcaste.",
"ToastBookmarkCreateFailed": "Izrada knjižne oznake nije uspjela", "ToastBookmarkCreateFailed": "Izrada knjižne oznake nije uspjela",
@@ -355,5 +377,7 @@
"ToastPodcastCreateSuccess": "Podcast uspješno izrađen", "ToastPodcastCreateSuccess": "Podcast uspješno izrađen",
"ToastRSSFeedCloseFailed": "Zatvaranje RSS izvora nije uspjelo", "ToastRSSFeedCloseFailed": "Zatvaranje RSS izvora nije uspjelo",
"ToastRSSFeedCloseSuccess": "RSS izvor zatvoren", "ToastRSSFeedCloseSuccess": "RSS izvor zatvoren",
"ToastStreamingNotAllowedOnCellular": "Strujanje korištenjem mobilnih podataka onemogućeno je" "ToastStreamingNotAllowedOnCellular": "Strujanje korištenjem mobilnih podataka onemogućeno je",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+25 -8
View File
@@ -63,6 +63,7 @@
"HeaderChapters": "Fejezetek", "HeaderChapters": "Fejezetek",
"HeaderCollection": "Gyűjtemény", "HeaderCollection": "Gyűjtemény",
"HeaderCollectionItems": "Gyűjtemény elemek", "HeaderCollectionItems": "Gyűjtemény elemek",
"HeaderConfirm": "Megerősítés",
"HeaderConnectionStatus": "Kapcsolat állapota", "HeaderConnectionStatus": "Kapcsolat állapota",
"HeaderDataSettings": "Adat beállítások", "HeaderDataSettings": "Adat beállítások",
"HeaderDetails": "Részletek", "HeaderDetails": "Részletek",
@@ -87,10 +88,11 @@
"HeaderSettings": "Beállítások", "HeaderSettings": "Beállítások",
"HeaderSleepTimer": "Alvásidőzítő", "HeaderSleepTimer": "Alvásidőzítő",
"HeaderSleepTimerSettings": "Alvásidőzítő beállításai", "HeaderSleepTimerSettings": "Alvásidőzítő beállításai",
"HeaderStatsMinutesListeningChart": "Hallgatási grafikon percekben (az elmúlt 7 napból)", "HeaderStatsMinutesListeningChart": "Hallgatási grafikon percben (az elmúlt 7 napból)",
"HeaderStatsRecentSessions": "Legutóbbi munkamenetek", "HeaderStatsRecentSessions": "Legutóbbi munkamenetek",
"HeaderTableOfContents": "Tartalomjegyzék", "HeaderTableOfContents": "Tartalomjegyzék",
"HeaderUserInterfaceSettings": "Felhasználói felület beállításai", "HeaderUserInterfaceSettings": "Felhasználói felület beállításai",
"HeaderWelcome": "Üdv, <strong>{0}</strong>",
"HeaderYourStats": "Saját statisztikák", "HeaderYourStats": "Saját statisztikák",
"LabelAddToPlaylist": "Hozzáadás a lejátszási listához", "LabelAddToPlaylist": "Hozzáadás a lejátszási listához",
"LabelAddedAt": "Hozzáadva ekkor", "LabelAddedAt": "Hozzáadva ekkor",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Amikor az automatikus alvásidőzítő befejeződik, az elem újrajátszásakor automatikusan visszatekeri a pozíciót.", "LabelAutoSleepTimerAutoRewindHelp": "Amikor az automatikus alvásidőzítő befejeződik, az elem újrajátszásakor automatikusan visszatekeri a pozíciót.",
"LabelAutoSleepTimerHelp": "Amikor a megadott kezdési és befejezési idők között média lejátszása történik, egy alvásidőzítő automatikusan elindul.", "LabelAutoSleepTimerHelp": "Amikor a megadott kezdési és befejezési idők között média lejátszása történik, egy alvásidőzítő automatikusan elindul.",
"LabelBooks": "Könyv", "LabelBooks": "Könyv",
"LabelByAuthor": "{0} által",
"LabelChapterTrack": "Fejezet sáv", "LabelChapterTrack": "Fejezet sáv",
"LabelChapters": "Fejezetek", "LabelChapters": "Fejezetek",
"LabelClosePlayer": "Lejátszó bezárása", "LabelClosePlayer": "Lejátszó bezárása",
@@ -126,7 +129,7 @@
"LabelCustomTime": "Egyéni idő", "LabelCustomTime": "Egyéni idő",
"LabelDescription": "Leírás", "LabelDescription": "Leírás",
"LabelDisableAudioFadeOut": "Hang fokozatos csendesítés letiltása", "LabelDisableAudioFadeOut": "Hang fokozatos csendesítés letiltása",
"LabelDisableAudioFadeOutHelp": "Az audio hangerő csökkenése kezdődik, amikor kevesebb, mint 1 perc marad az alvásidőzítőből. Engedélyezze ezt a beállítást, hogy ne csendesedjen el fokozatosan a hang.", "LabelDisableAudioFadeOutHelp": "A hangerő csökkenni kezd az alvásidőzítő utolsó percében. Kapcsold be ezt a beállítást a halkítás kikapcsolásához.",
"LabelDisableAutoRewind": "Automatikus visszatekerés letiltása", "LabelDisableAutoRewind": "Automatikus visszatekerés letiltása",
"LabelDisableShakeToReset": "Rázás a visszaállításhoz letiltása", "LabelDisableShakeToReset": "Rázás a visszaállításhoz letiltása",
"LabelDisableShakeToResetHelp": "Ha az időzítő futása alatt vagy az időzítő lejárta előtti 2 percben megrázza az eszközt, az alvásidőzítő visszaáll. Engedélyezze ezt a beállítást a rázásra történő visszaállítás letiltásához.", "LabelDisableShakeToResetHelp": "Ha az időzítő futása alatt vagy az időzítő lejárta előtti 2 percben megrázza az eszközt, az alvásidőzítő visszaáll. Engedélyezze ezt a beállítást a rázásra történő visszaállítás letiltásához.",
@@ -155,6 +158,9 @@
"LabelFinished": "Befejezve", "LabelFinished": "Befejezve",
"LabelFolder": "Mappa", "LabelFolder": "Mappa",
"LabelFontBoldness": "Betű vastagság", "LabelFontBoldness": "Betű vastagság",
"LabelFontFamily": "Betűcsalád",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Betűméret skála", "LabelFontScale": "Betűméret skála",
"LabelGenre": "Műfaj", "LabelGenre": "Műfaj",
"LabelGenres": "Műfajok", "LabelGenres": "Műfajok",
@@ -174,6 +180,9 @@
"LabelLayout": "Elrendezés", "LabelLayout": "Elrendezés",
"LabelLayoutAuto": "Automatikus", "LabelLayoutAuto": "Automatikus",
"LabelLayoutSinglePage": "Egyoldalas", "LabelLayoutSinglePage": "Egyoldalas",
"LabelLibrarySortByProgress": "Folyamat: Legutóbbi frissítés",
"LabelLibrarySortByProgressFinished": "Folyamat: Befejezve",
"LabelLibrarySortByProgressStarted": "Folyamat: Elindult",
"LabelLight": "Könnyű", "LabelLight": "Könnyű",
"LabelLineSpacing": "Sorköz", "LabelLineSpacing": "Sorköz",
"LabelListenAgain": "Újrahallgatás", "LabelListenAgain": "Újrahallgatás",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Eltelt idő skálázása sebesség szerint", "LabelScaleElapsedTimeBySpeed": "Eltelt idő skálázása sebesség szerint",
"LabelSeason": "Évad", "LabelSeason": "Évad",
"LabelSelectADevice": "Eszköz kiválasztása", "LabelSelectADevice": "Eszköz kiválasztása",
"LabelSelectMediaType": "Médiatípus kiválasztása",
"LabelSequenceAscending": "Emelkedő sorrend", "LabelSequenceAscending": "Emelkedő sorrend",
"LabelSequenceDescending": "Csökkenő sorrend", "LabelSequenceDescending": "Csökkenő sorrend",
"LabelSeries": "Sorozat", "LabelSeries": "Sorozat",
@@ -249,10 +259,10 @@
"LabelStartTime": "Kezdési idő", "LabelStartTime": "Kezdési idő",
"LabelStatsBestDay": "Legjobb nap", "LabelStatsBestDay": "Legjobb nap",
"LabelStatsDailyAverage": "Napi átlag", "LabelStatsDailyAverage": "Napi átlag",
"LabelStatsDays": "Napok", "LabelStatsDays": "Nap",
"LabelStatsDaysListened": "Hallgatással töltött napok", "LabelStatsDaysListened": "Hallgatással töltött nap",
"LabelStatsInARow": "egymás után", "LabelStatsInARow": "egymás után",
"LabelStatsItemsFinished": "Befejezett elemek", "LabelStatsItemsFinished": "Befejezett elem",
"LabelStatsMinutes": "perc", "LabelStatsMinutes": "perc",
"LabelStatsMinutesListening": "Hallgatás percekben", "LabelStatsMinutesListening": "Hallgatás percekben",
"LabelStatsWeekListening": "Heti hallgatás", "LabelStatsWeekListening": "Heti hallgatás",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Az Audiobookshelf szerver nem csatlakozik", "MessageAudiobookshelfServerNotConnected": "Az Audiobookshelf szerver nem csatlakozik",
"MessageAudiobookshelfServerRequired": "<strong>Fontos!</strong> Ez az alkalmazás egy Audiobookshelf szervert igényel, amit Ön vagy valaki, akit ismer, üzemeltet. Ez az alkalmazás nem biztosít tartalmat.", "MessageAudiobookshelfServerRequired": "<strong>Fontos!</strong> Ez az alkalmazás egy Audiobookshelf szervert igényel, amit Ön vagy valaki, akit ismer, üzemeltet. Ez az alkalmazás nem biztosít tartalmat.",
"MessageBookshelfEmpty": "A könyvespolc üres", "MessageBookshelfEmpty": "A könyvespolc üres",
"MessageConfirmAppExit": "Biztosan ki szeretne lépni az alkalmazásból?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Biztosan törölni szeretné az epizódletöltési várólistát?",
"MessageConfirmDeleteLocalEpisode": "\"{0}\" helyi epizód eltávolítása az eszközről? A szerveren lévő fájl nem érintett.", "MessageConfirmDeleteLocalEpisode": "\"{0}\" helyi epizód eltávolítása az eszközről? A szerveren lévő fájl nem érintett.",
"MessageConfirmDeleteLocalFiles": "Ezen elem helyi fájljainak eltávolítása az eszközről? A szerveren lévő fájlok és a haladás nem érintettek.", "MessageConfirmDeleteLocalFiles": "Ezen elem helyi fájljainak eltávolítása az eszközről? A szerveren lévő fájlok és a haladás nem érintettek.",
"MessageConfirmDeleteServerConfig": "Eltávolítja ezt a szerverkonfigurációt?",
"MessageConfirmDeleteServerEpisode": "Biztosan törölni szeretnéd a(z) „{0}” epizódot a szerverről?\nFigyelem: ezzel az audiófájl is törlődik.",
"MessageConfirmDisableAutoTimer": "Biztos, hogy a mai nap hátralévő részére ki akarja kapcsolni az automatikus időzítőt? Az időzítő újra aktiválódik az automatikus alvásidőzítő időszak végén, vagy ha újraindítja az alkalmazást.", "MessageConfirmDisableAutoTimer": "Biztos, hogy a mai nap hátralévő részére ki akarja kapcsolni az automatikus időzítőt? Az időzítő újra aktiválódik az automatikus alvásidőzítő időszak végén, vagy ha újraindítja az alkalmazást.",
"MessageConfirmDiscardProgress": "Biztosan alaphelyzetbe akarja állítani a haladást?", "MessageConfirmDiscardProgress": "Biztosan alaphelyzetbe akarja állítani a haladást?",
"MessageConfirmDownloadUsingCellular": "Mobiladat-hálózaton keresztül készülsz letölteni. Ez adatforgalmi díjakkal járhat. Szeretnéd folytatni?", "MessageConfirmDownloadUsingCellular": "Mobiladat-hálózaton keresztül készülsz letölteni. Ez adatforgalmi díjakkal járhat. Szeretnéd folytatni?",
"MessageConfirmMarkAsFinished": "Biztosan befejezettnek jelöli ezt az elemet?", "MessageConfirmMarkAsFinished": "Biztosan befejezettnek jelöli ezt az elemet?",
"MessageConfirmPlaybackTime": "Elindítod a(z) „{0}” lejátszását {1}-tól?",
"MessageConfirmRemoveBookmark": "Biztosan eltávolítja a könyvjelzőt?", "MessageConfirmRemoveBookmark": "Biztosan eltávolítja a könyvjelzőt?",
"MessageConfirmStreamingUsingCellular": "Mobiladat-hálózaton keresztül készülsz streamelni. Ez adatforgalmi díjakkal járhat. Szeretnéd folytatni?", "MessageConfirmStreamingUsingCellular": "Mobiladat-hálózaton keresztül készülsz streamelni. Ez adatforgalmi díjakkal járhat. Szeretnéd folytatni?",
"MessageDiscardProgress": "Haladás elvetése", "MessageDiscardProgress": "Haladás elvetése",
@@ -308,11 +323,11 @@
"MessageLoadingServerData": "Szerveradatok betöltése...", "MessageLoadingServerData": "Szerveradatok betöltése...",
"MessageLocalFolderDescription": "A „Belső alkalmazás-tároló” csak ezzel az alkalmazással érhető el. Ez az alkalmazás csak a közvetlenül az alkalmazáson keresztül letöltött médiát támogatja. A megosztott tároló mappák segítségével más alkalmazások is hozzáférhetnek az alkalmazás által letöltött médiához.", "MessageLocalFolderDescription": "A „Belső alkalmazás-tároló” csak ezzel az alkalmazással érhető el. Ez az alkalmazás csak a közvetlenül az alkalmazáson keresztül letöltött médiát támogatja. A megosztott tároló mappák segítségével más alkalmazások is hozzáférhetnek az alkalmazás által letöltött médiához.",
"MessageMarkAsFinished": "Befejezettnek jelölés", "MessageMarkAsFinished": "Befejezettnek jelölés",
"MessageMediaLinkedToADifferentServer": "A média egy másik címen található Audiobookshelf szerverhez kapcsolódik ({0}). A haladás szinkronizálva lesz, amikor csatlakozik ehhez a szervercímhez.", "MessageMediaLinkedToADifferentServer": "A média egy másik címen ({0}) lévő Audiobookshelf szerverhez van kapcsolva. A folyamatjelölés akkor lesz szinkronizálva, amikor ehhez a szervercímhez csatlakozik.",
"MessageMediaLinkedToADifferentUser": "A média ehhez a szerverhez kapcsolódik, de egy másik felhasználó töltötte le. A haladás csak a letöltést végrehajtó felhasználóhoz lesz szinkronizálva.", "MessageMediaLinkedToADifferentUser": "A média ehhez a szerverhez kapcsolódik, de egy másik felhasználó töltötte le. A haladás csak a letöltést végrehajtó felhasználóhoz lesz szinkronizálva.",
"MessageMediaLinkedToServer": "Szerverhez kapcsolt {0}", "MessageMediaLinkedToServer": "Szerverhez kapcsolt {0}",
"MessageMediaLinkedToThisServer": "A letöltött média ehhez a szerverhez kapcsolódik", "MessageMediaLinkedToThisServer": "A letöltött média ehhez a szerverhez kapcsolódik",
"MessageMediaNotLinkedToServer": "A média nem kapcsolódik egy Audiobookshelf szerverhez. Nem lesz szinkronizálva a haladás.", "MessageMediaNotLinkedToServer": "A média nincs Audiobookshelf szerverhez kapcsolva. A folyamatjelölés nem lesz szinkronizálva.",
"MessageNoBookmarks": "Nincsenek könyvjelzők", "MessageNoBookmarks": "Nincsenek könyvjelzők",
"MessageNoChapters": "Nincsenek fejezetek", "MessageNoChapters": "Nincsenek fejezetek",
"MessageNoCollections": "Nincs gyűjtemény", "MessageNoCollections": "Nincs gyűjtemény",
@@ -355,5 +370,7 @@
"ToastPodcastCreateSuccess": "A podcast sikeresen létrehozva", "ToastPodcastCreateSuccess": "A podcast sikeresen létrehozva",
"ToastRSSFeedCloseFailed": "Az RSS hírcsatorna bezárása sikertelen", "ToastRSSFeedCloseFailed": "Az RSS hírcsatorna bezárása sikertelen",
"ToastRSSFeedCloseSuccess": "Az RSS hírcsatorna sikeresen bezárva", "ToastRSSFeedCloseSuccess": "Az RSS hírcsatorna sikeresen bezárva",
"ToastStreamingNotAllowedOnCellular": "Streamelés mobilinternettel nem engedélyezett" "ToastStreamingNotAllowedOnCellular": "Streamelés mobilinternettel nem engedélyezett",
"UnitMinutesShort": "{0}p",
"UnitSecondsShort": "{0}mp"
} }
+12
View File
@@ -0,0 +1,12 @@
{
"ButtonAdd": "Bæta við",
"ButtonAddNewServer": "Bæta við server",
"ButtonAuthors": "Rithöfundar",
"ButtonBack": "Bakka",
"ButtonCancel": "Hætta við",
"ButtonCancelTimer": "Stöðva klukku",
"ButtonClearFilter": "Hreinsa síu",
"ButtonCollections": "Söfn",
"ButtonCreate": "Stofna",
"ButtonDelete": "Eyða"
}
+20 -3
View File
@@ -50,7 +50,7 @@
"ButtonSendEbookToDevice": "Invia il libro al dispositivo", "ButtonSendEbookToDevice": "Invia il libro al dispositivo",
"ButtonSeries": "Serie", "ButtonSeries": "Serie",
"ButtonSetTimer": "Imposta Timer", "ButtonSetTimer": "Imposta Timer",
"ButtonStream": "Stream", "ButtonStream": "Riproduci",
"ButtonSubmit": "Invia", "ButtonSubmit": "Invia",
"ButtonSwitchServerUser": "Cambia Server/Utente", "ButtonSwitchServerUser": "Cambia Server/Utente",
"ButtonUnmaskServerAddress": "Mostra l'indirizzo server", "ButtonUnmaskServerAddress": "Mostra l'indirizzo server",
@@ -63,6 +63,7 @@
"HeaderChapters": "Capitoli", "HeaderChapters": "Capitoli",
"HeaderCollection": "Raccolta", "HeaderCollection": "Raccolta",
"HeaderCollectionItems": "Elementi della raccolta", "HeaderCollectionItems": "Elementi della raccolta",
"HeaderConfirm": "Conferma",
"HeaderConnectionStatus": "Stato connessione", "HeaderConnectionStatus": "Stato connessione",
"HeaderDataSettings": "Impostazioni dei dati", "HeaderDataSettings": "Impostazioni dei dati",
"HeaderDetails": "Dettagli", "HeaderDetails": "Dettagli",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Sessioni Recenti", "HeaderStatsRecentSessions": "Sessioni Recenti",
"HeaderTableOfContents": "Indice", "HeaderTableOfContents": "Indice",
"HeaderUserInterfaceSettings": "Impostazioni interfaccia utente", "HeaderUserInterfaceSettings": "Impostazioni interfaccia utente",
"HeaderWelcome": "Benvenuto, <strong>{0}</strong>",
"HeaderYourStats": "Statistiche personali", "HeaderYourStats": "Statistiche personali",
"LabelAddToPlaylist": "Aggiungi alla playlist", "LabelAddToPlaylist": "Aggiungi alla playlist",
"LabelAddedAt": "Aggiunto il", "LabelAddedAt": "Aggiunto il",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Al termine del timer di spegnimento automatico, la riproduzione dell'elemento riavvolgerà automaticamente la tua posizione.", "LabelAutoSleepTimerAutoRewindHelp": "Al termine del timer di spegnimento automatico, la riproduzione dell'elemento riavvolgerà automaticamente la tua posizione.",
"LabelAutoSleepTimerHelp": "Durante la riproduzione di contenuti multimediali tra l'ora di inizio e quella di fine specificate, verrà avviato automaticamente un timer di spegnimento.", "LabelAutoSleepTimerHelp": "Durante la riproduzione di contenuti multimediali tra l'ora di inizio e quella di fine specificate, verrà avviato automaticamente un timer di spegnimento.",
"LabelBooks": "Libri", "LabelBooks": "Libri",
"LabelByAuthor": "di {0}",
"LabelChapterTrack": "Traccia capitolo", "LabelChapterTrack": "Traccia capitolo",
"LabelChapters": "Capitoli", "LabelChapters": "Capitoli",
"LabelClosePlayer": "Chiudi player", "LabelClosePlayer": "Chiudi player",
@@ -155,6 +158,9 @@
"LabelFinished": "Finita", "LabelFinished": "Finita",
"LabelFolder": "Cartella", "LabelFolder": "Cartella",
"LabelFontBoldness": "Grassetto", "LabelFontBoldness": "Grassetto",
"LabelFontFamily": "Famiglia caratteri",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Dimensione font", "LabelFontScale": "Dimensione font",
"LabelGenre": "Genere", "LabelGenre": "Genere",
"LabelGenres": "Generi", "LabelGenres": "Generi",
@@ -174,6 +180,9 @@
"LabelLayout": "Disposizione", "LabelLayout": "Disposizione",
"LabelLayoutAuto": "Automatico", "LabelLayoutAuto": "Automatico",
"LabelLayoutSinglePage": "Pagina singola", "LabelLayoutSinglePage": "Pagina singola",
"LabelLibrarySortByProgress": "Progresso: ultimo aggiornamento",
"LabelLibrarySortByProgressFinished": "Progresso: finito",
"LabelLibrarySortByProgressStarted": "Progresso: iniziato",
"LabelLight": "Leggera", "LabelLight": "Leggera",
"LabelLineSpacing": "Interlinea", "LabelLineSpacing": "Interlinea",
"LabelListenAgain": "Ascolta ancora", "LabelListenAgain": "Ascolta ancora",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Scala il tempo trascorso in base alla velocità", "LabelScaleElapsedTimeBySpeed": "Scala il tempo trascorso in base alla velocità",
"LabelSeason": "Stagione", "LabelSeason": "Stagione",
"LabelSelectADevice": "Seletiona dispositivo", "LabelSelectADevice": "Seletiona dispositivo",
"LabelSelectMediaType": "Seleziona il tipo di supporto",
"LabelSequenceAscending": "Sequenza ascendente", "LabelSequenceAscending": "Sequenza ascendente",
"LabelSequenceDescending": "Sequenza decrescente", "LabelSequenceDescending": "Sequenza decrescente",
"LabelSeries": "Serie", "LabelSeries": "Serie",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf server non connesso", "MessageAudiobookshelfServerNotConnected": "Audiobookshelf server non connesso",
"MessageAudiobookshelfServerRequired": "<strong>Importante!</strong> Questa app è progettata per funzionare con un server Audiobookshelf ospitato da te o da qualcuno che conosci. Questa app non fornisce alcun contenuto.", "MessageAudiobookshelfServerRequired": "<strong>Importante!</strong> Questa app è progettata per funzionare con un server Audiobookshelf ospitato da te o da qualcuno che conosci. Questa app non fornisce alcun contenuto.",
"MessageBookshelfEmpty": "Scaffale vuoto", "MessageBookshelfEmpty": "Scaffale vuoto",
"MessageConfirmAppExit": "Volevi uscire dall'app?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Sei sicuro di voler cancellare la coda di download degli episodi?",
"MessageConfirmDeleteLocalEpisode": "Rimuovi episodi locali \"{0}\" dal tuo dispositivo? i file sul server non verranno toccati.", "MessageConfirmDeleteLocalEpisode": "Rimuovi episodi locali \"{0}\" dal tuo dispositivo? i file sul server non verranno toccati.",
"MessageConfirmDeleteLocalFiles": "Rimuovi i file locali dell'oggetto? I file sul server e i progressi non verranno toccati.", "MessageConfirmDeleteLocalFiles": "Rimuovi i file locali dell'oggetto? I file sul server e i progressi non verranno toccati.",
"MessageConfirmDeleteServerConfig": "Rimuovere questa configurazione del server?",
"MessageConfirmDeleteServerEpisode": "Sei sicuro di voler eliminare l'episodio \"{0}\" dal server?\nAttenzione: questa operazione eliminerà il file audio.",
"MessageConfirmDisableAutoTimer": "Vuoi davvero disattivare il timer automatico per il resto della giornata? Il timer verrà riattivato alla fine di questo periodo di spegnimento automatico o se riavvii l'app.", "MessageConfirmDisableAutoTimer": "Vuoi davvero disattivare il timer automatico per il resto della giornata? Il timer verrà riattivato alla fine di questo periodo di spegnimento automatico o se riavvii l'app.",
"MessageConfirmDiscardProgress": "Sei sicuro/sicura di voler ripristinare i tuoi progressi?", "MessageConfirmDiscardProgress": "Sei sicuro/sicura di voler ripristinare i tuoi progressi?",
"MessageConfirmDownloadUsingCellular": "Stai per eseguire il download utilizzando la rete dati cellulare. Ciò potrebbe includere addebiti per i dati dell'operatore. Vuoi continuare?", "MessageConfirmDownloadUsingCellular": "Stai per eseguire il download utilizzando la rete dati cellulare. Ciò potrebbe includere addebiti per i dati dell'operatore. Vuoi continuare?",
"MessageConfirmMarkAsFinished": "Sei sicuro/sicura di voler contrassegnare questo elemento come finito?", "MessageConfirmMarkAsFinished": "Sei sicuro/sicura di voler contrassegnare questo elemento come finito?",
"MessageConfirmPlaybackTime": "Avviare la riproduzione per \"{0}\" alle {1}?",
"MessageConfirmRemoveBookmark": "Sei sicuro/sicura di voler rimuovere il segnalibro?", "MessageConfirmRemoveBookmark": "Sei sicuro/sicura di voler rimuovere il segnalibro?",
"MessageConfirmStreamingUsingCellular": "Stai per eseguire lo streaming utilizzando la rete dati. Ciò potrebbe includere addebiti per i dati dell'operatore. Vuoi continuare?", "MessageConfirmStreamingUsingCellular": "Stai per eseguire lo streaming utilizzando la rete dati. Ciò potrebbe includere addebiti per i dati dell'operatore. Vuoi continuare?",
"MessageDiscardProgress": "Elimina i progressi", "MessageDiscardProgress": "Elimina i progressi",
@@ -319,7 +334,7 @@
"MessageNoItems": "Nessun oggetto", "MessageNoItems": "Nessun oggetto",
"MessageNoItemsFound": "Nessun oggetto trovato", "MessageNoItemsFound": "Nessun oggetto trovato",
"MessageNoListeningSessions": "Nessuna sessione di ascolto", "MessageNoListeningSessions": "Nessuna sessione di ascolto",
"MessageNoLogs": "Nessun registro", "MessageNoLogs": "Nessun rapporto",
"MessageNoMediaFolders": "Nessuna cartella media", "MessageNoMediaFolders": "Nessuna cartella media",
"MessageNoNetworkConnection": "Nessuna connessione di rete", "MessageNoNetworkConnection": "Nessuna connessione di rete",
"MessageNoPodcastsFound": "Nessun podcast trovato", "MessageNoPodcastsFound": "Nessun podcast trovato",
@@ -355,5 +370,7 @@
"ToastPodcastCreateSuccess": "Podcast creato correttamente", "ToastPodcastCreateSuccess": "Podcast creato correttamente",
"ToastRSSFeedCloseFailed": "Errore chiusura flusso RSS", "ToastRSSFeedCloseFailed": "Errore chiusura flusso RSS",
"ToastRSSFeedCloseSuccess": "Flusso RSS chiuso", "ToastRSSFeedCloseSuccess": "Flusso RSS chiuso",
"ToastStreamingNotAllowedOnCellular": "Lo streaming non è consentito sui dati mobili" "ToastStreamingNotAllowedOnCellular": "Lo streaming non è consentito sui dati mobili",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+143 -19
View File
@@ -40,15 +40,17 @@
"ButtonPlayEpisode": "エピソードを再生", "ButtonPlayEpisode": "エピソードを再生",
"ButtonPlaylists": "プレイリスト", "ButtonPlaylists": "プレイリスト",
"ButtonRead": "読む", "ButtonRead": "読む",
"ButtonReadLess": "閉じる", "ButtonReadLess": "少なく表示",
"ButtonReadMore": "もっと見る", "ButtonReadMore": "もっと見る",
"ButtonRemove": "削除", "ButtonRemove": "削除",
"ButtonRemoveFromServer": "サーバーから削除", "ButtonRemoveFromServer": "サーバーから削除",
"ButtonSave": "保存", "ButtonSave": "保存",
"ButtonSaveOrder": "並び替えを保存", "ButtonSaveOrder": "並び替えを保存",
"ButtonSearch": "検索", "ButtonSearch": "検索",
"ButtonSendEbookToDevice": "Ebookをデバイスに転送",
"ButtonSeries": "シリーズ", "ButtonSeries": "シリーズ",
"ButtonSetTimer": "タイマーを設定", "ButtonSetTimer": "タイマーを設定",
"ButtonStream": "ストリーム",
"ButtonSubmit": "送信", "ButtonSubmit": "送信",
"ButtonSwitchServerUser": "サーバー/ユーザーの切り替え", "ButtonSwitchServerUser": "サーバー/ユーザーの切り替え",
"ButtonUnmaskServerAddress": "サーバーアドレスを表示", "ButtonUnmaskServerAddress": "サーバーアドレスを表示",
@@ -61,6 +63,7 @@
"HeaderChapters": "チャプター", "HeaderChapters": "チャプター",
"HeaderCollection": "コレクション", "HeaderCollection": "コレクション",
"HeaderCollectionItems": "コレクションの項目", "HeaderCollectionItems": "コレクションの項目",
"HeaderConfirm": "確認",
"HeaderConnectionStatus": "接続状況", "HeaderConnectionStatus": "接続状況",
"HeaderDataSettings": "データ設定", "HeaderDataSettings": "データ設定",
"HeaderDetails": "詳細", "HeaderDetails": "詳細",
@@ -74,91 +77,212 @@
"HeaderLocalLibraryItems": "ローカルライブラリーの項目", "HeaderLocalLibraryItems": "ローカルライブラリーの項目",
"HeaderNewPlaylist": "新しいプレイリスト", "HeaderNewPlaylist": "新しいプレイリスト",
"HeaderOpenRSSFeed": "RSS Feedを開く", "HeaderOpenRSSFeed": "RSS Feedを開く",
"HeaderPlaybackSettings": "再生設定",
"HeaderPlaylist": "プレイリスト", "HeaderPlaylist": "プレイリスト",
"HeaderPlaylistItems": "プレイリストアイテム", "HeaderPlaylistItems": "プレイリストアイテム",
"HeaderProgressSyncFailed": "進行中の同期失敗", "HeaderProgressSyncFailed": "進行中の同期失敗",
"HeaderRSSFeed": "RSS Feed", "HeaderRSSFeed": "RSSフィード",
"HeaderRSSFeedGeneral": "RSS 詳細", "HeaderRSSFeedGeneral": "RSS 詳細",
"HeaderRSSFeedIsOpen": "RSSフィードが開いています",
"HeaderSelectDownloadLocation": "保存場所の選択", "HeaderSelectDownloadLocation": "保存場所の選択",
"HeaderSettings": "設定", "HeaderSettings": "設定",
"HeaderSleepTimer": "スリープタイマー", "HeaderSleepTimer": "スリープタイマー",
"HeaderSleepTimerSettings": "スリープタイマー設定", "HeaderSleepTimerSettings": "スリープタイマー設定",
"HeaderStatsMinutesListeningChart": "過去7日間の視聴時間(分)", "HeaderStatsMinutesListeningChart": "過去7日間のリスニング時間(分)",
"HeaderUserInterfaceSettings": "UI設定", "HeaderStatsRecentSessions": "最近の再生履歴",
"HeaderTableOfContents": "目次",
"HeaderUserInterfaceSettings": "画面設定",
"HeaderWelcome": "ようこそ <strong>{0}</strong>",
"HeaderYourStats": "再生統計",
"LabelAddToPlaylist": "プレイリストの追加", "LabelAddToPlaylist": "プレイリストの追加",
"LabelAddedAt": "追加日時",
"LabelAddedDate": "追加日時 {0}",
"LabelAll": "すべて",
"LabelAllowSeekingOnMediaControls": "メディア通知コントロールでの位置情報の取得を許可", "LabelAllowSeekingOnMediaControls": "メディア通知コントロールでの位置情報の取得を許可",
"LabelAlways": "常に",
"LabelAndroidAutoBrowseLimitForGrouping": "アルファベット順表示の制限",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "表示項目数がこの数未満の場合、アルファベット順ドローダウンを使用しません",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "シリーズ内の順序",
"LabelAskConfirmation": "確認を求める", "LabelAskConfirmation": "確認を求める",
"LabelAuthor": "者", "LabelAuthor": "者",
"LabelAuthorFirstLast": "者(名 氏)", "LabelAuthorFirstLast": "者(名 氏)",
"LabelAuthorLastFirst": "者(氏 名)", "LabelAuthorLastFirst": "者(氏 名)",
"LabelAuthors": "者", "LabelAuthors": "者",
"LabelAutoDownloadEpisodes": "エピソードの自動ダウンロード", "LabelAutoDownloadEpisodes": "エピソードの自動ダウンロード",
"LabelAutoRewindTime": "自動巻き戻し時間",
"LabelAutoSleepTimer": "自動スリープタイマー", "LabelAutoSleepTimer": "自動スリープタイマー",
"LabelBooks": "ほん", "LabelAutoSleepTimerAutoRewind": "スリープタイマー時の自動巻き戻し",
"LabelAutoSleepTimerAutoRewindHelp": "自動スリープタイマーが終了した後、再生を再開すると再生位置が自動的に巻き戻されます。",
"LabelAutoSleepTimerHelp": "指定した開始時刻から終了時刻の間にメディアを再生すると、スリープタイマーが自動的に開始されます。",
"LabelBooks": "本",
"LabelByAuthor": "by {0}",
"LabelChapterTrack": "チャプタートラック", "LabelChapterTrack": "チャプタートラック",
"LabelChapters": "チャプター", "LabelChapters": "チャプター",
"LabelClosePlayer": "プレヤーを閉じる", "LabelClosePlayer": "プレヤーを閉じる",
"LabelCollapseSeries": "シリーズを折りたたむ",
"LabelComplete": "完了", "LabelComplete": "完了",
"LabelContinueListening": "続きから聞く", "LabelContinueBooks": "続きを読む本",
"LabelContinueEpisodes": "続きのエピソード",
"LabelContinueListening": "続きから聴く",
"LabelContinueReading": "続きを読む",
"LabelContinueSeries": "シリーズを続ける",
"LabelCustomTime": "指定時間",
"LabelDescription": "説明", "LabelDescription": "説明",
"LabelDisableAudioFadeOut": "オーディオフェードアウトを無効", "LabelDisableAudioFadeOut": "オーディオフェードアウトを無効",
"LabelDisableAudioFadeOutHelp": "スリープタイマーの残り時間が1分を切ると、音量が徐々に小さくなります。フェードアウトしないようにするには、この設定を有効にしてください。", "LabelDisableAudioFadeOutHelp": "スリープタイマーの残り時間が1分を切ると、音量が徐々に小さくなります。フェードアウトしないようにするには、この設定を有効にしてください。",
"LabelDisableAutoRewind": "自動巻き戻しを無効", "LabelDisableAutoRewind": "自動巻き戻しを無効",
"LabelDisableShakeToReset": "シェイクでリセットを無効化",
"LabelDisableShakeToResetHelp": "タイマー作動中、またはタイマー終了後2分以内に端末を振ると、スリープタイマーがリセットされます。この設定を有効にすると、振ってリセットする機能を無効にできます。", "LabelDisableShakeToResetHelp": "タイマー作動中、またはタイマー終了後2分以内に端末を振ると、スリープタイマーがリセットされます。この設定を有効にすると、振ってリセットする機能を無効にできます。",
"LabelDisableVibrateOnReset": "リセット時の振動を無効化",
"LabelDisableVibrateOnResetHelp": "スリープタイマーがリセットされると、デバイスが振動します。この設定を有効にすると、スリープタイマーがリセットされた際に振動しなくなります。", "LabelDisableVibrateOnResetHelp": "スリープタイマーがリセットされると、デバイスが振動します。この設定を有効にすると、スリープタイマーがリセットされた際に振動しなくなります。",
"LabelDiscover": "おすすめ",
"LabelDownload": "ダウンロード", "LabelDownload": "ダウンロード",
"LabelDownloadUsingCellular": "モバイル通信を使用してダウンロード", "LabelDownloadUsingCellular": "モバイル通信を使用してダウンロード",
"LabelDownloaded": "ダウンロード完了", "LabelDownloaded": "ダウンロード完了",
"LabelDuration": "長さ", "LabelDuration": "長さ",
"LabelEbook": "Eブック", "LabelEbook": "電子書籍",
"LabelEbooks": "Eブック", "LabelEbooks": "電子書籍",
"LabelEnable": "有効", "LabelEnable": "有効",
"LabelEnableMp3IndexSeeking": "mp3インデックス検索を有効化", "LabelEnableMp3IndexSeeking": "mp3インデックス検索を有効化",
"LabelEnableMp3IndexSeekingHelp": "この設定は、正しくシークできないMP3ファイルがある場合にのみ有効にしてください。シークの不正確さは、可変ビットレート(VBR)のMP3ファイルが原因である可能性が高いです。この設定はインデックスシークを強制し、ファイルの読み取り時に時間-バイトマッピングを構築します。大きなMP3ファイルの場合、ファイルの終わり付近をシークする際に遅延が生じる場合があります。", "LabelEnableMp3IndexSeekingHelp": "この設定は、正しくシークできないMP3ファイルがある場合にのみ有効にしてください。シークの不正確さは、可変ビットレート(VBR)のMP3ファイルが原因である可能性が高いです。この設定はインデックスシークを強制し、ファイルの読み取り時に時間-バイトマッピングを構築します。大きなMP3ファイルの場合、ファイルの終わり付近をシークする際に遅延が生じる場合があります。",
"LabelEnd": "終了",
"LabelEndOfChapter": "チャプターの最後", "LabelEndOfChapter": "チャプターの最後",
"LabelEndTime": "終了時間", "LabelEndTime": "終了時間",
"LabelEpisode": "エピソード", "LabelEpisode": "エピソード",
"LabelFeedURL": "Feed URL", "LabelExplicit": "露骨な表現",
"LabelFeedURL": "フィードURL",
"LabelFile": "ファイル", "LabelFile": "ファイル",
"LabelFileBirthtime": "ファイル作成日時",
"LabelFileModified": "ファイル更新日時",
"LabelFilename": "ファイル名", "LabelFilename": "ファイル名",
"LabelFinished": "完了", "LabelFinished": "完了",
"LabelFolder": "フォルダ", "LabelFolder": "フォルダ",
"LabelFontBoldness": "フォントの太さ", "LabelFontBoldness": "フォントの太さ",
"LabelFontFamily": "フォントファミリー",
"LabelFontFamilySans": "サン",
"LabelFontFamilySerif": "セリフ",
"LabelFontScale": "フォントサイズ", "LabelFontScale": "フォントサイズ",
"LabelGenre": "ジャンル", "LabelGenre": "ジャンル",
"LabelGenres": "ジャンル", "LabelGenres": "ジャンル",
"LabelHapticFeedback": "ハプティックフィードバック",
"LabelHasEbook": "電子書籍あり",
"LabelHasSupplementaryEbook": "付属電子書籍あり",
"LabelHeavy": "思い",
"LabelHigh": "高い",
"LabelHost": "ホスト", "LabelHost": "ホスト",
"LabelInProgress": "進行中", "LabelInProgress": "進行中",
"LabelIncomplete": "未完了",
"LabelInternalAppStorage": "アプリ内ストレージ", "LabelInternalAppStorage": "アプリ内ストレージ",
"LabelKeepScreenAwake": "画面をスリープにしない", "LabelKeepScreenAwake": "画面をスリープにしない",
"LabelLanguage": "言語", "LabelLanguage": "言語",
"LabelLayout": "レイアウト", "LabelLayout": "レイアウト",
"LabelLayoutAuto": "自動", "LabelLayoutAuto": "自動",
"LabelLayoutSinglePage": "単ページ", "LabelLayoutSinglePage": "単ページ",
"LabelLibrarySortByProgress": "進捗: 最終更新",
"LabelLibrarySortByProgressFinished": "進捗: 完了",
"LabelLibrarySortByProgressStarted": "進捗: 開始済み",
"LabelLight": "ライト", "LabelLight": "ライト",
"LabelLineSpacing": "行間", "LabelLineSpacing": "行間",
"LabelListenAgain": "再度視聴", "LabelListenAgain": "もう一度聴く",
"LabelLocalBooks": "ローカルブック", "LabelLocalBooks": "ローカルブック",
"LabelLocalPodcasts": "ローカルポッドキャスト", "LabelLocalPodcasts": "ローカルポッドキャスト",
"LabelLockPlayer": "プレイヤーのロック", "LabelLockPlayer": "プレイヤーのロック",
"LabelMediaType": "メディアの種類", "LabelMediaType": "メディアの種類",
"LabelMissing": "消失",
"LabelMore": "多い",
"LabelMoreInfo": "追加情報", "LabelMoreInfo": "追加情報",
"LabelName": "名", "LabelName": "名",
"LabelNarrator": "ナレーター", "LabelNarrator": "ナレーター",
"LabelNarrators": "ナレーター", "LabelNarrators": "ナレーター",
"LabelNavigateWithVolume": "音量キーで操作", "LabelNavigateWithVolume": "音量キーで操作",
"LabelNavigateWithVolumeWhilePlaying": "再生中に音量キーで操作を許可", "LabelNavigateWithVolumeWhilePlaying": "再生中に音量キーで操作を許可",
"LabelNavigateWithVolumeWhilePlayingDisabled": "オフ", "LabelNavigateWithVolumeWhilePlayingDisabled": "オフ",
"LabelNavigateWithVolumeWhilePlayingEnabled": "オン", "LabelNavigateWithVolumeWhilePlayingEnabled": "オン",
"LabelNewestAuthors": "最新の者", "LabelNewestAuthors": "最新の者",
"LabelNewestEpisodes": "最新エピソード", "LabelNewestEpisodes": "最新エピソード",
"LabelNotFinished": "未完了",
"LabelNotStarted": "未開始",
"LabelNumEpisodes": "{0} エピソード", "LabelNumEpisodes": "{0} エピソード",
"LabelNumEpisodesIncomplete": "{0} エピソード, {1} 未完了", "LabelNumEpisodesIncomplete": "{0} エピソード, {1} 未完了",
"LabelNumberOfEpisodes": "エピソード数",
"LabelPassword": "パスワード", "LabelPassword": "パスワード",
"LabelPath": "パス", "LabelPath": "パス",
"LabelPlaybackLocal": "ローカル", "LabelPlaybackLocal": "ローカル",
"LabelPodcast": "ポッドキャスト", "LabelPodcast": "ポッドキャスト",
"LabelPodcasts": "ポッドキャスト", "LabelPodcasts": "ポッドキャスト",
"LabelPreventIndexing": "フィードがiTunesおよびGoogleのポッドキャストディレクトリにインデックス登録されるのを防ぎます", "LabelPreventIndexing": "フィードがiTunesおよびGoogleのポッドキャストディレクトリにインデックス登録されるのを防ぎます",
"LabelPublishYear": "公開年" "LabelProgress": "進捗",
"LabelPubDate": "公開日",
"LabelPublishYear": "公開年",
"LabelPublishedDate": "{0}発行",
"LabelRSSFeedCustomOwnerEmail": "カスタムオーナーメール",
"LabelRSSFeedCustomOwnerName": "カスタムオーナー名",
"LabelRSSFeedOpen": "RSSフィードを公開",
"LabelRSSFeedPreventIndexing": "インデックス化を防止",
"LabelRSSFeedSlug": "RSSフィードスラグ",
"LabelRandomly": "ランダム",
"LabelRead": "読む",
"LabelReadAgain": "もう一度読む",
"LabelRecentSeries": "最近のシリーズ",
"LabelRecentlyAdded": "最近追加",
"LabelSeason": "シーズン",
"LabelSeries": "シリーズ",
"LabelSetEbookAsPrimary": "メインとして設定",
"LabelSetEbookAsSupplementary": "付属として設定",
"LabelShowAll": "すべて表示",
"LabelSize": "サイズ",
"LabelSleepTimer": "スリープタイマー",
"LabelStart": "開始",
"LabelStatsBestDay": "日ごとの最長",
"LabelStatsDailyAverage": "日ごとの平均",
"LabelStatsDays": "日数",
"LabelStatsDaysListened": "聴いた日数",
"LabelStatsInARow": "連続",
"LabelStatsItemsFinished": "完了したアイテム",
"LabelStatsMinutes": "分",
"LabelStatsMinutesListening": "聴いた時間(分)",
"LabelStatsWeekListening": "週間リスニング",
"LabelTag": "タグ",
"LabelTags": "タグ",
"LabelTheme": "テーマ",
"LabelThemeDark": "ダーク",
"LabelThemeLight": "ライト",
"LabelTimeRemaining": "残り{0}",
"LabelTitle": "タイトル",
"LabelTracks": "トラック",
"LabelType": "タイプ",
"LabelUnknown": "不明",
"LabelUser": "ユーザー",
"LabelUsername": "ユーザー名",
"LabelYearReviewHide": "年間振り返りを非表示",
"LabelYearReviewShow": "年間振り返りを表示",
"LabelYourBookmarks": "ブックマーク",
"LabelYourProgress": "進捗状況",
"MessageDownloadingEpisode": "エピソードをダウンロード中",
"MessageEpisodesQueuedForDownload": "{0}件のエピソードがダウンロードキューに追加されました",
"MessageFeedURLWillBe": "フィードURLは {0} になります",
"MessageFetching": "取得中...",
"MessageLoading": "読み込み中...",
"MessageMarkAsFinished": "完了済みにする",
"MessageNoBookmarks": "ブックマークなし",
"MessageNoChapters": "チャプターなし",
"MessageNoCollections": "コレクションなし",
"MessageNoItems": "アイテムなし",
"MessageNoItemsFound": "アイテムが見つかりません",
"MessageNoListeningSessions": "リスニングセッションなし",
"MessageNoPodcastsFound": "ポッドキャストが見つかりません",
"MessageNoUpdatesWereNecessary": "更新は不要でした",
"MessageNoUserPlaylists": "プレイリストがありません",
"MessagePodcastSearchField": "検索キーワードまたはRSSフィードURLを入力",
"MessageReportBugsAndContribute": "バグ報告、機能リクエスト、コントリビューション:",
"NoteRSSFeedPodcastAppsHttps": "警告: ほとんどのポッドキャストアプリはHTTPSのRSSフィードURLが必要です",
"NoteRSSFeedPodcastAppsPubDate": "警告: 1件以上のエピソードに公開日がありません。一部のポッドキャストアプリではこれが必要です。",
"ToastBookmarkCreateFailed": "ブックマークの作成に失敗しました",
"ToastItemMarkedAsFinishedFailed": "完了としてマークするのに失敗しました",
"ToastItemMarkedAsNotFinishedFailed": "未完了としてマークするのに失敗しました",
"ToastPlaylistCreateFailed": "プレイリストの作成に失敗しました",
"ToastPodcastCreateFailed": "ポッドキャストの作成に失敗しました",
"ToastPodcastCreateSuccess": "ポッドキャストを作成しました",
"ToastRSSFeedCloseFailed": "RSSフィードのクローズに失敗しました",
"ToastRSSFeedCloseSuccess": "RSSフィードをクローズしました"
} }
+362 -1
View File
@@ -1 +1,362 @@
{} {
"ButtonAdd": "추가",
"ButtonAddNewServer": "새로운 서버 추가",
"ButtonAuthors": "작가",
"ButtonBack": "뒤로",
"ButtonCancel": "취소",
"ButtonCancelTimer": "타이머 취소",
"ButtonClearFilter": "필터 지우기",
"ButtonClearLogs": "로그 지우기",
"ButtonCloseFeed": "피드 닫기",
"ButtonCollections": "컬렉션",
"ButtonConnect": "접속",
"ButtonConnectToServer": "서버에 연결",
"ButtonCreate": "만들기",
"ButtonCreateBookmark": "북마크 만들기",
"ButtonCreateNewPlaylist": "새로운 재생목록 만들기",
"ButtonDelete": "삭제",
"ButtonDeleteLocalEpisode": "로컬 에피소드 삭제",
"ButtonDeleteLocalFile": "로컬 파일 삭제",
"ButtonDeleteLocalItem": "로컬 항목 삭제",
"ButtonDisableAutoTimer": "자동 타이머 비활성화",
"ButtonDisconnect": "연결 해제",
"ButtonGoToWebClient": "웹 클라이언트로 이동",
"ButtonHistory": "히스토리",
"ButtonHome": "홈",
"ButtonIssues": "이슈",
"ButtonLatest": "최신",
"ButtonLibrary": "라이브러리",
"ButtonLocalMedia": "로컬 미디어",
"ButtonLogs": "로그",
"ButtonManageLocalFiles": "로컬 파일 관리",
"ButtonMaskServerAddress": "마스크 서버 주소",
"ButtonNewFolder": "새 폴더",
"ButtonNextEpisode": "다음 에피소드",
"ButtonOk": "확인",
"ButtonOpenFeed": "피드 열기",
"ButtonOverride": "Override",
"ButtonPause": "일시정지",
"ButtonPlay": "재생",
"ButtonPlayEpisode": "에피소드 재생",
"ButtonPlaylists": "재생목록",
"ButtonRead": "읽기",
"ButtonReadLess": "덜 읽기",
"ButtonReadMore": "더 읽기",
"ButtonRemove": "제거",
"ButtonRemoveFromServer": "서버에서 제거",
"ButtonSave": "저장",
"ButtonSaveOrder": "저장 순서",
"ButtonSearch": "검색",
"ButtonSendEbookToDevice": "기기로 전자책 보내기",
"ButtonSeries": "시리즈",
"ButtonSetTimer": "타이머 설정",
"ButtonStream": "스트림",
"ButtonSubmit": "제출",
"ButtonSwitchServerUser": "서버/사용자 전환",
"ButtonUnmaskServerAddress": "서버 주소 마스크 해제",
"ButtonUserStats": "사용자 통계",
"ButtonYes": "예",
"HeaderAccount": "계정",
"HeaderAdvanced": "고급",
"HeaderAndroidAutoSettings": "안드로이드 오토 설정",
"HeaderAudioTracks": "오디오 트랙",
"HeaderChapters": "챕터",
"HeaderCollection": "컬렉션",
"HeaderCollectionItems": "컬렉션 항목",
"HeaderConnectionStatus": "연결 상태",
"HeaderDataSettings": "데이터 설정",
"HeaderDetails": "세부사항",
"HeaderDownloads": "다운로드",
"HeaderEbookFiles": "전자책 파일",
"HeaderEpisodes": "에피소드",
"HeaderEreaderSettings": "전자책 리더 설정",
"HeaderLatestEpisodes": "최신 에피소드",
"HeaderLibraries": "라이브러리",
"HeaderLocalFolders": "로컬 폴더",
"HeaderLocalLibraryItems": "로컬 라이브러리 항목",
"HeaderNewPlaylist": "새로운 플레이리스트",
"HeaderOpenRSSFeed": "RSS 피드 열기",
"HeaderPlaybackSettings": "재생 설정",
"HeaderPlaylist": "재생목록",
"HeaderPlaylistItems": "재생 목록 항목",
"HeaderProgressSyncFailed": "진행률 동기화 실패",
"HeaderRSSFeed": "RSS 피드",
"HeaderRSSFeedGeneral": "RSS 자세히",
"HeaderRSSFeedIsOpen": "RSS 피드가 열려 있습니다",
"HeaderSelectDownloadLocation": "다운로드 위치 선택",
"HeaderSettings": "설정",
"HeaderSleepTimer": "슬립 타이머",
"HeaderSleepTimerSettings": "슬립 타이머 설정",
"HeaderStatsMinutesListeningChart": "청취 시간(지난 7일)",
"HeaderStatsRecentSessions": "최근 세션",
"HeaderTableOfContents": "목차",
"HeaderUserInterfaceSettings": "사용자 인터페이스 설정",
"HeaderYourStats": "내 통계",
"LabelAddToPlaylist": "재생목록에 추가",
"LabelAddedAt": "추가됨",
"LabelAddedDate": "{0} 추가됨",
"LabelAll": "모두",
"LabelAllowSeekingOnMediaControls": "미디어 알림 컨트롤에서 위치 탐색 허용",
"LabelAlways": "항상",
"LabelAndroidAutoBrowseLimitForGrouping": "알파벳별 인출 한도",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "표시할 항목 수가 이 금액보다 적을 경우 알파벳 순으로 삭감하지 마십시오.",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "시리즈 책 주문",
"LabelAskConfirmation": "확인을 요청하세요",
"LabelAuthor": "작가",
"LabelAuthorFirstLast": "작가 (이름 성)",
"LabelAuthorLastFirst": "작가 (성, 이름)",
"LabelAuthors": "작가",
"LabelAutoDownloadEpisodes": "에피소드 자동 다운로드",
"LabelAutoRewindTime": "자동 되감기 시간",
"LabelAutoSleepTimer": "자동 슬립 타이머",
"LabelAutoSleepTimerAutoRewind": "자동 슬립 타이머 자동 되감기",
"LabelAutoSleepTimerAutoRewindHelp": "자동 슬립 타이머가 끝나면 해당 항목을 다시 재생하면 자동으로 위치가 되돌려집니다.",
"LabelAutoSleepTimerHelp": "지정된 시작 및 종료 시간 사이에 미디어를 재생하면 슬립 타이머가 자동으로 시작됩니다.",
"LabelBooks": "전자책",
"LabelChapterTrack": "챕터 트랙",
"LabelChapters": "챕터",
"LabelClosePlayer": "플레이어 닫기",
"LabelCollapseSeries": "시리즈 펴기",
"LabelComplete": "완료",
"LabelContinueBooks": "책 계속 읽기",
"LabelContinueEpisodes": "에피소드 계속하기",
"LabelContinueListening": "계속 듣기",
"LabelContinueReading": "계속 읽기",
"LabelContinueSeries": "시리즈 계속하기",
"LabelCustomTime": "사용자 지정 시간",
"LabelDescription": "설명",
"LabelDisableAudioFadeOut": "오디오 페이드아웃 비활성화",
"LabelDisableAudioFadeOutHelp": "슬립 타이머가 1분 미만으로 남았을 때 오디오 볼륨이 감소하기 시작합니다. 이 설정을 활성화하면 볼륨이 점점 작아지지 않습니다.",
"LabelDisableAutoRewind": "자동 되감기 비활성화",
"LabelDisableShakeToReset": "흔들어서 재설정을 해제하세요",
"LabelDisableShakeToResetHelp": "타이머가 작동하는 동안 또는 타이머가 만료되기 2분 전에 기기를 흔들면 슬립 타이머가 재설정됩니다. 이 설정을 활성화하면 흔들어서 재설정하는 기능을 비활성화할 수 있습니다.",
"LabelDisableVibrateOnReset": "재설정 시 진동 비활성화",
"LabelDisableVibrateOnResetHelp": "슬립 타이머가 재설정되면 기기가 진동합니다. 슬립 타이머가 재설정될 때 진동하지 않도록 하려면 이 설정을 활성화하세요.",
"LabelDiscover": "발견",
"LabelDownload": "다운로드",
"LabelDownloadUsingCellular": "셀룰러를 사용하여 다운로드",
"LabelDownloaded": "다운로드됨",
"LabelDuration": "기간",
"LabelEbook": "전자책",
"LabelEbooks": "전자책",
"LabelEnable": "활성화",
"LabelEnableMp3IndexSeeking": "mp3 인덱스 검색 활성화",
"LabelEnableMp3IndexSeekingHelp": "이 설정은 MP3 파일의 탐색이 제대로 이루어지지 않는 경우에만 활성화해야 합니다. 탐색이 부정확한 이유는 가변 비트레이트(VBR) MP3 파일 때문일 가능성이 높습니다. 이 설정은 파일을 읽을 때 시간-바이트 매핑이 생성되는 인덱스 탐색을 강제로 실행합니다. 대용량 MP3 파일의 경우 파일 끝부분을 탐색할 때 지연이 발생할 수 있습니다.",
"LabelEnd": "마지막",
"LabelEndOfChapter": "챕터의 마지작",
"LabelEndTime": "종료 시간",
"LabelEpisode": "에피소드",
"LabelExplicit": "명백한",
"LabelFeedURL": "피드 URL",
"LabelFile": "파일",
"LabelFileBirthtime": "파일 생년월일",
"LabelFileModified": "파일 수정됨",
"LabelFilename": "파일 이름",
"LabelFinished": "완료됨",
"LabelFolder": "폴더",
"LabelFontBoldness": "글꼴 굵기",
"LabelFontFamily": "글꼴 모양",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "글꼴 크기",
"LabelGenre": "장르",
"LabelGenres": "장르",
"LabelHapticFeedback": "햅틱 피드백",
"LabelHasEbook": "보유한 전자책",
"LabelHasSupplementaryEbook": "보유한 보충 전자책",
"LabelHeavy": "무거운",
"LabelHigh": "높게",
"LabelHost": "호스트",
"LabelInProgress": "진행 중",
"LabelIncomplete": "불완전한",
"LabelInternalAppStorage": "내부 앱 저장소",
"LabelJumpBackwardsTime": "뒤로 시간 점프",
"LabelJumpForwardsTime": "앞으로 시간 점프",
"LabelKeepScreenAwake": "화면 항상 켜기",
"LabelLanguage": "언어",
"LabelLayout": "레이아웃",
"LabelLayoutAuto": "자동",
"LabelLayoutSinglePage": "단일 페이지",
"LabelLight": "Light",
"LabelLineSpacing": "줄 간격",
"LabelListenAgain": "다시 듣기",
"LabelLocalBooks": "로컬 전자책",
"LabelLocalPodcasts": "로컬 팟케스트",
"LabelLockOrientation": "잠금 방향",
"LabelLockPlayer": "플레이어 잠금",
"LabelLow": "낮게",
"LabelMediaType": "미디어 유형",
"LabelMedium": "중간",
"LabelMissing": "누락",
"LabelMore": "더보기",
"LabelMoreInfo": "정보 더 보기",
"LabelName": "이름",
"LabelNarrator": "내레이터",
"LabelNarrators": "내레이터",
"LabelNavigateWithVolume": "볼륨 키로 탐색",
"LabelNavigateWithVolumeMirrored": "미러링됨",
"LabelNavigateWithVolumeWhilePlaying": "재생 중 볼륨 키를 사용하여 탐색할 수 있도록 허용",
"LabelNavigateWithVolumeWhilePlayingDisabled": "오프",
"LabelNavigateWithVolumeWhilePlayingEnabled": "온",
"LabelNever": "Never",
"LabelNewestAuthors": "최신 작가",
"LabelNewestEpisodes": "최신 에피소드",
"LabelNo": "아니오",
"LabelNotFinished": "완료되지 않음",
"LabelNotStarted": "시작되지 않음",
"LabelNumEpisodes": "{0}개 에피소드",
"LabelNumEpisodesIncomplete": "{0}개 에피소드, {1}개 미완료",
"LabelNumberOfEpisodes": "# 의 에피소드",
"LabelOff": "오프",
"LabelOn": "온",
"LabelPassword": "비밀번호",
"LabelPath": "경로",
"LabelPlaybackDirect": "직접",
"LabelPlaybackLocal": "로컬",
"LabelPlaybackSpeed": "재생 속도",
"LabelPlaybackTranscode": "트랜스코드",
"LabelPodcast": "팟케스트",
"LabelPodcasts": "팟케스트",
"LabelPreventIndexing": "iTunes 및 Google Podcast 디렉토리에서 피드가 인덱싱되는 것을 방지합니다.",
"LabelProgress": "진행",
"LabelPubDate": "출판 날짜",
"LabelPublishYear": "출판 연도",
"LabelPublishedDate": "출판됨 {0}",
"LabelRSSFeedCustomOwnerEmail": "사용자 정의 이메일",
"LabelRSSFeedCustomOwnerName": "사용자 정의 이름",
"LabelRSSFeedOpen": "RSS 피드 열기",
"LabelRSSFeedPreventIndexing": "인덱싱 방지",
"LabelRSSFeedSlug": "RSS 피드 슬러그",
"LabelRandomly": "무작위로",
"LabelRead": "읽기",
"LabelReadAgain": "다시 읽기",
"LabelRecentSeries": "최근 시리즈",
"LabelRecentlyAdded": "최근 추가됨",
"LabelRemoveFromPlaylist": "재생 목록에서 제거",
"LabelScaleElapsedTimeBySpeed": "속도에 따른 경과 시간 스케일링",
"LabelSeason": "시즌",
"LabelSelectADevice": "기기를 선택하세요",
"LabelSequenceAscending": "시퀀스 오름차순",
"LabelSequenceDescending": "시퀀스 내림차순",
"LabelSeries": "시리즈",
"LabelServerAddress": "서버 주소",
"LabelSetEbookAsPrimary": "기본으로 설정",
"LabelSetEbookAsSupplementary": "보충으로 설정",
"LabelShakeSensitivity": "흔들림 감도",
"LabelShowAll": "모두 표시",
"LabelSize": "크기",
"LabelSleepTimer": "슬립 타이머",
"LabelSleepTimerAlmostDoneChime": "거의 끝나면 차임벨을 울리세요",
"LabelSleepTimerAlmostDoneChimeHelp": "슬립 타이머가 30초 남았을 때 차임음을 재생합니다.",
"LabelStart": "시작",
"LabelStartTime": "시작 시간",
"LabelStatsBestDay": "최고의 날",
"LabelStatsDailyAverage": "일일 평균",
"LabelStatsDays": "일",
"LabelStatsDaysListened": "청취 일수",
"LabelStatsInARow": "in a row",
"LabelStatsItemsFinished": "완료된 항목",
"LabelStatsMinutes": "분",
"LabelStatsMinutesListening": "청취 분수",
"LabelStatsWeekListening": "주간 청취",
"LabelStreamingUsingCellular": "셀룰러를 이용한 스트리밍",
"LabelTag": "태그",
"LabelTags": "태그",
"LabelTheme": "테마",
"LabelThemeBlack": "Black",
"LabelThemeDark": "Dark",
"LabelThemeLight": "Light",
"LabelTimeRemaining": "{0} 남음",
"LabelTitle": "제목",
"LabelTotalSize": "전체 크기",
"LabelTotalTrack": "총 트랙",
"LabelTracks": "트랙",
"LabelType": "유형",
"LabelUnknown": "미상",
"LabelUnlockPlayer": "플레이어 잠금 해제",
"LabelUseBookshelfView": "bookshelf view 사용",
"LabelUser": "사용자",
"LabelUsername": "사용자 이름",
"LabelVeryHigh": "매우 높음",
"LabelVeryLow": "매우 낮음",
"LabelYearReviewHide": "연간 리뷰 숨기기",
"LabelYearReviewShow": "올해의 리뷰 보기",
"LabelYourBookmarks": "내 북마크",
"LabelYourProgress": "내 진행 상황",
"MessageAndroid10Downloads": "Android 10 이하에서는 다운로드를 위해 내부 앱 저장소를 사용합니다.",
"MessageAttemptingServerConnection": "서버 연결을 시도 중입니다...",
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf 서버가 연결되지 않았습니다",
"MessageAudiobookshelfServerRequired": "<strong>중요!</strong> 이 앱은 본인이나 아는 사람이 호스팅하는 Audiobookshelf 서버와 함께 사용하도록 설계되었습니다. 이 앱은 어떠한 콘텐츠도 제공하지 않습니다.",
"MessageBookshelfEmpty": "책장이 비어 있음",
"MessageConfirmDeleteLocalEpisode": "기기에서 로컬 에피소드 \"{0}\"을(를) 삭제하시겠습니까? 서버에 있는 파일은 영향을 받지 않습니다.",
"MessageConfirmDeleteLocalFiles": "이 항목의 로컬 파일을 기기에서 제거하시겠습니까? 서버의 파일과 진행 상황에는 아무런 영향이 없습니다.",
"MessageConfirmDisableAutoTimer": "오늘 남은 시간 동안 자동 타이머를 비활성화하시겠습니까? 자동 슬립 타이머 기간이 끝나거나 앱을 다시 시작하면 타이머가 다시 활성화됩니다.",
"MessageConfirmDiscardProgress": "진행 상황을 재설정하시겠습니까?",
"MessageConfirmDownloadUsingCellular": "셀룰러 데이터를 사용하여 다운로드하려고 합니다. 이동통신사 데이터 요금이 부과될 수 있습니다. 계속하시겠습니까?",
"MessageConfirmMarkAsFinished": "이 항목을 완료로 표시하시겠습니까?",
"MessageConfirmRemoveBookmark": "북마크를 삭제하시겠습니까?",
"MessageConfirmStreamingUsingCellular": "셀룰러 데이터를 사용하여 스트리밍하려고 합니다. 이동통신사 데이터 요금이 부과될 수 있습니다. 계속하시겠습니까?",
"MessageDiscardProgress": "진행 상황 삭제",
"MessageDownloadCompleteProcessing": "다운로드가 완료되었습니다. 처리 중입니다...",
"MessageDownloading": "다운로드 중...",
"MessageDownloadingEpisode": "에피소드 다운로드 중",
"MessageEpisodesQueuedForDownload": "{0}개의 에피소드가 다운로드 대기 중입니다.",
"MessageFailedToRefreshToken": "토큰 새로 고침에 실패했습니다. 다시 로그인해야 합니다.",
"MessageFeedURLWillBe": "피드 URL은 {0}입니다.",
"MessageFetching": "가져오는 중...",
"MessageFollowTheProjectOnGithub": "Github에서 프로젝트를 팔로우하세요",
"MessageItemDownloadCompleteFailedToCreate": "항목 다운로드가 완료되었지만 라이브러리 항목을 생성하지 못했습니다.",
"MessageItemMissing": "항목이 누락되어 서버에서 수정해야 합니다. 일반적으로 파일 경로에 접근할 수 없기 때문에 항목이 누락으로 표시됩니다.",
"MessageLoading": "로딩중...",
"MessageLoadingServerData": "서버 데이터를 로딩 중입니다...",
"MessageLocalFolderDescription": "''내부 앱 저장소'는 이 앱에서만 접근할 수 있습니다. 이 앱은 앱을 통해 직접 다운로드한 미디어만 지원합니다. 공유 저장소 폴더를 사용하면 다른 앱이 이 앱에서 다운로드한 미디어에 접근할 수 있습니다.",
"MessageMarkAsFinished": "완료로 표시",
"MessageMediaLinkedToADifferentServer": "미디어가 다른 주소({0})의 오디오북셸프 서버에 연결되어 있습니다. 이 서버 주소에 연결되면 진행 상황이 동기화됩니다.",
"MessageMediaLinkedToADifferentUser": "미디어가 이 서버에 연결되어 있지만 다른 사용자가 다운로드했습니다. 진행 상황은 해당 미디어를 다운로드한 사용자에게만 동기화됩니다.",
"MessageMediaLinkedToServer": "서버 {0}에 연결됨",
"MessageMediaLinkedToThisServer": "다운로드한 미디어가 이 서버에 연결되었습니다.",
"MessageMediaNotLinkedToServer": "미디어가 오디오북 셸프 서버에 연결되지 않았습니다. 진행 상황이 동기화되지 않습니다.",
"MessageNoBookmarks": "북마크 없음",
"MessageNoChapters": "챕터 없음",
"MessageNoCollections": "컬렉션 없음",
"MessageNoItems": "항목 없음",
"MessageNoItemsFound": "항목을 찾을 수 없음",
"MessageNoListeningSessions": "청취 세션 없음",
"MessageNoLogs": "로그 없음",
"MessageNoMediaFolders": "미디어 폴더 없음",
"MessageNoNetworkConnection": "네트워크 연결이 없음",
"MessageNoPodcastsFound": "팟캐스트를 찾을 수 없음",
"MessageNoSeries": "시리즈 없음",
"MessageNoUpdatesWereNecessary": "업데이트가 필요하지 않음",
"MessageNoUserPlaylists": "재생목록이 없음",
"MessageOldServerAuthReLoginRequired": "서버 v2.26.0에서는 보안을 위해 인증 기능이 개선되었습니다. 모든 사용자는 다시 로그인해야 합니다.",
"MessageOldServerAuthWarning": "서버가 오래된 인증을 사용하고 있습니다.",
"MessageOldServerAuthWarningHelp": "이 서버는 v2.26.0 이전 버전을 사용하고 있습니다. v2.26.0에서는 더욱 안전한 인증 시스템이 추가되었습니다. 서버를 최신 버전으로 업데이트하는 것을 강력히 권장합니다. 이미 서버를 업데이트하셨다면, 새로운 인증을 사용하려면 다시 로그인하세요.",
"MessageOldServerConnectionWarning": "서버 연결 구성에서 이전 사용자 ID를 사용하고 있습니다. 이 서버 연결을 삭제하고 다시 추가하세요.",
"MessageOldServerConnectionWarningHelp": "2023년 6월에 출시된 2.3.0 버전의 데이터베이스 마이그레이션 이전에 이 서버에 대한 연결을 설정했습니다. 향후 서버 업데이트에서는 이 이전 연결로 로그인할 수 있는 기능이 제거됩니다. 기존 서버 연결을 삭제하고 동일한 서버 주소와 사용자 인증 정보를 사용하여 다시 연결하세요. 이 기기에 다운로드한 미디어가 있는 경우, 서버와 동기화하려면 미디어를 다시 다운로드해야 합니다.",
"MessagePodcastSearchField": "검색어 또는 RSS 피드 URL을 입력하세요",
"MessageProgressSyncFailed": "서버에 청취 진행 상황을 보고하려는 최근 시도가 실패했습니다. 미디어 재생 중에는 15초에서 1분 간격으로 진행 상황 동기화 요청이 계속 시도됩니다.",
"MessageReportBugsAndContribute": "버그 보고, 기능 요청 및 기여",
"MessageSeriesAlreadyDownloaded": "이 시리즈의 모든 책을 이미 다운로드했습니다.",
"MessageSeriesDownloadConfirm": "{1}개의 파일과 함께 누락된 {0}권의 책, 총 {2}권을 {3} 폴더로 다운로드하시겠습니까?",
"MessageSeriesDownloadConfirmIos": "{1}개의 파일과 함께 누락된 {0}권의 책을 다운로드하시겠습니까? 총 {2}개입니다.",
"MessageSocketConnectedOverMeteredCellular": "미터링된 셀룰러를 통해 연결된 소켓",
"MessageSocketConnectedOverMeteredWifi": "미터기 Wi-Fi로 연결된 소켓",
"MessageSocketConnectedOverUnmeteredCellular": "무제한 셀룰러를 통해 연결된 소켓",
"MessageSocketConnectedOverUnmeteredWifi": "무제한 Wi-Fi로 연결된 소켓",
"MessageSocketNotConnected": "소켓이 연결되지 않음",
"NoteRSSFeedPodcastAppsHttps": "경고: 대부분의 팟캐스트 앱에서는 RSS 피드 URL이 HTTPS를 사용해야 합니다.",
"NoteRSSFeedPodcastAppsPubDate": "경고: 1개 이상의 에피소드에 게시 날짜가 없습니다. 일부 팟캐스트 앱에서는 게시 날짜가 필요합니다.",
"ToastBookmarkCreateFailed": "북마크를 생성하지 못했습니다.",
"ToastBookmarkRemoveFailed": "북마크를 삭제하지 못했습니다.",
"ToastBookmarkUpdateFailed": "북마크 업데이트에 실패했습니다",
"ToastDownloadNotAllowedOnCellular": "셀룰러 데이터에서는 다운로드가 허용되지 않습니다.",
"ToastItemMarkedAsFinishedFailed": "완료로 표시하지 못했습니다.",
"ToastItemMarkedAsNotFinishedFailed": "완료되지 않음으로 표시하는 데 실패했습니다.",
"ToastPlaylistCreateFailed": "플레이리스트를 생성하지 못했습니다",
"ToastPodcastCreateFailed": "팟캐스트를 만들지 못했습니다",
"ToastPodcastCreateSuccess": "팟캐스트가 성공적으로 생성되었습니다.",
"ToastRSSFeedCloseFailed": "RSS 피드를 닫지 못했습니다.",
"ToastRSSFeedCloseSuccess": "RSS 피드가 닫혔습니다",
"ToastStreamingNotAllowedOnCellular": "셀룰러 데이터에서는 스트리밍이 허용되지 않습니다."
}
+9
View File
@@ -6,6 +6,7 @@
"ButtonCancel": "Atšaukti", "ButtonCancel": "Atšaukti",
"ButtonCancelTimer": "Atšaukti laikmatį", "ButtonCancelTimer": "Atšaukti laikmatį",
"ButtonClearFilter": "Valyti filtrą", "ButtonClearFilter": "Valyti filtrą",
"ButtonClearLogs": "Išvalyti žurnalus",
"ButtonCloseFeed": "Uždaryti srautą", "ButtonCloseFeed": "Uždaryti srautą",
"ButtonCollections": "Kolekcijos", "ButtonCollections": "Kolekcijos",
"ButtonConnect": "Prijungti", "ButtonConnect": "Prijungti",
@@ -21,6 +22,7 @@
"ButtonIssues": "Problemos", "ButtonIssues": "Problemos",
"ButtonLatest": "Naujausias", "ButtonLatest": "Naujausias",
"ButtonLibrary": "Biblioteka", "ButtonLibrary": "Biblioteka",
"ButtonLogs": "Žurnalai",
"ButtonManageLocalFiles": "Tvarkyti vietinius failus", "ButtonManageLocalFiles": "Tvarkyti vietinius failus",
"ButtonNewFolder": "Naujas aplankas", "ButtonNewFolder": "Naujas aplankas",
"ButtonNextEpisode": "Kitas epizodas", "ButtonNextEpisode": "Kitas epizodas",
@@ -68,6 +70,8 @@
"LabelAddToPlaylist": "Pridėti į grojaraštį", "LabelAddToPlaylist": "Pridėti į grojaraštį",
"LabelAddedAt": "Pridėta {0}", "LabelAddedAt": "Pridėta {0}",
"LabelAll": "Visi", "LabelAll": "Visi",
"LabelAlways": "Visada",
"LabelAskConfirmation": "Prašyti patvirtinimo",
"LabelAuthor": "Autorius", "LabelAuthor": "Autorius",
"LabelAuthorFirstLast": "Autorius (Vardas Pavardė)", "LabelAuthorFirstLast": "Autorius (Vardas Pavardė)",
"LabelAuthorLastFirst": "Autorius (Pavardė, Vardas)", "LabelAuthorLastFirst": "Autorius (Pavardė, Vardas)",
@@ -80,10 +84,13 @@
"LabelComplete": "Baigta", "LabelComplete": "Baigta",
"LabelDescription": "Aprašymas", "LabelDescription": "Aprašymas",
"LabelDownload": "Atsisiųsti", "LabelDownload": "Atsisiųsti",
"LabelDownloaded": "Atsisiųsta",
"LabelDuration": "Trukmė", "LabelDuration": "Trukmė",
"LabelEbook": "Elektroninė knyga", "LabelEbook": "Elektroninė knyga",
"LabelEbooks": "El. knygos",
"LabelEnable": "Įjungti", "LabelEnable": "Įjungti",
"LabelEnd": "Pabaiga", "LabelEnd": "Pabaiga",
"LabelEndOfChapter": "Skyriaus pabaiga",
"LabelEpisode": "Epizodas", "LabelEpisode": "Epizodas",
"LabelFeedURL": "Srauto URL", "LabelFeedURL": "Srauto URL",
"LabelFile": "Failas", "LabelFile": "Failas",
@@ -95,6 +102,8 @@
"LabelFontScale": "Šrifto mastelis", "LabelFontScale": "Šrifto mastelis",
"LabelGenre": "Žanras", "LabelGenre": "Žanras",
"LabelGenres": "Žanrai", "LabelGenres": "Žanrai",
"LabelHasEbook": "Turi el. knygą",
"LabelHasSupplementaryEbook": "Turi papildomą el. knygą",
"LabelHost": "Serveris", "LabelHost": "Serveris",
"LabelInProgress": "Vyksta", "LabelInProgress": "Vyksta",
"LabelIncomplete": "Nebaigta", "LabelIncomplete": "Nebaigta",
+1
View File
@@ -0,0 +1 @@
{}
+24 -7
View File
@@ -41,7 +41,7 @@
"ButtonPlaylists": "Afspeellijsten", "ButtonPlaylists": "Afspeellijsten",
"ButtonRead": "Lezen", "ButtonRead": "Lezen",
"ButtonReadLess": "Minder lezen", "ButtonReadLess": "Minder lezen",
"ButtonReadMore": "Meer lezen", "ButtonReadMore": "Lees meer",
"ButtonRemove": "Verwijder", "ButtonRemove": "Verwijder",
"ButtonRemoveFromServer": "Verwijder van server", "ButtonRemoveFromServer": "Verwijder van server",
"ButtonSave": "Opslaan", "ButtonSave": "Opslaan",
@@ -63,11 +63,12 @@
"HeaderChapters": "Hoofdstukken", "HeaderChapters": "Hoofdstukken",
"HeaderCollection": "Collectie", "HeaderCollection": "Collectie",
"HeaderCollectionItems": "Collectie-objecten", "HeaderCollectionItems": "Collectie-objecten",
"HeaderConfirm": "Bevestigen",
"HeaderConnectionStatus": "Verbindingsstatus", "HeaderConnectionStatus": "Verbindingsstatus",
"HeaderDataSettings": "Data Instellingen", "HeaderDataSettings": "Gegevens Instellingen",
"HeaderDetails": "Details", "HeaderDetails": "Details",
"HeaderDownloads": "Downloads", "HeaderDownloads": "Downloads",
"HeaderEbookFiles": "Ebook bestanden", "HeaderEbookFiles": "E-book bestanden",
"HeaderEpisodes": "Afleveringen", "HeaderEpisodes": "Afleveringen",
"HeaderEreaderSettings": "Ereader-instellingen", "HeaderEreaderSettings": "Ereader-instellingen",
"HeaderLatestEpisodes": "Laatste afleveringen", "HeaderLatestEpisodes": "Laatste afleveringen",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Recente sessies", "HeaderStatsRecentSessions": "Recente sessies",
"HeaderTableOfContents": "Inhoudsopgave", "HeaderTableOfContents": "Inhoudsopgave",
"HeaderUserInterfaceSettings": "Gebruiker Interface Instellingen", "HeaderUserInterfaceSettings": "Gebruiker Interface Instellingen",
"HeaderWelcome": "Welkom, <strong>{0}</strong>",
"HeaderYourStats": "Je statistieken", "HeaderYourStats": "Je statistieken",
"LabelAddToPlaylist": "Toevoegen aan afspeellijst", "LabelAddToPlaylist": "Toevoegen aan afspeellijst",
"LabelAddedAt": "Toegevoegd op", "LabelAddedAt": "Toegevoegd op",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Wanneer de automatische slaap timer afloop zal het item terugspoelen naar de vorige positie.", "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.", "LabelAutoSleepTimerHelp": "Wanneer er tussen de opgegeven begin- en eindtijd media wordt afgespeeld, start er automatisch een slaaptimer.",
"LabelBooks": "Boeken", "LabelBooks": "Boeken",
"LabelByAuthor": "door {0}",
"LabelChapterTrack": "Hoofdstuk Track", "LabelChapterTrack": "Hoofdstuk Track",
"LabelChapters": "Hoofdstukken", "LabelChapters": "Hoofdstukken",
"LabelClosePlayer": "Sluit speler", "LabelClosePlayer": "Sluit speler",
@@ -155,6 +158,9 @@
"LabelFinished": "Voltooid", "LabelFinished": "Voltooid",
"LabelFolder": "Map", "LabelFolder": "Map",
"LabelFontBoldness": "Lettertype Dikte", "LabelFontBoldness": "Lettertype Dikte",
"LabelFontFamily": "Letterfamilie",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Lettertype schaal", "LabelFontScale": "Lettertype schaal",
"LabelGenre": "Genre", "LabelGenre": "Genre",
"LabelGenres": "Categorieën", "LabelGenres": "Categorieën",
@@ -174,6 +180,9 @@
"LabelLayout": "Layout", "LabelLayout": "Layout",
"LabelLayoutAuto": "Automatisch", "LabelLayoutAuto": "Automatisch",
"LabelLayoutSinglePage": "Enkele pagina", "LabelLayoutSinglePage": "Enkele pagina",
"LabelLibrarySortByProgress": "Voortgang: Laatst geüpdatet",
"LabelLibrarySortByProgressFinished": "Voortgang: Voltooid",
"LabelLibrarySortByProgressStarted": "Voortgang: Gestart",
"LabelLight": "Licht", "LabelLight": "Licht",
"LabelLineSpacing": "Regelruimte", "LabelLineSpacing": "Regelruimte",
"LabelListenAgain": "Opnieuw Beluisteren", "LabelListenAgain": "Opnieuw Beluisteren",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Schaal verstreken tijd per snelheid", "LabelScaleElapsedTimeBySpeed": "Schaal verstreken tijd per snelheid",
"LabelSeason": "Seizoen", "LabelSeason": "Seizoen",
"LabelSelectADevice": "Apparaat Selecteren", "LabelSelectADevice": "Apparaat Selecteren",
"LabelSelectMediaType": "Selecteer mediatype",
"LabelSequenceAscending": "Sequentie Oplopend", "LabelSequenceAscending": "Sequentie Oplopend",
"LabelSequenceDescending": "Sequentie Aflopend", "LabelSequenceDescending": "Sequentie Aflopend",
"LabelSeries": "Serie", "LabelSeries": "Serie",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf server niet geconnecteerd", "MessageAudiobookshelfServerNotConnected": "Audiobookshelf server niet geconnecteerd",
"MessageAudiobookshelfServerRequired": "<strong>Important!</strong> Deze app is ontworpen om te werken met een Audiobookshelf-server die u of iemand die u kent host. Deze app biedt geen content.", "MessageAudiobookshelfServerRequired": "<strong>Important!</strong> Deze app is ontworpen om te werken met een Audiobookshelf-server die u of iemand die u kent host. Deze app biedt geen content.",
"MessageBookshelfEmpty": "Boekenplank leeg", "MessageBookshelfEmpty": "Boekenplank leeg",
"MessageConfirmDeleteLocalEpisode": "Lokale aflevering \"{0}\" van uw apparaat verwijderen? Het bestand op de server blijft onaangetast.", "MessageConfirmAppExit": "Wil je de app afsluiten?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Weet je zeker dat je de downloadwachtrij voor afleveringen wilt wissen?",
"MessageConfirmDeleteLocalEpisode": "Lokale aflevering \"{0}\" van uw apparaat verwijderen? Het bestand op de server blijft bewaard.",
"MessageConfirmDeleteLocalFiles": "Lokale bestanden van dit item van uw apparaat verwijderen? De bestanden op de server en uw voortgang worden niet beïnvloed.", "MessageConfirmDeleteLocalFiles": "Lokale bestanden van dit item van uw apparaat verwijderen? De bestanden op de server en uw voortgang worden niet beïnvloed.",
"MessageConfirmDeleteServerConfig": "Verwijder deze server configuratie?",
"MessageConfirmDeleteServerEpisode": "Ben je zeker dat je aflevering “{0}” van de server wil verwijderen?\nWaarschuwing: Dit zal het audiobestand verwijderden.",
"MessageConfirmDisableAutoTimer": "Weet u zeker dat je de automatische timer voor de rest van vandaag wilt uitschakelen? De timer wordt automatisch weer ingeschakeld aan het einde van deze sluimerperiode of als u de app opnieuw start.", "MessageConfirmDisableAutoTimer": "Weet u zeker dat je de automatische timer voor de rest van vandaag wilt uitschakelen? De timer wordt automatisch weer ingeschakeld aan het einde van deze sluimerperiode of als u de app opnieuw start.",
"MessageConfirmDiscardProgress": "Weet u zeker dat u uw voortgang wilt resetten?", "MessageConfirmDiscardProgress": "Weet u zeker dat u uw voortgang wilt resetten?",
"MessageConfirmDownloadUsingCellular": "U staat op het punt om te downloaden met behulp van mobiele data. Dit kan kosten voor data van de provider met zich meebrengen. Wilt u doorgaan?", "MessageConfirmDownloadUsingCellular": "U staat op het punt om te downloaden met behulp van mobiele data. Dit kan kosten voor data van de provider met zich meebrengen. Wilt u doorgaan?",
"MessageConfirmMarkAsFinished": "Weet u zeker dat u dit item als voltooid wilt markeren?", "MessageConfirmMarkAsFinished": "Weet u zeker dat u dit item als voltooid wilt markeren?",
"MessageConfirmPlaybackTime": "Start afspelen van “{0}” op {1}?",
"MessageConfirmRemoveBookmark": "Weet u zeker dat u de bladwijzer wilt verwijderen?", "MessageConfirmRemoveBookmark": "Weet u zeker dat u de bladwijzer wilt verwijderen?",
"MessageConfirmStreamingUsingCellular": "U staat op het punt om te streamen met behulp van mobiele data. Dit kan kosten voor data van de provider met zich meebrengen. Wilt u doorgaan?", "MessageConfirmStreamingUsingCellular": "U staat op het punt om te streamen met behulp van mobiele data. Dit kan kosten voor data van de provider met zich meebrengen. Wilt u doorgaan?",
"MessageDiscardProgress": "Voortgang negeren", "MessageDiscardProgress": "Voortgang negeren",
@@ -303,7 +318,7 @@
"MessageFetching": "Aan het ophalen...", "MessageFetching": "Aan het ophalen...",
"MessageFollowTheProjectOnGithub": "Volg dit project op Github", "MessageFollowTheProjectOnGithub": "Volg dit project op Github",
"MessageItemDownloadCompleteFailedToCreate": "Itemdownload voltooid, maar het aanmaken van een bibliotheekitem is mislukt", "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.", "MessageItemMissing": "Item is niet beschikbaar en moet opgelost worden op de server. Meestal is een item gemarkeerd als missend in verband met niet bereikbare bestandspaden.",
"MessageLoading": "Aan het laden...", "MessageLoading": "Aan het laden...",
"MessageLoadingServerData": "Server data 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.", "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.",
@@ -331,7 +346,7 @@
"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.", "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.", "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.", "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", "MessagePodcastSearchField": "Voer zoekterm of RSS-feed-URL in",
"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.", "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", "MessageReportBugsAndContribute": "Rapporteer bugs, vraag functionaliteiten aan en draag bij op",
"MessageSeriesAlreadyDownloaded": "Je hebt alle boeken in deze serie al gedownload.", "MessageSeriesAlreadyDownloaded": "Je hebt alle boeken in deze serie al gedownload.",
@@ -355,5 +370,7 @@
"ToastPodcastCreateSuccess": "Podcast aangemaakt", "ToastPodcastCreateSuccess": "Podcast aangemaakt",
"ToastRSSFeedCloseFailed": "Sluiten RSS-feed mislukt", "ToastRSSFeedCloseFailed": "Sluiten RSS-feed mislukt",
"ToastRSSFeedCloseSuccess": "RSS-feed gesloten", "ToastRSSFeedCloseSuccess": "RSS-feed gesloten",
"ToastStreamingNotAllowedOnCellular": "Streamen is niet toegstaan via mobiele verbinding" "ToastStreamingNotAllowedOnCellular": "Streamen is niet toegstaan via mobiele verbinding",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+19 -1
View File
@@ -63,6 +63,7 @@
"HeaderChapters": "Kapittel", "HeaderChapters": "Kapittel",
"HeaderCollection": "Samlinger", "HeaderCollection": "Samlinger",
"HeaderCollectionItems": "Samlingsgjenstander", "HeaderCollectionItems": "Samlingsgjenstander",
"HeaderConfirm": "Bekreft",
"HeaderConnectionStatus": "Tilkoblingsstatus", "HeaderConnectionStatus": "Tilkoblingsstatus",
"HeaderDataSettings": "Datainnstillinger", "HeaderDataSettings": "Datainnstillinger",
"HeaderDetails": "Detaljer", "HeaderDetails": "Detaljer",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Siste sesjoner", "HeaderStatsRecentSessions": "Siste sesjoner",
"HeaderTableOfContents": "Innholdsfortegnelse", "HeaderTableOfContents": "Innholdsfortegnelse",
"HeaderUserInterfaceSettings": "Innstillinger for Brukergrensesnitt", "HeaderUserInterfaceSettings": "Innstillinger for Brukergrensesnitt",
"HeaderWelcome": "Velkommen, <strong>{0}</strong>",
"HeaderYourStats": "Din statistikk", "HeaderYourStats": "Din statistikk",
"LabelAddToPlaylist": "Legg til i spilleliste", "LabelAddToPlaylist": "Legg til i spilleliste",
"LabelAddedAt": "Lagt Til", "LabelAddedAt": "Lagt Til",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Når automatisk søvntidsur fullføres og du gjenopptar avspillingen vil posisjonen din automatisk spoles tilbake.", "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.", "LabelAutoSleepTimerHelp": "Ved avspilling av media mellom angitt start og slutt vil automatisk søvntidsur starte automatisk.",
"LabelBooks": "Bøker", "LabelBooks": "Bøker",
"LabelByAuthor": "av {0}",
"LabelChapterTrack": "Kapittelspor", "LabelChapterTrack": "Kapittelspor",
"LabelChapters": "Kapitler", "LabelChapters": "Kapitler",
"LabelClosePlayer": "Lukk spiller", "LabelClosePlayer": "Lukk spiller",
@@ -155,6 +158,9 @@
"LabelFinished": "Fullført", "LabelFinished": "Fullført",
"LabelFolder": "Mappe", "LabelFolder": "Mappe",
"LabelFontBoldness": "Skrifttykkelse", "LabelFontBoldness": "Skrifttykkelse",
"LabelFontFamily": "Skriftfamilie",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Font størrelse", "LabelFontScale": "Font størrelse",
"LabelGenre": "Sjanger", "LabelGenre": "Sjanger",
"LabelGenres": "Sjangers", "LabelGenres": "Sjangers",
@@ -174,6 +180,9 @@
"LabelLayout": "Utseende", "LabelLayout": "Utseende",
"LabelLayoutAuto": "Automatisk", "LabelLayoutAuto": "Automatisk",
"LabelLayoutSinglePage": "Enkeltside", "LabelLayoutSinglePage": "Enkeltside",
"LabelLibrarySortByProgress": "Fremdrift: Sist oppdatert",
"LabelLibrarySortByProgressFinished": "Fremdrift: Fullført",
"LabelLibrarySortByProgressStarted": "Fremdrift: Startet",
"LabelLight": "Lett", "LabelLight": "Lett",
"LabelLineSpacing": "Linjemellomrom", "LabelLineSpacing": "Linjemellomrom",
"LabelListenAgain": "Lytt igjen", "LabelListenAgain": "Lytt igjen",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Skaler forløpt tid etter hastighet", "LabelScaleElapsedTimeBySpeed": "Skaler forløpt tid etter hastighet",
"LabelSeason": "Sesong", "LabelSeason": "Sesong",
"LabelSelectADevice": "Velg en enhet", "LabelSelectADevice": "Velg en enhet",
"LabelSelectMediaType": "Velg medietype",
"LabelSequenceAscending": "Stigende rekkefølge", "LabelSequenceAscending": "Stigende rekkefølge",
"LabelSequenceDescending": "Synkende rekkefølge", "LabelSequenceDescending": "Synkende rekkefølge",
"LabelSeries": "Serier", "LabelSeries": "Serier",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Ikke tilkoblet Audiobookshelf-tjener", "MessageAudiobookshelfServerNotConnected": "Ikke tilkoblet Audiobookshelf-tjener",
"MessageAudiobookshelfServerRequired": "<strong>Viktig!</strong> Programmet fungerer kun med en Audiobookshelf-tjener som du eller noen annen vertstjener. Programmet tilbyr ikke innhold.", "MessageAudiobookshelfServerRequired": "<strong>Viktig!</strong> Programmet fungerer kun med en Audiobookshelf-tjener som du eller noen annen vertstjener. Programmet tilbyr ikke innhold.",
"MessageBookshelfEmpty": "Tom bokhylle", "MessageBookshelfEmpty": "Tom bokhylle",
"MessageConfirmAppExit": "Vil du avslutte appen?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Er du sikker på at du vil tømme nedlastingskøen for episoder?",
"MessageConfirmDeleteLocalEpisode": "Fjen episoden «{0}» fra enheten? Filen på tjeneren forblir som den er.", "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.", "MessageConfirmDeleteLocalFiles": "Fjern dette elementets filer fra enheten? Filene på tjeneren og framdriften forblir som den er.",
"MessageConfirmDeleteServerConfig": "Fjerne denne serverkonfigurasjonen?",
"MessageConfirmDeleteServerEpisode": "Er du sikker på at du vil slette episoden \"{0}\" fra serveren?\nAdvarsel: Dette vil slette lydfilen.",
"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.", "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?", "MessageConfirmDiscardProgress": "Er du sikker på at du vil tilbakestille fremdriften din?",
"MessageConfirmDownloadUsingCellular": "Last ned over mobildata selv om kostnader kan tilkomme?", "MessageConfirmDownloadUsingCellular": "Last ned over mobildata selv om kostnader kan tilkomme?",
"MessageConfirmMarkAsFinished": "Er du sikker på at du vil merke element som fullført?", "MessageConfirmMarkAsFinished": "Er du sikker på at du vil merke element som fullført?",
"MessageConfirmPlaybackTime": "Starte avspilling av \"{0}\" ved {1}?",
"MessageConfirmRemoveBookmark": "Er du sikker på at du vil fjerne bokmerket?", "MessageConfirmRemoveBookmark": "Er du sikker på at du vil fjerne bokmerket?",
"MessageConfirmStreamingUsingCellular": "Strøm via mobildata selv om kostnader kan tilkomme?", "MessageConfirmStreamingUsingCellular": "Strøm via mobildata selv om kostnader kan tilkomme?",
"MessageDiscardProgress": "Forkast framdrift", "MessageDiscardProgress": "Forkast framdrift",
@@ -298,6 +313,7 @@
"MessageDownloading": "Laster ned …", "MessageDownloading": "Laster ned …",
"MessageDownloadingEpisode": "Laster ned episode", "MessageDownloadingEpisode": "Laster ned episode",
"MessageEpisodesQueuedForDownload": "{0} Episode(r) lagt til i kø for nedlasting", "MessageEpisodesQueuedForDownload": "{0} Episode(r) lagt til i kø for nedlasting",
"MessageFailedToRefreshToken": "Klarte ikke å oppdatere token, ny innlogging kreves",
"MessageFeedURLWillBe": "Feed URL vil bli {0}", "MessageFeedURLWillBe": "Feed URL vil bli {0}",
"MessageFetching": "Henter...", "MessageFetching": "Henter...",
"MessageFollowTheProjectOnGithub": "Følg prosjektet på GitHub", "MessageFollowTheProjectOnGithub": "Følg prosjektet på GitHub",
@@ -354,5 +370,7 @@
"ToastPodcastCreateSuccess": "Podcast opprettet", "ToastPodcastCreateSuccess": "Podcast opprettet",
"ToastRSSFeedCloseFailed": "Misslykkes å lukke RSS feed", "ToastRSSFeedCloseFailed": "Misslykkes å lukke RSS feed",
"ToastRSSFeedCloseSuccess": "RSS feed lukket", "ToastRSSFeedCloseSuccess": "RSS feed lukket",
"ToastStreamingNotAllowedOnCellular": "Strømming tillates ikke over mobildata" "ToastStreamingNotAllowedOnCellular": "Strømming tillates ikke over mobildata",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+33 -14
View File
@@ -32,7 +32,7 @@
"ButtonMaskServerAddress": "Ukryj adres serwera", "ButtonMaskServerAddress": "Ukryj adres serwera",
"ButtonNewFolder": "Nowy folder", "ButtonNewFolder": "Nowy folder",
"ButtonNextEpisode": "Następny odcinek", "ButtonNextEpisode": "Następny odcinek",
"ButtonOk": "Ok", "ButtonOk": "OK",
"ButtonOpenFeed": "Otwórz feed", "ButtonOpenFeed": "Otwórz feed",
"ButtonOverride": "Nadpisz", "ButtonOverride": "Nadpisz",
"ButtonPause": "Wstrzymaj", "ButtonPause": "Wstrzymaj",
@@ -48,7 +48,7 @@
"ButtonSaveOrder": "Zapisz kolejność", "ButtonSaveOrder": "Zapisz kolejność",
"ButtonSearch": "Szukaj", "ButtonSearch": "Szukaj",
"ButtonSendEbookToDevice": "Wyślij ebook do urządzenia", "ButtonSendEbookToDevice": "Wyślij ebook do urządzenia",
"ButtonSeries": "Seria", "ButtonSeries": "Serie",
"ButtonSetTimer": "Ustaw wyłącznik", "ButtonSetTimer": "Ustaw wyłącznik",
"ButtonStream": "Strumieniuj", "ButtonStream": "Strumieniuj",
"ButtonSubmit": "Zapisz", "ButtonSubmit": "Zapisz",
@@ -63,6 +63,7 @@
"HeaderChapters": "Rozdziały", "HeaderChapters": "Rozdziały",
"HeaderCollection": "Kolekcja", "HeaderCollection": "Kolekcja",
"HeaderCollectionItems": "Elementy kolekcji", "HeaderCollectionItems": "Elementy kolekcji",
"HeaderConfirm": "Potwierdzić",
"HeaderConnectionStatus": "Status połączenia", "HeaderConnectionStatus": "Status połączenia",
"HeaderDataSettings": "Ustawienia danych", "HeaderDataSettings": "Ustawienia danych",
"HeaderDetails": "Szczegóły", "HeaderDetails": "Szczegóły",
@@ -79,7 +80,7 @@
"HeaderPlaybackSettings": "Ustawienia odtwarzania", "HeaderPlaybackSettings": "Ustawienia odtwarzania",
"HeaderPlaylist": "Playlista", "HeaderPlaylist": "Playlista",
"HeaderPlaylistItems": "Pozycje listy odtwarzania", "HeaderPlaylistItems": "Pozycje listy odtwarzania",
"HeaderProgressSyncFailed": "Błąd synchronizacji postępów", "HeaderProgressSyncFailed": "Synchronizacja postępu nie powiodła się",
"HeaderRSSFeed": "Kanał RSS", "HeaderRSSFeed": "Kanał RSS",
"HeaderRSSFeedGeneral": "Szczegóły RSS", "HeaderRSSFeedGeneral": "Szczegóły RSS",
"HeaderRSSFeedIsOpen": "Kanał RSS jest otwarty", "HeaderRSSFeedIsOpen": "Kanał RSS jest otwarty",
@@ -91,10 +92,11 @@
"HeaderStatsRecentSessions": "Ostatnie sesje", "HeaderStatsRecentSessions": "Ostatnie sesje",
"HeaderTableOfContents": "Spis treści", "HeaderTableOfContents": "Spis treści",
"HeaderUserInterfaceSettings": "Ustawienia inferfejsu użytkownika", "HeaderUserInterfaceSettings": "Ustawienia inferfejsu użytkownika",
"HeaderWelcome": "Witaj, <strong>{0}</strong>",
"HeaderYourStats": "Twoje statystyki", "HeaderYourStats": "Twoje statystyki",
"LabelAddToPlaylist": "Dodaj do playlisty", "LabelAddToPlaylist": "Dodaj do playlisty",
"LabelAddedAt": "Dodano w", "LabelAddedAt": "Dodano",
"LabelAddedDate": "Dodano", "LabelAddedDate": "Dodano {0}",
"LabelAll": "Wszystkie", "LabelAll": "Wszystkie",
"LabelAllowSeekingOnMediaControls": "Zezwól na wyszukiwanie pozycji na ekranie blokady", "LabelAllowSeekingOnMediaControls": "Zezwól na wyszukiwanie pozycji na ekranie blokady",
"LabelAlways": "Zawsze", "LabelAlways": "Zawsze",
@@ -113,10 +115,11 @@
"LabelAutoSleepTimerAutoRewindHelp": "Po zakończeniu automatycznego wyłącznika czasowego ponowne odtworzenie elementu spowoduje automatyczne przewinięcie pozycji do tyłu.", "LabelAutoSleepTimerAutoRewindHelp": "Po zakończeniu automatycznego wyłącznika czasowego ponowne odtworzenie elementu spowoduje automatyczne przewinięcie pozycji do tyłu.",
"LabelAutoSleepTimerHelp": "Podczas odtwarzania multimediów między określonym czasem rozpoczęcia i zakończenia automatycznie uruchomi się wyłącznik czasowy.", "LabelAutoSleepTimerHelp": "Podczas odtwarzania multimediów między określonym czasem rozpoczęcia i zakończenia automatycznie uruchomi się wyłącznik czasowy.",
"LabelBooks": "Książki", "LabelBooks": "Książki",
"LabelByAuthor": "Autor {0}",
"LabelChapterTrack": "Postęp rozdziału", "LabelChapterTrack": "Postęp rozdziału",
"LabelChapters": "Rozdziały", "LabelChapters": "Rozdziały",
"LabelClosePlayer": "Zamknij odtwarzacz", "LabelClosePlayer": "Zamknij odtwarzacz",
"LabelCollapseSeries": "Podsumuj serię", "LabelCollapseSeries": "Zwiń serię",
"LabelComplete": "Ukończone", "LabelComplete": "Ukończone",
"LabelContinueBooks": "Kontynuuj książki", "LabelContinueBooks": "Kontynuuj książki",
"LabelContinueEpisodes": "Kontynuuj odcinki", "LabelContinueEpisodes": "Kontynuuj odcinki",
@@ -146,6 +149,7 @@
"LabelEndOfChapter": "Koniec rozdziału", "LabelEndOfChapter": "Koniec rozdziału",
"LabelEndTime": "Koniec czasu", "LabelEndTime": "Koniec czasu",
"LabelEpisode": "Odcinek", "LabelEpisode": "Odcinek",
"LabelExplicit": "18+",
"LabelFeedURL": "URL kanału", "LabelFeedURL": "URL kanału",
"LabelFile": "Plik", "LabelFile": "Plik",
"LabelFileBirthtime": "Data utworzenia pliku", "LabelFileBirthtime": "Data utworzenia pliku",
@@ -154,6 +158,9 @@
"LabelFinished": "Zakończone", "LabelFinished": "Zakończone",
"LabelFolder": "Katalog", "LabelFolder": "Katalog",
"LabelFontBoldness": "Grubość czcionki", "LabelFontBoldness": "Grubość czcionki",
"LabelFontFamily": "Krój pisma",
"LabelFontFamilySans": "Pismo bezszeryfowe",
"LabelFontFamilySerif": "Pismo szeryfowe",
"LabelFontScale": "Rozmiar czcionki", "LabelFontScale": "Rozmiar czcionki",
"LabelGenre": "Gatunek", "LabelGenre": "Gatunek",
"LabelGenres": "Gatunki", "LabelGenres": "Gatunki",
@@ -168,12 +175,15 @@
"LabelInternalAppStorage": "Pamięć wewnętrzna aplikacji", "LabelInternalAppStorage": "Pamięć wewnętrzna aplikacji",
"LabelJumpBackwardsTime": "Przeskok wstecz", "LabelJumpBackwardsTime": "Przeskok wstecz",
"LabelJumpForwardsTime": "Przeskok w przód", "LabelJumpForwardsTime": "Przeskok w przód",
"LabelKeepScreenAwake": "Utrzymuj włączony ekran", "LabelKeepScreenAwake": "Utrzymuj ekran w stanie czuwania",
"LabelLanguage": "Język", "LabelLanguage": "Język",
"LabelLayout": "Układ", "LabelLayout": "Układ",
"LabelLayoutAuto": "Automatycznie", "LabelLayoutAuto": "Automatycznie",
"LabelLayoutSinglePage": "Pojedyncza strona", "LabelLayoutSinglePage": "Pojedyncza strona",
"LabelLight": "Lekko", "LabelLibrarySortByProgress": "Postęp: Ostatnia aktualizacja",
"LabelLibrarySortByProgressFinished": "Postęp: Ukończony",
"LabelLibrarySortByProgressStarted": "Postęp: Rozpoczęty",
"LabelLight": "Jasny",
"LabelLineSpacing": "Odstęp między wierszami", "LabelLineSpacing": "Odstęp między wierszami",
"LabelListenAgain": "Słuchaj ponownie", "LabelListenAgain": "Słuchaj ponownie",
"LabelLocalBooks": "Książki lokalne", "LabelLocalBooks": "Książki lokalne",
@@ -232,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Skaluj czas, który upłynął według prędkości", "LabelScaleElapsedTimeBySpeed": "Skaluj czas, który upłynął według prędkości",
"LabelSeason": "Sezon", "LabelSeason": "Sezon",
"LabelSelectADevice": "Wybierz urządzenie", "LabelSelectADevice": "Wybierz urządzenie",
"LabelSelectMediaType": "Wybierz rodzaj medi",
"LabelSequenceAscending": "Koleność Rosnąca", "LabelSequenceAscending": "Koleność Rosnąca",
"LabelSequenceDescending": "Kolejność Malejąca", "LabelSequenceDescending": "Kolejność Malejąca",
"LabelSeries": "Serie", "LabelSeries": "Serie",
@@ -258,7 +269,7 @@
"LabelStreamingUsingCellular": "Transmisja przy użyciu sieci komórkowej", "LabelStreamingUsingCellular": "Transmisja przy użyciu sieci komórkowej",
"LabelTag": "Znacznik", "LabelTag": "Znacznik",
"LabelTags": "Znaczniki", "LabelTags": "Znaczniki",
"LabelTheme": "Kompozycja", "LabelTheme": "Motyw",
"LabelThemeBlack": "Czarny", "LabelThemeBlack": "Czarny",
"LabelThemeDark": "Ciemny", "LabelThemeDark": "Ciemny",
"LabelThemeLight": "Jasny", "LabelThemeLight": "Jasny",
@@ -284,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Brak połączenia z serwerem Audiobookshelf", "MessageAudiobookshelfServerNotConnected": "Brak połączenia z serwerem Audiobookshelf",
"MessageAudiobookshelfServerRequired": "<strong>Important!</strong> Ta aplikacja jest przeznaczona do współpracy z serwerem Audiobookshelf, który dostarczasz Ty lub ktoś, kogo znasz. Ta aplikacja nie dostarcza żadnych treści..", "MessageAudiobookshelfServerRequired": "<strong>Important!</strong> Ta aplikacja jest przeznaczona do współpracy z serwerem Audiobookshelf, który dostarczasz Ty lub ktoś, kogo znasz. Ta aplikacja nie dostarcza żadnych treści..",
"MessageBookshelfEmpty": "Półka pusta", "MessageBookshelfEmpty": "Półka pusta",
"MessageConfirmAppExit": "Czy chcesz zamknąć aplikację?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Czy na pewno chcesz wyczyścić kolejkę pobierania odcinków?",
"MessageConfirmDeleteLocalEpisode": "Usunąć lokalny odcinek \"{0}\" ze swojego urządzenia? Nie będzie to miało wpływu na plik na serwerze.", "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.", "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..", "MessageConfirmDeleteServerConfig": "Usunąć konfigurację tego serwera?",
"MessageConfirmDeleteServerEpisode": "Czy na pewno chcesz usunąć odcinek „{0}” z serwera?\nOstrzeżenie: spowoduje to usunięcie pliku audio.",
"MessageConfirmDisableAutoTimer": "Czy na pewno chcesz wyłączyć automatyczny timer na resztę dnia? Timer zostanie ponownie włączony pod koniec tego okresu automatycznego uśpienia lub po ponownym uruchomieniu aplikacji.",
"MessageConfirmDiscardProgress": "Na pewno chcesz zresetować postęp?", "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ć?", "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?", "MessageConfirmMarkAsFinished": "Na pewno chcesz oznaczyć ten element jako ukończony?",
"MessageConfirmPlaybackTime": "Rozpocząć odtwarzanie pliku „{0}” w {1}?",
"MessageConfirmRemoveBookmark": "Na pewno chcesz usunąć zakładkę?", "MessageConfirmRemoveBookmark": "Na pewno chcesz usunąć zakładkę?",
"MessageConfirmStreamingUsingCellular": "Transmisja strumieniowa będzie odbywać się przy użyciu danych komórkowych. Może to obejmować opłaty za transmisję danych operatora. Czy chcesz kontynuować?", "MessageConfirmStreamingUsingCellular": "Transmisja strumieniowa będzie odbywać się przy użyciu danych komórkowych. Może to obejmować opłaty za transmisję danych operatora. Czy chcesz kontynuować?",
"MessageDiscardProgress": "Zresetuj postęp", "MessageDiscardProgress": "Zresetuj postęp",
@@ -297,14 +313,15 @@
"MessageDownloading": "Pobieranie...", "MessageDownloading": "Pobieranie...",
"MessageDownloadingEpisode": "Pobieranie odcinka", "MessageDownloadingEpisode": "Pobieranie odcinka",
"MessageEpisodesQueuedForDownload": "{0} odcinki w kolejce do pobrania", "MessageEpisodesQueuedForDownload": "{0} odcinki w kolejce do pobrania",
"MessageFailedToRefreshToken": "Odświeżenie tokenu nie powiodło się. Wymagane jest ponowne zalogowanie.", "MessageFailedToRefreshToken": "Odświeżenie tokenu nie powiodło się. Wymagane jest ponowne zalogowanie",
"MessageFeedURLWillBe": "URL kanału: {0}", "MessageFeedURLWillBe": "URL kanału: {0}",
"MessageFetching": "Pobieranie...", "MessageFetching": "Pobieranie...",
"MessageFollowTheProjectOnGithub": "Obserwuj projekt na Github'ie", "MessageFollowTheProjectOnGithub": "Obserwuj projekt na Github'ie",
"MessageItemDownloadCompleteFailedToCreate": "Pobieranie elementu zostało zakończone, ale nie udało się utworzyć elementu biblioteki", "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.", "MessageItemMissing": "Brakuje elementu i 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...", "MessageLoading": "Ładowanie...",
"MessageLoadingServerData": "Ładowanie danych serwera...", "MessageLoadingServerData": "Ładowanie danych serwera...",
"MessageLocalFolderDescription": "„Wewnętrzna pamięć aplikacji” jest dostępna tylko dla tej aplikacji. Ta aplikacja obsługuje wyłącznie multimedia pobrane bezpośrednio za jej pośrednictwem. Udostępnione foldery pamięci można wykorzystać, aby umożliwić innym aplikacjom dostęp do multimediów pobranych przez tę aplikację.",
"MessageMarkAsFinished": "Oznacz jako ukończone", "MessageMarkAsFinished": "Oznacz jako ukończone",
"MessageMediaLinkedToADifferentServer": "Multimedia są połączone z serwerem Audiobookshelf pod innym adresem ({0}). Postęp zostanie zsynchronizowany po nawiązaniu połączenia z tym adresem serwera.", "MessageMediaLinkedToADifferentServer": "Multimedia są połączone z serwerem Audiobookshelf pod innym adresem ({0}). Postęp zostanie zsynchronizowany po nawiązaniu połączenia z tym adresem serwera.",
"MessageMediaLinkedToADifferentUser": "Multimedia są połączone z tym serwerem, ale zostały pobrane przez innego użytkownika. Postęp zostanie zsynchronizowany tylko z użytkownikiem, który je pobrał.", "MessageMediaLinkedToADifferentUser": "Multimedia są połączone z tym serwerem, ale zostały pobrane przez innego użytkownika. Postęp zostanie zsynchronizowany tylko z użytkownikiem, który je pobrał.",
@@ -330,7 +347,7 @@
"MessageOldServerConnectionWarning": "Konfiguracja połączenia z serwerem używa starego identyfikatora użytkownika. Usuń i dodaj ponownie to połączenie z serwerem.", "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.", "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", "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.", "MessageProgressSyncFailed": "Ostatnia próba zgłoszenia postępu słuchania do serwera nie powiodła się. Żądania synchronizacji postępu będą nadal podejmowane co 15 sekund do 1 minuty podczas odtwarzania multimediów.",
"MessageReportBugsAndContribute": "Zgłoś błędy, pomysły i pomóż rozwijać aplikację na", "MessageReportBugsAndContribute": "Zgłoś błędy, pomysły i pomóż rozwijać aplikację na",
"MessageSeriesAlreadyDownloaded": "Seria książek już pobrana.", "MessageSeriesAlreadyDownloaded": "Seria książek już pobrana.",
"MessageSeriesDownloadConfirm": "Pobrać serię?", "MessageSeriesDownloadConfirm": "Pobrać serię?",
@@ -353,5 +370,7 @@
"ToastPodcastCreateSuccess": "Podcast został pomyślnie utworzony", "ToastPodcastCreateSuccess": "Podcast został pomyślnie utworzony",
"ToastRSSFeedCloseFailed": "Zamknięcie kanału RSS nie powiodło się", "ToastRSSFeedCloseFailed": "Zamknięcie kanału RSS nie powiodło się",
"ToastRSSFeedCloseSuccess": "Zamknięcie kanału RSS powiodło się", "ToastRSSFeedCloseSuccess": "Zamknięcie kanału RSS powiodło się",
"ToastStreamingNotAllowedOnCellular": "Transmisja nie jest dozwolona w przy użyciu danych komórkowych" "ToastStreamingNotAllowedOnCellular": "Transmisja nie jest dozwolona w przy użyciu danych komórkowych",
"UnitMinutesShort": "{0}min",
"UnitSecondsShort": "{0}sek"
} }
+23 -6
View File
@@ -27,7 +27,7 @@
"ButtonLatest": "Mais Recentes", "ButtonLatest": "Mais Recentes",
"ButtonLibrary": "Biblioteca", "ButtonLibrary": "Biblioteca",
"ButtonLocalMedia": "Mídia Local", "ButtonLocalMedia": "Mídia Local",
"ButtonLogs": "Logs", "ButtonLogs": "Registros",
"ButtonManageLocalFiles": "Gerenciar Arquivos Locais", "ButtonManageLocalFiles": "Gerenciar Arquivos Locais",
"ButtonMaskServerAddress": "Mascarar endereço do servidor", "ButtonMaskServerAddress": "Mascarar endereço do servidor",
"ButtonNewFolder": "Nova Pasta", "ButtonNewFolder": "Nova Pasta",
@@ -50,7 +50,7 @@
"ButtonSendEbookToDevice": "Enviar Ebook para Dispositivo", "ButtonSendEbookToDevice": "Enviar Ebook para Dispositivo",
"ButtonSeries": "Série", "ButtonSeries": "Série",
"ButtonSetTimer": "Definir Timer", "ButtonSetTimer": "Definir Timer",
"ButtonStream": "Stream", "ButtonStream": "Transmissão",
"ButtonSubmit": "Enviar", "ButtonSubmit": "Enviar",
"ButtonSwitchServerUser": "Trocar Servidor/Usuário", "ButtonSwitchServerUser": "Trocar Servidor/Usuário",
"ButtonUnmaskServerAddress": "Revelar endereço do servidor", "ButtonUnmaskServerAddress": "Revelar endereço do servidor",
@@ -63,6 +63,7 @@
"HeaderChapters": "Capítulos", "HeaderChapters": "Capítulos",
"HeaderCollection": "Coleção", "HeaderCollection": "Coleção",
"HeaderCollectionItems": "Itens da Coleção", "HeaderCollectionItems": "Itens da Coleção",
"HeaderConfirm": "Confirmar",
"HeaderConnectionStatus": "Status da Conexão", "HeaderConnectionStatus": "Status da Conexão",
"HeaderDataSettings": "Configurações de dados", "HeaderDataSettings": "Configurações de dados",
"HeaderDetails": "Detalhes", "HeaderDetails": "Detalhes",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Sessões Recentes", "HeaderStatsRecentSessions": "Sessões Recentes",
"HeaderTableOfContents": "Sumário", "HeaderTableOfContents": "Sumário",
"HeaderUserInterfaceSettings": "Configurações da Interface do Usuário", "HeaderUserInterfaceSettings": "Configurações da Interface do Usuário",
"HeaderWelcome": "Olá, <strong>{0}</strong>",
"HeaderYourStats": "Suas Estatísticas", "HeaderYourStats": "Suas Estatísticas",
"LabelAddToPlaylist": "Adicionar à Lista de Reprodução", "LabelAddToPlaylist": "Adicionar à Lista de Reprodução",
"LabelAddedAt": "Acrescentado Em", "LabelAddedAt": "Acrescentado Em",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Após o timer terminar, da próxima vez que o item for reproduzido a sua posição será retrocedida automaticamente.", "LabelAutoSleepTimerAutoRewindHelp": "Após o timer terminar, da próxima vez que o item for reproduzido a sua posição será retrocedida automaticamente.",
"LabelAutoSleepTimerHelp": "Ao reproduzir uma mídia entre as horas especificadas como inicío e fim, um timer será iniciado automaticamente.", "LabelAutoSleepTimerHelp": "Ao reproduzir uma mídia entre as horas especificadas como inicío e fim, um timer será iniciado automaticamente.",
"LabelBooks": "Livros", "LabelBooks": "Livros",
"LabelByAuthor": "por {0}",
"LabelChapterTrack": "Trilha do Capítulo", "LabelChapterTrack": "Trilha do Capítulo",
"LabelChapters": "Capítulos", "LabelChapters": "Capítulos",
"LabelClosePlayer": "Fechar Reprodutor", "LabelClosePlayer": "Fechar Reprodutor",
@@ -137,8 +140,8 @@
"LabelDownloadUsingCellular": "Download usando o celular", "LabelDownloadUsingCellular": "Download usando o celular",
"LabelDownloaded": "Download realizado", "LabelDownloaded": "Download realizado",
"LabelDuration": "Duração", "LabelDuration": "Duração",
"LabelEbook": "Ebook", "LabelEbook": "E-book",
"LabelEbooks": "Ebooks", "LabelEbooks": "E-books",
"LabelEnable": "Habilitar", "LabelEnable": "Habilitar",
"LabelEnableMp3IndexSeeking": "Ativar busca por índice mp3", "LabelEnableMp3IndexSeeking": "Ativar busca por índice mp3",
"LabelEnableMp3IndexSeekingHelp": "Esta configuração só deve ser ativada se a busca em arquivos mp3 não estiver funcionando corretamente. Buscas incorretas são normalmente causadas por arquivos MP3 com bitrate variável (VBR). Esta configuração forçará a busca por índice, que é uma consulta do tempo-para-byte, elaborado durante a leitura do arquivo. Em alguns casos, com arquivos MP3 maiores, haverá um atraso ao buscar perto do final do arquivo.", "LabelEnableMp3IndexSeekingHelp": "Esta configuração só deve ser ativada se a busca em arquivos mp3 não estiver funcionando corretamente. Buscas incorretas são normalmente causadas por arquivos MP3 com bitrate variável (VBR). Esta configuração forçará a busca por índice, que é uma consulta do tempo-para-byte, elaborado durante a leitura do arquivo. Em alguns casos, com arquivos MP3 maiores, haverá um atraso ao buscar perto do final do arquivo.",
@@ -155,6 +158,9 @@
"LabelFinished": "Concluído", "LabelFinished": "Concluído",
"LabelFolder": "Pasta", "LabelFolder": "Pasta",
"LabelFontBoldness": "Intensidade do negrito", "LabelFontBoldness": "Intensidade do negrito",
"LabelFontFamily": "Família de fontes",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Escala de fonte", "LabelFontScale": "Escala de fonte",
"LabelGenre": "Gênero", "LabelGenre": "Gênero",
"LabelGenres": "Gêneros", "LabelGenres": "Gêneros",
@@ -174,6 +180,9 @@
"LabelLayout": "Layout", "LabelLayout": "Layout",
"LabelLayoutAuto": "Automático", "LabelLayoutAuto": "Automático",
"LabelLayoutSinglePage": "Uma página", "LabelLayoutSinglePage": "Uma página",
"LabelLibrarySortByProgress": "Progresso: Ultima Atualização",
"LabelLibrarySortByProgressFinished": "Progresso: Terminado",
"LabelLibrarySortByProgressStarted": "Progresso: Iniciado",
"LabelLight": "Leve", "LabelLight": "Leve",
"LabelLineSpacing": "Espaçamento entre linhas", "LabelLineSpacing": "Espaçamento entre linhas",
"LabelListenAgain": "Escutar novamente", "LabelListenAgain": "Escutar novamente",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Proporcionalizar Tempo Decorrido com a Velocidade", "LabelScaleElapsedTimeBySpeed": "Proporcionalizar Tempo Decorrido com a Velocidade",
"LabelSeason": "Temporada", "LabelSeason": "Temporada",
"LabelSelectADevice": "Selecione um dispositivo", "LabelSelectADevice": "Selecione um dispositivo",
"LabelSelectMediaType": "Escolha o tipo da media",
"LabelSequenceAscending": "Sequência Ascendente", "LabelSequenceAscending": "Sequência Ascendente",
"LabelSequenceDescending": "Sequence Descendente", "LabelSequenceDescending": "Sequence Descendente",
"LabelSeries": "Série", "LabelSeries": "Série",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Servidor Audiobookshelf não conectado", "MessageAudiobookshelfServerNotConnected": "Servidor Audiobookshelf não conectado",
"MessageAudiobookshelfServerRequired": "<strong>Importante!</strong> Esse aplicativo foi desenvolvido para funcionar com um servidor Audiobookshelf que você ou algum conhecido seu está mantendo. Esse aplicativo não oferece qualquer conteúdo.", "MessageAudiobookshelfServerRequired": "<strong>Importante!</strong> Esse aplicativo foi desenvolvido para funcionar com um servidor Audiobookshelf que você ou algum conhecido seu está mantendo. Esse aplicativo não oferece qualquer conteúdo.",
"MessageBookshelfEmpty": "Estante vazia", "MessageBookshelfEmpty": "Estante vazia",
"MessageConfirmAppExit": "Deseja sair do aplicativo?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Tem certeza de que deseja limpar a fila de downloads de episódios?",
"MessageConfirmDeleteLocalEpisode": "Remover episódio local \"{0}\" do seu dispositivo? O arquivo no servidor não será afetado.", "MessageConfirmDeleteLocalEpisode": "Remover episódio local \"{0}\" do seu dispositivo? O arquivo no servidor não será afetado.",
"MessageConfirmDeleteLocalFiles": "Remover arquivos locais deste item do seu dispositivo? Os arquivos no servidor e o seu progresso não serão afetados.", "MessageConfirmDeleteLocalFiles": "Remover arquivos locais deste item do seu dispositivo? Os arquivos no servidor e o seu progresso não serão afetados.",
"MessageConfirmDeleteServerConfig": "Remover esta configuração do servidor?",
"MessageConfirmDeleteServerEpisode": "Tem certeza de que deseja excluir o episódio \"{0}\" do servidor?\nAviso: Isso excluirá o arquivo de áudio.",
"MessageConfirmDisableAutoTimer": "Tem certeza de que deseja desabilitar o timer automático para o resto do dia? O timer será re-ativado ao final do período do timer automático ou se você reiniciar o aplicativo.", "MessageConfirmDisableAutoTimer": "Tem certeza de que deseja desabilitar o timer automático para o resto do dia? O timer será re-ativado ao final do período do timer automático ou se você reiniciar o aplicativo.",
"MessageConfirmDiscardProgress": "Tem certeza de que deseja resetar o seu progresso?", "MessageConfirmDiscardProgress": "Tem certeza de que deseja resetar o seu progresso?",
"MessageConfirmDownloadUsingCellular": "Você irá fazer o download usando dados do celular. Isso pode incidir em cobrança por sua operadora. Deseja continuar?", "MessageConfirmDownloadUsingCellular": "Você irá fazer o download usando dados do celular. Isso pode incidir em cobrança por sua operadora. Deseja continuar?",
"MessageConfirmMarkAsFinished": "Tem certeza de que deseja marcar esse item como concluído?", "MessageConfirmMarkAsFinished": "Tem certeza de que deseja marcar esse item como concluído?",
"MessageConfirmPlaybackTime": "Iniciar reprodução para \"{0}\" em {1}?",
"MessageConfirmRemoveBookmark": "Tem certeza de que deseja remover o marcador?", "MessageConfirmRemoveBookmark": "Tem certeza de que deseja remover o marcador?",
"MessageConfirmStreamingUsingCellular": "Você irá fazer o streaming usando dados do celular. Isso pode incidir em cobrança por sua operadora. Deseja continuar?", "MessageConfirmStreamingUsingCellular": "Você irá fazer o streaming usando dados do celular. Isso pode incidir em cobrança por sua operadora. Deseja continuar?",
"MessageDiscardProgress": "Descartar Progresso", "MessageDiscardProgress": "Descartar Progresso",
@@ -298,7 +313,7 @@
"MessageDownloading": "Realizando o download...", "MessageDownloading": "Realizando o download...",
"MessageDownloadingEpisode": "Realizando o downloading do episódio", "MessageDownloadingEpisode": "Realizando o downloading do episódio",
"MessageEpisodesQueuedForDownload": "{0} Episódio(s) na fila de download", "MessageEpisodesQueuedForDownload": "{0} Episódio(s) na fila de download",
"MessageFailedToRefreshToken": "Falha ao atualizar o token, faça o login novamente.", "MessageFailedToRefreshToken": "Falha ao atualizar o token, faça o login novamente",
"MessageFeedURLWillBe": "URL do Feed será {0}", "MessageFeedURLWillBe": "URL do Feed será {0}",
"MessageFetching": "Buscando...", "MessageFetching": "Buscando...",
"MessageFollowTheProjectOnGithub": "Siga este projeto no GitHub", "MessageFollowTheProjectOnGithub": "Siga este projeto no GitHub",
@@ -355,5 +370,7 @@
"ToastPodcastCreateSuccess": "Podcast criado", "ToastPodcastCreateSuccess": "Podcast criado",
"ToastRSSFeedCloseFailed": "Falha ao fechar feed RSS", "ToastRSSFeedCloseFailed": "Falha ao fechar feed RSS",
"ToastRSSFeedCloseSuccess": "Feed RSS fechado", "ToastRSSFeedCloseSuccess": "Feed RSS fechado",
"ToastStreamingNotAllowedOnCellular": "Streaming não é permitido usando dados do celular" "ToastStreamingNotAllowedOnCellular": "Streaming não é permitido usando dados do celular",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+21 -4
View File
@@ -23,7 +23,7 @@
"ButtonGoToWebClient": "Deschide în browse", "ButtonGoToWebClient": "Deschide în browse",
"ButtonHistory": "Istoric", "ButtonHistory": "Istoric",
"ButtonHome": "Acasă", "ButtonHome": "Acasă",
"ButtonIssues": "Erori", "ButtonIssues": "Probleme",
"ButtonLatest": "Noutăți", "ButtonLatest": "Noutăți",
"ButtonLibrary": "Bibliotecă", "ButtonLibrary": "Bibliotecă",
"ButtonLocalMedia": "Media locală", "ButtonLocalMedia": "Media locală",
@@ -40,7 +40,7 @@
"ButtonPlayEpisode": "Redă episodul", "ButtonPlayEpisode": "Redă episodul",
"ButtonPlaylists": "Liste", "ButtonPlaylists": "Liste",
"ButtonRead": "Citește", "ButtonRead": "Citește",
"ButtonReadLess": "Mai puțin", "ButtonReadLess": "Citește Mai Puțin",
"ButtonReadMore": "Afișează mai mult", "ButtonReadMore": "Afișează mai mult",
"ButtonRemove": "Elimină", "ButtonRemove": "Elimină",
"ButtonRemoveFromServer": "Elimină de pe server", "ButtonRemoveFromServer": "Elimină de pe server",
@@ -63,6 +63,7 @@
"HeaderChapters": "Capitole", "HeaderChapters": "Capitole",
"HeaderCollection": "Colecție", "HeaderCollection": "Colecție",
"HeaderCollectionItems": "Conținutul colecției", "HeaderCollectionItems": "Conținutul colecției",
"HeaderConfirm": "Confirmă",
"HeaderConnectionStatus": "Starea conexiunii", "HeaderConnectionStatus": "Starea conexiunii",
"HeaderDataSettings": "Configurare date", "HeaderDataSettings": "Configurare date",
"HeaderDetails": "Detalii", "HeaderDetails": "Detalii",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Sesiuni recente", "HeaderStatsRecentSessions": "Sesiuni recente",
"HeaderTableOfContents": "Cuprins", "HeaderTableOfContents": "Cuprins",
"HeaderUserInterfaceSettings": "Setări interfață", "HeaderUserInterfaceSettings": "Setări interfață",
"HeaderWelcome": "Bun venit, <strong>{0}</strong>",
"HeaderYourStats": "Progresul tău", "HeaderYourStats": "Progresul tău",
"LabelAddToPlaylist": "Adaugă în listă", "LabelAddToPlaylist": "Adaugă în listă",
"LabelAddedAt": "Adăugat la", "LabelAddedAt": "Adăugat la",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Când timerul de somn se termină, la reluarea redării vei fi mutat automat puțin înapoi.", "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.", "LabelAutoSleepTimerHelp": "Când redai media între orele specificate, se va porni automat timerul de somn.",
"LabelBooks": "Cărți", "LabelBooks": "Cărți",
"LabelByAuthor": "de {0}",
"LabelChapterTrack": "Marcaje capitole", "LabelChapterTrack": "Marcaje capitole",
"LabelChapters": "Capitole", "LabelChapters": "Capitole",
"LabelClosePlayer": "Închide playerul", "LabelClosePlayer": "Închide playerul",
@@ -147,7 +150,7 @@
"LabelEndTime": "Oră de sfârșit", "LabelEndTime": "Oră de sfârșit",
"LabelEpisode": "Episod", "LabelEpisode": "Episod",
"LabelExplicit": "Explicit", "LabelExplicit": "Explicit",
"LabelFeedURL": "URL flux", "LabelFeedURL": "Flux URL",
"LabelFile": "Fișier", "LabelFile": "Fișier",
"LabelFileBirthtime": "Data creării fișierului", "LabelFileBirthtime": "Data creării fișierului",
"LabelFileModified": "Fișier modificat", "LabelFileModified": "Fișier modificat",
@@ -155,6 +158,9 @@
"LabelFinished": "Finalizat", "LabelFinished": "Finalizat",
"LabelFolder": "Dosar", "LabelFolder": "Dosar",
"LabelFontBoldness": "Grosimea fontului", "LabelFontBoldness": "Grosimea fontului",
"LabelFontFamily": "Familia Fontului",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Mărimea fontului", "LabelFontScale": "Mărimea fontului",
"LabelGenre": "Gen", "LabelGenre": "Gen",
"LabelGenres": "Genuri", "LabelGenres": "Genuri",
@@ -174,6 +180,9 @@
"LabelLayout": "Aspect", "LabelLayout": "Aspect",
"LabelLayoutAuto": "Automat", "LabelLayoutAuto": "Automat",
"LabelLayoutSinglePage": "Pagină unică", "LabelLayoutSinglePage": "Pagină unică",
"LabelLibrarySortByProgress": "Progres: Ultima Actualizare",
"LabelLibrarySortByProgressFinished": "Progres: Finalizat",
"LabelLibrarySortByProgressStarted": "Progres: Început",
"LabelLight": "Deschis", "LabelLight": "Deschis",
"LabelLineSpacing": "Spațiere între rânduri", "LabelLineSpacing": "Spațiere între rânduri",
"LabelListenAgain": "Ascultă din nou", "LabelListenAgain": "Ascultă din nou",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Ajustează timpul scurs după viteză", "LabelScaleElapsedTimeBySpeed": "Ajustează timpul scurs după viteză",
"LabelSeason": "Sezon", "LabelSeason": "Sezon",
"LabelSelectADevice": "Alege un dispozitiv", "LabelSelectADevice": "Alege un dispozitiv",
"LabelSelectMediaType": "Alege tipul suportului media",
"LabelSequenceAscending": "Ordine crescătoare", "LabelSequenceAscending": "Ordine crescătoare",
"LabelSequenceDescending": "Ordine descrescătoare", "LabelSequenceDescending": "Ordine descrescătoare",
"LabelSeries": "Serii", "LabelSeries": "Serii",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Serverul Audiobookshelf nu este conectat", "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.", "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", "MessageBookshelfEmpty": "Raftul este gol",
"MessageConfirmAppExit": "Doriți să ieșiti din aplicație?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Sunteți sigur că doriți să goliți coada de descărcări a episoadelor?",
"MessageConfirmDeleteLocalEpisode": "Ștergi episodul local „{0}” de pe dispozitiv? Fișierul de pe server nu va fi afectat.", "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.", "MessageConfirmDeleteLocalFiles": "Ștergi fișierele locale ale acestui element de pe dispozitiv? Fișierele de pe server și progresul nu vor fi afectate.",
"MessageConfirmDeleteServerConfig": "Stergeți această configurație a serverului?",
"MessageConfirmDeleteServerEpisode": "Sunteți sigur că doriți să ștergeți episodul \"{0}\" de pe server?\nAtenție: Această acțiune va șterge fișierul audio.",
"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.", "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?", "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?", "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?", "MessageConfirmMarkAsFinished": "Sigur vrei să marchezi acest element ca finalizat?",
"MessageConfirmPlaybackTime": "Porniți redarea pentru \"{0}\" la \"{1}\"?",
"MessageConfirmRemoveBookmark": "Sigur vrei să ștergi semnul de carte?", "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?", "MessageConfirmStreamingUsingCellular": "Urmează să redai online folosind date mobile. Acest lucru poate genera costuri suplimentare. Vrei să continui?",
"MessageDiscardProgress": "Resetează progresul", "MessageDiscardProgress": "Resetează progresul",
@@ -355,5 +370,7 @@
"ToastPodcastCreateSuccess": "Podcast creat cu succes", "ToastPodcastCreateSuccess": "Podcast creat cu succes",
"ToastRSSFeedCloseFailed": "Nu s-a putut închide fluxul RSS", "ToastRSSFeedCloseFailed": "Nu s-a putut închide fluxul RSS",
"ToastRSSFeedCloseSuccess": "Flux RSS închis", "ToastRSSFeedCloseSuccess": "Flux RSS închis",
"ToastStreamingNotAllowedOnCellular": "Redarea online nu este permisă pe date mobile" "ToastStreamingNotAllowedOnCellular": "Redarea online nu este permisă pe date mobile",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+26 -2
View File
@@ -1,6 +1,7 @@
{ {
"ButtonAdd": "Добавить", "ButtonAdd": "Добавить",
"ButtonAddNewServer": "Добавить новый сервер", "ButtonAddNewServer": "Добавить новый сервер",
"ButtonAllow": "Разрешить",
"ButtonAuthors": "Авторы", "ButtonAuthors": "Авторы",
"ButtonBack": "Назад", "ButtonBack": "Назад",
"ButtonCancel": "Отмена", "ButtonCancel": "Отмена",
@@ -63,6 +64,7 @@
"HeaderChapters": "Главы", "HeaderChapters": "Главы",
"HeaderCollection": "Коллекция", "HeaderCollection": "Коллекция",
"HeaderCollectionItems": "Элементы коллекции", "HeaderCollectionItems": "Элементы коллекции",
"HeaderConfirm": "Подтвердить",
"HeaderConnectionStatus": "Состояние подключения", "HeaderConnectionStatus": "Состояние подключения",
"HeaderDataSettings": "Настройки данных", "HeaderDataSettings": "Настройки данных",
"HeaderDetails": "Подробности", "HeaderDetails": "Подробности",
@@ -91,6 +93,7 @@
"HeaderStatsRecentSessions": "Последние сеансы", "HeaderStatsRecentSessions": "Последние сеансы",
"HeaderTableOfContents": "Содержание", "HeaderTableOfContents": "Содержание",
"HeaderUserInterfaceSettings": "Настройки интерфейса", "HeaderUserInterfaceSettings": "Настройки интерфейса",
"HeaderWelcome": "Добро пожаловать,<strong>{0}</strong>",
"HeaderYourStats": "Ваша статистика", "HeaderYourStats": "Ваша статистика",
"LabelAddToPlaylist": "Добавить в плейлист", "LabelAddToPlaylist": "Добавить в плейлист",
"LabelAddedAt": "Дата добавления", "LabelAddedAt": "Дата добавления",
@@ -113,6 +116,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Когда таймер сна закончится, то позиция воспроизведения будет отмотана назад.", "LabelAutoSleepTimerAutoRewindHelp": "Когда таймер сна закончится, то позиция воспроизведения будет отмотана назад.",
"LabelAutoSleepTimerHelp": "Если медиа воспроизводится между указанными началом и окончанием, таймер сна будет включаться автоматически.", "LabelAutoSleepTimerHelp": "Если медиа воспроизводится между указанными началом и окончанием, таймер сна будет включаться автоматически.",
"LabelBooks": "Книги", "LabelBooks": "Книги",
"LabelByAuthor": "от {0}",
"LabelChapterTrack": "Трек главы", "LabelChapterTrack": "Трек главы",
"LabelChapters": "Главы", "LabelChapters": "Главы",
"LabelClosePlayer": "Закрыть проигрыватель", "LabelClosePlayer": "Закрыть проигрыватель",
@@ -155,11 +159,14 @@
"LabelFinished": "Закончен", "LabelFinished": "Закончен",
"LabelFolder": "Папка", "LabelFolder": "Папка",
"LabelFontBoldness": "Жирность шрифта", "LabelFontBoldness": "Жирность шрифта",
"LabelFontFamily": "Семейство шрифта",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Масштаб шрифта", "LabelFontScale": "Масштаб шрифта",
"LabelGenre": "Жанр", "LabelGenre": "Жанр",
"LabelGenres": "Жанры", "LabelGenres": "Жанры",
"LabelHapticFeedback": "Тактильная отдача", "LabelHapticFeedback": "Тактильная отдача",
"LabelHasEbook": "Есть e-книга", "LabelHasEbook": "Есть электронная книга",
"LabelHasSupplementaryEbook": "Есть дополнительная e-книга", "LabelHasSupplementaryEbook": "Есть дополнительная e-книга",
"LabelHeavy": "Тяжелый", "LabelHeavy": "Тяжелый",
"LabelHigh": "Сильно", "LabelHigh": "Сильно",
@@ -174,6 +181,9 @@
"LabelLayout": "Макет", "LabelLayout": "Макет",
"LabelLayoutAuto": "Авто", "LabelLayoutAuto": "Авто",
"LabelLayoutSinglePage": "Одна страница", "LabelLayoutSinglePage": "Одна страница",
"LabelLibrarySortByProgress": "Прогресс: Последнее обновление",
"LabelLibrarySortByProgressFinished": "Прогресс: Закончена",
"LabelLibrarySortByProgressStarted": "Прогресс: Начата",
"LabelLight": "Подсветка", "LabelLight": "Подсветка",
"LabelLineSpacing": "Межстрочный интервал", "LabelLineSpacing": "Межстрочный интервал",
"LabelListenAgain": "Послушать снова", "LabelListenAgain": "Послушать снова",
@@ -233,6 +243,7 @@
"LabelScaleElapsedTimeBySpeed": "Масштабирование затраченного времени по скорости", "LabelScaleElapsedTimeBySpeed": "Масштабирование затраченного времени по скорости",
"LabelSeason": "Сезон", "LabelSeason": "Сезон",
"LabelSelectADevice": "Выбор девайса", "LabelSelectADevice": "Выбор девайса",
"LabelSelectMediaType": "Выберите тип медиа",
"LabelSequenceAscending": "Последовательность по возрастанию", "LabelSequenceAscending": "Последовательность по возрастанию",
"LabelSequenceDescending": "Последовательность по убыванию", "LabelSequenceDescending": "Последовательность по убыванию",
"LabelSeries": "Серия", "LabelSeries": "Серия",
@@ -285,18 +296,24 @@
"MessageAudiobookshelfServerNotConnected": "Сервер Audiobookshelf не подключен", "MessageAudiobookshelfServerNotConnected": "Сервер Audiobookshelf не подключен",
"MessageAudiobookshelfServerRequired": "<strong>Важно!</strong> Это приложение предназначено для работы с сервером Audiobookshelf, который размещаете вы или кто-то из ваших знакомых. Это приложение не предоставляет никакого контента.", "MessageAudiobookshelfServerRequired": "<strong>Важно!</strong> Это приложение предназначено для работы с сервером Audiobookshelf, который размещаете вы или кто-то из ваших знакомых. Это приложение не предоставляет никакого контента.",
"MessageBookshelfEmpty": "Книжная полка пуста", "MessageBookshelfEmpty": "Книжная полка пуста",
"MessageConfirmAppExit": "Выйти из приложения?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Вы точно хотите очистить очередь загрузки выпусков?",
"MessageConfirmDeleteLocalEpisode": "Удалить локальный выпуск \"{0}\" с вашего устройства? Файл на сервере не будет затронут.", "MessageConfirmDeleteLocalEpisode": "Удалить локальный выпуск \"{0}\" с вашего устройства? Файл на сервере не будет затронут.",
"MessageConfirmDeleteLocalFiles": "Удалить локальные файлы этого элемента с вашего устройства? Это не повлияет на файлы на сервере и ваш прогресс.", "MessageConfirmDeleteLocalFiles": "Удалить локальные файлы этого элемента с вашего устройства? Это не повлияет на файлы на сервере и ваш прогресс.",
"MessageConfirmDeleteServerConfig": "Удалить эту конфигурацию сервера?",
"MessageConfirmDeleteServerEpisode": "Вы точно хотите удалить выпуск \"{0}\" с сервера?\nВнимание! Это физически удалит аудиофайл.",
"MessageConfirmDisableAutoTimer": "Вы уверены, что хотите отключить автоматический таймер до конца сегодняшнего дня? Таймер будет снова включен по истечении этого периода автоматического отключения или если вы перезапустите приложение.", "MessageConfirmDisableAutoTimer": "Вы уверены, что хотите отключить автоматический таймер до конца сегодняшнего дня? Таймер будет снова включен по истечении этого периода автоматического отключения или если вы перезапустите приложение.",
"MessageConfirmDiscardProgress": "Вы уверены, что хотите сбросить свой прогресс?", "MessageConfirmDiscardProgress": "Вы уверены, что хотите сбросить свой прогресс?",
"MessageConfirmDownloadUsingCellular": "Вы собираетесь выполнить загрузку с помощью сотовых данных. Это может включать плату за передачу данных оператором. Хотите продолжить?", "MessageConfirmDownloadUsingCellular": "Вы собираетесь выполнить загрузку с помощью сотовых данных. Это может включать плату за передачу данных оператором. Хотите продолжить?",
"MessageConfirmMarkAsFinished": "Вы уверены, что хотите пометить этот элемент как завершенный?", "MessageConfirmMarkAsFinished": "Вы уверены, что хотите пометить этот элемент как завершенный?",
"MessageConfirmPlaybackTime": "Начать воспроизведение \"{0}\" с {1}?",
"MessageConfirmRemoveBookmark": "Вы уверены, что хотите удалить закладку?", "MessageConfirmRemoveBookmark": "Вы уверены, что хотите удалить закладку?",
"MessageConfirmStreamingUsingCellular": "Вы собираетесь вести потоковую передачу с использованием сотовых данных. Это может включать плату за передачу данных оператором. Хотите продолжить?", "MessageConfirmStreamingUsingCellular": "Вы собираетесь вести потоковую передачу с использованием сотовых данных. Это может включать плату за передачу данных оператором. Хотите продолжить?",
"MessageDiscardProgress": "Отбросить прогресс", "MessageDiscardProgress": "Отбросить прогресс",
"MessageDownloadCompleteProcessing": "Загрузка завершена. Обработка...", "MessageDownloadCompleteProcessing": "Загрузка завершена. Обработка...",
"MessageDownloading": "Загрузка...", "MessageDownloading": "Загрузка...",
"MessageDownloadingEpisode": "Скачивание выпуска", "MessageDownloadingEpisode": "Скачивание выпуска",
"MessageDownloadingFile": "Загрузка {0}",
"MessageEpisodesQueuedForDownload": "{0} выпуск(ов) запланировано для скачивания", "MessageEpisodesQueuedForDownload": "{0} выпуск(ов) запланировано для скачивания",
"MessageFailedToRefreshToken": "Не удалось обновить токен, требуется повторный вход в систему", "MessageFailedToRefreshToken": "Не удалось обновить токен, требуется повторный вход в систему",
"MessageFeedURLWillBe": "URL-адрес ленты будет {0}", "MessageFeedURLWillBe": "URL-адрес ленты будет {0}",
@@ -332,6 +349,7 @@
"MessageOldServerConnectionWarning": "Конфигурация подключения к серверу использует старый идентификатор пользователя. Удалите и повторно добавьте это подключение к серверу.", "MessageOldServerConnectionWarning": "Конфигурация подключения к серверу использует старый идентификатор пользователя. Удалите и повторно добавьте это подключение к серверу.",
"MessageOldServerConnectionWarningHelp": "Первоначально вы настроили подключение к этому серверу до миграции базы данных в версии 2.3.0, выпущенной в июне 2023 года. Будущее обновление сервера уберет возможность входа с помощью этого старого подключения. Удалите существующее подключение к серверу и подключитесь снова (используя тот же адрес сервера и учетные данные). Если на этом устройстве есть загруженные медиафайлы, их необходимо будет загрузить снова для синхронизации с сервером.", "MessageOldServerConnectionWarningHelp": "Первоначально вы настроили подключение к этому серверу до миграции базы данных в версии 2.3.0, выпущенной в июне 2023 года. Будущее обновление сервера уберет возможность входа с помощью этого старого подключения. Удалите существующее подключение к серверу и подключитесь снова (используя тот же адрес сервера и учетные данные). Если на этом устройстве есть загруженные медиафайлы, их необходимо будет загрузить снова для синхронизации с сервером.",
"MessagePodcastSearchField": "Введите поисковый запрос или URL-адрес RSS-ленты", "MessagePodcastSearchField": "Введите поисковый запрос или URL-адрес RSS-ленты",
"MessagePreparingDownloads": "Подготовка загрузок",
"MessageProgressSyncFailed": "Последняя попытка сообщить о вашем прогрессе прослушивания на сервер не удалась. Запросы на синхронизацию прогресса будут продолжаться каждые 15 секунд в течении 1 минуты во время воспроизведения медиа.", "MessageProgressSyncFailed": "Последняя попытка сообщить о вашем прогрессе прослушивания на сервер не удалась. Запросы на синхронизацию прогресса будут продолжаться каждые 15 секунд в течении 1 минуты во время воспроизведения медиа.",
"MessageReportBugsAndContribute": "Сообщайте об ошибках, запрашивайте функции и вносите свой вклад на", "MessageReportBugsAndContribute": "Сообщайте об ошибках, запрашивайте функции и вносите свой вклад на",
"MessageSeriesAlreadyDownloaded": "Вы уже скачали все книги из этой серии.", "MessageSeriesAlreadyDownloaded": "Вы уже скачали все книги из этой серии.",
@@ -342,6 +360,10 @@
"MessageSocketConnectedOverUnmeteredCellular": "Сокет подключен по безлимитной сотовой связи", "MessageSocketConnectedOverUnmeteredCellular": "Сокет подключен по безлимитной сотовой связи",
"MessageSocketConnectedOverUnmeteredWifi": "Сокет подключен через безлимитный Wi-Fi", "MessageSocketConnectedOverUnmeteredWifi": "Сокет подключен через безлимитный Wi-Fi",
"MessageSocketNotConnected": "Сокет не подключен", "MessageSocketNotConnected": "Сокет не подключен",
"MessageStorageAccessDenied": "Доступ запрещен",
"MessageStoragePermissionDenied": "Недостаточно прав",
"MessageStorageWriteAccessRequired": "У вас нет доступа на запись в эту папку. Хотите предоставить доступ?",
"MessageWaitingForAvailableStorage": "Ожидание доступного хранилища",
"NoteRSSFeedPodcastAppsHttps": "Предупреждение: Большинству приложений подкастов потребуется, чтобы URL-адрес RSS-ленты использовал HTTPS", "NoteRSSFeedPodcastAppsHttps": "Предупреждение: Большинству приложений подкастов потребуется, чтобы URL-адрес RSS-ленты использовал HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Предупреждение: 1 или более выпусков не имеют даты публикации. Некоторые приложения для подкастов требуют этого.", "NoteRSSFeedPodcastAppsPubDate": "Предупреждение: 1 или более выпусков не имеют даты публикации. Некоторые приложения для подкастов требуют этого.",
"ToastBookmarkCreateFailed": "Не удалось создать закладку", "ToastBookmarkCreateFailed": "Не удалось создать закладку",
@@ -355,5 +377,7 @@
"ToastPodcastCreateSuccess": "Подкаст успешно создан", "ToastPodcastCreateSuccess": "Подкаст успешно создан",
"ToastRSSFeedCloseFailed": "Не удалось закрыть RSS-ленту", "ToastRSSFeedCloseFailed": "Не удалось закрыть RSS-ленту",
"ToastRSSFeedCloseSuccess": "RSS-лента закрыта", "ToastRSSFeedCloseSuccess": "RSS-лента закрыта",
"ToastStreamingNotAllowedOnCellular": "Потоковая передача данных по сотовой сети запрещена" "ToastStreamingNotAllowedOnCellular": "Потоковая передача данных по сотовой сети запрещена",
"UnitMinutesShort": "{0}м",
"UnitSecondsShort": "{0}с"
} }
+20 -3
View File
@@ -63,6 +63,7 @@
"HeaderChapters": "Kapitoly", "HeaderChapters": "Kapitoly",
"HeaderCollection": "Zbierky", "HeaderCollection": "Zbierky",
"HeaderCollectionItems": "Položky zbierky", "HeaderCollectionItems": "Položky zbierky",
"HeaderConfirm": "Potvrď",
"HeaderConnectionStatus": "Stav pripojenia", "HeaderConnectionStatus": "Stav pripojenia",
"HeaderDataSettings": "Nastavenie dát", "HeaderDataSettings": "Nastavenie dát",
"HeaderDetails": "Podrobnosti", "HeaderDetails": "Podrobnosti",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Nedávne relácie", "HeaderStatsRecentSessions": "Nedávne relácie",
"HeaderTableOfContents": "Obsah", "HeaderTableOfContents": "Obsah",
"HeaderUserInterfaceSettings": "Nastavenia používateľského rozhrania", "HeaderUserInterfaceSettings": "Nastavenia používateľského rozhrania",
"HeaderWelcome": "Vitajte, <strong>{0}</strong>",
"HeaderYourStats": "Vaše štatistiky", "HeaderYourStats": "Vaše štatistiky",
"LabelAddToPlaylist": "Pridať do playlistu", "LabelAddToPlaylist": "Pridať do playlistu",
"LabelAddedAt": "Pridané", "LabelAddedAt": "Pridané",
@@ -100,7 +102,7 @@
"LabelAlways": "Vždy", "LabelAlways": "Vždy",
"LabelAndroidAutoBrowseLimitForGrouping": "Limit abecedného posunu", "LabelAndroidAutoBrowseLimitForGrouping": "Limit abecedného posunu",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "Nezobrazí možnosť abecedného posunu, ak je počet zobrazených položiek menší ako limit", "LabelAndroidAutoBrowseLimitForGroupingHelp": "Nezobrazí možnosť abecedného posunu, ak je počet zobrazených položiek menší ako limit",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "Poradie kníh série", "LabelAndroidAutoBrowseSeriesSequenceOrder": "Poradie kníh v sérii",
"LabelAskConfirmation": "Požiadať o potvrdenie", "LabelAskConfirmation": "Požiadať o potvrdenie",
"LabelAuthor": "Autor", "LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Meno Priezvisko)", "LabelAuthorFirstLast": "Autor (Meno Priezvisko)",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Keď Auto časovač spánku skončí, pred nasledujúcim prehratím položky sa prehrávanie automaticky posunie.", "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ž.", "LabelAutoSleepTimerHelp": "keď sa prehrávanie začne vo vymedzenom časovom intervale, časovač spánku sa automaticky spustí tiež.",
"LabelBooks": "Knihy", "LabelBooks": "Knihy",
"LabelByAuthor": "od {0}",
"LabelChapterTrack": "Zvuková stopa kapitoly", "LabelChapterTrack": "Zvuková stopa kapitoly",
"LabelChapters": "Kapitoly", "LabelChapters": "Kapitoly",
"LabelClosePlayer": "Zavrieť prehrávač", "LabelClosePlayer": "Zavrieť prehrávač",
@@ -155,6 +158,9 @@
"LabelFinished": "Ukončené", "LabelFinished": "Ukončené",
"LabelFolder": "Priečinok", "LabelFolder": "Priečinok",
"LabelFontBoldness": "Hrúbka písma", "LabelFontBoldness": "Hrúbka písma",
"LabelFontFamily": "písmo",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Veľkosť písma", "LabelFontScale": "Veľkosť písma",
"LabelGenre": "Žáner", "LabelGenre": "Žáner",
"LabelGenres": "Žánre", "LabelGenres": "Žánre",
@@ -174,6 +180,9 @@
"LabelLayout": "Rozloženie", "LabelLayout": "Rozloženie",
"LabelLayoutAuto": "Auto", "LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Jedna stránka", "LabelLayoutSinglePage": "Jedna stránka",
"LabelLibrarySortByProgress": "Stav: Naposledy aktualizované",
"LabelLibrarySortByProgressFinished": "Stav: Dokončené",
"LabelLibrarySortByProgressStarted": "Stav: Začal",
"LabelLight": "Svetlá", "LabelLight": "Svetlá",
"LabelLineSpacing": "Riadkovanie", "LabelLineSpacing": "Riadkovanie",
"LabelListenAgain": "Počúvať znova", "LabelListenAgain": "Počúvať znova",
@@ -215,7 +224,7 @@
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasty", "LabelPodcasts": "Podcasty",
"LabelPreventIndexing": "Zabráni indexácii vašich zdrojov službami iTunes a Google podcast directories", "LabelPreventIndexing": "Zabráni indexácii vašich zdrojov službami iTunes a Google podcast directories",
"LabelProgress": "Stav", "LabelProgress": "Aktuálny stav",
"LabelPubDate": "Dátum publikovania", "LabelPubDate": "Dátum publikovania",
"LabelPublishYear": "Rok vydania", "LabelPublishYear": "Rok vydania",
"LabelPublishedDate": "Vydané {0}", "LabelPublishedDate": "Vydané {0}",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Zmeniť ubehnutý čas podľa rýchlosti", "LabelScaleElapsedTimeBySpeed": "Zmeniť ubehnutý čas podľa rýchlosti",
"LabelSeason": "Sezóna", "LabelSeason": "Sezóna",
"LabelSelectADevice": "Vyberte zariadenie", "LabelSelectADevice": "Vyberte zariadenie",
"LabelSelectMediaType": "Vybrať typ média",
"LabelSequenceAscending": "Vzostupné poradie", "LabelSequenceAscending": "Vzostupné poradie",
"LabelSequenceDescending": "Zostupné poradie", "LabelSequenceDescending": "Zostupné poradie",
"LabelSeries": "Série", "LabelSeries": "Série",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf server nie je pripojený", "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.", "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", "MessageBookshelfEmpty": "Polička je prázdna",
"MessageConfirmAppExit": "Chceli ste ukončiť aplikáciu?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Naozaj chcete vymazať frontu sťahovania epizód?",
"MessageConfirmDeleteLocalEpisode": "Želáte si odstrániť lokálnu epizódu \"{0}\" z vášho zariadenia? Súbor na serveri zostane nezmenený.", "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é.", "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é.",
"MessageConfirmDeleteServerConfig": "Odstrániť túto konfiguráciu servera?",
"MessageConfirmDeleteServerEpisode": "Naozaj chcete odstrániť epizódu „{0}“ zo servera?\nUpozornenie: Týmto sa vymaže aj zvukový súbor.",
"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.", "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?", "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ť?", "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ú?", "MessageConfirmMarkAsFinished": "Ste si istý, že chcete označiť túto položku ako dokončenú?",
"MessageConfirmPlaybackTime": "Spustiť prehrávanie „{0}“ od {1}?",
"MessageConfirmRemoveBookmark": "Ste si istý, že chcete odstrániť túto záložku?", "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ť?", "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", "MessageDiscardProgress": "Odstrániť aktuálny stav počúvania",
@@ -355,5 +370,7 @@
"ToastPodcastCreateSuccess": "Podcast bol vytvorený", "ToastPodcastCreateSuccess": "Podcast bol vytvorený",
"ToastRSSFeedCloseFailed": "Odstránenie RSS zdroja zlyhalo", "ToastRSSFeedCloseFailed": "Odstránenie RSS zdroja zlyhalo",
"ToastRSSFeedCloseSuccess": "RSS zdroj bol odstránený", "ToastRSSFeedCloseSuccess": "RSS zdroj bol odstránený",
"ToastStreamingNotAllowedOnCellular": "Streamovanie cez mobilné dáta nie je povolené" "ToastStreamingNotAllowedOnCellular": "Streamovanie cez mobilné dáta nie je povolené",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+19 -2
View File
@@ -51,7 +51,7 @@
"ButtonSeries": "Serije", "ButtonSeries": "Serije",
"ButtonSetTimer": "Nastavi časovnik", "ButtonSetTimer": "Nastavi časovnik",
"ButtonStream": "Pretakaj", "ButtonStream": "Pretakaj",
"ButtonSubmit": "Potrdi", "ButtonSubmit": "Pošlji",
"ButtonSwitchServerUser": "Preklopi strežnik/uporabnik", "ButtonSwitchServerUser": "Preklopi strežnik/uporabnik",
"ButtonUnmaskServerAddress": "Odkrij naslov strežnika", "ButtonUnmaskServerAddress": "Odkrij naslov strežnika",
"ButtonUserStats": "Uporabniška statistika", "ButtonUserStats": "Uporabniška statistika",
@@ -63,6 +63,7 @@
"HeaderChapters": "Poglavja", "HeaderChapters": "Poglavja",
"HeaderCollection": "Zbirka", "HeaderCollection": "Zbirka",
"HeaderCollectionItems": "Elementi zbirke", "HeaderCollectionItems": "Elementi zbirke",
"HeaderConfirm": "Potrdi",
"HeaderConnectionStatus": "Stanje povezave", "HeaderConnectionStatus": "Stanje povezave",
"HeaderDataSettings": "Podatkovne nastavitve", "HeaderDataSettings": "Podatkovne nastavitve",
"HeaderDetails": "Podrobnosti", "HeaderDetails": "Podrobnosti",
@@ -91,6 +92,7 @@
"HeaderStatsRecentSessions": "Nedavne seje", "HeaderStatsRecentSessions": "Nedavne seje",
"HeaderTableOfContents": "Kazalo", "HeaderTableOfContents": "Kazalo",
"HeaderUserInterfaceSettings": "Nastavitve uporabniškega vmesnika", "HeaderUserInterfaceSettings": "Nastavitve uporabniškega vmesnika",
"HeaderWelcome": "Dobrodošel, <strong>{0}</strong>",
"HeaderYourStats": "Tvoja statistika", "HeaderYourStats": "Tvoja statistika",
"LabelAddToPlaylist": "Dodaj na seznam predvajanja", "LabelAddToPlaylist": "Dodaj na seznam predvajanja",
"LabelAddedAt": "Dodano ob", "LabelAddedAt": "Dodano ob",
@@ -113,6 +115,7 @@
"LabelAutoSleepTimerAutoRewindHelp": "Ko se samodejni časovnik za spanje konča, bo ponovno predvajanje elementa samodejno previlo na zadnji položaj.", "LabelAutoSleepTimerAutoRewindHelp": "Ko se samodejni časovnik za spanje konča, bo ponovno predvajanje elementa samodejno previlo na zadnji položaj.",
"LabelAutoSleepTimerHelp": "Pri predvajanju medijev med določenim začetnim in končnim časom se samodejno zažene časovnik spanja.", "LabelAutoSleepTimerHelp": "Pri predvajanju medijev med določenim začetnim in končnim časom se samodejno zažene časovnik spanja.",
"LabelBooks": "knjig", "LabelBooks": "knjig",
"LabelByAuthor": "od {0}",
"LabelChapterTrack": "Posnetek poglavja", "LabelChapterTrack": "Posnetek poglavja",
"LabelChapters": "Poglavja", "LabelChapters": "Poglavja",
"LabelClosePlayer": "Zapri predvajalnik", "LabelClosePlayer": "Zapri predvajalnik",
@@ -155,6 +158,9 @@
"LabelFinished": "Zaključeno", "LabelFinished": "Zaključeno",
"LabelFolder": "Mapa", "LabelFolder": "Mapa",
"LabelFontBoldness": "Krepkost pisave", "LabelFontBoldness": "Krepkost pisave",
"LabelFontFamily": "Družina pisav",
"LabelFontFamilySans": "Sans",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Merilo pisave", "LabelFontScale": "Merilo pisave",
"LabelGenre": "Žanr", "LabelGenre": "Žanr",
"LabelGenres": "Žanri", "LabelGenres": "Žanri",
@@ -174,6 +180,9 @@
"LabelLayout": "Postavitev", "LabelLayout": "Postavitev",
"LabelLayoutAuto": "Auto", "LabelLayoutAuto": "Auto",
"LabelLayoutSinglePage": "Ena stran", "LabelLayoutSinglePage": "Ena stran",
"LabelLibrarySortByProgress": "Napredek: Zadnja posodobitev",
"LabelLibrarySortByProgressFinished": "Napredek: Končano",
"LabelLibrarySortByProgressStarted": "Napredek: Začelo se je",
"LabelLight": "Svetlo", "LabelLight": "Svetlo",
"LabelLineSpacing": "Vrstični razmak", "LabelLineSpacing": "Vrstični razmak",
"LabelListenAgain": "Poslušaj znova", "LabelListenAgain": "Poslušaj znova",
@@ -233,6 +242,7 @@
"LabelScaleElapsedTimeBySpeed": "Meri pretečeni čas glede na hitrost", "LabelScaleElapsedTimeBySpeed": "Meri pretečeni čas glede na hitrost",
"LabelSeason": "Sezona", "LabelSeason": "Sezona",
"LabelSelectADevice": "Izberite napravo", "LabelSelectADevice": "Izberite napravo",
"LabelSelectMediaType": "Izberite vrsto medija",
"LabelSequenceAscending": "Zaporedje naraščajoče", "LabelSequenceAscending": "Zaporedje naraščajoče",
"LabelSequenceDescending": "Zaporedje padajoče", "LabelSequenceDescending": "Zaporedje padajoče",
"LabelSeries": "Serije", "LabelSeries": "Serije",
@@ -285,12 +295,17 @@
"MessageAudiobookshelfServerNotConnected": "Strežnik Audiobookshelf ni povezan", "MessageAudiobookshelfServerNotConnected": "Strežnik Audiobookshelf ni povezan",
"MessageAudiobookshelfServerRequired": "<strong>Pomembno!</strong> Ta aplikacija je zasnovana za delo s strežnikom Audiobookshelf, ki ga gostite vi ali nekdo, ki ga poznate. Ta aplikacija ne ponuja nobene vsebine.", "MessageAudiobookshelfServerRequired": "<strong>Pomembno!</strong> Ta aplikacija je zasnovana za delo s strežnikom Audiobookshelf, ki ga gostite vi ali nekdo, ki ga poznate. Ta aplikacija ne ponuja nobene vsebine.",
"MessageBookshelfEmpty": "Knjižna polica prazna", "MessageBookshelfEmpty": "Knjižna polica prazna",
"MessageConfirmAppExit": "Ste želeli zapustiti aplikacijo?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Ali ste prepričani, da želite počistiti čakalno vrsto za prenos epizod?",
"MessageConfirmDeleteLocalEpisode": "Ali želite odstraniti lokalno epizodo \"{0}\" iz svoje naprave? Datoteka na strežniku ne bo prizadeta.", "MessageConfirmDeleteLocalEpisode": "Ali želite odstraniti lokalno epizodo \"{0}\" iz svoje naprave? Datoteka na strežniku ne bo prizadeta.",
"MessageConfirmDeleteLocalFiles": "Želite odstraniti lokalne datoteke tega elementa iz naprave? Datoteke na strežniku in vaš napredek ne bodo prizadeti.", "MessageConfirmDeleteLocalFiles": "Želite odstraniti lokalne datoteke tega elementa iz naprave? Datoteke na strežniku in vaš napredek ne bodo prizadeti.",
"MessageConfirmDeleteServerConfig": "Odstrani to konfiguracijo strežnika?",
"MessageConfirmDeleteServerEpisode": "Ali ste prepričani, da želite epizodo \"{0}\" izbrisati s strežnika?\nOpozorilo: S tem boste izbrisali zvočno datoteko.",
"MessageConfirmDisableAutoTimer": "Ali si prepričani, da želiš onemogočiti samodejni časovnik do konca današnjega dne? Časovnik bo znova omogočen ob koncu tega obdobja samodejnega preklopa v mirovanje ali če znova zaženeš aplikacijo.", "MessageConfirmDisableAutoTimer": "Ali si prepričani, da želiš onemogočiti samodejni časovnik do konca današnjega dne? Časovnik bo znova omogočen ob koncu tega obdobja samodejnega preklopa v mirovanje ali če znova zaženeš aplikacijo.",
"MessageConfirmDiscardProgress": "Ali ste prepričani, da želite ponastaviti svoj napredek?", "MessageConfirmDiscardProgress": "Ali ste prepričani, da želite ponastaviti svoj napredek?",
"MessageConfirmDownloadUsingCellular": "Prenesli boste z uporabo mobilnih podatkov. To lahko vključuje stroške prenosa podatkov. Želite nadaljevati?", "MessageConfirmDownloadUsingCellular": "Prenesli boste z uporabo mobilnih podatkov. To lahko vključuje stroške prenosa podatkov. Želite nadaljevati?",
"MessageConfirmMarkAsFinished": "Ali ste prepričani, da želite označiti ta element kot dokončan?", "MessageConfirmMarkAsFinished": "Ali ste prepričani, da želite označiti ta element kot dokončan?",
"MessageConfirmPlaybackTime": "Začni predvajanje skladbe \"{0}\" ob {1}?",
"MessageConfirmRemoveBookmark": "Ali ste prepričani, da želite odstraniti zaznamek?", "MessageConfirmRemoveBookmark": "Ali ste prepričani, da želite odstraniti zaznamek?",
"MessageConfirmStreamingUsingCellular": "Pretakali boste prek mobilnih podatkov. To lahko vključuje stroške prenosa podatkov. Želite nadaljevati?", "MessageConfirmStreamingUsingCellular": "Pretakali boste prek mobilnih podatkov. To lahko vključuje stroške prenosa podatkov. Želite nadaljevati?",
"MessageDiscardProgress": "Zavrzi napredek", "MessageDiscardProgress": "Zavrzi napredek",
@@ -355,5 +370,7 @@
"ToastPodcastCreateSuccess": "Podcast je bil uspešno ustvarjen", "ToastPodcastCreateSuccess": "Podcast je bil uspešno ustvarjen",
"ToastRSSFeedCloseFailed": "Vira RSS ni bilo mogoče zapreti", "ToastRSSFeedCloseFailed": "Vira RSS ni bilo mogoče zapreti",
"ToastRSSFeedCloseSuccess": "Vir RSS je bil zaprt", "ToastRSSFeedCloseSuccess": "Vir RSS je bil zaprt",
"ToastStreamingNotAllowedOnCellular": "Pretakanje ni dovoljeno na mobilnih podatkih" "ToastStreamingNotAllowedOnCellular": "Pretakanje ni dovoljeno na mobilnih podatkih",
"UnitMinutesShort": "{0}m",
"UnitSecondsShort": "{0}s"
} }
+200 -183
View File
@@ -4,161 +4,167 @@
"ButtonAuthors": "Författare", "ButtonAuthors": "Författare",
"ButtonBack": "Tillbaka", "ButtonBack": "Tillbaka",
"ButtonCancel": "Avbryt", "ButtonCancel": "Avbryt",
"ButtonCancelTimer": "Inaktivera Timer", "ButtonCancelTimer": "Avbryt timern",
"ButtonClearFilter": "Rensa filter", "ButtonClearFilter": "Rensa filter",
"ButtonClearLogs": "Rensa Loggar", "ButtonClearLogs": "Rensa loggar",
"ButtonCloseFeed": "Stäng flöde", "ButtonCloseFeed": "Stäng flödet",
"ButtonCollections": "Samlingar", "ButtonCollections": "Samlingar",
"ButtonConnect": "Anslut", "ButtonConnect": "Anslut",
"ButtonConnectToServer": "Anslut till Server", "ButtonConnectToServer": "Anslut till server",
"ButtonCreate": "Skapa", "ButtonCreate": "Skapa",
"ButtonCreateBookmark": "Skapa Bokmärke", "ButtonCreateBookmark": "Skapa bokmärke",
"ButtonCreateNewPlaylist": "Skapa Ny Spellista", "ButtonCreateNewPlaylist": "Skapa ny spellista",
"ButtonDelete": "Radera", "ButtonDelete": "Ta bort",
"ButtonDeleteLocalEpisode": "Radera lokal episod", "ButtonDeleteLocalEpisode": "Ta bort lokalt avsnitt",
"ButtonDeleteLocalFile": "Radera lokal fil", "ButtonDeleteLocalFile": "Ta bort lokal fil",
"ButtonDeleteLocalItem": "Radera lokalt objekt", "ButtonDeleteLocalItem": "Ta bort lokalt objekt",
"ButtonDisableAutoTimer": "Inaktivera Automatisk Timer", "ButtonDisableAutoTimer": "Inaktivera automatisk insomningstimer",
"ButtonDisconnect": "Koppla ur", "ButtonDisconnect": "Koppla från",
"ButtonGoToWebClient": "Öppna i webbläsare", "ButtonGoToWebClient": "Öppna i webbläsare",
"ButtonHistory": "Historik", "ButtonHistory": "Historik",
"ButtonHome": "Hem", "ButtonHome": "Startsida",
"ButtonIssues": "Objekt med problem", "ButtonIssues": "Objekt med problem",
"ButtonLatest": "Senaste", "ButtonLatest": "Senaste",
"ButtonLibrary": "Bibliotek", "ButtonLibrary": "Bibliotek",
"ButtonLocalMedia": "Lokal media", "ButtonLocalMedia": "Lokala medier",
"ButtonLogs": "Loggar", "ButtonLogs": "Loggar",
"ButtonManageLocalFiles": "Hantera Lokal Filer", "ButtonManageLocalFiles": "Hantera lokala filer",
"ButtonMaskServerAddress": "Dölj server adress", "ButtonMaskServerAddress": "Dölj serveradress",
"ButtonNewFolder": "Ny Katalog", "ButtonNewFolder": "Ny mapp",
"ButtonNextEpisode": "Ny Episod", "ButtonNextEpisode": "Nästa avsnitt",
"ButtonOk": "OK", "ButtonOk": "OK",
"ButtonOpenFeed": "Öppna flöde", "ButtonOpenFeed": "Öppna flödet",
"ButtonOverride": "Åsidosätt", "ButtonOverride": "Åsidosätt",
"ButtonPause": "Pausa", "ButtonPause": "Pausa",
"ButtonPlay": "Spela", "ButtonPlay": "Spela upp",
"ButtonPlayEpisode": "Spela Episod", "ButtonPlayEpisode": "Spela upp avsnitt",
"ButtonPlaylists": "Spellistor", "ButtonPlaylists": "Spellistor",
"ButtonRead": "Läs", "ButtonRead": "Läs",
"ButtonReadLess": "Visa mindre", "ButtonReadLess": "Visa mindre",
"ButtonReadMore": "Visa mer", "ButtonReadMore": "Visa mer",
"ButtonRemove": "Ta bort", "ButtonRemove": "Ta bort",
"ButtonRemoveFromServer": "Ta bort från Server", "ButtonRemoveFromServer": "Ta bort från servern",
"ButtonSave": "Spara", "ButtonSave": "Spara",
"ButtonSaveOrder": "Spara Ordning", "ButtonSaveOrder": "Spara ordning",
"ButtonSearch": "Sök", "ButtonSearch": "Sök",
"ButtonSendEbookToDevice": "Skicka E-bok till Enhet", "ButtonSendEbookToDevice": "Skicka e-bok till enhet",
"ButtonSeries": "Serie", "ButtonSeries": "Serier",
"ButtonSetTimer": "Ställ in timer", "ButtonSetTimer": "Ställ in insomningstimer",
"ButtonStream": "Ström", "ButtonStream": "Strömma",
"ButtonSubmit": "Spara", "ButtonSubmit": "Skicka",
"ButtonSwitchServerUser": "Byt Server/Användare", "ButtonSwitchServerUser": "Byt server/användare",
"ButtonUnmaskServerAddress": "Visa server adress", "ButtonUnmaskServerAddress": "Visa serveradress",
"ButtonUserStats": "Användarstatistik", "ButtonUserStats": "Användarstatistik",
"ButtonYes": "Ja", "ButtonYes": "Ja",
"HeaderAccount": "Konto", "HeaderAccount": "Konto",
"HeaderAdvanced": "Avancerad", "HeaderAdvanced": "Avancerat",
"HeaderAndroidAutoSettings": "Android Auto Inställningar", "HeaderAndroidAutoSettings": "Inställningar för Android Auto",
"HeaderAudioTracks": "Ljudfiler", "HeaderAudioTracks": "Ljudspår",
"HeaderChapters": "Kapitel", "HeaderChapters": "Kapitel",
"HeaderCollection": "Samling", "HeaderCollection": "Samling",
"HeaderCollectionItems": "Böcker i samlingen", "HeaderCollectionItems": "Objekt i samlingen",
"HeaderConnectionStatus": "Anslutnings Status", "HeaderConfirm": "Bekräfta",
"HeaderDataSettings": "Data Inställningar", "HeaderConnectionStatus": "Anslutningsstatus",
"HeaderDataSettings": "Datainställningar",
"HeaderDetails": "Detaljer", "HeaderDetails": "Detaljer",
"HeaderDownloads": "Nedladdningar", "HeaderDownloads": "Hämtningar",
"HeaderEbookFiles": "E-boksfiler", "HeaderEbookFiles": "E-boksfiler",
"HeaderEpisodes": "Avsnitt", "HeaderEpisodes": "Avsnitt",
"HeaderEreaderSettings": "E-boksinställningar", "HeaderEreaderSettings": "Inställningar för e-boksläsare",
"HeaderLatestEpisodes": "Senaste avsnitten", "HeaderLatestEpisodes": "Senaste avsnitten",
"HeaderLibraries": "Bibliotek", "HeaderLibraries": "Bibliotek",
"HeaderLocalFolders": "Lokala Kataloger", "HeaderLocalFolders": "Lokala mappar",
"HeaderLocalLibraryItems": "Lokala Bibliotek Filer", "HeaderLocalLibraryItems": "Lokala biblioteksobjekt",
"HeaderNewPlaylist": "Ny Spellista", "HeaderNewPlaylist": "Ny spellista",
"HeaderOpenRSSFeed": "Öppna RSS-flöde", "HeaderOpenRSSFeed": "Öppna RSS-flödet",
"HeaderPlaybackSettings": "Uppspelningsinställningar", "HeaderPlaybackSettings": "Uppspelningsinställningar",
"HeaderPlaylist": "Spellista", "HeaderPlaylist": "Spellista",
"HeaderPlaylistItems": "Böcker i spellistan", "HeaderPlaylistItems": "Objekt i spellistan",
"HeaderProgressSyncFailed": "Progress Sync misslyckades", "HeaderProgressSyncFailed": "Synkroniseringen av uppspelningspositionen misslyckades",
"HeaderRSSFeed": "RSS flöde", "HeaderRSSFeed": "RSS-flöde",
"HeaderRSSFeedGeneral": "RSS-information", "HeaderRSSFeedGeneral": "RSS-information",
"HeaderRSSFeedIsOpen": "RSS-flödet är öppet", "HeaderRSSFeedIsOpen": "RSS-flödet är öppet",
"HeaderSelectDownloadLocation": "Välj Nedladdnings Plats", "HeaderSelectDownloadLocation": "Välj hämtningsplats",
"HeaderSettings": "Inställningar", "HeaderSettings": "Inställningar",
"HeaderSleepTimer": "Timer för att sova", "HeaderSleepTimer": "Insomningstimer",
"HeaderSleepTimerSettings": "Sovtimer Inställningar", "HeaderSleepTimerSettings": "Inställningar för insomningstimer",
"HeaderStatsMinutesListeningChart": "Minuters lyssning (senaste 7 dagarna)", "HeaderStatsMinutesListeningChart": "Lyssnade minuter (senaste 7 dagarna)",
"HeaderStatsRecentSessions": "Senaste tillfällena", "HeaderStatsRecentSessions": "Senaste lyssningstillfällena",
"HeaderTableOfContents": "Innehållsförteckning", "HeaderTableOfContents": "Innehållsförteckning",
"HeaderUserInterfaceSettings": "Användargränssnittsinställningar", "HeaderUserInterfaceSettings": "Användargränssnittsinställningar",
"HeaderWelcome": "Välkommen, <strong>{0}</strong>",
"HeaderYourStats": "Din statistik", "HeaderYourStats": "Din statistik",
"LabelAddToPlaylist": "Lägg till i en spellista", "LabelAddToPlaylist": "Lägg till i spellista",
"LabelAddedAt": "Tillagd vid", "LabelAddedAt": "Tillagd",
"LabelAddedDate": "Adderad {0}", "LabelAddedDate": "Tillagd {0}",
"LabelAll": "Alla", "LabelAll": "Alla",
"LabelAllowSeekingOnMediaControls": "Tillåt positionssökning medieaviseringskontroller", "LabelAllowSeekingOnMediaControls": "Tillåt spolning med mediekontrollerna i aviseringen",
"LabelAlways": "Alltid", "LabelAlways": "Alltid",
"LabelAndroidAutoBrowseLimitForGrouping": "Alfabetisk neddragningsgräns", "LabelAndroidAutoBrowseLimitForGrouping": "Gräns för alfabetisk gruppering",
"LabelAndroidAutoBrowseLimitForGroupingHelp": "Använd inte alfabetisk neddragning när det finns mindre än denna mängd objekt att visa", "LabelAndroidAutoBrowseLimitForGroupingHelp": "Gruppera inte alfabetiskt när färre än detta antal objekt visas",
"LabelAndroidAutoBrowseSeriesSequenceOrder": "Serier bokföljd", "LabelAndroidAutoBrowseSeriesSequenceOrder": "Seriernas ordningsföljd",
"LabelAskConfirmation": "Fråga efter bekräftelse", "LabelAskConfirmation": "Begär bekräftelse",
"LabelAuthor": "Författare", "LabelAuthor": "Författare",
"LabelAuthorFirstLast": "Författare (Förnamn Efternamn)", "LabelAuthorFirstLast": "Författare (förnamn efternamn)",
"LabelAuthorLastFirst": "Författare (Efternamn, Förnamn)", "LabelAuthorLastFirst": "Författare (efternamn, förnamn)",
"LabelAuthors": "Författare", "LabelAuthors": "Författare",
"LabelAutoDownloadEpisodes": "Automatisk nedladdning av avsnitt", "LabelAutoDownloadEpisodes": "Automatisk hämtning av avsnitt",
"LabelAutoRewindTime": "Automatisk tillbakaspolningstid", "LabelAutoRewindTime": "Automatisk återspolningstid",
"LabelAutoSleepTimer": "Automatisk insomningstimer", "LabelAutoSleepTimer": "Automatisk insomningstimer",
"LabelAutoSleepTimerAutoRewind": "Automatisk insomningstimer automatisk bakåtspolning", "LabelAutoSleepTimerAutoRewind": "Automatisk återspolning efter insomningstimern",
"LabelAutoSleepTimerAutoRewindHelp": "När den automatiska insomningstimern är klar spolas din position automatiskt tillbaka.", "LabelAutoSleepTimerAutoRewindHelp": "När den automatiska insomningstimern löper ut spolas uppspelningspositionen automatiskt tillbaka.",
"LabelAutoSleepTimerHelp": "Vid uppspelning av media mellan de angivna start- och sluttiderna startar en insomningstimer automatiskt.", "LabelAutoSleepTimerHelp": "Vid uppspelning av media mellan de angivna start- och sluttiderna startar en insomningstimer automatiskt.",
"LabelBooks": "Böcker", "LabelBooks": "Böcker",
"LabelChapterTrack": "Kapitel Spår", "LabelByAuthor": "av {0}",
"LabelChapterTrack": "Kapitelspår",
"LabelChapters": "Kapitel", "LabelChapters": "Kapitel",
"LabelClosePlayer": "Stäng spelaren", "LabelClosePlayer": "Stäng spelaren",
"LabelCollapseSeries": "Komprimera serier", "LabelCollapseSeries": "Fäll ihop serier",
"LabelComplete": "Komplett", "LabelComplete": "Slutförd",
"LabelContinueBooks": "Fortsätt Böcker", "LabelContinueBooks": "Fortsätt med böcker",
"LabelContinueEpisodes": "Fortsätt Episoder", "LabelContinueEpisodes": "Fortsätt med avsnitt",
"LabelContinueListening": "Fortsätt att lyssna", "LabelContinueListening": "Fortsätt att lyssna",
"LabelContinueReading": "Fortsätt att läsa", "LabelContinueReading": "Fortsätt att läsa",
"LabelContinueSeries": "Forsätt Serie", "LabelContinueSeries": "Fortsätt med serien",
"LabelCustomTime": "Anpassad tid", "LabelCustomTime": "Anpassad tid",
"LabelDescription": "Beskrivning", "LabelDescription": "Beskrivning",
"LabelDisableAudioFadeOut": "Inaktivera att ljudet tonas ut", "LabelDisableAudioFadeOut": "Inaktivera uttoning av ljudet",
"LabelDisableAudioFadeOutHelp": "Ljudvolymen börjar minska när det är mindre än 1 minut kvar på insomningstimern. Aktivera den här inställningen för att inte tona ut.", "LabelDisableAudioFadeOutHelp": "Ljudvolymen börjar sänkas när mindre än en minut återstår av insomningstimern. Aktivera alternativet för att förhindra uttoningen.",
"LabelDisableAutoRewind": "Inaktivera automatisk tillbakaspolning", "LabelDisableAutoRewind": "Inaktivera automatisk återspolning",
"LabelDisableShakeToReset": "Inaktivera skakning för att återställa", "LabelDisableShakeToReset": "Inaktivera återställning genom skakning",
"LabelDisableShakeToResetHelp": "Om du skakar enheten medan timern är igång eller inom 2 minuter efter att timern har löpt ut återställs insomningstimern. Aktivera den här inställningen för att inaktivera skakning för att återställa.", "LabelDisableShakeToResetHelp": "Om du skakar enheten medan timern är aktiv, eller inom två minuter efter att den har löpt ut, återställs insomningstimern. Aktivera alternativet för att inaktivera funktionen.",
"LabelDisableVibrateOnReset": "Inaktivera vibration vid återställning", "LabelDisableVibrateOnReset": "Inaktivera vibration vid återställning",
"LabelDisableVibrateOnResetHelp": "När insomningstimern återställs kommer din enhet att vibrera. Aktivera den här inställningen för att inte vibrera när insomningstimern återställs.", "LabelDisableVibrateOnResetHelp": "Enheten vibrerar när insomningstimern återställs. Aktivera alternativet för att inaktivera vibrationen.",
"LabelDiscover": "Några förslag", "LabelDiscover": "Upptäck",
"LabelDownload": "Ladda ner", "LabelDownload": "Hämta",
"LabelDownloadUsingCellular": "Ladda ner med mobildata", "LabelDownloadUsingCellular": "Hämta med mobildata",
"LabelDownloaded": "Nedladdat", "LabelDownloaded": "Hämtad",
"LabelDuration": "Längd", "LabelDuration": "Längd",
"LabelEbook": "E-bok", "LabelEbook": "E-bok",
"LabelEbooks": "E-böcker", "LabelEbooks": "E-böcker",
"LabelEnable": "Aktivera", "LabelEnable": "Aktivera",
"LabelEnableMp3IndexSeeking": "Aktivera mp3 index sökning", "LabelEnableMp3IndexSeeking": "Aktivera indexbaserad spolning i MP3-filer",
"LabelEnableMp3IndexSeekingHelp": "Den här inställningen bör endast aktiveras om du har mp3-filer som inte söker korrekt. Felaktig sökning beror med största sannolikhet på mp3-filer med variabel bitrate (VBR). Den här inställningen tvingar fram indexsökning, där en tid-till-byte-mappning byggs upp när filen läses. I vissa fall med stora mp3-filer blir det en fördröjning vid sökning mot slutet av filen.", "LabelEnableMp3IndexSeekingHelp": "Aktivera endast alternativet om spolningen inte fungerar korrekt i vissa MP3-filer. Problemet orsakas oftast av MP3-filer med variabel bithastighet (VBR). Indexbaserad spolning skapar en koppling mellan tid och byteposition medan filen läses. Stora MP3-filer kan därför reagera långsammare när du spolar mot slutet.",
"LabelEnd": "Slut", "LabelEnd": "Slut",
"LabelEndOfChapter": "Slut av kapitel", "LabelEndOfChapter": "Kapitlets slut",
"LabelEndTime": "Slut tid", "LabelEndTime": "Sluttid",
"LabelEpisode": "Avsnitt", "LabelEpisode": "Avsnitt",
"LabelExplicit": "Bestämd", "LabelExplicit": "Vuxeninnehåll",
"LabelFeedURL": "URL-adress för flödet", "LabelFeedURL": "Flödets URL",
"LabelFile": "Fil", "LabelFile": "Fil",
"LabelFileBirthtime": "Tidpunkt, adderad", "LabelFileBirthtime": "Skapad",
"LabelFileModified": "Tidpunkt, ändrad", "LabelFileModified": "Ändrad",
"LabelFilename": "Filnamn", "LabelFilename": "Filnamn",
"LabelFinished": "Avslutad", "LabelFinished": "Avslutad",
"LabelFolder": "Mapp", "LabelFolder": "Mapp",
"LabelFontBoldness": "Fetstil", "LabelFontBoldness": "Fetstil",
"LabelFontScale": "Teckensnittsskala", "LabelFontFamily": "Teckensnittsfamilj",
"LabelFontFamilySans": "Sans serif",
"LabelFontFamilySerif": "Serif",
"LabelFontScale": "Teckenstorlek",
"LabelGenre": "Genre", "LabelGenre": "Genre",
"LabelGenres": "Genrer", "LabelGenres": "Genrer",
"LabelHapticFeedback": "Haptisk feedback", "LabelHapticFeedback": "Haptisk återkoppling",
"LabelHasEbook": "Har e-bok", "LabelHasEbook": "Har e-bok",
"LabelHasSupplementaryEbook": "Har kompletterande e-bok", "LabelHasSupplementaryEbook": "Har kompletterande e-bok",
"LabelHeavy": "Tung", "LabelHeavy": "Tung",
@@ -166,85 +172,89 @@
"LabelHost": "Värd", "LabelHost": "Värd",
"LabelInProgress": "Pågående", "LabelInProgress": "Pågående",
"LabelIncomplete": "Ofullständigt", "LabelIncomplete": "Ofullständigt",
"LabelInternalAppStorage": "Intern App Lagring", "LabelInternalAppStorage": "Appens interna lagring",
"LabelJumpBackwardsTime": "hoppa-bakåt-tid", "LabelJumpBackwardsTime": "Hoppintervall bakåt",
"LabelJumpForwardsTime": "hoppa-framåt-tid", "LabelJumpForwardsTime": "Hoppintervall framåt",
"LabelKeepScreenAwake": "Håll skärmen vaken", "LabelKeepScreenAwake": "Håll skärmen vaken",
"LabelLanguage": "Språk", "LabelLanguage": "Språk",
"LabelLayout": "Layout", "LabelLayout": "Layout",
"LabelLayoutAuto": "Automatisk", "LabelLayoutAuto": "Automatisk",
"LabelLayoutSinglePage": "En sida", "LabelLayoutSinglePage": "En sida",
"LabelLibrarySortByProgress": "Förlopp: senast uppdaterat",
"LabelLibrarySortByProgressFinished": "Förlopp: avslutat",
"LabelLibrarySortByProgressStarted": "Förlopp: påbörjat",
"LabelLight": "Lätt", "LabelLight": "Lätt",
"LabelLineSpacing": "Radavstånd", "LabelLineSpacing": "Radavstånd",
"LabelListenAgain": "Lyssna igen", "LabelListenAgain": "Lyssna igen",
"LabelLocalBooks": "Lokala böcker", "LabelLocalBooks": "Lokala böcker",
"LabelLocalPodcasts": "Lokala podcasts", "LabelLocalPodcasts": "Lokala poddar",
"LabelLockOrientation": "Lås skärmorientering", "LabelLockOrientation": "Lås skärmorientering",
"LabelLockPlayer": "Lås spelare", "LabelLockPlayer": "Lås spelaren",
"LabelLow": "Låg", "LabelLow": "Låg",
"LabelMediaType": "Mediatyp", "LabelMediaType": "Mediatyp",
"LabelMedium": "Medel", "LabelMedium": "Medel",
"LabelMissing": "Saknar", "LabelMissing": "Saknas",
"LabelMore": "Mer", "LabelMore": "Mer",
"LabelMoreInfo": "Mer information", "LabelMoreInfo": "Mer information",
"LabelName": "Namn", "LabelName": "Namn",
"LabelNarrator": "Berättare", "LabelNarrator": "Uppläsare",
"LabelNarrators": "Berättare", "LabelNarrators": "Uppläsare",
"LabelNavigateWithVolume": "Navigera med volymknapparna", "LabelNavigateWithVolume": "Navigera med volymknapparna",
"LabelNavigateWithVolumeMirrored": "Speglad", "LabelNavigateWithVolumeMirrored": "Speglad",
"LabelNavigateWithVolumeWhilePlaying": "Tillåt volymknappar att navigera medan du spelar", "LabelNavigateWithVolumeWhilePlaying": "Använd volymknapparna för navigering under uppspelning",
"LabelNavigateWithVolumeWhilePlayingDisabled": "Av", "LabelNavigateWithVolumeWhilePlayingDisabled": "Av",
"LabelNavigateWithVolumeWhilePlayingEnabled": "På", "LabelNavigateWithVolumeWhilePlayingEnabled": "På",
"LabelNever": "Aldrig", "LabelNever": "Aldrig",
"LabelNewestAuthors": "Senaste författarna", "LabelNewestAuthors": "Senast tillagda författare",
"LabelNewestEpisodes": "Senaste avsnitten", "LabelNewestEpisodes": "Senast tillagda avsnitt",
"LabelNo": "Nej", "LabelNo": "Nej",
"LabelNotFinished": "Ej avslutad", "LabelNotFinished": "Inte avslutad",
"LabelNotStarted": "Ej påbörjad", "LabelNotStarted": "Inte påbörjad",
"LabelNumEpisodes": "{0} avsnitt", "LabelNumEpisodes": "{0} avsnitt",
"LabelNumEpisodesIncomplete": "{0} avsnitt, {1} ofärdiga", "LabelNumEpisodesIncomplete": "{0} avsnitt, {1} inte slutförda",
"LabelNumberOfEpisodes": "# av Avsnitt", "LabelNumberOfEpisodes": "Antal avsnitt",
"LabelOff": "Av", "LabelOff": "Av",
"LabelOn": "På", "LabelOn": "På",
"LabelPassword": "Lösenord", "LabelPassword": "Lösenord",
"LabelPath": "Sökväg", "LabelPath": "Sökväg",
"LabelPlaybackDirect": "Direct", "LabelPlaybackDirect": "Direkt",
"LabelPlaybackLocal": "Lokal", "LabelPlaybackLocal": "Lokal",
"LabelPlaybackSpeed": "Uppspelningshastighet", "LabelPlaybackSpeed": "Uppspelningshastighet",
"LabelPlaybackTranscode": "Transkodning", "LabelPlaybackTranscode": "Omkodning",
"LabelPodcast": "Podcast", "LabelPodcast": "Podd",
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Poddar",
"LabelPreventIndexing": "Förhindra att ditt flöde indexeras av sökmotorer från iTunes och Google", "LabelPreventIndexing": "Förhindra att flödet indexeras av poddkatalogerna iTunes och Google",
"LabelProgress": "Framsteg", "LabelProgress": "Förlopp",
"LabelPubDate": "Publiceringsdatum", "LabelPubDate": "Publiceringsdatum",
"LabelPublishYear": "Utgivningsår", "LabelPublishYear": "Utgivningsår",
"LabelPublishedDate": "Publicerad {0}", "LabelPublishedDate": "Publicerad {0}",
"LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post", "LabelRSSFeedCustomOwnerEmail": "Anpassad e-postadress för ägaren",
"LabelRSSFeedCustomOwnerName": "Anpassat ägarnamn", "LabelRSSFeedCustomOwnerName": "Anpassat ägarnamn",
"LabelRSSFeedOpen": "RSS-flöde öppet", "LabelRSSFeedOpen": "RSS-flödet är öppet",
"LabelRSSFeedPreventIndexing": "Förhindra indexering", "LabelRSSFeedPreventIndexing": "Förhindra indexering",
"LabelRSSFeedSlug": "RSS-flödesslag", "LabelRSSFeedSlug": "Slug för RSS-flödet",
"LabelRandomly": "Slumpartat", "LabelRandomly": "Slumpmässigt",
"LabelRead": "Läs", "LabelRead": "Läs",
"LabelReadAgain": "Läs igen", "LabelReadAgain": "Läs igen",
"LabelRecentSeries": "Senaste serierna", "LabelRecentSeries": "Senaste serierna",
"LabelRecentlyAdded": "Nyligen adderade", "LabelRecentlyAdded": "Nyligen tillagda",
"LabelRemoveFromPlaylist": "Ta bort från spellista", "LabelRemoveFromPlaylist": "Ta bort från spellista",
"LabelScaleElapsedTimeBySpeed": "Skaländra uppspelningsposition efter uppspelningshastighet.", "LabelScaleElapsedTimeBySpeed": "Justera den visade uppspelningstiden efter uppspelningshastigheten.",
"LabelSeason": "Säsong", "LabelSeason": "Säsong",
"LabelSelectADevice": "Välj en enhet", "LabelSelectADevice": "Välj en enhet",
"LabelSelectMediaType": "Välj mediatyp",
"LabelSequenceAscending": "Stigande", "LabelSequenceAscending": "Stigande",
"LabelSequenceDescending": "Sekvens Fallande", "LabelSequenceDescending": "Fallande",
"LabelSeries": "Serie", "LabelSeries": "Serie",
"LabelServerAddress": "Server-adress", "LabelServerAddress": "Serveradress",
"LabelSetEbookAsPrimary": "Ange som primär", "LabelSetEbookAsPrimary": "Ange som primär",
"LabelSetEbookAsSupplementary": "Ange som kompletterande", "LabelSetEbookAsSupplementary": "Ange som kompletterande",
"LabelShakeSensitivity": "Skakkänslighet", "LabelShakeSensitivity": "Skakkänslighet",
"LabelShowAll": "Visa alla", "LabelShowAll": "Visa alla",
"LabelSize": "Storlek", "LabelSize": "Storlek",
"LabelSleepTimer": "Sovtimer", "LabelSleepTimer": "Insomningstimer",
"LabelSleepTimerAlmostDoneChime": "Spela ett ljud när det är nästan klart", "LabelSleepTimerAlmostDoneChime": "Spela upp ett ljud när timern nästan har löpt ut",
"LabelSleepTimerAlmostDoneChimeHelp": "Spela ett ljud när sovtimern har 30 sekunder kvar", "LabelSleepTimerAlmostDoneChimeHelp": "Spela upp ett ljud när 30 sekunder återstår av insomningstimern",
"LabelStart": "Start", "LabelStart": "Start",
"LabelStartTime": "Starttid", "LabelStartTime": "Starttid",
"LabelStatsBestDay": "Bästa dag", "LabelStatsBestDay": "Bästa dag",
@@ -265,8 +275,8 @@
"LabelThemeLight": "Ljust", "LabelThemeLight": "Ljust",
"LabelTimeRemaining": "{0} kvar", "LabelTimeRemaining": "{0} kvar",
"LabelTitle": "Titel", "LabelTitle": "Titel",
"LabelTotalSize": "Total Storlek", "LabelTotalSize": "Total storlek",
"LabelTotalTrack": "Total Spår", "LabelTotalTrack": "Totalt antal spår",
"LabelTracks": "Ljudspår", "LabelTracks": "Ljudspår",
"LabelType": "Typ", "LabelType": "Typ",
"LabelUnknown": "Okänd", "LabelUnknown": "Okänd",
@@ -274,86 +284,93 @@
"LabelUseBookshelfView": "Använd bokhyllevyn", "LabelUseBookshelfView": "Använd bokhyllevyn",
"LabelUser": "Användare", "LabelUser": "Användare",
"LabelUsername": "Användarnamn", "LabelUsername": "Användarnamn",
"LabelVeryHigh": "Väldigt hög", "LabelVeryHigh": "Mycket hög",
"LabelVeryLow": "Väldigt låg", "LabelVeryLow": "Mycket låg",
"LabelYearReviewHide": "Dölj årets sammanställning", "LabelYearReviewHide": "Dölj årets sammanställning",
"LabelYearReviewShow": "Visa årets sammanställning", "LabelYearReviewShow": "Visa årets sammanställning",
"LabelYourBookmarks": "Dina bokmärken", "LabelYourBookmarks": "Dina bokmärken",
"LabelYourProgress": "Framsteg", "LabelYourProgress": "Ditt förlopp",
"MessageAndroid10Downloads": "Android 10 och lägre använder intern applagring för nedladdningar.", "MessageAndroid10Downloads": "Android 10 och lägre använder intern applagring för hämtningar.",
"MessageAttemptingServerConnection": "Försöker ansluta till server...", "MessageAttemptingServerConnection": "Försöker ansluta till servern…",
"MessageAudiobookshelfServerNotConnected": "Audiobookshelf-server är ej ansluten", "MessageAudiobookshelfServerNotConnected": "Audiobookshelf-servern är inte ansluten",
"MessageAudiobookshelfServerRequired": "<strong>Viktigt!</strong> Den här appen måste användas tillsammans med en Audiobookshelf-server som hostas av dig eller dina bekanta. Appen tillhandahåller själv inget innehåll.", "MessageAudiobookshelfServerRequired": "<strong>Viktigt!</strong> Appen måste användas tillsammans med en Audiobookshelf-server som du eller någon du känner är värd för. Appen tillhandahåller inget eget innehåll.",
"MessageBookshelfEmpty": "Bokhyllan är tom", "MessageBookshelfEmpty": "Bokhyllan är tom",
"MessageConfirmAppExit": "Vill du avsluta appen?",
"MessageConfirmDeleteEpisodeDownloadQueue": "Är du säker på att du vill rensa kön för hämtning av avsnitt?",
"MessageConfirmDeleteLocalEpisode": "Ta bort lokalt avsnitt \"{0}\" från enheten? Filen på servern påverkas inte.", "MessageConfirmDeleteLocalEpisode": "Ta bort lokalt avsnitt \"{0}\" från enheten? Filen på servern påverkas inte.",
"MessageConfirmDeleteLocalFiles": "Ta bort lokala filer för detta objekt från din enhet? Filerna på servern och din uppspelningsposition raderas inte.", "MessageConfirmDeleteLocalFiles": "Ta bort lokala filer för detta objekt från din enhet? Filerna på servern och din uppspelningsposition tas inte bort.",
"MessageConfirmDisableAutoTimer": "Är du säker att du vill stänga av auto timer för resten av idag? Timern kommer att sättas på igen vid slutet av den här auto-sömn timer perioden, eller om du startar om appen.", "MessageConfirmDeleteServerConfig": "Vill du ta bort serverkonfigurationen?",
"MessageConfirmDeleteServerEpisode": "Vill du verkligen ta bort avsnittet ”{0}” från servern?\nOBSERVERA: Ljudfilen tas då bort.",
"MessageConfirmDisableAutoTimer": "Vill du inaktivera den automatiska insomningstimern under resten av dagen? Funktionen aktiveras igen när den nuvarande timerperioden är slut eller när appen startas om.",
"MessageConfirmDiscardProgress": "Är du säker på att du vill återställa din uppspelningsposition?", "MessageConfirmDiscardProgress": "Är du säker på att du vill återställa din uppspelningsposition?",
"MessageConfirmDownloadUsingCellular": "Du kommer påbörja en nedladdning över mobildata. Detta kan leda till avgifter från din operatör. Vill du fortsätta?", "MessageConfirmDownloadUsingCellular": "Du håller på att starta en hämtning via mobildata. Det kan medföra avgifter från operatören. Vill du fortsätta?",
"MessageConfirmMarkAsFinished": "Är du säker på att du vill färdigmarkera det här objektet.", "MessageConfirmMarkAsFinished": "Vill du markera objektet som avslutat?",
"MessageConfirmPlaybackTime": "Vill du starta uppspelning av \"{0}\" på {1}?",
"MessageConfirmRemoveBookmark": "Är du säker på att du vill ta bort bokmärket?", "MessageConfirmRemoveBookmark": "Är du säker på att du vill ta bort bokmärket?",
"MessageConfirmStreamingUsingCellular": "Du kommer påbörja en strömmning över mobildata. Detta kan leda till avgifter från din operatör. Vill du fortsätta?", "MessageConfirmStreamingUsingCellular": "Du håller på att starta strömning via mobildata. Det kan medföra avgifter från operatören. Vill du fortsätta?",
"MessageDiscardProgress": "Avfärda uppspelningsposition", "MessageDiscardProgress": "Återställ uppspelningspositionen",
"MessageDownloadCompleteProcessing": "Nedladdning klar. Behandlar...", "MessageDownloadCompleteProcessing": "Hämtningen är klar. Bearbetar…",
"MessageDownloading": "Laddar ned...", "MessageDownloading": "Hämtar…",
"MessageDownloadingEpisode": "Laddar ner avsnitt", "MessageDownloadingEpisode": "Hämtar avsnitt",
"MessageEpisodesQueuedForDownload": "{0} avsnitt i kö för nedladdning", "MessageEpisodesQueuedForDownload": "{0} avsnitt har lagts i hämtningskön",
"MessageFailedToRefreshToken": "Misslyckades med att uppdatera token, nytt inloggningsförsök krävs", "MessageFailedToRefreshToken": "Det gick inte att förnya token. Du måste logga in igen.",
"MessageFeedURLWillBe": "Flödes-URL kommer att vara {0}", "MessageFeedURLWillBe": "Flödets URL blir {0}",
"MessageFetching": "Hämtar...", "MessageFetching": "Hämtar",
"MessageFollowTheProjectOnGithub": "Följ projektet på Github", "MessageFollowTheProjectOnGithub": "Följ projektet på GitHub",
"MessageItemDownloadCompleteFailedToCreate": "Objektet laddades ned men det gick inte att skapa ett biblioteksobjekt", "MessageItemDownloadCompleteFailedToCreate": "Objektet hämtades men det gick inte att skapa ett biblioteksobjekt",
"MessageItemMissing": "Objekt saknas och måste fixas på servern. Vanligast när ett objekt är makerad som saknad beror det på att sökvägen inte är nåbar.", "MessageItemMissing": "Objektet saknas och måste åtgärdas på servern. Det beror oftast på att sökvägen till objektet inte kan nås.",
"MessageLoading": "Laddar...", "MessageLoading": "Läser in…",
"MessageLoadingServerData": "Läser in serverdata...", "MessageLoadingServerData": "Läser in serverdata",
"MessageLocalFolderDescription": "'Intern lagring för appen' är endast tillgänglig för denna app. Denna app stöder endast media som har laddats ner direkt via appen. Delade lagringsmappar kan användas för att tillåta andra appar att få åtkomst till media som laddats ner av denna app.", "MessageLocalFolderDescription": "'Intern lagring för appen' är endast tillgänglig för denna app. Denna app stöder endast media som har hämtats direkt via appen. Delade lagringsmappar kan användas för att tillåta andra appar att få åtkomst till media som hämtats av denna app.",
"MessageMarkAsFinished": "Markera som avslutad", "MessageMarkAsFinished": "Markera som avslutad",
"MessageMediaLinkedToADifferentServer": "Detta media är länkat till en Audiobookshelf-server på annan adress ({0}). Uppspelningspositioner kommer synkas nästa gång appen kan ansluta till servern.", "MessageMediaLinkedToADifferentServer": "Det här mediet är länkat till en Audiobookshelf-server på en annan adress ({0}). Uppspelningspositionen synkroniseras nästa gång appen kan ansluta till servern.",
"MessageMediaLinkedToADifferentUser": "Detta media är länkat till den här servern men laddades ned av en annan användare. Uppspelningspositioner kommer endast synkas för den användaren som laddade ned objektet.", "MessageMediaLinkedToADifferentUser": "Det här mediet är länkat till servern men hämtades av en annan användare. Uppspelningspositionen synkroniseras endast för användaren som hämtade objektet.",
"MessageMediaLinkedToServer": "Länkad till server {0}", "MessageMediaLinkedToServer": "Länkad till servern {0}",
"MessageMediaLinkedToThisServer": "Nedladdad media är länkad till denna server", "MessageMediaLinkedToThisServer": "Det hämtade mediet är länkat till den här servern",
"MessageMediaNotLinkedToServer": "Detta media är inte länkat till en Audiobookshelf-server. Inga uppspelningspositioner kommer synkas.", "MessageMediaNotLinkedToServer": "Det här mediet är inte länkat till en Audiobookshelf-server. Uppspelningspositionen synkroniseras inte.",
"MessageNoBookmarks": "Inga bokmärken", "MessageNoBookmarks": "Inga bokmärken",
"MessageNoChapters": "Inga kapitel", "MessageNoChapters": "Inga kapitel",
"MessageNoCollections": "Inga Samlingar", "MessageNoCollections": "Inga samlingar",
"MessageNoItems": "Inga objekt", "MessageNoItems": "Inga objekt",
"MessageNoItemsFound": "Inga objekt hittades", "MessageNoItemsFound": "Inga objekt hittades",
"MessageNoListeningSessions": "Inga lyssningstillfällen", "MessageNoListeningSessions": "Inga lyssningstillfällen",
"MessageNoLogs": "Inga loggar", "MessageNoLogs": "Inga loggar",
"MessageNoMediaFolders": "Inga Mediamappar", "MessageNoMediaFolders": "Inga mediemappar",
"MessageNoNetworkConnection": "Ingen nätverksanslutning", "MessageNoNetworkConnection": "Ingen nätverksanslutning",
"MessageNoPodcastsFound": "Inga podcasts hittade", "MessageNoPodcastsFound": "Inga poddar hittades",
"MessageNoSeries": "Inga serier", "MessageNoSeries": "Inga serier",
"MessageNoUpdatesWereNecessary": "Inga uppdateringar var nödvändiga", "MessageNoUpdatesWereNecessary": "Inga uppdateringar behövdes",
"MessageNoUserPlaylists": "Du har inga spellistor", "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.", "MessageOldServerAuthReLoginRequired": "Autentiseringen har förbättrats i serverversion 2.26.0. Alla användare måste logga in igen.",
"MessageOldServerAuthWarning": "Servern använder en gammal autentisering", "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.", "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.", "MessageOldServerConnectionWarning": "Serveranslutningen använder ett äldre användar-ID. Ta bort anslutningen och lägg till den 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 den här enheten måste den laddas ner igen för att synkroniseras med servern.", "MessageOldServerConnectionWarningHelp": "Anslutningen till servern skapades före databasflytten i version 2.3.0, som gavs ut i juni 2023. En framtida serveruppdatering tar bort möjligheten att logga in med den äldre anslutningen. Ta bort serveranslutningen och anslut igen med samma serveradress och inloggningsuppgifter. Om du har hämtade medier på enheten måste de hämtas igen för att kunna synkroniseras med servern.",
"MessagePodcastSearchField": "Skriv sökfrågan eller URL-adressen för RSS-flödet", "MessagePodcastSearchField": "Ange en sökfras eller RSS-flödets URL",
"MessageProgressSyncFailed": "Det senaste försöket att rapportera din lyssningsprogress till servern misslyckades. Försök att synkronisera progressen kommer att fortsätta göras var 15:e sekund till 1 minut medan media spelas.", "MessageProgressSyncFailed": "Det senaste försöket att skicka uppspelningspositionen till servern misslyckades. Appen fortsätter att försöka synkronisera positionen med 15 sekunders till en minuts mellanrum medan mediet spelas upp.",
"MessageReportBugsAndContribute": "Rapportera buggar, begär funktioner och bidra på", "MessageReportBugsAndContribute": "Rapportera fel, föreslå funktioner och bidra på",
"MessageSeriesAlreadyDownloaded": "Du har redan laddat ner alla böcker i den här serien.", "MessageSeriesAlreadyDownloaded": "Du har redan hämtat alla böcker i serien.",
"MessageSeriesDownloadConfirm": "Vill du ladda ner de saknade {0} boken/böckerna med {1} fil(er), totalt {2}, till mappen {3}?", "MessageSeriesDownloadConfirm": "Vill du hämta de {0} saknade böckerna med sammanlagt {1} filer ({2}) till mappen {3}?",
"MessageSeriesDownloadConfirmIos": "Vill du ladda ner de saknade {0} boken/böckerna med {1} fil(er), totalt {2}?", "MessageSeriesDownloadConfirmIos": "Vill du hämta de {0} saknade böckerna med sammanlagt {1} filer ({2})?",
"MessageSocketConnectedOverMeteredCellular": "Socket ansluten via mätad mobilanslutning", "MessageSocketConnectedOverMeteredCellular": "Socketanslutning via mobilnät med datagräns",
"MessageSocketConnectedOverMeteredWifi": "Socket connected over metered Wi-Fi", "MessageSocketConnectedOverMeteredWifi": "Socketanslutning via Wi-Fi med datagräns",
"MessageSocketConnectedOverUnmeteredCellular": "Socket ansluten via omättad mobilanslutning", "MessageSocketConnectedOverUnmeteredCellular": "Socketanslutning via mobilnät utan datagräns",
"MessageSocketConnectedOverUnmeteredWifi": "Socket ansluten via omättat Wi-Fi", "MessageSocketConnectedOverUnmeteredWifi": "Socketanslutning via Wi-Fi utan datagräns",
"MessageSocketNotConnected": "Socket inte ansluten", "MessageSocketNotConnected": "Socketanslutning saknas",
"NoteRSSFeedPodcastAppsHttps": "VARNING: De flesta applikationer för podcasts kräver att URL:en för RSS-flödet använder HTTPS", "NoteRSSFeedPodcastAppsHttps": "VARNING: De flesta poddappar kräver att RSS-flödets URL använder HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "VARNING: Ett eller flera av dina avsnitt saknar publiceringsdatum. Vissa applikationer för podcasts kräver detta.", "NoteRSSFeedPodcastAppsPubDate": "VARNING: Ett eller flera avsnitt saknar publiceringsdatum. Vissa poddappar kräver ett sådant datum.",
"ToastBookmarkCreateFailed": "Det gick inte att skapa bokmärket", "ToastBookmarkCreateFailed": "Det gick inte att skapa bokmärket",
"ToastBookmarkRemoveFailed": "Det gick inte att ta bort bokmärket", "ToastBookmarkRemoveFailed": "Det gick inte att ta bort bokmärket",
"ToastBookmarkUpdateFailed": "Det gick inte att uppdatera bokmärket", "ToastBookmarkUpdateFailed": "Det gick inte att uppdatera bokmärket",
"ToastDownloadNotAllowedOnCellular": "Nedladdning tillåts inte över mobildata", "ToastDownloadNotAllowedOnCellular": "Hämtning tillåts inte över mobildata",
"ToastItemMarkedAsFinishedFailed": "Misslyckades med att markera den som avslutad", "ToastItemMarkedAsFinishedFailed": "Det gick inte att markera objektet som avslutat",
"ToastItemMarkedAsNotFinishedFailed": "Misslyckades med att markera den som ej avslutad", "ToastItemMarkedAsNotFinishedFailed": "Det gick inte att markera objektet som inte avslutat",
"ToastPlaylistCreateFailed": "Det gick inte att skapa spellistan", "ToastPlaylistCreateFailed": "Det gick inte att skapa spellistan",
"ToastPodcastCreateFailed": "Misslyckades med att skapa podcasten", "ToastPodcastCreateFailed": "Det gick inte att skapa podden",
"ToastPodcastCreateSuccess": "Podcasten skapades framgångsrikt", "ToastPodcastCreateSuccess": "Podden skapades",
"ToastRSSFeedCloseFailed": "Misslyckades med att stänga RSS-flödet", "ToastRSSFeedCloseFailed": "Det gick inte att stänga RSS-flödet",
"ToastRSSFeedCloseSuccess": "RSS-flödet stängt", "ToastRSSFeedCloseSuccess": "RSS-flödet har stängts",
"ToastStreamingNotAllowedOnCellular": "Strömning tillåts inte över mobildata." "ToastStreamingNotAllowedOnCellular": "Strömning tillåts inte över mobildata",
"UnitMinutesShort": "{0} min",
"UnitSecondsShort": "{0} s"
} }

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