Compare commits

...

395 Commits

Author SHA1 Message Date
advplyr ed17dd9b51 Fix user stats heatmap caption text to be accurate 2025-01-01 13:49:22 -06:00
advplyr eb505a0be7 Merge pull request #3754 from maxlajoie99/feature/experimental-proxy-support
Experimental proxy support by manually following redirects
2025-01-01 12:54:25 -06:00
advplyr f3918a47e1 Auto formatting 2025-01-01 12:48:58 -06:00
advplyr c8a05920dd Merge pull request #3772 from advplyr/feed-episodes-upsert
Feed episode IDs changing on refresh & several other refresh issues
2025-01-01 12:17:10 -06:00
advplyr e7f7d1a573 Fix refresh feed when book is deleted and belonged to a series/collection 2025-01-01 12:06:01 -06:00
advplyr 5201625d38 Fix FeedEpisodes using a new ID when updating #3757 2025-01-01 11:32:39 -06:00
advplyr 8c4d0c503b Merge pull request #3767 from mikiher/book-query-optimizations
Book query optimizations
2025-01-01 10:10:51 -06:00
advplyr d3bda898d4 Merge pull request #3769 from advplyr/share-media-player-media-session-api
Use Media Session API in the Share audio player & pass chapterInfo to media sessions
2025-01-01 09:11:11 -06:00
advplyr 86809dcc62 Update audio player to pass chapterInfo to media session API 2025-01-01 09:02:31 -06:00
advplyr 9fa00a1904 Fix Share media player not using media session API #3768 2025-01-01 08:55:40 -06:00
mikiher 46247ecf78 Update migrations changelog 2025-01-01 08:41:27 +02:00
mikiher 0444829a9f Add index on duration 2025-01-01 08:37:57 +02:00
mikiher 754c121168 Add libraryItem size index 2025-01-01 07:34:29 +02:00
advplyr 1c2ee09f18 Fix user stats heatmap to use range of currently showing data only 2024-12-31 17:41:09 -06:00
advplyr ee310d967e Merge pull request #3766 from advplyr/remove-old-playlist
Remove old Playlist object + remove unnecessary toasts
2024-12-31 17:26:26 -06:00
advplyr 25b7f005c6 Remove unnecessary playlist toasts 2024-12-31 17:15:11 -06:00
advplyr 777c59458d Fix find all playlist endpoint 2024-12-31 17:11:31 -06:00
advplyr 9785bc02ea Update Playlist model & controller to remove usage of old Playlist object, remove old Playlist 2024-12-31 17:01:42 -06:00
advplyr 6780ef9b37 Merge pull request #3761 from advplyr/remove_old_collection_object
Remove old Collection object
2024-12-30 17:14:07 -06:00
advplyr 88a0e75576 Remove collection add/create modal toasts 2024-12-30 17:07:41 -06:00
advplyr 476933a144 Refactor Collection model/controller to not use old Collection object, remove 2024-12-30 16:54:48 -06:00
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
maxlajoie99 d7830f4bfc Experimental proxy support by manually following redirects 2024-12-27 20:26:55 -05: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
219 changed files with 10163 additions and 3749 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() {
+18 -14
View File
@@ -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
@@ -378,19 +374,27 @@ export default {
return return
} }
// https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API
if ('mediaSession' in navigator) { if ('mediaSession' in navigator) {
var coverImageSrc = this.$store.getters['globals/getLibraryItemCoverSrc'](this.streamLibraryItem, '/Logo.png', true) const chapterInfo = []
const artwork = [ if (this.chapters.length) {
{ this.chapters.forEach((chapter) => {
src: coverImageSrc chapterInfo.push({
} title: chapter.title,
] startTime: chapter.start
})
})
}
navigator.mediaSession.metadata = new MediaMetadata({ navigator.mediaSession.metadata = new MediaMetadata({
title: this.title, title: this.title,
artist: this.playerHandler.displayAuthor || this.mediaMetadata.authorName || 'Unknown', artist: this.playerHandler.displayAuthor || this.mediaMetadata.authorName || 'Unknown',
album: this.mediaMetadata.seriesName || '', album: this.mediaMetadata.seriesName || '',
artwork artwork: [
{
src: this.$store.getters['globals/getLibraryItemCoverSrc'](this.streamLibraryItem, '/Logo.png', true)
}
]
}) })
console.log('Set media session metadata', navigator.mediaSession.metadata) console.log('Set media session metadata', navigator.mediaSession.metadata)
@@ -525,7 +529,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>
@@ -138,7 +138,6 @@ export default {
.$post(`/api/collections/${collection.id}/batch/remove`, { books: this.selectedBookIds }) .$post(`/api/collections/${collection.id}/batch/remove`, { books: this.selectedBookIds })
.then((updatedCollection) => { .then((updatedCollection) => {
console.log(`Books removed from collection`, updatedCollection) console.log(`Books removed from collection`, updatedCollection)
this.$toast.success(this.$strings.ToastCollectionItemsRemoveSuccess)
this.processing = false this.processing = false
}) })
.catch((error) => { .catch((error) => {
@@ -152,7 +151,6 @@ export default {
.$delete(`/api/collections/${collection.id}/book/${this.selectedLibraryItemId}`) .$delete(`/api/collections/${collection.id}/book/${this.selectedLibraryItemId}`)
.then((updatedCollection) => { .then((updatedCollection) => {
console.log(`Book removed from collection`, updatedCollection) console.log(`Book removed from collection`, updatedCollection)
this.$toast.success(this.$strings.ToastCollectionItemsRemoveSuccess)
this.processing = false this.processing = false
}) })
.catch((error) => { .catch((error) => {
@@ -167,12 +165,11 @@ export default {
this.processing = true this.processing = true
if (this.showBatchCollectionModal) { if (this.showBatchCollectionModal) {
// BATCH Remove books // BATCH Add books
this.$axios this.$axios
.$post(`/api/collections/${collection.id}/batch/add`, { books: this.selectedBookIds }) .$post(`/api/collections/${collection.id}/batch/add`, { books: this.selectedBookIds })
.then((updatedCollection) => { .then((updatedCollection) => {
console.log(`Books added to collection`, updatedCollection) console.log(`Books added to collection`, updatedCollection)
this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
this.processing = false this.processing = false
}) })
.catch((error) => { .catch((error) => {
@@ -187,7 +184,6 @@ export default {
.$post(`/api/collections/${collection.id}/book`, { id: this.selectedLibraryItemId }) .$post(`/api/collections/${collection.id}/book`, { id: this.selectedLibraryItemId })
.then((updatedCollection) => { .then((updatedCollection) => {
console.log(`Book added to collection`, updatedCollection) console.log(`Book added to collection`, updatedCollection)
this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
this.processing = false this.processing = false
}) })
.catch((error) => { .catch((error) => {
@@ -214,7 +210,6 @@ export default {
.$post('/api/collections', newCollection) .$post('/api/collections', newCollection)
.then((data) => { .then((data) => {
console.log('New Collection Created', data) console.log('New Collection Created', data)
this.$toast.success(`Collection "${data.name}" created`)
this.processing = false this.processing = false
this.newCollectionName = '' this.newCollectionName = ''
}) })
@@ -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)
@@ -130,7 +130,6 @@ export default {
.$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects }) .$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects })
.then((updatedPlaylist) => { .then((updatedPlaylist) => {
console.log(`Items removed from playlist`, updatedPlaylist) console.log(`Items removed from playlist`, updatedPlaylist)
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
this.processing = false this.processing = false
}) })
.catch((error) => { .catch((error) => {
@@ -148,7 +147,6 @@ export default {
.$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects }) .$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects })
.then((updatedPlaylist) => { .then((updatedPlaylist) => {
console.log(`Items added to playlist`, updatedPlaylist) console.log(`Items added to playlist`, updatedPlaylist)
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
this.processing = false this.processing = false
}) })
.catch((error) => { .catch((error) => {
@@ -174,7 +172,6 @@ export default {
.$post('/api/playlists', newPlaylist) .$post('/api/playlists', newPlaylist)
.then((data) => { .then((data) => {
console.log('New playlist created', data) console.log('New playlist created', data)
this.$toast.success(this.$strings.ToastPlaylistCreateSuccess + ': ' + data.name)
this.processing = false this.processing = false
this.newPlaylistName = '' this.newPlaylistName = ''
}) })
@@ -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
+34 -31
View File
@@ -1,7 +1,7 @@
<template> <template>
<div id="heatmap" class="w-full"> <div id="heatmap" class="w-full">
<div class="mx-auto" :style="{ height: innerHeight + 160 + 'px', width: innerWidth + 52 + 'px' }" style="background-color: rgba(13, 17, 23, 0)"> <div class="mx-auto" :style="{ height: innerHeight + 160 + 'px', width: innerWidth + 52 + 'px' }" style="background-color: rgba(13, 17, 23, 0)">
<p class="mb-2 px-1 text-sm text-gray-200">{{ $getString('MessageListeningSessionsInTheLastYear', [Object.values(daysListening).length]) }}</p> <p class="mb-2 px-1 text-sm text-gray-200">{{ $getString('MessageDaysListenedInTheLastYear', [daysListenedInTheLastYear]) }}</p>
<div class="border border-white border-opacity-25 rounded py-2 w-full" style="background-color: #232323" :style="{ height: innerHeight + 80 + 'px' }"> <div class="border border-white border-opacity-25 rounded py-2 w-full" style="background-color: #232323" :style="{ height: innerHeight + 80 + 'px' }">
<div :style="{ width: innerWidth + 'px', height: innerHeight + 'px' }" class="ml-10 mt-5 absolute" @mouseover="mouseover" @mouseout="mouseout"> <div :style="{ width: innerWidth + 'px', height: innerHeight + 'px' }" class="ml-10 mt-5 absolute" @mouseover="mouseover" @mouseout="mouseout">
<div v-for="dayLabel in dayLabels" :key="dayLabel.label" :style="dayLabel.style" class="absolute top-0 left-0 text-gray-300">{{ dayLabel.label }}</div> <div v-for="dayLabel in dayLabels" :key="dayLabel.label" :style="dayLabel.style" class="absolute top-0 left-0 text-gray-300">{{ dayLabel.label }}</div>
@@ -37,6 +37,7 @@ export default {
innerHeight: 13 * 7, innerHeight: 13 * 7,
blockWidth: 13, blockWidth: 13,
data: [], data: [],
daysListenedInTheLastYear: 0,
monthLabels: [], monthLabels: [],
tooltipEl: null, tooltipEl: null,
tooltipTextEl: null, tooltipTextEl: null,
@@ -193,46 +194,47 @@ export default {
buildData() { buildData() {
this.data = [] this.data = []
var maxValue = 0 let maxValue = 0
var minValue = 0 let minValue = 0
Object.values(this.daysListening).forEach((val) => {
if (val > maxValue) maxValue = val
if (!minValue || val < minValue) minValue = val
})
const range = maxValue - minValue + 0.01
const dates = []
for (let i = 0; i < this.daysToShow + 1; i++) { for (let i = 0; i < this.daysToShow + 1; i++) {
const col = Math.floor(i / 7)
const row = i % 7
const date = i === 0 ? this.firstWeekStart : this.$addDaysToDate(this.firstWeekStart, i) const date = i === 0 ? this.firstWeekStart : this.$addDaysToDate(this.firstWeekStart, i)
const dateString = this.$formatJsDate(date, 'yyyy-MM-dd') const dateString = this.$formatJsDate(date, 'yyyy-MM-dd')
const datePretty = this.$formatJsDate(date, 'MMM d, yyyy') const dateObj = {
const monthString = this.$formatJsDate(date, 'MMM') col: Math.floor(i / 7),
const value = this.daysListening[dateString] || 0 row: i % 7,
const x = col * 13 date,
const y = row * 13 dateString,
datePretty: this.$formatJsDate(date, 'MMM d, yyyy'),
monthString: this.$formatJsDate(date, 'MMM'),
dayOfMonth: Number(dateString.split('-').pop()),
yearString: dateString.split('-').shift(),
value: this.daysListening[dateString] || 0
}
dates.push(dateObj)
var bgColor = this.bgColors[0] if (dateObj.value > 0) {
var outlineColor = this.outlineColors[0] this.daysListenedInTheLastYear++
if (value) { if (dateObj.value > maxValue) maxValue = dateObj.value
if (!minValue || dateObj.value < minValue) minValue = dateObj.value
}
}
const range = maxValue - minValue + 0.01
for (const dateObj of dates) {
let bgColor = this.bgColors[0]
let outlineColor = this.outlineColors[0]
if (dateObj.value) {
outlineColor = this.outlineColors[1] outlineColor = this.outlineColors[1]
var percentOfAvg = (value - minValue) / range const percentOfAvg = (dateObj.value - minValue) / range
var bgIndex = Math.floor(percentOfAvg * 4) + 1 const bgIndex = Math.floor(percentOfAvg * 4) + 1
bgColor = this.bgColors[bgIndex] || 'red' bgColor = this.bgColors[bgIndex] || 'red'
} }
this.data.push({ this.data.push({
date, ...dateObj,
dateString, style: `transform:translate(${dateObj.col * 13}px,${dateObj.row * 13}px);background-color:${bgColor};outline:1px solid ${outlineColor};outline-offset:-1px;`
datePretty,
monthString,
dayOfMonth: Number(dateString.split('-').pop()),
yearString: dateString.split('-').shift(),
value,
col,
row,
style: `transform:translate(${x}px,${y}px);background-color:${bgColor};outline:1px solid ${outlineColor};outline-offset:-1px;`
}) })
} }
@@ -260,6 +262,7 @@ export default {
const heatmapEl = document.getElementById('heatmap') const heatmapEl = document.getElementById('heatmap')
this.contentWidth = heatmapEl.clientWidth this.contentWidth = heatmapEl.clientWidth
this.maxInnerWidth = this.contentWidth - 52 this.maxInnerWidth = this.contentWidth - 52
this.daysListenedInTheLastYear = 0
this.buildData() this.buildData()
} }
}, },
+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)
@@ -218,7 +218,6 @@ export default {
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess) this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
} else { } else {
console.log(`Item removed from playlist`, updatedPlaylist) console.log(`Item removed from playlist`, updatedPlaylist)
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
} }
}) })
.catch((error) => { .catch((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>
+117 -12
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
@@ -105,6 +110,84 @@ export default {
} }
}, },
methods: { methods: {
mediaSessionPlay() {
console.log('Media session play')
this.play()
},
mediaSessionPause() {
console.log('Media session pause')
this.pause()
},
mediaSessionStop() {
console.log('Media session stop')
this.pause()
},
mediaSessionSeekBackward() {
console.log('Media session seek backward')
this.jumpBackward()
},
mediaSessionSeekForward() {
console.log('Media session seek forward')
this.jumpForward()
},
mediaSessionSeekTo(e) {
console.log('Media session seek to', e)
if (e.seekTime !== null && !isNaN(e.seekTime)) {
this.seek(e.seekTime)
}
},
mediaSessionPreviousTrack() {
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.prevChapter()
}
},
mediaSessionNextTrack() {
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.nextChapter()
}
},
updateMediaSessionPlaybackState() {
if ('mediaSession' in navigator) {
navigator.mediaSession.playbackState = this.isPlaying ? 'playing' : 'paused'
}
},
setMediaSession() {
// https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API
if ('mediaSession' in navigator) {
const chapterInfo = []
if (this.chapters.length > 0) {
this.chapters.forEach((chapter) => {
chapterInfo.push({
title: chapter.title,
startTime: chapter.start
})
})
}
navigator.mediaSession.metadata = new MediaMetadata({
title: this.mediaItemShare.playbackSession.displayTitle || 'No title',
artist: this.mediaItemShare.playbackSession.displayAuthor || 'Unknown',
artwork: [
{
src: this.coverUrl
}
],
chapterInfo
})
console.log('Set media session metadata', navigator.mediaSession.metadata)
navigator.mediaSession.setActionHandler('play', this.mediaSessionPlay)
navigator.mediaSession.setActionHandler('pause', this.mediaSessionPause)
navigator.mediaSession.setActionHandler('stop', this.mediaSessionStop)
navigator.mediaSession.setActionHandler('seekbackward', this.mediaSessionSeekBackward)
navigator.mediaSession.setActionHandler('seekforward', this.mediaSessionSeekForward)
navigator.mediaSession.setActionHandler('seekto', this.mediaSessionSeekTo)
navigator.mediaSession.setActionHandler('previoustrack', this.mediaSessionSeekBackward)
navigator.mediaSession.setActionHandler('nexttrack', this.mediaSessionSeekForward)
} else {
console.warn('Media session not available')
}
},
async coverImageLoaded(e) { async coverImageLoaded(e) {
if (!this.playbackSession.coverPath) return if (!this.playbackSession.coverPath) return
const fac = new FastAverageColor() const fac = new FastAverageColor()
@@ -121,19 +204,32 @@ export default {
}) })
}, },
playPause() { playPause() {
if (this.isPlaying) {
this.pause()
} else {
this.play()
}
},
play() {
if (!this.localAudioPlayer || !this.hasLoaded) return if (!this.localAudioPlayer || !this.hasLoaded) return
this.localAudioPlayer.playPause() this.localAudioPlayer.play()
},
pause() {
if (!this.localAudioPlayer || !this.hasLoaded) return
this.localAudioPlayer.pause()
}, },
jumpForward() { jumpForward() {
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 +250,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
@@ -205,6 +302,7 @@ export default {
} else { } else {
this.stopPlayInterval() this.stopPlayInterval()
} }
this.updateMediaSessionPlaybackState()
}, },
playerTimeUpdate(time) { playerTimeUpdate(time) {
this.setCurrentTime(time) this.setCurrentTime(time)
@@ -246,9 +344,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)
@@ -263,6 +366,8 @@ export default {
this.localAudioPlayer.on('timeupdate', this.playerTimeUpdate.bind(this)) this.localAudioPlayer.on('timeupdate', this.playerTimeUpdate.bind(this))
this.localAudioPlayer.on('error', this.playerError.bind(this)) this.localAudioPlayer.on('error', this.playerError.bind(this))
this.localAudioPlayer.on('finished', this.playerFinished.bind(this)) this.localAudioPlayer.on('finished', this.playerFinished.bind(this))
this.setMediaSession()
}, },
beforeDestroy() { beforeDestroy() {
window.removeEventListener('resize', this.resize) window.removeEventListener('resize', this.resize)
+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": "ØĨØ¯Ø§ØąØŠ Ø§Ų„ØšŲ„Ø§Ų…Ø§ØĒ"
}
-2
View File
@@ -629,7 +629,6 @@
"MessageItemsSelected": "{0} Đ¸ĐˇĐąŅ€Đ°ĐŊи", "MessageItemsSelected": "{0} Đ¸ĐˇĐąŅ€Đ°ĐŊи",
"MessageItemsUpdated": "{0} ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ° ОйĐŊОвĐĩĐŊи", "MessageItemsUpdated": "{0} ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ° ОйĐŊОвĐĩĐŊи",
"MessageJoinUsOn": "ĐŸŅ€Đ¸ŅŅŠĐĩдиĐŊĐĩŅ‚Đĩ ҁĐĩ ĐēҊĐŧ ĐŊĐ°Ņ", "MessageJoinUsOn": "ĐŸŅ€Đ¸ŅŅŠĐĩдиĐŊĐĩŅ‚Đĩ ҁĐĩ ĐēҊĐŧ ĐŊĐ°Ņ",
"MessageListeningSessionsInTheLastYear": "{0} ҁĐģŅƒŅˆĐ°Ņ‚ĐĩĐģҁĐēи ҁĐĩŅĐ¸Đ¸ ĐŋŅ€ĐĩС ĐŋĐžŅĐģĐĩĐ´ĐŊĐ°Ņ‚Đ° ĐŗĐžĐ´Đ¸ĐŊа",
"MessageLoading": "Đ—Đ°Ņ€ĐĩĐļдаĐŊĐĩ...", "MessageLoading": "Đ—Đ°Ņ€ĐĩĐļдаĐŊĐĩ...",
"MessageLoadingFolders": "Đ—Đ°Ņ€ĐĩĐļдаĐŊĐĩ ĐŊа ПаĐŋĐēи...", "MessageLoadingFolders": "Đ—Đ°Ņ€ĐĩĐļдаĐŊĐĩ ĐŊа ПаĐŋĐēи...",
"MessageM4BFailed": "M4B ĐŸŅ€ĐžĐ˛Đ°ĐģĐĩĐŊĐž!", "MessageM4BFailed": "M4B ĐŸŅ€ĐžĐ˛Đ°ĐģĐĩĐŊĐž!",
@@ -729,7 +728,6 @@
"ToastBookmarkUpdateSuccess": "ĐžŅ‚ĐŧĐĩŅ‚ĐēĐ°Ņ‚Đ° Đĩ ОйĐŊОвĐĩĐŊа", "ToastBookmarkUpdateSuccess": "ĐžŅ‚ĐŧĐĩŅ‚ĐēĐ°Ņ‚Đ° Đĩ ОйĐŊОвĐĩĐŊа",
"ToastChaptersHaveErrors": "ГĐģĐ°Đ˛Đ¸Ņ‚Đĩ иĐŧĐ°Ņ‚ ĐŗŅ€Đĩ҈Đēи", "ToastChaptersHaveErrors": "ГĐģĐ°Đ˛Đ¸Ņ‚Đĩ иĐŧĐ°Ņ‚ ĐŗŅ€Đĩ҈Đēи",
"ToastChaptersMustHaveTitles": "ГĐģĐ°Đ˛Đ¸Ņ‚Đĩ Ņ‚Ņ€ŅĐąĐ˛Đ° да иĐŧĐ°Ņ‚ ĐˇĐ°ĐŗĐģĐ°Đ˛Đ¸Ņ", "ToastChaptersMustHaveTitles": "ГĐģĐ°Đ˛Đ¸Ņ‚Đĩ Ņ‚Ņ€ŅĐąĐ˛Đ° да иĐŧĐ°Ņ‚ ĐˇĐ°ĐŗĐģĐ°Đ˛Đ¸Ņ",
"ToastCollectionItemsRemoveSuccess": "ЕĐģĐĩĐŧĐĩĐŊŅ‚(и) ĐŋŅ€ĐĩĐŧĐ°Ņ…ĐŊĐ°Ņ‚Đ¸ ĐžŅ‚ ĐēĐžĐģĐĩĐēŅ†Đ¸Ņ",
"ToastCollectionRemoveSuccess": "КоĐģĐĩĐēŅ†Đ¸ŅŅ‚Đ° Đĩ ĐŋŅ€ĐĩĐŧĐ°Ņ…ĐŊĐ°Ņ‚Đ°", "ToastCollectionRemoveSuccess": "КоĐģĐĩĐēŅ†Đ¸ŅŅ‚Đ° Đĩ ĐŋŅ€ĐĩĐŧĐ°Ņ…ĐŊĐ°Ņ‚Đ°",
"ToastCollectionUpdateSuccess": "КоĐģĐĩĐēŅ†Đ¸ŅŅ‚Đ° Đĩ ОйĐŊОвĐĩĐŊа", "ToastCollectionUpdateSuccess": "КоĐģĐĩĐēŅ†Đ¸ŅŅ‚Đ° Đĩ ОйĐŊОвĐĩĐŊа",
"ToastItemCoverUpdateSuccess": "ĐšĐžŅ€Đ¸Ņ†Đ°Ņ‚Đ° ĐŊа ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ° Đĩ ОйĐŊОвĐĩĐŊа", "ToastItemCoverUpdateSuccess": "ĐšĐžŅ€Đ¸Ņ†Đ°Ņ‚Đ° ĐŊа ĐĩĐģĐĩĐŧĐĩĐŊŅ‚Đ° Đĩ ОйĐŊОвĐĩĐŊа",
+82 -3
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}",
@@ -710,7 +763,6 @@
"MessageItemsSelected": "{0}āϟāĻŋ āφāχāĻŸā§‡āĻŽ āύāĻŋāĻ°ā§āĻŦāĻžāϚāĻŋāϤ", "MessageItemsSelected": "{0}āϟāĻŋ āφāχāĻŸā§‡āĻŽ āύāĻŋāĻ°ā§āĻŦāĻžāϚāĻŋāϤ",
"MessageItemsUpdated": "{0}āϟāĻŋ āφāχāĻŸā§‡āĻŽ āφāĻĒāĻĄā§‡āϟ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇", "MessageItemsUpdated": "{0}āϟāĻŋ āφāχāĻŸā§‡āĻŽ āφāĻĒāĻĄā§‡āϟ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇",
"MessageJoinUsOn": "āφāĻŽāĻžāĻĻ⧇āϰ āϏāĻžāĻĨ⧇ āϝ⧋āĻ— āĻĻāĻŋāύ", "MessageJoinUsOn": "āφāĻŽāĻžāĻĻ⧇āϰ āϏāĻžāĻĨ⧇ āϝ⧋āĻ— āĻĻāĻŋāύ",
"MessageListeningSessionsInTheLastYear": "āĻ—āϤ āĻŦāĻ›āϰ⧇ {0}āϟāĻŋ āĻļā§‹āύāĻžāϰ āϏ⧇āĻļāύ",
"MessageLoading": "āϞ⧋āĻĄ āĻšāĻšā§āϛ⧇.āĨ¤", "MessageLoading": "āϞ⧋āĻĄ āĻšāĻšā§āϛ⧇.āĨ¤",
"MessageLoadingFolders": "āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰ āϞ⧋āĻĄ āĻšāĻšā§āϛ⧇...", "MessageLoadingFolders": "āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰ āϞ⧋āĻĄ āĻšāĻšā§āϛ⧇...",
"MessageLogsDescription": "āϞāĻ—āϗ⧁āϞāĻŋ JSON āĻĢāĻžāχāϞ āĻšāĻŋāϏāĻžāĻŦ⧇ <code>/metadata/logs</code>-āĻ āϏāĻ‚āϰāĻ•ā§āώāĻŖ āĻ•āϰāĻž āĻšāϝāĻŧāĨ¤ āĻ•ā§āĻ°ā§āϝāĻžāĻļ āϞāĻ—āϗ⧁āϞāĻŋ <code>/metadata/logs/crash_logs.txt</code>-āĻ āϏāĻ‚āϰāĻ•ā§āώāĻŖ āĻ•āϰāĻž āĻšāϝāĻŧāĨ¤", "MessageLogsDescription": "āϞāĻ—āϗ⧁āϞāĻŋ JSON āĻĢāĻžāχāϞ āĻšāĻŋāϏāĻžāĻŦ⧇ <code>/metadata/logs</code>-āĻ āϏāĻ‚āϰāĻ•ā§āώāĻŖ āĻ•āϰāĻž āĻšāϝāĻŧāĨ¤ āĻ•ā§āĻ°ā§āϝāĻžāĻļ āϞāĻ—āϗ⧁āϞāĻŋ <code>/metadata/logs/crash_logs.txt</code>-āĻ āϏāĻ‚āϰāĻ•ā§āώāĻŖ āĻ•āϰāĻž āĻšāϝāĻŧāĨ¤",
@@ -744,6 +796,7 @@
"MessageNoLogs": "āϕ⧋āύāĻ“ āϞāĻ— āύ⧇āχ", "MessageNoLogs": "āϕ⧋āύāĻ“ āϞāĻ— āύ⧇āχ",
"MessageNoMediaProgress": "āĻŽāĻŋāĻĄāĻŋāϝāĻŧāĻž āĻ…āĻ—ā§āϰāĻ—āϤāĻŋ āύ⧇āχ", "MessageNoMediaProgress": "āĻŽāĻŋāĻĄāĻŋāϝāĻŧāĻž āĻ…āĻ—ā§āϰāĻ—āϤāĻŋ āύ⧇āχ",
"MessageNoNotifications": "āϕ⧋āύ⧋ āĻŦāĻŋāĻœā§āĻžāĻĒā§āϤāĻŋ āύ⧇āχ", "MessageNoNotifications": "āϕ⧋āύ⧋ āĻŦāĻŋāĻœā§āĻžāĻĒā§āϤāĻŋ āύ⧇āχ",
"MessageNoPodcastFeed": "āĻ…āĻŦ⧈āϧ āĻĒāĻĄāĻ•āĻžāĻ¸ā§āϟ: āϕ⧋āύ⧋ āĻĢāĻŋāĻĄ āύ⧇āχ",
"MessageNoPodcastsFound": "āϕ⧋āύ āĻĒāĻĄāĻ•āĻžāĻ¸ā§āϟ āĻĒāĻžāĻ“āϝāĻŧāĻž āϝāĻžāϝāĻŧāύāĻŋ", "MessageNoPodcastsFound": "āϕ⧋āύ āĻĒāĻĄāĻ•āĻžāĻ¸ā§āϟ āĻĒāĻžāĻ“āϝāĻŧāĻž āϝāĻžāϝāĻŧāύāĻŋ",
"MessageNoResults": "āϕ⧋āύ āĻĢāϞāĻžāĻĢāϞ āύ⧇āχ", "MessageNoResults": "āϕ⧋āύ āĻĢāϞāĻžāĻĢāϞ āύ⧇āχ",
"MessageNoSearchResultsFor": "\"{0}\" āĻāϰ āϜāĻ¨ā§āϝ āϕ⧋āύ āĻ…āύ⧁āϏāĻ¨ā§āϧāĻžāύ āĻĢāϞāĻžāĻĢāϞ āύ⧇āχ", "MessageNoSearchResultsFor": "\"{0}\" āĻāϰ āϜāĻ¨ā§āϝ āϕ⧋āύ āĻ…āύ⧁āϏāĻ¨ā§āϧāĻžāύ āĻĢāϞāĻžāĻĢāϞ āύ⧇āχ",
@@ -760,6 +813,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 +859,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 +886,10 @@
"NoteUploaderFoldersWithMediaFiles": "āĻŽāĻŋāĻĄāĻŋāϝāĻŧāĻž āĻĢāĻžāχāϞ āϏāĻš āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰāϗ⧁āϞāĻŋ āφāϞāĻžāĻĻāĻž āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āφāχāĻŸā§‡āĻŽ āĻšāĻŋāϏāĻžāĻŦ⧇ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰāĻž āĻšāĻŦ⧇āĨ¤", "NoteUploaderFoldersWithMediaFiles": "āĻŽāĻŋāĻĄāĻŋāϝāĻŧāĻž āĻĢāĻžāχāϞ āϏāĻš āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰāϗ⧁āϞāĻŋ āφāϞāĻžāĻĻāĻž āϞāĻžāχāĻŦā§āϰ⧇āϰāĻŋ āφāχāĻŸā§‡āĻŽ āĻšāĻŋāϏāĻžāĻŦ⧇ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰāĻž āĻšāĻŦ⧇āĨ¤",
"NoteUploaderOnlyAudioFiles": "āϝāĻĻāĻŋ āĻļ⧁āϧ⧁āĻŽāĻžāĻ¤ā§āϰ āĻ…āĻĄāĻŋāĻ“ āĻĢāĻžāχāϞ āφāĻĒāϞ⧋āĻĄ āĻ•āϰāĻž āĻšāϝāĻŧ āϤāĻŦ⧇ āĻĒā§āϰāϤāĻŋāϟāĻŋ āĻ…āĻĄāĻŋāĻ“ āĻĢāĻžāχāϞ āĻāĻ•āϟāĻŋ āĻĒ⧃āĻĨāĻ• āĻ…āĻĄāĻŋāĻ“āĻŦ⧁āĻ• āĻšāĻŋāϏāĻžāĻŦ⧇ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰāĻž āĻšāĻŦ⧇āĨ¤", "NoteUploaderOnlyAudioFiles": "āϝāĻĻāĻŋ āĻļ⧁āϧ⧁āĻŽāĻžāĻ¤ā§āϰ āĻ…āĻĄāĻŋāĻ“ āĻĢāĻžāχāϞ āφāĻĒāϞ⧋āĻĄ āĻ•āϰāĻž āĻšāϝāĻŧ āϤāĻŦ⧇ āĻĒā§āϰāϤāĻŋāϟāĻŋ āĻ…āĻĄāĻŋāĻ“ āĻĢāĻžāχāϞ āĻāĻ•āϟāĻŋ āĻĒ⧃āĻĨāĻ• āĻ…āĻĄāĻŋāĻ“āĻŦ⧁āĻ• āĻšāĻŋāϏāĻžāĻŦ⧇ āĻĒāϰāĻŋāϚāĻžāϞāύāĻž āĻ•āϰāĻž āĻšāĻŦ⧇āĨ¤",
"NoteUploaderUnsupportedFiles": "āĻ…āϏāĻŽāĻ°ā§āĻĨāĻŋāϤ āĻĢāĻžāχāϞāϗ⧁āϞāĻŋ āωāĻĒ⧇āĻ•ā§āώāĻž āĻ•āϰāĻž āĻšāϝāĻŧāĨ¤ āĻāĻ•āϟāĻŋ āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰ āĻŦ⧇āϛ⧇ āύ⧇āĻ“āϝāĻŧāĻž āĻŦāĻž āĻĢ⧇āϞ⧇ āĻĻ⧇āĻ“āϝāĻŧāĻžāϰ āϏāĻŽāϝāĻŧ, āφāχāĻŸā§‡āĻŽ āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰ⧇ āύ⧇āχ āĻāĻŽāύ āĻ…āĻ¨ā§āϝāĻžāĻ¨ā§āϝ āĻĢāĻžāχāϞāϗ⧁āϞāĻŋ āωāĻĒ⧇āĻ•ā§āώāĻž āĻ•āϰāĻž āĻšāϝāĻŧāĨ¤", "NoteUploaderUnsupportedFiles": "āĻ…āϏāĻŽāĻ°ā§āĻĨāĻŋāϤ āĻĢāĻžāχāϞāϗ⧁āϞāĻŋ āωāĻĒ⧇āĻ•ā§āώāĻž āĻ•āϰāĻž āĻšāϝāĻŧāĨ¤ āĻāĻ•āϟāĻŋ āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰ āĻŦ⧇āϛ⧇ āύ⧇āĻ“āϝāĻŧāĻž āĻŦāĻž āĻĢ⧇āϞ⧇ āĻĻ⧇āĻ“āϝāĻŧāĻžāϰ āϏāĻŽāϝāĻŧ, āφāχāĻŸā§‡āĻŽ āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰ⧇ āύ⧇āχ āĻāĻŽāύ āĻ…āĻ¨ā§āϝāĻžāĻ¨ā§āϝ āĻĢāĻžāχāϞāϗ⧁āϞāĻŋ āωāĻĒ⧇āĻ•ā§āώāĻž āĻ•āϰāĻž āĻšāϝāĻŧāĨ¤",
"NotificationOnBackupCompletedDescription": "āĻŦā§āϝāĻžāĻ•āφāĻĒ āϏāĻŽā§āĻĒā§‚āĻ°ā§āĻŖ āĻšāϞ⧇ āĻŸā§āϰāĻŋāĻ—āĻžāϰ āĻšāĻŦ⧇",
"NotificationOnBackupFailedDescription": "āĻŦā§āϝāĻžāĻ•āφāĻĒ āĻŦā§āϝāĻ°ā§āĻĨ āĻšāϞ⧇ āĻŸā§āϰāĻŋāĻ—āĻžāϰ āĻšāĻŦ⧇",
"NotificationOnEpisodeDownloadedDescription": "āĻāĻ•āϟāĻŋ āĻĒāĻĄāĻ•āĻžāĻ¸ā§āϟ āĻĒāĻ°ā§āĻŦ āĻ¸ā§āĻŦāϝāĻŧāĻ‚āĻ•ā§āϰāĻŋāϝāĻŧāĻ­āĻžāĻŦ⧇ āĻĄāĻžāωāύāϞ⧋āĻĄ āĻšāϞ⧇ āĻŸā§āϰāĻŋāĻ—āĻžāϰ āĻšāĻŦ⧇",
"NotificationOnTestDescription": "āĻŦāĻŋāĻœā§āĻžāĻĒā§āϤāĻŋ āϏāĻŋāĻ¸ā§āĻŸā§‡āĻŽ āĻĒāϰ⧀āĻ•ā§āώāĻžāϰ āϜāĻ¨ā§āϝ āχāϭ⧇āĻ¨ā§āϟ",
"PlaceholderNewCollection": "āύāϤ⧁āύ āϏāĻ‚āĻ—ā§āϰāĻšā§‡āϰ āύāĻžāĻŽ", "PlaceholderNewCollection": "āύāϤ⧁āύ āϏāĻ‚āĻ—ā§āϰāĻšā§‡āϰ āύāĻžāĻŽ",
"PlaceholderNewFolderPath": "āύāϤ⧁āύ āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰ āĻĒāĻĨ", "PlaceholderNewFolderPath": "āύāϤ⧁āύ āĻĢā§‹āĻ˛ā§āĻĄāĻžāϰ āĻĒāĻĨ",
"PlaceholderNewPlaylist": "āύāϤ⧁āύ āĻĒā§āϞ⧇āϞāĻŋāĻ¸ā§āĻŸā§‡āϰ āύāĻžāĻŽ", "PlaceholderNewPlaylist": "āύāϤ⧁āύ āĻĒā§āϞ⧇āϞāĻŋāĻ¸ā§āĻŸā§‡āϰ āύāĻžāĻŽ",
@@ -851,6 +915,7 @@
"StatsYearInReview": "āĻŦāĻžā§ŽāϏāϰāĻŋāĻ• āĻĒāĻ°ā§āϝāĻžāϞ⧋āϚāύāĻž", "StatsYearInReview": "āĻŦāĻžā§ŽāϏāϰāĻŋāĻ• āĻĒāĻ°ā§āϝāĻžāϞ⧋āϚāύāĻž",
"ToastAccountUpdateSuccess": "āĻ…ā§āϝāĻžāĻ•āĻžāωāĻ¨ā§āϟ āφāĻĒāĻĄā§‡āϟ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇", "ToastAccountUpdateSuccess": "āĻ…ā§āϝāĻžāĻ•āĻžāωāĻ¨ā§āϟ āφāĻĒāĻĄā§‡āϟ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇",
"ToastAppriseUrlRequired": "āĻāĻ•āϟāĻŋ Apprise āχāωāφāϰāĻāϞ āϞāĻŋāĻ–āϤ⧇ āĻšāĻŦ⧇", "ToastAppriseUrlRequired": "āĻāĻ•āϟāĻŋ Apprise āχāωāφāϰāĻāϞ āϞāĻŋāĻ–āϤ⧇ āĻšāĻŦ⧇",
"ToastAsinRequired": "ASIN āĻĒā§āϰāϝāĻŧā§‹āϜāύ",
"ToastAuthorImageRemoveSuccess": "āϞ⧇āĻ–āϕ⧇āϰ āĻ›āĻŦāĻŋ āϏāϰāĻžāύ⧋ āĻšāϝāĻŧ⧇āϛ⧇", "ToastAuthorImageRemoveSuccess": "āϞ⧇āĻ–āϕ⧇āϰ āĻ›āĻŦāĻŋ āϏāϰāĻžāύ⧋ āĻšāϝāĻŧ⧇āϛ⧇",
"ToastAuthorNotFound": "āϞ⧇āĻ–āĻ• \"{0}\" āϖ⧁āρāĻœā§‡ āĻĒāĻžāĻ“āϝāĻŧāĻž āϝāĻžāϝāĻŧāύāĻŋ", "ToastAuthorNotFound": "āϞ⧇āĻ–āĻ• \"{0}\" āϖ⧁āρāĻœā§‡ āĻĒāĻžāĻ“āϝāĻŧāĻž āϝāĻžāϝāĻŧāύāĻŋ",
"ToastAuthorRemoveSuccess": "āϞ⧇āĻ–āĻ• āϏāϰāĻžāύ⧋ āĻšāϝāĻŧ⧇āϛ⧇", "ToastAuthorRemoveSuccess": "āϞ⧇āĻ–āĻ• āϏāϰāĻžāύ⧋ āĻšāϝāĻŧ⧇āϛ⧇",
@@ -870,6 +935,8 @@
"ToastBackupUploadSuccess": "āĻŦā§āϝāĻžāĻ•āφāĻĒ āφāĻĒāϞ⧋āĻĄ āĻšāϝāĻŧ⧇āϛ⧇", "ToastBackupUploadSuccess": "āĻŦā§āϝāĻžāĻ•āφāĻĒ āφāĻĒāϞ⧋āĻĄ āĻšāϝāĻŧ⧇āϛ⧇",
"ToastBatchDeleteFailed": "āĻŦā§āϝāĻžāϚ āĻŽā§āϛ⧇ āĻĢ⧇āϞāϤ⧇ āĻŦā§āϝāĻ°ā§āĻĨ āĻšāϝāĻŧ⧇āϛ⧇", "ToastBatchDeleteFailed": "āĻŦā§āϝāĻžāϚ āĻŽā§āϛ⧇ āĻĢ⧇āϞāϤ⧇ āĻŦā§āϝāĻ°ā§āĻĨ āĻšāϝāĻŧ⧇āϛ⧇",
"ToastBatchDeleteSuccess": "āĻŦā§āϝāĻžāϚ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻž āϏāĻĢāϞ āĻšā§Ÿā§‡āϛ⧇", "ToastBatchDeleteSuccess": "āĻŦā§āϝāĻžāϚ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻž āϏāĻĢāϞ āĻšā§Ÿā§‡āϛ⧇",
"ToastBatchQuickMatchFailed": "āĻŦā§āϝāĻžāϚ āϕ⧁āχāĻ• āĻŽā§āϝāĻžāϚ āĻŦā§āϝāĻ°ā§āĻĨ!",
"ToastBatchQuickMatchStarted": "{0}āϟāĻŋ āĻŦāχāϝāĻŧ⧇āϰ āĻŦā§āϝāĻžāϚ āϕ⧁āχāĻ• āĻŽā§āϝāĻžāϚ āĻļ⧁āϰ⧁ āĻšāϝāĻŧ⧇āϛ⧇!",
"ToastBatchUpdateFailed": "āĻŦā§āϝāĻžāϚ āφāĻĒāĻĄā§‡āϟ āĻŦā§āϝāĻ°ā§āĻĨ āĻšāϝāĻŧ⧇āϛ⧇", "ToastBatchUpdateFailed": "āĻŦā§āϝāĻžāϚ āφāĻĒāĻĄā§‡āϟ āĻŦā§āϝāĻ°ā§āĻĨ āĻšāϝāĻŧ⧇āϛ⧇",
"ToastBatchUpdateSuccess": "āĻŦā§āϝāĻžāϚ āφāĻĒāĻĄā§‡āϟ āϏāĻžāĻĢāĻ˛ā§āϝ", "ToastBatchUpdateSuccess": "āĻŦā§āϝāĻžāϚ āφāĻĒāĻĄā§‡āϟ āϏāĻžāĻĢāĻ˛ā§āϝ",
"ToastBookmarkCreateFailed": "āĻŦ⧁āĻ•āĻŽāĻžāĻ°ā§āĻ• āϤ⧈āϰāĻŋ āĻ•āϰāϤ⧇ āĻŦā§āϝāĻ°ā§āĻĨ", "ToastBookmarkCreateFailed": "āĻŦ⧁āĻ•āĻŽāĻžāĻ°ā§āĻ• āϤ⧈āϰāĻŋ āĻ•āϰāϤ⧇ āĻŦā§āϝāĻ°ā§āĻĨ",
@@ -881,9 +948,8 @@
"ToastChaptersHaveErrors": "āĻ…āĻ§ā§āϝāĻžāϝāĻŧ⧇ āĻ¤ā§āϰ⧁āϟāĻŋ āφāϛ⧇", "ToastChaptersHaveErrors": "āĻ…āĻ§ā§āϝāĻžāϝāĻŧ⧇ āĻ¤ā§āϰ⧁āϟāĻŋ āφāϛ⧇",
"ToastChaptersMustHaveTitles": "āĻ…āĻ§ā§āϝāĻžāϝāĻŧ⧇āϰ āĻļāĻŋāϰ⧋āύāĻžāĻŽ āĻĨāĻžāĻ•āϤ⧇ āĻšāĻŦ⧇", "ToastChaptersMustHaveTitles": "āĻ…āĻ§ā§āϝāĻžāϝāĻŧ⧇āϰ āĻļāĻŋāϰ⧋āύāĻžāĻŽ āĻĨāĻžāĻ•āϤ⧇ āĻšāĻŦ⧇",
"ToastChaptersRemoved": "āĻ…āĻ§ā§āϝāĻžāϝāĻŧāϗ⧁āϞ⧋ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻž āĻšāϝāĻŧ⧇āϛ⧇", "ToastChaptersRemoved": "āĻ…āĻ§ā§āϝāĻžāϝāĻŧāϗ⧁āϞ⧋ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻž āĻšāϝāĻŧ⧇āϛ⧇",
"ToastChaptersUpdated": "āĻ…āĻ§ā§āϝāĻžāϝāĻŧ āφāĻĒāĻĄā§‡āϟ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇",
"ToastCollectionItemsAddFailed": "āφāχāĻŸā§‡āĻŽ(āϗ⧁āϞāĻŋ) āϏāĻ‚āĻ—ā§āϰāĻšā§‡ āϝ⧋āĻ— āĻ•āϰāĻž āĻŦā§āϝāĻ°ā§āĻĨ āĻšāϝāĻŧ⧇āϛ⧇", "ToastCollectionItemsAddFailed": "āφāχāĻŸā§‡āĻŽ(āϗ⧁āϞāĻŋ) āϏāĻ‚āĻ—ā§āϰāĻšā§‡ āϝ⧋āĻ— āĻ•āϰāĻž āĻŦā§āϝāĻ°ā§āĻĨ āĻšāϝāĻŧ⧇āϛ⧇",
"ToastCollectionItemsAddSuccess": "āφāχāĻŸā§‡āĻŽ(āϗ⧁āϞāĻŋ) āϏāĻ‚āĻ—ā§āϰāĻšā§‡ āϝ⧋āĻ— āĻ•āϰāĻž āϏāĻĢāϞ āĻšāϝāĻŧ⧇āϛ⧇",
"ToastCollectionItemsRemoveSuccess": "āφāχāĻŸā§‡āĻŽ(āϗ⧁āϞāĻŋ) āϏāĻ‚āĻ—ā§āϰāĻš āĻĨ⧇āϕ⧇ āϏāϰāĻžāύ⧋ āĻšāϝāĻŧ⧇āϛ⧇",
"ToastCollectionRemoveSuccess": "āϏāĻ‚āĻ—ā§āϰāĻš āϏāϰāĻžāύ⧋ āĻšāϝāĻŧ⧇āϛ⧇", "ToastCollectionRemoveSuccess": "āϏāĻ‚āĻ—ā§āϰāĻš āϏāϰāĻžāύ⧋ āĻšāϝāĻŧ⧇āϛ⧇",
"ToastCollectionUpdateSuccess": "āϏāĻ‚āĻ—ā§āϰāĻš āφāĻĒāĻĄā§‡āϟ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇", "ToastCollectionUpdateSuccess": "āϏāĻ‚āĻ—ā§āϰāĻš āφāĻĒāĻĄā§‡āϟ āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇",
"ToastCoverUpdateFailed": "āĻ•āĻ­āĻžāϰ āφāĻĒāĻĄā§‡āϟ āĻŦā§āϝāĻ°ā§āĻĨ āĻšāϝāĻŧ⧇āϛ⧇", "ToastCoverUpdateFailed": "āĻ•āĻ­āĻžāϰ āφāĻĒāĻĄā§‡āϟ āĻŦā§āϝāĻ°ā§āĻĨ āĻšāϝāĻŧ⧇āϛ⧇",
@@ -898,11 +964,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 +989,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 +1023,7 @@
"ToastPodcastGetFeedFailed": "āĻĒāĻĄāĻ•āĻžāĻ¸ā§āϟ āĻĢāĻŋāĻĄ āĻĒ⧇āϤ⧇ āĻŦā§āϝāĻ°ā§āĻĨ āĻšāϝāĻŧ⧇āϛ⧇", "ToastPodcastGetFeedFailed": "āĻĒāĻĄāĻ•āĻžāĻ¸ā§āϟ āĻĢāĻŋāĻĄ āĻĒ⧇āϤ⧇ āĻŦā§āϝāĻ°ā§āĻĨ āĻšāϝāĻŧ⧇āϛ⧇",
"ToastPodcastNoEpisodesInFeed": "āφāϰāĻāϏāĻāϏ āĻĢāĻŋāĻĄā§‡ āϕ⧋āύ⧋ āĻĒāĻ°ā§āĻŦ āĻĒāĻžāĻ“āϝāĻŧāĻž āϝāĻžāϝāĻŧāύāĻŋ", "ToastPodcastNoEpisodesInFeed": "āφāϰāĻāϏāĻāϏ āĻĢāĻŋāĻĄā§‡ āϕ⧋āύ⧋ āĻĒāĻ°ā§āĻŦ āĻĒāĻžāĻ“āϝāĻŧāĻž āϝāĻžāϝāĻŧāύāĻŋ",
"ToastPodcastNoRssFeed": "āĻĒāĻĄāĻ•āĻžāĻ¸ā§āĻŸā§‡āϰ āϕ⧋āύ āφāϰāĻāϏāĻāϏ āĻĢāĻŋāĻĄ āύ⧇āχ", "ToastPodcastNoRssFeed": "āĻĒāĻĄāĻ•āĻžāĻ¸ā§āĻŸā§‡āϰ āϕ⧋āύ āφāϰāĻāϏāĻāϏ āĻĢāĻŋāĻĄ āύ⧇āχ",
"ToastProgressIsNotBeingSynced": "āĻ…āĻ—ā§āϰāĻ—āϤāĻŋ āϏāĻŋāĻ™ā§āĻ• āĻšāĻšā§āϛ⧇ āύāĻž, āĻĒā§āϞ⧇āĻŦā§āϝāĻžāĻ• āĻĒ⧁āύāϰāĻžāϝāĻŧ āϚāĻžāϞ⧁ āĻ•āϰ⧁āύ",
"ToastProviderCreatedFailed": "āĻĒā§āϰāĻĻāĻžāύāĻ•āĻžāϰ⧀ āϝ⧋āĻ— āĻ•āϰāϤ⧇ āĻŦā§āϝāĻ°ā§āĻĨ āĻšāϝāĻŧ⧇āϛ⧇", "ToastProviderCreatedFailed": "āĻĒā§āϰāĻĻāĻžāύāĻ•āĻžāϰ⧀ āϝ⧋āĻ— āĻ•āϰāϤ⧇ āĻŦā§āϝāĻ°ā§āĻĨ āĻšāϝāĻŧ⧇āϛ⧇",
"ToastProviderCreatedSuccess": "āύāϤ⧁āύ āĻĒā§āϰāĻĻāĻžāύāĻ•āĻžāϰ⧀ āϝ⧋āĻ— āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇", "ToastProviderCreatedSuccess": "āύāϤ⧁āύ āĻĒā§āϰāĻĻāĻžāύāĻ•āĻžāϰ⧀ āϝ⧋āĻ— āĻ•āϰāĻž āĻšāϝāĻŧ⧇āϛ⧇",
"ToastProviderNameAndUrlRequired": "āύāĻžāĻŽ āĻāĻŦāĻ‚ āχāωāφāϰāĻāϞ āφāĻŦāĻļā§āϝāĻ•", "ToastProviderNameAndUrlRequired": "āύāĻžāĻŽ āĻāĻŦāĻ‚ āχāωāφāϰāĻāϞ āφāĻŦāĻļā§āϝāĻ•",
@@ -972,6 +1050,7 @@
"ToastSessionCloseFailed": "āĻ…āϧāĻŋāĻŦ⧇āĻļāύ āĻŦāĻ¨ā§āϧ āĻ•āϰāϤ⧇ āĻŦā§āϝāĻ°ā§āĻĨ āĻšāϝāĻŧ⧇āϛ⧇", "ToastSessionCloseFailed": "āĻ…āϧāĻŋāĻŦ⧇āĻļāύ āĻŦāĻ¨ā§āϧ āĻ•āϰāϤ⧇ āĻŦā§āϝāĻ°ā§āĻĨ āĻšāϝāĻŧ⧇āϛ⧇",
"ToastSessionDeleteFailed": "āϏ⧇āĻļāύ āĻŽā§āϛ⧇ āĻĢ⧇āϞāϤ⧇ āĻŦā§āϝāĻ°ā§āĻĨ", "ToastSessionDeleteFailed": "āϏ⧇āĻļāύ āĻŽā§āϛ⧇ āĻĢ⧇āϞāϤ⧇ āĻŦā§āϝāĻ°ā§āĻĨ",
"ToastSessionDeleteSuccess": "āϏ⧇āĻļāύ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻž āĻšāϝāĻŧ⧇āϛ⧇", "ToastSessionDeleteSuccess": "āϏ⧇āĻļāύ āĻŽā§āϛ⧇ āĻĢ⧇āϞāĻž āĻšāϝāĻŧ⧇āϛ⧇",
"ToastSleepTimerDone": "āĻ¸ā§āϞāĻŋāĻĒ āϟāĻžāχāĻŽāĻžāϰ āĻšāϝāĻŧ⧇ āϗ⧇āϛ⧇... zZzzZz",
"ToastSlugMustChange": "āĻ¸ā§āϞāĻžāϗ⧇ āĻ…āĻŦ⧈āϧ āĻ…āĻ•ā§āώāϰ āϰāϝāĻŧ⧇āϛ⧇", "ToastSlugMustChange": "āĻ¸ā§āϞāĻžāϗ⧇ āĻ…āĻŦ⧈āϧ āĻ…āĻ•ā§āώāϰ āϰāϝāĻŧ⧇āϛ⧇",
"ToastSlugRequired": "āĻ¸ā§āϞāĻžāĻ— āφāĻŦāĻļā§āϝāĻ•", "ToastSlugRequired": "āĻ¸ā§āϞāĻžāĻ— āφāĻŦāĻļā§āϝāĻ•",
"ToastSocketConnected": "āϏāϕ⧇āϟ āϏāĻ‚āϝ⧁āĻ•ā§āϤ", "ToastSocketConnected": "āϏāϕ⧇āϟ āϏāĻ‚āϝ⧁āĻ•ā§āϤ",

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