Compare commits

...

373 Commits

Author SHA1 Message Date
advplyr 2464aac2bf Version bump v2.17.6 2024-12-29 17:11:46 -06:00
advplyr b6b786e3a6 Merge pull request #3735 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-12-29 16:54:46 -06:00
Jan-Eric Myhrgren bacb8aeac7 Translated using Weblate (Swedish)
Currently translated at 68.7% (743 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2024-12-29 22:53:11 +00:00
pranelio ba9277cc44 Translated using Weblate (Lithuanian)
Currently translated at 65.2% (705 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/lt/
2024-12-29 22:53:10 +00:00
Plazec 3cc5fae586 Translated using Weblate (Czech)
Currently translated at 87.9% (950 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2024-12-29 22:53:10 +00:00
Tamanegii da7d9c10ad Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1080 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-12-29 22:53:09 +00:00
Øystein S. Hegnander aa82439125 Translated using Weblate (Norwegian Bokmål)
Currently translated at 91.9% (993 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2024-12-29 22:53:09 +00:00
ugyes 2e0156d9fa Translated using Weblate (Hungarian)
Currently translated at 95.0% (1026 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2024-12-29 22:53:08 +00:00
Øystein S. Hegnander 20e0172fa3 Translated using Weblate (Norwegian Bokmål)
Currently translated at 82.3% (889 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2024-12-29 22:53:07 +00:00
jonarihen 6928f6eeb6 Translated using Weblate (Danish)
Currently translated at 62.3% (673 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2024-12-29 22:53:07 +00:00
Greg Lorenzen 4cdc2a8c28 Feat/download via share link (#3666)
* Adds share download endpoint
* Adds Downloadable toggle to share modal

---------

Co-authored-by: advplyr <advplyr@protonmail.com>
2024-12-29 16:52:57 -06:00
advplyr e0c674d9a9 Fix:Opening audiobook RSS feeds use audiofile name #3752 2024-12-28 16:36:53 -06:00
advplyr 727310ab75 Merge pull request #3751 from nichwall/rss_feed_image_fix
Change: height of RSS feed preview to match aspect ratio
2024-12-27 17:19:24 -06:00
Nicholas Wallace f46b5a533c Change: height of RSS feed preview to match aspect ratio 2024-12-26 22:53:45 -07:00
advplyr f3e9cfbe45 Merge pull request #3726 from mikiher/lazy-bookshelf-optimizations
LazyBookshelf optimizations
2024-12-26 16:42:28 -06:00
advplyr 4d8501c347 Update skeleton card to have box shadow, fix last row of skeleton cards 2024-12-26 16:34:25 -06:00
advplyr b4e8f16174 Merge pull request #3575 from glorenzen/multi-select-keyboard-navigation
Multi select keyboard navigation
2024-12-25 09:45:28 -06:00
advplyr 7073f17cca Accessibility update for multi select inputs and edit series modal 2024-12-25 09:40:16 -06:00
advplyr e1c41e4e58 Accessibility update edit modal tabs 2024-12-25 09:18:18 -06:00
advplyr 13f73cc79d Merge branch 'master' into multi-select-keyboard-navigation 2024-12-25 09:04:43 -06:00
advplyr d811ec3806 Merge pull request #3714 from nichwall/zip_download_speedup
Change: no compression when downloading library item as zip file
2024-12-25 08:59:43 -06:00
advplyr e8505cb637 Merge pull request #3727 from brinlyau/patch-1
feat: Added Australia and New Zealand podcast regions
2024-12-24 15:18:50 -06:00
advplyr 94fdd99ab5 Fix wrong url used for SSRF filter in fileUtils 2024-12-24 15:07:11 -06:00
advplyr 331c7c011c Support SSRF_REQUEST_FILTER_WHITELIST as a comma separated string of hostnames to pass through the ssrf request filter #3742 2024-12-23 17:18:08 -06:00
advplyr 5fa263023f Fix:Quick match not removing empty series/authors #3743 2024-12-22 10:58:22 -06:00
advplyr 7eb315a371 Fix watcher skip dot files #3230 2024-12-21 17:22:48 -06:00
mikiher 780c0dcb99 Merge branch 'master' into lazy-bookshelf-optimizations 2024-12-21 17:50:51 +02:00
mikiher 004210ee02 reuse entityTransform in mountEntityCard 2024-12-21 17:48:22 +02:00
mikiher 921880445a Introduce static skeleton cards 2024-12-21 17:42:32 +02:00
advplyr 0099ae633a Config page localization updates 2024-12-20 17:27:31 -06:00
advplyr 91d99deba1 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-12-19 18:07:28 -06:00
advplyr e21cbc9ff4 Update .gitignore 2024-12-19 18:07:24 -06:00
advplyr 600c1e4668 Delete plugins directory 2024-12-19 18:06:42 -06:00
advplyr aea2951b89 Accessibility updates to config page settings 2024-12-19 18:04:56 -06:00
advplyr 71b943f434 Update mobile toolbar nav to show queue for podcast libraries #3719 2024-12-18 17:44:46 -06:00
advplyr ed0484a8e1 Merge pull request #3701 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-12-18 05:13:49 -06:00
kuci-JK 5302f3225b Translated using Weblate (Czech)
Currently translated at 86.8% (938 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2024-12-18 00:44:26 +01:00
Plazec a94a7b7940 Translated using Weblate (Czech)
Currently translated at 86.8% (938 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2024-12-18 00:44:26 +01:00
Dmitry 4318f64d60 Translated using Weblate (Russian)
Currently translated at 100.0% (1080 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2024-12-18 00:44:25 +01:00
ugyes 26a6618e8f Translated using Weblate (Hungarian)
Currently translated at 95.0% (1026 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2024-12-18 00:44:25 +01:00
ugyes c242e9d3d6 Translated using Weblate (Hungarian)
Currently translated at 92.4% (998 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2024-12-18 00:44:25 +01:00
gallegonovato 4ecb22f70d Translated using Weblate (Spanish)
Currently translated at 100.0% (1080 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-12-18 00:44:25 +01:00
kuci-JK 547a49e95b Translated using Weblate (Czech)
Currently translated at 86.2% (931 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2024-12-18 00:44:25 +01:00
Plazec b6875af148 Translated using Weblate (Czech)
Currently translated at 86.2% (931 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2024-12-18 00:44:25 +01:00
Pierrick Guillaume c652b5bf74 Translated using Weblate (French)
Currently translated at 99.4% (1074 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-12-18 00:44:25 +01:00
kuci-JK eb0b92a547 Translated using Weblate (Czech)
Currently translated at 83.8% (906 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2024-12-18 00:44:25 +01:00
advplyr b56bcbb802 Added translation using Weblate (Belarusian) 2024-12-18 00:44:25 +01:00
thehijacker 3b8af95211 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1080 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-12-18 00:44:25 +01:00
Bezruchenko Simon a3332f0478 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1080 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2024-12-18 00:44:25 +01:00
biuklija 46421d5f2c Translated using Weblate (Croatian)
Currently translated at 100.0% (1080 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-12-18 00:44:25 +01:00
Mario 7db28d0e98 Translated using Weblate (German)
Currently translated at 100.0% (1080 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-12-18 00:44:25 +01:00
Bezruchenko Simon 31d26929af Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1078 of 1078 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2024-12-18 00:44:25 +01:00
gallegonovato 086da5f6a1 Translated using Weblate (Spanish)
Currently translated at 100.0% (1078 of 1078 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-12-18 00:44:25 +01:00
thehijacker 09421a44e2 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1078 of 1078 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-12-18 00:44:25 +01:00
Vito0912 fde51da479 Translated using Weblate (German)
Currently translated at 100.0% (1078 of 1078 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-12-18 00:44:25 +01:00
Mario f3536dc3a3 Translated using Weblate (German)
Currently translated at 100.0% (1078 of 1078 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-12-18 00:44:25 +01:00
Bezruchenko Simon a0c93e5dec Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1076 of 1076 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2024-12-18 00:44:25 +01:00
biuklija 63aa6aa950 Translated using Weblate (Croatian)
Currently translated at 100.0% (1076 of 1076 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-12-18 00:44:25 +01:00
Vito0912 680099cab4 Translated using Weblate (German)
Currently translated at 100.0% (1076 of 1076 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-12-18 00:44:25 +01:00
thehijacker 66f3f3eddf Translated using Weblate (Slovenian)
Currently translated at 100.0% (1076 of 1076 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-12-18 00:44:25 +01:00
Alex a400c149a6 Translated using Weblate (Russian)
Currently translated at 100.0% (1076 of 1076 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2024-12-18 00:44:25 +01:00
Petter Schaug-Pettersen 244b5ab36d Translated using Weblate (Norwegian Bokmål)
Currently translated at 63.1% (679 of 1076 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2024-12-18 00:44:25 +01:00
ugyes f26747627e Translated using Weblate (Hungarian)
Currently translated at 87.3% (940 of 1076 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2024-12-18 00:44:25 +01:00
biuklija f57a07c483 Translated using Weblate (Croatian)
Currently translated at 99.9% (1075 of 1076 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-12-18 00:44:25 +01:00
gallegonovato 080b879d8a Translated using Weblate (Spanish)
Currently translated at 100.0% (1076 of 1076 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-12-18 00:44:25 +01:00
advplyr 63b3f22504 Trim podcast descriptions #3720 2024-12-17 17:44:18 -06:00
Brinly 91f17efd5f feat: Added Australia and New Zealand podcast regions 2024-12-17 12:42:28 +01:00
Vito0912 858d697d0f DropDown for Year in Review (#3717)
* Accessibility updates
* Show "Share" button on large screen sizes

---------

Co-authored-by: advplyr <advplyr@protonmail.com>
2024-12-16 16:44:06 -06:00
mikiher ba55413e63 LazyBookshelf optimizations 2024-12-16 19:21:44 +02:00
advplyr 6cef1e3f12 Merge pull request #3724 from advplyr/feed_migration
Refactor Feed model to create new feed for collection
2024-12-15 17:59:17 -06:00
advplyr b39268ccb0 Remove old Feed/FeedEpisode/FeedMeta objects 2024-12-15 17:54:36 -06:00
advplyr de8a9304d2 Remove unused old feed methods 2024-12-15 17:05:57 -06:00
advplyr f8fbd3ac8c Migrate Feed updating and build xml to new model 2024-12-15 16:56:59 -06:00
advplyr 369c05936b Fix feed create entityUpdatedAt value 2024-12-15 14:07:46 -06:00
advplyr 837a180dc1 Refactor RssFeedManager.init to use new model only 2024-12-15 13:14:55 -06:00
advplyr 302b651e7b Fix library item unit test 2024-12-15 12:38:50 -06:00
advplyr 4c68ad46f4 Refactor RssFeedManager to use new model when closing feeds, fix close series feed when series is removed, update RssFeedManager to singleton 2024-12-15 12:37:01 -06:00
advplyr e50bd93958 Refactor Feed model to create new feed for series 2024-12-15 11:44:07 -06:00
advplyr d576625cb7 Refactor Feed model to create new feed for collection 2024-12-15 10:53:31 -06:00
advplyr ca2327aba3 Merge pull request #3721 from advplyr/refactor-feeds-from-item
Refactor Feed model to create new feed for library item
2024-12-14 17:25:10 -06:00
advplyr 9bd1f9e3d5 Refactor Feed model to create new feed for library item 2024-12-14 16:55:56 -06:00
advplyr c4610e6102 Update:Remove outline for focused modal content 2024-12-13 16:22:32 -06:00
advplyr 329bbea043 Fix:Downloading podcast episode when file extension is mp3 but enclosure type is not mp3 #3711 2024-12-13 16:06:00 -06:00
advplyr e616b53877 Accessibility update for book & series cards, home page shelf scroll #2268 #3699 2024-12-12 16:51:36 -06:00
advplyr eab86f90a8 Accessibility update for config side nav and modal, set focus on modal content on open 2024-12-12 15:16:49 -06:00
advplyr f97389cb2b More accessibility updates: adding roles for toolbars, bookshelf cards, author sort #2268 #3699 2024-12-11 17:24:48 -06:00
advplyr c5c3aab130 Update:Accessibility for buttons on item page, context menu dropdown, library filter/sort #3699 2024-12-10 17:20:13 -06:00
advplyr 4610e58337 Update:Home shelf labels use h2 tag, play & edit buttons overlaying item page updated to button tag with aria-label for accessibility #3699 2024-12-09 17:24:21 -06:00
advplyr 190a1000d9 Version bump v2.17.5 2024-12-08 09:03:05 -06:00
advplyr 455b96d1ab Merge pull request #3694 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-12-08 09:02:14 -06:00
thehijacker 8aaf62f243 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1074 of 1074 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-12-08 15:57:55 +01:00
Bezruchenko Simon e6d754113e Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1074 of 1074 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2024-12-08 15:57:55 +01:00
Clara Papke 5f72e30e63 Translated using Weblate (German)
Currently translated at 100.0% (1074 of 1074 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-12-08 15:57:55 +01:00
advplyr 57906540fe Add:Server setting to allow iframe & update UI to differentiate web client settings #3684 2024-12-08 08:57:45 -06:00
advplyr 726adbb3bf Merge pull request #3692 from mikiher/rss-remove-server-address
Remove serverAddress from Feeds and FeedEpisodes URLs
2024-12-08 08:24:41 -06:00
advplyr f7b7b85673 Add v2.17.5 migration to changelog 2024-12-08 08:19:23 -06:00
advplyr 5646466aa3 Update JSDocs for feeds endpoints 2024-12-08 08:05:33 -06:00
mikiher b38ce41731 Remove xml cache from Feed object 2024-12-08 09:48:58 +02:00
mikiher a8ab8badd5 always set req.originalHostPrefix 2024-12-08 09:23:39 +02:00
Nicholas Wallace 61729881cb Change: no compression when downloading library item as zip file 2024-12-07 16:52:31 -07:00
advplyr 5eca43082e Merge pull request #3687 from jaumet/Catalan-version
Catalan translation added
2024-12-07 15:19:27 -06:00
advplyr 6fa11934be Add:Catalan language option 2024-12-07 15:15:47 -06:00
advplyr ff7edc32a1 Merge pull request #3689 from Vito0912/feat/fixServercrashPlaybacksession
Resolved a server crash when a playback session lacked media metadata
2024-12-07 15:02:20 -06:00
mikiher 9b8e059efe Remove serverAddress from Feeds and FeedEpisodes URLs 2024-12-07 19:27:37 +02:00
Vito0912 7486d6345d Resolved a server crash when a playback session lacked associated media metadata. 2024-12-07 09:34:06 +01:00
Jaume 835490a9fc Catalan translation added
new file 
client/strings/ca.json
2024-12-07 01:45:41 +01:00
advplyr 3b4a5b8785 Support ALLOW_IFRAME env variable to not include frame-ancestors header #3684 2024-12-06 17:17:32 -06:00
advplyr 9a1c773b7a Fix:Server crash on uploadCover temp file mv failed #3685 2024-12-06 16:59:34 -06:00
advplyr 890b0b949e Version bump v2.17.4 2024-12-05 16:50:30 -06:00
advplyr b19e360bbb Merge pull request #3674 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-12-05 16:32:58 -06:00
SunSpring 1ff7952074 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1074 of 1074 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-12-05 23:15:39 +01:00
SunSpring 259d93d882 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1074 of 1074 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-12-05 23:15:38 +01:00
Tamanegii 14f60a593b Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1074 of 1074 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-12-05 23:15:38 +01:00
SunSpring 7334580c8c Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1074 of 1074 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-12-05 23:15:37 +01:00
Tamanegii f467c44543 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.9% (1073 of 1074 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-12-05 23:15:37 +01:00
Milo Ivir 867354e59d Translated using Weblate (Croatian)
Currently translated at 100.0% (1074 of 1074 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-12-05 23:15:36 +01:00
gallegonovato 67952cc577 Translated using Weblate (Spanish)
Currently translated at 100.0% (1074 of 1074 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-12-05 23:15:35 +01:00
Milo Ivir 079a15541c Translated using Weblate (Croatian)
Currently translated at 100.0% (1072 of 1072 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-12-05 23:15:35 +01:00
Mario 658ac04268 Translated using Weblate (German)
Currently translated at 100.0% (1072 of 1072 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-12-05 23:15:34 +01:00
Mario cbee6d8f5e Translated using Weblate (German)
Currently translated at 100.0% (1072 of 1072 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-12-05 23:15:33 +01:00
thehijacker 68413ae2f6 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1072 of 1072 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-12-05 23:15:33 +01:00
Henning 252a233282 Translated using Weblate (German)
Currently translated at 100.0% (1072 of 1072 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-12-05 23:15:32 +01:00
advplyr c35185fff7 Update prober to accept grp1 as an alternative tag to grouping #3681 2024-12-05 16:15:23 -06:00
advplyr 9774b2cfa5 Update JSDocs for groupFileItemsIntoLibraryItemDirs 2024-12-04 16:30:35 -06:00
advplyr 344890fb45 Update watcher files changed function to use the same grouping function as other scans 2024-12-04 16:25:17 -06:00
advplyr 5fa0897ad7 Merge pull request #3665 from mikiher/subdirectory-fixes-3
Subdirectory support for OIDC and SocketIO
2024-12-03 17:29:57 -06:00
advplyr 95c80a5b18 Merge pull request #3672 from Techwolfy/disc-folder-support
Support additional disc folder names
2024-12-03 17:28:32 -06:00
advplyr 0f1b64b883 Add test for grouping book library items 2024-12-03 17:21:57 -06:00
advplyr 615ed26f0f Update:Users table show count next to header 2024-12-02 17:35:35 -06:00
advplyr 84803cef82 Fix:Load year in review stats for playback sessions with null mediaMetadata 2024-12-02 17:23:25 -06:00
Techwolf 605bd73c11 Fix third instance of regex 2024-12-01 23:57:47 -08:00
Techwolf cc89db059b Fix second instance of regex 2024-12-01 18:41:38 -08:00
Techwolf a03146e09c Support additional disc folder names 2024-12-01 18:10:44 -08:00
advplyr 33aa4f1952 Merge master 2024-12-01 13:27:20 -06:00
advplyr c03f18b90a Merge pull request #3670 from advplyr/fix_remove_authors_no_books
Fix:Remove authors with no books when a books is removed #3668
2024-12-01 12:56:57 -06:00
advplyr 0dedb09a07 Update:batchUpdate endpoint validate req.body is an array of objects 2024-12-01 12:49:39 -06:00
advplyr 2b5484243b Add LibraryItemController test for delete/batchDelete/updateMedia endpoint functions to correctly remove authors & series with no books 2024-12-01 12:44:21 -06:00
advplyr c496db7c95 Fix:Remove authors with no books when a books is removed #3668
- Handles bulk delete, single delete, deleting library folder, and removing items with issues
- Also handles bulk editing and removing authors
2024-12-01 09:51:26 -06:00
advplyr ea4d5ff665 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-11-30 16:26:52 -06:00
advplyr 468a547864 Version bump v2.17.3 2024-11-30 16:26:48 -06:00
advplyr cd9999d192 Merge pull request #3643 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-11-30 16:23:45 -06:00
Charlie 31e302ea59 Translated using Weblate (French)
Currently translated at 100.0% (1072 of 1072 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-11-30 23:10:44 +01:00
Dmitry 1ff1ba66fd Translated using Weblate (Russian)
Currently translated at 100.0% (1072 of 1072 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2024-11-30 23:10:43 +01:00
Pierrick Guillaume a5457d7e22 Translated using Weblate (French)
Currently translated at 100.0% (1072 of 1072 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-11-30 23:10:43 +01:00
Soaibuzzaman ddcbfd4500 Translated using Weblate (Bengali)
Currently translated at 100.0% (1072 of 1072 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bn/
2024-11-30 23:10:42 +01:00
biuklija 293e530297 Translated using Weblate (Croatian)
Currently translated at 100.0% (1072 of 1072 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-11-30 23:10:41 +01:00
thehijacker 7278ad4ee7 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1072 of 1072 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-11-30 23:10:40 +01:00
Bezruchenko Simon 0449fb5ef9 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1072 of 1072 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2024-11-30 23:10:40 +01:00
gallegonovato d2c28fc69c Translated using Weblate (Spanish)
Currently translated at 100.0% (1072 of 1072 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-11-30 23:10:39 +01:00
Vito0912 60ba0163af Translated using Weblate (German)
Currently translated at 99.9% (1071 of 1072 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-11-30 23:10:39 +01:00
advplyr 02ca926d88 Merge pull request #3664 from advplyr/v2.17.3-fk-constraints-migration
Add migration to fix dropped foreign key constraints dropped in v2.17.0 migration
2024-11-30 16:10:30 -06:00
advplyr 4b52f31d58 Update v2.17.3 migration file to first check if constraints need to be updated, add unit test 2024-11-30 15:48:20 -06:00
mikiher 9917f2d358 Change migration to v2.17.4 2024-11-29 09:01:03 +02:00
mikiher 8c3ba67583 Fix label order 2024-11-29 05:48:04 +02:00
mikiher 6d8720b404 Subfolder support for OIDC auth 2024-11-29 04:28:50 +02:00
mikiher 843dd0b1b2 Keep original socket.io server for non-subdir clients 2024-11-29 04:13:00 +02:00
advplyr 70f466d03c Add migration for v2.17.3 to fix dropped fk constraints 2024-11-28 17:18:34 -06:00
advplyr ef82e8b0d0 Fix:Server crash deleting user with sessions 2024-11-27 16:48:07 -06:00
advplyr c643d4cec8 Merge pull request #3655 from glorenzen/fix/player-settings-modal
Fix player settings modal on share page
2024-11-26 17:12:17 -06:00
advplyr 718d8b5999 Update jump backward amount for share player 2024-11-26 17:05:50 -06:00
advplyr 2ba0f9157d Update share player to load user settings 2024-11-26 17:03:01 -06:00
Greg Lorenzen 53fdb5273c Remove player settings modal from MediaPlayerContainer 2024-11-26 04:04:55 +00:00
Greg Lorenzen fabdfd5517 Add player settings modal to PlayerUi 2024-11-26 04:04:44 +00:00
advplyr 950993f652 Update:View episode modal includes audio filename and size #3648 2024-11-25 17:26:06 -06:00
advplyr 5a968b002a Update readme.md 2024-11-25 13:29:06 -06:00
advplyr 3acd29fab3 Update readme.md 2024-11-25 13:27:33 -06:00
advplyr 315b21db00 Fix:API get media progress for episode 2024-11-24 15:05:19 -06:00
advplyr f9aaeb3a34 Update:Set Content-Security-Policy header to disallow iframes 2024-11-23 11:17:13 -06:00
advplyr d19bb909b3 Fix:Server crash deleting library that has playback sessions #3634 2024-11-22 17:20:31 -06:00
advplyr f850db23fe Version bump v2.17.2 2024-11-21 15:24:45 -06:00
advplyr 5f81010f6a Merge pull request #3631 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-11-21 15:17:42 -06:00
burghy86 daf2493f50 Translated using Weblate (Italian)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2024-11-21 22:05:10 +01:00
DR 57222f3611 Translated using Weblate (Hebrew)
Currently translated at 72.8% (780 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/he/
2024-11-21 22:05:10 +01:00
Mohamad Dahhan 62b185979e Translated using Weblate (Arabic)
Currently translated at 14.2% (153 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ar/
2024-11-21 22:05:10 +01:00
DR ebcc85acc4 Translated using Weblate (Hebrew)
Currently translated at 70.5% (756 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/he/
2024-11-21 22:05:10 +01:00
advplyr 33a7ba4acd Merge pull request #3632 from sevenlayercookie/master
on iOS, do not restrict file types for upload
2024-11-21 15:05:05 -06:00
advplyr 1d4e6993fc Upload page UI updates for mobile 2024-11-21 14:56:43 -06:00
advplyr 784b761629 Fix:Unable to edit series sequence #3636 2024-11-21 14:19:40 -06:00
Harrison Rose 268fb2ce9a on iOS, hide UI on upload page related to folder selection (since iOS Webkit does not support folder selection) 2024-11-21 04:43:03 +00:00
Harrison Rose fc5f35b388 on iOS, do not restrict file types for upload 2024-11-21 02:06:53 +00:00
advplyr ff026a06bb Fix v2.17.0 migration to ensure mediaItemShares table exists 2024-11-20 16:48:09 -06:00
advplyr b148a57c98 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-11-19 16:48:09 -06:00
advplyr ee6e2d2983 Update:Persist podcast episode table sort and filter options in local storage #1321 2024-11-19 16:48:05 -06:00
advplyr ea3a6fd75e Merge pull request #3603 from nichwall/pr_template
PR Template
2024-11-19 16:15:29 -06:00
advplyr 22f85d3af9 Version bump v2.17.1 2024-11-18 08:02:46 -06:00
advplyr 75f4c2ee99 Merge pull request #3626 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-11-18 08:01:58 -06:00
thehijacker dd3467efa2 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-11-18 15:00:12 +01:00
Clara Papke 4adb15c11b Translated using Weblate (German)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-11-18 15:00:12 +01:00
advplyr a5e38d1473 Fix:Error adding new series if a series has a null title #3622 2024-11-18 07:59:02 -06:00
advplyr 778256ca16 Fix:Server crash on new libraries when getting filter data #3623 2024-11-18 07:42:24 -06:00
advplyr 2b0ba7d1e2 Version bump v2.17.0 2024-11-17 16:25:40 -06:00
advplyr 772f3fedb3 Merge pull request #3613 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-11-17 16:10:58 -06:00
Mohamad Dahhan fe25d1dccd Translated using Weblate (Arabic)
Currently translated at 11.9% (128 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ar/
2024-11-17 22:00:57 +00:00
Julio Cesar de jesus 10a7cd0987 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2024-11-17 22:00:56 +00:00
Julio Cesar de jesus 6786df6965 Translated using Weblate (Spanish)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-11-17 22:00:56 +00:00
Mohamad Dahhan 4cfd18c81a Translated using Weblate (Arabic)
Currently translated at 3.8% (41 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ar/
2024-11-17 22:00:55 +00:00
Paulo Henrique Dos Santos Garcia d25a21cd32 Translated using Weblate (Portuguese (Brazil))
Currently translated at 72.6% (778 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pt_BR/
2024-11-17 22:00:55 +00:00
DR b5f0a6f4a6 Translated using Weblate (Hebrew)
Currently translated at 70.5% (756 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/he/
2024-11-17 22:00:54 +00:00
SunSpring cf19dd23cf Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-11-17 22:00:53 +00:00
Bezruchenko Simon 3e6a2d670e Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2024-11-17 22:00:53 +00:00
biuklija 26ef33a4b6 Translated using Weblate (Croatian)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-11-17 22:00:52 +00:00
gallegonovato 9940f1d6db Translated using Weblate (Spanish)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-11-17 22:00:52 +00:00
advplyr 75eef8d722 Fix:Book library sort by publishedYear #3620
- Updated sort to cast publishedYear to INTEGER
2024-11-17 16:00:44 -06:00
advplyr 46a3c3de33 Merge pull request #3597 from nichwall/mediatype_uuid_migration
`MediaId` UUID migration
2024-11-17 15:50:10 -06:00
advplyr 2b7e3f0efe Update uuid migration to v2.17.0 and for all tables still using UUIDv4 2024-11-17 15:45:21 -06:00
Nicholas Wallace d5fbc1d455 Add: statement about workflows passing 2024-11-17 12:22:15 -07:00
advplyr bbe59499ad Merge pull request #3615 from mikiher/fullupdatefromold-fix
Use a simpler database fetch in fullUpdateFromOld
2024-11-16 16:26:13 -06:00
advplyr 4c88e9c8d2 Merge pull request #3594 from nichwall/filter_data_longer_cache
Increase cache time for `filterdata` in library
2024-11-16 16:18:54 -06:00
mikiher 5ccf5d7150 Use a simpler database fetch in fullUpdateFromOld 2024-11-16 06:26:32 +02:00
Greg Lorenzen 27c9381e1d Merge branch 'master' into multi-select-keyboard-navigation 2024-11-15 12:06:25 -08:00
advplyr 45f8b06d56 Fix:CBC Radio podcast RSS feeds not accepting our user-agent string #3322 2024-11-15 08:30:54 -06:00
advplyr 2a62992c75 Merge pull request #3576 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-11-12 17:15:21 -06:00
thehijacker 997afc1b2f Translated using Weblate (Slovenian)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-11-13 00:07:56 +01:00
burghy86 f941ea6500 Translated using Weblate (Italian)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2024-11-13 00:07:56 +01:00
thehijacker 92d083164f Translated using Weblate (Slovenian)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-11-13 00:07:56 +01:00
Tamanegii 2dd30c7a26 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 71.3% (764 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hant/
2024-11-13 00:07:56 +01:00
gallegonovato 3f0347253e Translated using Weblate (Spanish)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-11-13 00:07:56 +01:00
Nicholas W bb6377fb22 Deleted translation using Weblate (English (United States)) 2024-11-13 00:07:56 +01:00
Pavel Vachek 12c2071358 Translated using Weblate (Czech)
Currently translated at 83.4% (894 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2024-11-13 00:07:56 +01:00
kuci-JK ec4c4a4d5a Translated using Weblate (Czech)
Currently translated at 83.4% (894 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2024-11-13 00:07:56 +01:00
Languages add-on 876fcf3296 Added translation using Weblate (Arabic) 2024-11-13 00:07:56 +01:00
Bezruchenko Simon 023ceed286 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2024-11-13 00:07:56 +01:00
thehijacker cc42aa32ef Translated using Weblate (Slovenian)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-11-13 00:07:56 +01:00
Bezruchenko Simon 7cbb1c60a2 Translated using Weblate (Ukrainian)
Currently translated at 88.6% (949 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2024-11-13 00:07:56 +01:00
thehijacker 4ad130a11a Translated using Weblate (Slovenian)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-11-13 00:07:56 +01:00
gallegonovato 9bf46b6367 Translated using Weblate (Spanish)
Currently translated at 98.4% (1054 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-11-13 00:07:55 +01:00
Charlie 4be2909b24 Translated using Weblate (French)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-11-13 00:07:55 +01:00
gallegonovato f161158d83 Translated using Weblate (Spanish)
Currently translated at 98.3% (1053 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-11-13 00:07:55 +01:00
Dmitry 3a5f6ab6f1 Translated using Weblate (Russian)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2024-11-13 00:07:55 +01:00
Charlie c1b626da14 Translated using Weblate (French)
Currently translated at 97.5% (1045 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-11-13 00:07:55 +01:00
advplyr 48e0a3c450 Merge pull request #3604 from mikiher/episode-download-oom
Remove unnecessary episode_download_queue_updated socket event causing OOM
2024-11-12 17:07:49 -06:00
mikiher 8626fa3e00 Add episode_download_queue_cleared socket event 2024-11-12 07:37:38 +02:00
mikiher b50d7f0927 Remove unnecessary socket event causing OOM 2024-11-12 07:25:10 +02:00
Nicholas Wallace 0d54b57151 Add: PR template 2024-11-11 21:20:53 -07:00
advplyr 5a2bdc58da Merge pull request #3595 from nichwall/workflow_trigger_updates
Only run CodeQL and Integration actions if code changed
2024-11-10 17:24:34 -06:00
advplyr 01446c02aa Merge pull request #3599 from mikiher/add-user-cache
Add in-memory user cache
2024-11-10 17:23:58 -06:00
mikiher a382482173 Add in-memory user cache 2024-11-10 08:34:47 +02:00
advplyr 2e970cbb39 Fix:Series Progress filters incorrect - showing for any users progress #2923 2024-11-09 18:03:50 -06:00
Nicholas Wallace 161a3f4da9 Update migrations changelog for 2.16.3 2024-11-09 13:20:59 -07:00
Nicholas Wallace 713bdcbc41 Add: migration for mediaId to use UUID instead of UUIDV4 2024-11-09 13:10:46 -07:00
Nicholas Wallace 1fa67535f9 Update: only run CodeQL and Integration actions if code changed 2024-11-08 11:20:02 -07:00
Nicholas Wallace e8d8b67c0a Add: check for deleted items 2024-11-08 10:49:12 -07:00
Nicholas Wallace e57d4cc544 Add: filter update check to podcast libraries 2024-11-08 09:33:34 -07:00
Nicholas Wallace 435b7fda7e Add: check for changes to library items 2024-11-08 09:09:18 -07:00
advplyr d7e810fc2f Update readme localization chart to for web client only 2024-11-08 08:04:50 -06:00
advplyr 850ed48955 Fix:Podcast episodes duplicated when a scan runs while the episode is downloading #2785 2024-11-07 17:26:51 -06:00
advplyr a5ebd89817 Update FolderWatcher to singleton 2024-11-07 16:32:05 -06:00
advplyr a8ec07cfc9 Merge pull request #3589 from nichwall/migration_table_check_fix
Check that `migrationsMeta` table is well formed instead of just existing
2024-11-07 16:05:07 -06:00
Nicholas Wallace 41fe5373a7 Add: check that migrationsMeta table is well formed 2024-11-06 22:06:58 -07:00
Greg Lorenzen 0812e189f7 Add keyboard input to MultiSelect component 2024-11-07 03:38:30 +00:00
Greg Lorenzen 588def6d33 Merge branch 'advplyr:master' into multi-select-keyboard-navigation 2024-11-06 19:37:26 -08:00
advplyr 0c244cbf95 Merge pull request #3585 from snakehnb/master
Avoid parsing first and last names in Chinese, Japanese and Korean la…
2024-11-06 17:19:58 -06:00
snakehnb 7ef14aabed Avoid parsing first and last names in Chinese, Japanese and Korean languages 2024-11-04 16:13:14 +08:00
advplyr 978c2b05f2 Merge pull request #3584 from mikiher/author-image-performance
Improve author image performance
2024-11-03 13:25:06 -06:00
mikiher 68fd1d67cb Remove token from author image URLs 2024-11-03 08:46:09 +02:00
mikiher bf8407274e No auth for author images 2024-11-03 08:45:43 +02:00
mikiher 3bc2941445 No db access for author image if in disk cache 2024-11-03 08:44:57 +02:00
advplyr 654b1d6b34 Merge pull request #3580 from mikiher/cover-image-performance
Improve cover image performance
2024-11-02 13:10:00 -05:00
advplyr 7a49681dd2 Fix includes 2024-11-02 13:02:40 -05:00
advplyr 7a1623e6a1 Move cover path func to LibraryItem model 2024-11-02 12:56:40 -05:00
mikiher c25acb41fa Remove token from cover image urls 2024-11-02 15:37:14 +02:00
mikiher 4224b8a486 No auth and req.user for cover images 2024-11-02 15:17:11 +02:00
mikiher 9e990d7927 Optimize LibraryItemController.getCover 2024-11-02 09:05:30 +02:00
mikiher 431ae97593 add Database.getLibraryItemCoverPath 2024-11-02 09:02:23 +02:00
advplyr 633ff810cf Merge pull request #3574 from 4ch1m/add_mpeg_audio_type
'mpg' and 'mpeg' added as supported audio-type/file-extension
2024-11-01 09:17:52 -05:00
advplyr f3d2b781ab Add mime types for MPEG/MPG 2024-11-01 09:12:40 -05:00
Greg Lorenzen a0b3960ee4 Fix enter key and focus for edit modal 2024-10-31 16:29:48 +00:00
Greg Lorenzen e55db0afdc Add focus and enter key support to the add button in MultiSelectQueryInput 2024-10-31 15:44:19 +00:00
Greg Lorenzen ae9efe6359 Add keyboard focus to MultiSelectQueryInput edit and close 2024-10-31 15:30:51 +00:00
Achim 32105665c1 'mpg' and 'mpeg' added as supported audio-type/file-extension 2024-10-31 15:29:40 +01:00
advplyr 2b18efdfdc Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-10-30 15:27:22 -05:00
advplyr e0c66ea6df Fix:Global search unclickable from trackpad due to blur event closing menu 2024-10-30 15:27:18 -05:00
advplyr 667c7361d7 Merge pull request #3568 from nichwall/docker_compose_update
Update: user directive in docker compose file
2024-10-30 13:35:02 -05:00
Nicholas Wallace 63fdf0d18e Update: user directive in docker compose file 2024-10-29 18:22:38 -07:00
advplyr e05cb0ef4d Version bump v2.16.2 2024-10-29 16:11:36 -05:00
advplyr 925c7f7dc7 Merge pull request #3566 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-10-29 16:04:27 -05:00
Charlie c69e97ea24 Translated using Weblate (French)
Currently translated at 95.7% (1025 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-10-29 22:00:15 +01:00
advplyr 5e2aebc724 Merge pull request #3565 from mikiher/handle-download-errors-2
Fix incorrect call to handleDownloadError
2024-10-29 15:55:37 -05:00
advplyr 6eba467b91 Fix:Session sync for streaming podcast episodes using incorrect duration #3560 2024-10-29 15:41:31 -05:00
mikiher 524cf5ec5b Fix incorrect call to handleDownloadError 2024-10-29 21:42:44 +02:00
advplyr 50fd659749 Version bump v2.16.1 2024-10-28 17:05:47 -05:00
advplyr 8169afb59b Merge pull request #3554 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-10-28 17:01:24 -05:00
SunSpring d40086fea1 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-10-28 23:00:51 +01:00
biuklija 399c40debd Translated using Weblate (Croatian)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-10-28 23:00:50 +01:00
Vito0912 d986673dfd Translated using Weblate (German)
Currently translated at 99.8% (1069 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-10-28 23:00:50 +01:00
thehijacker f83f4d41f1 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-10-28 23:00:49 +01:00
Dmitry 7ed711730e Translated using Weblate (Russian)
Currently translated at 100.0% (1071 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2024-10-28 23:00:49 +01:00
Frantisek Nagy 94e2ea9df3 Translated using Weblate (Hungarian)
Currently translated at 75.6% (810 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2024-10-28 23:00:48 +01:00
Bálint Kristóf 8c8c4a15c3 Translated using Weblate (Hungarian)
Currently translated at 75.6% (810 of 1071 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2024-10-28 23:00:48 +01:00
advplyr 2a9159f106 Merge pull request #3553 from mikiher/handle-download-errors
Add proper error handing for file downloads
2024-10-28 17:00:40 -05:00
advplyr 8f113d17c2 Fix:Ensure library has all settings defined when validating settings for update #3559 2024-10-28 16:57:37 -05:00
mikiher 9084055b95 Add proper error handing for file downloads 2024-10-28 08:03:31 +02:00
advplyr fba9cce82e Version bump v2.16.0 2024-10-27 15:15:44 -05:00
advplyr 92cfb46c14 Merge pull request #3542 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-10-27 15:10:16 -05:00
thehijacker 449dc1a0e2 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1070 of 1070 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-10-26 20:34:51 +00:00
SunSpring d9c345b0f3 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1070 of 1070 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-10-26 20:34:50 +00:00
Ahetek 69a639f76c Translated using Weblate (Polish)
Currently translated at 75.5% (806 of 1067 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2024-10-26 20:34:50 +00:00
Mathias Franco d576efe759 Translated using Weblate (Dutch)
Currently translated at 100.0% (1067 of 1067 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2024-10-26 20:34:49 +00:00
biuklija 9ba2ecbc21 Translated using Weblate (Croatian)
Currently translated at 100.0% (1067 of 1067 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-10-26 20:34:48 +00:00
Henning 84003cd67e Translated using Weblate (German)
Currently translated at 99.5% (1062 of 1067 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-10-26 20:34:48 +00:00
kuci-JK be8c447216 Translated using Weblate (Czech)
Currently translated at 83.5% (891 of 1067 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2024-10-26 20:34:47 +00:00
SunSpring e534daf5d4 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1067 of 1067 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-10-26 20:34:47 +00:00
Mathias Franco 1fefc1af92 Translated using Weblate (Dutch)
Currently translated at 92.8% (991 of 1067 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2024-10-26 20:34:46 +00:00
thehijacker e76c4ed2a4 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1064 of 1064 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-10-26 20:34:46 +00:00
Mathias Franco e1caf13233 Translated using Weblate (Dutch)
Currently translated at 83.7% (891 of 1064 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2024-10-26 20:34:45 +00:00
Plazec a7a2fbbca8 Translated using Weblate (Czech)
Currently translated at 83.3% (887 of 1064 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2024-10-26 20:34:44 +00:00
Mathias Franco 28d93d9160 Translated using Weblate (Dutch)
Currently translated at 83.0% (884 of 1064 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2024-10-26 20:34:44 +00:00
gallegonovato 4e90f90c28 Translated using Weblate (Spanish)
Currently translated at 97.0% (1033 of 1064 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-10-26 20:34:43 +00:00
Plazec 2243fdddd3 Translated using Weblate (Czech)
Currently translated at 82.8% (882 of 1064 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2024-10-26 20:34:43 +00:00
biuklija 39be3a2ef9 Translated using Weblate (Croatian)
Currently translated at 100.0% (1064 of 1064 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-10-26 20:34:42 +00:00
Austin Spencer ecc30b85bc Allow users to create ereaders (#3531)
* add create eReader permission toggle

* add english label for create EReader permission

* add ereader table to account with user specific modal

* add createEreader permission

* create api endpoint and logic for updating user eReader devices

* add translated label for createEreader permission

* handle name duplicates and remove helper func

* toast for duplicate name error caught on server

* restrict user ereader updates to devices with sole ownership

* remove label

* fix other devices logic and client socket emitter

* fix for deleting ereaders

* User create ereader endpoint validate accessibility

---------

Co-authored-by: advplyr <advplyr@protonmail.com>
2024-10-26 15:34:34 -05:00
advplyr 6905b288d2 Fix:Latest version displayed when update is available 2024-10-26 14:57:04 -05:00
advplyr 0782146682 Update:Pass mark as finished library settings to media progress update #837 2024-10-25 17:27:50 -05:00
advplyr 91aea4f754 Add:Library settings for mark as finished when time remaining or percent complete #837 2024-10-24 17:19:51 -05:00
advplyr 6ca277a21d Update:Library settings tab settings in 2 columns and cleanup 2024-10-23 17:11:41 -05:00
advplyr c47c75aefe Update:More strings localized #3544 2024-10-22 17:24:31 -05:00
advplyr 9896e4381b Update:Setup variables to control when a media item is marked as finished. By time remaining or progress percentage #837 2024-10-21 17:48:02 -05:00
advplyr 953ffe889e Update:Book series embeds in grouping meta tag as semicolon deliminated, book meta tag parser falls back to using grouping tag for series if set #3473 2024-10-20 16:58:13 -05:00
advplyr 72e59e77a7 Merge pull request #3536 from nichwall/migration_indexes
Migration indexes
2024-10-19 15:51:12 -05:00
advplyr 35e2681ea9 Update index creation migration to be idempotent 2024-10-19 15:45:14 -05:00
Nicholas Wallace 84012d9090 Fix: podcast episode index name 2024-10-19 11:38:34 -07:00
Nicholas Wallace e8a1ea3b54 Fix: table naming 2024-10-19 11:20:29 -07:00
Nicholas Wallace ea6882d9ab Update changelog 2024-10-19 11:20:22 -07:00
Nicholas Wallace 1fa80e31d1 Add: migrations for authors, series, and podcast episodes 2024-10-19 10:40:17 -07:00
advplyr d80752cc9d Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-10-18 16:25:12 -05:00
advplyr b764e848c7 Version bump v2.15.1 2024-10-18 16:25:07 -05:00
advplyr b037c4e8a3 Merge pull request #3532 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-10-18 16:23:32 -05:00
thehijacker 6ba2360790 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1064 of 1064 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-10-18 23:10:42 +02:00
SunSpring ca4eb507f0 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1064 of 1064 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-10-18 23:10:41 +02:00
biuklija 965b094470 Translated using Weblate (Croatian)
Currently translated at 99.9% (1063 of 1064 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-10-18 23:10:41 +02:00
gallegonovato 0fe313ecfd Translated using Weblate (Spanish)
Currently translated at 96.8% (1031 of 1064 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-10-18 23:10:40 +02:00
SunSpring 35a2f8d44f Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1023 of 1023 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-10-18 23:10:40 +02:00
mikiher 50797879d5 Add a REINDEX NOCASE v2.15.1 migration and update v2.15.0 migration (#3533)
* Add REINDEX NOCASE migration and update v2.15.0 migration

* Update v2.15.0 migration test

* Fix typo
2024-10-18 16:10:29 -05:00
Nicholas W 9327331ee9 Localization updates for 2.15.0 (#3520)
* Add: episode edit dropdowns

* Update: lazy episode table and row

* Various string updates

* Batch quick match strings

* Author card strings

* Update translation key for quick match episodes confirm

---------

Co-authored-by: advplyr <advplyr@protonmail.com>
2024-10-17 17:03:08 -05:00
advplyr 1c15007e32 Merge pull request #3529 from asoluter/patch-1
Fix "Extract Cover Error" for files with multiple embedded covers
2024-10-17 16:44:58 -05:00
advplyr 2151ffa114 Merge pull request #3530 from mikiher/subdirectory-fixes-2
Add server proxies for all server paths
2024-10-17 16:00:48 -05:00
mikiher 49ed208a54 Add dev proxies for all server path 2024-10-17 11:25:57 +03:00
Ihor Sofiichenko d668462529 Fix Extract Cover Error for files with multiple embedded covers 2024-10-17 00:27:21 -07:00
advplyr f2102a0a23 Merge pull request #3512 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-10-16 17:42:46 -05:00
biuklija 5efc6b82c1 Translated using Weblate (Croatian)
Currently translated at 100.0% (1023 of 1023 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-10-17 00:42:17 +02:00
thehijacker 1e4e9768da Translated using Weblate (Slovenian)
Currently translated at 100.0% (1023 of 1023 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-10-17 00:42:17 +02:00
Daniel Schosser cc5109c305 Translated using Weblate (German)
Currently translated at 98.8% (1011 of 1023 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-10-17 00:42:16 +02:00
Mathias Franco e858d6a1d5 Translated using Weblate (Dutch)
Currently translated at 68.7% (703 of 1023 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2024-10-17 00:42:15 +02:00
biuklija b4cd5d2862 Translated using Weblate (Croatian)
Currently translated at 100.0% (1023 of 1023 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-10-17 00:42:14 +02:00
DiamondtipDR 0633a44cfb Translated using Weblate (Spanish)
Currently translated at 100.0% (1023 of 1023 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-10-17 00:42:13 +02:00
Vito0912 5748126b83 Translated using Weblate (German)
Currently translated at 97.8% (1001 of 1023 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-10-17 00:42:12 +02:00
thehijacker 06375743a3 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1023 of 1023 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-10-17 00:42:12 +02:00
Ahetek 2a41c186aa Translated using Weblate (Polish)
Currently translated at 77.6% (794 of 1023 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2024-10-17 00:42:11 +02:00
burghy86 af51b7254c Translated using Weblate (Italian)
Currently translated at 100.0% (1023 of 1023 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2024-10-17 00:42:11 +02:00
Charlie f63dfd769f Translated using Weblate (French)
Currently translated at 100.0% (1023 of 1023 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-10-17 00:42:10 +02:00
apineiro97 a1512f3174 Translated using Weblate (Spanish)
Currently translated at 100.0% (1023 of 1023 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-10-17 00:42:09 +02:00
gallegonovato 245751e2ce Translated using Weblate (Spanish)
Currently translated at 100.0% (1023 of 1023 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-10-17 00:42:09 +02:00
Alexander Künzel 37001d9425 Translated using Weblate (German)
Currently translated at 97.0% (993 of 1023 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-10-17 00:42:08 +02:00
advplyr 9d1f51c6ba Add in /dev proxy for development 2024-10-16 17:42:00 -05:00
advplyr cb234fe1fc Merge pull request #3521 from mikiher/subdirectory-fixes
Fixes and cleanup for subdirectory serving support
2024-10-15 16:55:54 -05:00
advplyr cb85e0255b Fix share URLs on dev 2024-10-15 16:52:04 -05:00
advplyr 61b4cfdab7 Merge pull request #3518 from glorenzen/fix-decade-filter
Fix and simplify filter logic for publishedDecades
2024-10-15 16:19:36 -05:00
advplyr d2c405c126 Fix decade filter and query by casting publishedYear to Int 2024-10-15 16:12:56 -05:00
mikiher cbca560f92 server.js: add base path to all non-base-path requests 2024-10-15 06:40:14 +03:00
mikiher 2d7b63b4cf Add base path to socket.io connections on client and server 2024-10-15 05:50:23 +03:00
Greg Lorenzen 217038b085 Fix and simplify filter logic for publishedDecades 2024-10-14 20:58:09 +00:00
advplyr 13dd4edd6a Fix:Ignore dot files in migrations folder #3510 2024-10-14 14:46:55 -05:00
advplyr a7288b4fbf Merge pull request #3514 from koralowiec/chore/docs-nginx-client-max-body-size
chore(docs): add client_max_body_size parameter in nginx config
2024-10-14 13:04:35 -05:00
koralowiec 3020e8104e chore(docs): change indentation in nginx config example 2024-10-14 17:22:39 +00:00
koralowiec 8fdeeaaf38 chore(docs): add client_max_body_size in nginx example 2024-10-14 17:20:08 +00:00
mikiher 42616b59de Cleanup: remove explicit localhost:3333 and remove unnessesary if(dev) blocks 2024-10-14 13:30:17 +03:00
mikiher bf16681bea Merge branch 'subdirectory-fixes' of https://github.com/mikiher/audiobookshelf into subdirectory-fixes 2024-10-14 13:19:30 +03:00
mikiher 027190b5a4 Merge branch 'advplyr:master' into subdirectory-fixes 2024-10-14 13:18:04 +03:00
mikiher 241c02be30 nuxt.config.js: more cleanup and additional proxies 2024-10-14 13:12:10 +03:00
advplyr dd87268848 Merge pull request #3508 from mikiher/fix-share-player-chapters
Fix next/previous chapter behavior on public share player
2024-10-13 14:29:57 -05:00
mikiher f2ac24e623 Fix next/previous chapter behavior on public share player 2024-10-13 10:56:38 +03:00
mikiher 99ffd3050c Cleanup: Define routerBasePath constant in nuxt.config.js 2024-10-12 11:46:44 +03:00
mikiher 69dd82d329 Remove unneeded /dev routing 2024-10-12 11:18:49 +03:00
207 changed files with 9250 additions and 2810 deletions
+33
View File
@@ -0,0 +1,33 @@
<!--
For Work In Progress Pull Requests, please use the Draft PR feature,
see https://github.blog/2019-02-14-introducing-draft-pull-requests/ for further details.
If you do not follow this template, the PR may be closed without review.
Please ensure all checks pass.
If you are a new contributor, the workflows will need to be manually approved before they run.
-->
## Brief summary
<!-- Please provide a brief summary of what your PR attempts to achieve. -->
## Which issue is fixed?
<!-- Which issue number does this PR fix? Ex: "Fixes #1234" -->
## In-depth Description
<!--
Describe your solution in more depth.
How does it work? Why is this the best solution?
Does it solve a problem that affects multiple users or is this an edge case for your setup?
-->
## How have you tested this?
<!-- Please describe in detail with reproducible steps how you tested your changes. -->
## Screenshots
<!-- If your PR includes any changes to the web client, please include screenshots or a short video from before and after your changes. -->
+45 -32
View File
@@ -1,11 +1,25 @@
name: "CodeQL" name: 'CodeQL'
on: on:
push: push:
branches: [ 'master' ] branches: ['master']
# Only build when files in these directories have been changed
paths:
- client/**
- server/**
- test/**
- index.js
- package.json
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ 'master' ] branches: ['master']
# Only build when files in these directories have been changed
paths:
- client/**
- server/**
- test/**
- index.js
- package.json
schedule: schedule:
- cron: '16 5 * * 4' - cron: '16 5 * * 4'
@@ -21,45 +35,44 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
language: [ 'javascript' ] language: ['javascript']
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Use only 'java' to analyze code written in Java, Kotlin or both # Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file. # By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file. # Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality # queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # ️ Command-line programs to run using the OS shell.
# If this step fails, then you should remove it and run the build manually (see below) # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ️ Command-line programs to run using the OS shell. # If the Autobuild fails above, remove it and uncomment the following three lines.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# If the Autobuild fails above, remove it and uncomment the following three lines. # - run: |
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
# - run: | - name: Perform CodeQL Analysis
# echo "Run, Build Application using script" uses: github/codeql-action/analyze@v2
# ./location_of_script_within_repo/buildscript.sh with:
category: '/language:${{matrix.language}}'
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"
+7
View File
@@ -5,6 +5,13 @@ on:
push: push:
branches-ignore: branches-ignore:
- 'dependabot/**' # Don't run dependabot branches, as they are already covered by pull requests - 'dependabot/**' # Don't run dependabot branches, as they are already covered by pull requests
# Only build when files in these directories have been changed
paths:
- client/**
- server/**
- test/**
- index.js
- package.json
jobs: jobs:
build: build:
+1
View File
@@ -7,6 +7,7 @@
/podcasts/ /podcasts/
/media/ /media/
/metadata/ /metadata/
/plugins/
/client/.nuxt/ /client/.nuxt/
/client/dist/ /client/dist/
/dist/ /dist/
+1 -1
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="w-full h-16 bg-primary relative"> <div class="w-full h-16 bg-primary relative">
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-60"> <div id="appbar" role="toolbar" aria-label="Appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-60">
<div class="flex h-full items-center"> <div class="flex h-full items-center">
<nuxt-link to="/"> <nuxt-link to="/">
<img src="~static/icon.svg" :alt="$strings.ButtonHome" class="w-8 min-w-8 h-8 mr-2 sm:w-10 sm:min-w-10 sm:h-10 sm:mr-4" /> <img src="~static/icon.svg" :alt="$strings.ButtonHome" class="w-8 min-w-8 h-8 mr-2 sm:w-10 sm:min-w-10 sm:h-10 sm:mr-4" />
@@ -17,7 +17,7 @@
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24e"> <div v-else-if="isAlternativeBookshelfView" class="w-full mb-24e">
<template v-for="(shelf, index) in supportedShelves"> <template v-for="(shelf, index) in supportedShelves">
<widgets-item-slider :shelf-id="shelf.id" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" :type="shelf.type" class="bookshelf-row pl-8e my-6e" @selectEntity="(payload) => selectEntity(payload, index)"> <widgets-item-slider :shelf-id="shelf.id" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" :type="shelf.type" class="bookshelf-row pl-8e my-6e" @selectEntity="(payload) => selectEntity(payload, index)">
<p class="font-semibold text-gray-100">{{ $strings[shelf.labelStringKey] }}</p> <h2 class="font-semibold text-gray-100">{{ $strings[shelf.labelStringKey] }}</h2>
</widgets-item-slider> </widgets-item-slider>
</template> </template>
</div> </div>
+5 -5
View File
@@ -37,18 +37,18 @@
<div class="relative"> <div class="relative">
<div class="relative text-center categoryPlacard transform z-30 top-0 left-4e md:left-8e w-44e rounded-md"> <div class="relative text-center categoryPlacard transform z-30 top-0 left-4e md:left-8e w-44e rounded-md">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em 0.5em` }"> <div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em 0.5em` }">
<p :style="{ fontSize: 0.9 + 'em' }">{{ $strings[shelf.labelStringKey] }}</p> <h2 :style="{ fontSize: 0.9 + 'em' }">{{ $strings[shelf.labelStringKey] }}</h2>
</div> </div>
</div> </div>
<div class="bookshelfDividerCategorized h-6e w-full absolute top-0 left-0 right-0 z-20"></div> <div class="bookshelfDividerCategorized h-6e w-full absolute top-0 left-0 right-0 z-20"></div>
</div> </div>
<div v-show="canScrollLeft && !isScrolling" class="hidden sm:flex absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollLeft"> <button v-show="canScrollLeft && !isScrolling" :aria-label="$strings.ButtonScrollLeft" class="hidden sm:flex absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollLeft">
<span class="material-symbols text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_left</span> <span class="material-symbols text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_left</span>
</div> </button>
<div v-show="canScrollRight && !isScrolling" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollRight"> <button v-show="canScrollRight && !isScrolling" :aria-label="$strings.ButtonScrollRight" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollRight">
<span class="material-symbols text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_right</span> <span class="material-symbols text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_right</span>
</div> </button>
</div> </div>
</template> </template>
+7 -1
View File
@@ -42,8 +42,11 @@
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> <nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p class="text-sm">{{ $strings.ButtonAdd }}</p> <p class="text-sm">{{ $strings.ButtonAdd }}</p>
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p class="text-sm">{{ $strings.ButtonDownloadQueue }}</p>
</nuxt-link>
</div> </div>
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-40 flex items-center justify-end md:justify-start px-2 md:px-8"> <div id="toolbar" role="toolbar" aria-label="Library Toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-40 flex items-center justify-end md:justify-start px-2 md:px-8">
<!-- Series books page --> <!-- Series books page -->
<template v-if="selectedSeries"> <template v-if="selectedSeries">
<p class="pl-2 text-base md:text-lg"> <p class="pl-2 text-base md:text-lg">
@@ -265,6 +268,9 @@ export default {
isPodcastLatestPage() { isPodcastLatestPage() {
return this.$route.name === 'library-library-podcast-latest' return this.$route.name === 'library-library-podcast-latest'
}, },
isPodcastDownloadQueuePage() {
return this.$route.name === 'library-library-podcast-download-queue'
},
isAuthorsPage() { isAuthorsPage() {
return this.page === 'authors' return this.page === 'authors'
}, },
+3 -3
View File
@@ -1,6 +1,6 @@
<template> <template>
<div> <div role="toolbar" aria-orientation="vertical" aria-label="Config Sidebar">
<div class="w-44 fixed left-0 top-16 bg-bg bg-opacity-100 md:bg-opacity-70 shadow-lg border-r border-white border-opacity-5 py-3 transform transition-transform mb-12 overflow-y-auto" :class="wrapperClass + ' ' + (streamLibraryItem ? 'h-[calc(100%-270px)]' : 'h-[calc(100%-110px)]')" v-click-outside="clickOutside"> <div role="navigation" aria-label="Config Navigation" class="w-44 fixed left-0 top-16 bg-bg bg-opacity-100 md:bg-opacity-70 shadow-lg border-r border-white border-opacity-5 py-3 transform transition-transform mb-12 overflow-y-auto" :class="wrapperClass + ' ' + (streamLibraryItem ? 'h-[calc(100%-270px)]' : 'h-[calc(100%-110px)]')" v-click-outside="clickOutside">
<div v-show="isMobilePortrait" class="flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer"> <div v-show="isMobilePortrait" class="flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer">
<span class="material-symbols text-2xl">arrow_back</span> <span class="material-symbols text-2xl">arrow_back</span>
</div> </div>
@@ -19,7 +19,7 @@
<p class="text-xs text-gray-300 italic">{{ Source }}</p> <p class="text-xs text-gray-300 italic">{{ Source }}</p>
</div> </div>
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ $config.version }}</a> <a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ versionData.latestVersion }}</a>
</div> </div>
</div> </div>
</template> </template>
+87 -73
View File
@@ -2,6 +2,10 @@
<div id="bookshelf" ref="bookshelf" class="w-full overflow-y-auto" :style="{ fontSize: sizeMultiplier + 'rem' }"> <div id="bookshelf" ref="bookshelf" class="w-full overflow-y-auto" :style="{ fontSize: sizeMultiplier + 'rem' }">
<template v-for="shelf in totalShelves"> <template v-for="shelf in totalShelves">
<div :key="shelf" :id="`shelf-${shelf - 1}`" class="w-full px-4e sm:px-8e relative" :class="{ bookshelfRow: !isAlternativeBookshelfView }" :style="{ height: shelfHeight + 'px' }"> <div :key="shelf" :id="`shelf-${shelf - 1}`" class="w-full px-4e sm:px-8e relative" :class="{ bookshelfRow: !isAlternativeBookshelfView }" :style="{ height: shelfHeight + 'px' }">
<!-- Card skeletons -->
<template v-for="entityIndex in entitiesInShelf(shelf)">
<div :key="entityIndex" class="w-full h-full absolute rounded z-5 top-0 left-0 bg-primary box-shadow-book" :style="{ transform: entityTransform(entityIndex), width: cardWidth + 'px', height: coverHeight + 'px' }" />
</template>
<div v-if="!isAlternativeBookshelfView" class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-20 h-6e" /> <div v-if="!isAlternativeBookshelfView" class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-20 h-6e" />
</div> </div>
</template> </template>
@@ -65,7 +69,13 @@ export default {
tempIsScanning: false, tempIsScanning: false,
cardWidth: 0, cardWidth: 0,
cardHeight: 0, cardHeight: 0,
resizeObserver: null coverHeight: 0,
resizeObserver: null,
lastScrollTop: 0,
lastTimestamp: 0,
postScrollTimeout: null,
currFirstEntityIndex: -1,
currLastEntityIndex: -1
} }
}, },
watch: { watch: {
@@ -171,9 +181,6 @@ export default {
bookWidth() { bookWidth() {
return this.cardWidth return this.cardWidth
}, },
bookHeight() {
return this.cardHeight
},
shelfPadding() { shelfPadding() {
if (this.bookshelfWidth < 640) return 32 * this.sizeMultiplier if (this.bookshelfWidth < 640) return 32 * this.sizeMultiplier
return 64 * this.sizeMultiplier return 64 * this.sizeMultiplier
@@ -184,9 +191,6 @@ export default {
entityWidth() { entityWidth() {
return this.cardWidth return this.cardWidth
}, },
entityHeight() {
return this.cardHeight
},
shelfPaddingHeight() { shelfPaddingHeight() {
return 16 return 16
}, },
@@ -354,50 +358,53 @@ export default {
} }
}, },
loadPage(page) { loadPage(page) {
this.pagesLoaded[page] = true if (!this.pagesLoaded[page]) this.pagesLoaded[page] = this.fetchEntites(page)
this.fetchEntites(page) return this.pagesLoaded[page]
}, },
showHideBookPlaceholder(index, show) { showHideBookPlaceholder(index, show) {
var el = document.getElementById(`book-${index}-placeholder`) var el = document.getElementById(`book-${index}-placeholder`)
if (el) el.style.display = show ? 'flex' : 'none' if (el) el.style.display = show ? 'flex' : 'none'
}, },
mountEntites(fromIndex, toIndex) { mountEntities(fromIndex, toIndex) {
for (let i = fromIndex; i < toIndex; i++) { for (let i = fromIndex; i < toIndex; i++) {
if (!this.entityIndexesMounted.includes(i)) { if (!this.entityIndexesMounted.includes(i)) {
this.cardsHelpers.mountEntityCard(i) this.cardsHelpers.mountEntityCard(i)
} }
} }
}, },
handleScroll(scrollTop) { getVisibleIndices(scrollTop) {
this.currScrollTop = scrollTop const firstShelfIndex = Math.floor(scrollTop / this.shelfHeight)
var firstShelfIndex = Math.floor(scrollTop / this.shelfHeight) const lastShelfIndex = Math.min(Math.ceil((scrollTop + this.bookshelfHeight) / this.shelfHeight), this.totalShelves - 1)
var lastShelfIndex = Math.ceil((scrollTop + this.bookshelfHeight) / this.shelfHeight) const firstEntityIndex = firstShelfIndex * this.entitiesPerShelf
lastShelfIndex = Math.min(this.totalShelves - 1, lastShelfIndex) const lastEntityIndex = Math.min(lastShelfIndex * this.entitiesPerShelf + this.entitiesPerShelf, this.totalEntities)
return { firstEntityIndex, lastEntityIndex }
var firstBookIndex = firstShelfIndex * this.entitiesPerShelf },
var lastBookIndex = lastShelfIndex * this.entitiesPerShelf + this.entitiesPerShelf postScroll() {
lastBookIndex = Math.min(this.totalEntities, lastBookIndex) const { firstEntityIndex, lastEntityIndex } = this.getVisibleIndices(this.currScrollTop)
var firstBookPage = Math.floor(firstBookIndex / this.booksPerFetch)
var lastBookPage = Math.floor(lastBookIndex / this.booksPerFetch)
if (!this.pagesLoaded[firstBookPage]) {
// console.log('Must load next batch', firstBookPage, 'book index', firstBookIndex)
this.loadPage(firstBookPage)
}
if (!this.pagesLoaded[lastBookPage]) {
// console.log('Must load last next batch', lastBookPage, 'book index', lastBookIndex)
this.loadPage(lastBookPage)
}
this.entityIndexesMounted = this.entityIndexesMounted.filter((_index) => { this.entityIndexesMounted = this.entityIndexesMounted.filter((_index) => {
if (_index < firstBookIndex || _index >= lastBookIndex) { if (_index < firstEntityIndex || _index >= lastEntityIndex) {
var el = document.getElementById(`book-card-${_index}`) var el = this.entityComponentRefs[_index]
if (el) el.remove() if (el && el.$el) el.$el.remove()
return false return false
} }
return true return true
}) })
this.mountEntites(firstBookIndex, lastBookIndex) },
handleScroll(scrollTop) {
this.currScrollTop = scrollTop
const { firstEntityIndex, lastEntityIndex } = this.getVisibleIndices(scrollTop)
if (firstEntityIndex === this.currFirstEntityIndex && lastEntityIndex === this.currLastEntityIndex) return
this.currFirstEntityIndex = firstEntityIndex
this.currLastEntityIndex = lastEntityIndex
clearTimeout(this.postScrollTimeout)
const firstPage = Math.floor(firstEntityIndex / this.booksPerFetch)
const lastPage = Math.floor(lastEntityIndex / this.booksPerFetch)
Promise.all([this.loadPage(firstPage), this.loadPage(lastPage)])
.then(() => this.mountEntities(firstEntityIndex, lastEntityIndex))
.catch((error) => console.error('Failed to load page', error))
this.postScrollTimeout = setTimeout(this.postScroll, 500)
}, },
async resetEntities() { async resetEntities() {
if (this.isFetchingEntities) { if (this.isFetchingEntities) {
@@ -405,8 +412,6 @@ export default {
return return
} }
this.destroyEntityComponents() this.destroyEntityComponents()
this.entityIndexesMounted = []
this.entityComponentRefs = {}
this.pagesLoaded = {} this.pagesLoaded = {}
this.entities = [] this.entities = []
this.totalShelves = 0 this.totalShelves = 0
@@ -416,40 +421,21 @@ export default {
this.initialized = false this.initialized = false
this.initSizeData() this.initSizeData()
this.pagesLoaded[0] = true await this.loadPage(0)
await this.fetchEntites(0)
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf) var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
this.mountEntites(0, lastBookIndex) this.mountEntities(0, lastBookIndex)
}, },
remountEntities() { async rebuild() {
for (const key in this.entityComponentRefs) {
if (this.entityComponentRefs[key]) {
this.entityComponentRefs[key].destroy()
}
}
this.entityComponentRefs = {}
this.entityIndexesMounted.forEach((i) => {
this.cardsHelpers.mountEntityCard(i)
})
},
rebuild() {
this.initSizeData() this.initSizeData()
var lastBookIndex = Math.min(this.totalEntities, this.booksPerFetch) var lastBookIndex = Math.min(this.totalEntities, this.booksPerFetch)
this.entityIndexesMounted = [] this.destroyEntityComponents()
for (let i = 0; i < lastBookIndex; i++) { await this.loadPage(0)
this.entityIndexesMounted.push(i)
if (!this.entities[i]) {
const page = Math.floor(i / this.booksPerFetch)
this.loadPage(page)
}
}
var bookshelfEl = document.getElementById('bookshelf') var bookshelfEl = document.getElementById('bookshelf')
if (bookshelfEl) { if (bookshelfEl) {
bookshelfEl.scrollTop = 0 bookshelfEl.scrollTop = 0
} }
this.mountEntities(0, lastBookIndex)
this.$nextTick(this.remountEntities)
}, },
buildSearchParams() { buildSearchParams() {
if (this.page === 'search' || this.page === 'collections') { if (this.page === 'search' || this.page === 'collections') {
@@ -513,12 +499,29 @@ export default {
if (wasUpdated) { if (wasUpdated) {
this.resetEntities() this.resetEntities()
} else if (settings.bookshelfCoverSize !== this.currentBookWidth) { } else if (settings.bookshelfCoverSize !== this.currentBookWidth) {
this.executeRebuild() this.rebuild()
} }
}, },
getScrollRate() {
const currentTimestamp = Date.now()
const timeDelta = currentTimestamp - this.lastTimestamp
const scrollDelta = this.currScrollTop - this.lastScrollTop
const scrollRate = Math.abs(scrollDelta) / (timeDelta || 1)
this.lastScrollTop = this.currScrollTop
this.lastTimestamp = currentTimestamp
return scrollRate
},
scroll(e) { scroll(e) {
if (!e || !e.target) return if (!e || !e.target) return
var { scrollTop } = e.target clearTimeout(this.scrollTimeout)
const { scrollTop } = e.target
const scrollRate = this.getScrollRate()
if (scrollRate > 5) {
this.scrollTimeout = setTimeout(() => {
this.handleScroll(scrollTop)
}, 25)
return
}
this.handleScroll(scrollTop) this.handleScroll(scrollTop)
}, },
libraryItemAdded(libraryItem) { libraryItemAdded(libraryItem) {
@@ -667,13 +670,14 @@ export default {
}, },
updatePagesLoaded() { updatePagesLoaded() {
let numPages = Math.ceil(this.totalEntities / this.booksPerFetch) let numPages = Math.ceil(this.totalEntities / this.booksPerFetch)
this.pagesLoaded = {}
for (let page = 0; page < numPages; page++) { for (let page = 0; page < numPages; page++) {
let numEntities = Math.min(this.totalEntities - page * this.booksPerFetch, this.booksPerFetch) let numEntities = Math.min(this.totalEntities - page * this.booksPerFetch, this.booksPerFetch)
this.pagesLoaded[page] = true this.pagesLoaded[page] = Promise.resolve()
for (let i = 0; i < numEntities; i++) { for (let i = 0; i < numEntities; i++) {
const index = page * this.booksPerFetch + i const index = page * this.booksPerFetch + i
if (!this.entities[index]) { if (!this.entities[index]) {
this.pagesLoaded[page] = false if (this.pagesLoaded[page]) delete this.pagesLoaded[page]
break break
} }
} }
@@ -688,7 +692,6 @@ export default {
var entitiesPerShelfBefore = this.entitiesPerShelf var entitiesPerShelfBefore = this.entitiesPerShelf
var { clientHeight, clientWidth } = bookshelf var { clientHeight, clientWidth } = bookshelf
// console.log('Init bookshelf width', clientWidth, 'window width', window.innerWidth)
this.mountWindowWidth = window.innerWidth this.mountWindowWidth = window.innerWidth
this.bookshelfHeight = clientHeight this.bookshelfHeight = clientHeight
this.bookshelfWidth = clientWidth this.bookshelfWidth = clientWidth
@@ -713,10 +716,9 @@ export default {
this.initSizeData(bookshelf) this.initSizeData(bookshelf)
this.checkUpdateSearchParams() this.checkUpdateSearchParams()
this.pagesLoaded[0] = true await this.loadPage(0)
await this.fetchEntites(0)
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf) var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
this.mountEntites(0, lastBookIndex) this.mountEntities(0, lastBookIndex)
// Set last scroll position for this bookshelf page // Set last scroll position for this bookshelf page
if (this.$store.state.lastBookshelfScrollData[this.page] && window.bookshelf) { if (this.$store.state.lastBookshelfScrollData[this.page] && window.bookshelf) {
@@ -747,7 +749,7 @@ export default {
var bookshelf = document.getElementById('bookshelf') var bookshelf = document.getElementById('bookshelf')
if (bookshelf) { if (bookshelf) {
this.init(bookshelf) this.init(bookshelf)
bookshelf.addEventListener('scroll', this.scroll) bookshelf.addEventListener('scroll', this.scroll, { passive: true })
} }
}) })
@@ -810,10 +812,14 @@ export default {
}, },
destroyEntityComponents() { destroyEntityComponents() {
for (const key in this.entityComponentRefs) { for (const key in this.entityComponentRefs) {
if (this.entityComponentRefs[key] && this.entityComponentRefs[key].destroy) { const ref = this.entityComponentRefs[key]
this.entityComponentRefs[key].destroy() if (ref && ref.destroy) {
if (ref.$el) ref.$el.remove()
ref.destroy()
} }
} }
this.entityComponentRefs = {}
this.entityIndexesMounted = []
}, },
scan() { scan() {
this.tempIsScanning = true this.tempIsScanning = true
@@ -826,6 +832,14 @@ export default {
.finally(() => { .finally(() => {
this.tempIsScanning = false this.tempIsScanning = false
}) })
},
entitiesInShelf(shelf) {
return shelf == this.totalShelves ? this.totalEntities % this.entitiesPerShelf || this.entitiesPerShelf : this.entitiesPerShelf
},
entityTransform(entityIndex) {
const shelfOffsetY = this.shelfPaddingHeight * this.sizeMultiplier
const shelfOffsetX = (entityIndex - 1) * this.totalEntityCardWidth + this.bookshelfMarginLeft
return `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)`
} }
}, },
async mounted() { async mounted() {
@@ -53,7 +53,6 @@
@showBookmarks="showBookmarks" @showBookmarks="showBookmarks"
@showSleepTimer="showSleepTimerModal = true" @showSleepTimer="showSleepTimerModal = true"
@showPlayerQueueItems="showPlayerQueueItemsModal = true" @showPlayerQueueItems="showPlayerQueueItemsModal = true"
@showPlayerSettings="showPlayerSettingsModal = true"
/> />
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :library-item-id="libraryItemId" @select="selectBookmark" /> <modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :library-item-id="libraryItemId" @select="selectBookmark" />
@@ -61,8 +60,6 @@
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-type="sleepTimerType" :remaining="sleepTimerRemaining" :has-chapters="!!chapters.length" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" /> <modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-type="sleepTimerType" :remaining="sleepTimerRemaining" :has-chapters="!!chapters.length" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" /> <modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" />
<modals-player-settings-modal v-model="showPlayerSettingsModal" />
</div> </div>
</template> </template>
@@ -81,7 +78,6 @@ export default {
currentTime: 0, currentTime: 0,
showSleepTimerModal: false, showSleepTimerModal: false,
showPlayerQueueItemsModal: false, showPlayerQueueItemsModal: false,
showPlayerSettingsModal: false,
sleepTimerSet: false, sleepTimerSet: false,
sleepTimerRemaining: 0, sleepTimerRemaining: 0,
sleepTimerType: null, sleepTimerType: null,
@@ -167,7 +163,7 @@ export default {
}, },
podcastAuthor() { podcastAuthor() {
if (!this.isPodcast) return null if (!this.isPodcast) return null
return this.mediaMetadata.author || 'Unknown' return this.mediaMetadata.author || this.$strings.LabelUnknown
}, },
hasNextItemInQueue() { hasNextItemInQueue() {
return this.currentPlayerQueueIndex < this.playerQueueItems.length - 1 return this.currentPlayerQueueIndex < this.playerQueueItems.length - 1
@@ -251,7 +247,7 @@ export default {
sleepTimerEnd() { sleepTimerEnd() {
this.clearSleepTimer() this.clearSleepTimer()
this.playerHandler.pause() this.playerHandler.pause()
this.$toast.info('Sleep Timer Done.. zZzzZz') this.$toast.info(this.$strings.ToastSleepTimerDone)
}, },
cancelSleepTimer() { cancelSleepTimer() {
this.showSleepTimerModal = false this.showSleepTimerModal = false
@@ -525,7 +521,7 @@ export default {
}, },
showFailedProgressSyncs() { showFailedProgressSyncs() {
if (!isNaN(this.syncFailedToast)) this.$toast.dismiss(this.syncFailedToast) if (!isNaN(this.syncFailedToast)) this.$toast.dismiss(this.syncFailedToast)
this.syncFailedToast = this.$toast('Progress is not being synced. Restart playback', { timeout: false, type: 'error' }) this.syncFailedToast = this.$toast(this.$strings.ToastProgressIsNotBeingSynced, { timeout: false, type: 'error' })
}, },
sessionClosedEvent(sessionId) { sessionClosedEvent(sessionId) {
if (this.playerHandler.currentSessionId === sessionId) { if (this.playerHandler.currentSessionId === sessionId) {
+2 -2
View File
@@ -1,9 +1,9 @@
<template> <template>
<div class="w-20 bg-bg h-full fixed left-0 box-shadow-side z-50" style="min-width: 80px" :style="{ top: offsetTop + 'px' }"> <div role="toolbar" aria-orientation="vertical" aria-label="Library Sidebar" class="w-20 bg-bg h-full fixed left-0 box-shadow-side z-50" style="min-width: 80px" :style="{ top: offsetTop + 'px' }">
<!-- ugly little workaround to cover up the shadow overlapping the bookshelf toolbar --> <!-- ugly little workaround to cover up the shadow overlapping the bookshelf toolbar -->
<div v-if="isShowingBookshelfToolbar" class="absolute top-0 -right-4 w-4 bg-bg h-10 pointer-events-none" /> <div v-if="isShowingBookshelfToolbar" class="absolute top-0 -right-4 w-4 bg-bg h-10 pointer-events-none" />
<div id="siderail-buttons-container" :class="{ 'player-open': streamLibraryItem }" class="w-full overflow-y-auto overflow-x-hidden"> <div id="siderail-buttons-container" role="navigation" aria-label="Library Navigation" :class="{ 'player-open': streamLibraryItem }" class="w-full overflow-y-auto overflow-x-hidden">
<nuxt-link :to="`/library/${currentLibraryId}`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="homePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link :to="`/library/${currentLibraryId}`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="homePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
+12 -6
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="pb-3e" :style="{ minWidth: cardWidth + 'px', maxWidth: cardWidth + 'px' }"> <article class="pb-3e" :style="{ minWidth: cardWidth + 'px', maxWidth: cardWidth + 'px' }">
<nuxt-link :to="`/author/${author?.id}`"> <nuxt-link :to="`/author/${author?.id}`">
<div cy-id="card" @mouseover="mouseover" @mouseleave="mouseleave"> <div cy-id="card" @mouseover="mouseover" @mouseleave="mouseleave">
<div cy-id="imageArea" :style="{ height: cardHeight + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden"> <div cy-id="imageArea" :style="{ height: cardHeight + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
@@ -34,7 +34,7 @@
</div> </div>
</div> </div>
</nuxt-link> </nuxt-link>
</div> </article>
</template> </template>
<script> <script>
@@ -68,6 +68,9 @@ export default {
cardHeight() { cardHeight() {
return this.height * this.sizeMultiplier return this.height * this.sizeMultiplier
}, },
coverHeight() {
return this.cardHeight
},
userToken() { userToken() {
return this.store.getters['user/getToken'] return this.store.getters['user/getToken']
}, },
@@ -125,12 +128,15 @@ export default {
return null return null
}) })
if (!response) { if (!response) {
this.$toast.error(`Author ${this.name} not found`) this.$toast.error(this.$getString('ToastAuthorNotFound', [this.name]))
} else if (response.updated) { } else if (response.updated) {
if (response.author.imagePath) this.$toast.success(`Author ${response.author.name} was updated`) if (response.author.imagePath) {
else this.$toast.success(`Author ${response.author.name} was updated (no image found)`) this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
} else {
this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
}
} else { } else {
this.$toast.info(`No updates were made for Author ${response.author.name}`) this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
} }
this.searching = false this.searching = false
}, },
+10 -10
View File
@@ -1,5 +1,5 @@
<template> <template>
<div ref="card" :id="`book-card-${index}`" :style="{ minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }" class="absolute rounded-sm z-10 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard"> <article ref="card" :id="`book-card-${index}`" tabindex="0" :aria-label="displayTitle" :style="{ minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }" class="absolute rounded-sm z-10 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<div :id="`cover-area-${index}`" class="relative w-full top-0 left-0 rounded overflow-hidden z-10 bg-primary box-shadow-book" :style="{ height: coverHeight + 'px ' }"> <div :id="`cover-area-${index}`" class="relative w-full top-0 left-0 rounded overflow-hidden z-10 bg-primary box-shadow-book" :style="{ height: coverHeight + 'px ' }">
<!-- When cover image does not fill --> <!-- When cover image does not fill -->
<div cy-id="coverBg" v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary"> <div cy-id="coverBg" v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
@@ -14,21 +14,21 @@
</div> </div>
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10"> <div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
<div cy-id="titleImageNotReady" v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: 0.5 + 'em' }"> <div cy-id="titleImageNotReady" v-show="libraryItem && !imageReady" aria-hidden="true" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: 0.5 + 'em' }">
<p :style="{ fontSize: 0.8 + 'em' }" class="text-gray-300 text-center">{{ title }}</p> <p :style="{ fontSize: 0.8 + 'em' }" class="text-gray-300 text-center">{{ title }}</p>
</div> </div>
<!-- Cover Image --> <!-- Cover Image -->
<img cy-id="coverImage" v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="relative w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" /> <img cy-id="coverImage" v-if="libraryItem" :alt="`${displayTitle}, ${$strings.LabelCover}`" ref="cover" aria-hidden="true" :src="bookCoverSrc" class="relative w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
<!-- Placeholder Cover Title & Author --> <!-- Placeholder Cover Title & Author -->
<div cy-id="placeholderTitle" v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'em' }"> <div cy-id="placeholderTitle" v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'em' }">
<div> <div>
<p cy-id="placeholderTitleText" class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'em' }">{{ titleCleaned }}</p> <p cy-id="placeholderTitleText" aria-hidden="true" class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'em' }">{{ titleCleaned }}</p>
</div> </div>
</div> </div>
<div cy-id="placeholderAuthor" v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'em', bottom: authorBottom + 'em' }"> <div cy-id="placeholderAuthor" v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'em', bottom: authorBottom + 'em' }">
<p cy-id="placeholderAuthorText" class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'em' }">{{ authorCleaned }}</p> <p cy-id="placeholderAuthorText" aria-hidden="true" class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'em' }">{{ authorCleaned }}</p>
</div> </div>
<div v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }" style="background-color: #78350f"> <div v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }" style="background-color: #78350f">
@@ -93,11 +93,11 @@
<!-- rss feed icon --> <!-- rss feed icon -->
<div cy-id="rssFeed" v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" :style="{ padding: 0.375 + 'em' }"> <div cy-id="rssFeed" v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" :style="{ padding: 0.375 + 'em' }">
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">rss_feed</span> <span class="material-symbols" aria-hidden="true" :style="{ fontSize: 1.5 + 'em' }">rss_feed</span>
</div> </div>
<!-- media item shared icon --> <!-- media item shared icon -->
<div cy-id="mediaItemShare" v-if="mediaItemShare && !isSelectionMode && !isHovering" class="absolute text-success left-0 z-10" :style="{ padding: 0.375 + 'em', top: rssFeed ? '2em' : '0px' }"> <div cy-id="mediaItemShare" v-if="mediaItemShare && !isSelectionMode && !isHovering" class="absolute text-success left-0 z-10" :style="{ padding: 0.375 + 'em', top: rssFeed ? '2em' : '0px' }">
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">public</span> <span class="material-symbols" aria-hidden="true" :style="{ fontSize: 1.5 + 'em' }">public</span>
</div> </div>
<!-- Series sequence --> <!-- Series sequence -->
@@ -114,7 +114,7 @@
<!-- Podcast Num Episodes --> <!-- Podcast Num Episodes -->
<div cy-id="numEpisodes" v-else-if="!numEpisodesIncomplete && numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', width: 1.25 + 'em', height: 1.25 + 'em' }"> <div cy-id="numEpisodes" v-else-if="!numEpisodesIncomplete && numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', width: 1.25 + 'em', height: 1.25 + 'em' }">
<p :style="{ fontSize: 0.8 + 'em' }">{{ numEpisodes }}</p> <p :style="{ fontSize: 0.8 + 'em' }" role="status" :aria-label="$strings.LabelNumberOfEpisodes">{{ numEpisodes }}</p>
</div> </div>
<!-- Podcast Num Episodes --> <!-- Podcast Num Episodes -->
@@ -128,7 +128,7 @@
<div cy-id="detailBottom" :id="`description-area-${index}`" v-if="isAlternativeBookshelfView || isAuthorBookshelfView" dir="auto" class="relative mt-2e mb-2e left-0 z-50 w-full"> <div cy-id="detailBottom" :id="`description-area-${index}`" v-if="isAlternativeBookshelfView || isAuthorBookshelfView" dir="auto" class="relative mt-2e mb-2e left-0 z-50 w-full">
<div :style="{ fontSize: 0.9 + 'em' }"> <div :style="{ fontSize: 0.9 + 'em' }">
<ui-tooltip v-if="displayTitle" :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center"> <ui-tooltip v-if="displayTitle" :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
<p cy-id="title" ref="displayTitle" class="truncate">{{ displayTitle }}</p> <p cy-id="title" ref="displayTitle" aria-hidden="true" class="truncate">{{ displayTitle }}</p>
<widgets-explicit-indicator cy-id="explicitIndicator" v-if="isExplicit" /> <widgets-explicit-indicator cy-id="explicitIndicator" v-if="isExplicit" />
</ui-tooltip> </ui-tooltip>
</div> </div>
@@ -138,7 +138,7 @@
<p cy-id="line2" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displayLineTwo || '&nbsp;' }}</p> <p cy-id="line2" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displayLineTwo || '&nbsp;' }}</p>
<p cy-id="line3" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displaySortLine }}</p> <p cy-id="line3" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displaySortLine }}</p>
</div> </div>
</div> </article>
</template> </template>
<script> <script>
@@ -1,5 +1,5 @@
<template> <template>
<div ref="card" :id="`collection-card-${index}`" :style="{ width: cardWidth + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard"> <div ref="card" :id="`collection-card-${index}`" role="button" :style="{ width: cardWidth + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<div class="relative" :style="{ height: coverHeight + 'px' }"> <div class="relative" :style="{ height: coverHeight + 'px' }">
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" /> <div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<div class="w-full h-full bg-primary relative rounded overflow-hidden"> <div class="w-full h-full bg-primary relative rounded overflow-hidden">
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<div ref="card" :id="`playlist-card-${index}`" :style="{ width: cardWidth + 'px', fontSize: sizeMultiplier + 'rem' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard"> <div ref="card" :id="`playlist-card-${index}`" role="button" :style="{ width: cardWidth + 'px', fontSize: sizeMultiplier + 'rem' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<div class="relative" :style="{ height: coverHeight + 'px' }"> <div class="relative" :style="{ height: coverHeight + 'px' }">
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" /> <div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<div class="w-full h-full bg-primary relative rounded overflow-hidden"> <div class="w-full h-full bg-primary relative rounded overflow-hidden">
+6 -6
View File
@@ -1,5 +1,5 @@
<template> <template>
<div cy-id="card" ref="card" :id="`series-card-${index}`" :style="{ width: cardWidth + 'px' }" class="absolute rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard"> <article cy-id="card" ref="card" :id="`series-card-${index}`" tabindex="0" :aria-label="displayTitle" :style="{ width: cardWidth + 'px' }" class="absolute rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<div cy-id="covers-area" class="relative" :style="{ height: coverHeight + 'px' }"> <div cy-id="covers-area" class="relative" :style="{ height: coverHeight + 'px' }">
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" /> <div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0"> <div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
@@ -7,12 +7,12 @@
</div> </div>
<div cy-id="seriesLengthMarker" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #cd9d49dd"> <div cy-id="seriesLengthMarker" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #cd9d49dd">
<p :style="{ fontSize: 0.8 + 'em' }">{{ books.length }}</p> <p :style="{ fontSize: 0.8 + 'em' }" role="status" :aria-label="$strings.LabelNumberOfBooks">{{ books.length }}</p>
</div> </div>
<div cy-id="seriesProgressBar" v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1e shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" /> <div cy-id="seriesProgressBar" v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1e shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
<div cy-id="hoveringDisplayTitle" v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: '1em' }"> <div cy-id="hoveringDisplayTitle" v-if="hasValidCovers" aria-hidden="true" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: '1em' }">
<p :style="{ fontSize: 1.2 + 'em' }">{{ displayTitle }}</p> <p :style="{ fontSize: 1.2 + 'em' }">{{ displayTitle }}</p>
</div> </div>
@@ -21,14 +21,14 @@
<div cy-id="standardBottomText" v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6e h-6e rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }"> <div cy-id="standardBottomText" v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6e h-6e rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em 0.5em` }"> <div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em 0.5em` }">
<p cy-id="standardBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ displayTitle }}</p> <p cy-id="standardBottomDisplayTitle" class="truncate" aria-hidden="true" :style="{ fontSize: labelFontSize + 'em' }">{{ displayTitle }}</p>
</div> </div>
</div> </div>
<div cy-id="detailBottomText" v-else class="relative z-30 left-0 right-0 mx-auto py-1e rounded-md text-center"> <div cy-id="detailBottomText" v-else class="relative z-30 left-0 right-0 mx-auto py-1e rounded-md text-center">
<p cy-id="detailBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ displayTitle }}</p> <p cy-id="detailBottomDisplayTitle" class="truncate" aria-hidden="true" :style="{ fontSize: labelFontSize + 'em' }">{{ displayTitle }}</p>
<p cy-id="detailBottomSortLine" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displaySortLine }}</p> <p cy-id="detailBottomSortLine" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displaySortLine }}</p>
</div> </div>
</div> </article>
</template> </template>
<script> <script>
+5 -5
View File
@@ -1,15 +1,15 @@
<template> <template>
<div class=""> <div class="">
<div class="w-full relative sm:w-80"> <div class="w-full relative sm:w-80">
<form @submit.prevent="submitSearch"> <form role="search" @submit.prevent="submitSearch">
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" /> <ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
</form> </form>
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear"> <button :aria-hidden="!search" class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
<span v-if="!search" class="material-symbols" style="font-size: 1.2rem">&#xe8b6;</span> <span v-if="!search" class="material-symbols" style="font-size: 1.2rem">&#xe8b6;</span>
<span v-else class="material-symbols" style="font-size: 1.2rem">close</span> <span v-else class="material-symbols" style="font-size: 1.2rem">close</span>
</div> </button>
</div> </div>
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full max-w-64 sm:max-w-80 sm:w-80 bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu"> <div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full max-w-64 sm:max-w-80 sm:w-80 bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu" @mousedown.stop.prevent>
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label"> <ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<li v-if="isTyping" class="py-2 px-2"> <li v-if="isTyping" class="py-2 px-2">
<p>{{ $strings.MessageThinking }}</p> <p>{{ $strings.MessageThinking }}</p>
@@ -157,7 +157,7 @@ export default {
clearTimeout(this.focusTimeout) clearTimeout(this.focusTimeout)
this.focusTimeout = setTimeout(() => { this.focusTimeout = setTimeout(() => {
this.showMenu = false this.showMenu = false
}, 200) }, 100)
}, },
async runSearch(value) { async runSearch(value) {
this.lastSearch = value this.lastSearch = value
@@ -1,28 +1,30 @@
<template> <template>
<div ref="wrapper" class="relative" v-click-outside="clickOutside"> <div ref="wrapper" class="relative" v-click-outside="clickOutside">
<button type="button" class="relative w-full h-full bg-bg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu"> <div class="relative h-7">
<span class="flex items-center justify-between"> <button type="button" class="relative w-full h-full bg-bg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="menu" :aria-expanded="showMenu" @click.prevent="showMenu = !showMenu">
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span> <span class="flex items-center justify-between">
</span> <span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
</span>
</button>
<span v-if="selected === 'all'" class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> <span v-if="selected === 'all'" class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg> </svg>
</span> </span>
<div v-else class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-200" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected"> <button v-else :aria-label="$strings.ButtonClearFilter" class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-200" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
<span class="material-symbols" style="font-size: 1.1rem">close</span> <span class="material-symbols" style="font-size: 1.1rem">close</span>
</div> </button>
</button> </div>
<div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm libraryFilterMenu"> <div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm libraryFilterMenu">
<ul v-show="!sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label"> <ul v-show="!sublist" class="h-full w-full" role="menu">
<template v-for="item in selectItems"> <template v-for="item in selectItems">
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="option" @click="clickedOption(item)"> <li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" :aria-haspopup="item.sublist ? '' : 'menu'" @click="clickedOption(item)">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="font-normal ml-3 block truncate text-sm">{{ item.text }}</span> <span class="font-normal ml-3 block truncate text-sm">{{ item.text }}</span>
</div> </div>
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center"> <div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
<span class="material-symbols text-2xl">arrow_right</span> <span class="material-symbols text-2xl" :aria-label="$strings.LabelMore">arrow_right</span>
</div> </div>
<!-- selected checkmark icon --> <!-- selected checkmark icon -->
<div v-if="item.value === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none"> <div v-if="item.value === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
@@ -31,8 +33,8 @@
</li> </li>
</template> </template>
</ul> </ul>
<ul v-show="sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label"> <ul v-show="sublist" class="h-full w-full" role="menu">
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-white/5" role="option" @click="sublist = null"> <li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-white/5" role="menuitem" @click="sublist = null">
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center"> <div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
<span class="material-symbols text-2xl">arrow_left</span> <span class="material-symbols text-2xl">arrow_left</span>
</div> </div>
@@ -40,13 +42,13 @@
<span class="font-normal block truncate">{{ $strings.ButtonBack }}</span> <span class="font-normal block truncate">{{ $strings.ButtonBack }}</span>
</div> </div>
</li> </li>
<li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="option"> <li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="menuitem">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<span class="font-normal block truncate py-2">{{ $getString('LabelLibraryFilterSublistEmpty', [selectedSublistText]) }}</span> <span class="font-normal block truncate py-2">{{ $getString('LabelLibraryFilterSublistEmpty', [selectedSublistText]) }}</span>
</div> </div>
</li> </li>
<template v-for="item in sublistItems"> <template v-for="item in sublistItems">
<li :key="item.value" class="select-none relative px-2 cursor-pointer hover:bg-white/5" :class="`${sublist}.${item.value}` === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="option" @click="clickedSublistOption(item.value)"> <li :key="item.value" class="select-none relative px-2 cursor-pointer hover:bg-white/5" :class="`${sublist}.${item.value}` === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" @click="clickedSublistOption(item.value)">
<div class="flex items-center"> <div class="flex items-center">
<span class="font-normal truncate py-2 text-xs">{{ item.text }}</span> <span class="font-normal truncate py-2 text-xs">{{ item.text }}</span>
</div> </div>
@@ -1,20 +1,20 @@
<template> <template>
<div ref="wrapper" class="relative" v-click-outside="clickOutside"> <div ref="wrapper" class="relative" v-click-outside="clickOutside">
<button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu"> <button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="menu" :aria-expanded="showMenu" @click.prevent="showMenu = !showMenu">
<span class="flex items-center justify-between"> <span class="flex items-center justify-between">
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span> <span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
<span class="material-symbols text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span> <span class="material-symbols text-lg text-yellow-400" :aria-label="descending ? $strings.LabelSortDescending : $strings.LabelSortAscending">{{ descending ? 'expand_more' : 'expand_less' }}</span>
</span> </span>
</button> </button>
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm" role="listbox" aria-labelledby="listbox-label"> <ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm" role="menu">
<template v-for="item in selectItems"> <template v-for="item in selectItems">
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="option" @click="clickedOption(item.value)"> <li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" @click="clickedOption(item.value)">
<div class="flex items-center"> <div class="flex items-center">
<span class="font-normal ml-3 block truncate">{{ item.text }}</span> <span class="font-normal ml-3 block truncate">{{ item.text }}</span>
</div> </div>
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4"> <span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
<span class="material-symbols text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span> <span class="material-symbols text-xl" :aria-label="descending ? $strings.LabelSortDescending : $strings.LabelSortAscending">{{ descending ? 'expand_more' : 'expand_less' }}</span>
</span> </span>
</li> </li>
</template> </template>
+5 -5
View File
@@ -1,20 +1,20 @@
<template> <template>
<div ref="wrapper" class="relative" v-click-outside="clickOutside"> <div ref="wrapper" class="relative" v-click-outside="clickOutside">
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu"> <button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none cursor-pointer" aria-haspopup="menu" :aria-expanded="showMenu" @click.prevent="showMenu = !showMenu">
<span class="flex items-center justify-between"> <span class="flex items-center justify-between">
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span> <span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
<span class="material-symbols text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span> <span class="material-symbols text-lg text-yellow-400" :aria-label="descending ? $strings.LabelSortDescending : $strings.LabelSortAscending">{{ descending ? 'expand_more' : 'expand_less' }}</span>
</span> </span>
</button> </button>
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-80 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm" role="listbox" aria-labelledby="listbox-label"> <ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-80 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm" role="menu">
<template v-for="item in items"> <template v-for="item in items">
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="option" @click="clickedOption(item.value)"> <li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" @click="clickedOption(item.value)">
<div class="flex items-center"> <div class="flex items-center">
<span class="font-normal ml-3 block truncate">{{ item.text }}</span> <span class="font-normal ml-3 block truncate">{{ item.text }}</span>
</div> </div>
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4"> <span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
<span class="material-symbols text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span> <span class="material-symbols text-xl" :aria-label="descending ? $strings.LabelSortDescending : $strings.LabelSortAscending">{{ descending ? 'expand_more' : 'expand_less' }}</span>
</span> </span>
</li> </li>
</template> </template>
+1 -5
View File
@@ -56,11 +56,7 @@ export default {
}, },
imgSrc() { imgSrc() {
if (!this.imagePath) return null if (!this.imagePath) return null
if (process.env.NODE_ENV !== 'production') { return `${this.$config.routerBasePath}/api/authors/${this.authorId}/image?ts=${this.updatedAt}`
// Testing
return `http://localhost:3333${this.$config.routerBasePath}/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
}
return `/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
} }
}, },
methods: { methods: {
+2
View File
@@ -121,6 +121,8 @@ export default {
var img = document.createElement('img') var img = document.createElement('img')
img.src = src img.src = src
img.alt = `${this.name}, ${this.$strings.LabelCover}`
img.ariaHidden = true
img.className = 'absolute top-0 left-0 w-full h-full' img.className = 'absolute top-0 left-0 w-full h-full'
img.style.objectFit = showCoverBg ? 'contain' : 'cover' img.style.objectFit = showCoverBg ? 'contain' : 'cover'
+13 -2
View File
@@ -69,6 +69,15 @@
</div> </div>
</div> </div>
<div class="flex items-center my-2 max-w-md">
<div class="w-1/2">
<p id="ereader-permissions-toggle">{{ $strings.LabelPermissionsCreateEreader }}</p>
</div>
<div class="w-1/2">
<ui-toggle-switch labeledBy="ereader-permissions-toggle" v-model="newUser.permissions.createEreader" />
</div>
</div>
<div class="flex items-center my-2 max-w-md"> <div class="flex items-center my-2 max-w-md">
<div class="w-1/2"> <div class="w-1/2">
<p id="explicit-content-permissions-toggle">{{ $strings.LabelPermissionsAccessExplicitContent }}</p> <p id="explicit-content-permissions-toggle">{{ $strings.LabelPermissionsAccessExplicitContent }}</p>
@@ -354,7 +363,8 @@ export default {
accessExplicitContent: type === 'admin', accessExplicitContent: type === 'admin',
accessAllLibraries: true, accessAllLibraries: true,
accessAllTags: true, accessAllTags: true,
selectedTagsNotAccessible: false selectedTagsNotAccessible: false,
createEreader: type === 'admin'
} }
}, },
init() { init() {
@@ -387,7 +397,8 @@ export default {
accessAllLibraries: true, accessAllLibraries: true,
accessAllTags: true, accessAllTags: true,
accessExplicitContent: false, accessExplicitContent: false,
selectedTagsNotAccessible: false selectedTagsNotAccessible: false,
createEreader: false
}, },
librariesAccessible: [], librariesAccessible: [],
itemTagsSelected: [] itemTagsSelected: []
@@ -54,8 +54,7 @@ export default {
options: { options: {
provider: undefined, provider: undefined,
overrideDetails: true, overrideDetails: true,
overrideCover: true, overrideCover: true
overrideDefaults: true
} }
} }
}, },
@@ -99,8 +98,8 @@ export default {
init() { init() {
// If we don't have a set provider (first open of dialog) or we've switched library, set // If we don't have a set provider (first open of dialog) or we've switched library, set
// the selected provider to the current library default provider // the selected provider to the current library default provider
if (!this.options.provider || this.options.lastUsedLibrary != this.currentLibraryId) { if (!this.options.provider || this.lastUsedLibrary != this.currentLibraryId) {
this.options.lastUsedLibrary = this.currentLibraryId this.lastUsedLibrary = this.currentLibraryId
this.options.provider = this.libraryProvider this.options.provider = this.libraryProvider
} }
}, },
@@ -116,10 +115,10 @@ export default {
libraryItemIds: this.selectedBookIds libraryItemIds: this.selectedBookIds
}) })
.then(() => { .then(() => {
this.$toast.info('Batch quick match of ' + this.selectedBookIds.length + ' books started!') this.$toast.info(this.$getString('ToastBatchQuickMatchStarted', [this.selectedBookIds.length]))
}) })
.catch((error) => { .catch((error) => {
this.$toast.error('Batch quick match failed') this.$toast.error(this.$strings.ToastBatchQuickMatchFailed)
console.error('Failed to batch quick match', error) console.error('Failed to batch quick match', error)
}) })
.finally(() => { .finally(() => {
@@ -1,8 +1,8 @@
<template> <template>
<div ref="wrapper" class="hidden absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 rounded-lg items-center justify-center" style="z-index: 61" @click="clickClose"> <div ref="wrapper" role="dialog" aria-modal="true" class="hidden absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 rounded-lg items-center justify-center" style="z-index: 61" @click="clickClose">
<div class="absolute top-3 right-3 md:top-5 md:right-5 h-8 w-8 md:h-12 md:w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300"> <button type="button" class="absolute top-3 right-3 md:top-5 md:right-5 h-8 w-8 md:h-12 md:w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" aria-label="Close modal">
<span class="material-symbols text-2xl md:text-4xl">close</span> <span class="material-symbols text-2xl md:text-4xl">close</span>
</div> </button>
<div ref="content" class="text-white"> <div ref="content" class="text-white">
<form v-if="selectedSeries" @submit.prevent="submitSeriesForm"> <form v-if="selectedSeries" @submit.prevent="submitSeriesForm">
<div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop> <div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop>
+5 -2
View File
@@ -1,12 +1,12 @@
<template> <template>
<div ref="wrapper" class="modal modal-bg w-full h-full fixed top-0 left-0 bg-primary items-center justify-center opacity-0 hidden" :class="`z-${zIndex} bg-opacity-${bgOpacity}`"> <div ref="wrapper" role="dialog" aria-modal="true" class="modal modal-bg w-full h-full fixed top-0 left-0 bg-primary items-center justify-center opacity-0 hidden" :class="`z-${zIndex} bg-opacity-${bgOpacity}`">
<div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" /> <div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" />
<button class="absolute top-4 right-4 landscape:top-4 landscape:right-4 md:portrait:top-5 md:portrait:right-5 lg:top-5 lg:right-5 inline-flex text-gray-200 hover:text-white" aria-label="Close modal" @click="clickClose"> <button class="absolute top-4 right-4 landscape:top-4 landscape:right-4 md:portrait:top-5 md:portrait:right-5 lg:top-5 lg:right-5 inline-flex text-gray-200 hover:text-white" aria-label="Close modal" @click="clickClose">
<span class="material-symbols text-2xl landscape:text-2xl md:portrait:text-4xl lg:text-4xl">close</span> <span class="material-symbols text-2xl landscape:text-2xl md:portrait:text-4xl lg:text-4xl">close</span>
</button> </button>
<slot name="outer" /> <slot name="outer" />
<div ref="content" style="min-width: 380px; min-height: 200px; max-width: 100vw" class="relative text-white" aria-modal="true" :style="{ height: modalHeight, width: modalWidth, marginTop: contentMarginTop + 'px' }" @mousedown="mousedownModal" @mouseup="mouseupModal" v-click-outside="clickBg"> <div ref="content" tabindex="0" style="min-width: 380px; min-height: 200px; max-width: 100vw" class="relative text-white outline-none" :style="{ height: modalHeight, width: modalWidth, marginTop: contentMarginTop + 'px' }" @mousedown="mousedownModal" @mouseup="mouseupModal" v-click-outside="clickBg">
<slot /> <slot />
<div v-if="processing" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-black bg-opacity-60 rounded-lg flex items-center justify-center"> <div v-if="processing" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-black bg-opacity-60 rounded-lg flex items-center justify-center">
<ui-loading-indicator /> <ui-loading-indicator />
@@ -126,6 +126,9 @@ export default {
this.$eventBus.$on('modal-hotkey', this.hotkey) this.$eventBus.$on('modal-hotkey', this.hotkey)
this.$store.commit('setOpenModal', this.name) this.$store.commit('setOpenModal', this.name)
// Set focus to the modal content
this.content.focus()
}, },
setHide() { setHide() {
if (this.content) this.content.style.transform = 'scale(0)' if (this.content) this.content.style.transform = 'scale(0)'
@@ -59,12 +59,19 @@ export default {
setJumpBackwardAmount(val) { setJumpBackwardAmount(val) {
this.jumpBackwardAmount = val this.jumpBackwardAmount = val
this.$store.dispatch('user/updateUserSettings', { jumpBackwardAmount: val }) this.$store.dispatch('user/updateUserSettings', { jumpBackwardAmount: val })
},
settingsUpdated() {
this.useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack')
this.jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount')
this.jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount')
} }
}, },
mounted() { mounted() {
this.useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') this.settingsUpdated()
this.jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount') this.$eventBus.$on('user-settings', this.settingsUpdated)
this.jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount') },
beforeDestroy() {
this.$eventBus.$off('user-settings', this.settingsUpdated)
} }
} }
</script> </script>
+18 -6
View File
@@ -19,12 +19,13 @@
<ui-text-input v-model="currentShareUrl" show-copy readonly class="text-base h-10" /> <ui-text-input v-model="currentShareUrl" show-copy readonly class="text-base h-10" />
</div> </div>
<div class="w-full py-2 px-1"> <div class="w-full py-2 px-1">
<p v-if="currentShare.expiresAt" class="text-base">{{ $getString('MessageShareExpiresIn', [currentShareTimeRemaining]) }}</p> <p v-if="currentShare.isDownloadable" class="text-sm mb-2">{{ $strings.LabelDownloadable }}</p>
<p v-if="currentShare.expiresAt">{{ $getString('MessageShareExpiresIn', [currentShareTimeRemaining]) }}</p>
<p v-else>{{ $strings.LabelPermanent }}</p> <p v-else>{{ $strings.LabelPermanent }}</p>
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div class="flex flex-col sm:flex-row items-center justify-between space-y-4 sm:space-y-0 sm:space-x-4 mb-4"> <div class="flex flex-col sm:flex-row items-center justify-between space-y-4 sm:space-y-0 sm:space-x-4 mb-2">
<div class="w-full sm:w-48"> <div class="w-full sm:w-48">
<label class="px-1 text-sm font-semibold block">{{ $strings.LabelSlug }}</label> <label class="px-1 text-sm font-semibold block">{{ $strings.LabelSlug }}</label>
<ui-text-input v-model="newShareSlug" class="text-base h-10" /> <ui-text-input v-model="newShareSlug" class="text-base h-10" />
@@ -46,6 +47,15 @@
</div> </div>
</div> </div>
</div> </div>
<div class="flex items-center w-full md:w-1/2 mb-4">
<p class="text-sm text-gray-300 py-1 px-1">{{ $strings.LabelDownloadable }}</p>
<ui-toggle-switch size="sm" v-model="isDownloadable" />
<ui-tooltip :text="$strings.LabelShareDownloadableHelp">
<p class="pl-4 text-sm">
<span class="material-symbols icon-text text-sm">info</span>
</p>
</ui-tooltip>
</div>
<p class="text-sm text-gray-300 py-1 px-1" v-html="$getString('MessageShareURLWillBe', [demoShareUrl])" /> <p class="text-sm text-gray-300 py-1 px-1" v-html="$getString('MessageShareURLWillBe', [demoShareUrl])" />
<p class="text-sm text-gray-300 py-1 px-1" v-html="$getString('MessageShareExpirationWillBe', [expirationDateString])" /> <p class="text-sm text-gray-300 py-1 px-1" v-html="$getString('MessageShareExpirationWillBe', [expirationDateString])" />
</template> </template>
@@ -81,7 +91,8 @@ export default {
text: this.$strings.LabelDays, text: this.$strings.LabelDays,
value: 'days' value: 'days'
} }
] ],
isDownloadable: false
} }
}, },
watch: { watch: {
@@ -112,11 +123,11 @@ export default {
return this.$store.state.user.user return this.$store.state.user.user
}, },
demoShareUrl() { demoShareUrl() {
return `${window.origin}/share/${this.newShareSlug}` return `${window.origin}${this.$config.routerBasePath}/share/${this.newShareSlug}`
}, },
currentShareUrl() { currentShareUrl() {
if (!this.currentShare) return '' if (!this.currentShare) return ''
return `${window.origin}/share/${this.currentShare.slug}` return `${window.origin}${this.$config.routerBasePath}/share/${this.currentShare.slug}`
}, },
currentShareTimeRemaining() { currentShareTimeRemaining() {
if (!this.currentShare) return 'Error' if (!this.currentShare) return 'Error'
@@ -172,7 +183,8 @@ export default {
slug: this.newShareSlug, slug: this.newShareSlug,
mediaItemType: 'book', mediaItemType: 'book',
mediaItemId: this.libraryItem.media.id, mediaItemId: this.libraryItem.media.id,
expiresAt: this.expireDurationSeconds ? Date.now() + this.expireDurationSeconds * 1000 : 0 expiresAt: this.expireDurationSeconds ? Date.now() + this.expireDurationSeconds * 1000 : 0,
isDownloadable: this.isDownloadable
} }
this.processing = true this.processing = true
this.$axios this.$axios
@@ -2,7 +2,7 @@
<modals-modal v-model="show" name="changelog" :width="800" :height="'unset'"> <modals-modal v-model="show" name="changelog" :width="800" :height="'unset'">
<template #outer> <template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden"> <div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
<p class="text-3xl text-white truncate">Changelog</p> <h1 class="text-3xl text-white truncate">Changelog</h1>
</div> </div>
</template> </template>
<div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh"> <div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh">
@@ -13,7 +13,7 @@
</p> </p>
<div class="custom-text" v-html="getChangelog(release)" /> <div class="custom-text" v-html="getChangelog(release)" />
</div> </div>
<div v-if="release !== releasesToShow[releasesToShow.length - 1]" class="border-b border-black-300 my-8" /> <div v-if="release !== releasesToShow[releasesToShow.length - 1]" :key="`${release.name}-divider`" class="border-b border-black-300 my-8" />
</template> </template>
</div> </div>
</modals-modal> </modals-modal>
@@ -0,0 +1,188 @@
<template>
<modals-modal ref="modal" v-model="show" name="ereader-device-edit" :width="800" :height="'unset'" :processing="processing">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
<p class="text-3xl text-white truncate">{{ title }}</p>
</div>
</template>
<form @submit.prevent="submitForm">
<div class="w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300">
<div class="w-full px-3 py-5 md:p-12">
<div class="flex items-center -mx-1 mb-4">
<div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="ereaderNameInput" v-model="newDevice.name" :disabled="processing" :label="$strings.LabelName" />
</div>
<div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="ereaderEmailInput" v-model="newDevice.email" :disabled="processing" :label="$strings.LabelEmail" />
</div>
</div>
<div class="flex items-center pt-4">
<div class="flex-grow" />
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
</div>
</div>
</div>
</form>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean,
existingDevices: {
type: Array,
default: () => []
},
ereaderDevice: {
type: Object,
default: () => null
}
},
data() {
return {
processing: false,
newDevice: {
name: '',
email: '',
availabilityOption: 'adminAndUp',
users: []
}
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.init()
}
}
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
user() {
return this.$store.state.user.user
},
title() {
return !this.ereaderDevice ? 'Create Device' : 'Update Device'
}
},
methods: {
submitForm() {
this.$refs.ereaderNameInput.blur()
this.$refs.ereaderEmailInput.blur()
if (!this.newDevice.name?.trim() || !this.newDevice.email?.trim()) {
this.$toast.error(this.$strings.ToastNameEmailRequired)
return
}
this.newDevice.name = this.newDevice.name.trim()
this.newDevice.email = this.newDevice.email.trim()
// Only catches duplicate names for the current user
// Duplicates with other users caught on server side
if (!this.ereaderDevice) {
if (this.existingDevices.some((d) => d.name === this.newDevice.name)) {
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
return
}
this.submitCreate()
} else {
if (this.ereaderDevice.name !== this.newDevice.name && this.existingDevices.some((d) => d.name === this.newDevice.name)) {
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
return
}
this.submitUpdate()
}
},
submitUpdate() {
this.processing = true
const existingDevicesWithoutThisOne = this.existingDevices.filter((d) => d.name !== this.ereaderDevice.name)
const payload = {
ereaderDevices: [
...existingDevicesWithoutThisOne,
{
...this.newDevice
}
]
}
this.$axios
.$post(`/api/me/ereader-devices`, payload)
.then((data) => {
this.$emit('update', data.ereaderDevices)
this.show = false
})
.catch((error) => {
console.error('Failed to update device', error)
if (error.response?.data?.toLowerCase().includes('duplicate')) {
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
} else {
this.$toast.error(this.$strings.ToastDeviceAddFailed)
}
})
.finally(() => {
this.processing = false
})
},
submitCreate() {
this.processing = true
const payload = {
ereaderDevices: [
...this.existingDevices,
{
...this.newDevice
}
]
}
this.$axios
.$post('/api/me/ereader-devices', payload)
.then((data) => {
this.$emit('update', data.ereaderDevices || [])
this.show = false
})
.catch((error) => {
console.error('Failed to add device', error)
if (error.response?.data?.toLowerCase().includes('duplicate')) {
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
} else {
this.$toast.error(this.$strings.ToastDeviceAddFailed)
}
})
.finally(() => {
this.processing = false
})
},
init() {
if (this.ereaderDevice) {
this.newDevice.name = this.ereaderDevice.name
this.newDevice.email = this.ereaderDevice.email
this.newDevice.availabilityOption = this.ereaderDevice.availabilityOption || 'specificUsers'
this.newDevice.users = this.ereaderDevice.users || [this.user.id]
} else {
this.newDevice.name = ''
this.newDevice.email = ''
this.newDevice.availabilityOption = 'specificUsers'
this.newDevice.users = [this.user.id]
}
}
},
mounted() {}
}
</script>
+10 -10
View File
@@ -2,24 +2,24 @@
<modals-modal v-model="show" name="edit-book" :width="800" :height="height" :processing="processing" :content-margin-top="marginTop"> <modals-modal v-model="show" name="edit-book" :width="800" :height="height" :processing="processing" :content-margin-top="marginTop">
<template #outer> <template #outer>
<div class="absolute top-0 left-0 p-4 landscape:px-4 landscape:py-2 md:portrait:p-5 lg:p-5 w-2/3 overflow-hidden pointer-events-none"> <div class="absolute top-0 left-0 p-4 landscape:px-4 landscape:py-2 md:portrait:p-5 lg:p-5 w-2/3 overflow-hidden pointer-events-none">
<p class="text-xl md:portrait:text-3xl md:landscape:text-lg lg:text-3xl text-white truncate pointer-events-none">{{ title }}</p> <h1 class="text-xl md:portrait:text-3xl md:landscape:text-lg lg:text-3xl text-white truncate pointer-events-none">{{ title }}</h1>
</div> </div>
</template> </template>
<div class="absolute -top-10 left-0 z-10 w-full flex"> <div role="tablist" class="absolute -top-10 left-0 z-10 w-full flex">
<template v-for="tab in availableTabs"> <template v-for="tab in availableTabs">
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div> <button :key="tab.id" role="tab" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</button>
</template> </template>
</div> </div>
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6"> <div role="tabpanel" class="w-full h-full max-h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative">
<div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevBook" @mousedown.prevent>arrow_back_ios</div> <component v-if="libraryItem && show" :is="tabName" :library-item="libraryItem" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
</div>
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
<div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextBook" @mousedown.prevent>arrow_forward_ios</div>
</div> </div>
<div class="w-full h-full max-h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative"> <div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
<component v-if="libraryItem && show" :is="tabName" :library-item="libraryItem" :processing.sync="processing" @close="show = false" @selectTab="selectTab" /> <button class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" :aria-label="$strings.ButtonNext" @click.stop.prevent="goPrevBook" @mousedown.prevent>arrow_back_ios</button>
</div>
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
<button class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" :aria-label="$strings.ButtonPrevious" @click.stop.prevent="goNextBook" @mousedown.prevent>arrow_forward_ios</button>
</div> </div>
</modals-modal> </modals-modal>
</template> </template>
@@ -6,7 +6,7 @@
<ui-text-input-with-label ref="maxEpisodesInput" v-model="maxEpisodesToDownload" :disabled="checkingNewEpisodes" type="number" :label="$strings.LabelLimit" class="w-16 mr-2" input-class="h-10"> <ui-text-input-with-label ref="maxEpisodesInput" v-model="maxEpisodesToDownload" :disabled="checkingNewEpisodes" type="number" :label="$strings.LabelLimit" class="w-16 mr-2" input-class="h-10">
<div class="flex -mb-0.5"> <div class="flex -mb-0.5">
<p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': checkingNewEpisodes }">{{ $strings.LabelLimit }}</p> <p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': checkingNewEpisodes }">{{ $strings.LabelLimit }}</p>
<ui-tooltip direction="top" text="Max # of episodes to download. Use 0 for unlimited."> <ui-tooltip direction="top" :text="$strings.LabelMaxEpisodesToDownload">
<span class="material-symbols text-base">info</span> <span class="material-symbols text-base">info</span>
</ui-tooltip> </ui-tooltip>
</div> </div>
@@ -99,7 +99,7 @@ export default {
if (this.maxEpisodesToDownload < 0) { if (this.maxEpisodesToDownload < 0) {
this.maxEpisodesToDownload = 3 this.maxEpisodesToDownload = 3
this.$toast.error('Invalid max episodes to download') this.$toast.error(this.$strings.ToastInvalidMaxEpisodesToDownload)
return return
} }
@@ -120,9 +120,9 @@ export default {
.then((response) => { .then((response) => {
if (response.episodes && response.episodes.length) { if (response.episodes && response.episodes.length) {
console.log('New episodes', response.episodes.length) console.log('New episodes', response.episodes.length)
this.$toast.success(`${response.episodes.length} new episodes found!`) this.$toast.success(this.$getString('ToastNewEpisodesFound', [response.episodes.length]))
} else { } else {
this.$toast.info('No new episodes found') this.$toast.info(this.$strings.ToastNoNewEpisodesFound)
} }
this.checkingNewEpisodes = false this.checkingNewEpisodes = false
}) })
+17 -17
View File
@@ -60,7 +60,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" :label="$strings.LabelTitle" /> <ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" :label="$strings.LabelTitle" />
<p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('title', mediaMetadata.title)">{{ mediaMetadata.title || '' }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('title', mediaMetadata.title)">{{ mediaMetadata.title || '' }}</a>
</p> </p>
</div> </div>
</div> </div>
@@ -69,7 +69,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" :label="$strings.LabelSubtitle" /> <ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" :label="$strings.LabelSubtitle" />
<p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('subtitle', mediaMetadata.subtitle)">{{ mediaMetadata.subtitle }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('subtitle', mediaMetadata.subtitle)">{{ mediaMetadata.subtitle }}</a>
</p> </p>
</div> </div>
</div> </div>
@@ -78,7 +78,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" :label="$strings.LabelAuthor" /> <ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" :label="$strings.LabelAuthor" />
<p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('author', mediaMetadata.authorName)">{{ mediaMetadata.authorName }}</a> {{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('author', mediaMetadata.authorName)">{{ mediaMetadata.authorName }}</a>
</p> </p>
</div> </div>
</div> </div>
@@ -87,7 +87,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-multi-select v-model="selectedMatch.narrator" :items="narrators" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" /> <ui-multi-select v-model="selectedMatch.narrator" :items="narrators" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" />
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('narrator', mediaMetadata.narrators)">{{ mediaMetadata.narratorName }}</a> {{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('narrator', mediaMetadata.narrators)">{{ mediaMetadata.narratorName }}</a>
</p> </p>
</div> </div>
</div> </div>
@@ -96,7 +96,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" /> <ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" />
<p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</a> {{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</a>
</p> </p>
</div> </div>
</div> </div>
@@ -105,7 +105,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" :label="$strings.LabelPublisher" /> <ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" :label="$strings.LabelPublisher" />
<p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publisher', mediaMetadata.publisher)">{{ mediaMetadata.publisher }}</a> {{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publisher', mediaMetadata.publisher)">{{ mediaMetadata.publisher }}</a>
</p> </p>
</div> </div>
</div> </div>
@@ -114,7 +114,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" :label="$strings.LabelPublishYear" /> <ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" :label="$strings.LabelPublishYear" />
<p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publishedYear', mediaMetadata.publishedYear)">{{ mediaMetadata.publishedYear }}</a> {{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publishedYear', mediaMetadata.publishedYear)">{{ mediaMetadata.publishedYear }}</a>
</p> </p>
</div> </div>
</div> </div>
@@ -124,7 +124,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<widgets-series-input-widget v-model="selectedMatch.series" :disabled="!selectedMatchUsage.series" /> <widgets-series-input-widget v-model="selectedMatch.series" :disabled="!selectedMatchUsage.series" />
<p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('series', mediaMetadata.series)">{{ mediaMetadata.seriesName }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('series', mediaMetadata.series)">{{ mediaMetadata.seriesName }}</a>
</p> </p>
</div> </div>
</div> </div>
@@ -133,7 +133,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-multi-select v-model="selectedMatch.genres" :items="genres" :disabled="!selectedMatchUsage.genres" :label="$strings.LabelGenres" /> <ui-multi-select v-model="selectedMatch.genres" :items="genres" :disabled="!selectedMatchUsage.genres" :label="$strings.LabelGenres" />
<p v-if="mediaMetadata.genres?.length" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.genres?.length" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('genres', mediaMetadata.genres)">{{ mediaMetadata.genres.join(', ') }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('genres', mediaMetadata.genres)">{{ mediaMetadata.genres.join(', ') }}</a>
</p> </p>
</div> </div>
</div> </div>
@@ -142,7 +142,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-multi-select v-model="selectedMatch.tags" :items="tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" /> <ui-multi-select v-model="selectedMatch.tags" :items="tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" />
<p v-if="media.tags?.length" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="media.tags?.length" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('tags', media.tags)">{{ media.tags.join(', ') }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('tags', media.tags)">{{ media.tags.join(', ') }}</a>
</p> </p>
</div> </div>
</div> </div>
@@ -151,7 +151,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" :label="$strings.LabelLanguage" /> <ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" :label="$strings.LabelLanguage" />
<p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('language', mediaMetadata.language)">{{ mediaMetadata.language }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('language', mediaMetadata.language)">{{ mediaMetadata.language }}</a>
</p> </p>
</div> </div>
</div> </div>
@@ -160,7 +160,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" /> <ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" />
<p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('isbn', mediaMetadata.isbn)">{{ mediaMetadata.isbn }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('isbn', mediaMetadata.isbn)">{{ mediaMetadata.isbn }}</a>
</p> </p>
</div> </div>
</div> </div>
@@ -169,7 +169,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" /> <ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" />
<p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('asin', mediaMetadata.asin)">{{ mediaMetadata.asin }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('asin', mediaMetadata.asin)">{{ mediaMetadata.asin }}</a>
</p> </p>
</div> </div>
</div> </div>
@@ -179,7 +179,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.itunesId" type="number" :disabled="!selectedMatchUsage.itunesId" label="iTunes ID" /> <ui-text-input-with-label v-model="selectedMatch.itunesId" type="number" :disabled="!selectedMatchUsage.itunesId" label="iTunes ID" />
<p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesId', mediaMetadata.itunesId)">{{ mediaMetadata.itunesId }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesId', mediaMetadata.itunesId)">{{ mediaMetadata.itunesId }}</a>
</p> </p>
</div> </div>
</div> </div>
@@ -188,7 +188,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.feedUrl" :disabled="!selectedMatchUsage.feedUrl" label="RSS Feed URL" /> <ui-text-input-with-label v-model="selectedMatch.feedUrl" :disabled="!selectedMatchUsage.feedUrl" label="RSS Feed URL" />
<p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('feedUrl', mediaMetadata.feedUrl)">{{ mediaMetadata.feedUrl }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('feedUrl', mediaMetadata.feedUrl)">{{ mediaMetadata.feedUrl }}</a>
</p> </p>
</div> </div>
</div> </div>
@@ -197,7 +197,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.itunesPageUrl" :disabled="!selectedMatchUsage.itunesPageUrl" label="iTunes Page URL" /> <ui-text-input-with-label v-model="selectedMatch.itunesPageUrl" :disabled="!selectedMatchUsage.itunesPageUrl" label="iTunes Page URL" />
<p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesPageUrl', mediaMetadata.itunesPageUrl)">{{ mediaMetadata.itunesPageUrl }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesPageUrl', mediaMetadata.itunesPageUrl)">{{ mediaMetadata.itunesPageUrl }}</a>
</p> </p>
</div> </div>
</div> </div>
@@ -206,7 +206,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" :label="$strings.LabelReleaseDate" /> <ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" :label="$strings.LabelReleaseDate" />
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('releaseDate', mediaMetadata.releaseDate)">{{ mediaMetadata.releaseDate }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('releaseDate', mediaMetadata.releaseDate)">{{ mediaMetadata.releaseDate }}</a>
</p> </p>
</div> </div>
</div> </div>
@@ -2,28 +2,28 @@
<div class="w-full h-full relative"> <div class="w-full h-full relative">
<div id="scheduleWrapper" class="w-full overflow-y-auto px-2 py-4 md:px-6 md:py-6"> <div id="scheduleWrapper" class="w-full overflow-y-auto px-2 py-4 md:px-6 md:py-6">
<template v-if="!feedUrl"> <template v-if="!feedUrl">
<widgets-alert type="warning" class="text-base mb-4">No RSS feed URL is set for this podcast</widgets-alert> <widgets-alert type="warning" class="text-base mb-4">{{ $strings.ToastPodcastNoRssFeed }}</widgets-alert>
</template> </template>
<template v-if="feedUrl || autoDownloadEpisodes"> <template v-if="feedUrl || autoDownloadEpisodes">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<p class="text-base md:text-xl font-semibold">Schedule Automatic Episode Downloads</p> <p class="text-base md:text-xl font-semibold">{{ $strings.HeaderScheduleEpisodeDownloads }}</p>
<ui-checkbox v-model="enableAutoDownloadEpisodes" label="Enable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" /> <ui-checkbox v-model="enableAutoDownloadEpisodes" :label="$strings.LabelEnable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" />
</div> </div>
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2"> <div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
<ui-text-input ref="maxEpisodesInput" type="number" v-model="newMaxEpisodesToKeep" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updatedMaxEpisodesToKeep" /> <ui-text-input ref="maxEpisodesInput" type="number" v-model="newMaxEpisodesToKeep" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updatedMaxEpisodesToKeep" />
<ui-tooltip text="Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes. <br>This will only delete 1 episode per new download."> <ui-tooltip :text="$strings.LabelMaxEpisodesToKeepHelp">
<p class="pl-4 text-base"> <p class="pl-4 text-base">
Max episodes to keep {{ $strings.LabelMaxEpisodesToKeep }}
<span class="material-symbols icon-text">info</span> <span class="material-symbols icon-text">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2"> <div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
<ui-text-input ref="maxEpisodesToDownloadInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" /> <ui-text-input ref="maxEpisodesToDownloadInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" />
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded."> <ui-tooltip :text="$strings.LabelUseZeroForUnlimited">
<p class="pl-4 text-base"> <p class="pl-4 text-base">
Max new episodes to download per check {{ $strings.LabelMaxEpisodesToDownloadPerCheck }}
<span class="material-symbols icon-text">info</span> <span class="material-symbols icon-text">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
@@ -36,7 +36,7 @@
<div v-if="feedUrl || autoDownloadEpisodes" class="absolute bottom-0 left-0 w-full py-2 md:py-4 bg-bg border-t border-white border-opacity-5"> <div v-if="feedUrl || autoDownloadEpisodes" class="absolute bottom-0 left-0 w-full py-2 md:py-4 bg-bg border-t border-white border-opacity-5">
<div class="flex items-center px-2 md:px-4"> <div class="flex items-center px-2 md:px-4">
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn @click="save" :disabled="!isUpdated" :color="isUpdated ? 'success' : 'primary'" class="mx-2">{{ isUpdated ? 'Save' : 'No update necessary' }}</ui-btn> <ui-btn @click="save" :disabled="!isUpdated" :color="isUpdated ? 'success' : 'primary'" class="mx-2">{{ isUpdated ? $strings.ButtonSave : $strings.MessageNoUpdatesWereNecessary }}</ui-btn>
</div> </div>
</div> </div>
</div> </div>
@@ -111,7 +111,6 @@ export default {
}, },
updateLibrary(library) { updateLibrary(library) {
this.mapLibraryToCopy(library) this.mapLibraryToCopy(library)
console.log('Updated library', this.libraryCopy)
}, },
getNewLibraryData() { getNewLibraryData() {
return { return {
@@ -128,7 +127,9 @@ export default {
autoScanCronExpression: null, autoScanCronExpression: null,
hideSingleBookSeries: false, hideSingleBookSeries: false,
onlyShowLaterBooksInContinueSeries: false, onlyShowLaterBooksInContinueSeries: false,
metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'],
markAsFinishedPercentComplete: null,
markAsFinishedTimeRemaining: 10
} }
} }
}, },
@@ -160,7 +161,7 @@ export default {
return false return false
} }
if (!this.libraryCopy.folders.length) { if (!this.libraryCopy.folders.length) {
this.$toast.error('Library must have at least 1 path') this.$toast.error(this.$strings.ToastMustHaveAtLeastOnePath)
return false return false
} }
@@ -236,7 +237,6 @@ export default {
this.show = false this.show = false
this.$toast.success(this.$getString('ToastLibraryCreateSuccess', [res.name])) this.$toast.success(this.$getString('ToastLibraryCreateSuccess', [res.name]))
if (!this.$store.state.libraries.currentLibraryId) { if (!this.$store.state.libraries.currentLibraryId) {
console.log('Setting initially library id', res.id)
// First library added // First library added
this.$store.dispatch('libraries/fetch', res.id) this.$store.dispatch('libraries/fetch', res.id)
} }
@@ -1,78 +1,94 @@
<template> <template>
<div class="w-full h-full px-1 md:px-4 py-1 mb-4"> <div class="w-full h-full px-1 md:px-4 py-1 mb-4">
<div class="flex items-center py-3"> <div class="flex flex-wrap">
<ui-toggle-switch v-model="useSquareBookCovers" @input="formUpdated" /> <div class="flex items-center p-2 w-full md:w-1/2">
<ui-tooltip :text="$strings.LabelSettingsSquareBookCoversHelp"> <ui-toggle-switch v-model="useSquareBookCovers" size="sm" @input="formUpdated" />
<p class="pl-4 text-base"> <ui-tooltip :text="$strings.LabelSettingsSquareBookCoversHelp">
{{ $strings.LabelSettingsSquareBookCovers }} <p class="pl-4 text-sm">
<span class="material-symbols icon-text text-sm">info</span> {{ $strings.LabelSettingsSquareBookCovers }}
</p>
</ui-tooltip>
</div>
<div class="py-3">
<div class="flex items-center">
<ui-toggle-switch v-if="!globalWatcherDisabled" v-model="enableWatcher" @input="formUpdated" />
<ui-toggle-switch v-else disabled :value="false" />
<p class="pl-4 text-base">{{ $strings.LabelSettingsEnableWatcherForLibrary }}</p>
</div>
<p v-if="globalWatcherDisabled" class="text-xs text-warning">*{{ $strings.MessageWatcherIsDisabledGlobally }}</p>
</div>
<div v-if="isBookLibrary" class="flex items-center py-3">
<ui-toggle-switch v-model="audiobooksOnly" @input="formUpdated" />
<ui-tooltip :text="$strings.LabelSettingsAudiobooksOnlyHelp">
<p class="pl-4 text-base">
{{ $strings.LabelSettingsAudiobooksOnly }}
<span class="material-symbols icon-text text-sm">info</span>
</p>
</ui-tooltip>
</div>
<div v-if="isBookLibrary" class="py-3">
<div class="flex items-center">
<ui-toggle-switch v-model="skipMatchingMediaWithAsin" @input="formUpdated" />
<p class="pl-4 text-base">{{ $strings.LabelSettingsSkipMatchingBooksWithASIN }}</p>
</div>
</div>
<div v-if="isBookLibrary" class="py-3">
<div class="flex items-center">
<ui-toggle-switch v-model="skipMatchingMediaWithIsbn" @input="formUpdated" />
<p class="pl-4 text-base">{{ $strings.LabelSettingsSkipMatchingBooksWithISBN }}</p>
</div>
</div>
<div v-if="isBookLibrary" class="py-3">
<div class="flex items-center">
<ui-toggle-switch v-model="hideSingleBookSeries" @input="formUpdated" />
<ui-tooltip :text="$strings.LabelSettingsHideSingleBookSeriesHelp">
<p class="pl-4 text-base">
{{ $strings.LabelSettingsHideSingleBookSeries }}
<span class="material-symbols icon-text text-sm">info</span> <span class="material-symbols icon-text text-sm">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
</div> <div class="p-2 w-full md:w-1/2">
<div v-if="isBookLibrary" class="py-3"> <div class="flex items-center">
<div class="flex items-center"> <ui-toggle-switch v-if="!globalWatcherDisabled" v-model="enableWatcher" size="sm" @input="formUpdated" />
<ui-toggle-switch v-model="onlyShowLaterBooksInContinueSeries" @input="formUpdated" /> <ui-toggle-switch v-else disabled size="sm" :value="false" />
<ui-tooltip :text="$strings.LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp"> <p class="pl-4 text-sm">{{ $strings.LabelSettingsEnableWatcherForLibrary }}</p>
<p class="pl-4 text-base"> </div>
{{ $strings.LabelSettingsOnlyShowLaterBooksInContinueSeries }} <p v-if="globalWatcherDisabled" class="text-xs text-warning">*{{ $strings.MessageWatcherIsDisabledGlobally }}</p>
</div>
<div v-if="isBookLibrary" class="flex items-center p-2 w-full md:w-1/2">
<ui-toggle-switch v-model="audiobooksOnly" size="sm" @input="formUpdated" />
<ui-tooltip :text="$strings.LabelSettingsAudiobooksOnlyHelp">
<p class="pl-4 text-sm">
{{ $strings.LabelSettingsAudiobooksOnly }}
<span class="material-symbols icon-text text-sm">info</span> <span class="material-symbols icon-text text-sm">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
</div> <div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
<div v-if="isBookLibrary" class="py-3"> <div class="flex items-center">
<div class="flex items-center"> <ui-toggle-switch v-model="skipMatchingMediaWithAsin" size="sm" @input="formUpdated" />
<ui-toggle-switch v-model="epubsAllowScriptedContent" @input="formUpdated" /> <p class="pl-4 text-sm">{{ $strings.LabelSettingsSkipMatchingBooksWithASIN }}</p>
<ui-tooltip :text="$strings.LabelSettingsEpubsAllowScriptedContentHelp"> </div>
<p class="pl-4 text-base"> </div>
{{ $strings.LabelSettingsEpubsAllowScriptedContent }} <div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
<span class="material-symbols icon-text text-sm">info</span> <div class="flex items-center">
</p> <ui-toggle-switch v-model="skipMatchingMediaWithIsbn" size="sm" @input="formUpdated" />
</ui-tooltip> <p class="pl-4 text-sm">{{ $strings.LabelSettingsSkipMatchingBooksWithISBN }}</p>
</div>
</div>
<div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
<div class="flex items-center">
<ui-toggle-switch v-model="hideSingleBookSeries" size="sm" @input="formUpdated" />
<ui-tooltip :text="$strings.LabelSettingsHideSingleBookSeriesHelp">
<p class="pl-4 text-sm">
{{ $strings.LabelSettingsHideSingleBookSeries }}
<span class="material-symbols icon-text text-sm">info</span>
</p>
</ui-tooltip>
</div>
</div>
<div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
<div class="flex items-center">
<ui-toggle-switch v-model="onlyShowLaterBooksInContinueSeries" size="sm" @input="formUpdated" />
<ui-tooltip :text="$strings.LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp">
<p class="pl-4 text-sm">
{{ $strings.LabelSettingsOnlyShowLaterBooksInContinueSeries }}
<span class="material-symbols icon-text text-sm">info</span>
</p>
</ui-tooltip>
</div>
</div>
<div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
<div class="flex items-center">
<ui-toggle-switch v-model="epubsAllowScriptedContent" size="sm" @input="formUpdated" />
<ui-tooltip :text="$strings.LabelSettingsEpubsAllowScriptedContentHelp">
<p class="pl-4 text-sm">
{{ $strings.LabelSettingsEpubsAllowScriptedContent }}
<span class="material-symbols icon-text text-sm">info</span>
</p>
</ui-tooltip>
</div>
</div>
<div v-if="isPodcastLibrary" class="p-2 w-full md:w-1/2">
<ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-72" menu-max-height="200px" @input="formUpdated" />
</div>
<div class="p-2 w-full flex items-center space-x-2 flex-wrap">
<div>
<ui-dropdown v-model="markAsFinishedWhen" :items="maskAsFinishedWhenItems" :label="$strings.LabelSettingsLibraryMarkAsFinishedWhen" small class="w-72 min-w-72 text-sm" menu-max-height="200px" @input="markAsFinishedWhenChanged" />
</div>
<div class="w-16">
<div>
<label class="px-1 text-sm font-semibold"></label>
<div class="relative">
<ui-text-input v-model="markAsFinishedValue" type="number" label="" no-spinner custom-input-class="pr-5" @input="markAsFinishedChanged" />
<div class="absolute top-0 bottom-0 right-4 flex items-center">{{ markAsFinishedWhen === 'timeRemaining' ? '' : '%' }}</div>
</div>
</div>
</div>
</div> </div>
</div>
<div v-if="isPodcastLibrary" class="py-3">
<ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-72" menu-max-height="200px" @input="formUpdated" />
</div> </div>
</div> </div>
</template> </template>
@@ -97,7 +113,9 @@ export default {
epubsAllowScriptedContent: false, epubsAllowScriptedContent: false,
hideSingleBookSeries: false, hideSingleBookSeries: false,
onlyShowLaterBooksInContinueSeries: false, onlyShowLaterBooksInContinueSeries: false,
podcastSearchRegion: 'us' podcastSearchRegion: 'us',
markAsFinishedWhen: 'timeRemaining',
markAsFinishedValue: 10
} }
}, },
computed: { computed: {
@@ -119,10 +137,34 @@ export default {
providers() { providers() {
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
return this.$store.state.scanners.providers return this.$store.state.scanners.providers
},
maskAsFinishedWhenItems() {
return [
{
text: this.$strings.LabelSettingsLibraryMarkAsFinishedTimeRemaining,
value: 'timeRemaining'
},
{
text: this.$strings.LabelSettingsLibraryMarkAsFinishedPercentComplete,
value: 'percentComplete'
}
]
} }
}, },
methods: { methods: {
markAsFinishedWhenChanged(val) {
if (val === 'percentComplete' && this.markAsFinishedValue > 100) {
this.markAsFinishedValue = 100
}
this.formUpdated()
},
markAsFinishedChanged(val) {
this.formUpdated()
},
getLibraryData() { getLibraryData() {
let markAsFinishedTimeRemaining = this.markAsFinishedWhen === 'timeRemaining' ? Number(this.markAsFinishedValue) : null
let markAsFinishedPercentComplete = this.markAsFinishedWhen === 'percentComplete' ? Number(this.markAsFinishedValue) : null
return { return {
settings: { settings: {
coverAspectRatio: this.useSquareBookCovers ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD, coverAspectRatio: this.useSquareBookCovers ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD,
@@ -133,7 +175,9 @@ export default {
epubsAllowScriptedContent: !!this.epubsAllowScriptedContent, epubsAllowScriptedContent: !!this.epubsAllowScriptedContent,
hideSingleBookSeries: !!this.hideSingleBookSeries, hideSingleBookSeries: !!this.hideSingleBookSeries,
onlyShowLaterBooksInContinueSeries: !!this.onlyShowLaterBooksInContinueSeries, onlyShowLaterBooksInContinueSeries: !!this.onlyShowLaterBooksInContinueSeries,
podcastSearchRegion: this.podcastSearchRegion podcastSearchRegion: this.podcastSearchRegion,
markAsFinishedTimeRemaining: markAsFinishedTimeRemaining,
markAsFinishedPercentComplete: markAsFinishedPercentComplete
} }
} }
}, },
@@ -150,6 +194,11 @@ export default {
this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries
this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries
this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us' this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us'
this.markAsFinishedWhen = this.librarySettings.markAsFinishedTimeRemaining ? 'timeRemaining' : 'percentComplete'
if (!this.librarySettings.markAsFinishedTimeRemaining && !this.librarySettings.markAsFinishedPercentComplete) {
this.markAsFinishedWhen = 'timeRemaining'
}
this.markAsFinishedValue = this.librarySettings.markAsFinishedTimeRemaining || this.librarySettings.markAsFinishedPercentComplete || 10
} }
}, },
mounted() { mounted() {
@@ -3,13 +3,13 @@
<div class="w-full border border-black-200 p-4 my-8"> <div class="w-full border border-black-200 p-4 my-8">
<div class="flex flex-wrap items-center"> <div class="flex flex-wrap items-center">
<div> <div>
<p class="text-lg">Remove metadata files in library item folders</p> <p class="text-lg">{{ $strings.LabelRemoveMetadataFile }}</p>
<p class="max-w-sm text-sm pt-2 text-gray-300">Remove all metadata.json or metadata.abs files in your {{ mediaType }} folders</p> <p class="max-w-sm text-sm pt-2 text-gray-300">{{ $getString('LabelRemoveMetadataFileHelp', [mediaType]) }}</p>
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
<div> <div>
<ui-btn class="mb-4 block" @click.stop="removeAllMetadataClick('json')">Remove all metadata.json</ui-btn> <ui-btn class="mb-4 block" @click.stop="removeAllMetadataClick('json')">{{ $strings.LabelRemoveAllMetadataJson }}</ui-btn>
<ui-btn @click.stop="removeAllMetadataClick('abs')">Remove all metadata.abs</ui-btn> <ui-btn @click.stop="removeAllMetadataClick('abs')">{{ $strings.LabelRemoveAllMetadataAbs }}</ui-btn>
</div> </div>
</div> </div>
</div> </div>
@@ -43,7 +43,7 @@ export default {
methods: { methods: {
removeAllMetadataClick(ext) { removeAllMetadataClick(ext) {
const payload = { const payload = {
message: `Are you sure you want to remove all metadata.${ext} files in your library item folders?`, message: this.$getString('MessageConfirmRemoveMetadataFiles', [ext]),
persistent: true, persistent: true,
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
@@ -60,16 +60,16 @@ export default {
.$post(`/api/libraries/${this.libraryId}/remove-metadata?ext=${ext}`) .$post(`/api/libraries/${this.libraryId}/remove-metadata?ext=${ext}`)
.then((data) => { .then((data) => {
if (!data.found) { if (!data.found) {
this.$toast.info(`No metadata.${ext} files were found in library`) this.$toast.info(this.$getString('ToastMetadataFilesRemovedNoneFound', [ext]))
} else if (!data.removed) { } else if (!data.removed) {
this.$toast.success(`No metadata.${ext} files removed`) this.$toast.success(this.$getString('ToastMetadataFilesRemovedNoneRemoved', [ext]))
} else { } else {
this.$toast.success(`Successfully removed ${data.removed} metadata.${ext} files`) this.$toast.success(this.$getString('ToastMetadataFilesRemovedSuccess', [data.removed, ext]))
} }
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove metadata files', error) console.error('Failed to remove metadata files', error)
this.$toast.error('Failed to remove metadata files') this.$toast.error(this.$getString('ToastMetadataFilesRemovedError', [ext]))
}) })
.finally(() => { .finally(() => {
this.$emit('update:processing', false) this.$emit('update:processing', false)
@@ -156,7 +156,12 @@ export default {
return this.selectedFolder.fullPath return this.selectedFolder.fullPath
}, },
podcastTypes() { podcastTypes() {
return this.$store.state.globals.podcastTypes || [] return this.$store.state.globals.podcastTypes.map((e) => {
return {
text: this.$strings[e.descriptionKey] || e.text,
value: e.value
}
})
} }
}, },
methods: { methods: {
@@ -18,6 +18,23 @@
<p dir="auto" class="text-lg font-semibold mb-6">{{ title }}</p> <p dir="auto" class="text-lg font-semibold mb-6">{{ title }}</p>
<div v-if="description" dir="auto" class="default-style" v-html="description" /> <div v-if="description" dir="auto" class="default-style" v-html="description" />
<p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p> <p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p>
<div class="w-full h-px bg-white/5 my-4" />
<div class="flex items-center">
<div class="flex-grow">
<p class="font-semibold text-xs mb-1">{{ $strings.LabelFilename }}</p>
<p class="mb-2 text-xs">
{{ audioFileFilename }}
</p>
</div>
<div class="flex-grow">
<p class="font-semibold text-xs mb-1">{{ $strings.LabelSize }}</p>
<p class="mb-2 text-xs">
{{ audioFileSize }}
</p>
</div>
</div>
</div> </div>
</modals-modal> </modals-modal>
</template> </template>
@@ -54,7 +71,7 @@ export default {
return this.episode.description || '' return this.episode.description || ''
}, },
media() { media() {
return this.libraryItem ? this.libraryItem.media || {} : {} return this.libraryItem?.media || {}
}, },
mediaMetadata() { mediaMetadata() {
return this.media.metadata || {} return this.media.metadata || {}
@@ -65,6 +82,14 @@ export default {
podcastAuthor() { podcastAuthor() {
return this.mediaMetadata.author return this.mediaMetadata.author
}, },
audioFileFilename() {
return this.episode.audioFile?.metadata?.filename || ''
},
audioFileSize() {
const size = this.episode.audioFile?.metadata?.size || 0
return this.$bytesPretty(size)
},
bookCoverAspectRatio() { bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio'] return this.$store.getters['libraries/getBookCoverAspectRatio']
} }
@@ -33,11 +33,11 @@
</div> </div>
<div v-if="enclosureUrl" class="pb-4 pt-6"> <div v-if="enclosureUrl" class="pb-4 pt-6">
<ui-text-input-with-label :value="enclosureUrl" readonly class="text-xs"> <ui-text-input-with-label :value="enclosureUrl" readonly class="text-xs">
<label class="px-1 text-xs text-gray-200 font-semibold">Episode URL from RSS feed</label> <label class="px-1 text-xs text-gray-200 font-semibold">{{ $strings.LabelEpisodeUrlFromRssFeed }}</label>
</ui-text-input-with-label> </ui-text-input-with-label>
</div> </div>
<div v-else class="py-4"> <div v-else class="py-4">
<p class="text-xs text-gray-300 font-semibold">Episode not linked to RSS feed episode</p> <p class="text-xs text-gray-300 font-semibold">{{ $strings.LabelEpisodeNotLinkedToRssFeed }}</p>
</div> </div>
</div> </div>
</template> </template>
@@ -97,7 +97,12 @@ export default {
return this.enclosure.url return this.enclosure.url
}, },
episodeTypes() { episodeTypes() {
return this.$store.state.globals.episodeTypes || [] return this.$store.state.globals.episodeTypes.map((e) => {
return {
text: this.$strings[e.descriptionKey] || e.text,
value: e.value
}
})
} }
}, },
methods: { methods: {
@@ -152,14 +157,14 @@ export default {
const updateResult = await this.$axios.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatedDetails).catch((error) => { const updateResult = await this.$axios.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatedDetails).catch((error) => {
console.error('Failed update episode', error) console.error('Failed update episode', error)
this.isProcessing = false this.isProcessing = false
this.$toast.error(error?.response?.data || 'Failed to update episode') this.$toast.error(error?.response?.data || this.$strings.ToastFailedToUpdate)
return false return false
}) })
this.isProcessing = false this.isProcessing = false
if (updateResult) { if (updateResult) {
if (updateResult) { if (updateResult) {
this.$toast.success('Podcast episode updated') this.$toast.success(this.$strings.ToastItemUpdateSuccess)
return true return true
} else { } else {
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary) this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
@@ -10,9 +10,9 @@
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</p> <p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</p>
<div class="w-full relative"> <div class="w-full relative">
<ui-text-input v-model="currentFeed.feedUrl" readonly /> <ui-text-input :value="feedUrl" readonly />
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(currentFeed.feedUrl)">content_copy</span> <span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feedUrl)">content_copy</span>
</div> </div>
<div v-if="currentFeed.meta" class="mt-5"> <div v-if="currentFeed.meta" class="mt-5">
@@ -111,8 +111,11 @@ export default {
userIsAdminOrUp() { userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp'] return this.$store.getters['user/getIsAdminOrUp']
}, },
feedUrl() {
return this.currentFeed ? `${window.origin}${this.$config.routerBasePath}${this.currentFeed.feedUrl}` : ''
},
demoFeedUrl() { demoFeedUrl() {
return `${window.origin}/feed/${this.newFeedSlug}` return `${window.origin}${this.$config.routerBasePath}/feed/${this.newFeedSlug}`
}, },
isHttp() { isHttp() {
return window.origin.startsWith('http://') return window.origin.startsWith('http://')
@@ -139,7 +142,7 @@ export default {
slug: this.newFeedSlug, slug: this.newFeedSlug,
metadataDetails: this.metadataDetails metadataDetails: this.metadataDetails
} }
if (this.$isDev) payload.serverAddress = `http://localhost:3333${this.$config.routerBasePath}` if (this.$isDev) payload.serverAddress = process.env.serverUrl
console.log('Payload', payload) console.log('Payload', payload)
this.$axios this.$axios
@@ -5,8 +5,8 @@
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedGeneral }}</p> <p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedGeneral }}</p>
<div class="w-full relative"> <div class="w-full relative">
<ui-text-input v-model="feed.feedUrl" readonly /> <ui-text-input :value="feedUrl" readonly />
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feed.feedUrl)">content_copy</span> <span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feedUrl)">content_copy</span>
</div> </div>
<div v-if="feed.meta" class="mt-5"> <div v-if="feed.meta" class="mt-5">
@@ -70,6 +70,9 @@ export default {
}, },
_feed() { _feed() {
return this.feed || {} return this.feed || {}
},
feedUrl() {
return this.feed ? `${window.origin}${this.$config.routerBasePath}${this.feed.feedUrl}` : ''
} }
}, },
methods: { methods: {
+7 -1
View File
@@ -37,7 +37,7 @@
</ui-tooltip> </ui-tooltip>
<ui-tooltip direction="top" :text="$strings.LabelViewPlayerSettings"> <ui-tooltip direction="top" :text="$strings.LabelViewPlayerSettings">
<button :aria-label="$strings.LabelViewPlayerSettings" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerSettings')"> <button :aria-label="$strings.LabelViewPlayerSettings" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="showPlayerSettings">
<span class="material-symbols text-2xl sm:text-2.5xl">settings_slow_motion</span> <span class="material-symbols text-2xl sm:text-2.5xl">settings_slow_motion</span>
</button> </button>
</ui-tooltip> </ui-tooltip>
@@ -64,6 +64,8 @@
</div> </div>
<modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :playback-rate="playbackRate" :chapters="chapters" @select="selectChapter" /> <modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :playback-rate="playbackRate" :chapters="chapters" @select="selectChapter" />
<modals-player-settings-modal v-model="showPlayerSettingsModal" />
</div> </div>
</template> </template>
@@ -96,6 +98,7 @@ export default {
audioEl: null, audioEl: null,
seekLoading: false, seekLoading: false,
showChaptersModal: false, showChaptersModal: false,
showPlayerSettingsModal: false,
currentTime: 0, currentTime: 0,
duration: 0 duration: 0
} }
@@ -315,6 +318,9 @@ export default {
if (!this.chapters.length) return if (!this.chapters.length) return
this.showChaptersModal = !this.showChaptersModal this.showChaptersModal = !this.showChaptersModal
}, },
showPlayerSettings() {
this.showPlayerSettingsModal = !this.showPlayerSettingsModal
},
init() { init() {
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1 this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
+2 -2
View File
@@ -1,9 +1,9 @@
<template> <template>
<div> <div>
<div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center"> <div v-if="processing" role="img" :aria-label="$strings.MessageLoading" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
<widgets-loading-spinner /> <widgets-loading-spinner />
</div> </div>
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" /> <img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" :aria-label="$getString('LabelPersonalYearReview', [variant + 1])" />
</div> </div>
</template> </template>
+56 -15
View File
@@ -7,7 +7,7 @@
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<p class="hidden md:block text-xl font-semibold">{{ $getString('HeaderYearReview', [yearInReviewYear]) }}</p> <h1 class="hidden md:block text-xl font-semibold">{{ $getString('HeaderYearReview', [yearInReviewYear]) }}</h1>
<div class="hidden md:block flex-grow" /> <div class="hidden md:block flex-grow" />
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide : $strings.LabelYearReviewShow }}</ui-btn> <ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide : $strings.LabelYearReviewShow }}</ui-btn>
</div> </div>
@@ -16,17 +16,22 @@
<div v-if="showYearInReview"> <div v-if="showYearInReview">
<div class="w-full h-px bg-slate-200/10 my-4" /> <div class="w-full h-px bg-slate-200/10 my-4" />
<div class="flex items-center justify-center mb-2 max-w-[800px] mx-auto"> <div v-if="availableYears.length > 1" class="mb-2 py-2 max-w-[800px] mx-auto">
<!-- year selector -->
<ui-dropdown v-model="yearInReviewYear" small :items="availableYears" :disabled="processingYearInReview" class="max-w-24" @input="yearInReviewYearChanged" />
</div>
<div role="toolbar" class="flex items-center justify-center mb-2 max-w-[800px] mx-auto">
<!-- previous button --> <!-- previous button -->
<ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--"> <ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" :aria-label="$strings.ButtonPrevious" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--">
<span class="material-symbols text-lg sm:pr-1 py-px sm:py-0">chevron_left</span> <span class="material-symbols text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span> <span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
</ui-btn> </ui-btn>
<!-- share button --> <!-- share button -->
<ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview">{{ $strings.ButtonShare }} </ui-btn> <ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview">{{ $strings.ButtonShare }} </ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelPersonalYearReview', [yearInReviewVariant + 1]) }}</p> <h2 class="hidden sm:block text-lg font-semibold">{{ $getString('LabelPersonalYearReview', [yearInReviewVariant + 1]) }}</h2>
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p> <p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
@@ -36,7 +41,7 @@
<span class="material-symbols sm:!hidden text-lg py-px">refresh</span> <span class="material-symbols sm:!hidden text-lg py-px">refresh</span>
</ui-btn> </ui-btn>
<!-- next button --> <!-- next button -->
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++"> <ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" :aria-label="$strings.ButtonNext" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span> <span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
<span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span> <span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn> </ui-btn>
@@ -46,23 +51,23 @@
<!-- your year in review short --> <!-- your year in review short -->
<div class="w-full max-w-[800px] mx-auto my-4"> <div class="w-full max-w-[800px] mx-auto my-4">
<!-- share button --> <!-- share button -->
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex sm:hidden items-center font-semibold mb-1" @click="shareYearInReviewShort">{{ $strings.ButtonShare }}</ui-btn> <ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex items-center font-semibold mb-1" @click="shareYearInReviewShort">{{ $strings.ButtonShare }}</ui-btn>
<stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" /> <stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" />
</div> </div>
<!-- your server in review --> <!-- your server in review -->
<div v-if="isAdminOrUp" class="w-full max-w-[800px] mx-auto mb-2 mt-4 border-t pt-4 border-white/10"> <div v-if="isAdminOrUp" role="toolbar" class="w-full max-w-[800px] mx-auto mb-2 mt-4 border-t pt-4 border-white/10">
<div class="flex items-center justify-center mb-2"> <div class="flex items-center justify-center mb-2">
<!-- previous button --> <!-- previous button -->
<ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--"> <ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" :aria-label="$strings.ButtonPrevious" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--">
<span class="material-symbols text-lg sm:pr-1 py-px sm:py-0">chevron_left</span> <span class="material-symbols text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span> <span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
</ui-btn> </ui-btn>
<!-- share button --> <!-- share button -->
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer">{{ $strings.ButtonShare }} </ui-btn> <ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer">{{ $strings.ButtonShare }} </ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelServerYearReview', [yearInReviewServerVariant + 1]) }}</p> <h2 class="hidden sm:block text-lg font-semibold">{{ $getString('LabelServerYearReview', [yearInReviewServerVariant + 1]) }}</h2>
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p> <p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
@@ -72,7 +77,7 @@
<span class="material-symbols sm:!hidden text-lg py-px">refresh</span> <span class="material-symbols sm:!hidden text-lg py-px">refresh</span>
</ui-btn> </ui-btn>
<!-- next button --> <!-- next button -->
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++"> <ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" :aria-label="$strings.ButtonNext" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span> <span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
<span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span> <span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn> </ui-btn>
@@ -88,6 +93,7 @@ export default {
data() { data() {
return { return {
showYearInReview: false, showYearInReview: false,
availableYears: [],
yearInReviewYear: 0, yearInReviewYear: 0,
yearInReviewVariant: 0, yearInReviewVariant: 0,
yearInReviewServerVariant: 0, yearInReviewServerVariant: 0,
@@ -100,6 +106,9 @@ export default {
computed: { computed: {
isAdminOrUp() { isAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp'] return this.$store.getters['user/getIsAdminOrUp']
},
user() {
return this.$store.state.user.user
} }
}, },
methods: { methods: {
@@ -112,25 +121,57 @@ export default {
shareYearInReviewShort() { shareYearInReviewShort() {
this.$refs.yearInReviewShort.share() this.$refs.yearInReviewShort.share()
}, },
yearInReviewYearChanged() {
this.$nextTick(() => {
this.refreshYearInReview()
this.refreshYearInReviewServer()
})
},
refreshYearInReviewServer() { refreshYearInReviewServer() {
this.$refs.yearInReviewServer.refresh() if (this.$refs.yearInReviewServer != null) {
this.$refs.yearInReviewServer.refresh()
}
}, },
refreshYearInReview() { refreshYearInReview() {
this.$refs.yearInReview.refresh() if (this.$refs.yearInReview != null && this.$refs.yearInReviewShort != null) {
this.$refs.yearInReviewShort.refresh() this.$refs.yearInReview.refresh()
this.$refs.yearInReviewShort.refresh()
}
}, },
clickShowYearInReview() { clickShowYearInReview() {
this.showYearInReview = !this.showYearInReview this.showYearInReview = !this.showYearInReview
},
getAvailableYears() {
if (this.user) {
const oldestDate = this.user.createdAt
if (oldestDate) {
const date = new Date(oldestDate)
const oldestYear = date.getFullYear()
const currentYear = new Date().getFullYear()
const years = []
for (let year = currentYear; year >= oldestYear; year--) {
years.push({ value: year, text: year.toString() })
}
return years
}
}
// Fallback on error
return [{ value: this.yearInReviewYear, text: this.yearInReviewYear.toString() }]
} }
}, },
beforeMount() { beforeMount() {
this.yearInReviewYear = new Date().getFullYear() this.yearInReviewYear = new Date().getFullYear()
// When not December show previous year // When not December show previous year
if (new Date().getMonth() < 11) { if (new Date().getMonth() < 11) {
this.yearInReviewYear-- this.yearInReviewYear--
} }
}, },
mounted() { mounted() {
this.availableYears = this.getAvailableYears()
if (typeof navigator.share !== 'undefined' && navigator.share) { if (typeof navigator.share !== 'undefined' && navigator.share) {
this.showShareButton = true this.showShareButton = true
} else { } else {
@@ -1,9 +1,9 @@
<template> <template>
<div> <div>
<div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center"> <div v-if="processing" role="img" :aria-label="$strings.MessageLoading" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
<widgets-loading-spinner /> <widgets-loading-spinner />
</div> </div>
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" /> <img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" :aria-label="$getString('LabelServerYearReview', [variant + 1])" />
</div> </div>
</template> </template>
+1
View File
@@ -120,6 +120,7 @@ export default {
this.users = res.users.sort((a, b) => { this.users = res.users.sort((a, b) => {
return a.createdAt - b.createdAt return a.createdAt - b.createdAt
}) })
this.$emit('numUsers', this.users.length)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
@@ -12,10 +12,10 @@
</div> </div>
<div class="h-8 flex items-center"> <div class="h-8 flex items-center">
<div class="w-full inline-flex justify-between max-w-xl"> <div class="w-full inline-flex justify-between max-w-xl">
<p v-if="episode?.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p> <p v-if="episode?.season" class="text-sm text-gray-300">{{ $getString('LabelSeasonNumber', [episode.season]) }}</p>
<p v-if="episode?.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p> <p v-if="episode?.episode" class="text-sm text-gray-300">{{ $getString('LabelEpisodeNumber', [episode.episode]) }}</p>
<p v-if="episode?.chapters?.length" class="text-sm text-gray-300">{{ episode.chapters.length }} Chapters</p> <p v-if="episode?.chapters?.length" class="text-sm text-gray-300">{{ $getString('LabelChapterCount', [episode.chapters.length]) }}</p>
<p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, dateFormat) }}</p> <p v-if="publishedAt" class="text-sm text-gray-300">{{ $getString('LabelPublishedDate', [$formatDate(publishedAt, dateFormat)]) }}</p>
</div> </div>
</div> </div>
@@ -132,13 +132,13 @@ export default {
return this.store.state.streamIsPlaying && this.isStreaming return this.store.state.streamIsPlaying && this.isStreaming
}, },
timeRemaining() { timeRemaining() {
if (this.streamIsPlaying) return 'Playing' if (this.streamIsPlaying) return this.$strings.ButtonPlaying
if (!this.itemProgress) return this.$elapsedPretty(this.episode?.duration || 0) if (!this.itemProgress) return this.$elapsedPretty(this.episode?.duration || 0)
if (this.userIsFinished) return 'Finished' if (this.userIsFinished) return this.$strings.LabelFinished
const duration = this.itemProgress.duration || this.episode?.duration || 0 const duration = this.itemProgress.duration || this.episode?.duration || 0
const remaining = Math.floor(duration - this.itemProgress.currentTime) const remaining = Math.floor(duration - this.itemProgress.currentTime)
return `${this.$elapsedPretty(remaining)} left` return this.$getString('LabelTimeLeft', [this.$elapsedPretty(remaining)])
} }
}, },
methods: { methods: {
@@ -182,7 +182,7 @@ export default {
toggleFinished(confirmed = false) { toggleFinished(confirmed = false) {
if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) { if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) {
const payload = { const payload = {
message: `Are you sure you want to mark "${this.episodeTitle}" as finished?`, message: this.$getString('MessageConfirmMarkItemFinished', [this.episodeTitle]),
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.toggleFinished(true) this.toggleFinished(true)
@@ -25,7 +25,6 @@
</template> </template>
</div> </div>
</div> </div>
<!-- <p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p> -->
<div v-if="episodes.length" class="w-full py-3 mx-auto flex"> <div v-if="episodes.length" class="w-full py-3 mx-auto flex">
<form @submit.prevent="submit" class="flex flex-grow"> <form @submit.prevent="submit" class="flex flex-grow">
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" /> <ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
@@ -96,7 +95,7 @@ export default {
const menuItems = [] const menuItems = []
if (this.userIsAdminOrUp) { if (this.userIsAdminOrUp) {
menuItems.push({ menuItems.push({
text: 'Quick match all episodes', text: this.$strings.MessageQuickMatchAllEpisodes,
action: 'quick-match-episodes' action: 'quick-match-episodes'
}) })
} }
@@ -262,21 +261,21 @@ export default {
this.processing = true this.processing = true
const payload = { const payload = {
message: 'Quick matching episodes will overwrite details if a match is found. Only unmatched episodes will be updated. Are you sure?', message: this.$strings.MessageConfirmQuickMatchEpisodes,
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.$axios this.$axios
.$post(`/api/podcasts/${this.libraryItem.id}/match-episodes?override=1`) .$post(`/api/podcasts/${this.libraryItem.id}/match-episodes?override=1`)
.then((data) => { .then((data) => {
if (data.numEpisodesUpdated) { if (data.numEpisodesUpdated) {
this.$toast.success(`${data.numEpisodesUpdated} episodes updated`) this.$toast.success(this.$getString('ToastEpisodeUpdateSuccess', [data.numEpisodesUpdated]))
} else { } else {
this.$toast.info(this.$strings.ToastNoUpdatesNecessary) this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
} }
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to request match episodes', error) console.error('Failed to request match episodes', error)
this.$toast.error('Failed to match episodes') this.$toast.error(this.$strings.ToastFailedToMatch)
}) })
} }
this.processing = false this.processing = false
@@ -515,6 +514,10 @@ export default {
} }
}, },
filterSortChanged() { filterSortChanged() {
// Save filterKey and sortKey to local storage
localStorage.setItem('podcastEpisodesFilter', this.filterKey)
localStorage.setItem('podcastEpisodesSortBy', this.sortKey + (this.sortDesc ? '-desc' : ''))
this.init() this.init()
}, },
refresh() { refresh() {
@@ -537,6 +540,11 @@ export default {
} }
}, },
mounted() { mounted() {
this.filterKey = localStorage.getItem('podcastEpisodesFilter') || 'incomplete'
const sortBy = localStorage.getItem('podcastEpisodesSortBy') || 'publishedAt-desc'
this.sortKey = sortBy.split('-')[0]
this.sortDesc = sortBy.split('-')[1] === 'desc'
this.episodesCopy = this.episodes.map((ep) => ({ ...ep })) this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
this.initListeners() this.initListeners()
this.init() this.init()
+8 -8
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj"> <div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
<slot :disabled="disabled" :showMenu="showMenu" :clickShowMenu="clickShowMenu" :processing="processing"> <slot :disabled="disabled" :showMenu="showMenu" :clickShowMenu="clickShowMenu" :processing="processing">
<button v-if="!processing" type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu"> <button v-if="!processing" type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" :aria-label="$strings.LabelMore" aria-haspopup="menu" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
<span class="material-symbols text-2xl" :class="iconClass">&#xe5d4;</span> <span class="material-symbols text-2xl" :class="iconClass">&#xe5d4;</span>
</button> </button>
<div v-else class="h-full w-full flex items-center justify-center"> <div v-else class="h-full w-full flex items-center justify-center">
@@ -10,12 +10,12 @@
</slot> </slot>
<transition name="menu"> <transition name="menu">
<div v-show="showMenu" ref="menuWrapper" class="absolute right-0 mt-1 z-10 bg-bg border border-black-200 shadow-lg rounded-md py-1 focus:outline-none sm:text-sm" :style="{ width: menuWidth + 'px' }"> <div v-show="showMenu" ref="menuWrapper" role="menu" class="absolute right-0 mt-1 z-10 bg-bg border border-black-200 shadow-lg rounded-md py-1 focus:outline-none sm:text-sm" :style="{ width: menuWidth + 'px' }">
<template v-for="(item, index) in items"> <template v-for="(item, index) in items">
<template v-if="item.subitems"> <template v-if="item.subitems">
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-default" :class="{ 'bg-white/5': mouseoverItemIndex == index }" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop> <button :key="index" role="menuitem" aria-haspopup="menu" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-default w-full" :class="{ 'bg-white/5': mouseoverItemIndex == index }" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop>
<p>{{ item.text }}</p> <p>{{ item.text }}</p>
</div> </button>
<div <div
v-if="mouseoverItemIndex === index" v-if="mouseoverItemIndex === index"
:key="`subitems-${index}`" :key="`subitems-${index}`"
@@ -25,14 +25,14 @@
:class="openSubMenuLeft ? 'rounded-l-md' : 'rounded-r-md'" :class="openSubMenuLeft ? 'rounded-l-md' : 'rounded-r-md'"
:style="{ left: submenuLeftPos + 'px', top: index * 28 + 'px', width: submenuWidth + 'px' }" :style="{ left: submenuLeftPos + 'px', top: index * 28 + 'px', width: submenuWidth + 'px' }"
> >
<div v-for="(subitem, subitemindex) in item.subitems" :key="`subitem-${subitemindex}`" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer" @click.stop="clickAction(subitem.action, subitem.data)"> <button v-for="(subitem, subitemindex) in item.subitems" :key="`subitem-${subitemindex}`" role="menuitem" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer w-full" @click.stop="clickAction(subitem.action, subitem.data)">
<p>{{ subitem.text }}</p> <p>{{ subitem.text }}</p>
</div> </button>
</div> </div>
</template> </template>
<div v-else :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.action)"> <button v-else :key="index" role="menuitem" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-pointer w-full" @click.stop="clickAction(item.action)">
<p class="text-left">{{ item.text }}</p> <p class="text-left">{{ item.text }}</p>
</div> </button>
</template> </template>
</div> </div>
</transition> </transition>
+3 -3
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="relative w-full" v-click-outside="clickOutsideObj"> <div class="relative w-full" v-click-outside="clickOutsideObj">
<p v-if="label" class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p> <p v-if="label" class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
<button type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu"> <button type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left sm:text-sm" :class="buttonClass" aria-haspopup="menu" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
<span class="flex items-center"> <span class="flex items-center">
<span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small }">{{ selectedText }}</span> <span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small }">{{ selectedText }}</span>
<span v-if="selectedSubtext">:&nbsp;</span> <span v-if="selectedSubtext">:&nbsp;</span>
@@ -13,9 +13,9 @@
</button> </button>
<transition name="menu"> <transition name="menu">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="listbox" :style="{ maxHeight: menuMaxHeight }"> <ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="menu" :style="{ maxHeight: menuMaxHeight }">
<template v-for="item in itemsToShow"> <template v-for="item in itemsToShow">
<li :key="item.value" class="text-gray-100 relative py-2 cursor-pointer hover:bg-black-400" :id="'listbox-option-' + item.value" role="option" tabindex="0" @keyup.enter="clickedOption(item.value)" @click="clickedOption(item.value)"> <li :key="item.value" class="text-gray-100 relative py-2 cursor-pointer hover:bg-black-400" role="menuitem" tabindex="0" @keyup.enter="clickedOption(item.value)" @click="clickedOption(item.value)">
<div class="flex items-center"> <div class="flex items-center">
<span class="ml-3 block truncate font-sans text-sm" :class="{ 'font-semibold': item.subtext }">{{ item.text }}</span> <span class="ml-3 block truncate font-sans text-sm" :class="{ 'font-semibold': item.subtext }">{{ item.text }}</span>
<span v-if="item.subtext">:&nbsp;</span> <span v-if="item.subtext">:&nbsp;</span>
+3 -2
View File
@@ -1,5 +1,5 @@
<template> <template>
<button class="icon-btn rounded-md flex items-center justify-center relative" @mousedown.prevent :disabled="disabled || loading" :class="className" @click="clickBtn"> <button :aria-label="ariaLabel" class="icon-btn rounded-md flex items-center justify-center relative" @mousedown.prevent :disabled="disabled || loading" :class="className" @click="clickBtn">
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100"> <div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24"> <svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" /> <path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
@@ -28,7 +28,8 @@ export default {
size: { size: {
type: Number, type: Number,
default: 9 default: 9
} },
ariaLabel: String
}, },
data() { data() {
return {} return {}
+3 -3
View File
@@ -4,7 +4,7 @@
type="button" type="button"
:disabled="disabled" :disabled="disabled"
class="w-10 sm:w-full relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200" class="w-10 sm:w-full relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200"
aria-haspopup="listbox" aria-haspopup="menu"
:aria-expanded="showMenu" :aria-expanded="showMenu"
:aria-label="$strings.ButtonLibrary + ': ' + currentLibrary.name" :aria-label="$strings.ButtonLibrary + ': ' + currentLibrary.name"
@click.stop.prevent="clickShowMenu" @click.stop.prevent="clickShowMenu"
@@ -16,9 +16,9 @@
</button> </button>
<transition name="menu"> <transition name="menu">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full min-w-48 bg-primary border border-black-200 shadow-lg rounded-b-md py-1 overflow-auto focus:outline-none sm:text-sm librariesDropdownMenu" tabindex="-1" role="listbox"> <ul v-show="showMenu" class="absolute z-10 -mt-px w-full min-w-48 bg-primary border border-black-200 shadow-lg rounded-b-md py-1 overflow-auto focus:outline-none sm:text-sm librariesDropdownMenu" tabindex="-1" role="menu">
<template v-for="library in librariesFiltered"> <template v-for="library in librariesFiltered">
<li :key="library.id" class="text-gray-400 hover:text-white relative py-2 cursor-pointer hover:bg-black-400" role="option" tabindex="0" @keydown.enter="selectLibrary(library)" @click="selectLibrary(library)"> <li :key="library.id" class="text-gray-400 hover:text-white relative py-2 cursor-pointer hover:bg-black-400" role="menuitem" tabindex="0" @keydown.enter="selectLibrary(library)" @click="selectLibrary(library)">
<div class="flex items-center px-2"> <div class="flex items-center px-2">
<ui-library-icon :icon="library.icon" class="mr-1.5" /> <ui-library-icon :icon="library.icon" class="mr-1.5" />
<span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span> <span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span>
+15 -8
View File
@@ -1,17 +1,17 @@
<template> <template>
<div class="w-full"> <div class="w-full">
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p> <label :for="identifier" class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</label>
<div ref="wrapper" class="relative"> <div ref="wrapper" class="relative">
<form @submit.prevent="submitForm"> <form @submit.prevent="submitForm">
<div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-1" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent> <div ref="inputWrapper" role="list" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-1" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
<div v-for="item in selected" :key="item" class="rounded-full px-2 py-1 mx-0.5 my-0.5 text-xs bg-bg flex flex-nowrap break-all items-center relative"> <div v-for="item in selected" :key="item" role="listitem" class="rounded-full px-2 py-1 mx-0.5 my-0.5 text-xs bg-bg flex flex-nowrap break-all items-center relative">
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 px-1 bg-bg bg-opacity-75 flex items-center justify-end opacity-0 hover:opacity-100"> <div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 px-1 bg-bg bg-opacity-75 flex items-center justify-end opacity-0 hover:opacity-100" :class="{ 'opacity-100': inputFocused }">
<span v-if="showEdit" class="material-symbols text-white hover:text-warning cursor-pointer" style="font-size: 1.1rem" @click.stop="editItem(item)">edit</span> <button v-if="showEdit" type="button" :aria-label="$strings.ButtonEdit" class="material-symbols text-white hover:text-warning cursor-pointer" style="font-size: 1.1rem" @click.stop="editItem(item)">edit</button>
<span class="material-symbols text-white hover:text-error cursor-pointer" style="font-size: 1.1rem" @click.stop="removeItem(item)">close</span> <button type="button" :aria-label="$strings.ButtonRemove" class="material-symbols text-white hover:text-error focus:text-error cursor-pointer" style="font-size: 1.1rem" @click.stop="removeItem(item)" @keydown.enter.stop.prevent="removeItem(item)" @focus="setInputFocused(true)" @blur="setInputFocused(false)" tabindex="0">close</button>
</div> </div>
{{ item }} {{ item }}
</div> </div>
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" /> <input v-show="!readonly" v-model="textInput" ref="input" :id="identifier" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
</div> </div>
</form> </form>
@@ -66,7 +66,8 @@ export default {
typingTimeout: null, typingTimeout: null,
isFocused: false, isFocused: false,
menu: null, menu: null,
filteredItems: null filteredItems: null,
inputFocused: false
} }
}, },
watch: { watch: {
@@ -100,6 +101,9 @@ export default {
} }
return this.filteredItems return this.filteredItems
},
identifier() {
return Math.random().toString(36).substring(2)
} }
}, },
methods: { methods: {
@@ -129,6 +133,9 @@ export default {
}, 100) }, 100)
this.setInputWidth() this.setInputWidth()
}, },
setInputFocused(focused) {
this.inputFocused = focused
},
setInputWidth() { setInputWidth() {
setTimeout(() => { setTimeout(() => {
var value = this.$refs.input.value var value = this.$refs.input.value
+15 -8
View File
@@ -1,20 +1,20 @@
<template> <template>
<div class="w-full"> <div class="w-full">
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p> <label :for="identifier" class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</label>
<div ref="wrapper" class="relative"> <div ref="wrapper" class="relative">
<form @submit.prevent="submitForm"> <form @submit.prevent="submitForm">
<div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-0.5" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent> <div ref="inputWrapper" role="list" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-0.5" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
<div v-for="item in selected" :key="item.id" class="rounded-full px-2 py-0.5 m-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center justify-center relative min-w-12"> <div v-for="item in selected" :key="item.id" role="listitem" class="rounded-full px-2 py-0.5 m-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center justify-center relative min-w-12">
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer"> <div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer" :class="{ 'opacity-100': inputFocused }">
<span v-if="showEdit" class="material-symbols text-base text-white hover:text-warning mr-1" @click.stop="editItem(item)">edit</span> <button v-if="showEdit" type="button" :aria-label="$strings.ButtonEdit" class="material-symbols text-base text-white hover:text-warning focus:text-warning mr-1" @click.stop="editItem(item)" @keydown.enter.stop.prevent="editItem(item)" @focus="setInputFocused(true)" @blur="setInputFocused(false)" tabindex="0">edit</button>
<span class="material-symbols text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item.id)">close</span> <button type="button" :aria-label="$strings.ButtonRemove" class="material-symbols text-white hover:text-error focus:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item.id)" @keydown.enter.stop="removeItem(item.id)" @focus="setInputFocused(true)" @blur="setInputFocused(false)" tabindex="0">close</button>
</div> </div>
{{ item[textKey] }} {{ item[textKey] }}
</div> </div>
<div v-if="showEdit && !disabled" class="rounded-full cursor-pointer w-6 h-6 mx-0.5 bg-bg flex items-center justify-center"> <div v-if="showEdit && !disabled" class="rounded-full cursor-pointer w-6 h-6 mx-0.5 bg-bg flex items-center justify-center">
<span class="material-symbols text-white hover:text-success pt-px pr-px" style="font-size: 1.1rem" @click.stop="addItem">add</span> <button type="button" :aria-label="$strings.ButtonAdd" class="material-symbols text-white hover:text-success focus:text-success pt-px pr-px" style="font-size: 1.1rem" @click.stop="addItem" @keydown.enter.stop="addItem" tabindex="0">add</button>
</div> </div>
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" /> <input v-show="!readonly" v-model="textInput" ref="input" :id="identifier" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
</div> </div>
</form> </form>
@@ -65,6 +65,7 @@ export default {
currentSearch: null, currentSearch: null,
typingTimeout: null, typingTimeout: null,
isFocused: false, isFocused: false,
inputFocused: false,
menu: null, menu: null,
items: [] items: []
} }
@@ -102,6 +103,9 @@ export default {
}, },
filterData() { filterData() {
return this.$store.state.libraries.filterData || {} return this.$store.state.libraries.filterData || {}
},
identifier() {
return Math.random().toString(36).substring(2)
} }
}, },
methods: { methods: {
@@ -114,6 +118,9 @@ export default {
getIsSelected(itemValue) { getIsSelected(itemValue) {
return !!this.selected.find((i) => i.id === itemValue) return !!this.selected.find((i) => i.id === itemValue)
}, },
setInputFocused(focused) {
this.inputFocused = focused
},
search() { search() {
if (!this.textInput) return if (!this.textInput) return
this.currentSearch = this.textInput this.currentSearch = this.textInput
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<button class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" :class="borderless ? '' : 'bg-primary border border-gray-600'" @click="clickBtn"> <button :aria-label="isRead ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" :class="borderless ? '' : 'bg-primary border border-gray-600'" @click="clickBtn">
<div class="w-5 h-5 text-white relative"> <div class="w-5 h-5 text-white relative">
<svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)"> <svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" /> <path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
+3 -1
View File
@@ -57,7 +57,8 @@ export default {
inputName: String, inputName: String,
showCopy: Boolean, showCopy: Boolean,
step: [String, Number], step: [String, Number],
min: [String, Number] min: [String, Number],
customInputClass: String
}, },
data() { data() {
return { return {
@@ -82,6 +83,7 @@ export default {
_list.push(`py-${this.paddingY}`) _list.push(`py-${this.paddingY}`)
if (this.noSpinner) _list.push('no-spinner') if (this.noSpinner) _list.push('no-spinner')
if (this.textCenter) _list.push('text-center') if (this.textCenter) _list.push('text-center')
if (this.customInputClass) _list.push(this.customInputClass)
return _list.join(' ') return _list.join(' ')
}, },
actualType() { actualType() {
+15 -3
View File
@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<button :aria-labelledby="labeledBy" role="checkbox" type="button" class="border rounded-full border-black-100 flex items-center cursor-pointer w-10 justify-start" :aria-checked="toggleValue" :class="className" @click="clickToggle"> <button :aria-labelledby="labeledBy" :aria-label="label" role="checkbox" type="button" class="border rounded-full border-black-100 flex items-center cursor-pointer justify-start" :style="{ width: buttonWidth + 'px' }" :aria-checked="toggleValue" :class="className" @click="clickToggle">
<span class="rounded-full border w-5 h-5 border-black-50 shadow transform transition-transform duration-100" :class="switchClassName"></span> <span class="rounded-full border border-black-50 shadow transform transition-transform duration-100" :style="{ width: cursorHeightWidth + 'px', height: cursorHeightWidth + 'px' }" :class="switchClassName"></span>
</button> </button>
</div> </div>
</template> </template>
@@ -19,7 +19,12 @@ export default {
default: 'primary' default: 'primary'
}, },
disabled: Boolean, disabled: Boolean,
labeledBy: String labeledBy: String,
label: String,
size: {
type: String,
default: 'md'
}
}, },
computed: { computed: {
toggleValue: { toggleValue: {
@@ -37,6 +42,13 @@ export default {
switchClassName() { switchClassName() {
var bgColor = this.disabled ? 'bg-gray-300' : 'bg-white' var bgColor = this.disabled ? 'bg-gray-300' : 'bg-white'
return this.toggleValue ? 'translate-x-5 ' + bgColor : bgColor return this.toggleValue ? 'translate-x-5 ' + bgColor : bgColor
},
cursorHeightWidth() {
if (this.size === 'sm') return 16
return 20
},
buttonWidth() {
return this.cursorHeightWidth * 2
} }
}, },
methods: { methods: {
@@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<div class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent> <div aria-hidden="true" class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent>
<span class="material-symbols" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize" aria-label="Decrease Cover Size" role="button">&#xe15b;</span> <span class="material-symbols" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize" aria-label="Decrease Cover Size" role="button">&#xe15b;</span>
<p class="px-2 font-mono" style="font-size: 1rem">{{ bookCoverWidth }}</p> <p class="px-2 font-mono" style="font-size: 1rem">{{ bookCoverWidth }}</p>
<span class="material-symbols" :class="selectedSizeIndex === availableSizes.length - 1 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="increaseSize" aria-label="Increase Cover Size" role="button">&#xe145;</span> <span class="material-symbols" :class="selectedSizeIndex === availableSizes.length - 1 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="increaseSize" aria-label="Increase Cover Size" role="button">&#xe145;</span>
+2 -2
View File
@@ -3,10 +3,10 @@
<div class="flex items-center py-3e"> <div class="flex items-center py-3e">
<slot /> <slot />
<div class="flex-grow" /> <div class="flex-grow" />
<button cy-id="leftScrollButton" v-if="isScrollable" class="w-8e h-8e mx-1e flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft"> <button cy-id="leftScrollButton" v-if="isScrollable" :aria-label="$strings.ButtonScrollLeft" class="w-8e h-8e mx-1e flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">chevron_left</span> <span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">chevron_left</span>
</button> </button>
<button cy-id="rightScrollButton" v-if="isScrollable" class="w-8e h-8e mx-1e flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight"> <button cy-id="rightScrollButton" v-if="isScrollable" :aria-label="$strings.ButtonScrollRight" class="w-8e h-8e mx-1e flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">chevron_right</span> <span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">chevron_right</span>
</button> </button>
</div> </div>
@@ -101,7 +101,12 @@ export default {
return this.$store.state.libraries.filterData || {} return this.$store.state.libraries.filterData || {}
}, },
podcastTypes() { podcastTypes() {
return this.$store.state.globals.podcastTypes || [] return this.$store.state.globals.podcastTypes.map((e) => {
return {
text: this.$strings[e.descriptionKey] || e.text,
value: e.value
}
})
} }
}, },
methods: { methods: {
@@ -71,8 +71,6 @@ export default {
this.showSeriesForm = true this.showSeriesForm = true
}, },
submitSeriesForm() { submitSeriesForm() {
console.log('submit series form', this.value, this.selectedSeries)
if (!this.selectedSeries.name) { if (!this.selectedSeries.name) {
this.$toast.error('Must enter a series') this.$toast.error('Must enter a series')
return return
+2 -1
View File
@@ -357,7 +357,8 @@ export default {
teardown: false, teardown: false,
transports: ['websocket'], transports: ['websocket'],
upgrade: false, upgrade: false,
reconnection: true reconnection: true,
path: `${this.$config.routerBasePath}/socket.io`
}) })
this.$root.socket = this.socket this.$root.socket = this.socket
console.log('Socket initialized') console.log('Socket initialized')
+4 -6
View File
@@ -57,9 +57,10 @@ export default {
for (let entry of entries) { for (let entry of entries) {
this.cardWidth = entry.borderBoxSize[0].inlineSize this.cardWidth = entry.borderBoxSize[0].inlineSize
this.cardHeight = entry.borderBoxSize[0].blockSize this.cardHeight = entry.borderBoxSize[0].blockSize
this.resizeObserver.disconnect()
this.$refs.bookshelf.removeChild(instance.$el)
} }
this.coverHeight = instance.coverHeight
this.resizeObserver.disconnect()
this.$refs.bookshelf.removeChild(instance.$el)
}) })
instance.$el.style.visibility = 'hidden' instance.$el.style.visibility = 'hidden'
instance.$el.style.position = 'absolute' instance.$el.style.position = 'absolute'
@@ -131,10 +132,7 @@ export default {
this.entityComponentRefs[index] = instance this.entityComponentRefs[index] = instance
instance.$mount() instance.$mount()
const shelfOffsetY = this.shelfPaddingHeight * this.sizeMultiplier instance.$el.style.transform = this.entityTransform((index % this.entitiesPerShelf) + 1)
const row = index % this.entitiesPerShelf
const shelfOffsetX = row * this.totalEntityCardWidth + this.bookshelfMarginLeft
instance.$el.style.transform = `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)`
instance.$el.classList.add('absolute', 'top-0', 'left-0') instance.$el.classList.add('absolute', 'top-0', 'left-0')
shelfEl.appendChild(instance.$el) shelfEl.appendChild(instance.$el)
+15 -15
View File
@@ -28,10 +28,8 @@ export default {
var validOtherFiles = [] var validOtherFiles = []
var ignoredFiles = [] var ignoredFiles = []
files.forEach((file) => { files.forEach((file) => {
// var filetype = this.checkFileType(file.name)
if (!file.filetype) ignoredFiles.push(file) if (!file.filetype) ignoredFiles.push(file)
else { else {
// file.filetype = filetype
if (file.filetype === 'audio' || (file.filetype === 'ebook' && mediaType === 'book')) validItemFiles.push(file) if (file.filetype === 'audio' || (file.filetype === 'ebook' && mediaType === 'book')) validItemFiles.push(file)
else validOtherFiles.push(file) else validOtherFiles.push(file)
} }
@@ -165,7 +163,7 @@ export default {
var firstBookPath = Path.dirname(firstBookFile.filepath) var firstBookPath = Path.dirname(firstBookFile.filepath)
var dirs = firstBookPath.split('/').filter(d => !!d && d !== '.') var dirs = firstBookPath.split('/').filter((d) => !!d && d !== '.')
if (dirs.length) { if (dirs.length) {
audiobook.title = dirs.pop() audiobook.title = dirs.pop()
if (dirs.length > 1) { if (dirs.length > 1) {
@@ -189,7 +187,7 @@ export default {
var firstAudioFile = podcast.itemFiles[0] var firstAudioFile = podcast.itemFiles[0]
if (!firstAudioFile.filepath) return podcast // No path if (!firstAudioFile.filepath) return podcast // No path
var firstPath = Path.dirname(firstAudioFile.filepath) var firstPath = Path.dirname(firstAudioFile.filepath)
var dirs = firstPath.split('/').filter(d => !!d && d !== '.') var dirs = firstPath.split('/').filter((d) => !!d && d !== '.')
if (dirs.length) { if (dirs.length) {
podcast.title = dirs.length > 1 ? dirs[1] : dirs[0] podcast.title = dirs.length > 1 ? dirs[1] : dirs[0]
} else { } else {
@@ -212,13 +210,15 @@ export default {
} }
var ignoredFiles = itemData.ignoredFiles var ignoredFiles = itemData.ignoredFiles
var index = 1 var index = 1
var items = itemData.items.filter((ab) => { var items = itemData.items
if (!ab.itemFiles.length) { .filter((ab) => {
if (ab.otherFiles.length) ignoredFiles = ignoredFiles.concat(ab.otherFiles) if (!ab.itemFiles.length) {
if (ab.ignoredFiles.length) ignoredFiles = ignoredFiles.concat(ab.ignoredFiles) if (ab.otherFiles.length) ignoredFiles = ignoredFiles.concat(ab.otherFiles)
} if (ab.ignoredFiles.length) ignoredFiles = ignoredFiles.concat(ab.ignoredFiles)
return ab.itemFiles.length }
}).map(ab => this.cleanItem(ab, mediaType, index++)) return ab.itemFiles.length
})
.map((ab) => this.cleanItem(ab, mediaType, index++))
return { return {
items, items,
ignoredFiles ignoredFiles
@@ -259,7 +259,7 @@ export default {
otherFiles.forEach((file) => { otherFiles.forEach((file) => {
var dir = Path.dirname(file.filepath) var dir = Path.dirname(file.filepath)
var findItem = Object.values(itemMap).find(b => dir.startsWith(b.path)) var findItem = Object.values(itemMap).find((b) => dir.startsWith(b.path))
if (findItem) { if (findItem) {
findItem.otherFiles.push(file) findItem.otherFiles.push(file)
} else { } else {
@@ -270,18 +270,18 @@ export default {
var items = [] var items = []
var index = 1 var index = 1
// If book media type and all files are audio files then treat each one as an audiobook // If book media type and all files are audio files then treat each one as an audiobook
if (itemMap[''] && !otherFiles.length && mediaType === 'book' && !itemMap[''].itemFiles.some(f => f.filetype !== 'audio')) { if (itemMap[''] && !otherFiles.length && mediaType === 'book' && !itemMap[''].itemFiles.some((f) => f.filetype !== 'audio')) {
items = itemMap[''].itemFiles.map((audioFile) => { items = itemMap[''].itemFiles.map((audioFile) => {
return this.cleanItem({ itemFiles: [audioFile], otherFiles: [], ignoredFiles: [] }, mediaType, index++) return this.cleanItem({ itemFiles: [audioFile], otherFiles: [], ignoredFiles: [] }, mediaType, index++)
}) })
} else { } else {
items = Object.values(itemMap).map(i => this.cleanItem(i, mediaType, index++)) items = Object.values(itemMap).map((i) => this.cleanItem(i, mediaType, index++))
} }
return { return {
items, items,
ignoredFiles: ignoredFiles ignoredFiles: ignoredFiles
} }
}, }
} }
} }
+34 -49
View File
@@ -1,19 +1,24 @@
const pkg = require('./package.json') const pkg = require('./package.json')
const routerBasePath = process.env.ROUTER_BASE_PATH || ''
const serverHostUrl = process.env.NODE_ENV === 'production' ? '' : 'http://localhost:3333'
const serverPaths = ['api/', 'public/', 'hls/', 'auth/', 'feed/', 'status', 'login', 'logout', 'init']
const proxy = Object.fromEntries(serverPaths.map((path) => [`${routerBasePath}/${path}`, { target: process.env.NODE_ENV !== 'production' ? serverHostUrl : '/' }]))
module.exports = { module.exports = {
// Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode // Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode
ssr: false, ssr: false,
target: 'static', target: 'static',
dev: process.env.NODE_ENV !== 'production', dev: process.env.NODE_ENV !== 'production',
env: { env: {
serverUrl: process.env.NODE_ENV === 'production' ? process.env.ROUTER_BASE_PATH || '' : 'http://localhost:3333', serverUrl: serverHostUrl + routerBasePath,
chromecastReceiver: 'FD1F76C5' chromecastReceiver: 'FD1F76C5'
}, },
telemetry: false, telemetry: false,
publicRuntimeConfig: { publicRuntimeConfig: {
version: pkg.version, version: pkg.version,
routerBasePath: process.env.ROUTER_BASE_PATH || '' routerBasePath
}, },
// Global page headers: https://go.nuxtjs.dev/config-head // Global page headers: https://go.nuxtjs.dev/config-head
@@ -22,38 +27,23 @@ module.exports = {
htmlAttrs: { htmlAttrs: {
lang: 'en' lang: 'en'
}, },
meta: [ meta: [{ charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', name: 'description', content: '' }, { hid: 'robots', name: 'robots', content: 'noindex' }],
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' },
{ hid: 'robots', name: 'robots', content: 'noindex' }
],
script: [], script: [],
link: [ link: [
{ rel: 'icon', type: 'image/x-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/favicon.ico' }, { rel: 'icon', type: 'image/x-icon', href: routerBasePath + '/favicon.ico' },
{ rel: 'apple-touch-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/ios_icon.png' } { rel: 'apple-touch-icon', href: routerBasePath + '/ios_icon.png' }
] ]
}, },
router: { router: {
base: process.env.ROUTER_BASE_PATH || '' base: routerBasePath
}, },
// Global CSS: https://go.nuxtjs.dev/config-css // Global CSS: https://go.nuxtjs.dev/config-css
css: [ css: ['@/assets/tailwind.css', '@/assets/app.css'],
'@/assets/tailwind.css',
'@/assets/app.css'
],
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [ plugins: ['@/plugins/constants.js', '@/plugins/init.client.js', '@/plugins/axios.js', '@/plugins/toast.js', '@/plugins/utils.js', '@/plugins/i18n.js'],
'@/plugins/constants.js',
'@/plugins/init.client.js',
'@/plugins/axios.js',
'@/plugins/toast.js',
'@/plugins/utils.js',
'@/plugins/i18n.js'
],
// Auto import components: https://go.nuxtjs.dev/config-components // Auto import components: https://go.nuxtjs.dev/config-components
components: true, components: true,
@@ -65,30 +55,25 @@ module.exports = {
], ],
// Modules: https://go.nuxtjs.dev/config-modules // Modules: https://go.nuxtjs.dev/config-modules
modules: [ modules: ['nuxt-socket-io', '@nuxtjs/axios', '@nuxtjs/proxy'],
'nuxt-socket-io',
'@nuxtjs/axios',
'@nuxtjs/proxy'
],
proxy: { proxy,
'/api/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
'/dev/': { target: 'http://localhost:3333', pathRewrite: { '^/dev/': '' } }
},
io: { io: {
sockets: [{ sockets: [
name: 'dev', {
url: 'http://localhost:3333' name: 'dev',
}, url: serverHostUrl
{ },
name: 'prod' {
}] name: 'prod'
}
]
}, },
// Axios module configuration: https://go.nuxtjs.dev/config-axios // Axios module configuration: https://go.nuxtjs.dev/config-axios
axios: { axios: {
baseURL: process.env.ROUTER_BASE_PATH || '' baseURL: routerBasePath
}, },
// nuxt/pwa https://pwa.nuxtjs.org // nuxt/pwa https://pwa.nuxtjs.org
@@ -108,11 +93,11 @@ module.exports = {
background_color: '#232323', background_color: '#232323',
icons: [ icons: [
{ {
src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg', src: routerBasePath + '/icon.svg',
sizes: 'any' sizes: 'any'
}, },
{ {
src: (process.env.ROUTER_BASE_PATH || '') + '/icon192.png', src: routerBasePath + '/icon192.png',
type: 'image/png', type: 'image/png',
sizes: 'any' sizes: 'any'
} }
@@ -132,7 +117,7 @@ module.exports = {
postcssOptions: { postcssOptions: {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {}
} }
} }
} }
@@ -149,12 +134,12 @@ module.exports = {
}, },
/** /**
* Temporary workaround for @nuxt-community/tailwindcss-module. * Temporary workaround for @nuxt-community/tailwindcss-module.
* *
* Reported: 2022-05-23 * Reported: 2022-05-23
* See: [Issue tracker](https://github.com/nuxt-community/tailwindcss-module/issues/480) * See: [Issue tracker](https://github.com/nuxt-community/tailwindcss-module/issues/480)
*/ */
devServerHandlers: [], devServerHandlers: [],
ignore: ["**/*.test.*", "**/*.cy.*"] ignore: ['**/*.test.*', '**/*.cy.*']
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.15.0", "version": "2.17.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.15.0", "version": "2.17.6",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@nuxtjs/axios": "^5.13.6", "@nuxtjs/axios": "^5.13.6",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.15.0", "version": "2.17.6",
"buildNumber": 1, "buildNumber": 1,
"description": "Self-hosted audiobook and podcast client", "description": "Self-hosted audiobook and podcast client",
"main": "index.js", "main": "index.js",
+97 -1
View File
@@ -32,9 +32,48 @@
</form> </form>
</div> </div>
<div v-if="showEreaderTable">
<div class="w-full h-px bg-white/10 my-4" />
<app-settings-content :header-text="$strings.HeaderEreaderDevices">
<template #header-items>
<div class="flex-grow" />
<ui-btn color="primary" small @click="addNewDeviceClick">{{ $strings.ButtonAddDevice }}</ui-btn>
</template>
<table v-if="ereaderDevices.length" class="tracksTable mt-4">
<tr>
<th class="text-left">{{ $strings.LabelName }}</th>
<th class="text-left">{{ $strings.LabelEmail }}</th>
<th class="w-40"></th>
</tr>
<tr v-for="device in ereaderDevices" :key="device.name">
<td>
<p class="text-sm md:text-base text-gray-100">{{ device.name }}</p>
</td>
<td class="text-left">
<p class="text-sm md:text-base text-gray-100">{{ device.email }}</p>
</td>
<td class="w-40">
<div class="flex justify-end items-center h-10">
<ui-icon-btn icon="edit" borderless :size="8" icon-font-size="1.1rem" :disabled="deletingDeviceName === device.name || device.users?.length !== 1" class="mx-1" @click="editDeviceClick(device)" />
<ui-icon-btn icon="delete" borderless :size="8" icon-font-size="1.1rem" :disabled="deletingDeviceName === device.name || device.users?.length !== 1" @click="deleteDeviceClick(device)" />
</div>
</td>
</tr>
</table>
<div v-else-if="!loading" class="text-center py-4">
<p class="text-lg text-gray-100">{{ $strings.MessageNoDevices }}</p>
</div>
</app-settings-content>
</div>
<div class="py-4 mt-8 flex"> <div class="py-4 mt-8 flex">
<ui-btn color="primary flex items-center text-lg" @click="logout"><span class="material-symbols mr-4 icon-text">logout</span>{{ $strings.ButtonLogout }}</ui-btn> <ui-btn color="primary flex items-center text-lg" @click="logout"><span class="material-symbols mr-4 icon-text">logout</span>{{ $strings.ButtonLogout }}</ui-btn>
</div> </div>
<modals-emails-user-e-reader-device-modal v-model="showEReaderDeviceModal" :existing-devices="revisedEreaderDevices" :ereader-device="selectedEReaderDevice" @update="ereaderDevicesUpdated" />
</div> </div>
</div> </div>
</template> </template>
@@ -43,11 +82,20 @@
export default { export default {
data() { data() {
return { return {
loading: false,
password: null, password: null,
newPassword: null, newPassword: null,
confirmPassword: null, confirmPassword: null,
changingPassword: false, changingPassword: false,
selectedLanguage: '' selectedLanguage: '',
newEReaderDevice: {
name: '',
email: ''
},
ereaderDevices: [],
deletingDeviceName: null,
selectedEReaderDevice: null,
showEReaderDeviceModal: false
} }
}, },
computed: { computed: {
@@ -75,6 +123,12 @@ export default {
}, },
showChangePasswordForm() { showChangePasswordForm() {
return !this.isGuest && this.isPasswordAuthEnabled return !this.isGuest && this.isPasswordAuthEnabled
},
showEreaderTable() {
return this.usertype !== 'root' && this.usertype !== 'admin' && this.user.permissions?.createEreader
},
revisedEreaderDevices() {
return this.ereaderDevices.filter((device) => device.users?.length === 1)
} }
}, },
methods: { methods: {
@@ -142,10 +196,52 @@ export default {
this.$toast.error(this.$strings.ToastUnknownError) this.$toast.error(this.$strings.ToastUnknownError)
this.changingPassword = false this.changingPassword = false
}) })
},
addNewDeviceClick() {
this.selectedEReaderDevice = null
this.showEReaderDeviceModal = true
},
editDeviceClick(device) {
this.selectedEReaderDevice = device
this.showEReaderDeviceModal = true
},
deleteDeviceClick(device) {
const payload = {
message: this.$getString('MessageConfirmDeleteDevice', [device.name]),
callback: (confirmed) => {
if (confirmed) {
this.deleteDevice(device)
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
deleteDevice(device) {
const payload = {
ereaderDevices: this.revisedEreaderDevices.filter((d) => d.name !== device.name)
}
this.deletingDeviceName = device.name
this.$axios
.$post(`/api/me/ereader-devices`, payload)
.then((data) => {
this.ereaderDevicesUpdated(data.ereaderDevices)
})
.catch((error) => {
console.error('Failed to delete device', error)
this.$toast.error(this.$strings.ToastRemoveFailed)
})
.finally(() => {
this.deletingDeviceName = null
})
},
ereaderDevicesUpdated(ereaderDevices) {
this.ereaderDevices = ereaderDevices
} }
}, },
mounted() { mounted() {
this.selectedLanguage = this.$languageCodes.current this.selectedLanguage = this.$languageCodes.current
this.ereaderDevices = this.$store.state.libraries.ereaderDevices || []
} }
} }
</script> </script>
+3 -3
View File
@@ -415,7 +415,7 @@ export default {
const audioEl = this.audioEl || document.createElement('audio') const audioEl = this.audioEl || document.createElement('audio')
var src = audioTrack.contentUrl + `?token=${this.userToken}` var src = audioTrack.contentUrl + `?token=${this.userToken}`
if (this.$isDev) { if (this.$isDev) {
src = `http://localhost:3333${this.$config.routerBasePath}${src}` src = `${process.env.serverUrl}${src}`
} }
audioEl.src = src audioEl.src = src
@@ -486,7 +486,7 @@ export default {
.then((data) => { .then((data) => {
this.saving = false this.saving = false
if (data.updated) { if (data.updated) {
this.$toast.success('Chapters updated') this.$toast.success(this.$strings.ToastChaptersUpdated)
if (this.previousRoute) { if (this.previousRoute) {
this.$router.push(this.previousRoute) this.$router.push(this.previousRoute)
} else { } else {
@@ -533,7 +533,7 @@ export default {
}, },
findChapters() { findChapters() {
if (!this.asinInput) { if (!this.asinInput) {
this.$toast.error('Must input an ASIN') this.$toast.error(this.$strings.ToastAsinRequired)
return return
} }
+37 -1
View File
@@ -64,6 +64,20 @@
<ui-multi-select ref="redirectUris" v-model="newAuthSettings.authOpenIDMobileRedirectURIs" :items="newAuthSettings.authOpenIDMobileRedirectURIs" :label="$strings.LabelMobileRedirectURIs" class="mb-2" :menuDisabled="true" :disabled="savingSettings" /> <ui-multi-select ref="redirectUris" v-model="newAuthSettings.authOpenIDMobileRedirectURIs" :items="newAuthSettings.authOpenIDMobileRedirectURIs" :label="$strings.LabelMobileRedirectURIs" class="mb-2" :menuDisabled="true" :disabled="savingSettings" />
<p class="sm:pl-4 text-sm text-gray-300 mb-2" v-html="$strings.LabelMobileRedirectURIsDescription" /> <p class="sm:pl-4 text-sm text-gray-300 mb-2" v-html="$strings.LabelMobileRedirectURIsDescription" />
<div class="flex sm:items-center flex-col sm:flex-row pt-1 mb-2">
<div class="w-44">
<ui-dropdown v-model="newAuthSettings.authOpenIDSubfolderForRedirectURLs" small :items="subfolderOptions" :label="$strings.LabelWebRedirectURLsSubfolder" :disabled="savingSettings" />
</div>
<div class="mt-2 sm:mt-5">
<p class="sm:pl-4 text-sm text-gray-300">{{ $strings.LabelWebRedirectURLsDescription }}</p>
<p class="sm:pl-4 text-sm text-gray-300 mb-2">
<code>{{ webCallbackURL }}</code>
<br />
<code>{{ mobileAppCallbackURL }}</code>
</p>
</div>
</div>
<ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="$strings.LabelButtonText" class="mb-2" /> <ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="$strings.LabelButtonText" class="mb-2" />
<div class="flex sm:items-center flex-col sm:flex-row pt-1 mb-2"> <div class="flex sm:items-center flex-col sm:flex-row pt-1 mb-2">
@@ -164,6 +178,27 @@ export default {
value: 'username' value: 'username'
} }
] ]
},
subfolderOptions() {
const options = [
{
text: 'None',
value: ''
}
]
if (this.$config.routerBasePath) {
options.push({
text: this.$config.routerBasePath,
value: this.$config.routerBasePath
})
}
return options
},
webCallbackURL() {
return `https://<your.server.com>${this.newAuthSettings.authOpenIDSubfolderForRedirectURLs ? this.newAuthSettings.authOpenIDSubfolderForRedirectURLs : ''}/auth/openid/callback`
},
mobileAppCallbackURL() {
return `https://<your.server.com>${this.newAuthSettings.authOpenIDSubfolderForRedirectURLs ? this.newAuthSettings.authOpenIDSubfolderForRedirectURLs : ''}/auth/openid/mobile-redirect`
} }
}, },
methods: { methods: {
@@ -325,7 +360,8 @@ export default {
}, },
init() { init() {
this.newAuthSettings = { this.newAuthSettings = {
...this.authSettings ...this.authSettings,
authOpenIDSubfolderForRedirectURLs: this.authSettings.authOpenIDSubfolderForRedirectURLs === undefined ? this.$config.routerBasePath : this.authSettings.authOpenIDSubfolderForRedirectURLs
} }
this.enableLocalAuth = this.authMethods.includes('local') this.enableLocalAuth = this.authMethods.includes('local')
this.enableOpenIDAuth = this.authMethods.includes('openid') this.enableOpenIDAuth = this.authMethods.includes('openid')
+49 -40
View File
@@ -6,9 +6,9 @@
<div class="pt-4"> <div class="pt-4">
<h2 class="font-semibold">{{ $strings.HeaderSettingsGeneral }}</h2> <h2 class="font-semibold">{{ $strings.HeaderSettingsGeneral }}</h2>
</div> </div>
<div class="flex items-end py-2"> <div role="article" :aria-label="$strings.LabelSettingsStoreCoversWithItemHelp" class="flex items-end py-2">
<ui-toggle-switch labeledBy="settings-store-cover-with-items" v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" /> <ui-toggle-switch :label="$strings.LabelSettingsStoreCoversWithItem" v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
<ui-tooltip :text="$strings.LabelSettingsStoreCoversWithItemHelp"> <ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsStoreCoversWithItemHelp">
<p class="pl-4"> <p class="pl-4">
<span id="settings-store-cover-with-items">{{ $strings.LabelSettingsStoreCoversWithItem }}</span> <span id="settings-store-cover-with-items">{{ $strings.LabelSettingsStoreCoversWithItem }}</span>
<span class="material-symbols icon-text">info</span> <span class="material-symbols icon-text">info</span>
@@ -16,9 +16,9 @@
</ui-tooltip> </ui-tooltip>
</div> </div>
<div class="flex items-center py-2"> <div role="article" :aria-label="$strings.LabelSettingsStoreMetadataWithItemHelp" class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-store-metadata-with-items" v-model="newServerSettings.storeMetadataWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithItem', val)" /> <ui-toggle-switch :label="$strings.LabelSettingsStoreMetadataWithItem" v-model="newServerSettings.storeMetadataWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithItem', val)" />
<ui-tooltip :text="$strings.LabelSettingsStoreMetadataWithItemHelp"> <ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsStoreMetadataWithItemHelp">
<p class="pl-4"> <p class="pl-4">
<span id="settings-store-metadata-with-items">{{ $strings.LabelSettingsStoreMetadataWithItem }}</span> <span id="settings-store-metadata-with-items">{{ $strings.LabelSettingsStoreMetadataWithItem }}</span>
<span class="material-symbols icon-text">info</span> <span class="material-symbols icon-text">info</span>
@@ -26,9 +26,9 @@
</ui-tooltip> </ui-tooltip>
</div> </div>
<div class="flex items-center py-2"> <div role="article" :aria-label="$strings.LabelSettingsSortingIgnorePrefixesHelp" class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-sorting-ignore-prefixes" v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" /> <ui-toggle-switch :label="$strings.LabelSettingsSortingIgnorePrefixes" v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
<ui-tooltip :text="$strings.LabelSettingsSortingIgnorePrefixesHelp"> <ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsSortingIgnorePrefixesHelp">
<p class="pl-4"> <p class="pl-4">
<span id="settings-sorting-ignore-prefixes">{{ $strings.LabelSettingsSortingIgnorePrefixes }}</span> <span id="settings-sorting-ignore-prefixes">{{ $strings.LabelSettingsSortingIgnorePrefixes }}</span>
<span class="material-symbols icon-text">info</span> <span class="material-symbols icon-text">info</span>
@@ -42,18 +42,13 @@
</div> </div>
</div> </div>
<div class="flex items-center py-2 mb-2">
<ui-toggle-switch labeledBy="settings-chromecast-support" v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
<p class="pl-4" id="settings-chromecast-support">{{ $strings.LabelSettingsChromecastSupport }}</p>
</div>
<div class="pt-4"> <div class="pt-4">
<h2 class="font-semibold">{{ $strings.HeaderSettingsScanner }}</h2> <h2 class="font-semibold">{{ $strings.HeaderSettingsScanner }}</h2>
</div> </div>
<div class="flex items-center py-2"> <div role="article" :aria-label="$strings.LabelSettingsParseSubtitlesHelp" class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-parse-subtitles" v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" /> <ui-toggle-switch :label="$strings.LabelSettingsParseSubtitles" v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" />
<ui-tooltip :text="$strings.LabelSettingsParseSubtitlesHelp"> <ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsParseSubtitlesHelp">
<p class="pl-4"> <p class="pl-4">
<span id="settings-parse-subtitles">{{ $strings.LabelSettingsParseSubtitles }}</span> <span id="settings-parse-subtitles">{{ $strings.LabelSettingsParseSubtitles }}</span>
<span class="material-symbols icon-text">info</span> <span class="material-symbols icon-text">info</span>
@@ -61,9 +56,9 @@
</ui-tooltip> </ui-tooltip>
</div> </div>
<div class="flex items-center py-2"> <div role="article" :aria-label="$strings.LabelSettingsFindCoversHelp" class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-find-covers" v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" /> <ui-toggle-switch :label="$strings.LabelSettingsFindCovers" v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" />
<ui-tooltip :text="$strings.LabelSettingsFindCoversHelp"> <ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsFindCoversHelp">
<p class="pl-4"> <p class="pl-4">
<span id="settings-find-covers">{{ $strings.LabelSettingsFindCovers }}</span> <span id="settings-find-covers">{{ $strings.LabelSettingsFindCovers }}</span>
<span class="material-symbols icon-text">info</span> <span class="material-symbols icon-text">info</span>
@@ -75,9 +70,9 @@
<ui-dropdown v-model="newServerSettings.scannerCoverProvider" small :items="providers" label="Cover Provider" @input="updateScannerCoverProvider" :disabled="updatingServerSettings" /> <ui-dropdown v-model="newServerSettings.scannerCoverProvider" small :items="providers" label="Cover Provider" @input="updateScannerCoverProvider" :disabled="updatingServerSettings" />
</div> </div>
<div class="flex items-center py-2"> <div role="article" :aria-label="$strings.LabelSettingsPreferMatchedMetadataHelp" class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-prefer-matched-metadata" v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" /> <ui-toggle-switch :label="$strings.LabelSettingsPreferMatchedMetadata" v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
<ui-tooltip :text="$strings.LabelSettingsPreferMatchedMetadataHelp"> <ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
<p class="pl-4"> <p class="pl-4">
<span id="settings-prefer-matched-metadata">{{ $strings.LabelSettingsPreferMatchedMetadata }}</span> <span id="settings-prefer-matched-metadata">{{ $strings.LabelSettingsPreferMatchedMetadata }}</span>
<span class="material-symbols icon-text">info</span> <span class="material-symbols icon-text">info</span>
@@ -85,15 +80,29 @@
</ui-tooltip> </ui-tooltip>
</div> </div>
<div class="flex items-center py-2"> <div role="article" :aria-label="$strings.LabelSettingsEnableWatcherHelp" class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-disable-watcher" v-model="scannerEnableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', !val)" /> <ui-toggle-switch :label="$strings.LabelSettingsEnableWatcher" v-model="scannerEnableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', !val)" />
<ui-tooltip :text="$strings.LabelSettingsEnableWatcherHelp"> <ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsEnableWatcherHelp">
<p class="pl-4"> <p class="pl-4">
<span id="settings-disable-watcher">{{ $strings.LabelSettingsEnableWatcher }}</span> <span id="settings-disable-watcher">{{ $strings.LabelSettingsEnableWatcher }}</span>
<span class="material-symbols icon-text">info</span> <span class="material-symbols icon-text">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div class="pt-4">
<h2 class="font-semibold">{{ $strings.HeaderSettingsWebClient }}</h2>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.chromecastEnabled" :label="$strings.LabelSettingsChromecastSupport" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
<p aria-hidden="true" class="pl-4">{{ $strings.LabelSettingsChromecastSupport }}</p>
</div>
<div class="flex items-center py-2 mb-2">
<ui-toggle-switch v-model="newServerSettings.allowIframe" :label="$strings.LabelSettingsAllowIframe" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('allowIframe', val)" />
<p aria-hidden="true" class="pl-4">{{ $strings.LabelSettingsAllowIframe }}</p>
</div>
</div> </div>
<div class="flex-1"> <div class="flex-1">
@@ -324,21 +333,21 @@ export default {
}, },
updateServerSettings(payload) { updateServerSettings(payload) {
this.updatingServerSettings = true this.updatingServerSettings = true
this.$store this.$store.dispatch('updateServerSettings', payload).then((response) => {
.dispatch('updateServerSettings', payload) this.updatingServerSettings = false
.then(() => {
this.updatingServerSettings = false
if (payload.language) { if (response.error) {
// Updating language after save allows for re-rendering console.error('Failed to update server settins', response.error)
this.$setLanguageCode(payload.language) this.$toast.error(response.error)
} this.initServerSettings()
}) return
.catch((error) => { }
console.error('Failed to update server settings', error)
this.updatingServerSettings = false if (payload.language) {
this.$toast.error(this.$strings.ToastFailedToUpdate) // Updating language after save allows for re-rendering
}) this.$setLanguageCode(payload.language)
}
})
}, },
initServerSettings() { initServerSettings() {
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {} this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
+2 -2
View File
@@ -25,7 +25,7 @@
<tr v-for="feed in feeds" :key="feed.id" class="cursor-pointer h-12" @click="showFeed(feed)"> <tr v-for="feed in feeds" :key="feed.id" class="cursor-pointer h-12" @click="showFeed(feed)">
<!-- --> <!-- -->
<td> <td>
<img :src="coverUrl(feed)" class="h-full w-full" /> <img :src="coverUrl(feed)" class="h-auto w-full" />
</td> </td>
<!-- --> <!-- -->
<td class="w-48 max-w-64 min-w-24 text-left truncate"> <td class="w-48 max-w-64 min-w-24 text-left truncate">
@@ -126,7 +126,7 @@ export default {
}, },
coverUrl(feed) { coverUrl(feed) {
if (!feed.coverPath) return `${this.$config.routerBasePath}/Logo.png` if (!feed.coverPath) return `${this.$config.routerBasePath}/Logo.png`
return `${feed.feedUrl}/cover` return `${this.$config.routerBasePath}${feed.feedUrl}/cover`
}, },
async loadFeeds() { async loadFeeds() {
const data = await this.$axios.$get(`/api/feeds`).catch((err) => { const data = await this.$axios.$get(`/api/feeds`).catch((err) => {
+2 -2
View File
@@ -88,7 +88,7 @@
<ui-dropdown v-model="itemsPerPage" :items="itemsPerPageOptions" small class="w-24 mx-2" @input="updatedItemsPerPage" /> <ui-dropdown v-model="itemsPerPage" :items="itemsPerPageOptions" small class="w-24 mx-2" @input="updatedItemsPerPage" />
</div> </div>
<div class="inline-flex items-center"> <div class="inline-flex items-center">
<p class="text-sm mx-2">Page {{ currentPage + 1 }} of {{ numPages }}</p> <p class="text-sm mx-2">{{ $getString('LabelPaginationPageXOfY', [currentPage + 1, numPages]) }}</p>
<ui-icon-btn icon="arrow_back_ios_new" :size="9" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" /> <ui-icon-btn icon="arrow_back_ios_new" :size="9" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
<ui-icon-btn icon="arrow_forward_ios" :size="9" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" /> <ui-icon-btn icon="arrow_forward_ios" :size="9" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
</div> </div>
@@ -103,7 +103,7 @@
<div v-if="openListeningSessions.length" class="w-full my-8 h-px bg-white/10" /> <div v-if="openListeningSessions.length" class="w-full my-8 h-px bg-white/10" />
<!-- open listening sessions table --> <!-- open listening sessions table -->
<p v-if="openListeningSessions.length" class="text-lg my-4">Open Listening Sessions</p> <p v-if="openListeningSessions.length" class="text-lg my-4">{{ $strings.HeaderOpenListeningSessions }}</p>
<div v-if="openListeningSessions.length" class="block max-w-full"> <div v-if="openListeningSessions.length" class="block max-w-full">
<table class="userSessionsTable"> <table class="userSessionsTable">
<tr class="bg-primary bg-opacity-40"> <tr class="bg-primary bg-opacity-40">
+1 -1
View File
@@ -14,7 +14,7 @@
<h1 class="text-xl pl-2">{{ username }}</h1> <h1 class="text-xl pl-2">{{ username }}</h1>
</div> </div>
<div v-if="userToken" class="flex text-xs mt-4"> <div v-if="userToken" class="flex text-xs mt-4">
<ui-text-input-with-label label="API Token" :value="userToken" readonly /> <ui-text-input-with-label :label="$strings.LabelApiToken" :value="userToken" readonly />
<div class="px-1 mt-8 cursor-pointer" @click="copyToClipboard(userToken)"> <div class="px-1 mt-8 cursor-pointer" @click="copyToClipboard(userToken)">
<span class="material-symbols pl-2 text-base">content_copy</span> <span class="material-symbols pl-2 text-base">content_copy</span>
+1 -1
View File
@@ -54,7 +54,7 @@
</table> </table>
<div class="flex items-center justify-end py-1"> <div class="flex items-center justify-end py-1">
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" /> <ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
<p class="text-sm mx-1">Page {{ currentPage + 1 }} of {{ numPages }}</p> <p class="text-sm mx-1">{{ $getString('LabelPaginationPageXOfY', [currentPage + 1, numPages]) }}</p>
<ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" /> <ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
</div> </div>
</div> </div>
+7 -2
View File
@@ -2,6 +2,10 @@
<div> <div>
<app-settings-content :header-text="$strings.HeaderUsers"> <app-settings-content :header-text="$strings.HeaderUsers">
<template #header-items> <template #header-items>
<div v-if="numUsers" class="mx-2 px-1.5 rounded-lg bg-primary/50 text-gray-300/90 text-sm inline-flex items-center justify-center">
<span>{{ numUsers }}</span>
</div>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2"> <ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/users" target="_blank" class="inline-flex"> <a href="https://www.audiobookshelf.org/guides/users" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span> <span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
@@ -13,7 +17,7 @@
<ui-btn color="primary" small @click="setShowUserModal()">{{ $strings.ButtonAddUser }}</ui-btn> <ui-btn color="primary" small @click="setShowUserModal()">{{ $strings.ButtonAddUser }}</ui-btn>
</template> </template>
<tables-users-table class="pt-2" @edit="setShowUserModal" /> <tables-users-table class="pt-2" @edit="setShowUserModal" @numUsers="(count) => (numUsers = count)" />
</app-settings-content> </app-settings-content>
<modals-account-modal ref="accountModal" v-model="showAccountModal" :account="selectedAccount" /> <modals-account-modal ref="accountModal" v-model="showAccountModal" :account="selectedAccount" />
</div> </div>
@@ -29,7 +33,8 @@ export default {
data() { data() {
return { return {
selectedAccount: null, selectedAccount: null,
showAccountModal: false showAccountModal: false,
numUsers: 0
} }
}, },
computed: {}, computed: {},
+15 -8
View File
@@ -12,12 +12,12 @@
<!-- Item Cover Overlay --> <!-- Item Cover Overlay -->
<div class="absolute top-0 left-0 w-full h-full z-10 opacity-0 group-hover:opacity-100 pointer-events-none"> <div class="absolute top-0 left-0 w-full h-full z-10 opacity-0 group-hover:opacity-100 pointer-events-none">
<div v-show="showPlayButton && !isStreaming" class="h-full flex items-center justify-center pointer-events-none"> <div v-show="showPlayButton && !isStreaming" class="h-full flex items-center justify-center pointer-events-none">
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto cursor-pointer" @click.stop.prevent="playItem"> <button class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto cursor-pointer" :aria-label="$strings.ButtonPlay" @click.stop.prevent="playItem">
<span class="material-symbols fill text-4xl">play_arrow</span> <span class="material-symbols fill text-4xl">play_arrow</span>
</div> </button>
</div> </div>
<span class="absolute bottom-2.5 right-2.5 z-10 material-symbols text-lg cursor-pointer text-white text-opacity-75 hover:text-opacity-100 hover:scale-110 transform duration-200 pointer-events-auto" @click="showEditCover">edit</span> <button class="absolute bottom-2.5 right-2.5 z-10 material-symbols text-lg cursor-pointer text-white text-opacity-75 hover:text-opacity-100 hover:scale-110 transform duration-200 pointer-events-auto" :aria-label="$strings.ButtonEdit" @click="showEditCover">edit</button>
</div> </div>
</div> </div>
</div> </div>
@@ -87,7 +87,7 @@
</ui-btn> </ui-btn>
<ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2"> <ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
<span v-show="!isStreaming" class="material-symbols text-2xl -ml-2 pr-1 text-white">error</span> <span class="material-symbols text-2xl -ml-2 pr-1 text-white">error</span>
{{ isMissing ? $strings.LabelMissing : $strings.LabelIncomplete }} {{ isMissing ? $strings.LabelMissing : $strings.LabelIncomplete }}
</ui-btn> </ui-btn>
@@ -96,12 +96,12 @@
</ui-tooltip> </ui-tooltip>
<ui-btn v-if="showReadButton" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook"> <ui-btn v-if="showReadButton" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
<span class="material-symbols text-2xl -ml-2 pr-2 text-white">auto_stories</span> <span class="material-symbols text-2xl -ml-2 pr-2 text-white" aria-hidden="true">auto_stories</span>
{{ $strings.ButtonRead }} {{ $strings.ButtonRead }}
</ui-btn> </ui-btn>
<ui-tooltip v-if="userCanUpdate" :text="$strings.LabelEdit" direction="top"> <ui-tooltip v-if="userCanUpdate" :text="$strings.LabelEdit" direction="top">
<ui-icon-btn icon="&#xe3c9;" outlined class="mx-0.5" @click="editClick" /> <ui-icon-btn icon="&#xe3c9;" outlined class="mx-0.5" :aria-label="$strings.LabelEdit" @click="editClick" />
</ui-tooltip> </ui-tooltip>
<ui-tooltip v-if="!isPodcast" :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top"> <ui-tooltip v-if="!isPodcast" :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
@@ -110,12 +110,12 @@
<!-- Only admin or root user can download new episodes --> <!-- Only admin or root user can download new episodes -->
<ui-tooltip v-if="isPodcast && userIsAdminOrUp" :text="$strings.LabelFindEpisodes" direction="top"> <ui-tooltip v-if="isPodcast && userIsAdminOrUp" :text="$strings.LabelFindEpisodes" direction="top">
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" /> <ui-icon-btn icon="search" class="mx-0.5" :aria-label="$strings.LabelFindEpisodes" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
</ui-tooltip> </ui-tooltip>
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="148" @action="contextMenuAction"> <ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="148" @action="contextMenuAction">
<template #default="{ showMenu, clickShowMenu, disabled }"> <template #default="{ showMenu, clickShowMenu, disabled }">
<button type="button" :disabled="disabled" class="mx-0.5 icon-btn bg-primary border border-gray-600 w-9 h-9 rounded-md flex items-center justify-center relative" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu"> <button type="button" :disabled="disabled" class="mx-0.5 icon-btn bg-primary border border-gray-600 w-9 h-9 rounded-md flex items-center justify-center relative" aria-haspopup="listbox" :aria-expanded="showMenu" :aria-label="$strings.LabelMore" @click.stop.prevent="clickShowMenu">
<span class="material-symbols text-2xl">&#xe5d3;</span> <span class="material-symbols text-2xl">&#xe5d3;</span>
</button> </button>
</template> </template>
@@ -638,6 +638,11 @@ export default {
this.episodesDownloading = this.episodesDownloading.filter((d) => d.id !== episodeDownload.id) this.episodesDownloading = this.episodesDownloading.filter((d) => d.id !== episodeDownload.id)
} }
}, },
episodeDownloadQueueCleared(libraryItemId) {
if (libraryItemId === this.libraryItemId) {
this.episodeDownloadsQueued = []
}
},
rssFeedOpen(data) { rssFeedOpen(data) {
if (data.entityId === this.libraryItemId) { if (data.entityId === this.libraryItemId) {
console.log('RSS Feed Opened', data) console.log('RSS Feed Opened', data)
@@ -776,6 +781,7 @@ export default {
this.$root.socket.on('episode_download_queued', this.episodeDownloadQueued) this.$root.socket.on('episode_download_queued', this.episodeDownloadQueued)
this.$root.socket.on('episode_download_started', this.episodeDownloadStarted) this.$root.socket.on('episode_download_started', this.episodeDownloadStarted)
this.$root.socket.on('episode_download_finished', this.episodeDownloadFinished) this.$root.socket.on('episode_download_finished', this.episodeDownloadFinished)
this.$root.socket.on('episode_download_queue_cleared', this.episodeDownloadQueueCleared)
}, },
beforeDestroy() { beforeDestroy() {
this.$eventBus.$off(`${this.libraryItem.id}_updated`, this.libraryItemUpdated) this.$eventBus.$off(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
@@ -787,6 +793,7 @@ export default {
this.$root.socket.off('episode_download_queued', this.episodeDownloadQueued) this.$root.socket.off('episode_download_queued', this.episodeDownloadQueued)
this.$root.socket.off('episode_download_started', this.episodeDownloadStarted) this.$root.socket.off('episode_download_started', this.episodeDownloadStarted)
this.$root.socket.off('episode_download_finished', this.episodeDownloadFinished) this.$root.socket.off('episode_download_finished', this.episodeDownloadFinished)
this.$root.socket.off('episode_download_queue_cleared', this.episodeDownloadQueueCleared)
} }
} }
</script> </script>
+1 -1
View File
@@ -120,7 +120,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to updated narrator', error) console.error('Failed to updated narrator', error)
this.$toast.error('Failed to update narrator') this.$toast.error(this.$strings.ToastFailedToUpdate)
this.loading = false this.loading = false
}) })
}, },
@@ -104,9 +104,6 @@ export default {
this.episodesDownloading = this.episodesDownloading.filter((d) => d.id !== episodeDownload.id) this.episodesDownloading = this.episodesDownloading.filter((d) => d.id !== episodeDownload.id)
} }
}, },
episodeDownloadQueueUpdated(downloadQueueDetails) {
this.episodeDownloadsQueued = downloadQueueDetails.queue.filter((q) => q.libraryId == this.libraryId)
},
async loadInitialDownloadQueue() { async loadInitialDownloadQueue() {
this.processing = true this.processing = true
const queuePayload = await this.$axios.$get(`/api/libraries/${this.libraryId}/episode-downloads`).catch((error) => { const queuePayload = await this.$axios.$get(`/api/libraries/${this.libraryId}/episode-downloads`).catch((error) => {
@@ -128,7 +125,6 @@ export default {
this.$root.socket.on('episode_download_queued', this.episodeDownloadQueued) this.$root.socket.on('episode_download_queued', this.episodeDownloadQueued)
this.$root.socket.on('episode_download_started', this.episodeDownloadStarted) this.$root.socket.on('episode_download_started', this.episodeDownloadStarted)
this.$root.socket.on('episode_download_finished', this.episodeDownloadFinished) this.$root.socket.on('episode_download_finished', this.episodeDownloadFinished)
this.$root.socket.on('episode_download_queue_updated', this.episodeDownloadQueueUpdated)
} }
}, },
mounted() { mounted() {
@@ -138,7 +134,6 @@ export default {
this.$root.socket.off('episode_download_queued', this.episodeDownloadQueued) this.$root.socket.off('episode_download_queued', this.episodeDownloadQueued)
this.$root.socket.off('episode_download_started', this.episodeDownloadStarted) this.$root.socket.off('episode_download_started', this.episodeDownloadStarted)
this.$root.socket.off('episode_download_finished', this.episodeDownloadFinished) this.$root.socket.off('episode_download_finished', this.episodeDownloadFinished)
this.$root.socket.off('episode_download_queue_updated', this.episodeDownloadQueueUpdated)
} }
} }
</script> </script>
+24 -11
View File
@@ -10,8 +10,12 @@
<p v-if="mediaItemShare.playbackSession.displayAuthor" class="text-lg lg:text-xl text-slate-400 font-semibold text-center mb-1 truncate">{{ mediaItemShare.playbackSession.displayAuthor }}</p> <p v-if="mediaItemShare.playbackSession.displayAuthor" class="text-lg lg:text-xl text-slate-400 font-semibold text-center mb-1 truncate">{{ mediaItemShare.playbackSession.displayAuthor }}</p>
<div class="w-full pt-16"> <div class="w-full pt-16">
<player-ui ref="audioPlayer" :chapters="chapters" :paused="isPaused" :loading="!hasLoaded" :is-podcast="false" hide-bookmarks hide-sleep-timer @playPause="playPause" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setVolume="setVolume" @setPlaybackRate="setPlaybackRate" @seek="seek" /> <player-ui ref="audioPlayer" :chapters="chapters" :current-chapter="currentChapter" :paused="isPaused" :loading="!hasLoaded" :is-podcast="false" hide-bookmarks hide-sleep-timer @playPause="playPause" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setVolume="setVolume" @setPlaybackRate="setPlaybackRate" @seek="seek" />
</div> </div>
<ui-tooltip v-if="mediaItemShare.isDownloadable" direction="bottom" :text="$strings.LabelDownload" class="absolute top-0 left-0 m-4">
<button aria-label="Download" class="text-gray-300 hover:text-white" @click="downloadShareItem"><span class="material-symbols text-2xl sm:text-3xl">download</span></button>
</ui-tooltip>
</div> </div>
</div> </div>
</div> </div>
@@ -51,7 +55,8 @@ export default {
windowHeight: 0, windowHeight: 0,
listeningTimeSinceSync: 0, listeningTimeSinceSync: 0,
coverRgb: null, coverRgb: null,
coverBgIsLight: false coverBgIsLight: false,
currentTime: 0
} }
}, },
computed: { computed: {
@@ -60,16 +65,13 @@ export default {
}, },
coverUrl() { coverUrl() {
if (!this.playbackSession.coverPath) return `${this.$config.routerBasePath}/book_placeholder.jpg` if (!this.playbackSession.coverPath) return `${this.$config.routerBasePath}/book_placeholder.jpg`
if (process.env.NODE_ENV === 'development') { return `${this.$config.routerBasePath}/public/share/${this.mediaItemShare.slug}/cover`
return `http://localhost:3333/public/share/${this.mediaItemShare.slug}/cover` },
} downloadUrl() {
return `/public/share/${this.mediaItemShare.slug}/cover` return `${process.env.serverUrl}/public/share/${this.mediaItemShare.slug}/download`
}, },
audioTracks() { audioTracks() {
return (this.playbackSession.audioTracks || []).map((track) => { return (this.playbackSession.audioTracks || []).map((track) => {
if (process.env.NODE_ENV === 'development') {
track.contentUrl = `${process.env.serverUrl}${track.contentUrl}`
}
track.relativeContentUrl = track.contentUrl track.relativeContentUrl = track.contentUrl
return track return track
}) })
@@ -83,6 +85,9 @@ export default {
chapters() { chapters() {
return this.playbackSession.chapters || [] return this.playbackSession.chapters || []
}, },
currentChapter() {
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
},
coverAspectRatio() { coverAspectRatio() {
const coverAspectRatio = this.playbackSession.coverAspectRatio const coverAspectRatio = this.playbackSession.coverAspectRatio
return coverAspectRatio === this.$constants.BookCoverAspectRatio.STANDARD ? 1.6 : 1 return coverAspectRatio === this.$constants.BookCoverAspectRatio.STANDARD ? 1.6 : 1
@@ -128,12 +133,14 @@ export default {
if (!this.localAudioPlayer || !this.hasLoaded) return if (!this.localAudioPlayer || !this.hasLoaded) return
const currentTime = this.localAudioPlayer.getCurrentTime() const currentTime = this.localAudioPlayer.getCurrentTime()
const duration = this.localAudioPlayer.getDuration() const duration = this.localAudioPlayer.getDuration()
this.seek(Math.min(currentTime + 10, duration)) const jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount') || 10
this.seek(Math.min(currentTime + jumpForwardAmount, duration))
}, },
jumpBackward() { jumpBackward() {
if (!this.localAudioPlayer || !this.hasLoaded) return if (!this.localAudioPlayer || !this.hasLoaded) return
const currentTime = this.localAudioPlayer.getCurrentTime() const currentTime = this.localAudioPlayer.getCurrentTime()
this.seek(Math.max(currentTime - 10, 0)) const jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount') || 10
this.seek(Math.max(currentTime - jumpBackwardAmount, 0))
}, },
setVolume(volume) { setVolume(volume) {
if (!this.localAudioPlayer || !this.hasLoaded) return if (!this.localAudioPlayer || !this.hasLoaded) return
@@ -154,6 +161,7 @@ export default {
// Update UI // Update UI
this.$refs.audioPlayer.setCurrentTime(time) this.$refs.audioPlayer.setCurrentTime(time)
this.currentTime = time
}, },
setDuration() { setDuration() {
if (!this.localAudioPlayer) return if (!this.localAudioPlayer) return
@@ -246,9 +254,14 @@ export default {
}, },
playerFinished() { playerFinished() {
console.log('Player finished') console.log('Player finished')
},
downloadShareItem() {
this.$downloadFile(this.downloadUrl)
} }
}, },
mounted() { mounted() {
this.$store.dispatch('user/loadUserSettings')
this.resize() this.resize()
window.addEventListener('resize', this.resize) window.addEventListener('resize', this.resize)
window.addEventListener('keydown', this.keyDown) window.addEventListener('keydown', this.keyDown)
+16 -12
View File
@@ -1,20 +1,20 @@
<template> <template>
<div id="page-wrapper" class="page p-0 sm:p-6 overflow-y-auto" :class="streamLibraryItem ? 'streaming' : ''"> <div id="page-wrapper" class="page p-1 sm:p-6 overflow-y-auto" :class="streamLibraryItem ? 'streaming' : ''">
<div class="w-full max-w-6xl mx-auto"> <div class="w-full max-w-6xl mx-auto">
<!-- Library & folder picker --> <!-- Library & folder picker -->
<div class="flex my-6 -mx-2"> <div class="flex flex-wrap my-6 md:-mx-2">
<div class="w-1/5 px-2"> <div class="w-full md:w-1/5 px-2">
<ui-dropdown v-model="selectedLibraryId" :items="libraryItems" :label="$strings.LabelLibrary" :disabled="!!items.length" @input="libraryChanged" /> <ui-dropdown v-model="selectedLibraryId" :items="libraryItems" :label="$strings.LabelLibrary" :disabled="!!items.length" @input="libraryChanged" />
</div> </div>
<div class="w-3/5 px-2"> <div class="w-full md:w-3/5 px-2">
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="!selectedLibraryId || !!items.length" :label="$strings.LabelFolder" /> <ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="!selectedLibraryId || !!items.length" :label="$strings.LabelFolder" />
</div> </div>
<div class="w-1/5 px-2"> <div class="w-full md:w-1/5 px-2">
<ui-text-input-with-label :value="selectedLibraryMediaType" readonly :label="$strings.LabelMediaType" /> <ui-text-input-with-label :value="selectedLibraryMediaType" readonly :label="$strings.LabelMediaType" />
</div> </div>
</div> </div>
<div v-if="!selectedLibraryIsPodcast" class="flex items-center mb-6"> <div v-if="!selectedLibraryIsPodcast" class="flex items-center mb-6 px-2 md:px-0">
<label class="flex cursor-pointer pt-4"> <label class="flex cursor-pointer pt-4">
<ui-toggle-switch v-model="fetchMetadata.enabled" class="inline-flex" /> <ui-toggle-switch v-model="fetchMetadata.enabled" class="inline-flex" />
<span class="pl-2 text-base">{{ $strings.LabelAutoFetchMetadata }}</span> <span class="pl-2 text-base">{{ $strings.LabelAutoFetchMetadata }}</span>
@@ -33,13 +33,13 @@
</widgets-alert> </widgets-alert>
<!-- Picker display --> <!-- Picker display -->
<div v-if="!items.length && !ignoredFiles.length" class="w-full mx-auto border border-white border-opacity-20 px-12 pt-12 pb-4 my-12 relative" :class="isDragging ? 'bg-primary bg-opacity-40' : 'border-dashed'"> <div v-if="!items.length && !ignoredFiles.length" class="w-full mx-auto border border-white border-opacity-20 px-4 md:px-12 pt-12 pb-4 my-12 relative" :class="isDragging ? 'bg-primary bg-opacity-40' : 'border-dashed'">
<p class="text-2xl text-center">{{ isDragging ? $strings.LabelUploaderDropFiles : $strings.LabelUploaderDragAndDrop }}</p> <p class="text-2xl text-center">{{ isDragging ? $strings.LabelUploaderDropFiles : isIOS ? $strings.LabelUploaderDragAndDropFilesOnly : $strings.LabelUploaderDragAndDrop }}</p>
<p class="text-center text-sm my-5">{{ $strings.MessageOr }}</p> <p class="text-center text-sm my-5">{{ $strings.MessageOr }}</p>
<div class="w-full max-w-xl mx-auto"> <div class="w-full max-w-xl mx-auto">
<div class="flex"> <div class="flex">
<ui-btn class="w-full mx-1" @click="openFilePicker">{{ $strings.ButtonChooseFiles }}</ui-btn> <ui-btn class="w-full mx-1" @click="openFilePicker">{{ $strings.ButtonChooseFiles }}</ui-btn>
<ui-btn class="w-full mx-1" @click="openFolderPicker">{{ $strings.ButtonChooseAFolder }}</ui-btn> <ui-btn v-if="!isIOS" class="w-full mx-1" @click="openFolderPicker">{{ $strings.ButtonChooseAFolder }} </ui-btn>
</div> </div>
</div> </div>
<div class="pt-8 text-center"> <div class="pt-8 text-center">
@@ -48,7 +48,7 @@
</p> </p>
<p class="text-sm text-white text-opacity-70"> <p class="text-sm text-white text-opacity-70">
{{ $strings.NoteUploaderFoldersWithMediaFiles }} <span v-if="selectedLibraryMediaType === 'book'">{{ $strings.NoteUploaderOnlyAudioFiles }}</span> <span v-if="!isIOS">{{ $strings.NoteUploaderFoldersWithMediaFiles }}</span> <span v-if="selectedLibraryMediaType === 'book'">{{ $strings.NoteUploaderOnlyAudioFiles }}</span>
</p> </p>
</div> </div>
</div> </div>
@@ -84,8 +84,8 @@
</div> </div>
</div> </div>
<input ref="fileInput" type="file" multiple :accept="inputAccept" class="hidden" @change="inputChanged" /> <input ref="fileInput" type="file" multiple :accept="isIOS ? '' : inputAccept" class="hidden" @change="inputChanged" />
<input ref="fileFolderInput" type="file" webkitdirectory multiple :accept="inputAccept" class="hidden" @change="inputChanged" /> <input ref="fileFolderInput" type="file" webkitdirectory multiple :accept="inputAccept" class="hidden" @change="inputChanged" v-if="!isIOS" />
</div> </div>
</template> </template>
@@ -127,6 +127,10 @@ export default {
}) })
return extensions return extensions
}, },
isIOS() {
const ua = window.navigator.userAgent
return /iPad|iPhone|iPod/.test(ua) && !window.MSStream
},
streamLibraryItem() { streamLibraryItem() {
return this.$store.state.streamLibraryItem return this.$store.state.streamLibraryItem
}, },
-4
View File
@@ -23,10 +23,6 @@ export default class AudioTrack {
get relativeContentUrl() { get relativeContentUrl() {
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
if (process.env.NODE_ENV === 'development') {
return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
}
return this.contentUrl + `?token=${this.userToken}` return this.contentUrl + `?token=${this.userToken}`
} }
} }
+4 -5
View File
@@ -147,7 +147,7 @@ export default class LocalAudioPlayer extends EventEmitter {
timeoutRetry: { timeoutRetry: {
maxNumRetry: 4, maxNumRetry: 4,
retryDelayMs: 0, retryDelayMs: 0,
maxRetryDelayMs: 0, maxRetryDelayMs: 0
}, },
errorRetry: { errorRetry: {
maxNumRetry: 8, maxNumRetry: 8,
@@ -160,7 +160,7 @@ export default class LocalAudioPlayer extends EventEmitter {
} }
return retry return retry
} }
}, }
} }
} }
} }
@@ -194,7 +194,7 @@ export default class LocalAudioPlayer extends EventEmitter {
setDirectPlay() { setDirectPlay() {
// Set initial track and track time offset // Set initial track and track time offset
var trackIndex = this.audioTracks.findIndex(t => this.startTime >= t.startOffset && this.startTime < (t.startOffset + t.duration)) var trackIndex = this.audioTracks.findIndex((t) => this.startTime >= t.startOffset && this.startTime < t.startOffset + t.duration)
this.currentTrackIndex = trackIndex >= 0 ? trackIndex : 0 this.currentTrackIndex = trackIndex >= 0 ? trackIndex : 0
this.loadCurrentTrack() this.loadCurrentTrack()
@@ -270,7 +270,7 @@ export default class LocalAudioPlayer extends EventEmitter {
// Seeking Direct play // Seeking Direct play
if (time < this.currentTrack.startOffset || time > this.currentTrack.startOffset + this.currentTrack.duration) { if (time < this.currentTrack.startOffset || time > this.currentTrack.startOffset + this.currentTrack.duration) {
// Change Track // Change Track
var trackIndex = this.audioTracks.findIndex(t => time >= t.startOffset && time < (t.startOffset + t.duration)) var trackIndex = this.audioTracks.findIndex((t) => time >= t.startOffset && time < t.startOffset + t.duration)
if (trackIndex >= 0) { if (trackIndex >= 0) {
this.startTime = time this.startTime = time
this.currentTrackIndex = trackIndex this.currentTrackIndex = trackIndex
@@ -293,7 +293,6 @@ export default class LocalAudioPlayer extends EventEmitter {
this.player.volume = volume this.player.volume = volume
} }
// Utils // Utils
isValidDuration(duration) { isValidDuration(duration) {
if (duration && !isNaN(duration) && duration !== Number.POSITIVE_INFINITY && duration !== Number.NEGATIVE_INFINITY) { if (duration && !isNaN(duration) && duration !== Number.POSITIVE_INFINITY && duration !== Number.NEGATIVE_INFINITY) {
-2
View File
@@ -297,7 +297,6 @@ export default class PlayerHandler {
if (listeningTimeToAdd > 20) { if (listeningTimeToAdd > 20) {
syncData = { syncData = {
timeListened: listeningTimeToAdd, timeListened: listeningTimeToAdd,
duration: this.getDuration(),
currentTime: this.getCurrentTime() currentTime: this.getCurrentTime()
} }
} }
@@ -317,7 +316,6 @@ export default class PlayerHandler {
const listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync)) const listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
const syncData = { const syncData = {
timeListened: listeningTimeToAdd, timeListened: listeningTimeToAdd,
duration: this.getDuration(),
currentTime currentTime
} }
+2 -3
View File
@@ -1,5 +1,5 @@
export default function ({ $axios, store, $config }) { export default function ({ $axios, store, $config }) {
$axios.onRequest(config => { $axios.onRequest((config) => {
if (!config.url) { if (!config.url) {
console.error('Axios request invalid config', config) console.error('Axios request invalid config', config)
return return
@@ -13,12 +13,11 @@ export default function ({ $axios, store, $config }) {
} }
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
config.url = `/dev${config.url}`
console.log('Making request to ' + config.url) console.log('Making request to ' + config.url)
} }
}) })
$axios.onError(error => { $axios.onError((error) => {
const code = parseInt(error.response && error.response.status) const code = parseInt(error.response && error.response.status)
const message = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error' const message = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
console.error('Axios error', code, message) console.error('Axios error', code, message)
+2 -4
View File
@@ -1,6 +1,6 @@
const SupportedFileTypes = { const SupportedFileTypes = {
image: ['png', 'jpg', 'jpeg', 'webp'], image: ['png', 'jpg', 'jpeg', 'webp'],
audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma', 'mka', 'awb', 'caf'], audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma', 'mka', 'awb', 'caf', 'mpeg', 'mpg'],
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'], ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
info: ['nfo'], info: ['nfo'],
text: ['txt'], text: ['txt'],
@@ -81,9 +81,7 @@ const Hotkeys = {
} }
} }
export { export { Constants }
Constants
}
export default ({ app }, inject) => { export default ({ app }, inject) => {
inject('constants', Constants) inject('constants', Constants)
inject('keynames', KeyNames) inject('keynames', KeyNames)
+3
View File
@@ -7,6 +7,7 @@ const defaultCode = 'en-us'
const languageCodeMap = { const languageCodeMap = {
bg: { label: 'Български', dateFnsLocale: 'bg' }, bg: { label: 'Български', dateFnsLocale: 'bg' },
bn: { label: 'বাংলা', dateFnsLocale: 'bn' }, bn: { label: 'বাংলা', dateFnsLocale: 'bn' },
ca: { label: 'Català', dateFnsLocale: 'ca' },
cs: { label: 'Čeština', dateFnsLocale: 'cs' }, cs: { label: 'Čeština', dateFnsLocale: 'cs' },
da: { label: 'Dansk', dateFnsLocale: 'da' }, da: { label: 'Dansk', dateFnsLocale: 'da' },
de: { label: 'Deutsch', dateFnsLocale: 'de' }, de: { label: 'Deutsch', dateFnsLocale: 'de' },
@@ -41,6 +42,7 @@ Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map((code) =>
// iTunes search API uses ISO 3166 country codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 // iTunes search API uses ISO 3166 country codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
const podcastSearchRegionMap = { const podcastSearchRegionMap = {
au: { label: 'Australia' },
br: { label: 'Brasil' }, br: { label: 'Brasil' },
be: { label: 'België / Belgique / Belgien' }, be: { label: 'België / Belgique / Belgien' },
cz: { label: 'Česko' }, cz: { label: 'Česko' },
@@ -56,6 +58,7 @@ const podcastSearchRegionMap = {
hu: { label: 'Magyarország' }, hu: { label: 'Magyarország' },
nl: { label: 'Nederland' }, nl: { label: 'Nederland' },
no: { label: 'Norge' }, no: { label: 'Norge' },
nz: { label: 'New Zealand' },
at: { label: 'Österreich' }, at: { label: 'Österreich' },
pl: { label: 'Polska' }, pl: { label: 'Polska' },
pt: { label: 'Portugal' }, pt: { label: 'Portugal' },
+7 -17
View File
@@ -72,13 +72,13 @@ export const state = () => ({
} }
], ],
podcastTypes: [ podcastTypes: [
{ text: 'Episodic', value: 'episodic' }, { text: 'Episodic', value: 'episodic', descriptionKey: 'LabelEpisodic' },
{ text: 'Serial', value: 'serial' } { text: 'Serial', value: 'serial', descriptionKey: 'LabelSerial' }
], ],
episodeTypes: [ episodeTypes: [
{ text: 'Full', value: 'full' }, { text: 'Full', value: 'full', descriptionKey: 'LabelFull' },
{ text: 'Trailer', value: 'trailer' }, { text: 'Trailer', value: 'trailer', descriptionKey: 'LabelTrailer' },
{ text: 'Bonus', value: 'bonus' } { text: 'Bonus', value: 'bonus', descriptionKey: 'LabelBonus' }
], ],
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']
}) })
@@ -98,13 +98,7 @@ export const getters = {
const userToken = rootGetters['user/getToken'] const userToken = rootGetters['user/getToken']
const lastUpdate = libraryItem.updatedAt || Date.now() const lastUpdate = libraryItem.updatedAt || Date.now()
const libraryItemId = libraryItem.libraryItemId || libraryItem.id // Workaround for /users/:id page showing media progress covers const libraryItemId = libraryItem.libraryItemId || libraryItem.id // Workaround for /users/:id page showing media progress covers
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?ts=${lastUpdate}${raw ? '&raw=1' : ''}`
if (process.env.NODE_ENV !== 'production') {
// Testing
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}${raw ? '&raw=1' : ''}`
}
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}${raw ? '&raw=1' : ''}`
}, },
getLibraryItemCoverSrcById: getLibraryItemCoverSrcById:
(state, getters, rootState, rootGetters) => (state, getters, rootState, rootGetters) =>
@@ -112,11 +106,7 @@ export const getters = {
const placeholder = `${rootState.routerBasePath}/book_placeholder.jpg` const placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
if (!libraryItemId) return placeholder if (!libraryItemId) return placeholder
const userToken = rootGetters['user/getToken'] const userToken = rootGetters['user/getToken']
if (process.env.NODE_ENV !== 'production') { return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?${raw ? '&raw=1' : ''}${timestamp ? `&ts=${timestamp}` : ''}`
// Testing
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}${timestamp ? `&ts=${timestamp}` : ''}`
}
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}${timestamp ? `&ts=${timestamp}` : ''}`
}, },
getIsBatchSelectingMediaItems: (state) => { getIsBatchSelectingMediaItems: (state) => {
return state.selectedMediaItems.length return state.selectedMediaItems.length
+6 -5
View File
@@ -72,16 +72,17 @@ export const actions = {
return this.$axios return this.$axios
.$patch('/api/settings', updatePayload) .$patch('/api/settings', updatePayload)
.then((result) => { .then((result) => {
if (result.success) { if (result.serverSettings) {
commit('setServerSettings', result.serverSettings) commit('setServerSettings', result.serverSettings)
return true
} else {
return false
} }
return result
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update server settings', error) console.error('Failed to update server settings', error)
return false const errorMsg = error.response?.data || 'Unknown error'
return {
error: errorMsg
}
}) })
}, },
checkForUpdate({ commit }) { checkForUpdate({ commit }) {
+2 -2
View File
@@ -309,9 +309,9 @@ export const mutations = {
} }
// Add publishedDecades // Add publishedDecades
if (mediaMetadata.publishedYear) { if (mediaMetadata.publishedYear && !isNaN(mediaMetadata.publishedYear)) {
const publishedYear = parseInt(mediaMetadata.publishedYear, 10) const publishedYear = parseInt(mediaMetadata.publishedYear, 10)
const decade = Math.floor(publishedYear / 10) * 10 const decade = (Math.floor(publishedYear / 10) * 10).toString()
if (!state.filterData.publishedDecades.includes(decade)) { if (!state.filterData.publishedDecades.includes(decade)) {
state.filterData.publishedDecades.push(decade) state.filterData.publishedDecades.push(decade)
state.filterData.publishedDecades.sort((a, b) => a - b) state.filterData.publishedDecades.sort((a, b) => a - b)
+156
View File
@@ -0,0 +1,156 @@
{
"ButtonAdd": "إضافة",
"ButtonAddChapters": "إضافة الفصول",
"ButtonAddDevice": "إضافة جهاز",
"ButtonAddLibrary": "إضافة مكتبة",
"ButtonAddPodcasts": "إضافة بودكاست",
"ButtonAddUser": "إضافة مستخدم",
"ButtonAddYourFirstLibrary": "أضف مكتبتك الأولى",
"ButtonApply": "حفظ",
"ButtonApplyChapters": "حفظ الفصول",
"ButtonAuthors": "المؤلفون",
"ButtonBack": "الرجوع",
"ButtonBrowseForFolder": "البحث عن المجلد",
"ButtonCancel": "إلغاء",
"ButtonCancelEncode": "إلغاء الترميز",
"ButtonChangeRootPassword": "تغيير كلمة المرور الرئيسية",
"ButtonCheckAndDownloadNewEpisodes": "التحقق من الحلقات الجديدة وتنزيلها",
"ButtonChooseAFolder": "اختر المجلد",
"ButtonChooseFiles": "اختر الملفات",
"ButtonClearFilter": "تصفية الفرز",
"ButtonCloseFeed": "إغلاق",
"ButtonCloseSession": "إغلاق الجلسة المفتوحة",
"ButtonCollections": "المجموعات",
"ButtonConfigureScanner": "إعدادات الماسح الضوئي",
"ButtonCreate": "إنشاء",
"ButtonCreateBackup": "إنشاء نسخة احتياطية",
"ButtonDelete": "حذف",
"ButtonDownloadQueue": "قائمة",
"ButtonEdit": "تعديل",
"ButtonEditChapters": "تعديل الفصول",
"ButtonEditPodcast": "تعديل البودكاست",
"ButtonEnable": "تفعيل",
"ButtonFireAndFail": "النار والفشل",
"ButtonFireOnTest": "حادثة إطلاق النار",
"ButtonForceReScan": "فرض إعادة المسح",
"ButtonFullPath": "المسار الكامل",
"ButtonHide": "إخفاء",
"ButtonHome": "الرئيسية",
"ButtonIssues": "مشاكل",
"ButtonJumpBackward": "اقفز للخلف",
"ButtonJumpForward": "اقفز للأمام",
"ButtonLatest": "أحدث",
"ButtonLibrary": "المكتبة",
"ButtonLogout": "تسجيل الخروج",
"ButtonLookup": "البحث",
"ButtonManageTracks": "إدارة المقاطع",
"ButtonMapChapterTitles": "مطابقة عناوين الفصول",
"ButtonMatchAllAuthors": "مطابقة كل المؤلفون",
"ButtonMatchBooks": "مطابقة الكتب",
"ButtonNevermind": "لا تهتم",
"ButtonNext": "التالي",
"ButtonNextChapter": "الفصل التالي",
"ButtonNextItemInQueue": "العنصر التالي في قائمة الانتظار",
"ButtonOk": "نعم",
"ButtonOpenFeed": "فتح التغذية",
"ButtonOpenManager": "فتح الإدارة",
"ButtonPause": "تَوَقَّف",
"ButtonPlay": "تشغيل",
"ButtonPlayAll": "تشغيل الكل",
"ButtonPlaying": "مشغل الآن",
"ButtonPlaylists": "قوائم التشغيل",
"ButtonPrevious": "سابِق",
"ButtonPreviousChapter": "الفصل السابق",
"ButtonProbeAudioFile": "فحص ملف الصوت",
"ButtonPurgeAllCache": "مسح كافة ذاكرة التخزين المؤقتة",
"ButtonPurgeItemsCache": "مسح ذاكرة التخزين المؤقتة للعناصر",
"ButtonQueueAddItem": "أضف إلى قائمة الانتظار",
"ButtonQueueRemoveItem": "إزالة من قائمة الانتظار",
"ButtonQuickEmbed": "التضمين السريع",
"ButtonQuickEmbedMetadata": "إدراج سريع للبيانات الوصفية",
"ButtonQuickMatch": "مطابقة سريعة",
"ButtonReScan": "إعادة البحث",
"ButtonRead": "اقرأ",
"ButtonReadLess": "قلص",
"ButtonReadMore": "المزيد",
"ButtonRefresh": "تحديث",
"ButtonRemove": "إزالة",
"ButtonRemoveAll": "إزالة الكل",
"ButtonRemoveAllLibraryItems": "إزالة كافة عناصر المكتبة",
"ButtonRemoveFromContinueListening": "إزالة من متابعة الاستماع",
"ButtonRemoveFromContinueReading": "إزالة من متابعة القراءة",
"ButtonRemoveSeriesFromContinueSeries": "إزالة السلسلة من استمرار السلسلة",
"ButtonReset": "إعادة ضبط",
"ButtonResetToDefault": "إعادة ضبط إلى الوضع الافتراضي",
"ButtonRestore": "إستِعادة",
"ButtonSave": "حفظ",
"ButtonSaveAndClose": "حفظ و إغلاق",
"ButtonSaveTracklist": "حفظ قائمة التشغيل",
"ButtonScan": "تَحَقُق",
"ButtonScanLibrary": "تَحَقُق من المكتبة",
"ButtonSearch": "بحث",
"ButtonSelectFolderPath": "حدد مسار المجلد",
"ButtonSeries": "سلسلة",
"ButtonSetChaptersFromTracks": "تعيين الفصول من الملفات",
"ButtonShare": "نشر",
"ButtonShiftTimes": "أوقات العمل",
"ButtonShow": "عرض",
"ButtonStartM4BEncode": "ابدأ ترميز M4B",
"ButtonStartMetadataEmbed": "ابدأ تضمين البيانات الوصفية",
"ButtonStats": "الإحصائيات",
"ButtonSubmit": "تقديم",
"ButtonTest": "اختبار",
"ButtonUnlinkOpenId": "إلغاء ربط المعرف",
"ButtonUpload": "رفع",
"ButtonUploadBackup": "تحميل النسخة الاحتياطية",
"ButtonUploadCover": "ارفق الغلاف",
"ButtonUploadOPMLFile": "رفع ملف OPML",
"ButtonUserDelete": "حذف المستخدم {0}",
"ButtonUserEdit": "تعديل المستخدم {0}",
"ButtonViewAll": "عرض الكل",
"ButtonYes": "نعم",
"ErrorUploadFetchMetadataAPI": "خطأ في جلب البيانات الوصفية",
"ErrorUploadFetchMetadataNoResults": "لم يتم العثور على البيانات الوصفية - حاول تحديث العنوان و/أو المؤلف",
"ErrorUploadLacksTitle": "يجب أن يكون له عنوان",
"HeaderAccount": "الحساب",
"HeaderAddCustomMetadataProvider": "إضافة موفر بيانات تعريفية مخصص",
"HeaderAdvanced": "متقدم",
"HeaderAppriseNotificationSettings": "إعدادات الإشعارات",
"HeaderAudioTracks": "المسارات الصوتية",
"HeaderAudiobookTools": "أدوات إدارة ملفات الكتب الصوتية",
"HeaderAuthentication": "المصادقة",
"HeaderBackups": "النسخ الاحتياطية",
"HeaderChangePassword": "تغيير كلمة المرور",
"HeaderChapters": "الفصول",
"HeaderChooseAFolder": "اختيار المجلد",
"HeaderCollection": "مجموعة",
"HeaderCollectionItems": "عناصر المجموعة",
"HeaderCover": "الغلاف",
"HeaderCurrentDownloads": "التنزيلات الجارية",
"HeaderCustomMessageOnLogin": "رسالة مخصصة عند تسجيل الدخول",
"HeaderCustomMetadataProviders": "مقدمو البيانات الوصفية المخصصة",
"HeaderDetails": "التفاصيل",
"HeaderDownloadQueue": "تنزيل قائمة الانتظار",
"HeaderEbookFiles": "ملفات الكتب الإلكترونية",
"HeaderEmail": "البريد الإلكتروني",
"HeaderEmailSettings": "إعدادات البريد الإلكتروني",
"HeaderEpisodes": "الحلقات",
"HeaderEreaderDevices": "أجهزة قراءة الكتب الإلكترونية",
"HeaderEreaderSettings": "إعدادات القارئ الإلكتروني",
"HeaderFiles": "ملفات",
"HeaderFindChapters": "البحث عن الفصول",
"HeaderIgnoredFiles": "الملفات المتجاهلة",
"HeaderItemFiles": "ملفات العنصر",
"HeaderItemMetadataUtils": "بيانات تعريف العنصر",
"HeaderLastListeningSession": "آخر جلسة استماع",
"HeaderLatestEpisodes": "أحدث الحلقات",
"HeaderLibraries": "المكتبات",
"HeaderLibraryFiles": "ملفات المكتبة",
"HeaderLibraryStats": "إحصائيات المكتبة",
"HeaderListeningSessions": "جلسات الاستماع",
"HeaderListeningStats": "جلسات الاستماع",
"HeaderLogin": "تسجيل الدخول",
"HeaderLogs": "السجلات",
"HeaderManageGenres": "إدارة الانواع",
"HeaderManageTags": "إدارة العلامات"
}
+82
View File
@@ -66,6 +66,7 @@
"ButtonPurgeItemsCache": "আইটেম ক্যাশে পরিষ্কার করুন", "ButtonPurgeItemsCache": "আইটেম ক্যাশে পরিষ্কার করুন",
"ButtonQueueAddItem": "সারিতে যোগ করুন", "ButtonQueueAddItem": "সারিতে যোগ করুন",
"ButtonQueueRemoveItem": "সারি থেকে মুছে ফেলুন", "ButtonQueueRemoveItem": "সারি থেকে মুছে ফেলুন",
"ButtonQuickEmbed": "দ্রুত এম্বেড করুন",
"ButtonQuickEmbedMetadata": "মেটাডেটা দ্রুত এম্বেড করুন", "ButtonQuickEmbedMetadata": "মেটাডেটা দ্রুত এম্বেড করুন",
"ButtonQuickMatch": "দ্রুত ম্যাচ", "ButtonQuickMatch": "দ্রুত ম্যাচ",
"ButtonReScan": "পুনরায় স্ক্যান", "ButtonReScan": "পুনরায় স্ক্যান",
@@ -162,6 +163,7 @@
"HeaderNotificationUpdate": "বিজ্ঞপ্তি আপডেট করুন", "HeaderNotificationUpdate": "বিজ্ঞপ্তি আপডেট করুন",
"HeaderNotifications": "বিজ্ঞপ্তি", "HeaderNotifications": "বিজ্ঞপ্তি",
"HeaderOpenIDConnectAuthentication": "ওপেনআইডি সংযোগ প্রমাণীকরণ", "HeaderOpenIDConnectAuthentication": "ওপেনআইডি সংযোগ প্রমাণীকরণ",
"HeaderOpenListeningSessions": "শোনার সেশন খুলুন",
"HeaderOpenRSSFeed": "আরএসএস ফিড খুলুন", "HeaderOpenRSSFeed": "আরএসএস ফিড খুলুন",
"HeaderOtherFiles": "অন্যান্য ফাইল", "HeaderOtherFiles": "অন্যান্য ফাইল",
"HeaderPasswordAuthentication": "পাসওয়ার্ড প্রমাণীকরণ", "HeaderPasswordAuthentication": "পাসওয়ার্ড প্রমাণীকরণ",
@@ -179,6 +181,7 @@
"HeaderRemoveEpisodes": "{0}টি পর্ব সরান", "HeaderRemoveEpisodes": "{0}টি পর্ব সরান",
"HeaderSavedMediaProgress": "মিডিয়া সংরক্ষণের অগ্রগতি", "HeaderSavedMediaProgress": "মিডিয়া সংরক্ষণের অগ্রগতি",
"HeaderSchedule": "সময়সূচী", "HeaderSchedule": "সময়সূচী",
"HeaderScheduleEpisodeDownloads": "স্বয়ংক্রিয় পর্ব ডাউনলোডের সময়সূচী নির্ধারন করুন",
"HeaderScheduleLibraryScans": "স্বয়ংক্রিয় লাইব্রেরি স্ক্যানের সময়সূচী", "HeaderScheduleLibraryScans": "স্বয়ংক্রিয় লাইব্রেরি স্ক্যানের সময়সূচী",
"HeaderSession": "সেশন", "HeaderSession": "সেশন",
"HeaderSetBackupSchedule": "ব্যাকআপ সময়সূচী সেট করুন", "HeaderSetBackupSchedule": "ব্যাকআপ সময়সূচী সেট করুন",
@@ -224,7 +227,11 @@
"LabelAllUsersExcludingGuests": "অতিথি ব্যতীত সকল ব্যবহারকারী", "LabelAllUsersExcludingGuests": "অতিথি ব্যতীত সকল ব্যবহারকারী",
"LabelAllUsersIncludingGuests": "অতিথি সহ সকল ব্যবহারকারী", "LabelAllUsersIncludingGuests": "অতিথি সহ সকল ব্যবহারকারী",
"LabelAlreadyInYourLibrary": "ইতিমধ্যেই আপনার লাইব্রেরিতে রয়েছে", "LabelAlreadyInYourLibrary": "ইতিমধ্যেই আপনার লাইব্রেরিতে রয়েছে",
"LabelApiToken": "API টোকেন",
"LabelAppend": "সংযোজন", "LabelAppend": "সংযোজন",
"LabelAudioBitrate": "অডিও বিটরেট (যেমন- 128k)",
"LabelAudioChannels": "অডিও চ্যানেল (১ বা ২)",
"LabelAudioCodec": "অডিও কোডেক",
"LabelAuthor": "লেখক", "LabelAuthor": "লেখক",
"LabelAuthorFirstLast": "লেখক (প্রথম শেষ)", "LabelAuthorFirstLast": "লেখক (প্রথম শেষ)",
"LabelAuthorLastFirst": "লেখক (শেষ, প্রথম)", "LabelAuthorLastFirst": "লেখক (শেষ, প্রথম)",
@@ -237,6 +244,7 @@
"LabelAutoRegister": "স্বয়ংক্রিয় নিবন্ধন", "LabelAutoRegister": "স্বয়ংক্রিয় নিবন্ধন",
"LabelAutoRegisterDescription": "লগ ইন করার পর স্বয়ংক্রিয়ভাবে নতুন ব্যবহারকারী তৈরি করুন", "LabelAutoRegisterDescription": "লগ ইন করার পর স্বয়ংক্রিয়ভাবে নতুন ব্যবহারকারী তৈরি করুন",
"LabelBackToUser": "ব্যবহারকারীর কাছে ফিরে যান", "LabelBackToUser": "ব্যবহারকারীর কাছে ফিরে যান",
"LabelBackupAudioFiles": "অডিও ফাইলগুলো ব্যাকআপ",
"LabelBackupLocation": "ব্যাকআপ অবস্থান", "LabelBackupLocation": "ব্যাকআপ অবস্থান",
"LabelBackupsEnableAutomaticBackups": "স্বয়ংক্রিয় ব্যাকআপ সক্ষম করুন", "LabelBackupsEnableAutomaticBackups": "স্বয়ংক্রিয় ব্যাকআপ সক্ষম করুন",
"LabelBackupsEnableAutomaticBackupsHelp": "ব্যাকআপগুলি /মেটাডাটা/ব্যাকআপে সংরক্ষিত", "LabelBackupsEnableAutomaticBackupsHelp": "ব্যাকআপগুলি /মেটাডাটা/ব্যাকআপে সংরক্ষিত",
@@ -245,15 +253,18 @@
"LabelBackupsNumberToKeep": "ব্যাকআপের সংখ্যা রাখুন", "LabelBackupsNumberToKeep": "ব্যাকআপের সংখ্যা রাখুন",
"LabelBackupsNumberToKeepHelp": "এক সময়ে শুধুমাত্র ১ টি ব্যাকআপ সরানো হবে তাই যদি আপনার কাছে ইতিমধ্যে এর চেয়ে বেশি ব্যাকআপ থাকে তাহলে আপনাকে ম্যানুয়ালি সেগুলি সরিয়ে ফেলতে হবে।", "LabelBackupsNumberToKeepHelp": "এক সময়ে শুধুমাত্র ১ টি ব্যাকআপ সরানো হবে তাই যদি আপনার কাছে ইতিমধ্যে এর চেয়ে বেশি ব্যাকআপ থাকে তাহলে আপনাকে ম্যানুয়ালি সেগুলি সরিয়ে ফেলতে হবে।",
"LabelBitrate": "বিটরেট", "LabelBitrate": "বিটরেট",
"LabelBonus": "উপরিলাভ",
"LabelBooks": "বইগুলো", "LabelBooks": "বইগুলো",
"LabelButtonText": "ঘর পাঠ্য", "LabelButtonText": "ঘর পাঠ্য",
"LabelByAuthor": "দ্বারা {0}", "LabelByAuthor": "দ্বারা {0}",
"LabelChangePassword": "পাসওয়ার্ড পরিবর্তন করুন", "LabelChangePassword": "পাসওয়ার্ড পরিবর্তন করুন",
"LabelChannels": "চ্যানেল", "LabelChannels": "চ্যানেল",
"LabelChapterCount": "{0} অধ্যায়",
"LabelChapterTitle": "অধ্যায়ের শিরোনাম", "LabelChapterTitle": "অধ্যায়ের শিরোনাম",
"LabelChapters": "অধ্যায়", "LabelChapters": "অধ্যায়",
"LabelChaptersFound": "অধ্যায় পাওয়া গেছে", "LabelChaptersFound": "অধ্যায় পাওয়া গেছে",
"LabelClickForMoreInfo": "আরো তথ্যের জন্য ক্লিক করুন", "LabelClickForMoreInfo": "আরো তথ্যের জন্য ক্লিক করুন",
"LabelClickToUseCurrentValue": "বর্তমান মান ব্যবহার করতে ক্লিক করুন",
"LabelClosePlayer": "প্লেয়ার বন্ধ করুন", "LabelClosePlayer": "প্লেয়ার বন্ধ করুন",
"LabelCodec": "কোডেক", "LabelCodec": "কোডেক",
"LabelCollapseSeries": "সিরিজ সঙ্কুচিত করুন", "LabelCollapseSeries": "সিরিজ সঙ্কুচিত করুন",
@@ -303,12 +314,25 @@
"LabelEmailSettingsTestAddress": "পরীক্ষার ঠিকানা", "LabelEmailSettingsTestAddress": "পরীক্ষার ঠিকানা",
"LabelEmbeddedCover": "এম্বেডেড কভার", "LabelEmbeddedCover": "এম্বেডেড কভার",
"LabelEnable": "সক্ষম করুন", "LabelEnable": "সক্ষম করুন",
"LabelEncodingBackupLocation": "আপনার আসল অডিও ফাইলগুলোর একটি ব্যাকআপ এখানে সংরক্ষণ করা হবে:",
"LabelEncodingChaptersNotEmbedded": "মাল্টি-ট্র্যাক অডিওবুকগুলোতে অধ্যায় এম্বেড করা হয় না।",
"LabelEncodingClearItemCache": "পর্যায়ক্রমে আইটেম ক্যাশে পরিষ্কার করতে ভুলবেন না।",
"LabelEncodingFinishedM4B": "সমাপ্ত হওয়া M4B-গুলো আপনার অডিওবুক ফোল্ডারে এখানে রাখা হবে:",
"LabelEncodingInfoEmbedded": "আপনার অডিওবুক ফোল্ডারের ভিতরে অডিও ট্র্যাকগুলোতে মেটাডেটা এমবেড করা হবে।",
"LabelEncodingStartedNavigation": "একবার টাস্ক শুরু হলে আপনি এই পৃষ্ঠা থেকে অন্যত্র যেতে পারেন।",
"LabelEncodingTimeWarning": "এনকোডিং ৩০ মিনিট পর্যন্ত সময় নিতে পারে।",
"LabelEncodingWarningAdvancedSettings": "সতর্কতা: এই সেটিংস আপডেট করবেন না, যদি না আপনি ffmpeg এনকোডিং বিকল্পগুলোর সাথে পরিচিত হন।",
"LabelEncodingWatcherDisabled": "আপনার যদি পর্যবেক্ষক অক্ষম থাকে তবে আপনাকে পরে এই অডিওবুকটি পুনরায় স্ক্যান করতে হবে।",
"LabelEnd": "সমাপ্ত", "LabelEnd": "সমাপ্ত",
"LabelEndOfChapter": "অধ্যায়ের সমাপ্তি", "LabelEndOfChapter": "অধ্যায়ের সমাপ্তি",
"LabelEpisode": "পর্ব", "LabelEpisode": "পর্ব",
"LabelEpisodeNotLinkedToRssFeed": "পর্বটি আরএসএস ফিডের সাথে সংযুক্ত করা হয়নি",
"LabelEpisodeNumber": "পর্ব #{0}",
"LabelEpisodeTitle": "পর্বের শিরোনাম", "LabelEpisodeTitle": "পর্বের শিরোনাম",
"LabelEpisodeType": "পর্বের ধরন", "LabelEpisodeType": "পর্বের ধরন",
"LabelEpisodeUrlFromRssFeed": "আরএসএস ফিড থেকে পর্ব URL",
"LabelEpisodes": "পর্বগুলো", "LabelEpisodes": "পর্বগুলো",
"LabelEpisodic": "প্রাসঙ্গিক",
"LabelExample": "উদাহরণ", "LabelExample": "উদাহরণ",
"LabelExpandSeries": "সিরিজ প্রসারিত করুন", "LabelExpandSeries": "সিরিজ প্রসারিত করুন",
"LabelExpandSubSeries": "সাব সিরিজ প্রসারিত করুন", "LabelExpandSubSeries": "সাব সিরিজ প্রসারিত করুন",
@@ -336,6 +360,7 @@
"LabelFontScale": "ফন্ট স্কেল", "LabelFontScale": "ফন্ট স্কেল",
"LabelFontStrikethrough": "অবচ্ছেদন রেখা", "LabelFontStrikethrough": "অবচ্ছেদন রেখা",
"LabelFormat": "ফরম্যাট", "LabelFormat": "ফরম্যাট",
"LabelFull": "পূর্ণ",
"LabelGenre": "ঘরানা", "LabelGenre": "ঘরানা",
"LabelGenres": "ঘরানাগুলো", "LabelGenres": "ঘরানাগুলো",
"LabelHardDeleteFile": "জোরপূর্বক ফাইল মুছে ফেলুন", "LabelHardDeleteFile": "জোরপূর্বক ফাইল মুছে ফেলুন",
@@ -391,6 +416,10 @@
"LabelLowestPriority": "সর্বনিম্ন অগ্রাধিকার", "LabelLowestPriority": "সর্বনিম্ন অগ্রাধিকার",
"LabelMatchExistingUsersBy": "বিদ্যমান ব্যবহারকারীদের দ্বারা মিলিত করুন", "LabelMatchExistingUsersBy": "বিদ্যমান ব্যবহারকারীদের দ্বারা মিলিত করুন",
"LabelMatchExistingUsersByDescription": "বিদ্যমান ব্যবহারকারীদের সংযোগ করার জন্য ব্যবহৃত হয়। একবার সংযুক্ত হলে, ব্যবহারকারীদের আপনার SSO প্রদানকারীর থেকে একটি অনন্য আইডি দ্বারা মিলিত হবে", "LabelMatchExistingUsersByDescription": "বিদ্যমান ব্যবহারকারীদের সংযোগ করার জন্য ব্যবহৃত হয়। একবার সংযুক্ত হলে, ব্যবহারকারীদের আপনার SSO প্রদানকারীর থেকে একটি অনন্য আইডি দ্বারা মিলিত হবে",
"LabelMaxEpisodesToDownload": "সর্বাধিক # টি পর্ব ডাউনলোড করা হবে। অসীমের জন্য 0 ব্যবহার করুন।",
"LabelMaxEpisodesToDownloadPerCheck": "প্রতি কিস্তিতে সর্বাধিক # টি নতুন পর্ব ডাউনলোড করা হবে",
"LabelMaxEpisodesToKeep": "সর্বোচ্চ # টি পর্ব রাখা হবে",
"LabelMaxEpisodesToKeepHelp": "০ কোন সর্বোচ্চ সীমা সেট করে না। একটি নতুন পর্ব স্বয়ংক্রিয়-ডাউনলোড হওয়ার পরে আপনার যদি X-এর বেশি পর্ব থাকে তবে এটি সবচেয়ে পুরানো পর্বটি মুছে ফেলবে। এটি প্রতি নতুন ডাউনলোডের জন্য শুধুমাত্র ১ টি পর্ব মুছে ফেলবে।",
"LabelMediaPlayer": "মিডিয়া প্লেয়ার", "LabelMediaPlayer": "মিডিয়া প্লেয়ার",
"LabelMediaType": "মিডিয়ার ধরন", "LabelMediaType": "মিডিয়ার ধরন",
"LabelMetaTag": "মেটা ট্যাগ", "LabelMetaTag": "মেটা ট্যাগ",
@@ -436,12 +465,14 @@
"LabelOpenIDGroupClaimDescription": "ওপেনআইডি দাবির নাম যাতে ব্যবহারকারীর গোষ্ঠীর একটি তালিকা থাকে। সাধারণত <code>গ্রুপ</code> হিসাবে উল্লেখ করা হয়। <b>কনফিগার করা থাকলে</b>, অ্যাপ্লিকেশনটি স্বয়ংক্রিয়ভাবে এর উপর ভিত্তি করে ব্যবহারকারীর গোষ্ঠীর সদস্যপদ নির্ধারণ করবে, শর্ত এই যে এই গোষ্ঠীগুলি কেস-অসংবেদনশীলভাবে দাবিতে 'অ্যাডমিন', 'ব্যবহারকারী' বা 'অতিথি' নাম দেওয়া হয়৷ দাবিতে একটি তালিকা থাকা উচিত এবং যদি একজন ব্যবহারকারী একাধিক গোষ্ঠীর অন্তর্গত হয় তবে অ্যাপ্লিকেশনটি বরাদ্দ করবে সর্বোচ্চ স্তরের অ্যাক্সেসের সাথে সঙ্গতিপূর্ণ ভূমিকা৷ যদি কোনও গোষ্ঠীর সাথে মেলে না, তবে অ্যাক্সেস অস্বীকার করা হবে।", "LabelOpenIDGroupClaimDescription": "ওপেনআইডি দাবির নাম যাতে ব্যবহারকারীর গোষ্ঠীর একটি তালিকা থাকে। সাধারণত <code>গ্রুপ</code> হিসাবে উল্লেখ করা হয়। <b>কনফিগার করা থাকলে</b>, অ্যাপ্লিকেশনটি স্বয়ংক্রিয়ভাবে এর উপর ভিত্তি করে ব্যবহারকারীর গোষ্ঠীর সদস্যপদ নির্ধারণ করবে, শর্ত এই যে এই গোষ্ঠীগুলি কেস-অসংবেদনশীলভাবে দাবিতে 'অ্যাডমিন', 'ব্যবহারকারী' বা 'অতিথি' নাম দেওয়া হয়৷ দাবিতে একটি তালিকা থাকা উচিত এবং যদি একজন ব্যবহারকারী একাধিক গোষ্ঠীর অন্তর্গত হয় তবে অ্যাপ্লিকেশনটি বরাদ্দ করবে সর্বোচ্চ স্তরের অ্যাক্সেসের সাথে সঙ্গতিপূর্ণ ভূমিকা৷ যদি কোনও গোষ্ঠীর সাথে মেলে না, তবে অ্যাক্সেস অস্বীকার করা হবে।",
"LabelOpenRSSFeed": "আরএসএস ফিড খুলুন", "LabelOpenRSSFeed": "আরএসএস ফিড খুলুন",
"LabelOverwrite": "পুনঃলিখিত", "LabelOverwrite": "পুনঃলিখিত",
"LabelPaginationPageXOfY": "{1} টির মধ্যে {0} পৃষ্ঠা",
"LabelPassword": "পাসওয়ার্ড", "LabelPassword": "পাসওয়ার্ড",
"LabelPath": "পথ", "LabelPath": "পথ",
"LabelPermanent": "স্থায়ী", "LabelPermanent": "স্থায়ী",
"LabelPermissionsAccessAllLibraries": "সমস্ত লাইব্রেরি অ্যাক্সেস করতে পারবে", "LabelPermissionsAccessAllLibraries": "সমস্ত লাইব্রেরি অ্যাক্সেস করতে পারবে",
"LabelPermissionsAccessAllTags": "সমস্ত ট্যাগ অ্যাক্সেস করতে পারবে", "LabelPermissionsAccessAllTags": "সমস্ত ট্যাগ অ্যাক্সেস করতে পারবে",
"LabelPermissionsAccessExplicitContent": "স্পষ্ট বিষয়বস্তু অ্যাক্সেস করতে পারে", "LabelPermissionsAccessExplicitContent": "স্পষ্ট বিষয়বস্তু অ্যাক্সেস করতে পারে",
"LabelPermissionsCreateEreader": "ইরিডার তৈরি করতে পারেন",
"LabelPermissionsDelete": "মুছে দিতে পারবে", "LabelPermissionsDelete": "মুছে দিতে পারবে",
"LabelPermissionsDownload": "ডাউনলোড করতে পারবে", "LabelPermissionsDownload": "ডাউনলোড করতে পারবে",
"LabelPermissionsUpdate": "আপডেট করতে পারবে", "LabelPermissionsUpdate": "আপডেট করতে পারবে",
@@ -465,6 +496,8 @@
"LabelPubDate": "প্রকাশের তারিখ", "LabelPubDate": "প্রকাশের তারিখ",
"LabelPublishYear": "প্রকাশের বছর", "LabelPublishYear": "প্রকাশের বছর",
"LabelPublishedDate": "প্রকাশিত {0}", "LabelPublishedDate": "প্রকাশিত {0}",
"LabelPublishedDecade": "প্রকাশনার দশক",
"LabelPublishedDecades": "প্রকাশনার দশকগুলো",
"LabelPublisher": "প্রকাশক", "LabelPublisher": "প্রকাশক",
"LabelPublishers": "প্রকাশকরা", "LabelPublishers": "প্রকাশকরা",
"LabelRSSFeedCustomOwnerEmail": "কাস্টম মালিকের ইমেইল", "LabelRSSFeedCustomOwnerEmail": "কাস্টম মালিকের ইমেইল",
@@ -484,21 +517,28 @@
"LabelRedo": "পুনরায় করুন", "LabelRedo": "পুনরায় করুন",
"LabelRegion": "অঞ্চল", "LabelRegion": "অঞ্চল",
"LabelReleaseDate": "উন্মোচনের তারিখ", "LabelReleaseDate": "উন্মোচনের তারিখ",
"LabelRemoveAllMetadataAbs": "সমস্ত metadata.abs ফাইল সরান",
"LabelRemoveAllMetadataJson": "সমস্ত metadata.json ফাইল সরান",
"LabelRemoveCover": "কভার সরান", "LabelRemoveCover": "কভার সরান",
"LabelRemoveMetadataFile": "লাইব্রেরি আইটেম ফোল্ডারে মেটাডেটা ফাইল সরান",
"LabelRemoveMetadataFileHelp": "আপনার {0} ফোল্ডারের সমস্ত metadata.json এবং metadata.abs ফাইলগুলি সরান।",
"LabelRowsPerPage": "প্রতি পৃষ্ঠায় সারি", "LabelRowsPerPage": "প্রতি পৃষ্ঠায় সারি",
"LabelSearchTerm": "অনুসন্ধান শব্দ", "LabelSearchTerm": "অনুসন্ধান শব্দ",
"LabelSearchTitle": "অনুসন্ধান শিরোনাম", "LabelSearchTitle": "অনুসন্ধান শিরোনাম",
"LabelSearchTitleOrASIN": "অনুসন্ধান শিরোনাম বা ASIN", "LabelSearchTitleOrASIN": "অনুসন্ধান শিরোনাম বা ASIN",
"LabelSeason": "সেশন", "LabelSeason": "সেশন",
"LabelSeasonNumber": "মরসুম #{0}",
"LabelSelectAll": "সব নির্বাচন করুন", "LabelSelectAll": "সব নির্বাচন করুন",
"LabelSelectAllEpisodes": "সমস্ত পর্ব নির্বাচন করুন", "LabelSelectAllEpisodes": "সমস্ত পর্ব নির্বাচন করুন",
"LabelSelectEpisodesShowing": "দেখানো {0}টি পর্ব নির্বাচন করুন", "LabelSelectEpisodesShowing": "দেখানো {0}টি পর্ব নির্বাচন করুন",
"LabelSelectUsers": "ব্যবহারকারী নির্বাচন করুন", "LabelSelectUsers": "ব্যবহারকারী নির্বাচন করুন",
"LabelSendEbookToDevice": "ই-বই পাঠান...", "LabelSendEbookToDevice": "ই-বই পাঠান...",
"LabelSequence": "ক্রম", "LabelSequence": "ক্রম",
"LabelSerial": "ধারাবাহিক",
"LabelSeries": "সিরিজ", "LabelSeries": "সিরিজ",
"LabelSeriesName": "সিরিজের নাম", "LabelSeriesName": "সিরিজের নাম",
"LabelSeriesProgress": "সিরিজের অগ্রগতি", "LabelSeriesProgress": "সিরিজের অগ্রগতি",
"LabelServerLogLevel": "সার্ভার লগ লেভেল",
"LabelServerYearReview": "সার্ভারের বাৎসরিক পর্যালোচনা ({0})", "LabelServerYearReview": "সার্ভারের বাৎসরিক পর্যালোচনা ({0})",
"LabelSetEbookAsPrimary": "প্রাথমিক হিসাবে সেট করুন", "LabelSetEbookAsPrimary": "প্রাথমিক হিসাবে সেট করুন",
"LabelSetEbookAsSupplementary": "পরিপূরক হিসেবে সেট করুন", "LabelSetEbookAsSupplementary": "পরিপূরক হিসেবে সেট করুন",
@@ -523,6 +563,9 @@
"LabelSettingsHideSingleBookSeriesHelp": "যে সিরিজগুলোতে একটি বই আছে সেগুলো সিরিজের পাতা এবং নীড় পেজের তাক থেকে লুকিয়ে রাখা হবে।", "LabelSettingsHideSingleBookSeriesHelp": "যে সিরিজগুলোতে একটি বই আছে সেগুলো সিরিজের পাতা এবং নীড় পেজের তাক থেকে লুকিয়ে রাখা হবে।",
"LabelSettingsHomePageBookshelfView": "নীড় পেজে বুকশেলফ ভিউ ব্যবহার করুন", "LabelSettingsHomePageBookshelfView": "নীড় পেজে বুকশেলফ ভিউ ব্যবহার করুন",
"LabelSettingsLibraryBookshelfView": "লাইব্রেরি বুকশেলফ ভিউ ব্যবহার করুন", "LabelSettingsLibraryBookshelfView": "লাইব্রেরি বুকশেলফ ভিউ ব্যবহার করুন",
"LabelSettingsLibraryMarkAsFinishedPercentComplete": "শতকরা সম্পূর্ণ এর চেয়ে বেশি",
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "বাকি সময় (সেকেন্ড) এর চেয়ে কম",
"LabelSettingsLibraryMarkAsFinishedWhen": "মিডিয়া আইটেমকে সমাপ্ত হিসাবে চিহ্নিত করুন যখন",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "কন্টিনিউ সিরিজে আগের বইগুলো এড়িয়ে যান", "LabelSettingsOnlyShowLaterBooksInContinueSeries": "কন্টিনিউ সিরিজে আগের বইগুলো এড়িয়ে যান",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "কন্টিনিউ সিরিজের নীড় পেজ শেল্ফ দেখায় যে সিরিজে শুরু হয়নি এমন প্রথম বই যার অন্তত একটি বই শেষ হয়েছে এবং কোনো বই চলছে না। এই সেটিংটি সক্ষম করলে শুরু না হওয়া প্রথম বইটির পরিবর্তে সবচেয়ে দূরের সম্পূর্ণ বই থেকে সিরিজ চলতে থাকবে।", "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "কন্টিনিউ সিরিজের নীড় পেজ শেল্ফ দেখায় যে সিরিজে শুরু হয়নি এমন প্রথম বই যার অন্তত একটি বই শেষ হয়েছে এবং কোনো বই চলছে না। এই সেটিংটি সক্ষম করলে শুরু না হওয়া প্রথম বইটির পরিবর্তে সবচেয়ে দূরের সম্পূর্ণ বই থেকে সিরিজ চলতে থাকবে।",
"LabelSettingsParseSubtitles": "সাবটাইটেল পার্স করুন", "LabelSettingsParseSubtitles": "সাবটাইটেল পার্স করুন",
@@ -587,6 +630,7 @@
"LabelTimeDurationXMinutes": "{0} মিনিট", "LabelTimeDurationXMinutes": "{0} মিনিট",
"LabelTimeDurationXSeconds": "{0} সেকেন্ড", "LabelTimeDurationXSeconds": "{0} সেকেন্ড",
"LabelTimeInMinutes": "মিনিটে সময়", "LabelTimeInMinutes": "মিনিটে সময়",
"LabelTimeLeft": "{0} বাকি",
"LabelTimeListened": "সময় শোনা হয়েছে", "LabelTimeListened": "সময় শোনা হয়েছে",
"LabelTimeListenedToday": "আজ শোনার সময়", "LabelTimeListenedToday": "আজ শোনার সময়",
"LabelTimeRemaining": "{0}টি অবশিষ্ট", "LabelTimeRemaining": "{0}টি অবশিষ্ট",
@@ -594,6 +638,7 @@
"LabelTitle": "শিরোনাম", "LabelTitle": "শিরোনাম",
"LabelToolsEmbedMetadata": "মেটাডেটা এম্বেড করুন", "LabelToolsEmbedMetadata": "মেটাডেটা এম্বেড করুন",
"LabelToolsEmbedMetadataDescription": "কভার ইমেজ এবং অধ্যায় সহ অডিও ফাইলগুলিতে মেটাডেটা এম্বেড করুন।", "LabelToolsEmbedMetadataDescription": "কভার ইমেজ এবং অধ্যায় সহ অডিও ফাইলগুলিতে মেটাডেটা এম্বেড করুন।",
"LabelToolsM4bEncoder": "M4B এনকোডার",
"LabelToolsMakeM4b": "M4B অডিওবুক ফাইল তৈরি করুন", "LabelToolsMakeM4b": "M4B অডিওবুক ফাইল তৈরি করুন",
"LabelToolsMakeM4bDescription": "এমবেডেড মেটাডেটা, কভার ইমেজ এবং অধ্যায় সহ একটি .M4B অডিওবুক ফাইল তৈরি করুন।", "LabelToolsMakeM4bDescription": "এমবেডেড মেটাডেটা, কভার ইমেজ এবং অধ্যায় সহ একটি .M4B অডিওবুক ফাইল তৈরি করুন।",
"LabelToolsSplitM4b": "M4B কে MP3 তে বিভক্ত করুন", "LabelToolsSplitM4b": "M4B কে MP3 তে বিভক্ত করুন",
@@ -606,6 +651,7 @@
"LabelTracksMultiTrack": "মাল্টি-ট্র্যাক", "LabelTracksMultiTrack": "মাল্টি-ট্র্যাক",
"LabelTracksNone": "কোন ট্র্যাক নেই", "LabelTracksNone": "কোন ট্র্যাক নেই",
"LabelTracksSingleTrack": "একক-ট্র্যাক", "LabelTracksSingleTrack": "একক-ট্র্যাক",
"LabelTrailer": "আনুগমিক",
"LabelType": "টাইপ", "LabelType": "টাইপ",
"LabelUnabridged": "অসংলগ্ন", "LabelUnabridged": "অসংলগ্ন",
"LabelUndo": "পূর্বাবস্থা", "LabelUndo": "পূর্বাবস্থা",
@@ -617,10 +663,13 @@
"LabelUpdateDetailsHelp": "একটি মিল থাকা অবস্থায় নির্বাচিত বইগুলির বিদ্যমান বিবরণ ওভাররাইট করার অনুমতি দিন", "LabelUpdateDetailsHelp": "একটি মিল থাকা অবস্থায় নির্বাচিত বইগুলির বিদ্যমান বিবরণ ওভাররাইট করার অনুমতি দিন",
"LabelUpdatedAt": "আপডেট করা হয়েছে", "LabelUpdatedAt": "আপডেট করা হয়েছে",
"LabelUploaderDragAndDrop": "ফাইল বা ফোল্ডার টেনে আনুন এবং ফেলে দিন", "LabelUploaderDragAndDrop": "ফাইল বা ফোল্ডার টেনে আনুন এবং ফেলে দিন",
"LabelUploaderDragAndDropFilesOnly": "ফাইল টেনে আনুন",
"LabelUploaderDropFiles": "ফাইলগুলো ফেলে দিন", "LabelUploaderDropFiles": "ফাইলগুলো ফেলে দিন",
"LabelUploaderItemFetchMetadataHelp": "স্বয়ংক্রিয়ভাবে শিরোনাম, লেখক এবং সিরিজ আনুন", "LabelUploaderItemFetchMetadataHelp": "স্বয়ংক্রিয়ভাবে শিরোনাম, লেখক এবং সিরিজ আনুন",
"LabelUseAdvancedOptions": "উন্নত বিকল্প ব্যবহার করুন",
"LabelUseChapterTrack": "অধ্যায় ট্র্যাক ব্যবহার করুন", "LabelUseChapterTrack": "অধ্যায় ট্র্যাক ব্যবহার করুন",
"LabelUseFullTrack": "সম্পূর্ণ ট্র্যাক ব্যবহার করুন", "LabelUseFullTrack": "সম্পূর্ণ ট্র্যাক ব্যবহার করুন",
"LabelUseZeroForUnlimited": "অসীমের জন্য 0 ব্যবহার করুন",
"LabelUser": "ব্যবহারকারী", "LabelUser": "ব্যবহারকারী",
"LabelUsername": "ব্যবহারকারীর নাম", "LabelUsername": "ব্যবহারকারীর নাম",
"LabelValue": "মান", "LabelValue": "মান",
@@ -667,6 +716,7 @@
"MessageConfirmDeleteMetadataProvider": "আপনি কি নিশ্চিতভাবে কাস্টম মেটাডেটা প্রদানকারী \"{0}\" মুছতে চান?", "MessageConfirmDeleteMetadataProvider": "আপনি কি নিশ্চিতভাবে কাস্টম মেটাডেটা প্রদানকারী \"{0}\" মুছতে চান?",
"MessageConfirmDeleteNotification": "আপনি কি নিশ্চিতভাবে এই বিজ্ঞপ্তিটি মুছতে চান?", "MessageConfirmDeleteNotification": "আপনি কি নিশ্চিতভাবে এই বিজ্ঞপ্তিটি মুছতে চান?",
"MessageConfirmDeleteSession": "আপনি কি নিশ্চিত আপনি এই অধিবেশন মুছে দিতে চান?", "MessageConfirmDeleteSession": "আপনি কি নিশ্চিত আপনি এই অধিবেশন মুছে দিতে চান?",
"MessageConfirmEmbedMetadataInAudioFiles": "আপনি কি {0}টি অডিও ফাইলে মেটাডেটা এম্বেড করার বিষয়ে নিশ্চিত?",
"MessageConfirmForceReScan": "আপনি কি নিশ্চিত যে আপনি জোর করে পুনরায় স্ক্যান করতে চান?", "MessageConfirmForceReScan": "আপনি কি নিশ্চিত যে আপনি জোর করে পুনরায় স্ক্যান করতে চান?",
"MessageConfirmMarkAllEpisodesFinished": "আপনি কি নিশ্চিত যে আপনি সমস্ত পর্ব সমাপ্ত হিসাবে চিহ্নিত করতে চান?", "MessageConfirmMarkAllEpisodesFinished": "আপনি কি নিশ্চিত যে আপনি সমস্ত পর্ব সমাপ্ত হিসাবে চিহ্নিত করতে চান?",
"MessageConfirmMarkAllEpisodesNotFinished": "আপনি কি নিশ্চিত যে আপনি সমস্ত পর্বকে শেষ হয়নি বলে চিহ্নিত করতে চান?", "MessageConfirmMarkAllEpisodesNotFinished": "আপনি কি নিশ্চিত যে আপনি সমস্ত পর্বকে শেষ হয়নি বলে চিহ্নিত করতে চান?",
@@ -678,6 +728,7 @@
"MessageConfirmPurgeCache": "ক্যাশে পরিষ্কারক <code>/metadata/cache</code>-এ সম্পূর্ণ ডিরেক্টরি মুছে ফেলবে। <br /><br />আপনি কি নিশ্চিত আপনি ক্যাশে ডিরেক্টরি সরাতে চান?", "MessageConfirmPurgeCache": "ক্যাশে পরিষ্কারক <code>/metadata/cache</code>-এ সম্পূর্ণ ডিরেক্টরি মুছে ফেলবে। <br /><br />আপনি কি নিশ্চিত আপনি ক্যাশে ডিরেক্টরি সরাতে চান?",
"MessageConfirmPurgeItemsCache": "আইটেম ক্যাশে পরিষ্কারক <code>/metadata/cache/items</code>-এ সম্পূর্ণ ডিরেক্টরি মুছে ফেলবে।<br />আপনি কি নিশ্চিত?", "MessageConfirmPurgeItemsCache": "আইটেম ক্যাশে পরিষ্কারক <code>/metadata/cache/items</code>-এ সম্পূর্ণ ডিরেক্টরি মুছে ফেলবে।<br />আপনি কি নিশ্চিত?",
"MessageConfirmQuickEmbed": "সতর্কতা! দ্রুত এম্বেড আপনার অডিও ফাইলের ব্যাকআপ করবে না। নিশ্চিত করুন যে আপনার অডিও ফাইলগুলির একটি ব্যাকআপ আছে। <br><br>আপনি কি চালিয়ে যেতে চান?", "MessageConfirmQuickEmbed": "সতর্কতা! দ্রুত এম্বেড আপনার অডিও ফাইলের ব্যাকআপ করবে না। নিশ্চিত করুন যে আপনার অডিও ফাইলগুলির একটি ব্যাকআপ আছে। <br><br>আপনি কি চালিয়ে যেতে চান?",
"MessageConfirmQuickMatchEpisodes": "একটি মিল পাওয়া গেলে দ্রুত ম্যাচিং পর্বগুলি বিস্তারিত ওভাররাইট করবে। শুধুমাত্র অতুলনীয় পর্ব আপডেট করা হবে। আপনি কি নিশ্চিত?",
"MessageConfirmReScanLibraryItems": "আপনি কি নিশ্চিত যে আপনি {0}টি আইটেম পুনরায় স্ক্যান করতে চান?", "MessageConfirmReScanLibraryItems": "আপনি কি নিশ্চিত যে আপনি {0}টি আইটেম পুনরায় স্ক্যান করতে চান?",
"MessageConfirmRemoveAllChapters": "আপনি কি নিশ্চিত যে আপনি সমস্ত অধ্যায় সরাতে চান?", "MessageConfirmRemoveAllChapters": "আপনি কি নিশ্চিত যে আপনি সমস্ত অধ্যায় সরাতে চান?",
"MessageConfirmRemoveAuthor": "আপনি কি নিশ্চিত যে আপনি লেখক \"{0}\" অপসারণ করতে চান?", "MessageConfirmRemoveAuthor": "আপনি কি নিশ্চিত যে আপনি লেখক \"{0}\" অপসারণ করতে চান?",
@@ -685,6 +736,7 @@
"MessageConfirmRemoveEpisode": "আপনি কি নিশ্চিত আপনি \"{0}\" পর্বটি সরাতে চান?", "MessageConfirmRemoveEpisode": "আপনি কি নিশ্চিত আপনি \"{0}\" পর্বটি সরাতে চান?",
"MessageConfirmRemoveEpisodes": "আপনি কি নিশ্চিত যে আপনি {0}টি পর্ব সরাতে চান?", "MessageConfirmRemoveEpisodes": "আপনি কি নিশ্চিত যে আপনি {0}টি পর্ব সরাতে চান?",
"MessageConfirmRemoveListeningSessions": "আপনি কি নিশ্চিত যে আপনি {0}টি শোনার সেশন সরাতে চান?", "MessageConfirmRemoveListeningSessions": "আপনি কি নিশ্চিত যে আপনি {0}টি শোনার সেশন সরাতে চান?",
"MessageConfirmRemoveMetadataFiles": "আপনি কি আপনার লাইব্রেরি আইটেম ফোল্ডারে থাকা সমস্ত মেটাডেটা {0} ফাইল মুছে ফেলার বিষয়ে নিশ্চিত?",
"MessageConfirmRemoveNarrator": "আপনি কি \"{0}\" বর্ণনাকারীকে সরানোর বিষয়ে নিশ্চিত?", "MessageConfirmRemoveNarrator": "আপনি কি \"{0}\" বর্ণনাকারীকে সরানোর বিষয়ে নিশ্চিত?",
"MessageConfirmRemovePlaylist": "আপনি কি নিশ্চিত যে আপনি আপনার প্লেলিস্ট \"{0}\" সরাতে চান?", "MessageConfirmRemovePlaylist": "আপনি কি নিশ্চিত যে আপনি আপনার প্লেলিস্ট \"{0}\" সরাতে চান?",
"MessageConfirmRenameGenre": "আপনি কি নিশ্চিত যে আপনি সমস্ত আইটেমের জন্য \"{0}\" ধারার নাম পরিবর্তন করে \"{1}\" করতে চান?", "MessageConfirmRenameGenre": "আপনি কি নিশ্চিত যে আপনি সমস্ত আইটেমের জন্য \"{0}\" ধারার নাম পরিবর্তন করে \"{1}\" করতে চান?",
@@ -700,6 +752,7 @@
"MessageDragFilesIntoTrackOrder": "সঠিক ট্র্যাক অর্ডারে ফাইল টেনে আনুন", "MessageDragFilesIntoTrackOrder": "সঠিক ট্র্যাক অর্ডারে ফাইল টেনে আনুন",
"MessageEmbedFailed": "এম্বেড ব্যর্থ হয়েছে!", "MessageEmbedFailed": "এম্বেড ব্যর্থ হয়েছে!",
"MessageEmbedFinished": "এম্বেড করা শেষ!", "MessageEmbedFinished": "এম্বেড করা শেষ!",
"MessageEmbedQueue": "মেটাডেটা এম্বেডের জন্য সারিবদ্ধ ({0} সারিতে)",
"MessageEpisodesQueuedForDownload": "{0} পর্ব(গুলি) ডাউনলোডের জন্য সারিবদ্ধ", "MessageEpisodesQueuedForDownload": "{0} পর্ব(গুলি) ডাউনলোডের জন্য সারিবদ্ধ",
"MessageEreaderDevices": "ই-বুক সরবরাহ নিশ্চিত করতে, আপনাকে নীচে তালিকাভুক্ত প্রতিটি ডিভাইসের জন্য একটি বৈধ প্রেরক হিসাবে উপরের ইমেল ঠিকানাটি যুক্ত করতে হতে পারে।", "MessageEreaderDevices": "ই-বুক সরবরাহ নিশ্চিত করতে, আপনাকে নীচে তালিকাভুক্ত প্রতিটি ডিভাইসের জন্য একটি বৈধ প্রেরক হিসাবে উপরের ইমেল ঠিকানাটি যুক্ত করতে হতে পারে।",
"MessageFeedURLWillBe": "ফিড URL হবে {0}", "MessageFeedURLWillBe": "ফিড URL হবে {0}",
@@ -744,6 +797,7 @@
"MessageNoLogs": "কোনও লগ নেই", "MessageNoLogs": "কোনও লগ নেই",
"MessageNoMediaProgress": "মিডিয়া অগ্রগতি নেই", "MessageNoMediaProgress": "মিডিয়া অগ্রগতি নেই",
"MessageNoNotifications": "কোনো বিজ্ঞপ্তি নেই", "MessageNoNotifications": "কোনো বিজ্ঞপ্তি নেই",
"MessageNoPodcastFeed": "অবৈধ পডকাস্ট: কোনো ফিড নেই",
"MessageNoPodcastsFound": "কোন পডকাস্ট পাওয়া যায়নি", "MessageNoPodcastsFound": "কোন পডকাস্ট পাওয়া যায়নি",
"MessageNoResults": "কোন ফলাফল নেই", "MessageNoResults": "কোন ফলাফল নেই",
"MessageNoSearchResultsFor": "\"{0}\" এর জন্য কোন অনুসন্ধান ফলাফল নেই", "MessageNoSearchResultsFor": "\"{0}\" এর জন্য কোন অনুসন্ধান ফলাফল নেই",
@@ -760,6 +814,10 @@
"MessagePlaylistCreateFromCollection": "সংগ্রহ থেকে প্লেলিস্ট তৈরি করুন", "MessagePlaylistCreateFromCollection": "সংগ্রহ থেকে প্লেলিস্ট তৈরি করুন",
"MessagePleaseWait": "অনুগ্রহ করে অপেক্ষা করুন..।", "MessagePleaseWait": "অনুগ্রহ করে অপেক্ষা করুন..।",
"MessagePodcastHasNoRSSFeedForMatching": "পডকাস্টের সাথে মিলের জন্য ব্যবহার করার জন্য কোন RSS ফিড ইউআরএল নেই", "MessagePodcastHasNoRSSFeedForMatching": "পডকাস্টের সাথে মিলের জন্য ব্যবহার করার জন্য কোন RSS ফিড ইউআরএল নেই",
"MessagePodcastSearchField": "অনুসন্ধান শব্দ বা RSS ফিড URL লিখুন",
"MessageQuickEmbedInProgress": "দ্রুত এম্বেড করা হচ্ছে",
"MessageQuickEmbedQueue": "দ্রুত এম্বেড করার জন্য সারিবদ্ধ ({0} সারিতে)",
"MessageQuickMatchAllEpisodes": "দ্রুত ম্যাচ সব পর্ব",
"MessageQuickMatchDescription": "খালি আইটেমের বিশদ বিবরণ এবং '{0}' থেকে প্রথম ম্যাচের ফলাফলের সাথে কভার করুন। সার্ভার সেটিং সক্ষম না থাকলে বিশদ ওভাররাইট করে না।", "MessageQuickMatchDescription": "খালি আইটেমের বিশদ বিবরণ এবং '{0}' থেকে প্রথম ম্যাচের ফলাফলের সাথে কভার করুন। সার্ভার সেটিং সক্ষম না থাকলে বিশদ ওভাররাইট করে না।",
"MessageRemoveChapter": "অধ্যায় সরান", "MessageRemoveChapter": "অধ্যায় সরান",
"MessageRemoveEpisodes": "{0}টি পর্ব(গুলি) সরান", "MessageRemoveEpisodes": "{0}টি পর্ব(গুলি) সরান",
@@ -802,6 +860,9 @@
"MessageTaskOpmlImportFeedPodcastExists": "পডকাস্ট আগে থেকেই পাথে বিদ্যমান", "MessageTaskOpmlImportFeedPodcastExists": "পডকাস্ট আগে থেকেই পাথে বিদ্যমান",
"MessageTaskOpmlImportFeedPodcastFailed": "পডকাস্ট তৈরি করতে ব্যর্থ", "MessageTaskOpmlImportFeedPodcastFailed": "পডকাস্ট তৈরি করতে ব্যর্থ",
"MessageTaskOpmlImportFinished": "{0}টি পডকাস্ট যোগ করা হয়েছে", "MessageTaskOpmlImportFinished": "{0}টি পডকাস্ট যোগ করা হয়েছে",
"MessageTaskOpmlParseFailed": "OPML ফাইল পার্স করতে ব্যর্থ হয়েছে",
"MessageTaskOpmlParseFastFail": "অবৈধ OPML ফাইল <opml> ট্যাগ পাওয়া যায়নি বা একটি <outline> ট্যাগ পাওয়া যায়নি",
"MessageTaskOpmlParseNoneFound": "OPML ফাইলে কোনো ফিড পাওয়া যায়নি",
"MessageTaskScanItemsAdded": "{0}টি করা হয়েছে", "MessageTaskScanItemsAdded": "{0}টি করা হয়েছে",
"MessageTaskScanItemsMissing": "{0}টি অনুপস্থিত", "MessageTaskScanItemsMissing": "{0}টি অনুপস্থিত",
"MessageTaskScanItemsUpdated": "{0} টি আপডেট করা হয়েছে", "MessageTaskScanItemsUpdated": "{0} টি আপডেট করা হয়েছে",
@@ -826,6 +887,10 @@
"NoteUploaderFoldersWithMediaFiles": "মিডিয়া ফাইল সহ ফোল্ডারগুলি আলাদা লাইব্রেরি আইটেম হিসাবে পরিচালনা করা হবে।", "NoteUploaderFoldersWithMediaFiles": "মিডিয়া ফাইল সহ ফোল্ডারগুলি আলাদা লাইব্রেরি আইটেম হিসাবে পরিচালনা করা হবে।",
"NoteUploaderOnlyAudioFiles": "যদি শুধুমাত্র অডিও ফাইল আপলোড করা হয় তবে প্রতিটি অডিও ফাইল একটি পৃথক অডিওবুক হিসাবে পরিচালনা করা হবে।", "NoteUploaderOnlyAudioFiles": "যদি শুধুমাত্র অডিও ফাইল আপলোড করা হয় তবে প্রতিটি অডিও ফাইল একটি পৃথক অডিওবুক হিসাবে পরিচালনা করা হবে।",
"NoteUploaderUnsupportedFiles": "অসমর্থিত ফাইলগুলি উপেক্ষা করা হয়। একটি ফোল্ডার বেছে নেওয়া বা ফেলে দেওয়ার সময়, আইটেম ফোল্ডারে নেই এমন অন্যান্য ফাইলগুলি উপেক্ষা করা হয়।", "NoteUploaderUnsupportedFiles": "অসমর্থিত ফাইলগুলি উপেক্ষা করা হয়। একটি ফোল্ডার বেছে নেওয়া বা ফেলে দেওয়ার সময়, আইটেম ফোল্ডারে নেই এমন অন্যান্য ফাইলগুলি উপেক্ষা করা হয়।",
"NotificationOnBackupCompletedDescription": "ব্যাকআপ সম্পূর্ণ হলে ট্রিগার হবে",
"NotificationOnBackupFailedDescription": "ব্যাকআপ ব্যর্থ হলে ট্রিগার হবে",
"NotificationOnEpisodeDownloadedDescription": "একটি পডকাস্ট পর্ব স্বয়ংক্রিয়ভাবে ডাউনলোড হলে ট্রিগার হবে",
"NotificationOnTestDescription": "বিজ্ঞপ্তি সিস্টেম পরীক্ষার জন্য ইভেন্ট",
"PlaceholderNewCollection": "নতুন সংগ্রহের নাম", "PlaceholderNewCollection": "নতুন সংগ্রহের নাম",
"PlaceholderNewFolderPath": "নতুন ফোল্ডার পথ", "PlaceholderNewFolderPath": "নতুন ফোল্ডার পথ",
"PlaceholderNewPlaylist": "নতুন প্লেলিস্টের নাম", "PlaceholderNewPlaylist": "নতুন প্লেলিস্টের নাম",
@@ -851,6 +916,7 @@
"StatsYearInReview": "বাৎসরিক পর্যালোচনা", "StatsYearInReview": "বাৎসরিক পর্যালোচনা",
"ToastAccountUpdateSuccess": "অ্যাকাউন্ট আপডেট করা হয়েছে", "ToastAccountUpdateSuccess": "অ্যাকাউন্ট আপডেট করা হয়েছে",
"ToastAppriseUrlRequired": "একটি Apprise ইউআরএল লিখতে হবে", "ToastAppriseUrlRequired": "একটি Apprise ইউআরএল লিখতে হবে",
"ToastAsinRequired": "ASIN প্রয়োজন",
"ToastAuthorImageRemoveSuccess": "লেখকের ছবি সরানো হয়েছে", "ToastAuthorImageRemoveSuccess": "লেখকের ছবি সরানো হয়েছে",
"ToastAuthorNotFound": "লেখক \"{0}\" খুঁজে পাওয়া যায়নি", "ToastAuthorNotFound": "লেখক \"{0}\" খুঁজে পাওয়া যায়নি",
"ToastAuthorRemoveSuccess": "লেখক সরানো হয়েছে", "ToastAuthorRemoveSuccess": "লেখক সরানো হয়েছে",
@@ -870,6 +936,8 @@
"ToastBackupUploadSuccess": "ব্যাকআপ আপলোড হয়েছে", "ToastBackupUploadSuccess": "ব্যাকআপ আপলোড হয়েছে",
"ToastBatchDeleteFailed": "ব্যাচ মুছে ফেলতে ব্যর্থ হয়েছে", "ToastBatchDeleteFailed": "ব্যাচ মুছে ফেলতে ব্যর্থ হয়েছে",
"ToastBatchDeleteSuccess": "ব্যাচ মুছে ফেলা সফল হয়েছে", "ToastBatchDeleteSuccess": "ব্যাচ মুছে ফেলা সফল হয়েছে",
"ToastBatchQuickMatchFailed": "ব্যাচ কুইক ম্যাচ ব্যর্থ!",
"ToastBatchQuickMatchStarted": "{0}টি বইয়ের ব্যাচ কুইক ম্যাচ শুরু হয়েছে!",
"ToastBatchUpdateFailed": "ব্যাচ আপডেট ব্যর্থ হয়েছে", "ToastBatchUpdateFailed": "ব্যাচ আপডেট ব্যর্থ হয়েছে",
"ToastBatchUpdateSuccess": "ব্যাচ আপডেট সাফল্য", "ToastBatchUpdateSuccess": "ব্যাচ আপডেট সাফল্য",
"ToastBookmarkCreateFailed": "বুকমার্ক তৈরি করতে ব্যর্থ", "ToastBookmarkCreateFailed": "বুকমার্ক তৈরি করতে ব্যর্থ",
@@ -881,6 +949,7 @@
"ToastChaptersHaveErrors": "অধ্যায়ে ত্রুটি আছে", "ToastChaptersHaveErrors": "অধ্যায়ে ত্রুটি আছে",
"ToastChaptersMustHaveTitles": "অধ্যায়ের শিরোনাম থাকতে হবে", "ToastChaptersMustHaveTitles": "অধ্যায়ের শিরোনাম থাকতে হবে",
"ToastChaptersRemoved": "অধ্যায়গুলো মুছে ফেলা হয়েছে", "ToastChaptersRemoved": "অধ্যায়গুলো মুছে ফেলা হয়েছে",
"ToastChaptersUpdated": "অধ্যায় আপডেট করা হয়েছে",
"ToastCollectionItemsAddFailed": "আইটেম(গুলি) সংগ্রহে যোগ করা ব্যর্থ হয়েছে", "ToastCollectionItemsAddFailed": "আইটেম(গুলি) সংগ্রহে যোগ করা ব্যর্থ হয়েছে",
"ToastCollectionItemsAddSuccess": "আইটেম(গুলি) সংগ্রহে যোগ করা সফল হয়েছে", "ToastCollectionItemsAddSuccess": "আইটেম(গুলি) সংগ্রহে যোগ করা সফল হয়েছে",
"ToastCollectionItemsRemoveSuccess": "আইটেম(গুলি) সংগ্রহ থেকে সরানো হয়েছে", "ToastCollectionItemsRemoveSuccess": "আইটেম(গুলি) সংগ্রহ থেকে সরানো হয়েছে",
@@ -898,11 +967,14 @@
"ToastEncodeCancelSucces": "এনকোড বাতিল করা হয়েছে", "ToastEncodeCancelSucces": "এনকোড বাতিল করা হয়েছে",
"ToastEpisodeDownloadQueueClearFailed": "সারি সাফ করতে ব্যর্থ হয়েছে", "ToastEpisodeDownloadQueueClearFailed": "সারি সাফ করতে ব্যর্থ হয়েছে",
"ToastEpisodeDownloadQueueClearSuccess": "পর্ব ডাউনলোড সারি পরিষ্কার করা হয়েছে", "ToastEpisodeDownloadQueueClearSuccess": "পর্ব ডাউনলোড সারি পরিষ্কার করা হয়েছে",
"ToastEpisodeUpdateSuccess": "{0}টি পর্ব আপডেট করা হয়েছে",
"ToastErrorCannotShare": "এই ডিভাইসে স্থানীয়ভাবে শেয়ার করা যাবে না", "ToastErrorCannotShare": "এই ডিভাইসে স্থানীয়ভাবে শেয়ার করা যাবে না",
"ToastFailedToLoadData": "ডেটা লোড করা যায়নি", "ToastFailedToLoadData": "ডেটা লোড করা যায়নি",
"ToastFailedToMatch": "মেলাতে ব্যর্থ হয়েছে",
"ToastFailedToShare": "শেয়ার করতে ব্যর্থ", "ToastFailedToShare": "শেয়ার করতে ব্যর্থ",
"ToastFailedToUpdate": "আপডেট করতে ব্যর্থ হয়েছে", "ToastFailedToUpdate": "আপডেট করতে ব্যর্থ হয়েছে",
"ToastInvalidImageUrl": "অকার্যকর ছবির ইউআরএল", "ToastInvalidImageUrl": "অকার্যকর ছবির ইউআরএল",
"ToastInvalidMaxEpisodesToDownload": "ডাউনলোড করার জন্য অবৈধ সর্বোচ্চ পর্ব",
"ToastInvalidUrl": "অকার্যকর ইউআরএল", "ToastInvalidUrl": "অকার্যকর ইউআরএল",
"ToastItemCoverUpdateSuccess": "আইটেম কভার আপডেট করা হয়েছে", "ToastItemCoverUpdateSuccess": "আইটেম কভার আপডেট করা হয়েছে",
"ToastItemDeletedFailed": "আইটেম মুছে ফেলতে ব্যর্থ", "ToastItemDeletedFailed": "আইটেম মুছে ফেলতে ব্যর্থ",
@@ -920,14 +992,22 @@
"ToastLibraryScanFailedToStart": "স্ক্যান শুরু করতে ব্যর্থ", "ToastLibraryScanFailedToStart": "স্ক্যান শুরু করতে ব্যর্থ",
"ToastLibraryScanStarted": "লাইব্রেরি স্ক্যান শুরু হয়েছে", "ToastLibraryScanStarted": "লাইব্রেরি স্ক্যান শুরু হয়েছে",
"ToastLibraryUpdateSuccess": "লাইব্রেরি \"{0}\" আপডেট করা হয়েছে", "ToastLibraryUpdateSuccess": "লাইব্রেরি \"{0}\" আপডেট করা হয়েছে",
"ToastMatchAllAuthorsFailed": "সমস্ত লেখকের সাথে মিলতে ব্যর্থ হয়েছে",
"ToastMetadataFilesRemovedError": "মেটাডেটা সরানোর সময় ত্রুটি {0} ফাইল",
"ToastMetadataFilesRemovedNoneFound": "কোনো মেটাডেটা নেই।লাইব্রেরিতে {0} ফাইল পাওয়া গেছে",
"ToastMetadataFilesRemovedNoneRemoved": "কোনো মেটাডেটা নেই।{0} ফাইল সরানো হয়েছে",
"ToastMetadataFilesRemovedSuccess": "{0} মেটাডেটা৷{1} ফাইল সরানো হয়েছে",
"ToastMustHaveAtLeastOnePath": "অন্তত একটি পথ থাকতে হবে",
"ToastNameEmailRequired": "নাম এবং ইমেইল আবশ্যক", "ToastNameEmailRequired": "নাম এবং ইমেইল আবশ্যক",
"ToastNameRequired": "নাম আবশ্যক", "ToastNameRequired": "নাম আবশ্যক",
"ToastNewEpisodesFound": "{0}টি নতুন পর্ব পাওয়া গেছে",
"ToastNewUserCreatedFailed": "অ্যাকাউন্ট তৈরি করতে ব্যর্থ: \"{0}\"", "ToastNewUserCreatedFailed": "অ্যাকাউন্ট তৈরি করতে ব্যর্থ: \"{0}\"",
"ToastNewUserCreatedSuccess": "নতুন একাউন্ট তৈরি হয়েছে", "ToastNewUserCreatedSuccess": "নতুন একাউন্ট তৈরি হয়েছে",
"ToastNewUserLibraryError": "অন্তত একটি লাইব্রেরি নির্বাচন করতে হবে", "ToastNewUserLibraryError": "অন্তত একটি লাইব্রেরি নির্বাচন করতে হবে",
"ToastNewUserPasswordError": "অন্তত একটি পাসওয়ার্ড থাকতে হবে, শুধুমাত্র রুট ব্যবহারকারীর একটি খালি পাসওয়ার্ড থাকতে পারে", "ToastNewUserPasswordError": "অন্তত একটি পাসওয়ার্ড থাকতে হবে, শুধুমাত্র রুট ব্যবহারকারীর একটি খালি পাসওয়ার্ড থাকতে পারে",
"ToastNewUserTagError": "অন্তত একটি ট্যাগ নির্বাচন করতে হবে", "ToastNewUserTagError": "অন্তত একটি ট্যাগ নির্বাচন করতে হবে",
"ToastNewUserUsernameError": "একটি ব্যবহারকারীর নাম লিখুন", "ToastNewUserUsernameError": "একটি ব্যবহারকারীর নাম লিখুন",
"ToastNoNewEpisodesFound": "কোন নতুন পর্ব পাওয়া যায়নি",
"ToastNoUpdatesNecessary": "কোন আপডেটের প্রয়োজন নেই", "ToastNoUpdatesNecessary": "কোন আপডেটের প্রয়োজন নেই",
"ToastNotificationCreateFailed": "বিজ্ঞপ্তি তৈরি করতে ব্যর্থ", "ToastNotificationCreateFailed": "বিজ্ঞপ্তি তৈরি করতে ব্যর্থ",
"ToastNotificationDeleteFailed": "বিজ্ঞপ্তি মুছে ফেলতে ব্যর্থ", "ToastNotificationDeleteFailed": "বিজ্ঞপ্তি মুছে ফেলতে ব্যর্থ",
@@ -946,6 +1026,7 @@
"ToastPodcastGetFeedFailed": "পডকাস্ট ফিড পেতে ব্যর্থ হয়েছে", "ToastPodcastGetFeedFailed": "পডকাস্ট ফিড পেতে ব্যর্থ হয়েছে",
"ToastPodcastNoEpisodesInFeed": "আরএসএস ফিডে কোনো পর্ব পাওয়া যায়নি", "ToastPodcastNoEpisodesInFeed": "আরএসএস ফিডে কোনো পর্ব পাওয়া যায়নি",
"ToastPodcastNoRssFeed": "পডকাস্টের কোন আরএসএস ফিড নেই", "ToastPodcastNoRssFeed": "পডকাস্টের কোন আরএসএস ফিড নেই",
"ToastProgressIsNotBeingSynced": "অগ্রগতি সিঙ্ক হচ্ছে না, প্লেব্যাক পুনরায় চালু করুন",
"ToastProviderCreatedFailed": "প্রদানকারী যোগ করতে ব্যর্থ হয়েছে", "ToastProviderCreatedFailed": "প্রদানকারী যোগ করতে ব্যর্থ হয়েছে",
"ToastProviderCreatedSuccess": "নতুন প্রদানকারী যোগ করা হয়েছে", "ToastProviderCreatedSuccess": "নতুন প্রদানকারী যোগ করা হয়েছে",
"ToastProviderNameAndUrlRequired": "নাম এবং ইউআরএল আবশ্যক", "ToastProviderNameAndUrlRequired": "নাম এবং ইউআরএল আবশ্যক",
@@ -972,6 +1053,7 @@
"ToastSessionCloseFailed": "অধিবেশন বন্ধ করতে ব্যর্থ হয়েছে", "ToastSessionCloseFailed": "অধিবেশন বন্ধ করতে ব্যর্থ হয়েছে",
"ToastSessionDeleteFailed": "সেশন মুছে ফেলতে ব্যর্থ", "ToastSessionDeleteFailed": "সেশন মুছে ফেলতে ব্যর্থ",
"ToastSessionDeleteSuccess": "সেশন মুছে ফেলা হয়েছে", "ToastSessionDeleteSuccess": "সেশন মুছে ফেলা হয়েছে",
"ToastSleepTimerDone": "স্লিপ টাইমার হয়ে গেছে... zZzzZz",
"ToastSlugMustChange": "স্লাগে অবৈধ অক্ষর রয়েছে", "ToastSlugMustChange": "স্লাগে অবৈধ অক্ষর রয়েছে",
"ToastSlugRequired": "স্লাগ আবশ্যক", "ToastSlugRequired": "স্লাগ আবশ্যক",
"ToastSocketConnected": "সকেট সংযুক্ত", "ToastSocketConnected": "সকেট সংযুক্ত",
File diff suppressed because it is too large Load Diff
+78 -2
View File
@@ -19,6 +19,7 @@
"ButtonChooseFiles": "Vybrat soubory", "ButtonChooseFiles": "Vybrat soubory",
"ButtonClearFilter": "Vymazat filtr", "ButtonClearFilter": "Vymazat filtr",
"ButtonCloseFeed": "Zavřít kanál", "ButtonCloseFeed": "Zavřít kanál",
"ButtonCloseSession": "Zavřít otevřenou relaci",
"ButtonCollections": "Kolekce", "ButtonCollections": "Kolekce",
"ButtonConfigureScanner": "Konfigurovat Prohledávání", "ButtonConfigureScanner": "Konfigurovat Prohledávání",
"ButtonCreate": "Vytvořit", "ButtonCreate": "Vytvořit",
@@ -29,6 +30,8 @@
"ButtonEditChapters": "Upravit kapitoly", "ButtonEditChapters": "Upravit kapitoly",
"ButtonEditPodcast": "Upravit podcast", "ButtonEditPodcast": "Upravit podcast",
"ButtonEnable": "Povolit", "ButtonEnable": "Povolit",
"ButtonFireAndFail": "Spustit a selhat",
"ButtonFireOnTest": "Spustit událost onTest",
"ButtonForceReScan": "Vynutit opětovné prohledání", "ButtonForceReScan": "Vynutit opětovné prohledání",
"ButtonFullPath": "Úplná cesta", "ButtonFullPath": "Úplná cesta",
"ButtonHide": "Skrýt", "ButtonHide": "Skrýt",
@@ -58,11 +61,13 @@
"ButtonPlaylists": "Seznamy skladeb", "ButtonPlaylists": "Seznamy skladeb",
"ButtonPrevious": "Předchozí", "ButtonPrevious": "Předchozí",
"ButtonPreviousChapter": "Předchozí Kapitola", "ButtonPreviousChapter": "Předchozí Kapitola",
"ButtonProbeAudioFile": "Prozkoumat audio soubor",
"ButtonPurgeAllCache": "Vyčistit veškerou mezipaměť", "ButtonPurgeAllCache": "Vyčistit veškerou mezipaměť",
"ButtonPurgeItemsCache": "Vyčistit mezipaměť položek", "ButtonPurgeItemsCache": "Vyčistit mezipaměť položek",
"ButtonQueueAddItem": "Přidat do fronty", "ButtonQueueAddItem": "Přidat do fronty",
"ButtonQueueRemoveItem": "Odstranit z fronty", "ButtonQueueRemoveItem": "Odstranit z fronty",
"ButtonQuickEmbedMetadata": "Rychle Zapsat Metadata", "ButtonQuickEmbed": "Rychle Zapsat",
"ButtonQuickEmbedMetadata": "Rychle zapsat Metadata",
"ButtonQuickMatch": "Rychlé přiřazení", "ButtonQuickMatch": "Rychlé přiřazení",
"ButtonReScan": "Znovu prohledat", "ButtonReScan": "Znovu prohledat",
"ButtonRead": "Číst", "ButtonRead": "Číst",
@@ -83,6 +88,8 @@
"ButtonSaveTracklist": "Uložit seznam skladeb", "ButtonSaveTracklist": "Uložit seznam skladeb",
"ButtonScan": "Prohledat", "ButtonScan": "Prohledat",
"ButtonScanLibrary": "Prohledat Knihovnu", "ButtonScanLibrary": "Prohledat Knihovnu",
"ButtonScrollLeft": "Posunout vlevo",
"ButtonScrollRight": "Posunout vpravo",
"ButtonSearch": "Hledat", "ButtonSearch": "Hledat",
"ButtonSelectFolderPath": "Vybrat cestu ke složce", "ButtonSelectFolderPath": "Vybrat cestu ke složce",
"ButtonSeries": "Série", "ButtonSeries": "Série",
@@ -158,6 +165,7 @@
"HeaderNotificationUpdate": "Aktualizovat notifikaci", "HeaderNotificationUpdate": "Aktualizovat notifikaci",
"HeaderNotifications": "Oznámení", "HeaderNotifications": "Oznámení",
"HeaderOpenIDConnectAuthentication": "Ověřování pomocí OpenID Connect", "HeaderOpenIDConnectAuthentication": "Ověřování pomocí OpenID Connect",
"HeaderOpenListeningSessions": "Otevřené relace přehrávače",
"HeaderOpenRSSFeed": "Otevřít RSS kanál", "HeaderOpenRSSFeed": "Otevřít RSS kanál",
"HeaderOtherFiles": "Ostatní soubory", "HeaderOtherFiles": "Ostatní soubory",
"HeaderPasswordAuthentication": "Autentizace heslem", "HeaderPasswordAuthentication": "Autentizace heslem",
@@ -175,6 +183,7 @@
"HeaderRemoveEpisodes": "Odstranit {0} epizody", "HeaderRemoveEpisodes": "Odstranit {0} epizody",
"HeaderSavedMediaProgress": "Průběh uložených médií", "HeaderSavedMediaProgress": "Průběh uložených médií",
"HeaderSchedule": "Plán", "HeaderSchedule": "Plán",
"HeaderScheduleEpisodeDownloads": "Naplánovat automatické stahování epizod",
"HeaderScheduleLibraryScans": "Naplánovat automatické prohledávání knihoven", "HeaderScheduleLibraryScans": "Naplánovat automatické prohledávání knihoven",
"HeaderSession": "Relace", "HeaderSession": "Relace",
"HeaderSetBackupSchedule": "Nastavit plán zálohování", "HeaderSetBackupSchedule": "Nastavit plán zálohování",
@@ -183,6 +192,7 @@
"HeaderSettingsExperimental": "Experimentální funkce", "HeaderSettingsExperimental": "Experimentální funkce",
"HeaderSettingsGeneral": "Obecné", "HeaderSettingsGeneral": "Obecné",
"HeaderSettingsScanner": "Skener", "HeaderSettingsScanner": "Skener",
"HeaderSettingsWebClient": "Webový klient",
"HeaderSleepTimer": "Časovač vypnutí", "HeaderSleepTimer": "Časovač vypnutí",
"HeaderStatsLargestItems": "Největší položky", "HeaderStatsLargestItems": "Největší položky",
"HeaderStatsLongestItems": "Nejdelší položky (hod.)", "HeaderStatsLongestItems": "Nejdelší položky (hod.)",
@@ -220,7 +230,11 @@
"LabelAllUsersExcludingGuests": "Všichni uživatelé kromě hostů", "LabelAllUsersExcludingGuests": "Všichni uživatelé kromě hostů",
"LabelAllUsersIncludingGuests": "Všichni uživatelé včetně hostů", "LabelAllUsersIncludingGuests": "Všichni uživatelé včetně hostů",
"LabelAlreadyInYourLibrary": "Již ve vaší knihovně", "LabelAlreadyInYourLibrary": "Již ve vaší knihovně",
"LabelApiToken": "API Token",
"LabelAppend": "Připojit", "LabelAppend": "Připojit",
"LabelAudioBitrate": "Bitový tok zvuku (např. 128k)",
"LabelAudioChannels": "Zvukové kanály (1 nebo 2)",
"LabelAudioCodec": "Audio Kodek",
"LabelAuthor": "Autor", "LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (jméno a příjmení)", "LabelAuthorFirstLast": "Autor (jméno a příjmení)",
"LabelAuthorLastFirst": "Autor (příjmení a jméno)", "LabelAuthorLastFirst": "Autor (příjmení a jméno)",
@@ -233,6 +247,7 @@
"LabelAutoRegister": "Automatická registrace", "LabelAutoRegister": "Automatická registrace",
"LabelAutoRegisterDescription": "Automaticky vytvářet nové uživatele po přihlášení", "LabelAutoRegisterDescription": "Automaticky vytvářet nové uživatele po přihlášení",
"LabelBackToUser": "Zpět k uživateli", "LabelBackToUser": "Zpět k uživateli",
"LabelBackupAudioFiles": "Zálohovat zvukové soubory",
"LabelBackupLocation": "Umístění zálohy", "LabelBackupLocation": "Umístění zálohy",
"LabelBackupsEnableAutomaticBackups": "Povolit automatické zálohování", "LabelBackupsEnableAutomaticBackups": "Povolit automatické zálohování",
"LabelBackupsEnableAutomaticBackupsHelp": "Zálohy uložené v /metadata/backups", "LabelBackupsEnableAutomaticBackupsHelp": "Zálohy uložené v /metadata/backups",
@@ -241,15 +256,18 @@
"LabelBackupsNumberToKeep": "Počet záloh, které se mají uchovat", "LabelBackupsNumberToKeep": "Počet záloh, které se mají uchovat",
"LabelBackupsNumberToKeepHelp": "Najednou bude odstraněna pouze 1 záloha, takže pokud již máte více záloh, měli byste je odstranit ručně.", "LabelBackupsNumberToKeepHelp": "Najednou bude odstraněna pouze 1 záloha, takže pokud již máte více záloh, měli byste je odstranit ručně.",
"LabelBitrate": "Datový tok", "LabelBitrate": "Datový tok",
"LabelBonus": "Bonus",
"LabelBooks": "Knihy", "LabelBooks": "Knihy",
"LabelButtonText": "Text tlačítka", "LabelButtonText": "Text tlačítka",
"LabelByAuthor": "od {0}", "LabelByAuthor": "od {0}",
"LabelChangePassword": "Změnit heslo", "LabelChangePassword": "Změnit heslo",
"LabelChannels": "Kanály", "LabelChannels": "Kanály",
"LabelChapterCount": "{0} Kapitol",
"LabelChapterTitle": "Název kapitoly", "LabelChapterTitle": "Název kapitoly",
"LabelChapters": "Kapitoly", "LabelChapters": "Kapitoly",
"LabelChaptersFound": "Kapitoly nalezeny", "LabelChaptersFound": "Kapitoly nalezeny",
"LabelClickForMoreInfo": "Klikněte pro více informací", "LabelClickForMoreInfo": "Klikněte pro více informací",
"LabelClickToUseCurrentValue": "Klikni pro použití aktuální hodnoty",
"LabelClosePlayer": "Zavřít přehrávač", "LabelClosePlayer": "Zavřít přehrávač",
"LabelCodec": "Kodek", "LabelCodec": "Kodek",
"LabelCollapseSeries": "Sbalit sérii", "LabelCollapseSeries": "Sbalit sérii",
@@ -299,12 +317,25 @@
"LabelEmailSettingsTestAddress": "Testovací adresa", "LabelEmailSettingsTestAddress": "Testovací adresa",
"LabelEmbeddedCover": "Vložená obálka", "LabelEmbeddedCover": "Vložená obálka",
"LabelEnable": "Povolit", "LabelEnable": "Povolit",
"LabelEncodingBackupLocation": "Záloha původních audio souborů bude uložena v:",
"LabelEncodingChaptersNotEmbedded": "Kapitoly nejsou vloženy ve vícestopých audioknihách.",
"LabelEncodingClearItemCache": "Nezapomeňte pravidelně promazávat mezipaměť položek.",
"LabelEncodingFinishedM4B": "Výsledné M4B bude uloženo do složky s audioknihou v:",
"LabelEncodingInfoEmbedded": "Metadata budou vložena do audio stop ve složce s audioknihou.",
"LabelEncodingStartedNavigation": "Po spuštění úlohy můžete opustit tuto stránku.",
"LabelEncodingTimeWarning": "Encoding může zabrat až 30 minut.",
"LabelEncodingWarningAdvancedSettings": "Varování: Neměňte toto nastavení pokud neznáte možnosti encodingu ffmpeg.",
"LabelEncodingWatcherDisabled": "Pokud máte zakázaný watcher, budete po skončení muset znovu naskenovat tuto audioknihu.",
"LabelEnd": "Konec", "LabelEnd": "Konec",
"LabelEndOfChapter": "Konec kapitoly", "LabelEndOfChapter": "Konec kapitoly",
"LabelEpisode": "Epizoda", "LabelEpisode": "Epizoda",
"LabelEpisodeNotLinkedToRssFeed": "Epizoda není propojená s RSS feed",
"LabelEpisodeNumber": "Epizoda #{0}",
"LabelEpisodeTitle": "Název epizody", "LabelEpisodeTitle": "Název epizody",
"LabelEpisodeType": "Typ epizody", "LabelEpisodeType": "Typ epizody",
"LabelEpisodeUrlFromRssFeed": "URL epizody z RSS feed",
"LabelEpisodes": "Epizody", "LabelEpisodes": "Epizody",
"LabelEpisodic": "Epizodické",
"LabelExample": "Příklad", "LabelExample": "Příklad",
"LabelExpandSeries": "Rozbalit série", "LabelExpandSeries": "Rozbalit série",
"LabelExpandSubSeries": "Rozbalit podsérie", "LabelExpandSubSeries": "Rozbalit podsérie",
@@ -332,6 +363,7 @@
"LabelFontScale": "Měřítko písma", "LabelFontScale": "Měřítko písma",
"LabelFontStrikethrough": "Přeškrtnutí", "LabelFontStrikethrough": "Přeškrtnutí",
"LabelFormat": "Formát", "LabelFormat": "Formát",
"LabelFull": "Plné",
"LabelGenre": "Žánr", "LabelGenre": "Žánr",
"LabelGenres": "Žánry", "LabelGenres": "Žánry",
"LabelHardDeleteFile": "Trvale smazat soubor", "LabelHardDeleteFile": "Trvale smazat soubor",
@@ -374,6 +406,7 @@
"LabelLess": "Méně", "LabelLess": "Méně",
"LabelLibrariesAccessibleToUser": "Knihovny přístupné uživateli", "LabelLibrariesAccessibleToUser": "Knihovny přístupné uživateli",
"LabelLibrary": "Knihovna", "LabelLibrary": "Knihovna",
"LabelLibraryFilterSublistEmpty": "Žádné {0}",
"LabelLibraryItem": "Položka knihovny", "LabelLibraryItem": "Položka knihovny",
"LabelLibraryName": "Název knihovny", "LabelLibraryName": "Název knihovny",
"LabelLimit": "Omezit", "LabelLimit": "Omezit",
@@ -386,6 +419,10 @@
"LabelLowestPriority": "Nejnižší priorita", "LabelLowestPriority": "Nejnižší priorita",
"LabelMatchExistingUsersBy": "Přiřadit stávající uživatele podle", "LabelMatchExistingUsersBy": "Přiřadit stávající uživatele podle",
"LabelMatchExistingUsersByDescription": "Slouží k propojení stávajících uživatelů. Po propojení budou uživatelé přiřazeni k jedinečnému ID od poskytovatele SSO.", "LabelMatchExistingUsersByDescription": "Slouží k propojení stávajících uživatelů. Po propojení budou uživatelé přiřazeni k jedinečnému ID od poskytovatele SSO.",
"LabelMaxEpisodesToDownload": "Maximální # epizod pro stažení. Použijte 0 pro bez omezení.",
"LabelMaxEpisodesToDownloadPerCheck": "Maximální počet nových epizod ke stažení při jedné kontrole",
"LabelMaxEpisodesToKeep": "Maximální počet epizod k zachování",
"LabelMaxEpisodesToKeepHelp": "Hodnotou 0 není nastaven žádný maximální limit. Po automatickém stažení nové epizody se odstraní nejstarší epizoda, pokud máte více než X epizod. Při každém novém stažení se odstraní pouze 1 epizoda.",
"LabelMediaPlayer": "Přehrávač médií", "LabelMediaPlayer": "Přehrávač médií",
"LabelMediaType": "Typ média", "LabelMediaType": "Typ média",
"LabelMetaTag": "Metaznačka", "LabelMetaTag": "Metaznačka",
@@ -431,12 +468,14 @@
"LabelOpenIDGroupClaimDescription": "Název požadavku OpenID, který obsahuje seznam uživatelských skupin. Běžně se označuje jako <code>groups</code>. <b>Je-li nakonfigurováno</b>, plikace automaticky přiřadí role na základě členství uživatele ve skupinách, pokud jsou tyto skupiny v požadavku pojmenovány case-insensitive 'admin', 'user' nebo 'guest'. Požadavek by měl obsahovat seznam, a pokud uživatel patří do více skupin, aplikace přiřadí roli odpovídající nejvyšší úrovni práva přístupu. Pokud žádná skupina není shodná, bude přístup odepřen.", "LabelOpenIDGroupClaimDescription": "Název požadavku OpenID, který obsahuje seznam uživatelských skupin. Běžně se označuje jako <code>groups</code>. <b>Je-li nakonfigurováno</b>, plikace automaticky přiřadí role na základě členství uživatele ve skupinách, pokud jsou tyto skupiny v požadavku pojmenovány case-insensitive 'admin', 'user' nebo 'guest'. Požadavek by měl obsahovat seznam, a pokud uživatel patří do více skupin, aplikace přiřadí roli odpovídající nejvyšší úrovni práva přístupu. Pokud žádná skupina není shodná, bude přístup odepřen.",
"LabelOpenRSSFeed": "Otevřít RSS kanál", "LabelOpenRSSFeed": "Otevřít RSS kanál",
"LabelOverwrite": "Přepsat", "LabelOverwrite": "Přepsat",
"LabelPaginationPageXOfY": "Strana {0} z {1}",
"LabelPassword": "Heslo", "LabelPassword": "Heslo",
"LabelPath": "Cesta", "LabelPath": "Cesta",
"LabelPermanent": "Trvalé", "LabelPermanent": "Trvalé",
"LabelPermissionsAccessAllLibraries": "Má přístup ke všem knihovnám", "LabelPermissionsAccessAllLibraries": "Má přístup ke všem knihovnám",
"LabelPermissionsAccessAllTags": "Má přístup ke všem značkám", "LabelPermissionsAccessAllTags": "Má přístup ke všem značkám",
"LabelPermissionsAccessExplicitContent": "Má přístup k explicitnímu obsahu", "LabelPermissionsAccessExplicitContent": "Má přístup k explicitnímu obsahu",
"LabelPermissionsCreateEreader": "Může vytvořit Ereader",
"LabelPermissionsDelete": "Může mazat", "LabelPermissionsDelete": "Může mazat",
"LabelPermissionsDownload": "Může stahovat", "LabelPermissionsDownload": "Může stahovat",
"LabelPermissionsUpdate": "Může aktualizovat", "LabelPermissionsUpdate": "Může aktualizovat",
@@ -460,6 +499,8 @@
"LabelPubDate": "Datum vydání", "LabelPubDate": "Datum vydání",
"LabelPublishYear": "Rok vydání", "LabelPublishYear": "Rok vydání",
"LabelPublishedDate": "Vydáno {0}", "LabelPublishedDate": "Vydáno {0}",
"LabelPublishedDecade": "Publikováno (dekáda)",
"LabelPublishedDecades": "Publikováno (dekády)",
"LabelPublisher": "Vydavatel", "LabelPublisher": "Vydavatel",
"LabelPublishers": "Vydavatelé", "LabelPublishers": "Vydavatelé",
"LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka", "LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka",
@@ -479,24 +520,32 @@
"LabelRedo": "Přepracovat", "LabelRedo": "Přepracovat",
"LabelRegion": "Region", "LabelRegion": "Region",
"LabelReleaseDate": "Datum vydání", "LabelReleaseDate": "Datum vydání",
"LabelRemoveAllMetadataAbs": "Odebrat všechny soubory metadata.abs",
"LabelRemoveAllMetadataJson": "Smazat všechny soubory metadata.json",
"LabelRemoveCover": "Odstranit obálku", "LabelRemoveCover": "Odstranit obálku",
"LabelRemoveMetadataFile": "Odstranit soubory metadat ve složkách položek knihovny",
"LabelRemoveMetadataFileHelp": "Odstraníte všechny soubory metadata.json a metadata.abs ve svých složkách {0}.",
"LabelRowsPerPage": "Řádky na stránku", "LabelRowsPerPage": "Řádky na stránku",
"LabelSearchTerm": "Vyhledat termín", "LabelSearchTerm": "Vyhledat termín",
"LabelSearchTitle": "Vyhledat název", "LabelSearchTitle": "Vyhledat název",
"LabelSearchTitleOrASIN": "Vyhledat název nebo ASIN", "LabelSearchTitleOrASIN": "Vyhledat název nebo ASIN",
"LabelSeason": "Sezóna", "LabelSeason": "Sezóna",
"LabelSeasonNumber": "Sezóna č.{0}",
"LabelSelectAll": "Vybrat vše", "LabelSelectAll": "Vybrat vše",
"LabelSelectAllEpisodes": "Vybrat všechny epizody", "LabelSelectAllEpisodes": "Vybrat všechny epizody",
"LabelSelectEpisodesShowing": "Vyberte {0} epizody, které se zobrazují", "LabelSelectEpisodesShowing": "Vyberte {0} epizody, které se zobrazují",
"LabelSelectUsers": "Vybrat uživatele", "LabelSelectUsers": "Vybrat uživatele",
"LabelSendEbookToDevice": "Odeslat e-knihu do...", "LabelSendEbookToDevice": "Odeslat e-knihu do...",
"LabelSequence": "Sekvence", "LabelSequence": "Sekvence",
"LabelSerial": "Sériové",
"LabelSeries": "Série", "LabelSeries": "Série",
"LabelSeriesName": "Název série", "LabelSeriesName": "Název série",
"LabelSeriesProgress": "Průběh série", "LabelSeriesProgress": "Průběh série",
"LabelServerLogLevel": "Úroveň protokolu serveru",
"LabelServerYearReview": "Přehled roku na serveru ({0})", "LabelServerYearReview": "Přehled roku na serveru ({0})",
"LabelSetEbookAsPrimary": "Nastavit jako primární", "LabelSetEbookAsPrimary": "Nastavit jako primární",
"LabelSetEbookAsSupplementary": "Nastavit jako doplňkové", "LabelSetEbookAsSupplementary": "Nastavit jako doplňkové",
"LabelSettingsAllowIframe": "Povolit vložení do rámce iframe",
"LabelSettingsAudiobooksOnly": "Pouze audioknihy", "LabelSettingsAudiobooksOnly": "Pouze audioknihy",
"LabelSettingsAudiobooksOnlyHelp": "Povolením tohoto nastavení budou soubory e-knih ignorovány, pokud nejsou ve složce audioknih, v takovém případě budou nastaveny jako doplňkové e-knihy", "LabelSettingsAudiobooksOnlyHelp": "Povolením tohoto nastavení budou soubory e-knih ignorovány, pokud nejsou ve složce audioknih, v takovém případě budou nastaveny jako doplňkové e-knihy",
"LabelSettingsBookshelfViewHelp": "Skeumorfní design s dřevěnými policemi", "LabelSettingsBookshelfViewHelp": "Skeumorfní design s dřevěnými policemi",
@@ -518,6 +567,9 @@
"LabelSettingsHideSingleBookSeriesHelp": "Série, které mají jedinou knihu, budou skryty na stránce série a na domovské stránce.", "LabelSettingsHideSingleBookSeriesHelp": "Série, které mají jedinou knihu, budou skryty na stránce série a na domovské stránce.",
"LabelSettingsHomePageBookshelfView": "Domovská stránka používá zobrazení police s knihami", "LabelSettingsHomePageBookshelfView": "Domovská stránka používá zobrazení police s knihami",
"LabelSettingsLibraryBookshelfView": "Knihovna používá zobrazení police s knihami", "LabelSettingsLibraryBookshelfView": "Knihovna používá zobrazení police s knihami",
"LabelSettingsLibraryMarkAsFinishedPercentComplete": "Procento dokončení je vyšší než",
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Zbývající čas je kratší než (sekund)",
"LabelSettingsLibraryMarkAsFinishedWhen": "Označit položku médií jako dokončenou, když",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Přeskočit předchozí knihy v pokračování série", "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Přeskočit předchozí knihy v pokračování série",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Polička Pokračovat v sérii na domovské stránce zobrazuje první nezačatou knihu v sériích, které mají alespoň jednu knihu dokončenou a žádnou rozečtenou. Povolením tohoto nastavení budou série pokračovat od poslední dokončené knihy namísto první nezačaté knihy.", "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Polička Pokračovat v sérii na domovské stránce zobrazuje první nezačatou knihu v sériích, které mají alespoň jednu knihu dokončenou a žádnou rozečtenou. Povolením tohoto nastavení budou série pokračovat od poslední dokončené knihy namísto první nezačaté knihy.",
"LabelSettingsParseSubtitles": "Analzyovat podtitul", "LabelSettingsParseSubtitles": "Analzyovat podtitul",
@@ -536,12 +588,16 @@
"LabelSettingsStoreMetadataWithItemHelp": "Ve výchozím nastavení jsou soubory metadat uloženy v adresáři /metadata/items, povolením tohoto nastavení budou soubory metadat uloženy ve složkách položek knihovny", "LabelSettingsStoreMetadataWithItemHelp": "Ve výchozím nastavení jsou soubory metadat uloženy v adresáři /metadata/items, povolením tohoto nastavení budou soubory metadat uloženy ve složkách položek knihovny",
"LabelSettingsTimeFormat": "Formát času", "LabelSettingsTimeFormat": "Formát času",
"LabelShare": "Sdílet", "LabelShare": "Sdílet",
"LabelShareOpen": "Otevřít sdílení",
"LabelShareURL": "Sdílet URL", "LabelShareURL": "Sdílet URL",
"LabelShowAll": "Zobrazit vše", "LabelShowAll": "Zobrazit vše",
"LabelShowSeconds": "Zobrazit sekundy", "LabelShowSeconds": "Zobrazit sekundy",
"LabelShowSubtitles": "Zobrazit titulky", "LabelShowSubtitles": "Zobrazit titulky",
"LabelSize": "Velikost", "LabelSize": "Velikost",
"LabelSleepTimer": "Časovač vypnutí", "LabelSleepTimer": "Časovač vypnutí",
"LabelSlug": "URL název",
"LabelSortAscending": "Vzestupně",
"LabelSortDescending": "Sestupně",
"LabelStart": "Spustit", "LabelStart": "Spustit",
"LabelStartTime": "Čas Spuštění", "LabelStartTime": "Čas Spuštění",
"LabelStarted": "Spuštěno", "LabelStarted": "Spuštěno",
@@ -580,6 +636,7 @@
"LabelTimeDurationXMinutes": "{0} minut", "LabelTimeDurationXMinutes": "{0} minut",
"LabelTimeDurationXSeconds": "{0} sekund", "LabelTimeDurationXSeconds": "{0} sekund",
"LabelTimeInMinutes": "Čas v minutách", "LabelTimeInMinutes": "Čas v minutách",
"LabelTimeLeft": "{0} zbývá",
"LabelTimeListened": "Čas poslechu", "LabelTimeListened": "Čas poslechu",
"LabelTimeListenedToday": "Čas poslechu dnes", "LabelTimeListenedToday": "Čas poslechu dnes",
"LabelTimeRemaining": "{0} zbývá", "LabelTimeRemaining": "{0} zbývá",
@@ -587,6 +644,7 @@
"LabelTitle": "Název", "LabelTitle": "Název",
"LabelToolsEmbedMetadata": "Vložit metadata", "LabelToolsEmbedMetadata": "Vložit metadata",
"LabelToolsEmbedMetadataDescription": "Vložit metadata do zvukových souborů včetně obálky a kapitol.", "LabelToolsEmbedMetadataDescription": "Vložit metadata do zvukových souborů včetně obálky a kapitol.",
"LabelToolsM4bEncoder": "Enkodér M4B",
"LabelToolsMakeM4b": "Vytvořit soubor audioknihy M4B", "LabelToolsMakeM4b": "Vytvořit soubor audioknihy M4B",
"LabelToolsMakeM4bDescription": "Vygenerovat soubor audioknihy M4B s vloženými metadaty, obálkou a kapitolami.", "LabelToolsMakeM4bDescription": "Vygenerovat soubor audioknihy M4B s vloženými metadaty, obálkou a kapitolami.",
"LabelToolsSplitM4b": "Rozdělit M4B na MP3", "LabelToolsSplitM4b": "Rozdělit M4B na MP3",
@@ -599,6 +657,7 @@
"LabelTracksMultiTrack": "Více stop", "LabelTracksMultiTrack": "Více stop",
"LabelTracksNone": "Žádné stopy", "LabelTracksNone": "Žádné stopy",
"LabelTracksSingleTrack": "Jedna stopa", "LabelTracksSingleTrack": "Jedna stopa",
"LabelTrailer": "Upoutávka",
"LabelType": "Typ", "LabelType": "Typ",
"LabelUnabridged": "Nezkráceno", "LabelUnabridged": "Nezkráceno",
"LabelUndo": "Zpět", "LabelUndo": "Zpět",
@@ -610,10 +669,13 @@
"LabelUpdateDetailsHelp": "Povolit přepsání existujících údajů o vybraných knihách, když je nalezena shoda", "LabelUpdateDetailsHelp": "Povolit přepsání existujících údajů o vybraných knihách, když je nalezena shoda",
"LabelUpdatedAt": "Aktualizováno v", "LabelUpdatedAt": "Aktualizováno v",
"LabelUploaderDragAndDrop": "Přetáhnout soubory nebo složky", "LabelUploaderDragAndDrop": "Přetáhnout soubory nebo složky",
"LabelUploaderDragAndDropFilesOnly": "Přetáhnout a upustit soubory",
"LabelUploaderDropFiles": "Odstranit soubory", "LabelUploaderDropFiles": "Odstranit soubory",
"LabelUploaderItemFetchMetadataHelp": "Automaticky načíst název, autora a sérii", "LabelUploaderItemFetchMetadataHelp": "Automaticky načíst název, autora a sérii",
"LabelUseAdvancedOptions": "Použít pokročilé možnosti",
"LabelUseChapterTrack": "Použít stopu kapitoly", "LabelUseChapterTrack": "Použít stopu kapitoly",
"LabelUseFullTrack": "Použít celou stopu", "LabelUseFullTrack": "Použít celou stopu",
"LabelUseZeroForUnlimited": "Použijte 0 pro neomezené",
"LabelUser": "Uživatel", "LabelUser": "Uživatel",
"LabelUsername": "Uživatelské jméno", "LabelUsername": "Uživatelské jméno",
"LabelValue": "Hodnota", "LabelValue": "Hodnota",
@@ -623,6 +685,8 @@
"LabelViewPlayerSettings": "Zobrazit nastavení přehrávače", "LabelViewPlayerSettings": "Zobrazit nastavení přehrávače",
"LabelViewQueue": "Zobrazit frontu přehrávače", "LabelViewQueue": "Zobrazit frontu přehrávače",
"LabelVolume": "Hlasitost", "LabelVolume": "Hlasitost",
"LabelWebRedirectURLsDescription": "Autorizujte tyto adresy URL ve zprostředkovateli OAuth, abyste po přihlášení umožnili přesměrování zpět do webové aplikace:",
"LabelWebRedirectURLsSubfolder": "Podsložka pro přesměrování adres URL",
"LabelWeekdaysToRun": "Dny v týdnu ke spuštění", "LabelWeekdaysToRun": "Dny v týdnu ke spuštění",
"LabelXBooks": "{0} knih", "LabelXBooks": "{0} knih",
"LabelXItems": "{0} položky", "LabelXItems": "{0} položky",
@@ -660,6 +724,7 @@
"MessageConfirmDeleteMetadataProvider": "Opravdu chcete vymazat vlastního poskytovatele metadat \"{0}\"?", "MessageConfirmDeleteMetadataProvider": "Opravdu chcete vymazat vlastního poskytovatele metadat \"{0}\"?",
"MessageConfirmDeleteNotification": "Opravdu chcete vymazat tuto notifikaci?", "MessageConfirmDeleteNotification": "Opravdu chcete vymazat tuto notifikaci?",
"MessageConfirmDeleteSession": "Opravdu chcete smazat tuto relaci?", "MessageConfirmDeleteSession": "Opravdu chcete smazat tuto relaci?",
"MessageConfirmEmbedMetadataInAudioFiles": "Jste si jisti, že chcete vložit metadata do {0} zvukových souborů?",
"MessageConfirmForceReScan": "Opravdu chcete vynutit opětovné prohledání?", "MessageConfirmForceReScan": "Opravdu chcete vynutit opětovné prohledání?",
"MessageConfirmMarkAllEpisodesFinished": "Opravdu chcete označit všechny epizody jako dokončené?", "MessageConfirmMarkAllEpisodesFinished": "Opravdu chcete označit všechny epizody jako dokončené?",
"MessageConfirmMarkAllEpisodesNotFinished": "Opravdu chcete označit všechny epizody jako nedokončené?", "MessageConfirmMarkAllEpisodesNotFinished": "Opravdu chcete označit všechny epizody jako nedokončené?",
@@ -667,9 +732,11 @@
"MessageConfirmMarkItemNotFinished": "Opravdu chcete označit \"{0}\" jako nedokončené?", "MessageConfirmMarkItemNotFinished": "Opravdu chcete označit \"{0}\" jako nedokončené?",
"MessageConfirmMarkSeriesFinished": "Opravdu chcete označit všechny knihy z této série jako dokončené?", "MessageConfirmMarkSeriesFinished": "Opravdu chcete označit všechny knihy z této série jako dokončené?",
"MessageConfirmMarkSeriesNotFinished": "Opravdu chcete označit všechny knihy z této série jako nedokončené?", "MessageConfirmMarkSeriesNotFinished": "Opravdu chcete označit všechny knihy z této série jako nedokončené?",
"MessageConfirmNotificationTestTrigger": "Spustit toto oznámení s testovacími daty?",
"MessageConfirmPurgeCache": "Vyčistit mezipaměť odstraní celý adresář na adrese <code>/metadata/cache</code>. <br /><br />Určitě chcete odstranit adresář mezipaměti?", "MessageConfirmPurgeCache": "Vyčistit mezipaměť odstraní celý adresář na adrese <code>/metadata/cache</code>. <br /><br />Určitě chcete odstranit adresář mezipaměti?",
"MessageConfirmPurgeItemsCache": "Vyčištění mezipaměti položek odstraní celý adresář <code>/metadata/cache/items</code>.<br />Jste si jistí?", "MessageConfirmPurgeItemsCache": "Vyčištění mezipaměti položek odstraní celý adresář <code>/metadata/cache/items</code>.<br />Jste si jistí?",
"MessageConfirmQuickEmbed": "Varování! Rychlé vložení nezálohuje vaše zvukové soubory. Ujistěte se, že máte zálohu zvukových souborů. <br><br>Chcete pokračovat?", "MessageConfirmQuickEmbed": "Varování! Rychlé vložení nezálohuje vaše zvukové soubory. Ujistěte se, že máte zálohu zvukových souborů. <br><br>Chcete pokračovat?",
"MessageConfirmQuickMatchEpisodes": "Pokud je nalezena shoda při rychlém párování epizod, dojde k přepsání podrobností. Aktualizovány budou pouze nespárované epizody. Jste si jisti?",
"MessageConfirmReScanLibraryItems": "Opravdu chcete znovu prohledat {0} položky?", "MessageConfirmReScanLibraryItems": "Opravdu chcete znovu prohledat {0} položky?",
"MessageConfirmRemoveAllChapters": "Opravdu chcete odstranit všechny kapitoly?", "MessageConfirmRemoveAllChapters": "Opravdu chcete odstranit všechny kapitoly?",
"MessageConfirmRemoveAuthor": "Opravdu chcete odstranit autora \"{0}\"?", "MessageConfirmRemoveAuthor": "Opravdu chcete odstranit autora \"{0}\"?",
@@ -677,6 +744,7 @@
"MessageConfirmRemoveEpisode": "Opravdu chcete odstranit epizodu \"{0}\"?", "MessageConfirmRemoveEpisode": "Opravdu chcete odstranit epizodu \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Opravdu chcete odstranit {0} epizody?", "MessageConfirmRemoveEpisodes": "Opravdu chcete odstranit {0} epizody?",
"MessageConfirmRemoveListeningSessions": "Opravdu chcete odebrat {0} poslechových relací?", "MessageConfirmRemoveListeningSessions": "Opravdu chcete odebrat {0} poslechových relací?",
"MessageConfirmRemoveMetadataFiles": "Jste si jisti, že chcete odstranit všechny metadata.{0} soubory ve složkách s položkami ve vaší knihovně?",
"MessageConfirmRemoveNarrator": "Opravdu chcete odebrat předčítání \"{0}\"?", "MessageConfirmRemoveNarrator": "Opravdu chcete odebrat předčítání \"{0}\"?",
"MessageConfirmRemovePlaylist": "Opravdu chcete odstranit svůj playlist \"{0}\"?", "MessageConfirmRemovePlaylist": "Opravdu chcete odstranit svůj playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Opravdu chcete přejmenovat žánr \"{0}\" na \"{1}\" pro všechny položky?", "MessageConfirmRenameGenre": "Opravdu chcete přejmenovat žánr \"{0}\" na \"{1}\" pro všechny položky?",
@@ -692,6 +760,7 @@
"MessageDragFilesIntoTrackOrder": "Přetáhněte soubory do správného pořadí stop", "MessageDragFilesIntoTrackOrder": "Přetáhněte soubory do správného pořadí stop",
"MessageEmbedFailed": "Vložení selhalo!", "MessageEmbedFailed": "Vložení selhalo!",
"MessageEmbedFinished": "Vložení dokončeno!", "MessageEmbedFinished": "Vložení dokončeno!",
"MessageEmbedQueue": "Zařazeno do fronty pro vložení metadat ({0} ve frontě)",
"MessageEpisodesQueuedForDownload": "{0} Epizody zařazené do fronty ke stažení", "MessageEpisodesQueuedForDownload": "{0} Epizody zařazené do fronty ke stažení",
"MessageEreaderDevices": "Aby bylo zajištěno doručení elektronických knih, může být nutné přidat výše uvedenou e-mailovou adresu jako platného odesílatele pro každé zařízení uvedené níže.", "MessageEreaderDevices": "Aby bylo zajištěno doručení elektronických knih, může být nutné přidat výše uvedenou e-mailovou adresu jako platného odesílatele pro každé zařízení uvedené níže.",
"MessageFeedURLWillBe": "URL zdroje bude {0}", "MessageFeedURLWillBe": "URL zdroje bude {0}",
@@ -736,6 +805,7 @@
"MessageNoLogs": "Žádné protokoly", "MessageNoLogs": "Žádné protokoly",
"MessageNoMediaProgress": "Žádný průběh médií", "MessageNoMediaProgress": "Žádný průběh médií",
"MessageNoNotifications": "Žádná oznámení", "MessageNoNotifications": "Žádná oznámení",
"MessageNoPodcastFeed": "Neplatný podcast: Žádný kanál",
"MessageNoPodcastsFound": "Nebyly nalezeny žádné podcasty", "MessageNoPodcastsFound": "Nebyly nalezeny žádné podcasty",
"MessageNoResults": "Žádné výsledky", "MessageNoResults": "Žádné výsledky",
"MessageNoSearchResultsFor": "Nebyly nalezeny žádné výsledky hledání pro \"{0}\"", "MessageNoSearchResultsFor": "Nebyly nalezeny žádné výsledky hledání pro \"{0}\"",
@@ -752,7 +822,10 @@
"MessagePlaylistCreateFromCollection": "Vytvořit seznam skladeb z kolekce", "MessagePlaylistCreateFromCollection": "Vytvořit seznam skladeb z kolekce",
"MessagePleaseWait": "Čekejte prosím...", "MessagePleaseWait": "Čekejte prosím...",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nemá žádnou adresu URL kanálu RSS, kterou by mohl použít pro porovnávání", "MessagePodcastHasNoRSSFeedForMatching": "Podcast nemá žádnou adresu URL kanálu RSS, kterou by mohl použít pro porovnávání",
"MessageQuickMatchDescription": "Vyplňte prázdné detaily položky a obálku prvním výsledkem shody z '{0}'. Nepřepisuje podrobnosti, pokud není povoleno nastavení serveru \"Preferovat párování metadata\".", "MessageQuickEmbedInProgress": "Probíhá rychlé vkládání",
"MessageQuickEmbedQueue": "Zařazeno do fronty pro rychlé vložení ({0} ve frontě)",
"MessageQuickMatchAllEpisodes": "Rychlá shoda všech epizod",
"MessageQuickMatchDescription": "Vyplnit prázdné detaily položky a obálky prvním výsledkem shody z '{0}'. Nepřepisuje detaily, pokud není povoleno nastavení serveru 'Preferovat shodná metadata'.",
"MessageRemoveChapter": "Odstranit kapitolu", "MessageRemoveChapter": "Odstranit kapitolu",
"MessageRemoveEpisodes": "Odstranit {0} epizodu", "MessageRemoveEpisodes": "Odstranit {0} epizodu",
"MessageRemoveFromPlayerQueue": "Odstranit z fronty přehrávače", "MessageRemoveFromPlayerQueue": "Odstranit z fronty přehrávače",
@@ -783,10 +856,13 @@
"MessageTaskFailedToMergeAudioFiles": "Spojení audio souborů selhalo", "MessageTaskFailedToMergeAudioFiles": "Spojení audio souborů selhalo",
"MessageTaskFailedToMoveM4bFile": "Přesunutí m4b souboru selhalo", "MessageTaskFailedToMoveM4bFile": "Přesunutí m4b souboru selhalo",
"MessageTaskFailedToWriteMetadataFile": "Zápis souboru metadat selhal", "MessageTaskFailedToWriteMetadataFile": "Zápis souboru metadat selhal",
"MessageTaskMatchingBooksInLibrary": "Párování knih v knihovně „{0}“",
"MessageTaskNoFilesToScan": "Žádné soubory ke skenování", "MessageTaskNoFilesToScan": "Žádné soubory ke skenování",
"MessageTaskOpmlImport": "Import OPML", "MessageTaskOpmlImport": "Import OPML",
"MessageTaskOpmlImportDescription": "Vytváření podcastů z {0} RSS feedů", "MessageTaskOpmlImportDescription": "Vytváření podcastů z {0} RSS feedů",
"MessageTaskOpmlImportFeed": "Importní zdroj OPML",
"MessageTaskOpmlImportFeedDescription": "Importování RSS feedu \"{0}\"", "MessageTaskOpmlImportFeedDescription": "Importování RSS feedu \"{0}\"",
"MessageTaskOpmlImportFeedFailed": "Nepodařilo se získat kanál podcastu",
"MessageTaskOpmlImportFeedPodcastDescription": "Vytváření podcastu \"{0}\"", "MessageTaskOpmlImportFeedPodcastDescription": "Vytváření podcastu \"{0}\"",
"MessageTaskOpmlImportFeedPodcastExists": "Podcast se stejnou cestou již existuje", "MessageTaskOpmlImportFeedPodcastExists": "Podcast se stejnou cestou již existuje",
"MessageTaskOpmlImportFeedPodcastFailed": "Vytváření podcastu selhalo", "MessageTaskOpmlImportFeedPodcastFailed": "Vytváření podcastu selhalo",
+4 -1
View File
@@ -19,6 +19,7 @@
"ButtonChooseFiles": "Vælg filer", "ButtonChooseFiles": "Vælg filer",
"ButtonClearFilter": "Ryd filter", "ButtonClearFilter": "Ryd filter",
"ButtonCloseFeed": "Luk feed", "ButtonCloseFeed": "Luk feed",
"ButtonCloseSession": "Luk Åben Session",
"ButtonCollections": "Samlinger", "ButtonCollections": "Samlinger",
"ButtonConfigureScanner": "Konfigurer scanner", "ButtonConfigureScanner": "Konfigurer scanner",
"ButtonCreate": "Opret", "ButtonCreate": "Opret",
@@ -29,7 +30,9 @@
"ButtonEditChapters": "Rediger kapitler", "ButtonEditChapters": "Rediger kapitler",
"ButtonEditPodcast": "Rediger podcast", "ButtonEditPodcast": "Rediger podcast",
"ButtonEnable": "Aktiver", "ButtonEnable": "Aktiver",
"ButtonForceReScan": "Tvungen genindlæsning", "ButtonFireAndFail": "Affyring Og Fejl",
"ButtonFireOnTest": "Affyring vedTest begivenhed",
"ButtonForceReScan": "Tving genindlæsning",
"ButtonFullPath": "Fuld sti", "ButtonFullPath": "Fuld sti",
"ButtonHide": "Skjul", "ButtonHide": "Skjul",
"ButtonHome": "Hjem", "ButtonHome": "Hjem",
+96 -9
View File
@@ -66,12 +66,13 @@
"ButtonPurgeItemsCache": "Lösche Medien-Cache", "ButtonPurgeItemsCache": "Lösche Medien-Cache",
"ButtonQueueAddItem": "Zur Warteschlange hinzufügen", "ButtonQueueAddItem": "Zur Warteschlange hinzufügen",
"ButtonQueueRemoveItem": "Aus der Warteschlange entfernen", "ButtonQueueRemoveItem": "Aus der Warteschlange entfernen",
"ButtonQuickEmbed": "Schnelles Hinzufügen",
"ButtonQuickEmbedMetadata": "Schnelles Hinzufügen von Metadaten", "ButtonQuickEmbedMetadata": "Schnelles Hinzufügen von Metadaten",
"ButtonQuickMatch": "Schnellabgleich", "ButtonQuickMatch": "Schnellabgleich",
"ButtonReScan": "Neu scannen", "ButtonReScan": "Neu scannen",
"ButtonRead": "Lesen", "ButtonRead": "Lesen",
"ButtonReadLess": "Weniger anzeigen", "ButtonReadLess": "weniger Anzeigen",
"ButtonReadMore": "Mehr anzeigen", "ButtonReadMore": "Mehr Anzeigen",
"ButtonRefresh": "Neu Laden", "ButtonRefresh": "Neu Laden",
"ButtonRemove": "Entfernen", "ButtonRemove": "Entfernen",
"ButtonRemoveAll": "Alles entfernen", "ButtonRemoveAll": "Alles entfernen",
@@ -87,6 +88,8 @@
"ButtonSaveTracklist": "Speichere die Titelliste", "ButtonSaveTracklist": "Speichere die Titelliste",
"ButtonScan": "Partial-Scan (nur geänderte/neue Medien)", "ButtonScan": "Partial-Scan (nur geänderte/neue Medien)",
"ButtonScanLibrary": "Bibliothek scannen", "ButtonScanLibrary": "Bibliothek scannen",
"ButtonScrollLeft": "Nach Links scrollen",
"ButtonScrollRight": "Nach Rechts scrollen",
"ButtonSearch": "Suchen", "ButtonSearch": "Suchen",
"ButtonSelectFolderPath": "Ordnerpfad auswählen", "ButtonSelectFolderPath": "Ordnerpfad auswählen",
"ButtonSeries": "Serien", "ButtonSeries": "Serien",
@@ -162,6 +165,7 @@
"HeaderNotificationUpdate": "Benachrichtigung bearbeiten", "HeaderNotificationUpdate": "Benachrichtigung bearbeiten",
"HeaderNotifications": "Benachrichtigungen", "HeaderNotifications": "Benachrichtigungen",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentifizierung", "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentifizierung",
"HeaderOpenListeningSessions": "Aktive Hörbuch-Sitzungen",
"HeaderOpenRSSFeed": "RSS-Feed öffnen", "HeaderOpenRSSFeed": "RSS-Feed öffnen",
"HeaderOtherFiles": "Sonstige Dateien", "HeaderOtherFiles": "Sonstige Dateien",
"HeaderPasswordAuthentication": "Passwortauthentifizierung", "HeaderPasswordAuthentication": "Passwortauthentifizierung",
@@ -179,6 +183,7 @@
"HeaderRemoveEpisodes": "Entferne {0} Episoden", "HeaderRemoveEpisodes": "Entferne {0} Episoden",
"HeaderSavedMediaProgress": "Gespeicherte Hörfortschritte", "HeaderSavedMediaProgress": "Gespeicherte Hörfortschritte",
"HeaderSchedule": "Zeitplan", "HeaderSchedule": "Zeitplan",
"HeaderScheduleEpisodeDownloads": "Automatische Episoden-Downloads planen",
"HeaderScheduleLibraryScans": "Automatische Bibliotheksscans", "HeaderScheduleLibraryScans": "Automatische Bibliotheksscans",
"HeaderSession": "Sitzung", "HeaderSession": "Sitzung",
"HeaderSetBackupSchedule": "Zeitplan für die Datensicherung festlegen", "HeaderSetBackupSchedule": "Zeitplan für die Datensicherung festlegen",
@@ -187,6 +192,7 @@
"HeaderSettingsExperimental": "Experimentelle Funktionen", "HeaderSettingsExperimental": "Experimentelle Funktionen",
"HeaderSettingsGeneral": "Allgemein", "HeaderSettingsGeneral": "Allgemein",
"HeaderSettingsScanner": "Scanner", "HeaderSettingsScanner": "Scanner",
"HeaderSettingsWebClient": "Web-Client",
"HeaderSleepTimer": "Sleep-Timer", "HeaderSleepTimer": "Sleep-Timer",
"HeaderStatsLargestItems": "Größte Medien", "HeaderStatsLargestItems": "Größte Medien",
"HeaderStatsLongestItems": "Längste Medien (h)", "HeaderStatsLongestItems": "Längste Medien (h)",
@@ -217,14 +223,18 @@
"LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen", "LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen",
"LabelAddToPlaylistBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Wiedergabeliste hinzu", "LabelAddToPlaylistBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Wiedergabeliste hinzu",
"LabelAddedAt": "Hinzugefügt am", "LabelAddedAt": "Hinzugefügt am",
"LabelAddedDate": "Hinzugefügt {0}", "LabelAddedDate": "{0} Hinzugefügt",
"LabelAdminUsersOnly": "Nur Admin Benutzer", "LabelAdminUsersOnly": "Nur Admin Benutzer",
"LabelAll": "Alle", "LabelAll": "Alle",
"LabelAllUsers": "Alle Benutzer", "LabelAllUsers": "Alle Benutzer",
"LabelAllUsersExcludingGuests": "Alle Benutzer außer Gästen", "LabelAllUsersExcludingGuests": "Alle Benutzer außer Gästen",
"LabelAllUsersIncludingGuests": "Alle Benutzer und Gäste", "LabelAllUsersIncludingGuests": "Alle Benutzer und Gäste",
"LabelAlreadyInYourLibrary": "Bereits in der Bibliothek", "LabelAlreadyInYourLibrary": "Bereits in der Bibliothek",
"LabelApiToken": "API Schlüssel",
"LabelAppend": "Anhängen", "LabelAppend": "Anhängen",
"LabelAudioBitrate": "Audiobitrate (z. B. 128 kbit/s)",
"LabelAudioChannels": "Audiokanäle (1 oder 2)",
"LabelAudioCodec": "Audiocodec",
"LabelAuthor": "Autor", "LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Vorname Nachname)", "LabelAuthorFirstLast": "Autor (Vorname Nachname)",
"LabelAuthorLastFirst": "Autor (Nachname, Vorname)", "LabelAuthorLastFirst": "Autor (Nachname, Vorname)",
@@ -237,6 +247,7 @@
"LabelAutoRegister": "Automatische Registrierung", "LabelAutoRegister": "Automatische Registrierung",
"LabelAutoRegisterDescription": "Automatische neue Neutzer anlegen nach dem Registrieren", "LabelAutoRegisterDescription": "Automatische neue Neutzer anlegen nach dem Registrieren",
"LabelBackToUser": "Zurück zum Benutzer", "LabelBackToUser": "Zurück zum Benutzer",
"LabelBackupAudioFiles": "Audio-Dateien sichern",
"LabelBackupLocation": "Backup-Ort", "LabelBackupLocation": "Backup-Ort",
"LabelBackupsEnableAutomaticBackups": "Automatische Sicherung aktivieren", "LabelBackupsEnableAutomaticBackups": "Automatische Sicherung aktivieren",
"LabelBackupsEnableAutomaticBackupsHelp": "Backups werden in /metadata/backups gespeichert", "LabelBackupsEnableAutomaticBackupsHelp": "Backups werden in /metadata/backups gespeichert",
@@ -245,15 +256,18 @@
"LabelBackupsNumberToKeep": "Anzahl der aufzubewahrenden Sicherungen", "LabelBackupsNumberToKeep": "Anzahl der aufzubewahrenden Sicherungen",
"LabelBackupsNumberToKeepHelp": "Es wird immer nur 1 Sicherung auf einmal entfernt. Wenn du bereits mehrere Sicherungen als die definierte max. Anzahl hast, solltest du diese manuell entfernen.", "LabelBackupsNumberToKeepHelp": "Es wird immer nur 1 Sicherung auf einmal entfernt. Wenn du bereits mehrere Sicherungen als die definierte max. Anzahl hast, solltest du diese manuell entfernen.",
"LabelBitrate": "Bitrate", "LabelBitrate": "Bitrate",
"LabelBonus": "Bonus",
"LabelBooks": "Bücher", "LabelBooks": "Bücher",
"LabelButtonText": "Knopftext", "LabelButtonText": "Knopftext",
"LabelByAuthor": "von {0}", "LabelByAuthor": "von {0}",
"LabelChangePassword": "Passwort ändern", "LabelChangePassword": "Passwort ändern",
"LabelChannels": "Kanäle", "LabelChannels": "Kanäle",
"LabelChapterCount": "{0} Kapitel",
"LabelChapterTitle": "Kapitelüberschrift", "LabelChapterTitle": "Kapitelüberschrift",
"LabelChapters": "Kapitel", "LabelChapters": "Kapitel",
"LabelChaptersFound": "Gefundene Kapitel", "LabelChaptersFound": "Gefundene Kapitel",
"LabelClickForMoreInfo": "Klicken für mehr Informationen", "LabelClickForMoreInfo": "Klicken für mehr Informationen",
"LabelClickToUseCurrentValue": "Anklicken um aktuellen Wert zu verwenden",
"LabelClosePlayer": "Player schließen", "LabelClosePlayer": "Player schließen",
"LabelCodec": "Codec", "LabelCodec": "Codec",
"LabelCollapseSeries": "Serien einklappen", "LabelCollapseSeries": "Serien einklappen",
@@ -303,12 +317,25 @@
"LabelEmailSettingsTestAddress": "Test-Adresse", "LabelEmailSettingsTestAddress": "Test-Adresse",
"LabelEmbeddedCover": "Eingebettetes Cover", "LabelEmbeddedCover": "Eingebettetes Cover",
"LabelEnable": "Aktivieren", "LabelEnable": "Aktivieren",
"LabelEncodingBackupLocation": "Eine Sicherungskopie der originalen Audiodateien wird gespeichert in:",
"LabelEncodingChaptersNotEmbedded": "Kapitel sind in mehrspurigen Hörbüchern nicht eingebettet.",
"LabelEncodingClearItemCache": "Stelle sicher, dass der Cache regelmäßig geleert wird.",
"LabelEncodingFinishedM4B": "Die fertige M4B-Datei wird im Hörbuch-Ordner unter folgendem Pfad abgelegt:",
"LabelEncodingInfoEmbedded": "Metadaten werden in die Audiodateien innerhalb des Audiobook Ordners eingebunden.",
"LabelEncodingStartedNavigation": "Sobald die Aufgabe gestartet ist, kann die Seite verlassen werden.",
"LabelEncodingTimeWarning": "Kodierung kann bis zu 30 Minuten dauern.",
"LabelEncodingWarningAdvancedSettings": "Achtung: Ändere diese Einstellungen nur, wenn du dich mit ffmpeg Kodierung auskennst.",
"LabelEncodingWatcherDisabled": "Wenn der Watcher deaktiviert ist musst du das Hörbuch danach erneut scannen.",
"LabelEnd": "Ende", "LabelEnd": "Ende",
"LabelEndOfChapter": "Ende des Kapitels", "LabelEndOfChapter": "Ende des Kapitels",
"LabelEpisode": "Episode", "LabelEpisode": "Episode",
"LabelEpisodeNotLinkedToRssFeed": "Episode nicht mit RSS-Feed verknüpft",
"LabelEpisodeNumber": "Episode #{0}",
"LabelEpisodeTitle": "Episodentitel", "LabelEpisodeTitle": "Episodentitel",
"LabelEpisodeType": "Episodentyp", "LabelEpisodeType": "Episodentyp",
"LabelEpisodeUrlFromRssFeed": "Episoden URL vom RSS-Feed",
"LabelEpisodes": "Episoden", "LabelEpisodes": "Episoden",
"LabelEpisodic": "Episodisch",
"LabelExample": "Beispiel", "LabelExample": "Beispiel",
"LabelExpandSeries": "Serie ausklappen", "LabelExpandSeries": "Serie ausklappen",
"LabelExpandSubSeries": "Unterserie ausklappen", "LabelExpandSubSeries": "Unterserie ausklappen",
@@ -336,6 +363,7 @@
"LabelFontScale": "Schriftgröße", "LabelFontScale": "Schriftgröße",
"LabelFontStrikethrough": "Durchgestrichen", "LabelFontStrikethrough": "Durchgestrichen",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelFull": "Voll",
"LabelGenre": "Kategorie", "LabelGenre": "Kategorie",
"LabelGenres": "Kategorien", "LabelGenres": "Kategorien",
"LabelHardDeleteFile": "Datei dauerhaft löschen", "LabelHardDeleteFile": "Datei dauerhaft löschen",
@@ -391,6 +419,10 @@
"LabelLowestPriority": "Niedrigste Priorität", "LabelLowestPriority": "Niedrigste Priorität",
"LabelMatchExistingUsersBy": "Zuordnen existierender Benutzer mit", "LabelMatchExistingUsersBy": "Zuordnen existierender Benutzer mit",
"LabelMatchExistingUsersByDescription": "Wird zum Verbinden vorhandener Benutzer verwendet. Sobald die Verbindung hergestellt ist, wird den Benutzern eine eindeutige ID vom SSO-Anbieter zugeordnet", "LabelMatchExistingUsersByDescription": "Wird zum Verbinden vorhandener Benutzer verwendet. Sobald die Verbindung hergestellt ist, wird den Benutzern eine eindeutige ID vom SSO-Anbieter zugeordnet",
"LabelMaxEpisodesToDownload": "Max. Anzahl an Episoden zum Herunterladen, 0 für unbegrenzte Episoden.",
"LabelMaxEpisodesToDownloadPerCheck": "Max. Anzahl neuer Episoden zum Herunterladen pro Abfrage",
"LabelMaxEpisodesToKeep": "Max. Anzahl zu behaltender Episoden",
"LabelMaxEpisodesToKeepHelp": "0 setzt keine Begrenzung. Wenn eine neue Episode automatisch heruntergeladen wird, wird die älteste Episode gelöscht, wenn du mehr als X Episoden gespeichert hast. Es wird nur eine Episode pro neuem Download gelöscht.",
"LabelMediaPlayer": "Mediaplayer", "LabelMediaPlayer": "Mediaplayer",
"LabelMediaType": "Medientyp", "LabelMediaType": "Medientyp",
"LabelMetaTag": "Meta Schlagwort", "LabelMetaTag": "Meta Schlagwort",
@@ -436,12 +468,14 @@
"LabelOpenIDGroupClaimDescription": "Name des OpenID-Claims, der eine Liste der Benutzergruppen enthält. Wird häufig als <code>groups</code> bezeichnet. <b>Wenn konfiguriert</b>, wird die Anwendung automatisch Rollen basierend auf den Gruppenmitgliedschaften des Benutzers zuweisen, vorausgesetzt, dass diese Gruppen im Claim als 'admin', 'user' oder 'guest' benannt sind (Groß/Kleinschreibung ist irrelevant). Der Claim eine Liste sein, und wenn ein Benutzer mehreren Gruppen angehört, wird die Anwendung die Rolle zuordnen, die dem höchsten Zugriffslevel entspricht. Wenn keine Gruppe übereinstimmt, wird der Zugang verweigert.", "LabelOpenIDGroupClaimDescription": "Name des OpenID-Claims, der eine Liste der Benutzergruppen enthält. Wird häufig als <code>groups</code> bezeichnet. <b>Wenn konfiguriert</b>, wird die Anwendung automatisch Rollen basierend auf den Gruppenmitgliedschaften des Benutzers zuweisen, vorausgesetzt, dass diese Gruppen im Claim als 'admin', 'user' oder 'guest' benannt sind (Groß/Kleinschreibung ist irrelevant). Der Claim eine Liste sein, und wenn ein Benutzer mehreren Gruppen angehört, wird die Anwendung die Rolle zuordnen, die dem höchsten Zugriffslevel entspricht. Wenn keine Gruppe übereinstimmt, wird der Zugang verweigert.",
"LabelOpenRSSFeed": "Öffne RSS-Feed", "LabelOpenRSSFeed": "Öffne RSS-Feed",
"LabelOverwrite": "Überschreiben", "LabelOverwrite": "Überschreiben",
"LabelPaginationPageXOfY": "Seite {0} von {1}",
"LabelPassword": "Passwort", "LabelPassword": "Passwort",
"LabelPath": "Pfad", "LabelPath": "Pfad",
"LabelPermanent": "Dauerhaft", "LabelPermanent": "Dauerhaft",
"LabelPermissionsAccessAllLibraries": "Zugriff auf alle Bibliotheken", "LabelPermissionsAccessAllLibraries": "Zugriff auf alle Bibliotheken",
"LabelPermissionsAccessAllTags": "Zugriff auf alle Schlagwörter", "LabelPermissionsAccessAllTags": "Zugriff auf alle Schlagwörter",
"LabelPermissionsAccessExplicitContent": "Zugriff auf explizite (alterbeschränkte) Inhalte", "LabelPermissionsAccessExplicitContent": "Zugriff auf explizite (alterbeschränkte) Inhalte",
"LabelPermissionsCreateEreader": "Kann E-Reader erstellen",
"LabelPermissionsDelete": "Darf Löschen", "LabelPermissionsDelete": "Darf Löschen",
"LabelPermissionsDownload": "Herunterladen", "LabelPermissionsDownload": "Herunterladen",
"LabelPermissionsUpdate": "Aktualisieren", "LabelPermissionsUpdate": "Aktualisieren",
@@ -486,24 +520,32 @@
"LabelRedo": "Wiederholen", "LabelRedo": "Wiederholen",
"LabelRegion": "Region", "LabelRegion": "Region",
"LabelReleaseDate": "Veröffentlichungsdatum", "LabelReleaseDate": "Veröffentlichungsdatum",
"LabelRemoveAllMetadataAbs": "Alle metadata.abs Dateien löschen",
"LabelRemoveAllMetadataJson": "Alle metadata.json Dateien löschen",
"LabelRemoveCover": "Entferne Titelbild", "LabelRemoveCover": "Entferne Titelbild",
"LabelRemoveMetadataFile": "Metadaten-Dateien in Bibliotheksordnern löschen",
"LabelRemoveMetadataFileHelp": "Alle metadata.json und metadata.abs Dateien aus den Ordnern {0} löschen.",
"LabelRowsPerPage": "Zeilen pro Seite", "LabelRowsPerPage": "Zeilen pro Seite",
"LabelSearchTerm": "Begriff suchen", "LabelSearchTerm": "Begriff suchen",
"LabelSearchTitle": "Titel suchen", "LabelSearchTitle": "Titel suchen",
"LabelSearchTitleOrASIN": "Titel oder ASIN suchen", "LabelSearchTitleOrASIN": "Titel oder ASIN suchen",
"LabelSeason": "Staffel", "LabelSeason": "Staffel",
"LabelSeasonNumber": "Staffel #{0}",
"LabelSelectAll": "Alles auswählen", "LabelSelectAll": "Alles auswählen",
"LabelSelectAllEpisodes": "Alle Episoden auswählen", "LabelSelectAllEpisodes": "Alle Episoden auswählen",
"LabelSelectEpisodesShowing": "{0} ausgewählte Episoden werden angezeigt", "LabelSelectEpisodesShowing": "{0} ausgewählte Episoden werden angezeigt",
"LabelSelectUsers": "Benutzer auswählen", "LabelSelectUsers": "Benutzer auswählen",
"LabelSendEbookToDevice": "E-Buch senden an …", "LabelSendEbookToDevice": "E-Buch senden an …",
"LabelSequence": "Reihenfolge", "LabelSequence": "Reihenfolge",
"LabelSerial": "fortlaufend",
"LabelSeries": "Serien", "LabelSeries": "Serien",
"LabelSeriesName": "Serienname", "LabelSeriesName": "Serienname",
"LabelSeriesProgress": "Serienfortschritt", "LabelSeriesProgress": "Serienfortschritt",
"LabelServerLogLevel": "Server Log Level",
"LabelServerYearReview": "Server Jahr in Übersicht ({0})", "LabelServerYearReview": "Server Jahr in Übersicht ({0})",
"LabelSetEbookAsPrimary": "Als Hauptbuch setzen", "LabelSetEbookAsPrimary": "Als Hauptbuch setzen",
"LabelSetEbookAsSupplementary": "Als Ergänzung setzen", "LabelSetEbookAsSupplementary": "Als Ergänzung setzen",
"LabelSettingsAllowIframe": "Einbetten in einem iFrame erlauben",
"LabelSettingsAudiobooksOnly": "Nur Hörbücher", "LabelSettingsAudiobooksOnly": "Nur Hörbücher",
"LabelSettingsAudiobooksOnlyHelp": "Wenn du diese Einstellung aktivierst, werden E-Buch-Dateien ignoriert, es sei denn, sie befinden sich in einem Hörbuchordner. In diesem Fall werden sie als zusätzliche E-Bücher festgelegt", "LabelSettingsAudiobooksOnlyHelp": "Wenn du diese Einstellung aktivierst, werden E-Buch-Dateien ignoriert, es sei denn, sie befinden sich in einem Hörbuchordner. In diesem Fall werden sie als zusätzliche E-Bücher festgelegt",
"LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden", "LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden",
@@ -525,6 +567,9 @@
"LabelSettingsHideSingleBookSeriesHelp": "Serien, die nur ein einzelnes Buch enthalten, werden auf der Startseite und in der Serienansicht ausgeblendet.", "LabelSettingsHideSingleBookSeriesHelp": "Serien, die nur ein einzelnes Buch enthalten, werden auf der Startseite und in der Serienansicht ausgeblendet.",
"LabelSettingsHomePageBookshelfView": "Startseite verwendet die Bücherregalansicht", "LabelSettingsHomePageBookshelfView": "Startseite verwendet die Bücherregalansicht",
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht", "LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
"LabelSettingsLibraryMarkAsFinishedPercentComplete": "In Prozent gehört größer als",
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Verbleibende Zeit ist weniger als (Sekunden)",
"LabelSettingsLibraryMarkAsFinishedWhen": "Markiere Mediendateien als fertig, wenn",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Überspringe vorherige Bücher in fortführender Serie", "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Überspringe vorherige Bücher in fortführender Serie",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Die Startseite von \"Fortführende Serien\" zeigt das erste noch nicht begonnene Buch in Serien an, die mindestens ein Buch abgeschlossen und keine Bücher begonnen haben. Wenn diese Einstellung aktiviert wird, werden Serien ab dem letzten abgeschlossenen Buch fortgesetzt und nicht ab dem ersten nicht begonnenen Buch.", "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Die Startseite von \"Fortführende Serien\" zeigt das erste noch nicht begonnene Buch in Serien an, die mindestens ein Buch abgeschlossen und keine Bücher begonnen haben. Wenn diese Einstellung aktiviert wird, werden Serien ab dem letzten abgeschlossenen Buch fortgesetzt und nicht ab dem ersten nicht begonnenen Buch.",
"LabelSettingsParseSubtitles": "Analysiere Untertitel", "LabelSettingsParseSubtitles": "Analysiere Untertitel",
@@ -543,7 +588,7 @@
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet", "LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet",
"LabelSettingsTimeFormat": "Zeitformat", "LabelSettingsTimeFormat": "Zeitformat",
"LabelShare": "Freigeben", "LabelShare": "Freigeben",
"LabelShareOpen": "Freigabe", "LabelShareOpen": "Freigeben",
"LabelShareURL": "Freigabe URL", "LabelShareURL": "Freigabe URL",
"LabelShowAll": "Alles anzeigen", "LabelShowAll": "Alles anzeigen",
"LabelShowSeconds": "Zeige Sekunden", "LabelShowSeconds": "Zeige Sekunden",
@@ -551,6 +596,8 @@
"LabelSize": "Größe", "LabelSize": "Größe",
"LabelSleepTimer": "Schlummerfunktion", "LabelSleepTimer": "Schlummerfunktion",
"LabelSlug": "URL Teil", "LabelSlug": "URL Teil",
"LabelSortAscending": "Aufsteigend",
"LabelSortDescending": "Absteigend",
"LabelStart": "Start", "LabelStart": "Start",
"LabelStartTime": "Startzeit", "LabelStartTime": "Startzeit",
"LabelStarted": "Gestartet", "LabelStarted": "Gestartet",
@@ -569,7 +616,7 @@
"LabelStatsMinutesListening": "Gehörte Minuten", "LabelStatsMinutesListening": "Gehörte Minuten",
"LabelStatsOverallDays": "Gesamte Tage", "LabelStatsOverallDays": "Gesamte Tage",
"LabelStatsOverallHours": "Gesamte Stunden", "LabelStatsOverallHours": "Gesamte Stunden",
"LabelStatsWeekListening": "7-Tage-Durchschnitt", "LabelStatsWeekListening": "Wochenhördauer",
"LabelSubtitle": "Untertitel", "LabelSubtitle": "Untertitel",
"LabelSupportedFileTypes": "Unterstützte Dateitypen", "LabelSupportedFileTypes": "Unterstützte Dateitypen",
"LabelTag": "Schlagwort", "LabelTag": "Schlagwort",
@@ -589,6 +636,7 @@
"LabelTimeDurationXMinutes": "{0} Minuten", "LabelTimeDurationXMinutes": "{0} Minuten",
"LabelTimeDurationXSeconds": "{0} Sekunden", "LabelTimeDurationXSeconds": "{0} Sekunden",
"LabelTimeInMinutes": "Zeit in Minuten", "LabelTimeInMinutes": "Zeit in Minuten",
"LabelTimeLeft": "{0} verbleibend",
"LabelTimeListened": "Gehörte Zeit", "LabelTimeListened": "Gehörte Zeit",
"LabelTimeListenedToday": "Heute gehörte Zeit", "LabelTimeListenedToday": "Heute gehörte Zeit",
"LabelTimeRemaining": "{0} verbleibend", "LabelTimeRemaining": "{0} verbleibend",
@@ -596,6 +644,7 @@
"LabelTitle": "Titel", "LabelTitle": "Titel",
"LabelToolsEmbedMetadata": "Metadaten einbetten", "LabelToolsEmbedMetadata": "Metadaten einbetten",
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.", "LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.",
"LabelToolsM4bEncoder": "M4B Kodierer",
"LabelToolsMakeM4b": "M4B-Datei erstellen", "LabelToolsMakeM4b": "M4B-Datei erstellen",
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Datei (Endung \".m4b\") welche mehrere mp3-Dateien in einer einzigen Datei inkl. derer Metadaten (Beschreibung, Titelbild, Kapitel, ...) zusammenfasst. M4B-Datei können darüber hinaus Lesezeichen speichern und mit einem Abspielschutz (Passwort) versehen werden.", "LabelToolsMakeM4bDescription": "Erstellt eine M4B-Datei (Endung \".m4b\") welche mehrere mp3-Dateien in einer einzigen Datei inkl. derer Metadaten (Beschreibung, Titelbild, Kapitel, ...) zusammenfasst. M4B-Datei können darüber hinaus Lesezeichen speichern und mit einem Abspielschutz (Passwort) versehen werden.",
"LabelToolsSplitM4b": "M4B in MP3s aufteilen", "LabelToolsSplitM4b": "M4B in MP3s aufteilen",
@@ -608,6 +657,7 @@
"LabelTracksMultiTrack": "Mehrfachdatei", "LabelTracksMultiTrack": "Mehrfachdatei",
"LabelTracksNone": "Keine Dateien", "LabelTracksNone": "Keine Dateien",
"LabelTracksSingleTrack": "Einzeldatei", "LabelTracksSingleTrack": "Einzeldatei",
"LabelTrailer": "Vorschau",
"LabelType": "Typ", "LabelType": "Typ",
"LabelUnabridged": "Ungekürzt", "LabelUnabridged": "Ungekürzt",
"LabelUndo": "Rückgängig machen", "LabelUndo": "Rückgängig machen",
@@ -619,10 +669,13 @@
"LabelUpdateDetailsHelp": "Erlaube das Überschreiben bestehender Details für die ausgewählten Hörbücher, wenn eine Übereinstimmung gefunden wird", "LabelUpdateDetailsHelp": "Erlaube das Überschreiben bestehender Details für die ausgewählten Hörbücher, wenn eine Übereinstimmung gefunden wird",
"LabelUpdatedAt": "Aktualisiert am", "LabelUpdatedAt": "Aktualisiert am",
"LabelUploaderDragAndDrop": "Ziehen und Ablegen von Dateien oder Ordnern", "LabelUploaderDragAndDrop": "Ziehen und Ablegen von Dateien oder Ordnern",
"LabelUploaderDragAndDropFilesOnly": "Dateien per Drag & Drop hierher ziehen",
"LabelUploaderDropFiles": "Dateien löschen", "LabelUploaderDropFiles": "Dateien löschen",
"LabelUploaderItemFetchMetadataHelp": "Automatisches Aktualisieren von Titel, Autor und Serie", "LabelUploaderItemFetchMetadataHelp": "Automatisches Aktualisieren von Titel, Autor und Serie",
"LabelUseAdvancedOptions": "Nutze Erweiterte Optionen",
"LabelUseChapterTrack": "Kapiteldatei verwenden", "LabelUseChapterTrack": "Kapiteldatei verwenden",
"LabelUseFullTrack": "Gesamte Datei verwenden", "LabelUseFullTrack": "Gesamte Datei verwenden",
"LabelUseZeroForUnlimited": "0 für unbegrenzt",
"LabelUser": "Benutzer", "LabelUser": "Benutzer",
"LabelUsername": "Benutzername", "LabelUsername": "Benutzername",
"LabelValue": "Wert", "LabelValue": "Wert",
@@ -632,11 +685,13 @@
"LabelViewPlayerSettings": "Zeige player Einstellungen", "LabelViewPlayerSettings": "Zeige player Einstellungen",
"LabelViewQueue": "Player-Warteschlange anzeigen", "LabelViewQueue": "Player-Warteschlange anzeigen",
"LabelVolume": "Lautstärke", "LabelVolume": "Lautstärke",
"LabelWebRedirectURLsDescription": "Autorisiere diese URLs bei deinem OAuth-Anbieter, um die Weiterleitung zurück zur Webanwendung nach dem Login zu ermöglichen:",
"LabelWebRedirectURLsSubfolder": "Unterordner für Weiterleitung-URLs",
"LabelWeekdaysToRun": "Wochentage für die Ausführung", "LabelWeekdaysToRun": "Wochentage für die Ausführung",
"LabelXBooks": "{0} Bücher", "LabelXBooks": "{0} Bücher",
"LabelXItems": "{0} Medien", "LabelXItems": "{0} Medien",
"LabelYearReviewHide": "Verstecke Jahr in Übersicht", "LabelYearReviewHide": "Jahresrückblick verbergen",
"LabelYearReviewShow": "Zeige Jahr in Übersicht", "LabelYearReviewShow": "Jahresrückblick anzeigen",
"LabelYourAudiobookDuration": "Laufzeit deines Mediums", "LabelYourAudiobookDuration": "Laufzeit deines Mediums",
"LabelYourBookmarks": "Lesezeichen", "LabelYourBookmarks": "Lesezeichen",
"LabelYourPlaylists": "Eigene Wiedergabelisten", "LabelYourPlaylists": "Eigene Wiedergabelisten",
@@ -669,6 +724,7 @@
"MessageConfirmDeleteMetadataProvider": "Möchtest du den benutzerdefinierten Metadatenanbieter \"{0}\" wirklich löschen?", "MessageConfirmDeleteMetadataProvider": "Möchtest du den benutzerdefinierten Metadatenanbieter \"{0}\" wirklich löschen?",
"MessageConfirmDeleteNotification": "Möchtest du diese Benachrichtigung wirklich löschen?", "MessageConfirmDeleteNotification": "Möchtest du diese Benachrichtigung wirklich löschen?",
"MessageConfirmDeleteSession": "Sitzung wird gelöscht! Bist du dir sicher?", "MessageConfirmDeleteSession": "Sitzung wird gelöscht! Bist du dir sicher?",
"MessageConfirmEmbedMetadataInAudioFiles": "Bist du dir sicher, dass die Metadaten in {0} Audiodateien eingebettet werden sollen?",
"MessageConfirmForceReScan": "Scanvorgang erzwingen! Bist du dir sicher?", "MessageConfirmForceReScan": "Scanvorgang erzwingen! Bist du dir sicher?",
"MessageConfirmMarkAllEpisodesFinished": "Alle Episoden werden als abgeschlossen markiert! Bist du dir sicher?", "MessageConfirmMarkAllEpisodesFinished": "Alle Episoden werden als abgeschlossen markiert! Bist du dir sicher?",
"MessageConfirmMarkAllEpisodesNotFinished": "Alle Episoden werden als nicht abgeschlossen markiert! Bist du dir sicher?", "MessageConfirmMarkAllEpisodesNotFinished": "Alle Episoden werden als nicht abgeschlossen markiert! Bist du dir sicher?",
@@ -680,6 +736,7 @@
"MessageConfirmPurgeCache": "Cache leeren wird das ganze Verzeichnis <code>/metadata/cache</code> löschen. <br /><br />Bist du dir sicher, dass das Cache Verzeichnis gelöscht werden soll?", "MessageConfirmPurgeCache": "Cache leeren wird das ganze Verzeichnis <code>/metadata/cache</code> löschen. <br /><br />Bist du dir sicher, dass das Cache Verzeichnis gelöscht werden soll?",
"MessageConfirmPurgeItemsCache": "Durch Elementcache leeren wird das gesamte Verzeichnis unter <code>/metadata/cache/items</code> gelöscht.<br />Bist du dir sicher?", "MessageConfirmPurgeItemsCache": "Durch Elementcache leeren wird das gesamte Verzeichnis unter <code>/metadata/cache/items</code> gelöscht.<br />Bist du dir sicher?",
"MessageConfirmQuickEmbed": "Warnung! Audiodateien werden bei der Schnelleinbettung nicht gesichert! Achte darauf, dass du eine Sicherungskopie der Audiodateien besitzt. <br><br>Möchtest du fortfahren?", "MessageConfirmQuickEmbed": "Warnung! Audiodateien werden bei der Schnelleinbettung nicht gesichert! Achte darauf, dass du eine Sicherungskopie der Audiodateien besitzt. <br><br>Möchtest du fortfahren?",
"MessageConfirmQuickMatchEpisodes": "Schnellabgleich von Episoden überschreibt deren Details, wenn ein passender Eintrag gefunden wurde, wird aber nur auf bisher unbearbeitete Episoden angewendet. Wirklich fortfahren?",
"MessageConfirmReScanLibraryItems": "{0} Elemente werden erneut gescannt! Bist du dir sicher?", "MessageConfirmReScanLibraryItems": "{0} Elemente werden erneut gescannt! Bist du dir sicher?",
"MessageConfirmRemoveAllChapters": "Alle Kapitel werden entfernt! Bist du dir sicher?", "MessageConfirmRemoveAllChapters": "Alle Kapitel werden entfernt! Bist du dir sicher?",
"MessageConfirmRemoveAuthor": "Autor \"{0}\" wird enfernt! Bist du dir sicher?", "MessageConfirmRemoveAuthor": "Autor \"{0}\" wird enfernt! Bist du dir sicher?",
@@ -687,6 +744,7 @@
"MessageConfirmRemoveEpisode": "Episode \"{0}\" wird entfernt! Bist du dir sicher?", "MessageConfirmRemoveEpisode": "Episode \"{0}\" wird entfernt! Bist du dir sicher?",
"MessageConfirmRemoveEpisodes": "{0} Episoden werden entfernt! Bist du dir sicher?", "MessageConfirmRemoveEpisodes": "{0} Episoden werden entfernt! Bist du dir sicher?",
"MessageConfirmRemoveListeningSessions": "Bist du dir sicher, dass du {0} Hörsitzungen enfernen möchtest?", "MessageConfirmRemoveListeningSessions": "Bist du dir sicher, dass du {0} Hörsitzungen enfernen möchtest?",
"MessageConfirmRemoveMetadataFiles": "Bist du sicher, dass du alle metadata.{0} Dateien in deinen Bibliotheksordnern löschen willst?",
"MessageConfirmRemoveNarrator": "Erzähler \"{0}\" wird entfernt! Bist du dir sicher?", "MessageConfirmRemoveNarrator": "Erzähler \"{0}\" wird entfernt! Bist du dir sicher?",
"MessageConfirmRemovePlaylist": "Wiedergabeliste \"{0}\" wird entfernt! Bist du dir sicher?", "MessageConfirmRemovePlaylist": "Wiedergabeliste \"{0}\" wird entfernt! Bist du dir sicher?",
"MessageConfirmRenameGenre": "Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Bist du dir sicher?", "MessageConfirmRenameGenre": "Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Bist du dir sicher?",
@@ -702,6 +760,7 @@
"MessageDragFilesIntoTrackOrder": "Verschiebe die Dateien in die richtige Reihenfolge", "MessageDragFilesIntoTrackOrder": "Verschiebe die Dateien in die richtige Reihenfolge",
"MessageEmbedFailed": "Einbetten fehlgeschlagen!", "MessageEmbedFailed": "Einbetten fehlgeschlagen!",
"MessageEmbedFinished": "Einbettung abgeschlossen!", "MessageEmbedFinished": "Einbettung abgeschlossen!",
"MessageEmbedQueue": "Eingereiht zum einbinden von Metadaten ({0} in Warteschlange)",
"MessageEpisodesQueuedForDownload": "{0} Episode(n) in der Warteschlange zum Herunterladen", "MessageEpisodesQueuedForDownload": "{0} Episode(n) in der Warteschlange zum Herunterladen",
"MessageEreaderDevices": "Um die Zustellung von E-Büchern sicherzustellen, musst du eventuell die oben genannte E-Mail-Adresse als gültigen Absender für jedes unten aufgeführte Gerät hinzufügen.", "MessageEreaderDevices": "Um die Zustellung von E-Büchern sicherzustellen, musst du eventuell die oben genannte E-Mail-Adresse als gültigen Absender für jedes unten aufgeführte Gerät hinzufügen.",
"MessageFeedURLWillBe": "Feed-URL wird {0} sein", "MessageFeedURLWillBe": "Feed-URL wird {0} sein",
@@ -746,6 +805,7 @@
"MessageNoLogs": "Keine Protokolle", "MessageNoLogs": "Keine Protokolle",
"MessageNoMediaProgress": "Kein Medienfortschritt", "MessageNoMediaProgress": "Kein Medienfortschritt",
"MessageNoNotifications": "Keine Benachrichtigungen", "MessageNoNotifications": "Keine Benachrichtigungen",
"MessageNoPodcastFeed": "Ungültiger Podcast: Kein Feed",
"MessageNoPodcastsFound": "Keine Podcasts gefunden", "MessageNoPodcastsFound": "Keine Podcasts gefunden",
"MessageNoResults": "Keine Ergebnisse", "MessageNoResults": "Keine Ergebnisse",
"MessageNoSearchResultsFor": "Keine Suchergebnisse für \"{0}\"", "MessageNoSearchResultsFor": "Keine Suchergebnisse für \"{0}\"",
@@ -762,6 +822,10 @@
"MessagePlaylistCreateFromCollection": "Erstelle eine Wiedergabeliste aus der Sammlung", "MessagePlaylistCreateFromCollection": "Erstelle eine Wiedergabeliste aus der Sammlung",
"MessagePleaseWait": "Bitte warten...", "MessagePleaseWait": "Bitte warten...",
"MessagePodcastHasNoRSSFeedForMatching": "Der Podcast hat keine RSS-Feed-Url welche für den Online-Abgleich verwendet werden kann", "MessagePodcastHasNoRSSFeedForMatching": "Der Podcast hat keine RSS-Feed-Url welche für den Online-Abgleich verwendet werden kann",
"MessagePodcastSearchField": "Suchbegriff oder RSS-Feed URL eingeben",
"MessageQuickEmbedInProgress": "Schnellabgleich läuft",
"MessageQuickEmbedQueue": "In Warteschlange für Schnelles einbinden ({0} eingereiht)",
"MessageQuickMatchAllEpisodes": "Quick Match aller Episoden",
"MessageQuickMatchDescription": "Füllt leere Details und Titelbilder mit dem ersten Treffer aus '{0}'. Überschreibt keine Details, es sei denn, die Server-Einstellung \"Passende Metadaten bevorzugen\" ist aktiviert.", "MessageQuickMatchDescription": "Füllt leere Details und Titelbilder mit dem ersten Treffer aus '{0}'. Überschreibt keine Details, es sei denn, die Server-Einstellung \"Passende Metadaten bevorzugen\" ist aktiviert.",
"MessageRemoveChapter": "Kapitel entfernen", "MessageRemoveChapter": "Kapitel entfernen",
"MessageRemoveEpisodes": "Entferne {0} Episode(n)", "MessageRemoveEpisodes": "Entferne {0} Episode(n)",
@@ -777,7 +841,7 @@
"MessageSetChaptersFromTracksDescription": "Kaitelerstellung basiert auf den existierenden einzelnen Audiodateien. Pro existierende Audiodatei wird 1 Kapitel erstellt, wobei deren Kapitelname aus dem Audiodateinamen extrahiert wird", "MessageSetChaptersFromTracksDescription": "Kaitelerstellung basiert auf den existierenden einzelnen Audiodateien. Pro existierende Audiodatei wird 1 Kapitel erstellt, wobei deren Kapitelname aus dem Audiodateinamen extrahiert wird",
"MessageShareExpirationWillBe": "Läuft am <strong>{0}</strong> ab", "MessageShareExpirationWillBe": "Läuft am <strong>{0}</strong> ab",
"MessageShareExpiresIn": "Läuft in {0} ab", "MessageShareExpiresIn": "Läuft in {0} ab",
"MessageShareURLWillBe": "Der Freigabe Link wird <strong>{0}</strong> sein.", "MessageShareURLWillBe": "Der Freigabe Link wird <strong>{0}</strong> sein",
"MessageStartPlaybackAtTime": "Start der Wiedergabe für \"{0}\" bei {1}?", "MessageStartPlaybackAtTime": "Start der Wiedergabe für \"{0}\" bei {1}?",
"MessageTaskAudioFileNotWritable": "Die Audiodatei \"{0}\" ist schreibgeschützt", "MessageTaskAudioFileNotWritable": "Die Audiodatei \"{0}\" ist schreibgeschützt",
"MessageTaskCanceledByUser": "Aufgabe vom Benutzer abgebrochen", "MessageTaskCanceledByUser": "Aufgabe vom Benutzer abgebrochen",
@@ -804,6 +868,9 @@
"MessageTaskOpmlImportFeedPodcastExists": "Der Podcast ist bereits im Pfad vorhanden", "MessageTaskOpmlImportFeedPodcastExists": "Der Podcast ist bereits im Pfad vorhanden",
"MessageTaskOpmlImportFeedPodcastFailed": "Erstellen des Podcasts fehlgeschlagen", "MessageTaskOpmlImportFeedPodcastFailed": "Erstellen des Podcasts fehlgeschlagen",
"MessageTaskOpmlImportFinished": "{0} Podcasts hinzugefügt", "MessageTaskOpmlImportFinished": "{0} Podcasts hinzugefügt",
"MessageTaskOpmlParseFailed": "Fehler beim lesen der OPML Datei",
"MessageTaskOpmlParseFastFail": "Ungültie OPML Datei: <opml> ODER <outline> tag wurde nicht gefunden",
"MessageTaskOpmlParseNoneFound": "Keine feeds in der OPML Datei gefunden",
"MessageTaskScanItemsAdded": "{0} hinzugefügt", "MessageTaskScanItemsAdded": "{0} hinzugefügt",
"MessageTaskScanItemsMissing": "{0} fehlend", "MessageTaskScanItemsMissing": "{0} fehlend",
"MessageTaskScanItemsUpdated": "{0} aktualisiert", "MessageTaskScanItemsUpdated": "{0} aktualisiert",
@@ -828,6 +895,10 @@
"NoteUploaderFoldersWithMediaFiles": "Ordner mit Mediendateien werden als separate Bibliothekselemente behandelt.", "NoteUploaderFoldersWithMediaFiles": "Ordner mit Mediendateien werden als separate Bibliothekselemente behandelt.",
"NoteUploaderOnlyAudioFiles": "Wenn du nur Audiodateien hochlädst, wird jede Audiodatei als ein separates Medium behandelt.", "NoteUploaderOnlyAudioFiles": "Wenn du nur Audiodateien hochlädst, wird jede Audiodatei als ein separates Medium behandelt.",
"NoteUploaderUnsupportedFiles": "Nicht unterstützte Dateien werden ignoriert. Bei der Auswahl oder dem Löschen eines Ordners werden andere Dateien, die sich nicht in einem Elementordner befinden, ignoriert.", "NoteUploaderUnsupportedFiles": "Nicht unterstützte Dateien werden ignoriert. Bei der Auswahl oder dem Löschen eines Ordners werden andere Dateien, die sich nicht in einem Elementordner befinden, ignoriert.",
"NotificationOnBackupCompletedDescription": "Wird ausgeführt wenn ein Backup erstellt wurde",
"NotificationOnBackupFailedDescription": "Wird ausgeführt wenn ein Backup fehlgeschlagen ist",
"NotificationOnEpisodeDownloadedDescription": "Wird ausgeführt wenn eine Podcast Folge automatisch heruntergeladen wird",
"NotificationOnTestDescription": "Wird ausgeführt wenn das Benachrichtigungssystem getestet wird",
"PlaceholderNewCollection": "Neuer Sammlungsname", "PlaceholderNewCollection": "Neuer Sammlungsname",
"PlaceholderNewFolderPath": "Neuer Ordnerpfad", "PlaceholderNewFolderPath": "Neuer Ordnerpfad",
"PlaceholderNewPlaylist": "Neuer Wiedergabelistenname", "PlaceholderNewPlaylist": "Neuer Wiedergabelistenname",
@@ -853,6 +924,7 @@
"StatsYearInReview": "DAS JAHR IM RÜCKBLICK", "StatsYearInReview": "DAS JAHR IM RÜCKBLICK",
"ToastAccountUpdateSuccess": "Konto aktualisiert", "ToastAccountUpdateSuccess": "Konto aktualisiert",
"ToastAppriseUrlRequired": "Eine Apprise-URL ist notwendig", "ToastAppriseUrlRequired": "Eine Apprise-URL ist notwendig",
"ToastAsinRequired": "ASIN ist erforderlich",
"ToastAuthorImageRemoveSuccess": "Autorenbild entfernt", "ToastAuthorImageRemoveSuccess": "Autorenbild entfernt",
"ToastAuthorNotFound": "Autor \"{0}\" nicht gefunden", "ToastAuthorNotFound": "Autor \"{0}\" nicht gefunden",
"ToastAuthorRemoveSuccess": "Autor entfernt", "ToastAuthorRemoveSuccess": "Autor entfernt",
@@ -872,6 +944,8 @@
"ToastBackupUploadSuccess": "Sicherung hochgeladen", "ToastBackupUploadSuccess": "Sicherung hochgeladen",
"ToastBatchDeleteFailed": "Batch-Löschen fehlgeschlagen", "ToastBatchDeleteFailed": "Batch-Löschen fehlgeschlagen",
"ToastBatchDeleteSuccess": "Batch-Löschung erfolgreich", "ToastBatchDeleteSuccess": "Batch-Löschung erfolgreich",
"ToastBatchQuickMatchFailed": "Batch-Schnellabgleich fehlgeschlagen!",
"ToastBatchQuickMatchStarted": "Batch-Schnellabgleich für {0} Bücher gestartet!",
"ToastBatchUpdateFailed": "Stapelaktualisierung fehlgeschlagen", "ToastBatchUpdateFailed": "Stapelaktualisierung fehlgeschlagen",
"ToastBatchUpdateSuccess": "Stapelaktualisierung erfolgreich", "ToastBatchUpdateSuccess": "Stapelaktualisierung erfolgreich",
"ToastBookmarkCreateFailed": "Lesezeichen konnte nicht erstellt werden", "ToastBookmarkCreateFailed": "Lesezeichen konnte nicht erstellt werden",
@@ -883,6 +957,7 @@
"ToastChaptersHaveErrors": "Kapitel sind fehlerhaft", "ToastChaptersHaveErrors": "Kapitel sind fehlerhaft",
"ToastChaptersMustHaveTitles": "Kapitel benötigen eindeutige Namen", "ToastChaptersMustHaveTitles": "Kapitel benötigen eindeutige Namen",
"ToastChaptersRemoved": "Kapitel entfernt", "ToastChaptersRemoved": "Kapitel entfernt",
"ToastChaptersUpdated": "Kapitel aktualisiert",
"ToastCollectionItemsAddFailed": "Das Hinzufügen von Element(en) zur Sammlung ist fehlgeschlagen", "ToastCollectionItemsAddFailed": "Das Hinzufügen von Element(en) zur Sammlung ist fehlgeschlagen",
"ToastCollectionItemsAddSuccess": "Element(e) erfolgreich zur Sammlung hinzugefügt", "ToastCollectionItemsAddSuccess": "Element(e) erfolgreich zur Sammlung hinzugefügt",
"ToastCollectionItemsRemoveSuccess": "Medien aus der Sammlung entfernt", "ToastCollectionItemsRemoveSuccess": "Medien aus der Sammlung entfernt",
@@ -900,11 +975,14 @@
"ToastEncodeCancelSucces": "Encoding abgebrochen", "ToastEncodeCancelSucces": "Encoding abgebrochen",
"ToastEpisodeDownloadQueueClearFailed": "Warteschlange konnte nicht gelöscht werden", "ToastEpisodeDownloadQueueClearFailed": "Warteschlange konnte nicht gelöscht werden",
"ToastEpisodeDownloadQueueClearSuccess": "Warteschlange für Episoden-Downloads gelöscht", "ToastEpisodeDownloadQueueClearSuccess": "Warteschlange für Episoden-Downloads gelöscht",
"ToastEpisodeUpdateSuccess": "{0} Episoden aktualisiert",
"ToastErrorCannotShare": "Das kann nicht nativ auf diesem Gerät freigegeben werden", "ToastErrorCannotShare": "Das kann nicht nativ auf diesem Gerät freigegeben werden",
"ToastFailedToLoadData": "Daten laden fehlgeschlagen", "ToastFailedToLoadData": "Daten laden fehlgeschlagen",
"ToastFailedToMatch": "Fehler beim Abgleich",
"ToastFailedToShare": "Fehler beim Teilen", "ToastFailedToShare": "Fehler beim Teilen",
"ToastFailedToUpdate": "Aktualisierung ist fehlgeschlagen", "ToastFailedToUpdate": "Aktualisierung ist fehlgeschlagen",
"ToastInvalidImageUrl": "Ungültiger Bild URL", "ToastInvalidImageUrl": "Ungültiger Bild URL",
"ToastInvalidMaxEpisodesToDownload": "Ungültige Max. Anzahl an Episoden zum Herunterladen",
"ToastInvalidUrl": "Ungültiger URL", "ToastInvalidUrl": "Ungültiger URL",
"ToastItemCoverUpdateSuccess": "Titelbild aktualisiert", "ToastItemCoverUpdateSuccess": "Titelbild aktualisiert",
"ToastItemDeletedFailed": "Fehler beim löschen des Artikels", "ToastItemDeletedFailed": "Fehler beim löschen des Artikels",
@@ -923,14 +1001,21 @@
"ToastLibraryScanStarted": "Bibliotheksscan gestartet", "ToastLibraryScanStarted": "Bibliotheksscan gestartet",
"ToastLibraryUpdateSuccess": "Bibliothek \"{0}\" aktualisiert", "ToastLibraryUpdateSuccess": "Bibliothek \"{0}\" aktualisiert",
"ToastMatchAllAuthorsFailed": "Nicht alle Autoren konnten zugeordnet werden", "ToastMatchAllAuthorsFailed": "Nicht alle Autoren konnten zugeordnet werden",
"ToastMetadataFilesRemovedError": "Fehler beim löschen von metadata.{0} Dateien",
"ToastMetadataFilesRemovedNoneFound": "Keine metadata.{0} Dateien in Bibliothek gefunden",
"ToastMetadataFilesRemovedNoneRemoved": "Keine metadata.{0} Dateien gelöscht",
"ToastMetadataFilesRemovedSuccess": "{0} metadata.{1} Datei(en) gelöscht",
"ToastMustHaveAtLeastOnePath": "Es muss mindestens ein Pfad angegeben werden",
"ToastNameEmailRequired": "Name und E-Mail sind erforderlich", "ToastNameEmailRequired": "Name und E-Mail sind erforderlich",
"ToastNameRequired": "Name ist erforderlich", "ToastNameRequired": "Name ist erforderlich",
"ToastNewEpisodesFound": "{0} neue Episoden gefunden",
"ToastNewUserCreatedFailed": "Fehler beim erstellen des Accounts: \"{ 0}\"", "ToastNewUserCreatedFailed": "Fehler beim erstellen des Accounts: \"{ 0}\"",
"ToastNewUserCreatedSuccess": "Neuer Account erstellt", "ToastNewUserCreatedSuccess": "Neuer Account erstellt",
"ToastNewUserLibraryError": "Mindestens eine Bibliothek muss ausgewählt werden", "ToastNewUserLibraryError": "Mindestens eine Bibliothek muss ausgewählt werden",
"ToastNewUserPasswordError": "Passwort erforderlich, nur der root Benutzer darf ein leeres Passwort haben", "ToastNewUserPasswordError": "Passwort erforderlich, nur der root Benutzer darf ein leeres Passwort haben",
"ToastNewUserTagError": "Mindestens ein Tag muss ausgewählt sein", "ToastNewUserTagError": "Mindestens ein Tag muss ausgewählt sein",
"ToastNewUserUsernameError": "Nutzername eingeben", "ToastNewUserUsernameError": "Nutzername eingeben",
"ToastNoNewEpisodesFound": "Keine neuen Episoden gefunden",
"ToastNoUpdatesNecessary": "Keine Änderungen nötig", "ToastNoUpdatesNecessary": "Keine Änderungen nötig",
"ToastNotificationCreateFailed": "Fehler beim erstellen der Benachrichtig", "ToastNotificationCreateFailed": "Fehler beim erstellen der Benachrichtig",
"ToastNotificationDeleteFailed": "Fehler beim löschen der Benachrichtigung", "ToastNotificationDeleteFailed": "Fehler beim löschen der Benachrichtigung",
@@ -949,6 +1034,7 @@
"ToastPodcastGetFeedFailed": "Fehler beim abrufen des Podcast Feeds", "ToastPodcastGetFeedFailed": "Fehler beim abrufen des Podcast Feeds",
"ToastPodcastNoEpisodesInFeed": "Keine Episoden in RSS Feed gefunden", "ToastPodcastNoEpisodesInFeed": "Keine Episoden in RSS Feed gefunden",
"ToastPodcastNoRssFeed": "Podcast enthält keinen RSS Feed", "ToastPodcastNoRssFeed": "Podcast enthält keinen RSS Feed",
"ToastProgressIsNotBeingSynced": "Fortschritt wird nicht synchronisiert, Wiedergabe wird neu gestartet",
"ToastProviderCreatedFailed": "Fehler beim hinzufügen des Anbieters", "ToastProviderCreatedFailed": "Fehler beim hinzufügen des Anbieters",
"ToastProviderCreatedSuccess": "Neuer Anbieter hinzugefügt", "ToastProviderCreatedSuccess": "Neuer Anbieter hinzugefügt",
"ToastProviderNameAndUrlRequired": "Name und URL notwendig", "ToastProviderNameAndUrlRequired": "Name und URL notwendig",
@@ -963,7 +1049,7 @@
"ToastRenameFailed": "Umbenennen fehlgeschlagen", "ToastRenameFailed": "Umbenennen fehlgeschlagen",
"ToastRescanFailed": "Erneut scannen fehlgeschlagen für {0}", "ToastRescanFailed": "Erneut scannen fehlgeschlagen für {0}",
"ToastRescanRemoved": "Erneut scannen erledigt, Artikel wurde entfernt", "ToastRescanRemoved": "Erneut scannen erledigt, Artikel wurde entfernt",
"ToastRescanUpToDate": "Erneut scannen erledigt, Artikel wahr auf dem neusten Stand", "ToastRescanUpToDate": "Erneut scannen erledigt, Artikel war auf dem neusten Stand",
"ToastRescanUpdated": "Erneut scannen erledigt, Artikel wurde verändert", "ToastRescanUpdated": "Erneut scannen erledigt, Artikel wurde verändert",
"ToastScanFailed": "Fehler beim scannen des Artikels der Bibliothek", "ToastScanFailed": "Fehler beim scannen des Artikels der Bibliothek",
"ToastSelectAtLeastOneUser": "Wähle mindestens einen Benutzer aus", "ToastSelectAtLeastOneUser": "Wähle mindestens einen Benutzer aus",
@@ -975,6 +1061,7 @@
"ToastSessionCloseFailed": "Fehler beim schließen der Sitzung", "ToastSessionCloseFailed": "Fehler beim schließen der Sitzung",
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden", "ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
"ToastSessionDeleteSuccess": "Sitzung gelöscht", "ToastSessionDeleteSuccess": "Sitzung gelöscht",
"ToastSleepTimerDone": "Einschlaf-Timer aktiviert... zZzzZz",
"ToastSlugMustChange": "URL-Schlüssel enthält ungültige Zeichen", "ToastSlugMustChange": "URL-Schlüssel enthält ungültige Zeichen",
"ToastSlugRequired": "URL-Schlüssel erforderlich", "ToastSlugRequired": "URL-Schlüssel erforderlich",
"ToastSocketConnected": "Verbindung zum WebSocket hergestellt", "ToastSocketConnected": "Verbindung zum WebSocket hergestellt",
+59
View File
@@ -88,6 +88,8 @@
"ButtonSaveTracklist": "Save Tracklist", "ButtonSaveTracklist": "Save Tracklist",
"ButtonScan": "Scan", "ButtonScan": "Scan",
"ButtonScanLibrary": "Scan Library", "ButtonScanLibrary": "Scan Library",
"ButtonScrollLeft": "Scroll Left",
"ButtonScrollRight": "Scroll Right",
"ButtonSearch": "Search", "ButtonSearch": "Search",
"ButtonSelectFolderPath": "Select Folder Path", "ButtonSelectFolderPath": "Select Folder Path",
"ButtonSeries": "Series", "ButtonSeries": "Series",
@@ -163,6 +165,7 @@
"HeaderNotificationUpdate": "Update Notification", "HeaderNotificationUpdate": "Update Notification",
"HeaderNotifications": "Notifications", "HeaderNotifications": "Notifications",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenListeningSessions": "Open Listening Sessions",
"HeaderOpenRSSFeed": "Open RSS Feed", "HeaderOpenRSSFeed": "Open RSS Feed",
"HeaderOtherFiles": "Other Files", "HeaderOtherFiles": "Other Files",
"HeaderPasswordAuthentication": "Password Authentication", "HeaderPasswordAuthentication": "Password Authentication",
@@ -180,6 +183,7 @@
"HeaderRemoveEpisodes": "Remove {0} Episodes", "HeaderRemoveEpisodes": "Remove {0} Episodes",
"HeaderSavedMediaProgress": "Saved Media Progress", "HeaderSavedMediaProgress": "Saved Media Progress",
"HeaderSchedule": "Schedule", "HeaderSchedule": "Schedule",
"HeaderScheduleEpisodeDownloads": "Schedule Automatic Episode Downloads",
"HeaderScheduleLibraryScans": "Schedule Automatic Library Scans", "HeaderScheduleLibraryScans": "Schedule Automatic Library Scans",
"HeaderSession": "Session", "HeaderSession": "Session",
"HeaderSetBackupSchedule": "Set Backup Schedule", "HeaderSetBackupSchedule": "Set Backup Schedule",
@@ -188,6 +192,7 @@
"HeaderSettingsExperimental": "Experimental Features", "HeaderSettingsExperimental": "Experimental Features",
"HeaderSettingsGeneral": "General", "HeaderSettingsGeneral": "General",
"HeaderSettingsScanner": "Scanner", "HeaderSettingsScanner": "Scanner",
"HeaderSettingsWebClient": "Web Client",
"HeaderSleepTimer": "Sleep Timer", "HeaderSleepTimer": "Sleep Timer",
"HeaderStatsLargestItems": "Largest Items", "HeaderStatsLargestItems": "Largest Items",
"HeaderStatsLongestItems": "Longest Items (hrs)", "HeaderStatsLongestItems": "Longest Items (hrs)",
@@ -225,6 +230,7 @@
"LabelAllUsersExcludingGuests": "All users excluding guests", "LabelAllUsersExcludingGuests": "All users excluding guests",
"LabelAllUsersIncludingGuests": "All users including guests", "LabelAllUsersIncludingGuests": "All users including guests",
"LabelAlreadyInYourLibrary": "Already in your library", "LabelAlreadyInYourLibrary": "Already in your library",
"LabelApiToken": "API Token",
"LabelAppend": "Append", "LabelAppend": "Append",
"LabelAudioBitrate": "Audio Bitrate (e.g. 128k)", "LabelAudioBitrate": "Audio Bitrate (e.g. 128k)",
"LabelAudioChannels": "Audio Channels (1 or 2)", "LabelAudioChannels": "Audio Channels (1 or 2)",
@@ -250,15 +256,18 @@
"LabelBackupsNumberToKeep": "Number of backups to keep", "LabelBackupsNumberToKeep": "Number of backups to keep",
"LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.", "LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.",
"LabelBitrate": "Bitrate", "LabelBitrate": "Bitrate",
"LabelBonus": "Bonus",
"LabelBooks": "Books", "LabelBooks": "Books",
"LabelButtonText": "Button Text", "LabelButtonText": "Button Text",
"LabelByAuthor": "by {0}", "LabelByAuthor": "by {0}",
"LabelChangePassword": "Change Password", "LabelChangePassword": "Change Password",
"LabelChannels": "Channels", "LabelChannels": "Channels",
"LabelChapterCount": "{0} Chapters",
"LabelChapterTitle": "Chapter Title", "LabelChapterTitle": "Chapter Title",
"LabelChapters": "Chapters", "LabelChapters": "Chapters",
"LabelChaptersFound": "chapters found", "LabelChaptersFound": "chapters found",
"LabelClickForMoreInfo": "Click for more info", "LabelClickForMoreInfo": "Click for more info",
"LabelClickToUseCurrentValue": "Click to use current value",
"LabelClosePlayer": "Close player", "LabelClosePlayer": "Close player",
"LabelCodec": "Codec", "LabelCodec": "Codec",
"LabelCollapseSeries": "Collapse Series", "LabelCollapseSeries": "Collapse Series",
@@ -291,6 +300,7 @@
"LabelDiscover": "Discover", "LabelDiscover": "Discover",
"LabelDownload": "Download", "LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download {0} episodes", "LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDownloadable": "Downloadable",
"LabelDuration": "Duration", "LabelDuration": "Duration",
"LabelDurationComparisonExactMatch": "(exact match)", "LabelDurationComparisonExactMatch": "(exact match)",
"LabelDurationComparisonLonger": "({0} longer)", "LabelDurationComparisonLonger": "({0} longer)",
@@ -320,9 +330,13 @@
"LabelEnd": "End", "LabelEnd": "End",
"LabelEndOfChapter": "End of Chapter", "LabelEndOfChapter": "End of Chapter",
"LabelEpisode": "Episode", "LabelEpisode": "Episode",
"LabelEpisodeNotLinkedToRssFeed": "Episode not linked to RSS feed",
"LabelEpisodeNumber": "Episode #{0}",
"LabelEpisodeTitle": "Episode Title", "LabelEpisodeTitle": "Episode Title",
"LabelEpisodeType": "Episode Type", "LabelEpisodeType": "Episode Type",
"LabelEpisodeUrlFromRssFeed": "Episode URL from RSS feed",
"LabelEpisodes": "Episodes", "LabelEpisodes": "Episodes",
"LabelEpisodic": "Episodic",
"LabelExample": "Example", "LabelExample": "Example",
"LabelExpandSeries": "Expand Series", "LabelExpandSeries": "Expand Series",
"LabelExpandSubSeries": "Expand Sub Series", "LabelExpandSubSeries": "Expand Sub Series",
@@ -350,6 +364,7 @@
"LabelFontScale": "Font scale", "LabelFontScale": "Font scale",
"LabelFontStrikethrough": "Strikethrough", "LabelFontStrikethrough": "Strikethrough",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelFull": "Full",
"LabelGenre": "Genre", "LabelGenre": "Genre",
"LabelGenres": "Genres", "LabelGenres": "Genres",
"LabelHardDeleteFile": "Hard delete file", "LabelHardDeleteFile": "Hard delete file",
@@ -405,6 +420,10 @@
"LabelLowestPriority": "Lowest Priority", "LabelLowestPriority": "Lowest Priority",
"LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersBy": "Match existing users by",
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
"LabelMaxEpisodesToDownload": "Max # of episodes to download. Use 0 for unlimited.",
"LabelMaxEpisodesToDownloadPerCheck": "Max # of new episodes to download per check",
"LabelMaxEpisodesToKeep": "Max # of episodes to keep",
"LabelMaxEpisodesToKeepHelp": "Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes. This will only delete 1 episode per new download.",
"LabelMediaPlayer": "Media Player", "LabelMediaPlayer": "Media Player",
"LabelMediaType": "Media Type", "LabelMediaType": "Media Type",
"LabelMetaTag": "Meta Tag", "LabelMetaTag": "Meta Tag",
@@ -450,12 +469,14 @@
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Open RSS Feed", "LabelOpenRSSFeed": "Open RSS Feed",
"LabelOverwrite": "Overwrite", "LabelOverwrite": "Overwrite",
"LabelPaginationPageXOfY": "Page {0} of {1}",
"LabelPassword": "Password", "LabelPassword": "Password",
"LabelPath": "Path", "LabelPath": "Path",
"LabelPermanent": "Permanent", "LabelPermanent": "Permanent",
"LabelPermissionsAccessAllLibraries": "Can Access All Libraries", "LabelPermissionsAccessAllLibraries": "Can Access All Libraries",
"LabelPermissionsAccessAllTags": "Can Access All Tags", "LabelPermissionsAccessAllTags": "Can Access All Tags",
"LabelPermissionsAccessExplicitContent": "Can Access Explicit Content", "LabelPermissionsAccessExplicitContent": "Can Access Explicit Content",
"LabelPermissionsCreateEreader": "Can Create Ereader",
"LabelPermissionsDelete": "Can Delete", "LabelPermissionsDelete": "Can Delete",
"LabelPermissionsDownload": "Can Download", "LabelPermissionsDownload": "Can Download",
"LabelPermissionsUpdate": "Can Update", "LabelPermissionsUpdate": "Can Update",
@@ -500,18 +521,24 @@
"LabelRedo": "Redo", "LabelRedo": "Redo",
"LabelRegion": "Region", "LabelRegion": "Region",
"LabelReleaseDate": "Release Date", "LabelReleaseDate": "Release Date",
"LabelRemoveAllMetadataAbs": "Remove all metadata.abs files",
"LabelRemoveAllMetadataJson": "Remove all metadata.json files",
"LabelRemoveCover": "Remove cover", "LabelRemoveCover": "Remove cover",
"LabelRemoveMetadataFile": "Remove metadata files in library item folders",
"LabelRemoveMetadataFileHelp": "Remove all metadata.json and metadata.abs files in your {0} folders.",
"LabelRowsPerPage": "Rows per page", "LabelRowsPerPage": "Rows per page",
"LabelSearchTerm": "Search Term", "LabelSearchTerm": "Search Term",
"LabelSearchTitle": "Search Title", "LabelSearchTitle": "Search Title",
"LabelSearchTitleOrASIN": "Search Title or ASIN", "LabelSearchTitleOrASIN": "Search Title or ASIN",
"LabelSeason": "Season", "LabelSeason": "Season",
"LabelSeasonNumber": "Season #{0}",
"LabelSelectAll": "Select all", "LabelSelectAll": "Select all",
"LabelSelectAllEpisodes": "Select all episodes", "LabelSelectAllEpisodes": "Select all episodes",
"LabelSelectEpisodesShowing": "Select {0} episodes showing", "LabelSelectEpisodesShowing": "Select {0} episodes showing",
"LabelSelectUsers": "Select users", "LabelSelectUsers": "Select users",
"LabelSendEbookToDevice": "Send Ebook to...", "LabelSendEbookToDevice": "Send Ebook to...",
"LabelSequence": "Sequence", "LabelSequence": "Sequence",
"LabelSerial": "Serial",
"LabelSeries": "Series", "LabelSeries": "Series",
"LabelSeriesName": "Series Name", "LabelSeriesName": "Series Name",
"LabelSeriesProgress": "Series Progress", "LabelSeriesProgress": "Series Progress",
@@ -519,6 +546,7 @@
"LabelServerYearReview": "Server Year in Review ({0})", "LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Set as primary", "LabelSetEbookAsPrimary": "Set as primary",
"LabelSetEbookAsSupplementary": "Set as supplementary", "LabelSetEbookAsSupplementary": "Set as supplementary",
"LabelSettingsAllowIframe": "Allow embedding in an iframe",
"LabelSettingsAudiobooksOnly": "Audiobooks only", "LabelSettingsAudiobooksOnly": "Audiobooks only",
"LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks", "LabelSettingsAudiobooksOnlyHelp": "Enabling this setting will ignore ebook files unless they are inside an audiobook folder in which case they will be set as supplementary ebooks",
"LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves", "LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves",
@@ -540,6 +568,9 @@
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.", "LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view", "LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view", "LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
"LabelSettingsLibraryMarkAsFinishedPercentComplete": "Percent complete is greater than",
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Time remaining is less than (seconds)",
"LabelSettingsLibraryMarkAsFinishedWhen": "Mark media item as finished when",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Parse subtitles", "LabelSettingsParseSubtitles": "Parse subtitles",
@@ -558,6 +589,7 @@
"LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders", "LabelSettingsStoreMetadataWithItemHelp": "By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders",
"LabelSettingsTimeFormat": "Time Format", "LabelSettingsTimeFormat": "Time Format",
"LabelShare": "Share", "LabelShare": "Share",
"LabelShareDownloadableHelp": "Allows users with the share link to download a zip file of the library item.",
"LabelShareOpen": "Share Open", "LabelShareOpen": "Share Open",
"LabelShareURL": "Share URL", "LabelShareURL": "Share URL",
"LabelShowAll": "Show All", "LabelShowAll": "Show All",
@@ -566,6 +598,8 @@
"LabelSize": "Size", "LabelSize": "Size",
"LabelSleepTimer": "Sleep timer", "LabelSleepTimer": "Sleep timer",
"LabelSlug": "Slug", "LabelSlug": "Slug",
"LabelSortAscending": "Ascending",
"LabelSortDescending": "Descending",
"LabelStart": "Start", "LabelStart": "Start",
"LabelStartTime": "Start Time", "LabelStartTime": "Start Time",
"LabelStarted": "Started", "LabelStarted": "Started",
@@ -604,6 +638,7 @@
"LabelTimeDurationXMinutes": "{0} minutes", "LabelTimeDurationXMinutes": "{0} minutes",
"LabelTimeDurationXSeconds": "{0} seconds", "LabelTimeDurationXSeconds": "{0} seconds",
"LabelTimeInMinutes": "Time in minutes", "LabelTimeInMinutes": "Time in minutes",
"LabelTimeLeft": "{0} left",
"LabelTimeListened": "Time Listened", "LabelTimeListened": "Time Listened",
"LabelTimeListenedToday": "Time Listened Today", "LabelTimeListenedToday": "Time Listened Today",
"LabelTimeRemaining": "{0} remaining", "LabelTimeRemaining": "{0} remaining",
@@ -624,6 +659,7 @@
"LabelTracksMultiTrack": "Multi-track", "LabelTracksMultiTrack": "Multi-track",
"LabelTracksNone": "No tracks", "LabelTracksNone": "No tracks",
"LabelTracksSingleTrack": "Single-track", "LabelTracksSingleTrack": "Single-track",
"LabelTrailer": "Trailer",
"LabelType": "Type", "LabelType": "Type",
"LabelUnabridged": "Unabridged", "LabelUnabridged": "Unabridged",
"LabelUndo": "Undo", "LabelUndo": "Undo",
@@ -635,11 +671,13 @@
"LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located", "LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
"LabelUpdatedAt": "Updated At", "LabelUpdatedAt": "Updated At",
"LabelUploaderDragAndDrop": "Drag & drop files or folders", "LabelUploaderDragAndDrop": "Drag & drop files or folders",
"LabelUploaderDragAndDropFilesOnly": "Drag & drop files",
"LabelUploaderDropFiles": "Drop files", "LabelUploaderDropFiles": "Drop files",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series", "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseAdvancedOptions": "Use Advanced Options", "LabelUseAdvancedOptions": "Use Advanced Options",
"LabelUseChapterTrack": "Use chapter track", "LabelUseChapterTrack": "Use chapter track",
"LabelUseFullTrack": "Use full track", "LabelUseFullTrack": "Use full track",
"LabelUseZeroForUnlimited": "Use 0 for unlimited",
"LabelUser": "User", "LabelUser": "User",
"LabelUsername": "Username", "LabelUsername": "Username",
"LabelValue": "Value", "LabelValue": "Value",
@@ -649,6 +687,8 @@
"LabelViewPlayerSettings": "View player settings", "LabelViewPlayerSettings": "View player settings",
"LabelViewQueue": "View player queue", "LabelViewQueue": "View player queue",
"LabelVolume": "Volume", "LabelVolume": "Volume",
"LabelWebRedirectURLsDescription": "Authorize these URLs in your OAuth provider to allow redirection back to the web app after login:",
"LabelWebRedirectURLsSubfolder": "Subfolder for Redirect URLs",
"LabelWeekdaysToRun": "Weekdays to run", "LabelWeekdaysToRun": "Weekdays to run",
"LabelXBooks": "{0} books", "LabelXBooks": "{0} books",
"LabelXItems": "{0} items", "LabelXItems": "{0} items",
@@ -698,6 +738,7 @@
"MessageConfirmPurgeCache": "Purge cache will delete the entire directory at <code>/metadata/cache</code>. <br /><br />Are you sure you want to remove the cache directory?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at <code>/metadata/cache</code>. <br /><br />Are you sure you want to remove the cache directory?",
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?", "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
"MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?",
"MessageConfirmQuickMatchEpisodes": "Quick matching episodes will overwrite details if a match is found. Only unmatched episodes will be updated. Are you sure?",
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?", "MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
@@ -705,6 +746,7 @@
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?", "MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?", "MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveMetadataFiles": "Are you sure you want to remove all metadata.{0} files in your library item folders?",
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?", "MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?", "MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
@@ -785,6 +827,7 @@
"MessagePodcastSearchField": "Enter search term or RSS feed URL", "MessagePodcastSearchField": "Enter search term or RSS feed URL",
"MessageQuickEmbedInProgress": "Quick embed in progress", "MessageQuickEmbedInProgress": "Quick embed in progress",
"MessageQuickEmbedQueue": "Queued for quick embed ({0} in queue)", "MessageQuickEmbedQueue": "Queued for quick embed ({0} in queue)",
"MessageQuickMatchAllEpisodes": "Quick Match All Episodes",
"MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.", "MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.",
"MessageRemoveChapter": "Remove chapter", "MessageRemoveChapter": "Remove chapter",
"MessageRemoveEpisodes": "Remove {0} episode(s)", "MessageRemoveEpisodes": "Remove {0} episode(s)",
@@ -883,6 +926,7 @@
"StatsYearInReview": "YEAR IN REVIEW", "StatsYearInReview": "YEAR IN REVIEW",
"ToastAccountUpdateSuccess": "Account updated", "ToastAccountUpdateSuccess": "Account updated",
"ToastAppriseUrlRequired": "Must enter an Apprise URL", "ToastAppriseUrlRequired": "Must enter an Apprise URL",
"ToastAsinRequired": "ASIN is required",
"ToastAuthorImageRemoveSuccess": "Author image removed", "ToastAuthorImageRemoveSuccess": "Author image removed",
"ToastAuthorNotFound": "Author \"{0}\" not found", "ToastAuthorNotFound": "Author \"{0}\" not found",
"ToastAuthorRemoveSuccess": "Author removed", "ToastAuthorRemoveSuccess": "Author removed",
@@ -902,6 +946,8 @@
"ToastBackupUploadSuccess": "Backup uploaded", "ToastBackupUploadSuccess": "Backup uploaded",
"ToastBatchDeleteFailed": "Batch delete failed", "ToastBatchDeleteFailed": "Batch delete failed",
"ToastBatchDeleteSuccess": "Batch delete success", "ToastBatchDeleteSuccess": "Batch delete success",
"ToastBatchQuickMatchFailed": "Batch Quick Match failed!",
"ToastBatchQuickMatchStarted": "Batch Quick Match of {0} books started!",
"ToastBatchUpdateFailed": "Batch update failed", "ToastBatchUpdateFailed": "Batch update failed",
"ToastBatchUpdateSuccess": "Batch update success", "ToastBatchUpdateSuccess": "Batch update success",
"ToastBookmarkCreateFailed": "Failed to create bookmark", "ToastBookmarkCreateFailed": "Failed to create bookmark",
@@ -913,6 +959,7 @@
"ToastChaptersHaveErrors": "Chapters have errors", "ToastChaptersHaveErrors": "Chapters have errors",
"ToastChaptersMustHaveTitles": "Chapters must have titles", "ToastChaptersMustHaveTitles": "Chapters must have titles",
"ToastChaptersRemoved": "Chapters removed", "ToastChaptersRemoved": "Chapters removed",
"ToastChaptersUpdated": "Chapters updated",
"ToastCollectionItemsAddFailed": "Item(s) added to collection failed", "ToastCollectionItemsAddFailed": "Item(s) added to collection failed",
"ToastCollectionItemsAddSuccess": "Item(s) added to collection success", "ToastCollectionItemsAddSuccess": "Item(s) added to collection success",
"ToastCollectionItemsRemoveSuccess": "Item(s) removed from collection", "ToastCollectionItemsRemoveSuccess": "Item(s) removed from collection",
@@ -930,11 +977,14 @@
"ToastEncodeCancelSucces": "Encode canceled", "ToastEncodeCancelSucces": "Encode canceled",
"ToastEpisodeDownloadQueueClearFailed": "Failed to clear queue", "ToastEpisodeDownloadQueueClearFailed": "Failed to clear queue",
"ToastEpisodeDownloadQueueClearSuccess": "Episode download queue cleared", "ToastEpisodeDownloadQueueClearSuccess": "Episode download queue cleared",
"ToastEpisodeUpdateSuccess": "{0} episodes updated",
"ToastErrorCannotShare": "Cannot share natively on this device", "ToastErrorCannotShare": "Cannot share natively on this device",
"ToastFailedToLoadData": "Failed to load data", "ToastFailedToLoadData": "Failed to load data",
"ToastFailedToMatch": "Failed to match",
"ToastFailedToShare": "Failed to share", "ToastFailedToShare": "Failed to share",
"ToastFailedToUpdate": "Failed to update", "ToastFailedToUpdate": "Failed to update",
"ToastInvalidImageUrl": "Invalid image URL", "ToastInvalidImageUrl": "Invalid image URL",
"ToastInvalidMaxEpisodesToDownload": "Invalid max episodes to download",
"ToastInvalidUrl": "Invalid URL", "ToastInvalidUrl": "Invalid URL",
"ToastItemCoverUpdateSuccess": "Item cover updated", "ToastItemCoverUpdateSuccess": "Item cover updated",
"ToastItemDeletedFailed": "Failed to delete item", "ToastItemDeletedFailed": "Failed to delete item",
@@ -953,14 +1003,21 @@
"ToastLibraryScanStarted": "Library scan started", "ToastLibraryScanStarted": "Library scan started",
"ToastLibraryUpdateSuccess": "Library \"{0}\" updated", "ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
"ToastMatchAllAuthorsFailed": "Failed to match all authors", "ToastMatchAllAuthorsFailed": "Failed to match all authors",
"ToastMetadataFilesRemovedError": "Error removing metadata.{0} files",
"ToastMetadataFilesRemovedNoneFound": "No metadata.{0} files found in library",
"ToastMetadataFilesRemovedNoneRemoved": "No metadata.{0} files removed",
"ToastMetadataFilesRemovedSuccess": "{0} metadata.{1} files removed",
"ToastMustHaveAtLeastOnePath": "Must have at least one path",
"ToastNameEmailRequired": "Name and email are required", "ToastNameEmailRequired": "Name and email are required",
"ToastNameRequired": "Name is required", "ToastNameRequired": "Name is required",
"ToastNewEpisodesFound": "{0} new episodes found",
"ToastNewUserCreatedFailed": "Failed to create account: \"{0}\"", "ToastNewUserCreatedFailed": "Failed to create account: \"{0}\"",
"ToastNewUserCreatedSuccess": "New account created", "ToastNewUserCreatedSuccess": "New account created",
"ToastNewUserLibraryError": "Must select at least one library", "ToastNewUserLibraryError": "Must select at least one library",
"ToastNewUserPasswordError": "Must have a password, only root user can have an empty password", "ToastNewUserPasswordError": "Must have a password, only root user can have an empty password",
"ToastNewUserTagError": "Must select at least one tag", "ToastNewUserTagError": "Must select at least one tag",
"ToastNewUserUsernameError": "Enter a username", "ToastNewUserUsernameError": "Enter a username",
"ToastNoNewEpisodesFound": "No new episodes found",
"ToastNoUpdatesNecessary": "No updates necessary", "ToastNoUpdatesNecessary": "No updates necessary",
"ToastNotificationCreateFailed": "Failed to create notification", "ToastNotificationCreateFailed": "Failed to create notification",
"ToastNotificationDeleteFailed": "Failed to delete notification", "ToastNotificationDeleteFailed": "Failed to delete notification",
@@ -979,6 +1036,7 @@
"ToastPodcastGetFeedFailed": "Failed to get podcast feed", "ToastPodcastGetFeedFailed": "Failed to get podcast feed",
"ToastPodcastNoEpisodesInFeed": "No episodes found in RSS feed", "ToastPodcastNoEpisodesInFeed": "No episodes found in RSS feed",
"ToastPodcastNoRssFeed": "Podcast does not have an RSS feed", "ToastPodcastNoRssFeed": "Podcast does not have an RSS feed",
"ToastProgressIsNotBeingSynced": "Progress is not being synced, restart playback",
"ToastProviderCreatedFailed": "Failed to add provider", "ToastProviderCreatedFailed": "Failed to add provider",
"ToastProviderCreatedSuccess": "New provider added", "ToastProviderCreatedSuccess": "New provider added",
"ToastProviderNameAndUrlRequired": "Name and Url required", "ToastProviderNameAndUrlRequired": "Name and Url required",
@@ -1005,6 +1063,7 @@
"ToastSessionCloseFailed": "Failed to close session", "ToastSessionCloseFailed": "Failed to close session",
"ToastSessionDeleteFailed": "Failed to delete session", "ToastSessionDeleteFailed": "Failed to delete session",
"ToastSessionDeleteSuccess": "Session deleted", "ToastSessionDeleteSuccess": "Session deleted",
"ToastSleepTimerDone": "Sleep timer done... zZzzZz",
"ToastSlugMustChange": "Slug contains invalid characters", "ToastSlugMustChange": "Slug contains invalid characters",
"ToastSlugRequired": "Slug is required", "ToastSlugRequired": "Slug is required",
"ToastSocketConnected": "Socket connected", "ToastSocketConnected": "Socket connected",

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