Compare commits

...

445 Commits

Author SHA1 Message Date
advplyr
6a7469851d Adds box shadow to progress bar on covers for visibility #3825 2025-01-29 17:54:22 -06:00
advplyr
1d57daa9f9 Merge pull request #3907 from nichwall/close_blank_issues
Add: workflow to close blank issues
2025-01-29 17:01:20 -06:00
Nicholas Wallace
caf2b664f1 Add: workflow to close blank issues 2025-01-28 20:22:46 -07:00
advplyr
b3b2bd7772 Fix feeds for collection/series pub date increment #3442 2025-01-28 17:11:57 -06:00
advplyr
95864705dc Update clean database to remove invalid CollectionBook records 2025-01-28 16:58:42 -06:00
advplyr
0fbba3efbd Merge pull request #3906 from tharvik/master
server/podcast: stabilize random ID
2025-01-28 16:41:54 -06:00
tharvik
575927c101 server/podcast: stabilize random ID 2025-01-28 20:36:35 +01:00
advplyr
45aaaf9f0b Pass ChapterInfo to media session 2025-01-28 09:47:26 -06:00
advplyr
51704f41aa Merge pull request #3892 from glorenzen/feat/adjustable-playback-speed-increment-decrement
Add adjustable increment and decrement value for playback rate
2025-01-27 16:51:23 -06:00
advplyr
e701a0a32e Update playback rate display value number of decimals 2025-01-27 16:46:32 -06:00
advplyr
fbe186a925 Merge pull request #3899 from mikiher/pragma-from-env
Allows setting of some pragma values through environment variables
2025-01-27 16:21:40 -06:00
advplyr
6ed2b575b0 Merge pull request #3898 from mikiher/fix-batch-quick-match
Add bookSeries id attribute to findAllExpandedWhere
2025-01-26 13:27:41 -06:00
advplyr
558173e086 Update custom metadata provider results to sanitize html descriptions #3880 2025-01-26 10:51:18 -06:00
mikiher
23067e1818 Allows setting of some pragma values through environment variables 2025-01-26 13:44:57 +02:00
mikiher
9b4732c207 Add bookSeries id attribute to findAllExpandedWhere 2025-01-26 12:21:54 +02:00
advplyr
e096da1b4d Update description to div tag #3880 2025-01-25 14:12:10 -06:00
advplyr
a4d0f95ecc Merge pull request #3880 from mikiher/rich-text-book-descriptionss
Support rich text book descriptions
2025-01-25 13:42:37 -06:00
advplyr
922a5039ce Update descriptionPlain to only be available in json expanded 2025-01-25 13:30:30 -06:00
Greg Lorenzen
f258782e2e Handle playback rate increment and decrmenet value in UI 2025-01-25 01:59:24 +00:00
Greg Lorenzen
1ea1e60d4b Add string for LabelPlaybackRateIncrementDecrement 2025-01-25 01:58:48 +00:00
Greg Lorenzen
7c4bcfb4f9 Add dropdown to player settings modal to set the playbackRateIncrementDecrement amount 2025-01-25 01:58:13 +00:00
Greg Lorenzen
3eefe937d9 Add user setting value for playbackRateIncrementDecrement 2025-01-25 01:57:41 +00:00
advplyr
d4ba8b9d9f Fix server crash on failed to extract epub image #3889 2025-01-24 17:24:37 -06:00
advplyr
c735fea8ba Merge pull request #3871 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-01-24 17:22:04 -06:00
advplyr
9e3010681e Added translation using Weblate (Japanese) 2025-01-24 23:21:26 +00:00
thehijacker
c6f724edff Translated using Weblate (Slovenian)
Currently translated at 100.0% (1086 of 1086 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-01-24 23:21:26 +00:00
Максим Горпиніч
358c3a15b5 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1086 of 1086 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-01-24 23:21:25 +00:00
Jan-Eric Myhrgren
32819860aa Translated using Weblate (Swedish)
Currently translated at 84.4% (914 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-01-24 23:21:25 +00:00
Jan-Eric Myhrgren
7dff571fd5 Translated using Weblate (Swedish)
Currently translated at 83.4% (903 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-01-24 23:21:24 +00:00
Lucas
36dd96fd87 Translated using Weblate (Spanish)
Currently translated at 100.0% (1082 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2025-01-24 23:21:24 +00:00
Lucas
e6244b8676 Translated using Weblate (Spanish)
Currently translated at 99.9% (1081 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2025-01-24 23:21:23 +00:00
Milo Ivir
9b561e4367 Translated using Weblate (Croatian)
Currently translated at 100.0% (1082 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-01-24 23:21:23 +00:00
Charlie
d25b46e9fa Translated using Weblate (French)
Currently translated at 100.0% (1082 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2025-01-24 23:21:22 +00:00
Andreas Morell-Reng
7a89836c3e Translated using Weblate (Danish)
Currently translated at 100.0% (1082 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2025-01-24 23:21:22 +00:00
Andreas Morell-Reng
a9dd67cf75 Translated using Weblate (Danish)
Currently translated at 74.2% (803 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2025-01-24 23:21:21 +00:00
Andreas Morell-Reng
6f2384e4f2 Translated using Weblate (Danish)
Currently translated at 74.1% (802 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2025-01-24 23:21:20 +00:00
Andreas Morell-Reng
254558f7a6 Translated using Weblate (Danish)
Currently translated at 74.1% (802 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2025-01-24 23:21:20 +00:00
Andreas Morell-Reng
a4a7cddcff Translated using Weblate (Danish)
Currently translated at 74.1% (802 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2025-01-24 23:21:19 +00:00
Jan-Eric Myhrgren
fc116ce1ed Translated using Weblate (Swedish)
Currently translated at 83.4% (903 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-01-24 23:21:19 +00:00
Andreas Morell-Reng
f77dd6b1ad Translated using Weblate (Danish)
Currently translated at 74.1% (802 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2025-01-24 23:21:18 +00:00
advplyr
647a722b06 Update readme for subdirectory 2025-01-24 17:21:11 -06:00
advplyr
6ec33f4bfa Merge pull request #3884 from adjokic/patch-1
Update README on using websockets with Apache as a reverse proxy
2025-01-24 17:16:22 -06:00
advplyr
bb0cc1bb6f Merge pull request #3887 from advplyr/batch-edit-populate-map-details
Add populate map details from buttons to batch editor
2025-01-23 18:03:15 -06:00
advplyr
abb5bd3a2d Update string order 2025-01-23 17:58:53 -06:00
advplyr
79acc41d16 Add populate from buttons to batch edit 2025-01-23 17:49:58 -06:00
adjokic
9fbf57bbef Update README on using websockets with Apache as a reverse proxy 2025-01-22 22:10:38 -06:00
advplyr
598a93d224 Update copy to clipboard buttons to be standardized 2025-01-22 17:56:46 -06:00
mikiher
286185329d Support rich text book descriptions 2025-01-22 08:53:23 +02:00
advplyr
c3c846f82d Update rss feed copy to clipboard to show checkmark instead of toast 2025-01-21 17:58:10 -06:00
advplyr
66b90e0841 Version bump v2.18.1 2025-01-20 15:45:09 -06:00
advplyr
9b21812feb Merge pull request #3862 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-01-20 15:29:41 -06:00
Jan-Eric Myhrgren
e9d8b62858 Translated using Weblate (Swedish)
Currently translated at 83.4% (903 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-01-20 22:08:48 +01:00
Nicky Larstrup
6d5aeaa42f Translated using Weblate (Danish)
Currently translated at 69.4% (751 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2025-01-20 22:08:48 +01:00
SunSpring
3fd9721da6 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1082 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-01-20 22:08:48 +01:00
Jan-Eric Myhrgren
63b2c6a3ea Translated using Weblate (Swedish)
Currently translated at 83.4% (903 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-01-20 22:08:48 +01:00
ugyes
1506589ec8 Translated using Weblate (Hungarian)
Currently translated at 97.8% (1059 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2025-01-20 22:08:48 +01:00
Losicek
035590236b Translated using Weblate (Czech)
Currently translated at 100.0% (1082 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2025-01-20 22:08:48 +01:00
SunSpring
eea446e217 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1082 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-01-20 22:08:48 +01:00
J. Lavoie
63dc819728 Translated using Weblate (Italian)
Currently translated at 98.7% (1068 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2025-01-20 22:08:48 +01:00
J. Lavoie
ff537de132 Translated using Weblate (French)
Currently translated at 99.9% (1081 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2025-01-20 22:08:48 +01:00
J. Lavoie
56550157d1 Translated using Weblate (German)
Currently translated at 100.0% (1082 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-01-20 22:08:48 +01:00
advplyr
28681d3783 Merge pull request #3732 from Timtam/allow-mrss-item-enclosures-for-podcasts
check for mrss item media:content when extracting item enclosures
2025-01-20 15:08:43 -06:00
advplyr
24ce4a208d Merge pull request #3867 from advplyr/feed_generator_updates
Updates to generated RSS feed & Fix series/collection feeds
2025-01-20 15:02:24 -06:00
advplyr
b816c0e7c4 Fix opening feed for series and collections 2025-01-20 14:18:22 -06:00
advplyr
a8b92819d1 Update feed episode description to use CDATA 2025-01-20 14:04:18 -06:00
advplyr
54a4b09592 Update RSS feed to exclude empty tags, format duration, use CDATA 2025-01-20 13:57:56 -06:00
advplyr
f13283b950 Merge pull request #3864 from mikiher/subdir-support-fix-missing-img
Fix missing texture image & epub ebook url for subdirectory support
2025-01-20 09:09:39 -06:00
advplyr
78994b3589 Update epub ebook url to include routerBasePath 2025-01-20 09:06:45 -06:00
advplyr
6745efc4d6 Revert case-insensitive cache manager update in #3780 2025-01-20 08:59:45 -06:00
Toni Barth
bdd8e5bb58 Merge remote-tracking branch 'remotes/upstream/master' into allow-mrss-item-enclosures-for-podcasts 2025-01-20 10:28:09 +01:00
mikiher
6c540ad789 Fix missing texture image for subdirectory support 2025-01-20 08:38:58 +02:00
advplyr
64992b3308 Version bump v2.18.0 2025-01-19 17:11:36 -06:00
advplyr
ea9552e9a9 Merge pull request #3854 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-01-19 17:09:47 -06:00
J. Lavoie
60add37ba0 Translated using Weblate (Italian)
Currently translated at 98.6% (1067 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2025-01-19 18:56:13 +01:00
J. Lavoie
6182764340 Translated using Weblate (French)
Currently translated at 98.8% (1070 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2025-01-19 18:56:13 +01:00
J. Lavoie
d8de61437c Translated using Weblate (German)
Currently translated at 100.0% (1082 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-01-19 18:56:12 +01:00
J. Lavoie
ca5c8a4d41 Translated using Weblate (French)
Currently translated at 98.8% (1070 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2025-01-19 18:52:09 +01:00
thehijacker
152683ff9c Translated using Weblate (Slovenian)
Currently translated at 100.0% (1082 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-01-19 14:44:53 +00:00
Jan-Eric Myhrgren
0ac92b6dc1 Translated using Weblate (Swedish)
Currently translated at 82.7% (895 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-01-19 14:44:52 +00:00
Илья Червонный
831f9ab9e7 Translated using Weblate (Russian)
Currently translated at 100.0% (1082 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-01-19 14:44:51 +00:00
Максим Горпиніч
3a33553aec Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1082 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-01-19 14:44:51 +00:00
Kieli Puoli
94df14f0cb Translated using Weblate (Finnish)
Currently translated at 50.9% (551 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-01-19 14:44:50 +00:00
Максим Горпиніч
1d1bdb2f00 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1082 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-01-19 14:44:50 +00:00
Jan-Eric Myhrgren
3aa6b358b3 Translated using Weblate (Swedish)
Currently translated at 79.9% (865 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-01-19 14:44:49 +00:00
Kieli Puoli
6052bb9fda Translated using Weblate (Finnish)
Currently translated at 44.5% (482 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-01-19 14:44:48 +00:00
Kieli Puoli
76b270ddf6 Translated using Weblate (Finnish)
Currently translated at 44.4% (481 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-01-19 14:44:48 +00:00
Jan-Eric Myhrgren
318e57170d Translated using Weblate (Swedish)
Currently translated at 78.1% (846 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-01-19 14:44:47 +00:00
Kieli Puoli
5294335bca Translated using Weblate (Finnish)
Currently translated at 44.3% (480 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-01-19 14:44:46 +00:00
Kieli Puoli
68af5933e5 Translated using Weblate (Finnish)
Currently translated at 44.2% (479 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-01-19 14:44:46 +00:00
Kieli Puoli
bc2d7ff14d Translated using Weblate (Finnish)
Currently translated at 44.1% (478 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-01-19 14:44:45 +00:00
Jan-Eric Myhrgren
7d278ebc56 Translated using Weblate (Swedish)
Currently translated at 78.1% (846 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-01-19 14:44:45 +00:00
Kieli Puoli
47247323cf Translated using Weblate (Finnish)
Currently translated at 44.0% (477 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-01-19 14:44:44 +00:00
Jan-Eric Myhrgren
77ad9c8a16 Translated using Weblate (Swedish)
Currently translated at 78.1% (846 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-01-19 14:44:44 +00:00
advplyr
58ca26436d Merge pull request #3810 from mikiher/enable-subdirectory
Enable subdirectory support by default
2025-01-19 08:44:33 -06:00
advplyr
4a3254d338 Fix create library with mark media as finished when setting #3856 2025-01-18 15:57:44 -06:00
advplyr
ebaae98a12 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2025-01-17 17:21:39 -06:00
advplyr
4701b3ed0c Update audiobook rss feeds to increment pub dates in 1 minute intervals #3442 2025-01-17 17:21:35 -06:00
advplyr
4843be89e7 Merge pull request #3833 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-01-17 10:41:54 -06:00
advplyr
9a2fb49950 Merge branch 'master' into weblate-audiobookshelf-abs-web-client 2025-01-17 10:41:46 -06:00
advplyr
ecbcc8470b Merge pull request #3847 from advplyr/bookmark-modal-updates
Bookmark modal updates
2025-01-16 17:18:02 -06:00
advplyr
32b886a0c3 Update bookmark modal to scale with playback rate #3728 2025-01-16 17:06:06 -06:00
advplyr
2463c62bbf Update bookmark modal scrollable with create always visible, make UI consistent, hide create when bookmark already exists 2025-01-16 16:56:56 -06:00
Jan-Eric Myhrgren
d55faabb6d Translated using Weblate (Swedish)
Currently translated at 77.4% (839 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-01-16 23:43:16 +01:00
Jan-Eric Myhrgren
222ce6ca00 Translated using Weblate (Swedish)
Currently translated at 74.5% (807 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-01-15 20:48:18 +01:00
Jan-Eric Myhrgren
be5dc6d2ec Translated using Weblate (Swedish)
Currently translated at 73.7% (799 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-01-15 20:48:18 +01:00
ugyes
804b446dae Translated using Weblate (Hungarian)
Currently translated at 97.8% (1060 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2025-01-15 20:48:17 +01:00
Milo Ivir
5897aee3b7 Translated using Weblate (Croatian)
Currently translated at 100.0% (1083 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-01-15 20:48:16 +01:00
Rasmus Enevoldsen
1e5e507eb0 Translated using Weblate (Danish)
Currently translated at 68.6% (743 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2025-01-15 20:48:15 +01:00
Milo Ivir
760af51c5d Translated using Weblate (Croatian)
Currently translated at 99.9% (1082 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-01-15 20:48:14 +01:00
Rasmus Enevoldsen
24705ca06a Translated using Weblate (Danish)
Currently translated at 68.3% (740 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2025-01-15 20:48:14 +01:00
thehijacker
56cba44154 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1083 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-01-15 20:48:13 +01:00
SunSpring
9360165f6b Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1083 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-01-15 20:48:12 +01:00
Marcus skoding
adef6ede12 Translated using Weblate (Swedish)
Currently translated at 72.0% (780 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-01-15 20:48:12 +01:00
Jan-Eric Myhrgren
b8afcd1664 Translated using Weblate (Swedish)
Currently translated at 72.0% (780 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-01-15 20:48:11 +01:00
Mathias Franco
d8da793bca Translated using Weblate (Dutch)
Currently translated at 100.0% (1083 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2025-01-15 20:48:10 +01:00
David
1856d68299 Translated using Weblate (Spanish)
Currently translated at 99.9% (1082 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2025-01-15 20:48:09 +01:00
Vito0912
89247f1786 Translated using Weblate (German)
Currently translated at 100.0% (1083 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-01-15 20:48:08 +01:00
Stefan Ha
5995c52ab7 Translated using Weblate (German)
Currently translated at 100.0% (1083 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-01-15 20:48:07 +01:00
D0ckW0rka
07264544ef Translated using Weblate (German)
Currently translated at 100.0% (1083 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-01-15 20:48:07 +01:00
advplyr
6057930507 Merge pull request #3842 from mikiher/dynamic-episode-row-height
Dynamically calculate episode row height on LazyEpisodeTable init
2025-01-15 13:47:56 -06:00
advplyr
9bbb23b853 Merge pull request #3832 from daneroo/fix_rounding_elapsedPrettyExtended
Fixes #3817 Correct rounding and carry of minutes in client/plugins/utils.js::$elapsedPrettyExtended
2025-01-15 13:32:23 -06:00
mikiher
e865241258 Dynamically calculate episode row height on init 2025-01-15 10:39:59 +02:00
advplyr
1a67f57551 Update podcast downloads to fallback to download without tagging due to inaccurate rss feed enclosures #3837 2025-01-14 15:48:06 -06:00
advplyr
9b5bdc1fdb Merge pull request #3822 from mikiher/episode-table-refresh-fix
Episode table refresh fixes
2025-01-13 16:12:38 -06:00
Daniel Lauzon
acda776e3e Fixes #3817
Correct rounding and carry of minutes in Vue.prototype.$elapsedPrettyExtended

-Add cypress tests for Vue.prototype.$elapsedPrettyExtended function
2025-01-13 13:36:15 -05:00
advplyr
8c4a9280ab Merge pull request #3828 from mikiher/nginx-host-fix
recommend using $http_host for ngnix
2025-01-12 10:55:38 -06:00
mikiher
1812282946 recommend using $http_host for ngnix 2025-01-12 18:35:49 +02:00
advplyr
64e9ac9d8f Fix merging embedded chapters for multi-track audiobooks giving incorrect chapter ids #3361
- Also trim chapter titles on probe (remove carriage return)
2025-01-12 09:56:48 -06:00
advplyr
0da9a04d8e Merge pull request #3788 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-01-12 05:05:02 -06:00
Максим Горпиніч
11178f58bd Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1083 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-01-11 21:27:24 +01:00
Илья Червонный
08b2d07f65 Translated using Weblate (Russian)
Currently translated at 100.0% (1083 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-01-11 21:27:23 +01:00
Øystein S. Hegnander
3c210170b2 Translated using Weblate (Norwegian Bokmål)
Currently translated at 94.3% (1022 of 1083 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2025-01-11 21:27:22 +01:00
thehijacker
03d35421b4 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1082 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-01-11 21:27:22 +01:00
Максим Горпиніч
a176ba53e0 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1082 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-01-11 21:27:21 +01:00
Stefan Ha
e34dff8f30 Translated using Weblate (German)
Currently translated at 100.0% (1081 of 1081 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-01-11 21:27:21 +01:00
thehijacker
0881ab4bfb Translated using Weblate (Slovenian)
Currently translated at 100.0% (1081 of 1081 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-01-11 21:27:20 +01:00
Максим Горпиніч
20c32efd62 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1081 of 1081 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-01-11 21:27:19 +01:00
ugyes
e2b8127a5b Translated using Weblate (Hungarian)
Currently translated at 97.7% (1057 of 1081 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2025-01-11 21:27:19 +01:00
kuci-JK
90f32cefca Translated using Weblate (Czech)
Currently translated at 88.7% (958 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2025-01-11 21:27:18 +01:00
Jaroslav Lichtblau
ab2e661e22 Translated using Weblate (Czech)
Currently translated at 88.7% (958 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2025-01-11 21:27:18 +01:00
Troja
a073aedca2 Translated using Weblate (Belarusian)
Currently translated at 10.7% (116 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-01-11 21:27:17 +01:00
Troja
b440a22ec9 Translated using Weblate (Belarusian)
Currently translated at 2.8% (31 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-01-11 21:27:16 +01:00
Perttu Niskanen
ec695e5f48 Translated using Weblate (Finnish)
Currently translated at 44.1% (477 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-01-11 21:27:16 +01:00
Perttu Niskanen
69ad0bf113 Translated using Weblate (Finnish)
Currently translated at 42.0% (454 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-01-11 21:27:15 +01:00
David
88f464398a Translated using Weblate (Catalan)
Currently translated at 93.7% (1013 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ca/
2025-01-11 21:27:15 +01:00
thehijacker
6fce501389 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/
2025-01-11 21:27:14 +01:00
Perttu Niskanen
559fab0d90 Translated using Weblate (Finnish)
Currently translated at 41.7% (451 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-01-11 21:27:13 +01:00
Максим Горпиніч
69c428802b 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/
2025-01-11 21:27:13 +01:00
Fredrik Drugge
6da631fa4f Translated using Weblate (Swedish)
Currently translated at 69.1% (747 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-01-11 21:27:12 +01:00
Dawid Kuźnicki
f83b081791 Translated using Weblate (Polish)
Currently translated at 75.0% (811 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-01-11 21:27:11 +01:00
ugyes
a6ce5fdd98 Translated using Weblate (Hungarian)
Currently translated at 97.8% (1057 of 1080 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2025-01-11 21:27:11 +01:00
biuklija
0a2e725bd3 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/
2025-01-11 21:27:10 +01:00
DiamondtipDR
c07c4a3341 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/
2025-01-11 21:27:10 +01:00
David
422773e745 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/
2025-01-11 21:27:09 +01:00
Vito0912
7a298aa6f5 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/
2025-01-11 21:27:08 +01:00
advplyr
41daf557aa Update get all libraries endpoint to support include stats for android auto to detect audio libraries 2025-01-11 14:26:57 -06:00
mikiher
de5bc63d88 Remove deleted episode from returned libraryItem object 2025-01-11 22:26:36 +02:00
mikiher
5e2282ef76 Fix LazyEpisodeTable.init to respect non-zero scrollTop 2025-01-11 22:25:30 +02:00
advplyr
c819afc53b Merge pull request #3816 from mikiher/fix-trix-issues
Fix Trix to use paragraphs and break on return
2025-01-10 16:37:24 -06:00
advplyr
37221a0446 Fix missing translation string 2025-01-10 15:48:44 -06:00
advplyr
0f20ed101e Update podcast RSS parser to handle HTML not wrapped in CDATA #3778 2025-01-10 15:42:52 -06:00
mikiher
b0dbccd283 Fix Trix to use paragraphs and break on return 2025-01-10 08:03:41 +02:00
advplyr
7001adb4dd Add message to schedule library scan tab #3734 2025-01-09 16:25:41 -06:00
mikiher
9668b49df9 Enable subdirectory support 2025-01-09 07:41:09 +02:00
advplyr
02ecf7ccfe Fix catch exception on failed to parse comic metadata #3804 2025-01-08 16:53:56 -06:00
advplyr
05ff5f1956 Merge pull request #3771 from sbyrx/master
Adds a configuration for podcast feed and episode download timeout
2025-01-08 14:10:20 -06:00
advplyr
1649fb40db Merge pull request #3808 from mikiher/merge-prod-js-index-js
Merge prod.js into index.js
2025-01-08 14:04:48 -06:00
mikiher
052e0059ff Restore prod.js 2025-01-08 07:23:08 +02:00
advplyr
5edd799b3e Update media player volume tooltip to be below the volume icon 2025-01-07 16:44:13 -06:00
advplyr
1632d8edee Update episode list item to fallback to using description if subtitle is not set, matching latest page 2025-01-07 15:21:11 -06:00
advplyr
e6181196a7 Merge pull request #3805 from nichwall/text_input_date_validation
Text input date validation
2025-01-07 14:46:34 -06:00
advplyr
bea9d6aff4 Update date time input validation, add red border for invalid datetime 2025-01-07 14:08:57 -06:00
mikiher
d410b13c9b Merge prod.js into index.js 2025-01-07 17:41:09 +02:00
advplyr
8286aad7a4 Fix updating cover from match requests #3807 2025-01-07 09:05:53 -06:00
advplyr
ed5960825b Fix podcast episode continue and listen again home page shelves 2025-01-07 08:37:05 -06:00
Nicholas Wallace
7fd8178dde Add: datetime check for new episode modal 2025-01-06 20:30:27 -07:00
Nicholas Wallace
db17a5c88b Change: toast date error to be generic 2025-01-06 20:22:47 -07:00
Nicholas Wallace
2ec84edb5e Add: episode pubdate validation before saving 2025-01-06 20:00:42 -07:00
advplyr
0eed38b771 Fix playback sessions num days listened in last year to be accurate for smaller screen sizes 2025-01-06 14:32:10 -06:00
advplyr
977bdbf0bb Fix podcast episode AudioTrack object 2025-01-06 13:30:31 -06:00
advplyr
a1ec10bd0d Fix sync request responding with 500 status code 2025-01-06 11:39:55 -06:00
advplyr
57d742b862 Merge pull request #3800 from advplyr/migrate-library-item-in-scanner
Migrate to new library item in scanner
2025-01-05 14:31:42 -06:00
advplyr
108eaba022 Migrate tools and collapse series. fix continue shelves. remove old objects 2025-01-05 14:09:03 -06:00
advplyr
ac159bea72 Update unit test stub function 2025-01-05 12:12:20 -06:00
advplyr
d5ce7b4939 Migrate to new library item in scanner 2025-01-05 12:05:01 -06:00
sbyrx
e64302f1d4 Merge branch 'advplyr:master' into master 2025-01-04 20:15:59 -05:00
advplyr
fdbca4feb6 Merge pull request #3776 from mikiher/fix-ffmpeg-concat-file
Fix ffmpeg concat file escaping
2025-01-04 16:04:18 -06:00
advplyr
f366dfa909 Merge pull request #3780 from nichwall/api_cache_case_insensitive
API Cache Manager route uses case-insensitive match
2025-01-04 16:03:14 -06:00
advplyr
5d1a17ffa8 Merge pull request #3794 from mikiher/fix-stream-ffmpeg-add-option
Fix ffmpeg.addOption for transcoding
2025-01-04 16:01:56 -06:00
advplyr
0ed4ea9138 Merge pull request #3798 from advplyr/migrate-new-library-items
Migrate controllers to use new toOldJSON functions
2025-01-04 16:01:17 -06:00
advplyr
1e9470b840 Update AuthorController library item usage and remove unused 2025-01-04 15:59:40 -06:00
advplyr
726a9eaea5 Fix local playback sync 2025-01-04 15:35:05 -06:00
advplyr
6d52f88a96 Update controllers to use toOldJSON functions 2025-01-04 15:20:41 -06:00
advplyr
7fae25a726 Merge pull request #3795 from advplyr/migrate-podcasts-new-library-item-2
Update podcasts to new library item model
2025-01-04 12:52:50 -06:00
advplyr
d8823c8b1c Update podcasts to new library item model 2025-01-04 12:41:09 -06:00
mikiher
43d8d9b286 Fix ffmpeg.addOption for transcoding 2025-01-04 20:16:48 +02:00
advplyr
4a398f6113 Merge pull request #3789 from advplyr/migrate-podcasts-new-library-item
Update podcasts to new library item model
2025-01-03 16:59:13 -06:00
advplyr
69d1744496 Update podcasts to new library item model 2025-01-03 16:48:24 -06:00
advplyr
0357dc90d4 Update libraryItem.updatedAt on media update 2025-01-03 14:07:27 -06:00
advplyr
6cd874dffc Merge pull request #3787 from advplyr/fix-remove-episode-from-playlist
Fix remove episode from playlist
2025-01-03 13:04:18 -06:00
advplyr
6467a92de6 Remove media progress when deleting podcast episode audio file 2025-01-03 12:12:56 -06:00
advplyr
63466ec48b Fix deleting episode library file removes episode from playlist #3784 2025-01-03 12:06:20 -06:00
advplyr
de7296eaab Merge pull request #3785 from advplyr/playback-session-use-new-library-item
Update PlaybackSession to use new library item model
2025-01-03 11:20:33 -06:00
advplyr
c251f1899d Update PlaybackSession to use new library item model 2025-01-03 11:16:03 -06:00
Nicholas Wallace
f70f21455f Req URL is lowercase in ApiCacheManager 2025-01-02 20:13:38 -07:00
Nicholas Wallace
a6fd0c95b2 API cache manager case-insensitive match 2025-01-02 20:07:21 -07:00
advplyr
d205c6f734 Merge pull request #3779 from advplyr/refactor-library-item
Refactor LibraryItem to use new model
2025-01-02 17:30:22 -06:00
advplyr
5e8678f1cc Remove unused 2025-01-02 17:25:10 -06:00
advplyr
12c6f2e9a5 Update updateMedia endpoint to use new model 2025-01-02 17:21:07 -06:00
advplyr
5cd14108f9 Remove req.oldLibraryItem usage 2025-01-02 15:54:10 -06:00
advplyr
eb853d9f09 Fix LibraryItemController unit test 2025-01-02 15:51:21 -06:00
advplyr
4787e7fdb5 Updates to LibraryItemController to use new model 2025-01-02 15:42:52 -06:00
advplyr
dd0ebdf2d8 Implementing toOld functions for LibraryItem/Book/Podcast 2025-01-02 12:49:58 -06:00
Toni Barth
18dfbdd983 Merge remote-tracking branch 'remotes/upstream/master' into allow-mrss-item-enclosures-for-podcasts 2025-01-02 17:10:09 +01:00
mikiher
fe2ba083be Fix ffmpeg concat file escaping 2025-01-02 13:34:25 +02:00
advplyr
de8b0abc3a Version bump v2.17.7 2025-01-01 14:52:25 -06:00
advplyr
08bbe1ba02 Merge pull request #3762 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-01-01 14:48:19 -06:00
Soaibuzzaman
87bac1e33b Translated using Weblate (Bengali)
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/bn/
2025-01-01 21:31:35 +01:00
thehijacker
e9eeab6fb5 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1082 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-01-01 21:31:35 +01:00
Deleted User
235d05eff3 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1082 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-01-01 21:31:34 +01:00
Dmitry
f9f8c6d751 Translated using Weblate (Russian)
Currently translated at 100.0% (1082 of 1082 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-01-01 21:31:33 +01:00
advplyr
e175a9c533 Revert book cards, author cards and series cards to div #2268 2025-01-01 14:31:24 -06:00
advplyr
f9130a138e Merge pull request #3773 from advplyr/fix-heatmap-caption
Fix user stats heatmap caption text to be accurate
2025-01-01 14:21:57 -06:00
advplyr
ed17dd9b51 Fix user stats heatmap caption text to be accurate 2025-01-01 13:49:22 -06:00
sbyrx
0d8d0a650b Adds a configuration for podcast feed and episode download timeout 2025-01-01 19:41:19 +00: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
Toni Barth
4d2241769e also check for mrss item enclosures when extracting items 2024-12-18 19:15:09 +01: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
Nicholas Wallace
d5fbc1d455 Add: statement about workflows passing 2024-11-17 12:22:15 -07:00
Greg Lorenzen
27c9381e1d Merge branch 'master' into multi-select-keyboard-navigation 2024-11-15 12:06:25 -08:00
Nicholas Wallace
0d54b57151 Add: PR template 2024-11-11 21:20:53 -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
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
234 changed files with 11665 additions and 6913 deletions

33
.github/pull_request_template.md vendored Normal file
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. -->

View File

@@ -0,0 +1,42 @@
name: Close Issues not using a template
on:
issues:
types:
- opened
permissions:
issues: write
jobs:
close_issue:
runs-on: ubuntu-latest
steps:
- name: Check issue headings
uses: actions/github-script@v6
with:
script: |
const issueBody = context.payload.issue.body || "";
// Match Markdown headings (e.g., # Heading, ## Heading)
const headingRegex = /^(#{1,6})\s.+/gm;
const headings = [...issueBody.matchAll(headingRegex)];
if (headings.length < 3) {
// Post a comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
body: "Thank you for opening an issue! To help us review your request efficiently, please use one of the provided issue templates. If you're seeking information or have a general question, consider opening a Discussion or joining the conversation on our Discord. Thanks!"
});
// Close the issue
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
state: "closed"
});
}

1
.gitignore vendored
View File

@@ -7,6 +7,7 @@
/podcasts/
/media/
/metadata/
/plugins/
/client/.nuxt/
/client/dist/
/dist/

View File

@@ -46,5 +46,10 @@ RUN apk del make python3 g++
EXPOSE 80
ENV PORT=80
ENV CONFIG_PATH="/config"
ENV METADATA_PATH="/metadata"
ENV SOURCE="docker"
ENTRYPOINT ["tini", "--"]
CMD ["node", "index.js"]

View File

@@ -5,7 +5,7 @@
@import './absicons.css';
:root {
--bookshelf-texture-img: url(/textures/wood_default.jpg);
--bookshelf-texture-img: url(~static/textures/wood_default.jpg);
--bookshelf-divider-bg: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%);
}
@@ -92,11 +92,10 @@
}
/* Firefox */
input[type=number] {
input[type='number'] {
-moz-appearance: textfield;
}
.tracksTable {
border-collapse: collapse;
width: 100%;
@@ -177,6 +176,10 @@ input[type=number] {
box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166;
}
.box-shadow-progressbar {
box-shadow: 0px -1px 4px rgb(62, 50, 2, 0.5);
}
.shadow-height {
height: calc(100% - 4px);
}
@@ -204,7 +207,6 @@ Bookshelf Label
color: #fce3a6;
}
.cover-bg {
width: calc(100% + 40px);
height: calc(100% + 40px);
@@ -247,4 +249,4 @@ Bookshelf Label
.abs-btn:disabled::before {
background-color: rgba(0, 0, 0, 0.2);
}
}

View File

@@ -52,4 +52,17 @@
text-indent: 0px !important;
text-align: start !important;
text-align-last: start !important;
}
}
.default-style.less-spacing p {
margin-block-start: 0;
}
.default-style.less-spacing ul {
margin-block-start: 0;
}
.default-style.less-spacing ol {
margin-block-start: 0;
}

View File

@@ -446,7 +446,7 @@ trix-editor .attachment__metadata .attachment__size {
}
.trix-content {
line-height: 1.5;
line-height: inherit;
}
.trix-content * {
@@ -455,6 +455,13 @@ trix-editor .attachment__metadata .attachment__size {
padding: 0;
}
.trix-content p {
box-sizing: border-box;
margin-top: 0;
margin-bottom: 0.5em;
padding: 0;
}
.trix-content h1 {
font-size: 1.2em;
line-height: 1.2;
@@ -560,4 +567,4 @@ trix-editor .attachment__metadata .attachment__size {
.trix-content .attachment-gallery.attachment-gallery--4 .attachment {
flex-basis: 50%;
max-width: 50%;
}
}

View File

@@ -1,6 +1,6 @@
<template>
<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">
<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" />

View File

@@ -17,7 +17,7 @@
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24e">
<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)">
<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>
</template>
</div>

View File

@@ -37,18 +37,18 @@
<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="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 class="bookshelfDividerCategorized h-6e w-full absolute top-0 left-0 right-0 z-20"></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>
</div>
<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>
<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>
</div>
</button>
</div>
</template>

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'">
<p class="text-sm">{{ $strings.ButtonAdd }}</p>
</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 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 -->
<template v-if="selectedSeries">
<p class="pl-2 text-base md:text-lg">
@@ -265,6 +268,9 @@ export default {
isPodcastLatestPage() {
return this.$route.name === 'library-library-podcast-latest'
},
isPodcastDownloadQueuePage() {
return this.$route.name === 'library-library-podcast-download-queue'
},
isAuthorsPage() {
return this.page === 'authors'
},

View File

@@ -1,6 +1,6 @@
<template>
<div>
<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="toolbar" aria-orientation="vertical" aria-label="Config Sidebar">
<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">
<span class="material-symbols text-2xl">arrow_back</span>
</div>

View File

@@ -2,6 +2,10 @@
<div id="bookshelf" ref="bookshelf" class="w-full overflow-y-auto" :style="{ fontSize: sizeMultiplier + 'rem' }">
<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' }">
<!-- 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>
</template>
@@ -65,7 +69,13 @@ export default {
tempIsScanning: false,
cardWidth: 0,
cardHeight: 0,
resizeObserver: null
coverHeight: 0,
resizeObserver: null,
lastScrollTop: 0,
lastTimestamp: 0,
postScrollTimeout: null,
currFirstEntityIndex: -1,
currLastEntityIndex: -1
}
},
watch: {
@@ -171,9 +181,6 @@ export default {
bookWidth() {
return this.cardWidth
},
bookHeight() {
return this.cardHeight
},
shelfPadding() {
if (this.bookshelfWidth < 640) return 32 * this.sizeMultiplier
return 64 * this.sizeMultiplier
@@ -184,9 +191,6 @@ export default {
entityWidth() {
return this.cardWidth
},
entityHeight() {
return this.cardHeight
},
shelfPaddingHeight() {
return 16
},
@@ -354,50 +358,53 @@ export default {
}
},
loadPage(page) {
this.pagesLoaded[page] = true
this.fetchEntites(page)
if (!this.pagesLoaded[page]) this.pagesLoaded[page] = this.fetchEntites(page)
return this.pagesLoaded[page]
},
showHideBookPlaceholder(index, show) {
var el = document.getElementById(`book-${index}-placeholder`)
if (el) el.style.display = show ? 'flex' : 'none'
},
mountEntites(fromIndex, toIndex) {
mountEntities(fromIndex, toIndex) {
for (let i = fromIndex; i < toIndex; i++) {
if (!this.entityIndexesMounted.includes(i)) {
this.cardsHelpers.mountEntityCard(i)
}
}
},
handleScroll(scrollTop) {
this.currScrollTop = scrollTop
var firstShelfIndex = Math.floor(scrollTop / this.shelfHeight)
var lastShelfIndex = Math.ceil((scrollTop + this.bookshelfHeight) / this.shelfHeight)
lastShelfIndex = Math.min(this.totalShelves - 1, lastShelfIndex)
var firstBookIndex = firstShelfIndex * this.entitiesPerShelf
var lastBookIndex = lastShelfIndex * this.entitiesPerShelf + this.entitiesPerShelf
lastBookIndex = Math.min(this.totalEntities, lastBookIndex)
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)
}
getVisibleIndices(scrollTop) {
const firstShelfIndex = Math.floor(scrollTop / this.shelfHeight)
const lastShelfIndex = Math.min(Math.ceil((scrollTop + this.bookshelfHeight) / this.shelfHeight), this.totalShelves - 1)
const firstEntityIndex = firstShelfIndex * this.entitiesPerShelf
const lastEntityIndex = Math.min(lastShelfIndex * this.entitiesPerShelf + this.entitiesPerShelf, this.totalEntities)
return { firstEntityIndex, lastEntityIndex }
},
postScroll() {
const { firstEntityIndex, lastEntityIndex } = this.getVisibleIndices(this.currScrollTop)
this.entityIndexesMounted = this.entityIndexesMounted.filter((_index) => {
if (_index < firstBookIndex || _index >= lastBookIndex) {
var el = document.getElementById(`book-card-${_index}`)
if (el) el.remove()
if (_index < firstEntityIndex || _index >= lastEntityIndex) {
var el = this.entityComponentRefs[_index]
if (el && el.$el) el.$el.remove()
return false
}
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() {
if (this.isFetchingEntities) {
@@ -405,8 +412,6 @@ export default {
return
}
this.destroyEntityComponents()
this.entityIndexesMounted = []
this.entityComponentRefs = {}
this.pagesLoaded = {}
this.entities = []
this.totalShelves = 0
@@ -416,40 +421,21 @@ export default {
this.initialized = false
this.initSizeData()
this.pagesLoaded[0] = true
await this.fetchEntites(0)
await this.loadPage(0)
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
this.mountEntites(0, lastBookIndex)
this.mountEntities(0, lastBookIndex)
},
remountEntities() {
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() {
async rebuild() {
this.initSizeData()
var lastBookIndex = Math.min(this.totalEntities, this.booksPerFetch)
this.entityIndexesMounted = []
for (let i = 0; i < lastBookIndex; i++) {
this.entityIndexesMounted.push(i)
if (!this.entities[i]) {
const page = Math.floor(i / this.booksPerFetch)
this.loadPage(page)
}
}
this.destroyEntityComponents()
await this.loadPage(0)
var bookshelfEl = document.getElementById('bookshelf')
if (bookshelfEl) {
bookshelfEl.scrollTop = 0
}
this.$nextTick(this.remountEntities)
this.mountEntities(0, lastBookIndex)
},
buildSearchParams() {
if (this.page === 'search' || this.page === 'collections') {
@@ -513,12 +499,29 @@ export default {
if (wasUpdated) {
this.resetEntities()
} 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) {
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)
},
libraryItemAdded(libraryItem) {
@@ -667,13 +670,14 @@ export default {
},
updatePagesLoaded() {
let numPages = Math.ceil(this.totalEntities / this.booksPerFetch)
this.pagesLoaded = {}
for (let page = 0; page < numPages; page++) {
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++) {
const index = page * this.booksPerFetch + i
if (!this.entities[index]) {
this.pagesLoaded[page] = false
if (this.pagesLoaded[page]) delete this.pagesLoaded[page]
break
}
}
@@ -688,7 +692,6 @@ export default {
var entitiesPerShelfBefore = this.entitiesPerShelf
var { clientHeight, clientWidth } = bookshelf
// console.log('Init bookshelf width', clientWidth, 'window width', window.innerWidth)
this.mountWindowWidth = window.innerWidth
this.bookshelfHeight = clientHeight
this.bookshelfWidth = clientWidth
@@ -713,10 +716,9 @@ export default {
this.initSizeData(bookshelf)
this.checkUpdateSearchParams()
this.pagesLoaded[0] = true
await this.fetchEntites(0)
await this.loadPage(0)
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
if (this.$store.state.lastBookshelfScrollData[this.page] && window.bookshelf) {
@@ -747,7 +749,7 @@ export default {
var bookshelf = document.getElementById('bookshelf')
if (bookshelf) {
this.init(bookshelf)
bookshelf.addEventListener('scroll', this.scroll)
bookshelf.addEventListener('scroll', this.scroll, { passive: true })
}
})
@@ -810,10 +812,14 @@ export default {
},
destroyEntityComponents() {
for (const key in this.entityComponentRefs) {
if (this.entityComponentRefs[key] && this.entityComponentRefs[key].destroy) {
this.entityComponentRefs[key].destroy()
const ref = this.entityComponentRefs[key]
if (ref && ref.destroy) {
if (ref.$el) ref.$el.remove()
ref.destroy()
}
}
this.entityComponentRefs = {}
this.entityIndexesMounted = []
},
scan() {
this.tempIsScanning = true
@@ -826,6 +832,14 @@ export default {
.finally(() => {
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() {

View File

@@ -53,16 +53,13 @@
@showBookmarks="showBookmarks"
@showSleepTimer="showSleepTimerModal = 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" :playback-rate="currentPlaybackRate" :library-item-id="libraryItemId" @select="selectBookmark" />
<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-settings-modal v-model="showPlayerSettingsModal" />
</div>
</template>
@@ -81,7 +78,6 @@ export default {
currentTime: 0,
showSleepTimerModal: false,
showPlayerQueueItemsModal: false,
showPlayerSettingsModal: false,
sleepTimerSet: false,
sleepTimerRemaining: 0,
sleepTimerType: null,
@@ -378,19 +374,28 @@ export default {
return
}
// https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API
if ('mediaSession' in navigator) {
var coverImageSrc = this.$store.getters['globals/getLibraryItemCoverSrc'](this.streamLibraryItem, '/Logo.png', true)
const artwork = [
{
src: coverImageSrc
}
]
const chapterInfo = []
if (this.chapters.length) {
this.chapters.forEach((chapter) => {
chapterInfo.push({
title: chapter.title,
startTime: chapter.start
})
})
}
navigator.mediaSession.metadata = new MediaMetadata({
title: this.title,
artist: this.playerHandler.displayAuthor || this.mediaMetadata.authorName || 'Unknown',
album: this.mediaMetadata.seriesName || '',
artwork
artwork: [
{
src: this.$store.getters['globals/getLibraryItemCoverSrc'](this.streamLibraryItem, '/Logo.png', true)
}
],
chapterInfo
})
console.log('Set media session metadata', navigator.mediaSession.metadata)

View File

@@ -1,9 +1,9 @@
<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 -->
<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'">
<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" />

View File

@@ -68,6 +68,9 @@ export default {
cardHeight() {
return this.height * this.sizeMultiplier
},
coverHeight() {
return this.cardHeight
},
userToken() {
return this.store.getters['user/getToken']
},

View File

@@ -24,7 +24,7 @@
</div>
</div>
<div class="w-full max-h-12 overflow-hidden">
<p class="text-gray-500 text-xs">{{ book.description }}</p>
<p class="text-gray-500 text-xs">{{ book.descriptionPlain }}</p>
</div>
</div>
<div v-else class="px-4 flex-grow">

View File

@@ -1,5 +1,5 @@
<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">
<div ref="card" :id="`book-card-${index}`" tabindex="0" :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 ' }">
<!-- 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">
@@ -14,21 +14,21 @@
</div>
<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>
</div>
<!-- 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 -->
<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>
<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 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 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">
@@ -39,7 +39,7 @@
</div>
<!-- No progress shown for podcasts (unless showing podcast episode) -->
<div cy-id="progressBar" v-if="!isPodcast || episodeProgress" class="absolute bottom-0 left-0 h-1e shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * userProgressPercent + 'px' }"></div>
<div cy-id="progressBar" v-if="!isPodcast || episodeProgress" class="absolute bottom-0 left-0 h-1e max-w-full z-20 rounded-b box-shadow-progressbar" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * userProgressPercent + 'px' }"></div>
<!-- Overlay is not shown if collapsing series in library -->
<div cy-id="overlay" v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen) && !processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded md:block" :class="overlayWrapperClasslist">
@@ -93,11 +93,11 @@
<!-- 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' }">
<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>
<!-- 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' }">
<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>
<!-- Series sequence -->
@@ -114,7 +114,7 @@
<!-- 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' }">
<p :style="{ fontSize: 0.8 + 'em' }">{{ numEpisodes }}</p>
<p :style="{ fontSize: 0.8 + 'em' }" role="status" :aria-label="$strings.LabelNumberOfEpisodes">{{ numEpisodes }}</p>
</div>
<!-- Podcast Num Episodes -->

View File

@@ -1,5 +1,5 @@
<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="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<div class="w-full h-full bg-primary relative rounded overflow-hidden">

View File

@@ -1,5 +1,5 @@
<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="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<div class="w-full h-full bg-primary relative rounded overflow-hidden">

View File

@@ -1,5 +1,5 @@
<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">
<div cy-id="card" ref="card" :id="`series-card-${index}`" tabindex="0" :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 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">
@@ -7,12 +7,12 @@
</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">
<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 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 box-shadow-progressbar" :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>
</div>

View File

@@ -1,13 +1,13 @@
<template>
<div class="">
<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" />
</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-else class="material-symbols" style="font-size: 1.2rem">close</span>
</div>
</button>
</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" @mousedown.stop.prevent>
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">

View File

@@ -1,28 +1,30 @@
<template>
<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">
<span class="flex items-center justify-between">
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
</span>
<div class="relative h-7">
<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="flex items-center justify-between">
<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">
<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" />
</svg>
</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>
</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">
<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">
<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">
<span class="font-normal ml-3 block truncate text-sm">{{ item.text }}</span>
</div>
<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>
<!-- selected checkmark icon -->
<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>
</template>
</ul>
<ul v-show="sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-white/5" role="option" @click="sublist = null">
<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="menuitem" @click="sublist = null">
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
<span class="material-symbols text-2xl">arrow_left</span>
</div>
@@ -40,13 +42,13 @@
<span class="font-normal block truncate">{{ $strings.ButtonBack }}</span>
</div>
</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">
<span class="font-normal block truncate py-2">{{ $getString('LabelLibraryFilterSublistEmpty', [selectedSublistText]) }}</span>
</div>
</li>
<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">
<span class="font-normal truncate py-2 text-xs">{{ item.text }}</span>
</div>

View File

@@ -1,20 +1,20 @@
<template>
<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="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>
</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">
<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">
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
</div>
<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>
</li>
</template>

View File

@@ -1,7 +1,7 @@
<template>
<div ref="wrapper" class="relative ml-4 sm:ml-8" v-click-outside="clickOutside">
<div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="setShowMenu(true)">
<span class="text-gray-200 text-sm sm:text-base">{{ playbackRate.toFixed(1) }}<span class="text-base">x</span></span>
<span class="text-gray-200 text-sm sm:text-base">{{ playbackRateDisplay }}<span class="text-base">x</span></span>
</div>
<div v-show="showMenu" class="absolute -top-[5.5rem] z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }">
<div class="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }">
@@ -19,7 +19,7 @@
<div class="w-full py-1 px-1">
<div class="flex items-center justify-between">
<ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" />
<p class="px-2 text-2xl sm:text-3xl">{{ playbackRate }}<span class="text-2xl">x</span></p>
<p class="px-2 text-2xl sm:text-3xl">{{ playbackRateDisplay }}<span class="text-2xl">x</span></p>
<ui-icon-btn :disabled="!canIncrement" icon="add" @click="increment" />
</div>
</div>
@@ -33,6 +33,10 @@ export default {
value: {
type: [String, Number],
default: 1
},
playbackRateIncrementDecrement: {
type: Number,
default: 0.1
}
},
data() {
@@ -58,10 +62,17 @@ export default {
return [0.5, 1, 1.2, 1.5, 2]
},
canIncrement() {
return this.playbackRate + 0.1 <= this.MAX_SPEED
return this.playbackRate + this.playbackRateIncrementDecrement <= this.MAX_SPEED
},
canDecrement() {
return this.playbackRate - 0.1 >= this.MIN_SPEED
return this.playbackRate - this.playbackRateIncrementDecrement >= this.MIN_SPEED
},
playbackRateDisplay() {
if (this.playbackRateIncrementDecrement == 0.05) return this.playbackRate.toFixed(2)
// For 0.1 increment: Only show 2 decimal places if the playback rate is 2 decimals
const numDecimals = String(this.playbackRate).split('.')[1]?.length || 0
if (numDecimals <= 1) return this.playbackRate.toFixed(1)
return this.playbackRate.toFixed(2)
}
},
methods: {
@@ -73,14 +84,14 @@ export default {
this.$nextTick(() => this.setShowMenu(false))
},
increment() {
if (this.playbackRate + 0.1 > this.MAX_SPEED) return
var newPlaybackRate = this.playbackRate + 0.1
this.playbackRate = Number(newPlaybackRate.toFixed(1))
if (this.playbackRate + this.playbackRateIncrementDecrement > this.MAX_SPEED) return
var newPlaybackRate = this.playbackRate + this.playbackRateIncrementDecrement
this.playbackRate = Number(newPlaybackRate.toFixed(2))
},
decrement() {
if (this.playbackRate - 0.1 < this.MIN_SPEED) return
var newPlaybackRate = this.playbackRate - 0.1
this.playbackRate = Number(newPlaybackRate.toFixed(1))
if (this.playbackRate - this.playbackRateIncrementDecrement < this.MIN_SPEED) return
var newPlaybackRate = this.playbackRate - this.playbackRateIncrementDecrement
this.playbackRate = Number(newPlaybackRate.toFixed(2))
},
updateMenuPositions() {
if (!this.$refs.wrapper) return

View File

@@ -1,20 +1,20 @@
<template>
<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="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>
</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">
<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">
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
</div>
<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>
</li>
</template>

View File

@@ -121,6 +121,8 @@ export default {
var img = document.createElement('img')
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.style.objectFit = showCoverBg ? 'contain' : 'cover'

View File

@@ -90,8 +90,8 @@
<div class="relative">
<ui-textarea-with-label :value="prettyFfprobeData" readonly :rows="30" class="text-xs" />
<button class="absolute top-4 right-4" :class="copiedToClipboard ? 'text-success' : 'text-white/50 hover:text-white/80'" @click.stop="copyFfprobeData">
<span class="material-symbols">{{ copiedToClipboard ? 'check' : 'content_copy' }}</span>
<button class="absolute top-4 right-4" :class="hasCopied ? 'text-success' : 'text-gray-400 hover:text-white'" @click.stop="copyToClipboard">
<span class="material-symbols">{{ hasCopied ? 'done' : 'content_copy' }}</span>
</button>
</div>
</div>
@@ -113,14 +113,13 @@ export default {
return {
probingFile: false,
ffprobeData: null,
copiedToClipboard: false
hasCopied: null
}
},
watch: {
show(newVal) {
if (newVal) {
this.ffprobeData = null
this.copiedToClipboard = false
this.probingFile = false
}
}
@@ -165,8 +164,13 @@ export default {
this.probingFile = false
})
},
async copyFfprobeData() {
this.copiedToClipboard = await this.$copyToClipboard(this.prettyFfprobeData)
copyToClipboard() {
clearTimeout(this.hasCopied)
this.$copyToClipboard(this.prettyFfprobeData).then((success) => {
this.hasCopied = setTimeout(() => {
this.hasCopied = null
}, 2000)
})
}
},
mounted() {}

View File

@@ -54,8 +54,7 @@ export default {
options: {
provider: undefined,
overrideDetails: true,
overrideCover: true,
overrideDefaults: true
overrideCover: true
}
}
},
@@ -99,8 +98,8 @@ export default {
init() {
// 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
if (!this.options.provider || this.options.lastUsedLibrary != this.currentLibraryId) {
this.options.lastUsedLibrary = this.currentLibraryId
if (!this.options.provider || this.lastUsedLibrary != this.currentLibraryId) {
this.lastUsedLibrary = this.currentLibraryId
this.options.provider = this.libraryProvider
}
},

View File

@@ -5,24 +5,26 @@
<p class="text-3xl text-white truncate">{{ $strings.LabelYourBookmarks }}</p>
</div>
</template>
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<div v-if="show" class="w-full h-full">
<div v-if="show" class="w-full rounded-lg bg-bg box-shadow-md relative" style="max-height: 80vh">
<div v-if="bookmarks.length" class="h-full max-h-[calc(80vh-60px)] w-full relative overflow-y-auto overflow-x-hidden">
<template v-for="bookmark in bookmarks">
<modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" @click="clickBookmark" @update="submitUpdateBookmark" @delete="deleteBookmark" />
<modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" :playback-rate="playbackRate" @click="clickBookmark" @delete="deleteBookmark" />
</template>
<div v-if="!bookmarks.length" class="flex h-32 items-center justify-center">
<p class="text-xl">{{ $strings.MessageNoBookmarks }}</p>
</div>
<div v-if="!hideCreate" class="w-full h-px bg-white bg-opacity-10" />
<form v-if="!hideCreate" @submit.prevent="submitCreateBookmark">
<div v-show="canCreateBookmark" class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">
</div>
<div v-else class="flex h-32 items-center justify-center">
<p class="text-xl">{{ $strings.MessageNoBookmarks }}</p>
</div>
<div v-if="canCreateBookmark && !hideCreate" class="w-full border-t border-white/10">
<form @submit.prevent="submitCreateBookmark">
<div class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">
<div class="w-16 max-w-16 text-center">
<p class="text-sm font-mono text-gray-400">
{{ this.$secondsToTimestamp(currentTime) }}
{{ this.$secondsToTimestamp(currentTime / playbackRate) }}
</p>
</div>
<div class="flex-grow px-2">
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full h-10" />
</div>
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-symbols text-2xl -mt-px">add</span></ui-btn>
</div>
@@ -45,6 +47,7 @@ export default {
default: 0
},
libraryItemId: String,
playbackRate: Number,
hideCreate: Boolean
},
data() {
@@ -57,6 +60,7 @@ export default {
watch: {
show(newVal) {
if (newVal) {
this.selectedBookmark = null
this.showBookmarkTitleInput = false
this.newBookmarkTitle = ''
}
@@ -72,7 +76,7 @@ export default {
}
},
canCreateBookmark() {
return !this.bookmarks.find((bm) => bm.time === this.currentTime)
return !this.bookmarks.find((bm) => Math.abs(this.currentTime - bm.time) < 1)
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
@@ -102,19 +106,6 @@ export default {
clickBookmark(bm) {
this.$emit('select', bm)
},
submitUpdateBookmark(updatedBookmark) {
var bookmark = { ...updatedBookmark }
this.$axios
.$patch(`/api/me/item/${this.libraryItemId}/bookmark`, bookmark)
.then(() => {
this.$toast.success(this.$strings.ToastBookmarkUpdateSuccess)
})
.catch((error) => {
this.$toast.error(this.$strings.ToastFailedToUpdate)
console.error(error)
})
this.show = false
},
submitCreateBookmark() {
if (!this.newBookmarkTitle) {
this.newBookmarkTitle = this.$formatDatetime(Date.now(), this.dateFormat, this.timeFormat)

View File

@@ -1,8 +1,8 @@
<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 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">
<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">
<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>
</div>
</button>
<div ref="content" class="text-white">
<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>

View File

@@ -1,12 +1,12 @@
<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" />
<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>
</button>
<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 />
<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 />
@@ -126,6 +126,9 @@ export default {
this.$eventBus.$on('modal-hotkey', this.hotkey)
this.$store.commit('setOpenModal', this.name)
// Set focus to the modal content
this.content.focus()
},
setHide() {
if (this.content) this.content.style.transform = 'scale(0)'

View File

@@ -11,9 +11,12 @@
<div class="flex items-center mb-4">
<ui-select-input v-model="jumpForwardAmount" :label="$strings.LabelJumpForwardAmount" menuMaxHeight="250px" :items="jumpValues" @input="setJumpForwardAmount" />
</div>
<div class="flex items-center">
<div class="flex items-center mb-4">
<ui-select-input v-model="jumpBackwardAmount" :label="$strings.LabelJumpBackwardAmount" menuMaxHeight="250px" :items="jumpValues" @input="setJumpBackwardAmount" />
</div>
<div class="flex items-center mb-4">
<ui-select-input v-model="playbackRateIncrementDecrement" :label="$strings.LabelPlaybackRateIncrementDecrement" menuMaxHeight="250px" :items="playbackRateIncrementDecrementValues" @input="setPlaybackRateIncrementDecrementAmount" />
</div>
</div>
</modals-modal>
</template>
@@ -35,7 +38,9 @@ export default {
{ text: this.$getString('LabelTimeDurationXMinutes', ['5']), value: 300 }
],
jumpForwardAmount: 10,
jumpBackwardAmount: 10
jumpBackwardAmount: 10,
playbackRateIncrementDecrementValues: [0.1, 0.05],
playbackRateIncrementDecrement: 0.1
}
},
computed: {
@@ -59,12 +64,24 @@ export default {
setJumpBackwardAmount(val) {
this.jumpBackwardAmount = val
this.$store.dispatch('user/updateUserSettings', { jumpBackwardAmount: val })
},
setPlaybackRateIncrementDecrementAmount(val) {
this.playbackRateIncrementDecrement = val
this.$store.dispatch('user/updateUserSettings', { playbackRateIncrementDecrement: 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')
this.playbackRateIncrementDecrement = this.$store.getters['user/getUserSetting']('playbackRateIncrementDecrement')
}
},
mounted() {
this.useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack')
this.jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount')
this.jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount')
this.settingsUpdated()
this.$eventBus.$on('user-settings', this.settingsUpdated)
},
beforeDestroy() {
this.$eventBus.$off('user-settings', this.settingsUpdated)
}
}
</script>

View File

@@ -16,15 +16,16 @@
<template v-if="currentShare">
<div class="w-full py-2">
<label class="px-1 text-sm font-semibold block">{{ $strings.LabelShareURL }}</label>
<ui-text-input v-model="currentShareUrl" show-copy readonly class="text-base h-10" />
<ui-text-input v-model="currentShareUrl" show-copy readonly />
</div>
<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>
</div>
</template>
<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">
<label class="px-1 text-sm font-semibold block">{{ $strings.LabelSlug }}</label>
<ui-text-input v-model="newShareSlug" class="text-base h-10" />
@@ -46,6 +47,15 @@
</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('MessageShareExpirationWillBe', [expirationDateString])" />
</template>
@@ -81,7 +91,8 @@ export default {
text: this.$strings.LabelDays,
value: 'days'
}
]
],
isDownloadable: false
}
},
watch: {
@@ -172,7 +183,8 @@ export default {
slug: this.newShareSlug,
mediaItemType: 'book',
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.$axios

View File

@@ -1,8 +1,8 @@
<template>
<div class="flex items-center px-4 py-4 justify-start relative bg-primary hover:bg-opacity-25" :class="wrapperClass" @click.stop="click" @mouseover="mouseover" @mouseleave="mouseleave">
<div class="flex items-center px-4 py-4 justify-start relative hover:bg-primary/10" :class="wrapperClass" @click.stop="click" @mouseover="mouseover" @mouseleave="mouseleave">
<div class="w-16 max-w-16 text-center">
<p class="text-sm font-mono text-gray-400">
{{ this.$secondsToTimestamp(bookmark.time) }}
{{ this.$secondsToTimestamp(bookmark.time / playbackRate) }}
</p>
</div>
<div class="flex-grow overflow-hidden px-2">
@@ -10,7 +10,7 @@
<form @submit.prevent="submitUpdate">
<div class="flex items-center">
<div class="flex-grow pr-2">
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full h-10" />
</div>
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-symbols text-2xl -mt-px">forward</span></ui-btn>
<div class="pl-2 flex items-center">
@@ -35,7 +35,8 @@ export default {
type: Object,
default: () => {}
},
highlight: Boolean
highlight: Boolean,
playbackRate: Number
},
data() {
return {
@@ -83,11 +84,19 @@ export default {
if (this.newBookmarkTitle === this.bookmark.title) {
return this.cancelEditing()
}
var bookmark = { ...this.bookmark }
const bookmark = { ...this.bookmark }
bookmark.title = this.newBookmarkTitle
this.$emit('update', bookmark)
this.$axios
.$patch(`/api/me/item/${bookmark.libraryItemId}/bookmark`, bookmark)
.then(() => {
this.isEditing = false
})
.catch((error) => {
this.$toast.error(this.$strings.ToastFailedToUpdate)
console.error(error)
})
}
},
mounted() {}
}
}
</script>

View File

@@ -2,7 +2,7 @@
<modals-modal v-model="show" name="changelog" :width="800" :height="'unset'">
<template #outer>
<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>
</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">
@@ -13,7 +13,7 @@
</p>
<div class="custom-text" v-html="getChangelog(release)" />
</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>
</div>
</modals-modal>

View File

@@ -138,7 +138,6 @@ export default {
.$post(`/api/collections/${collection.id}/batch/remove`, { books: this.selectedBookIds })
.then((updatedCollection) => {
console.log(`Books removed from collection`, updatedCollection)
this.$toast.success(this.$strings.ToastCollectionItemsRemoveSuccess)
this.processing = false
})
.catch((error) => {
@@ -152,7 +151,6 @@ export default {
.$delete(`/api/collections/${collection.id}/book/${this.selectedLibraryItemId}`)
.then((updatedCollection) => {
console.log(`Book removed from collection`, updatedCollection)
this.$toast.success(this.$strings.ToastCollectionItemsRemoveSuccess)
this.processing = false
})
.catch((error) => {
@@ -167,12 +165,11 @@ export default {
this.processing = true
if (this.showBatchCollectionModal) {
// BATCH Remove books
// BATCH Add books
this.$axios
.$post(`/api/collections/${collection.id}/batch/add`, { books: this.selectedBookIds })
.then((updatedCollection) => {
console.log(`Books added to collection`, updatedCollection)
this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
this.processing = false
})
.catch((error) => {
@@ -187,7 +184,6 @@ export default {
.$post(`/api/collections/${collection.id}/book`, { id: this.selectedLibraryItemId })
.then((updatedCollection) => {
console.log(`Book added to collection`, updatedCollection)
this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
this.processing = false
})
.catch((error) => {
@@ -214,7 +210,6 @@ export default {
.$post('/api/collections', newCollection)
.then((data) => {
console.log('New Collection Created', data)
this.$toast.success(`Collection "${data.name}" created`)
this.processing = false
this.newCollectionName = ''
})

View File

@@ -2,24 +2,24 @@
<modals-modal v-model="show" name="edit-book" :width="800" :height="height" :processing="processing" :content-margin-top="marginTop">
<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">
<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>
</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">
<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>
</div>
<div v-show="canGoPrev" class="absolute -left-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="goPrevBook" @mousedown.prevent>arrow_back_ios</div>
</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 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">
<component v-if="libraryItem && show" :is="tabName" :library-item="libraryItem" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
</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">
<component v-if="libraryItem && show" :is="tabName" :library-item="libraryItem" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
<div v-show="canGoPrev" class="absolute -left-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.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>
</modals-modal>
</template>

View File

@@ -113,6 +113,10 @@ export default {
return false
})
console.log('updateResult', updateResult)
} else if (!lastEpisodeCheck) {
this.$toast.error(this.$strings.ToastDateTimeInvalidOrIncomplete)
this.checkingNewEpisodes = false
return false
}
this.$axios

View File

@@ -94,9 +94,9 @@
<div v-if="selectedMatchOrig.description" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.description" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" />
<ui-rich-text-editor v-model="selectedMatch.description" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" />
<p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60">
{{ $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>
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.descriptionPlain.substr(0, 100) + (mediaMetadata.descriptionPlain.length > 100 ? '...' : '') }}</a>
</p>
</div>
</div>

View File

@@ -5,6 +5,9 @@
<ui-checkbox v-model="enableAutoScan" @input="toggleEnableAutoScan" :label="$strings.LabelEnable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" />
</div>
<widgets-cron-expression-builder ref="cronExpressionBuilder" v-if="enableAutoScan" v-model="cronExpression" @input="updatedCron" />
<div v-else>
<p class="text-yellow-400 text-base">{{ $strings.MessageScheduleLibraryScanNote }}</p>
</div>
</div>
</template>

View File

@@ -130,7 +130,6 @@ export default {
.$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects })
.then((updatedPlaylist) => {
console.log(`Items removed from playlist`, updatedPlaylist)
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
this.processing = false
})
.catch((error) => {
@@ -148,7 +147,6 @@ export default {
.$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects })
.then((updatedPlaylist) => {
console.log(`Items added to playlist`, updatedPlaylist)
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
this.processing = false
})
.catch((error) => {
@@ -174,7 +172,6 @@ export default {
.$post('/api/playlists', newPlaylist)
.then((data) => {
console.log('New playlist created', data)
this.$toast.success(this.$strings.ToastPlaylistCreateSuccess + ': ' + data.name)
this.processing = false
this.newPlaylistName = ''
})

View File

@@ -170,6 +170,12 @@ export default {
this.show = false
}
},
libraryItemUpdated(libraryItem) {
const episode = libraryItem.media.episodes.find((e) => e.id === this.selectedEpisodeId)
if (episode) {
this.episodeItem = episode
}
},
hotkey(action) {
if (action === this.$hotkeys.Modal.NEXT_PAGE) {
this.goNextEpisode()
@@ -178,9 +184,15 @@ export default {
}
},
registerListeners() {
if (this.libraryItem) {
this.$eventBus.$on(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
}
this.$eventBus.$on('modal-hotkey', this.hotkey)
},
unregisterListeners() {
if (this.libraryItem) {
this.$eventBus.$on(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
}
this.$eventBus.$off('modal-hotkey', this.hotkey)
}
},

View File

@@ -16,8 +16,25 @@
</div>
</div>
<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 less-spacing" v-html="description" />
<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>
</modals-modal>
</template>
@@ -54,7 +71,7 @@ export default {
return this.episode.description || ''
},
media() {
return this.libraryItem ? this.libraryItem.media || {} : {}
return this.libraryItem?.media || {}
},
mediaMetadata() {
return this.media.metadata || {}
@@ -65,6 +82,14 @@ export default {
podcastAuthor() {
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() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
}

View File

@@ -11,7 +11,7 @@
<ui-dropdown v-model="newEpisode.episodeType" :label="$strings.LabelEpisodeType" :items="episodeTypes" small />
</div>
<div class="w-2/5 p-1">
<ui-text-input-with-label v-model="pubDateInput" @input="updatePubDate" type="datetime-local" :label="$strings.LabelPubDate" />
<ui-text-input-with-label v-model="pubDateInput" ref="pubdate" type="datetime-local" :label="$strings.LabelPubDate" @input="updatePubDate" />
</div>
<div class="w-full p-1">
<ui-text-input-with-label v-model="newEpisode.title" :label="$strings.LabelTitle" />
@@ -145,11 +145,18 @@ export default {
return null
}
// Check pubdate is valid if it is being updated. Cannot be set to null in the web client
if (this.newEpisode.pubDate === null && this.$refs.pubdate?.$refs?.input?.isInvalidDate) {
this.$toast.error(this.$strings.ToastDateTimeInvalidOrIncomplete)
return null
}
const updatedDetails = this.getUpdatePayload()
if (!Object.keys(updatedDetails).length) {
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
return false
}
return this.updateDetails(updatedDetails)
},
async updateDetails(updatedDetails) {
@@ -163,13 +170,10 @@ export default {
this.isProcessing = false
if (updateResult) {
if (updateResult) {
this.$toast.success(this.$strings.ToastItemUpdateSuccess)
return true
} else {
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
}
this.$toast.success(this.$strings.ToastItemUpdateSuccess)
return true
}
return false
}
},

View File

@@ -10,9 +10,7 @@
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</p>
<div class="w-full relative">
<ui-text-input v-model="currentFeed.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>
<ui-text-input :value="feedUrl" readonly show-copy />
</div>
<div v-if="currentFeed.meta" class="mt-5">
@@ -111,8 +109,11 @@ export default {
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
feedUrl() {
return this.currentFeed ? `${window.origin}${this.$config.routerBasePath}${this.currentFeed.feedUrl}` : ''
},
demoFeedUrl() {
return `${window.origin}/feed/${this.newFeedSlug}`
return `${window.origin}${this.$config.routerBasePath}/feed/${this.newFeedSlug}`
},
isHttp() {
return window.origin.startsWith('http://')
@@ -157,9 +158,6 @@ export default {
this.processing = false
})
},
copyToClipboard(str) {
this.$copyToClipboard(str, this)
},
closeFeed() {
this.processing = true
this.$axios

View File

@@ -5,8 +5,7 @@
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedGeneral }}</p>
<div class="w-full relative">
<ui-text-input v-model="feed.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>
<ui-text-input :value="feedUrl" readonly show-copy />
</div>
<div v-if="feed.meta" class="mt-5">
@@ -70,14 +69,11 @@ export default {
},
_feed() {
return this.feed || {}
},
feedUrl() {
return this.feed ? `${window.origin}${this.$config.routerBasePath}${this.feed.feedUrl}` : ''
}
},
methods: {
copyToClipboard(str) {
this.$copyToClipboard(str, this)
}
},
mounted() {}
}
}
</script>

View File

@@ -2,9 +2,9 @@
<div class="w-full -mt-6">
<div class="w-full relative mb-1">
<div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full">
<controls-playback-speed-control v-model="playbackRate" @input="setPlaybackRate" @change="playbackRateChanged" class="mx-2 block" />
<controls-playback-speed-control v-model="playbackRate" @input="setPlaybackRate" @change="playbackRateChanged" :playbackRateIncrementDecrement="playbackRateIncrementDecrement" class="mx-2 block" />
<ui-tooltip direction="left" :text="$strings.LabelVolume">
<ui-tooltip direction="bottom" :text="$strings.LabelVolume">
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden sm:block" />
</ui-tooltip>
@@ -37,7 +37,7 @@
</ui-tooltip>
<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>
</button>
</ui-tooltip>
@@ -64,6 +64,8 @@
</div>
<modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :playback-rate="playbackRate" :chapters="chapters" @select="selectChapter" />
<modals-player-settings-modal v-model="showPlayerSettingsModal" />
</div>
</template>
@@ -96,6 +98,7 @@ export default {
audioEl: null,
seekLoading: false,
showChaptersModal: false,
showPlayerSettingsModal: false,
currentTime: 0,
duration: 0
}
@@ -177,6 +180,9 @@ export default {
useChapterTrack() {
const _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
return this.chapters.length ? _useChapterTrack : false
},
playbackRateIncrementDecrement() {
return this.$store.getters['user/getUserSetting']('playbackRateIncrementDecrement')
}
},
methods: {
@@ -220,12 +226,12 @@ export default {
},
increasePlaybackRate() {
if (this.playbackRate >= 10) return
this.playbackRate = Number((this.playbackRate + 0.1).toFixed(1))
this.playbackRate = Number((this.playbackRate + this.playbackRateIncrementDecrement || 0.1).toFixed(2))
this.setPlaybackRate(this.playbackRate)
},
decreasePlaybackRate() {
if (this.playbackRate <= 0.5) return
this.playbackRate = Number((this.playbackRate - 0.1).toFixed(1))
this.playbackRate = Number((this.playbackRate - this.playbackRateIncrementDecrement || 0.1).toFixed(2))
this.setPlaybackRate(this.playbackRate)
},
playbackRateChanged(playbackRate) {
@@ -315,6 +321,9 @@ export default {
if (!this.chapters.length) return
this.showChaptersModal = !this.showChaptersModal
},
showPlayerSettings() {
this.showPlayerSettingsModal = !this.showPlayerSettingsModal
},
init() {
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1

View File

@@ -97,9 +97,9 @@ export default {
},
ebookUrl() {
if (this.fileId) {
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
return `${this.$config.routerBasePath}/api/items/${this.libraryItemId}/ebook/${this.fileId}`
}
return `/api/items/${this.libraryItemId}/ebook`
return `${this.$config.routerBasePath}/api/items/${this.libraryItemId}/ebook`
},
themeRules() {
const isDark = this.ereaderSettings.theme === 'dark'

View File

@@ -1,7 +1,7 @@
<template>
<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)">
<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 :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>
@@ -37,6 +37,7 @@ export default {
innerHeight: 13 * 7,
blockWidth: 13,
data: [],
daysListenedInTheLastYear: 0,
monthLabels: [],
tooltipEl: null,
tooltipTextEl: null,
@@ -62,9 +63,6 @@ export default {
dayOfWeekToday() {
return new Date().getDay()
},
firstWeekStart() {
return this.$addDaysToToday(-this.daysToShow)
},
dayLabels() {
return [
{
@@ -193,46 +191,59 @@ export default {
buildData() {
this.data = []
var maxValue = 0
var minValue = 0
Object.values(this.daysListening).forEach((val) => {
if (val > maxValue) maxValue = val
if (!minValue || val < minValue) minValue = val
})
let maxValue = 0
let minValue = 0
const dates = []
const numDaysInTheLastYear = 52 * 7 + this.dayOfWeekToday
const firstDay = this.$addDaysToToday(-numDaysInTheLastYear)
for (let i = 0; i < numDaysInTheLastYear + 1; i++) {
const date = i === 0 ? firstDay : this.$addDaysToDate(firstDay, i)
const dateString = this.$formatJsDate(date, 'yyyy-MM-dd')
if (this.daysListening[dateString] > 0) {
this.daysListenedInTheLastYear++
}
const visibleDayIndex = i - (numDaysInTheLastYear - this.daysToShow)
if (visibleDayIndex < 0) {
continue
}
const dateObj = {
col: Math.floor(visibleDayIndex / 7),
row: visibleDayIndex % 7,
date,
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)
if (dateObj.value > 0) {
if (dateObj.value > maxValue) maxValue = dateObj.value
if (!minValue || dateObj.value < minValue) minValue = dateObj.value
}
}
const range = maxValue - minValue + 0.01
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 dateString = this.$formatJsDate(date, 'yyyy-MM-dd')
const datePretty = this.$formatJsDate(date, 'MMM d, yyyy')
const monthString = this.$formatJsDate(date, 'MMM')
const value = this.daysListening[dateString] || 0
const x = col * 13
const y = row * 13
var bgColor = this.bgColors[0]
var outlineColor = this.outlineColors[0]
if (value) {
for (const dateObj of dates) {
let bgColor = this.bgColors[0]
let outlineColor = this.outlineColors[0]
if (dateObj.value) {
outlineColor = this.outlineColors[1]
var percentOfAvg = (value - minValue) / range
var bgIndex = Math.floor(percentOfAvg * 4) + 1
const percentOfAvg = (dateObj.value - minValue) / range
const bgIndex = Math.floor(percentOfAvg * 4) + 1
bgColor = this.bgColors[bgIndex] || 'red'
}
this.data.push({
date,
dateString,
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;`
...dateObj,
style: `transform:translate(${dateObj.col * 13}px,${dateObj.row * 13}px);background-color:${bgColor};outline:1px solid ${outlineColor};outline-offset:-1px;`
})
}
@@ -260,6 +271,7 @@ export default {
const heatmapEl = document.getElementById('heatmap')
this.contentWidth = heatmapEl.clientWidth
this.maxInnerWidth = this.contentWidth - 52
this.daysListenedInTheLastYear = 0
this.buildData()
}
},

View File

@@ -1,9 +1,9 @@
<template>
<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 />
</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>
</template>

View File

@@ -7,7 +7,7 @@
</div>
<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" />
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide : $strings.LabelYearReviewShow }}</ui-btn>
</div>
@@ -16,17 +16,22 @@
<div v-if="showYearInReview">
<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 -->
<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="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
</ui-btn>
<!-- 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" />
<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>
<div class="flex-grow" />
@@ -36,7 +41,7 @@
<span class="material-symbols sm:!hidden text-lg py-px">refresh</span>
</ui-btn>
<!-- 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="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn>
@@ -46,23 +51,23 @@
<!-- your year in review short -->
<div class="w-full max-w-[800px] mx-auto my-4">
<!-- 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" />
</div>
<!-- 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">
<!-- 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="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
</ui-btn>
<!-- 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" />
<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>
<div class="flex-grow" />
@@ -72,7 +77,7 @@
<span class="material-symbols sm:!hidden text-lg py-px">refresh</span>
</ui-btn>
<!-- 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="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn>
@@ -88,6 +93,7 @@ export default {
data() {
return {
showYearInReview: false,
availableYears: [],
yearInReviewYear: 0,
yearInReviewVariant: 0,
yearInReviewServerVariant: 0,
@@ -100,6 +106,9 @@ export default {
computed: {
isAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
user() {
return this.$store.state.user.user
}
},
methods: {
@@ -112,25 +121,57 @@ export default {
shareYearInReviewShort() {
this.$refs.yearInReviewShort.share()
},
yearInReviewYearChanged() {
this.$nextTick(() => {
this.refreshYearInReview()
this.refreshYearInReviewServer()
})
},
refreshYearInReviewServer() {
this.$refs.yearInReviewServer.refresh()
if (this.$refs.yearInReviewServer != null) {
this.$refs.yearInReviewServer.refresh()
}
},
refreshYearInReview() {
this.$refs.yearInReview.refresh()
this.$refs.yearInReviewShort.refresh()
if (this.$refs.yearInReview != null && this.$refs.yearInReviewShort != null) {
this.$refs.yearInReview.refresh()
this.$refs.yearInReviewShort.refresh()
}
},
clickShowYearInReview() {
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() {
this.yearInReviewYear = new Date().getFullYear()
// When not December show previous year
if (new Date().getMonth() < 11) {
this.yearInReviewYear--
}
},
mounted() {
this.availableYears = this.getAvailableYears()
if (typeof navigator.share !== 'undefined' && navigator.share) {
this.showShareButton = true
} else {

View File

@@ -1,9 +1,9 @@
<template>
<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 />
</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>
</template>

View File

@@ -120,6 +120,7 @@ export default {
this.users = res.users.sort((a, b) => {
return a.createdAt - b.createdAt
})
this.$emit('numUsers', this.users.length)
})
.catch((error) => {
console.error('Failed', error)

View File

@@ -218,7 +218,6 @@ export default {
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
} else {
console.log(`Item removed from playlist`, updatedPlaylist)
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
}
})
.catch((error) => {

View File

@@ -96,7 +96,7 @@ export default {
return this.episode?.title || ''
},
episodeSubtitle() {
return this.episode?.subtitle || ''
return this.episode?.subtitle || this.episode?.description || ''
},
episodeType() {
return this.episode?.episodeType || ''

View File

@@ -25,13 +25,12 @@
</template>
</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">
<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" />
</form>
</div>
<div class="relative min-h-[176px]">
<div class="relative min-h-44">
<template v-for="episode in totalEpisodes">
<div :key="episode" :id="`episode-${episode - 1}`" class="w-full h-44 px-2 py-3 overflow-hidden relative border-b border-white/10">
<!-- episode is mounted here -->
@@ -40,7 +39,7 @@
<div v-if="isSearching" class="w-full h-full absolute inset-0 flex justify-center py-12" :class="{ 'bg-black/50': totalEpisodes }">
<ui-loading-indicator />
</div>
<div v-else-if="!totalEpisodes" class="h-44 flex items-center justify-center">
<div v-else-if="!totalEpisodes" id="no-episodes" class="h-44 flex items-center justify-center">
<p class="text-lg">{{ $strings.MessageNoEpisodes }}</p>
</div>
</div>
@@ -81,7 +80,8 @@ export default {
episodeComponentRefs: {},
windowHeight: 0,
episodesTableOffsetTop: 0,
episodeRowHeight: 176
episodeRowHeight: 44 * 4, // h-44,
currScrollTop: 0
}
},
watch: {
@@ -485,9 +485,8 @@ export default {
}
}
},
scroll(evt) {
if (!evt?.target?.scrollTop) return
const scrollTop = Math.max(evt.target.scrollTop - this.episodesTableOffsetTop, 0)
handleScroll() {
const scrollTop = this.currScrollTop
let firstEpisodeIndex = Math.floor(scrollTop / this.episodeRowHeight)
let lastEpisodeIndex = Math.ceil((scrollTop + this.windowHeight) / this.episodeRowHeight)
lastEpisodeIndex = Math.min(this.totalEpisodes - 1, lastEpisodeIndex)
@@ -502,6 +501,12 @@ export default {
})
this.mountEpisodes(firstEpisodeIndex, lastEpisodeIndex + 1)
},
scroll(evt) {
if (!evt?.target?.scrollTop) return
const scrollTop = Math.max(evt.target.scrollTop - this.episodesTableOffsetTop, 0)
this.currScrollTop = scrollTop
this.handleScroll()
},
initListeners() {
const itemPageWrapper = document.getElementById('item-page-wrapper')
if (itemPageWrapper) {
@@ -515,6 +520,10 @@ export default {
}
},
filterSortChanged() {
// Save filterKey and sortKey to local storage
localStorage.setItem('podcastEpisodesFilter', this.filterKey)
localStorage.setItem('podcastEpisodesSortBy', this.sortKey + (this.sortDesc ? '-desc' : ''))
this.init()
},
refresh() {
@@ -529,14 +538,32 @@ export default {
this.episodesTableOffsetTop = (lazyEpisodesTableEl?.offsetTop || 0) + 64
this.windowHeight = window.innerHeight
this.episodesPerPage = Math.ceil(this.windowHeight / this.episodeRowHeight)
this.$nextTick(() => {
this.mountEpisodes(0, Math.min(this.episodesPerPage, this.totalEpisodes))
this.recalcEpisodeRowHeight()
this.episodesPerPage = Math.ceil(this.windowHeight / this.episodeRowHeight)
// Maybe update currScrollTop if items were removed
const itemPageWrapper = document.getElementById('item-page-wrapper')
const { scrollHeight, clientHeight } = itemPageWrapper
const maxScrollTop = scrollHeight - clientHeight
this.currScrollTop = Math.min(this.currScrollTop, maxScrollTop)
this.handleScroll()
})
},
recalcEpisodeRowHeight() {
const episodeRowEl = document.getElementById('episode-0') || document.getElementById('no-episodes')
if (episodeRowEl) {
const height = getComputedStyle(episodeRowEl).height
this.episodeRowHeight = parseInt(height) || this.episodeRowHeight
}
}
},
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.initListeners()
this.init()

View File

@@ -1,7 +1,7 @@
<template>
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
<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>
</button>
<div v-else class="h-full w-full flex items-center justify-center">
@@ -10,12 +10,12 @@
</slot>
<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-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>
</div>
</button>
<div
v-if="mouseoverItemIndex === index"
:key="`subitems-${index}`"
@@ -25,14 +25,14 @@
:class="openSubMenuLeft ? 'rounded-l-md' : 'rounded-r-md'"
: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>
</div>
</button>
</div>
</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>
</div>
</button>
</template>
</div>
</transition>

View File

@@ -1,7 +1,7 @@
<template>
<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>
<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="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small }">{{ selectedText }}</span>
<span v-if="selectedSubtext">:&nbsp;</span>
@@ -13,9 +13,9 @@
</button>
<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">
<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">
<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>
@@ -119,4 +119,4 @@ export default {
},
mounted() {}
}
</script>
</script>

View File

@@ -1,5 +1,5 @@
<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">
<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" />
@@ -28,7 +28,8 @@ export default {
size: {
type: Number,
default: 9
}
},
ariaLabel: String
},
data() {
return {}

View File

@@ -4,7 +4,7 @@
type="button"
: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"
aria-haspopup="listbox"
aria-haspopup="menu"
:aria-expanded="showMenu"
:aria-label="$strings.ButtonLibrary + ': ' + currentLibrary.name"
@click.stop.prevent="clickShowMenu"
@@ -16,9 +16,9 @@
</button>
<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">
<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">
<ui-library-icon :icon="library.icon" class="mr-1.5" />
<span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span>

View File

@@ -1,17 +1,17 @@
<template>
<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">
<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 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-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">
<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>
<span class="material-symbols text-white hover:text-error cursor-pointer" style="font-size: 1.1rem" @click.stop="removeItem(item)">close</span>
<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" 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" :class="{ 'opacity-100': inputFocused }">
<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>
<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>
{{ item }}
</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>
</form>
@@ -66,7 +66,8 @@ export default {
typingTimeout: null,
isFocused: false,
menu: null,
filteredItems: null
filteredItems: null,
inputFocused: false
}
},
watch: {
@@ -100,6 +101,9 @@ export default {
}
return this.filteredItems
},
identifier() {
return Math.random().toString(36).substring(2)
}
},
methods: {
@@ -129,6 +133,9 @@ export default {
}, 100)
this.setInputWidth()
},
setInputFocused(focused) {
this.inputFocused = focused
},
setInputWidth() {
setTimeout(() => {
var value = this.$refs.input.value

View File

@@ -1,20 +1,20 @@
<template>
<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">
<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 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-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">
<span v-if="showEdit" class="material-symbols text-base text-white hover:text-warning mr-1" @click.stop="editItem(item)">edit</span>
<span class="material-symbols text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item.id)">close</span>
<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" 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" :class="{ 'opacity-100': inputFocused }">
<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>
<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>
{{ item[textKey] }}
</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">
<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>
<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>
</form>
@@ -65,6 +65,7 @@ export default {
currentSearch: null,
typingTimeout: null,
isFocused: false,
inputFocused: false,
menu: null,
items: []
}
@@ -102,6 +103,9 @@ export default {
},
filterData() {
return this.$store.state.libraries.filterData || {}
},
identifier() {
return Math.random().toString(36).substring(2)
}
},
methods: {
@@ -114,6 +118,9 @@ export default {
getIsSelected(itemValue) {
return !!this.selected.find((i) => i.id === itemValue)
},
setInputFocused(focused) {
this.inputFocused = focused
},
search() {
if (!this.textInput) return
this.currentSearch = this.textInput

View File

@@ -1,5 +1,5 @@
<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">
<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" />

View File

@@ -1,9 +1,9 @@
<template>
<div class="default-style">
<p v-if="label" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
<p v-if="label" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }" style="margin-top: 0; margin-bottom: 0.125em">
{{ label }}
</p>
<ui-vue-trix v-model="content" :config="config" :disabled-editor="disabled" @trix-file-accept="trixFileAccept" />
<ui-vue-trix ref="input" v-model="content" :disabled-editor="disabled" @trix-file-accept="trixFileAccept" />
</div>
</template>
@@ -12,7 +12,10 @@ export default {
props: {
value: String,
label: String,
disabled: Boolean
disabled: {
type: Boolean,
default: false
}
},
data() {
return {}
@@ -25,49 +28,19 @@ export default {
set(val) {
this.$emit('input', val)
}
},
config() {
return {
toolbar: {
getDefaultHTML: () => `<div class="trix-button-row">
<span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" title="${this.$strings.LabelFontBold}" tabindex="-1">${this.$strings.LabelFontBold}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" title="${this.$strings.LabelFontItalic}" tabindex="-1">${this.$strings.LabelFontItalic}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" title="${this.$strings.LabelFontStrikethrough}" tabindex="-1">${this.$strings.LabelFontStrikethrough}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="${this.$strings.LabelTextEditorLink}" tabindex="-1">${this.$strings.LabelTextEditorLink}</button>
</span>
<span class="trix-button-group trix-button-group--block-tools" data-trix-button-group="block-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" title="${this.$strings.LabelTextEditorBulletedList}" tabindex="-1">${this.$strings.LabelTextEditorBulletedList}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" title="${this.$strings.LabelTextEditorNumberedList}" tabindex="-1">${this.$strings.LabelTextEditorNumberedList}</button>
</span>
<span class="trix-button-group-spacer"></span>
<span class="trix-button-group trix-button-group--history-tools" data-trix-button-group="history-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" title="${this.$strings.LabelUndo}" tabindex="-1">${this.$strings.LabelUndo}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" title="${this.$strings.LabelRedo}" tabindex="-1">${this.$strings.LabelRedo}</button>
</span>
</div>
<div class="trix-dialogs" data-trix-dialogs>
<div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
<div class="trix-dialog__link-fields">
<input type="url" name="href" class="trix-input trix-input--dialog" placeholder="" aria-label="URL" required data-trix-input>
<div class="trix-button-group">
<input type="button" class="trix-button trix-button--dialog" value="${this.$strings.LabelTextEditorLink}" data-trix-method="setAttribute">
<input type="button" class="trix-button trix-button--dialog" value="${this.$strings.LabelTextEditorUnlink}" data-trix-method="removeAttribute">
</div>
</div>
</div>
</div>`
}
}
}
},
methods: {
trixFileAccept(e) {
e.preventDefault()
},
blur() {
if (this.$refs.input && this.$refs.input.blur) {
this.$refs.input.blur()
}
}
},
mounted() {},
beforeDestroy() {}
}
</script>
</script>

View File

@@ -1,32 +1,14 @@
<template>
<div ref="wrapper" class="relative">
<input
:id="inputId"
:name="inputName"
ref="input"
v-model="inputValue"
:type="actualType"
:step="step"
:min="min"
:readonly="readonly"
:disabled="disabled"
:placeholder="placeholder"
dir="auto"
class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full"
:class="classList"
@keyup="keyup"
@change="change"
@focus="focused"
@blur="blurred"
/>
<input :id="inputId" :name="inputName" ref="input" v-model="inputValue" :type="actualType" :step="step" :min="min" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" dir="auto" class="rounded bg-primary text-gray-200 focus:bg-bg focus:outline-none border h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
<div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
<span class="material-symbols text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
</div>
<div v-if="type === 'password' && isHovering" class="absolute top-0 right-0 h-full px-4 flex items-center justify-center">
<span class="material-symbols text-gray-400 cursor-pointer text-lg" @click.stop.prevent="showPassword = !showPassword">{{ !showPassword ? 'visibility' : 'visibility_off' }}</span>
</div>
<div v-else-if="showCopy" class="absolute top-0 right-0 h-full px-4 flex items-center justify-center">
<span class="material-symbols text-gray-400 cursor-pointer text-lg" @click.stop.prevent="copyToClipboard">{{ !hasCopied ? 'content_copy' : 'done' }}</span>
<div v-else-if="showCopy" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
<span class="material-symbols cursor-pointer text-lg" :class="hasCopied ? 'text-success' : 'text-gray-400 hover:text-white'" @click.stop.prevent="copyToClipboard">{{ !hasCopied ? 'content_copy' : 'done' }}</span>
</div>
</div>
</template>
@@ -65,7 +47,8 @@ export default {
showPassword: false,
isHovering: false,
isFocused: false,
hasCopied: false
hasCopied: null,
isInvalidDate: false
}
},
computed: {
@@ -79,11 +62,20 @@ export default {
},
classList() {
var _list = []
_list.push(`px-${this.paddingX}`)
if (this.showCopy) {
_list.push('pl-3', 'pr-8')
} else {
_list.push(`px-${this.paddingX}`)
}
_list.push(`py-${this.paddingY}`)
if (this.noSpinner) _list.push('no-spinner')
if (this.textCenter) _list.push('text-center')
if (this.customInputClass) _list.push(this.customInputClass)
if (this.isInvalidDate) _list.push('border-error')
else _list.push('focus:border-gray-300 border-gray-600')
return _list.join(' ')
},
actualType() {
@@ -93,11 +85,10 @@ export default {
},
methods: {
copyToClipboard() {
if (this.hasCopied) return
clearTimeout(this.hasCopied)
this.$copyToClipboard(this.inputValue).then((success) => {
this.hasCopied = success
setTimeout(() => {
this.hasCopied = false
this.hasCopied = setTimeout(() => {
this.hasCopied = null
}, 2000)
})
},
@@ -118,6 +109,14 @@ export default {
},
keyup(e) {
this.$emit('keyup', e)
if (this.type === 'datetime-local') {
if (e.target.validity?.badInput) {
this.isInvalidDate = true
} else {
this.isInvalidDate = false
}
}
},
blur() {
if (this.$refs.input) this.$refs.input.blur()

View File

@@ -1,11 +1,12 @@
<template>
<div class="w-full">
<slot>
<label :for="identifier" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }"
>{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em></label
>
<label :for="identifier" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
{{ label }}
<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
</label>
</slot>
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" :class="inputClass" @blur="inputBlurred" />
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :show-copy="showCopy" class="w-full" :class="inputClass" @blur="inputBlurred" />
</div>
</template>
@@ -22,7 +23,8 @@ export default {
},
readonly: Boolean,
disabled: Boolean,
inputClass: String
inputClass: String,
showCopy: Boolean
},
data() {
return {}
@@ -57,4 +59,4 @@ export default {
},
mounted() {}
}
</script>
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<button :aria-labelledby="labeledBy" 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">
<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 border-black-50 shadow transform transition-transform duration-100" :style="{ width: cursorHeightWidth + 'px', height: cursorHeightWidth + 'px' }" :class="switchClassName"></span>
</button>
</div>
@@ -20,6 +20,7 @@ export default {
},
disabled: Boolean,
labeledBy: String,
label: String,
size: {
type: String,
default: 'md'

View File

@@ -1,6 +1,37 @@
<template>
<div>
<trix-editor :contenteditable="!disabledEditor" :class="['trix-content']" ref="trix" :input="computedId" :placeholder="placeholder" @trix-change="handleContentChange" @trix-initialize="handleInitialize" @trix-focus="processTrixFocus" @trix-blur="processTrixBlur" />
<trix-toolbar :id="toolbarId">
<div v-show="!disabledEditor" class="trix-button-row">
<span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" :title="$strings.LabelFontBold" tabindex="-1">{{ $strings.LabelFontBold }}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" :title="$strings.LabelFontItalic" tabindex="-1">{{ $strings.LabelFontItalic }}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" :title="$strings.LabelFontStrikethrough" tabindex="-1">{{ $strings.LabelFontStrikethrough }}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" :title="$strings.LabelTextEditorLink" tabindex="-1">{{ $strings.LabelTextEditorLink }}</button>
</span>
<span class="trix-button-group trix-button-group--block-tools" data-trix-button-group="block-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" :title="$strings.LabelTextEditorBulletedList" tabindex="-1">{{ $strings.LabelTextEditorBulletedList }}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" :title="$strings.LabelTextEditorNumberedList" tabindex="-1">{{ $strings.LabelTextEditorNumberedList }}</button>
</span>
<span class="trix-button-group-spacer"></span>
<span class="trix-button-group trix-button-group--history-tools" data-trix-button-group="history-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" :title="$strings.LabelUndo" tabindex="-1">{{ $strings.LabelUndo }}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" :title="$strings.LabelRedo" tabindex="-1">{{ $strings.LabelRedo }}</button>
</span>
</div>
<div class="trix-dialogs" data-trix-dialogs>
<div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
<div class="trix-dialog__link-fields">
<input type="url" name="href" class="trix-input trix-input--dialog" placeholder="" aria-label="URL" required data-trix-input />
<div class="trix-button-group">
<input type="button" class="trix-button trix-button--dialog" :value="$strings.LabelTextEditorLink" data-trix-method="setAttribute" />
<input type="button" class="trix-button trix-button--dialog" :value="$strings.LabelTextEditorUnlink" data-trix-method="removeAttribute" />
</div>
</div>
</div>
</div>
</trix-toolbar>
<trix-editor :toolbar="toolbarId" :contenteditable="!disabledEditor" :class="['trix-content']" ref="trix" :input="computedId" :placeholder="placeholder" @trix-change="handleContentChange" @trix-initialize="handleInitialize" @trix-focus="processTrixFocus" @trix-blur="processTrixBlur" />
<input type="hidden" :name="inputName" :id="computedId" :value="editorContent" />
</div>
</template>
@@ -14,6 +45,30 @@
import Trix from 'trix'
import '@/assets/trix.css'
function enableBreakParagraphOnReturn() {
// Trix works with divs by default, we want paragraphs instead
Trix.config.blockAttributes.default.tagName = 'p'
// Enable break paragraph on Enter (Shift + Enter will still create a line break)
Trix.config.blockAttributes.default.breakOnReturn = true
// Hack to fix buggy paragraph breaks
// Copied from https://github.com/basecamp/trix/issues/680#issuecomment-735742942
Trix.Block.prototype.breaksOnReturn = function () {
const attr = this.getLastAttribute()
const config = Trix.getBlockConfig(attr ? attr : 'default')
return config ? config.breakOnReturn : false
}
Trix.LineBreakInsertion.prototype.shouldInsertBlockBreak = function () {
if (this.block.hasAttributes() && this.block.isListItem() && !this.block.isEmpty()) {
return this.startLocation.offset > 0
} else {
return !this.shouldBreakFormattedBlock() ? this.breaksOnReturn : false
}
}
}
enableBreakParagraphOnReturn()
export default {
name: 'vue-trix',
model: {
@@ -134,6 +189,9 @@ export default {
* Compute a random id of hidden input
* when it haven't been specified.
*/
toolbarId() {
return `trix-toolbar-${this.generateId}`
},
generateId() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
var r = (Math.random() * 16) | 0
@@ -223,13 +281,17 @@ export default {
decorateDisabledEditor(editorState) {
/** Disable toolbar and editor by pointer events styling */
if (editorState) {
this.$refs.trix.toolbarElement.style['pointer-events'] = 'none'
this.$refs.trix.disabled = true
this.$refs.trix.contentEditable = false
this.$refs.trix.style['background'] = '#e9ecef'
this.$refs.trix.style['pointer-events'] = 'none'
this.$refs.trix.style['background-color'] = '#444'
this.$refs.trix.style['color'] = '#bbb'
} else {
this.$refs.trix.toolbarElement.style['pointer-events'] = 'unset'
this.$refs.trix.disabled = false
this.$refs.trix.contentEditable = true
this.$refs.trix.style['pointer-events'] = 'unset'
this.$refs.trix.style['background'] = 'transparent'
this.$refs.trix.style['background-color'] = ''
this.$refs.trix.style['color'] = ''
}
},
overrideConfig(config) {
@@ -249,6 +311,11 @@ export default {
}
}
return target
},
blur() {
if (this.$refs.trix && this.$refs.trix.blur) {
this.$refs.trix.blur()
}
}
},
mounted() {
@@ -283,4 +350,12 @@ export default {
.trix_container .trix-content {
background-color: white;
}
</style>
trix-editor {
max-height: calc(4 * 1lh);
overflow-y: auto;
}
trix-editor * {
pointer-events: inherit;
}
</style>

View File

@@ -26,7 +26,7 @@
</div>
</div>
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" />
<ui-rich-text-editor ref="descriptionInput" v-model="details.description" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" />
<div class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-1/2 px-1">

View File

@@ -1,6 +1,6 @@
<template>
<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>
<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>

View File

@@ -3,10 +3,10 @@
<div class="flex items-center py-3e">
<slot />
<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>
</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>
</button>
</div>

View File

@@ -71,8 +71,6 @@ export default {
this.showSeriesForm = true
},
submitSeriesForm() {
console.log('submit series form', this.value, this.selectedSeries)
if (!this.selectedSeries.name) {
this.$toast.error('Must enter a series')
return

View File

@@ -0,0 +1,188 @@
import Vue from 'vue'
import '@/plugins/utils'
// This is the actual function that is being tested
const elapsedPrettyExtended = Vue.prototype.$elapsedPrettyExtended
// Helper function to convert days, hours, minutes, seconds to total seconds
function DHMStoSeconds(days, hours, minutes, seconds) {
return seconds + minutes * 60 + hours * 3600 + days * 86400
}
describe('$elapsedPrettyExtended', () => {
describe('function is on the Vue Prototype', () => {
it('exists as a function on Vue.prototype', () => {
expect(Vue.prototype.$elapsedPrettyExtended).to.exist
expect(Vue.prototype.$elapsedPrettyExtended).to.be.a('function')
})
})
describe('param default values', () => {
const testSeconds = DHMStoSeconds(0, 25, 1, 5) // 25h 1m 5s = 90065 seconds
it('uses useDays=true showSeconds=true by default', () => {
expect(elapsedPrettyExtended(testSeconds)).to.equal('1d 1h 1m 5s')
})
it('only useDays=false overrides useDays but keeps showSeconds=true', () => {
expect(elapsedPrettyExtended(testSeconds, false)).to.equal('25h 1m 5s')
})
it('explicit useDays=false showSeconds=false overrides both', () => {
expect(elapsedPrettyExtended(testSeconds, false, false)).to.equal('25h 1m')
})
})
describe('useDays=false showSeconds=true', () => {
const useDaysFalse = false
const showSecondsTrue = true
const testCases = [
[[0, 0, 0, 0], '', '0s -> ""'],
[[0, 1, 0, 1], '1h 1s', '1h 1s -> 1h 1s'],
[[0, 25, 0, 1], '25h 1s', '25h 1s -> 25h 1s']
]
testCases.forEach(([dhms, expected, description]) => {
it(description, () => {
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysFalse, showSecondsTrue)).to.equal(expected)
})
})
})
describe('useDays=true showSeconds=true', () => {
const useDaysTrue = true
const showSecondsTrue = true
const testCases = [
[[0, 0, 0, 0], '', '0s -> ""'],
[[0, 1, 0, 1], '1h 1s', '1h 1s -> 1h 1s'],
[[0, 25, 0, 1], '1d 1h 1s', '25h 1s -> 1d 1h 1s']
]
testCases.forEach(([dhms, expected, description]) => {
it(description, () => {
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysTrue, showSecondsTrue)).to.equal(expected)
})
})
})
describe('useDays=true showSeconds=false', () => {
const useDaysTrue = true
const showSecondsFalse = false
const testCases = [
[[0, 0, 0, 0], '', '0s -> ""'],
[[0, 1, 0, 0], '1h', '1h -> 1h'],
[[0, 1, 0, 1], '1h', '1h 1s -> 1h'],
[[0, 1, 1, 0], '1h 1m', '1h 1m -> 1h 1m'],
[[0, 25, 0, 0], '1d 1h', '25h -> 1d 1h'],
[[0, 25, 0, 1], '1d 1h', '25h 1s -> 1d 1h'],
[[2, 0, 0, 0], '2d', '2d -> 2d']
]
testCases.forEach(([dhms, expected, description]) => {
it(description, () => {
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysTrue, showSecondsFalse)).to.equal(expected)
})
})
})
describe('rounding useDays=true showSeconds=true', () => {
const useDaysTrue = true
const showSecondsTrue = true
const testCases = [
// Seconds rounding
[[0, 0, 0, 1], '1s', '1s -> 1s'],
[[0, 0, 0, 29.9], '30s', '29.9s -> 30s'],
[[0, 0, 0, 30], '30s', '30s -> 30s'],
[[0, 0, 0, 30.1], '30s', '30.1s -> 30s'],
[[0, 0, 0, 59.4], '59s', '59.4s -> 59s'],
[[0, 0, 0, 59.5], '1m', '59.5s -> 1m'],
// Minutes rounding
[[0, 0, 59, 29], '59m 29s', '59m 29s -> 59m 29s'],
[[0, 0, 59, 30], '59m 30s', '59m 30s -> 59m 30s'],
[[0, 0, 59, 59.5], '1h', '59m 59.5s -> 1h'],
// Hours rounding
[[0, 23, 59, 29], '23h 59m 29s', '23h 59m 29s -> 23h 59m 29s'],
[[0, 23, 59, 30], '23h 59m 30s', '23h 59m 30s -> 23h 59m 30s'],
[[0, 23, 59, 59.5], '1d', '23h 59m 59.5s -> 1d'],
// The actual bug case
[[44, 23, 59, 30], '44d 23h 59m 30s', '44d 23h 59m 30s -> 44d 23h 59m 30s']
]
testCases.forEach(([dhms, expected, description]) => {
it(description, () => {
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysTrue, showSecondsTrue)).to.equal(expected)
})
})
})
describe('rounding useDays=true showSeconds=false', () => {
const useDaysTrue = true
const showSecondsFalse = false
const testCases = [
// Seconds rounding - these cases changed behavior from original
[[0, 0, 0, 1], '', '1s -> ""'],
[[0, 0, 0, 29.9], '', '29.9s -> ""'],
[[0, 0, 0, 30], '', '30s -> ""'],
[[0, 0, 0, 30.1], '', '30.1s -> ""'],
[[0, 0, 0, 59.4], '', '59.4s -> ""'],
[[0, 0, 0, 59.5], '1m', '59.5s -> 1m'],
// This is unexpected behavior, but it's consistent with the original behavior
// We preserved the test case, to document the current behavior
// - with showSeconds=false,
// one might expect: 1m 29.5s --round(1.4901m)-> 1m
// actual implementation: 1h 29.5s --roundSeconds-> 1h 30s --roundMinutes-> 2m
// So because of the separate rounding of seconds, and then minutes, it returns 2m
[[0, 0, 1, 29.5], '2m', '1m 29.5s -> 2m'],
// Minutes carry - actual bug fixes below
[[0, 0, 59, 29], '59m', '59m 29s -> 59m'],
[[0, 0, 59, 30], '1h', '59m 30s -> 1h'], // This was an actual bug, used to return 60m
[[0, 0, 59, 59.5], '1h', '59m 59.5s -> 1h'],
// Hours carry
[[0, 23, 59, 29], '23h 59m', '23h 59m 29s -> 23h 59m'],
[[0, 23, 59, 30], '1d', '23h 59m 30s -> 1d'], // This was an actual bug, used to return 23h 60m
[[0, 23, 59, 59.5], '1d', '23h 59m 59.5s -> 1d'],
// The actual bug case
[[44, 23, 59, 30], '45d', '44d 23h 59m 30s -> 45d'] // This was an actual bug, used to return 44d 23h 60m
]
testCases.forEach(([dhms, expected, description]) => {
it(description, () => {
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysTrue, showSecondsFalse)).to.equal(expected)
})
})
})
describe('empty values', () => {
const paramCombos = [
// useDays, showSeconds, description
[true, true, 'with days and seconds'],
[true, false, 'with days, no seconds'],
[false, true, 'no days, with seconds'],
[false, false, 'no days, no seconds']
]
const emptyInputs = [
// input, description
[null, 'null input'],
[undefined, 'undefined input'],
[0, 'zero'],
[0.49, 'rounds to zero'] // Just under rounding threshold
]
paramCombos.forEach(([useDays, showSeconds, paramDesc]) => {
describe(paramDesc, () => {
emptyInputs.forEach(([input, desc]) => {
it(desc, () => {
expect(elapsedPrettyExtended(input, useDays, showSeconds)).to.equal('')
})
})
})
})
})
})

View File

@@ -57,9 +57,10 @@ export default {
for (let entry of entries) {
this.cardWidth = entry.borderBoxSize[0].inlineSize
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.position = 'absolute'
@@ -131,10 +132,7 @@ export default {
this.entityComponentRefs[index] = instance
instance.$mount()
const shelfOffsetY = this.shelfPaddingHeight * this.sizeMultiplier
const row = index % this.entitiesPerShelf
const shelfOffsetX = row * this.totalEntityCardWidth + this.bookshelfMarginLeft
instance.$el.style.transform = `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)`
instance.$el.style.transform = this.entityTransform((index % this.entitiesPerShelf) + 1)
instance.$el.classList.add('absolute', 'top-0', 'left-0')
shelfEl.appendChild(instance.$el)

View File

@@ -28,10 +28,8 @@ export default {
var validOtherFiles = []
var ignoredFiles = []
files.forEach((file) => {
// var filetype = this.checkFileType(file.name)
if (!file.filetype) ignoredFiles.push(file)
else {
// file.filetype = filetype
if (file.filetype === 'audio' || (file.filetype === 'ebook' && mediaType === 'book')) validItemFiles.push(file)
else validOtherFiles.push(file)
}
@@ -165,7 +163,7 @@ export default {
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) {
audiobook.title = dirs.pop()
if (dirs.length > 1) {
@@ -189,7 +187,7 @@ export default {
var firstAudioFile = podcast.itemFiles[0]
if (!firstAudioFile.filepath) return podcast // No path
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) {
podcast.title = dirs.length > 1 ? dirs[1] : dirs[0]
} else {
@@ -212,13 +210,15 @@ export default {
}
var ignoredFiles = itemData.ignoredFiles
var index = 1
var items = itemData.items.filter((ab) => {
if (!ab.itemFiles.length) {
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++))
var items = itemData.items
.filter((ab) => {
if (!ab.itemFiles.length) {
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 {
items,
ignoredFiles
@@ -259,7 +259,7 @@ export default {
otherFiles.forEach((file) => {
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) {
findItem.otherFiles.push(file)
} else {
@@ -270,18 +270,18 @@ export default {
var items = []
var index = 1
// 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) => {
return this.cleanItem({ itemFiles: [audioFile], otherFiles: [], ignoredFiles: [] }, mediaType, index++)
})
} else {
items = Object.values(itemMap).map(i => this.cleanItem(i, mediaType, index++))
items = Object.values(itemMap).map((i) => this.cleanItem(i, mediaType, index++))
}
return {
items,
ignoredFiles: ignoredFiles
}
},
}
}
}
}

View File

@@ -1,6 +1,6 @@
const pkg = require('./package.json')
const routerBasePath = process.env.ROUTER_BASE_PATH || ''
const routerBasePath = process.env.ROUTER_BASE_PATH || '/audiobookshelf'
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 : '/' }]))

View File

@@ -1,12 +1,12 @@
{
"name": "audiobookshelf-client",
"version": "2.17.0",
"version": "2.18.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf-client",
"version": "2.17.0",
"version": "2.18.1",
"license": "ISC",
"dependencies": {
"@nuxtjs/axios": "^5.13.6",

View File

@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "2.17.0",
"version": "2.18.1",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast client",
"main": "index.js",

View File

@@ -86,7 +86,12 @@
</div>
</div>
<div class="w-full flex items-center justify-end p-4">
<div class="w-full flex items-center p-4 space-x-2">
<ui-btn small @click.stop="resetMapDetails">{{ $strings.ButtonReset }}</ui-btn>
<ui-tooltip direction="bottom" :text="$strings.MessageBatchEditPopulateMapDetailsAllHelp">
<ui-btn small :disabled="!hasSelectedBatchUsage" @click.stop="populateFromExisting()">{{ $strings.ButtonBatchEditPopulateFromExisting }}</ui-btn>
</ui-tooltip>
<div class="flex-grow" />
<ui-btn color="success" :disabled="!hasSelectedBatchUsage" :padding-x="8" small class="text-base" :loading="isProcessing" @click="mapBatchDetails">{{ $strings.ButtonApply }}</ui-btn>
</div>
</div>
@@ -97,6 +102,11 @@
<div class="flex justify-center flex-wrap">
<template v-for="libraryItem in libraryItemCopies">
<div :key="libraryItem.id" class="w-full max-w-3xl border border-black-300 p-6 -ml-px -mt-px">
<div class="flex items-center justify-end">
<ui-tooltip direction="bottom" :text="$strings.MessageBatchEditPopulateMapDetailsItemHelp">
<ui-btn small :disabled="!hasSelectedBatchUsage" @click="populateFromExisting(libraryItem.id)">{{ $strings.ButtonBatchEditPopulateMapDetails }}</ui-btn>
</ui-tooltip>
</div>
<widgets-book-details-edit v-if="libraryItem.mediaType === 'book'" :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" @change="handleItemChange" />
<widgets-podcast-details-edit v-else :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" @change="handleItemChange" />
</div>
@@ -228,6 +238,88 @@ export default {
}
},
methods: {
resetMapDetails() {
this.blurBatchForm()
this.batchDetails = {
subtitle: null,
authors: null,
publishedYear: null,
series: [],
genres: [],
tags: [],
narrators: [],
publisher: null,
language: null,
explicit: false,
abridged: false
}
this.selectedBatchUsage = {
subtitle: false,
authors: false,
publishedYear: false,
series: false,
genres: false,
tags: false,
narrators: false,
publisher: false,
language: false,
explicit: false,
abridged: false
}
},
populateFromExisting(libraryItemId) {
this.blurBatchForm()
let libraryItemsToMap = this.libraryItemCopies
if (libraryItemId) {
libraryItemsToMap = this.libraryItemCopies.filter((li) => li.id === libraryItemId)
}
for (const key in this.selectedBatchUsage) {
if (!this.selectedBatchUsage[key]) continue
if (this.isMapAppend && !this.appendableKeys.includes(key)) continue
let existingValues = undefined
libraryItemsToMap.forEach((li) => {
if (key === 'tags') {
if (!existingValues) existingValues = []
li.media.tags.forEach((tag) => {
if (!existingValues.includes(tag)) {
existingValues.push(tag)
}
})
} else if (key === 'authors') {
if (!existingValues) existingValues = []
li.media.metadata[key].forEach((entity) => {
if (!existingValues.some((au) => au.id === entity.id)) {
existingValues.push({
id: entity.id,
name: entity.name
})
}
})
} else if (key === 'series') {
if (!existingValues) existingValues = []
li.media.metadata[key].forEach((entity) => {
if (!existingValues.includes(entity.name)) {
existingValues.push(entity.name)
}
})
} else if (key === 'genres' || key === 'narrators') {
if (!existingValues) existingValues = []
li.media.metadata[key].forEach((item) => {
if (!existingValues.includes(item)) {
existingValues.push(item)
}
})
} else if (existingValues === undefined) {
existingValues = li.media.metadata[key]
}
})
this.batchDetails[key] = existingValues
}
},
handleItemChange(itemChange) {
if (!itemChange.hasChanges) {
this.itemsWithChanges = this.itemsWithChanges.filter((id) => id !== itemChange.libraryItemId)

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" />
<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" />
<div class="flex sm:items-center flex-col sm:flex-row pt-1 mb-2">
@@ -164,6 +178,27 @@ export default {
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: {
@@ -325,7 +360,8 @@ export default {
},
init() {
this.newAuthSettings = {
...this.authSettings
...this.authSettings,
authOpenIDSubfolderForRedirectURLs: this.authSettings.authOpenIDSubfolderForRedirectURLs === undefined ? this.$config.routerBasePath : this.authSettings.authOpenIDSubfolderForRedirectURLs
}
this.enableLocalAuth = this.authMethods.includes('local')
this.enableOpenIDAuth = this.authMethods.includes('openid')

View File

@@ -6,9 +6,9 @@
<div class="pt-4">
<h2 class="font-semibold">{{ $strings.HeaderSettingsGeneral }}</h2>
</div>
<div 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-tooltip :text="$strings.LabelSettingsStoreCoversWithItemHelp">
<div role="article" :aria-label="$strings.LabelSettingsStoreCoversWithItemHelp" class="flex items-end py-2">
<ui-toggle-switch :label="$strings.LabelSettingsStoreCoversWithItem" v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsStoreCoversWithItemHelp">
<p class="pl-4">
<span id="settings-store-cover-with-items">{{ $strings.LabelSettingsStoreCoversWithItem }}</span>
<span class="material-symbols icon-text">info</span>
@@ -16,9 +16,9 @@
</ui-tooltip>
</div>
<div 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-tooltip :text="$strings.LabelSettingsStoreMetadataWithItemHelp">
<div role="article" :aria-label="$strings.LabelSettingsStoreMetadataWithItemHelp" class="flex items-center py-2">
<ui-toggle-switch :label="$strings.LabelSettingsStoreMetadataWithItem" v-model="newServerSettings.storeMetadataWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithItem', val)" />
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsStoreMetadataWithItemHelp">
<p class="pl-4">
<span id="settings-store-metadata-with-items">{{ $strings.LabelSettingsStoreMetadataWithItem }}</span>
<span class="material-symbols icon-text">info</span>
@@ -26,9 +26,9 @@
</ui-tooltip>
</div>
<div 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-tooltip :text="$strings.LabelSettingsSortingIgnorePrefixesHelp">
<div role="article" :aria-label="$strings.LabelSettingsSortingIgnorePrefixesHelp" class="flex items-center py-2">
<ui-toggle-switch :label="$strings.LabelSettingsSortingIgnorePrefixes" v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsSortingIgnorePrefixesHelp">
<p class="pl-4">
<span id="settings-sorting-ignore-prefixes">{{ $strings.LabelSettingsSortingIgnorePrefixes }}</span>
<span class="material-symbols icon-text">info</span>
@@ -42,18 +42,13 @@
</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">
<h2 class="font-semibold">{{ $strings.HeaderSettingsScanner }}</h2>
</div>
<div 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-tooltip :text="$strings.LabelSettingsParseSubtitlesHelp">
<div role="article" :aria-label="$strings.LabelSettingsParseSubtitlesHelp" class="flex items-center py-2">
<ui-toggle-switch :label="$strings.LabelSettingsParseSubtitles" v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" />
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsParseSubtitlesHelp">
<p class="pl-4">
<span id="settings-parse-subtitles">{{ $strings.LabelSettingsParseSubtitles }}</span>
<span class="material-symbols icon-text">info</span>
@@ -61,9 +56,9 @@
</ui-tooltip>
</div>
<div 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-tooltip :text="$strings.LabelSettingsFindCoversHelp">
<div role="article" :aria-label="$strings.LabelSettingsFindCoversHelp" class="flex items-center py-2">
<ui-toggle-switch :label="$strings.LabelSettingsFindCovers" v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" />
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsFindCoversHelp">
<p class="pl-4">
<span id="settings-find-covers">{{ $strings.LabelSettingsFindCovers }}</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" />
</div>
<div 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-tooltip :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
<div role="article" :aria-label="$strings.LabelSettingsPreferMatchedMetadataHelp" class="flex items-center py-2">
<ui-toggle-switch :label="$strings.LabelSettingsPreferMatchedMetadata" v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
<p class="pl-4">
<span id="settings-prefer-matched-metadata">{{ $strings.LabelSettingsPreferMatchedMetadata }}</span>
<span class="material-symbols icon-text">info</span>
@@ -85,15 +80,29 @@
</ui-tooltip>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-disable-watcher" v-model="scannerEnableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', !val)" />
<ui-tooltip :text="$strings.LabelSettingsEnableWatcherHelp">
<div role="article" :aria-label="$strings.LabelSettingsEnableWatcherHelp" class="flex items-center py-2">
<ui-toggle-switch :label="$strings.LabelSettingsEnableWatcher" v-model="scannerEnableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', !val)" />
<ui-tooltip aria-hidden="true" :text="$strings.LabelSettingsEnableWatcherHelp">
<p class="pl-4">
<span id="settings-disable-watcher">{{ $strings.LabelSettingsEnableWatcher }}</span>
<span class="material-symbols icon-text">info</span>
</p>
</ui-tooltip>
</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 class="flex-1">
@@ -324,21 +333,21 @@ export default {
},
updateServerSettings(payload) {
this.updatingServerSettings = true
this.$store
.dispatch('updateServerSettings', payload)
.then(() => {
this.updatingServerSettings = false
this.$store.dispatch('updateServerSettings', payload).then((response) => {
this.updatingServerSettings = false
if (payload.language) {
// Updating language after save allows for re-rendering
this.$setLanguageCode(payload.language)
}
})
.catch((error) => {
console.error('Failed to update server settings', error)
this.updatingServerSettings = false
this.$toast.error(this.$strings.ToastFailedToUpdate)
})
if (response.error) {
console.error('Failed to update server settins', response.error)
this.$toast.error(response.error)
this.initServerSettings()
return
}
if (payload.language) {
// Updating language after save allows for re-rendering
this.$setLanguageCode(payload.language)
}
})
},
initServerSettings() {
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}

View File

@@ -25,7 +25,7 @@
<tr v-for="feed in feeds" :key="feed.id" class="cursor-pointer h-12" @click="showFeed(feed)">
<!-- -->
<td>
<img :src="coverUrl(feed)" class="h-full w-full" />
<img :src="coverUrl(feed)" class="h-auto w-full" />
</td>
<!-- -->
<td class="w-48 max-w-64 min-w-24 text-left truncate">
@@ -126,7 +126,7 @@ export default {
},
coverUrl(feed) {
if (!feed.coverPath) return `${this.$config.routerBasePath}/Logo.png`
return `${feed.feedUrl}/cover`
return `${this.$config.routerBasePath}${feed.feedUrl}/cover`
},
async loadFeeds() {
const data = await this.$axios.$get(`/api/feeds`).catch((err) => {

View File

@@ -14,11 +14,7 @@
<h1 class="text-xl pl-2">{{ username }}</h1>
</div>
<div v-if="userToken" class="flex text-xs mt-4">
<ui-text-input-with-label :label="$strings.LabelApiToken" :value="userToken" readonly />
<div class="px-1 mt-8 cursor-pointer" @click="copyToClipboard(userToken)">
<span class="material-symbols pl-2 text-base">content_copy</span>
</div>
<ui-text-input-with-label :label="$strings.LabelApiToken" :value="userToken" readonly show-copy />
</div>
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
<div class="py-2">
@@ -140,9 +136,6 @@ export default {
}
},
methods: {
copyToClipboard(str) {
this.$copyToClipboard(str, this)
},
async init() {
this.listeningSessions = await this.$axios
.$get(`/api/users/${this.user.id}/listening-sessions?page=0&itemsPerPage=10`)

View File

@@ -2,6 +2,10 @@
<div>
<app-settings-content :header-text="$strings.HeaderUsers">
<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">
<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>
@@ -13,7 +17,7 @@
<ui-btn color="primary" small @click="setShowUserModal()">{{ $strings.ButtonAddUser }}</ui-btn>
</template>
<tables-users-table class="pt-2" @edit="setShowUserModal" />
<tables-users-table class="pt-2" @edit="setShowUserModal" @numUsers="(count) => (numUsers = count)" />
</app-settings-content>
<modals-account-modal ref="accountModal" v-model="showAccountModal" :account="selectedAccount" />
</div>
@@ -29,7 +33,8 @@ export default {
data() {
return {
selectedAccount: null,
showAccountModal: false
showAccountModal: false,
numUsers: 0
}
},
computed: {},

View File

@@ -12,12 +12,12 @@
<!-- 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 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>
</div>
</button>
</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>
@@ -87,7 +87,7 @@
</ui-btn>
<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 }}
</ui-btn>
@@ -96,12 +96,12 @@
</ui-tooltip>
<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 }}
</ui-btn>
<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 v-if="!isPodcast" :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
@@ -110,12 +110,12 @@
<!-- Only admin or root user can download new episodes -->
<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-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="148" @action="contextMenuAction">
<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>
</button>
</template>
@@ -123,7 +123,8 @@
</div>
<div class="my-4 w-full">
<p ref="description" id="item-description" dir="auto" class="text-base text-gray-100 whitespace-pre-line mb-1" :class="{ 'show-full': showFullDescription }">{{ description }}</p>
<div ref="description" id="item-description" dir="auto" class="default-style less-spacing text-base text-gray-100 whitespace-pre-line mb-1" :class="{ 'show-full': showFullDescription }" v-html="description" />
<button v-if="isDescriptionClamped" class="py-0.5 flex items-center text-slate-300 hover:text-white" @click="showFullDescription = !showFullDescription">{{ showFullDescription ? $strings.ButtonReadLess : $strings.ButtonReadMore }} <span class="material-symbols text-xl pl-1" v-html="showFullDescription ? 'expand_less' : '&#xe313;'" /></button>
</div>
@@ -141,7 +142,7 @@
</div>
<modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" />
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :library-item-id="libraryItemId" hide-create @select="selectBookmark" />
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :playback-rate="1" :library-item-id="libraryItemId" hide-create @select="selectBookmark" />
</div>
</template>
@@ -804,8 +805,7 @@ export default {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
max-height: 6.25rem;
transition: all 0.3s ease-in-out;
max-height: calc(6 * 1lh);
}
#item-description.show-full {
-webkit-line-clamp: unset;

View File

@@ -12,6 +12,10 @@
<div class="w-full pt-16">
<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>
<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>
@@ -63,6 +67,9 @@ export default {
if (!this.playbackSession.coverPath) return `${this.$config.routerBasePath}/book_placeholder.jpg`
return `${this.$config.routerBasePath}/public/share/${this.mediaItemShare.slug}/cover`
},
downloadUrl() {
return `${process.env.serverUrl}/public/share/${this.mediaItemShare.slug}/download`
},
audioTracks() {
return (this.playbackSession.audioTracks || []).map((track) => {
track.relativeContentUrl = track.contentUrl
@@ -103,6 +110,84 @@ export default {
}
},
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) {
if (!this.playbackSession.coverPath) return
const fac = new FastAverageColor()
@@ -119,19 +204,32 @@ export default {
})
},
playPause() {
if (this.isPlaying) {
this.pause()
} else {
this.play()
}
},
play() {
if (!this.localAudioPlayer || !this.hasLoaded) return
this.localAudioPlayer.playPause()
this.localAudioPlayer.play()
},
pause() {
if (!this.localAudioPlayer || !this.hasLoaded) return
this.localAudioPlayer.pause()
},
jumpForward() {
if (!this.localAudioPlayer || !this.hasLoaded) return
const currentTime = this.localAudioPlayer.getCurrentTime()
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() {
if (!this.localAudioPlayer || !this.hasLoaded) return
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) {
if (!this.localAudioPlayer || !this.hasLoaded) return
@@ -204,6 +302,7 @@ export default {
} else {
this.stopPlayInterval()
}
this.updateMediaSessionPlaybackState()
},
playerTimeUpdate(time) {
this.setCurrentTime(time)
@@ -245,9 +344,14 @@ export default {
},
playerFinished() {
console.log('Player finished')
},
downloadShareItem() {
this.$downloadFile(this.downloadUrl)
}
},
mounted() {
this.$store.dispatch('user/loadUserSettings')
this.resize()
window.addEventListener('resize', this.resize)
window.addEventListener('keydown', this.keyDown)
@@ -262,6 +366,8 @@ export default {
this.localAudioPlayer.on('timeupdate', this.playerTimeUpdate.bind(this))
this.localAudioPlayer.on('error', this.playerError.bind(this))
this.localAudioPlayer.on('finished', this.playerFinished.bind(this))
this.setMediaSession()
},
beforeDestroy() {
window.removeEventListener('resize', this.resize)

View File

@@ -1,20 +1,20 @@
<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">
<!-- Library & folder picker -->
<div class="flex my-6 -mx-2">
<div class="w-1/5 px-2">
<div class="flex flex-wrap my-6 md:-mx-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" />
</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" />
</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" />
</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">
<ui-toggle-switch v-model="fetchMetadata.enabled" class="inline-flex" />
<span class="pl-2 text-base">{{ $strings.LabelAutoFetchMetadata }}</span>
@@ -33,13 +33,13 @@
</widgets-alert>
<!-- 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'">
<p class="text-2xl text-center">{{ isDragging ? $strings.LabelUploaderDropFiles : $strings.LabelUploaderDragAndDrop }}</p>
<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 : isIOS ? $strings.LabelUploaderDragAndDropFilesOnly : $strings.LabelUploaderDragAndDrop }}</p>
<p class="text-center text-sm my-5">{{ $strings.MessageOr }}</p>
<div class="w-full max-w-xl mx-auto">
<div class="flex">
<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 class="pt-8 text-center">
@@ -48,7 +48,7 @@
</p>
<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>
</div>
</div>
@@ -84,8 +84,8 @@
</div>
</div>
<input ref="fileInput" type="file" multiple :accept="inputAccept" class="hidden" @change="inputChanged" />
<input ref="fileFolderInput" type="file" webkitdirectory 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" v-if="!isIOS" />
</div>
</template>
@@ -127,6 +127,10 @@ export default {
})
return extensions
},
isIOS() {
const ua = window.navigator.userAgent
return /iPad|iPhone|iPod/.test(ua) && !window.MSStream
},
streamLibraryItem() {
return this.$store.state.streamLibraryItem
},

View File

@@ -7,6 +7,7 @@ const defaultCode = 'en-us'
const languageCodeMap = {
bg: { label: 'Български', dateFnsLocale: 'bg' },
bn: { label: 'বাংলা', dateFnsLocale: 'bn' },
ca: { label: 'Català', dateFnsLocale: 'ca' },
cs: { label: 'Čeština', dateFnsLocale: 'cs' },
da: { label: 'Dansk', dateFnsLocale: 'da' },
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
const podcastSearchRegionMap = {
au: { label: 'Australia' },
br: { label: 'Brasil' },
be: { label: 'België / Belgique / Belgien' },
cz: { label: 'Česko' },
@@ -56,6 +58,7 @@ const podcastSearchRegionMap = {
hu: { label: 'Magyarország' },
nl: { label: 'Nederland' },
no: { label: 'Norge' },
nz: { label: 'New Zealand' },
at: { label: 'Österreich' },
pl: { label: 'Polska' },
pt: { label: 'Portugal' },

View File

@@ -128,12 +128,11 @@ Vue.prototype.$sanitizeSlug = (str) => {
return str
}
Vue.prototype.$copyToClipboard = (str, ctx) => {
Vue.prototype.$copyToClipboard = (str) => {
return new Promise((resolve) => {
if (navigator.clipboard) {
navigator.clipboard.writeText(str).then(
() => {
if (ctx) ctx.$toast.success('Copied to clipboard')
resolve(true)
},
(err) => {
@@ -152,7 +151,6 @@ Vue.prototype.$copyToClipboard = (str, ctx) => {
document.execCommand('copy')
document.body.removeChild(el)
if (ctx) ctx.$toast.success('Copied to clipboard')
resolve(true)
}
})

View File

@@ -69,17 +69,22 @@ Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true, showSeconds = t
let hours = Math.floor(minutes / 60)
minutes -= hours * 60
// Handle rollovers before days calculation
if (minutes && seconds && !showSeconds) {
if (seconds >= 30) minutes++
if (minutes >= 60) {
hours++ // Increment hours if minutes roll over
minutes -= 60 // adjust minutes
}
}
// Now calculate days with the final hours value
let days = 0
if (useDays || Math.floor(hours / 24) >= 100) {
days = Math.floor(hours / 24)
hours -= days * 24
}
// If not showing seconds then round minutes up
if (minutes && seconds && !showSeconds) {
if (seconds >= 30) minutes++
}
const strs = []
if (days) strs.push(`${days}d`)
if (hours) strs.push(`${hours}h`)

View File

@@ -72,16 +72,17 @@ export const actions = {
return this.$axios
.$patch('/api/settings', updatePayload)
.then((result) => {
if (result.success) {
if (result.serverSettings) {
commit('setServerSettings', result.serverSettings)
return true
} else {
return false
}
return result
})
.catch((error) => {
console.error('Failed to update server settings', error)
return false
const errorMsg = error.response?.data || 'Unknown error'
return {
error: errorMsg
}
})
},
checkForUpdate({ commit }) {

View File

@@ -5,6 +5,7 @@ export const state = () => ({
orderDesc: false,
filterBy: 'all',
playbackRate: 1,
playbackRateIncrementDecrement: 0.1,
bookshelfCoverSize: 120,
collapseSeries: false,
collapseBookSeries: false,

View File

@@ -127,5 +127,30 @@
"HeaderCollectionItems": "عناصر المجموعة",
"HeaderCover": "الغلاف",
"HeaderCurrentDownloads": "التنزيلات الجارية",
"HeaderCustomMessageOnLogin": "رسالة مخصصة عند تسجيل الدخول"
"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": "إدارة العلامات"
}

117
client/strings/be.json Normal file
View File

@@ -0,0 +1,117 @@
{
"ButtonAdd": "Дадаць",
"ButtonAddChapters": "Дадаць раздзелы",
"ButtonAddDevice": "Дадаць прыладу",
"ButtonAddLibrary": "Дадаць бібліятэку",
"ButtonAddPodcasts": "Дадаць падкасты",
"ButtonAddUser": "Дадаць карыстальніка",
"ButtonAddYourFirstLibrary": "Дадайце сваю першую бібліятэку",
"ButtonApply": "Ужыць",
"ButtonApplyChapters": "Ужыць раздзелы",
"ButtonAuthors": "Аўтары",
"ButtonBack": "Назад",
"ButtonBrowseForFolder": "Знайсці тэчку",
"ButtonCancel": "Адмяніць",
"ButtonCancelEncode": "Адмяніць кадзіраванне",
"ButtonChangeRootPassword": "Зменіце Root пароль",
"ButtonCheckAndDownloadNewEpisodes": "Праверыць і спампаваць новыя эпізоды",
"ButtonChooseAFolder": "Выбраць тэчку",
"ButtonChooseFiles": "Выбраць файлы",
"ButtonClearFilter": "Ачысціць фільтр",
"ButtonCloseFeed": "Закрыць стужку",
"ButtonCloseSession": "Закрыць адкрыты сеанс",
"ButtonCollections": "Калекцыі",
"ButtonConfigureScanner": "Наладзіць сканер",
"ButtonCreate": "Ствараць",
"ButtonCreateBackup": "Стварыць рэзервовую копію",
"ButtonDelete": "Выдаліць",
"ButtonDownloadQueue": "Чарга",
"ButtonEdit": "Рэдагаваць",
"ButtonEditChapters": "Рэдагаваць раздзелы",
"ButtonEditPodcast": "Рэдагаваць падкаст",
"ButtonEnable": "Уключыць",
"ButtonFireAndFail": "Агонь і няўдача",
"ButtonFireOnTest": "Тэст на вогнеўстойлівасць",
"ButtonForceReScan": "Прымусовае паўторнае сканаванне",
"ButtonFullPath": "Поўны шлях",
"ButtonHide": "Схаваць",
"ButtonIssues": "Праблемы",
"ButtonJumpBackward": "Перайсці назад",
"ButtonJumpForward": "Перайсці наперад",
"ButtonLibrary": "Бібліятэка",
"ButtonLogout": "Выйсці",
"ButtonLookup": "",
"ButtonMapChapterTitles": "Супаставіць назвы раздзелаў",
"ButtonMatchAllAuthors": "Супадзенне ўсіх аўтараў",
"ButtonNevermind": "Няважна",
"ButtonNext": "Далей",
"ButtonNextChapter": "Наступны раздзел",
"ButtonNextItemInQueue": "Наступны элемент у чарзе",
"ButtonOk": "Добра",
"ButtonOpenFeed": "Адкрыць стужку",
"ButtonOpenManager": "Адкрыць менеджар",
"ButtonPause": "Паўза",
"ButtonPlay": "Прайграць",
"ButtonPlayAll": "Прайграць усё",
"ButtonPlaying": "Прайграваецца",
"ButtonPlaylists": "Плэйлісты",
"ButtonPrevious": "Папярэдні",
"ButtonPreviousChapter": "Папярэдні раздзел",
"ButtonProbeAudioFile": "Праверыць аўдыяфайл",
"ButtonPurgeAllCache": "Ачысціць увесь кэш",
"ButtonPurgeItemsCache": "Ачысціць кэш элементаў",
"ButtonQueueAddItem": "Дадаць у чаргу",
"ButtonQueueRemoveItem": "Выдаліць з чаргі",
"ButtonQuickEmbed": "Хуткае ўбудаванне",
"ButtonQuickEmbedMetadata": "Хуткае ўбудаванне метаданых",
"ButtonQuickMatch": "Хуткі пошук",
"ButtonReScan": "Паўторнае сканаванне",
"ButtonRead": "Чытаць",
"ButtonRefresh": "Абнавіць",
"ButtonRemove": "Выдаліць",
"ButtonRemoveAll": "Выдаліць усе",
"ButtonRemoveAllLibraryItems": "Выдаліць усе элементы бібліятэкі",
"ButtonReset": "Скінуць",
"ButtonResetToDefault": "Скінуць па змаўчанні",
"ButtonRestore": "Аднавіць",
"ButtonSave": "Захаваць",
"ButtonSaveAndClose": "Захаваць і зачыніць",
"ButtonSaveTracklist": "Захаваць спіс трэкаў",
"ButtonScan": "Сканаваць",
"ButtonScanLibrary": "Сканіраваць бібліятэку",
"ButtonScrollLeft": "Пракруціць улева",
"ButtonScrollRight": "Пракруціць направа",
"ButtonSearch": "Пошук",
"ButtonSelectFolderPath": "Выбраць шлях да тэчкі",
"ButtonSeries": "Серыі",
"ButtonSetChaptersFromTracks": "Усталяваць раздзелы з трэкаў",
"ButtonShare": "Падзяліцца",
"ButtonStartM4BEncode": "Пачаць кадзіраванне ў M4B",
"ButtonStartMetadataEmbed": "Пачаць убудаванне метаданых",
"ButtonStats": "Статыстыка",
"ButtonSubmit": "Адправіць",
"ButtonTest": "Тэст",
"ButtonUnlinkOpenId": "Адвязаць OpenID",
"ButtonUpload": "Загрузіць",
"ButtonUploadBackup": "Загрузіць рэзервовую копію",
"ButtonUploadCover": "Загрузіць вокладку",
"ButtonUploadOPMLFile": "Загрузіць OPML файл",
"ButtonUserDelete": "Выдаліць карыстальніка {0}",
"ButtonUserEdit": "Рэдагаваць карыстальніка {0}",
"ButtonViewAll": "Прагледзець усе",
"ButtonYes": "Так",
"HeaderAccount": "Уліковы запіс",
"HeaderAddCustomMetadataProvider": "Дадаць карыстальніцкага пастаўшчыка метаданных",
"HeaderAppriseNotificationSettings": "Налады апавяшчэнняў Apprise",
"HeaderAudiobookTools": "Сродкі кіравання файламі аўдыякніг",
"HeaderAuthentication": "Аўтэнтыфікацыя",
"HeaderBackups": "Рэзервовыя копіі",
"HeaderChangePassword": "Змяніць пароль",
"HeaderChapters": "Раздзелы",
"HeaderChooseAFolder": "Выбраць тэчку",
"HeaderCollection": "Калекцыя",
"HeaderCollectionItems": "Элементы калекцыі",
"HeaderCover": "Вокладка",
"HeaderCurrentDownloads": "Бягучыя загрузкі",
"HeaderCustomMessageOnLogin": "Карыстальніцкае паведамленне пры ўваходзе"
}

View File

@@ -629,7 +629,6 @@
"MessageItemsSelected": "{0} избрани",
"MessageItemsUpdated": "{0} елемента обновени",
"MessageJoinUsOn": "Присъединете се към нас",
"MessageListeningSessionsInTheLastYear": "{0} слушателски сесии през последната година",
"MessageLoading": "Зареждане...",
"MessageLoadingFolders": "Зареждане на Папки...",
"MessageM4BFailed": "M4B Провалено!",
@@ -726,10 +725,8 @@
"ToastBookmarkCreateFailed": "Неуспешно създаване на отметка",
"ToastBookmarkCreateSuccess": "Отметката е създадена",
"ToastBookmarkRemoveSuccess": "Отметката е премахната",
"ToastBookmarkUpdateSuccess": "Отметката е обновена",
"ToastChaptersHaveErrors": "Главите имат грешки",
"ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия",
"ToastCollectionItemsRemoveSuccess": "Елемент(и) премахнати от колекция",
"ToastCollectionRemoveSuccess": "Колекцията е премахната",
"ToastCollectionUpdateSuccess": "Колекцията е обновена",
"ToastItemCoverUpdateSuccess": "Корицата на елемента е обновена",

View File

@@ -66,6 +66,7 @@
"ButtonPurgeItemsCache": "আইটেম ক্যাশে পরিষ্কার করুন",
"ButtonQueueAddItem": "সারিতে যোগ করুন",
"ButtonQueueRemoveItem": "সারি থেকে মুছে ফেলুন",
"ButtonQuickEmbed": "দ্রুত এম্বেড করুন",
"ButtonQuickEmbedMetadata": "মেটাডেটা দ্রুত এম্বেড করুন",
"ButtonQuickMatch": "দ্রুত ম্যাচ",
"ButtonReScan": "পুনরায় স্ক্যান",
@@ -87,6 +88,8 @@
"ButtonSaveTracklist": "ট্র্যাকলিস্ট সংরক্ষণ করুন",
"ButtonScan": "স্ক্যান",
"ButtonScanLibrary": "স্ক্যান লাইব্রেরি",
"ButtonScrollLeft": "বাম দিকে স্ক্রল করুন",
"ButtonScrollRight": "ডানদিকে স্ক্রল করুন",
"ButtonSearch": "অনুসন্ধান",
"ButtonSelectFolderPath": "ফোল্ডারের পথ নির্বাচন করুন",
"ButtonSeries": "সিরিজ",
@@ -162,6 +165,7 @@
"HeaderNotificationUpdate": "বিজ্ঞপ্তি আপডেট করুন",
"HeaderNotifications": "বিজ্ঞপ্তি",
"HeaderOpenIDConnectAuthentication": "ওপেনআইডি সংযোগ প্রমাণীকরণ",
"HeaderOpenListeningSessions": "শোনার সেশন খুলুন",
"HeaderOpenRSSFeed": "আরএসএস ফিড খুলুন",
"HeaderOtherFiles": "অন্যান্য ফাইল",
"HeaderPasswordAuthentication": "পাসওয়ার্ড প্রমাণীকরণ",
@@ -179,6 +183,7 @@
"HeaderRemoveEpisodes": "{0}টি পর্ব সরান",
"HeaderSavedMediaProgress": "মিডিয়া সংরক্ষণের অগ্রগতি",
"HeaderSchedule": "সময়সূচী",
"HeaderScheduleEpisodeDownloads": "স্বয়ংক্রিয় পর্ব ডাউনলোডের সময়সূচী নির্ধারন করুন",
"HeaderScheduleLibraryScans": "স্বয়ংক্রিয় লাইব্রেরি স্ক্যানের সময়সূচী",
"HeaderSession": "সেশন",
"HeaderSetBackupSchedule": "ব্যাকআপ সময়সূচী সেট করুন",
@@ -187,6 +192,7 @@
"HeaderSettingsExperimental": "পরীক্ষামূলক ফিচার",
"HeaderSettingsGeneral": "সাধারণ",
"HeaderSettingsScanner": "স্ক্যানার",
"HeaderSettingsWebClient": "ওয়েব ক্লায়েন্ট",
"HeaderSleepTimer": "স্লিপ টাইমার",
"HeaderStatsLargestItems": "সবচেয়ে বড় আইটেম",
"HeaderStatsLongestItems": "দীর্ঘতম আইটেম (ঘন্টা)",
@@ -224,7 +230,11 @@
"LabelAllUsersExcludingGuests": "অতিথি ব্যতীত সকল ব্যবহারকারী",
"LabelAllUsersIncludingGuests": "অতিথি সহ সকল ব্যবহারকারী",
"LabelAlreadyInYourLibrary": "ইতিমধ্যেই আপনার লাইব্রেরিতে রয়েছে",
"LabelApiToken": "API টোকেন",
"LabelAppend": "সংযোজন",
"LabelAudioBitrate": "অডিও বিটরেট (যেমন- 128k)",
"LabelAudioChannels": "অডিও চ্যানেল (১ বা ২)",
"LabelAudioCodec": "অডিও কোডেক",
"LabelAuthor": "লেখক",
"LabelAuthorFirstLast": "লেখক (প্রথম শেষ)",
"LabelAuthorLastFirst": "লেখক (শেষ, প্রথম)",
@@ -237,6 +247,7 @@
"LabelAutoRegister": "স্বয়ংক্রিয় নিবন্ধন",
"LabelAutoRegisterDescription": "লগ ইন করার পর স্বয়ংক্রিয়ভাবে নতুন ব্যবহারকারী তৈরি করুন",
"LabelBackToUser": "ব্যবহারকারীর কাছে ফিরে যান",
"LabelBackupAudioFiles": "অডিও ফাইলগুলো ব্যাকআপ",
"LabelBackupLocation": "ব্যাকআপ অবস্থান",
"LabelBackupsEnableAutomaticBackups": "স্বয়ংক্রিয় ব্যাকআপ সক্ষম করুন",
"LabelBackupsEnableAutomaticBackupsHelp": "ব্যাকআপগুলি /মেটাডাটা/ব্যাকআপে সংরক্ষিত",
@@ -245,15 +256,18 @@
"LabelBackupsNumberToKeep": "ব্যাকআপের সংখ্যা রাখুন",
"LabelBackupsNumberToKeepHelp": "এক সময়ে শুধুমাত্র ১ টি ব্যাকআপ সরানো হবে তাই যদি আপনার কাছে ইতিমধ্যে এর চেয়ে বেশি ব্যাকআপ থাকে তাহলে আপনাকে ম্যানুয়ালি সেগুলি সরিয়ে ফেলতে হবে।",
"LabelBitrate": "বিটরেট",
"LabelBonus": "উপরিলাভ",
"LabelBooks": "বইগুলো",
"LabelButtonText": "ঘর পাঠ্য",
"LabelByAuthor": "দ্বারা {0}",
"LabelChangePassword": "পাসওয়ার্ড পরিবর্তন করুন",
"LabelChannels": "চ্যানেল",
"LabelChapterCount": "{0} অধ্যায়",
"LabelChapterTitle": "অধ্যায়ের শিরোনাম",
"LabelChapters": "অধ্যায়",
"LabelChaptersFound": "অধ্যায় পাওয়া গেছে",
"LabelClickForMoreInfo": "আরো তথ্যের জন্য ক্লিক করুন",
"LabelClickToUseCurrentValue": "বর্তমান মান ব্যবহার করতে ক্লিক করুন",
"LabelClosePlayer": "প্লেয়ার বন্ধ করুন",
"LabelCodec": "কোডেক",
"LabelCollapseSeries": "সিরিজ সঙ্কুচিত করুন",
@@ -286,6 +300,7 @@
"LabelDiscover": "আবিষ্কার",
"LabelDownload": "ডাউনলোড করুন",
"LabelDownloadNEpisodes": "{0}টি পর্ব ডাউনলোড করুন",
"LabelDownloadable": "ডাউনলোডযোগ্য",
"LabelDuration": "সময়কাল",
"LabelDurationComparisonExactMatch": "(সঠিক মিল)",
"LabelDurationComparisonLonger": "({0} দীর্ঘ)",
@@ -303,12 +318,25 @@
"LabelEmailSettingsTestAddress": "পরীক্ষার ঠিকানা",
"LabelEmbeddedCover": "এম্বেডেড কভার",
"LabelEnable": "সক্ষম করুন",
"LabelEncodingBackupLocation": "আপনার আসল অডিও ফাইলগুলোর একটি ব্যাকআপ এখানে সংরক্ষণ করা হবে:",
"LabelEncodingChaptersNotEmbedded": "মাল্টি-ট্র্যাক অডিওবুকগুলোতে অধ্যায় এম্বেড করা হয় না।",
"LabelEncodingClearItemCache": "পর্যায়ক্রমে আইটেম ক্যাশে পরিষ্কার করতে ভুলবেন না।",
"LabelEncodingFinishedM4B": "সমাপ্ত হওয়া M4B-গুলো আপনার অডিওবুক ফোল্ডারে এখানে রাখা হবে:",
"LabelEncodingInfoEmbedded": "আপনার অডিওবুক ফোল্ডারের ভিতরে অডিও ট্র্যাকগুলোতে মেটাডেটা এমবেড করা হবে।",
"LabelEncodingStartedNavigation": "একবার টাস্ক শুরু হলে আপনি এই পৃষ্ঠা থেকে অন্যত্র যেতে পারেন।",
"LabelEncodingTimeWarning": "এনকোডিং ৩০ মিনিট পর্যন্ত সময় নিতে পারে।",
"LabelEncodingWarningAdvancedSettings": "সতর্কতা: এই সেটিংস আপডেট করবেন না, যদি না আপনি ffmpeg এনকোডিং বিকল্পগুলোর সাথে পরিচিত হন।",
"LabelEncodingWatcherDisabled": "আপনার যদি পর্যবেক্ষক অক্ষম থাকে তবে আপনাকে পরে এই অডিওবুকটি পুনরায় স্ক্যান করতে হবে।",
"LabelEnd": "সমাপ্ত",
"LabelEndOfChapter": "অধ্যায়ের সমাপ্তি",
"LabelEpisode": "পর্ব",
"LabelEpisodeNotLinkedToRssFeed": "পর্বটি আরএসএস ফিডের সাথে সংযুক্ত করা হয়নি",
"LabelEpisodeNumber": "পর্ব #{0}",
"LabelEpisodeTitle": "পর্বের শিরোনাম",
"LabelEpisodeType": "পর্বের ধরন",
"LabelEpisodeUrlFromRssFeed": "আরএসএস ফিড থেকে পর্ব URL",
"LabelEpisodes": "পর্বগুলো",
"LabelEpisodic": "প্রাসঙ্গিক",
"LabelExample": "উদাহরণ",
"LabelExpandSeries": "সিরিজ প্রসারিত করুন",
"LabelExpandSubSeries": "সাব সিরিজ প্রসারিত করুন",
@@ -336,6 +364,7 @@
"LabelFontScale": "ফন্ট স্কেল",
"LabelFontStrikethrough": "অবচ্ছেদন রেখা",
"LabelFormat": "ফরম্যাট",
"LabelFull": "পূর্ণ",
"LabelGenre": "ঘরানা",
"LabelGenres": "ঘরানাগুলো",
"LabelHardDeleteFile": "জোরপূর্বক ফাইল মুছে ফেলুন",
@@ -391,6 +420,10 @@
"LabelLowestPriority": "সর্বনিম্ন অগ্রাধিকার",
"LabelMatchExistingUsersBy": "বিদ্যমান ব্যবহারকারীদের দ্বারা মিলিত করুন",
"LabelMatchExistingUsersByDescription": "বিদ্যমান ব্যবহারকারীদের সংযোগ করার জন্য ব্যবহৃত হয়। একবার সংযুক্ত হলে, ব্যবহারকারীদের আপনার SSO প্রদানকারীর থেকে একটি অনন্য আইডি দ্বারা মিলিত হবে",
"LabelMaxEpisodesToDownload": "সর্বাধিক # টি পর্ব ডাউনলোড করা হবে। অসীমের জন্য 0 ব্যবহার করুন।",
"LabelMaxEpisodesToDownloadPerCheck": "প্রতি কিস্তিতে সর্বাধিক # টি নতুন পর্ব ডাউনলোড করা হবে",
"LabelMaxEpisodesToKeep": "সর্বোচ্চ # টি পর্ব রাখা হবে",
"LabelMaxEpisodesToKeepHelp": " কোন সর্বোচ্চ সীমা সেট করে না। একটি নতুন পর্ব স্বয়ংক্রিয়-ডাউনলোড হওয়ার পরে আপনার যদি X-এর বেশি পর্ব থাকে তবে এটি সবচেয়ে পুরানো পর্বটি মুছে ফেলবে। এটি প্রতি নতুন ডাউনলোডের জন্য শুধুমাত্র ১ টি পর্ব মুছে ফেলবে।",
"LabelMediaPlayer": "মিডিয়া প্লেয়ার",
"LabelMediaType": "মিডিয়ার ধরন",
"LabelMetaTag": "মেটা ট্যাগ",
@@ -436,12 +469,14 @@
"LabelOpenIDGroupClaimDescription": "ওপেনআইডি দাবির নাম যাতে ব্যবহারকারীর গোষ্ঠীর একটি তালিকা থাকে। সাধারণত <code>গ্রুপ</code> হিসাবে উল্লেখ করা হয়। <b>কনফিগার করা থাকলে</b>, অ্যাপ্লিকেশনটি স্বয়ংক্রিয়ভাবে এর উপর ভিত্তি করে ব্যবহারকারীর গোষ্ঠীর সদস্যপদ নির্ধারণ করবে, শর্ত এই যে এই গোষ্ঠীগুলি কেস-অসংবেদনশীলভাবে দাবিতে 'অ্যাডমিন', 'ব্যবহারকারী' বা 'অতিথি' নাম দেওয়া হয়৷ দাবিতে একটি তালিকা থাকা উচিত এবং যদি একজন ব্যবহারকারী একাধিক গোষ্ঠীর অন্তর্গত হয় তবে অ্যাপ্লিকেশনটি বরাদ্দ করবে সর্বোচ্চ স্তরের অ্যাক্সেসের সাথে সঙ্গতিপূর্ণ ভূমিকা৷ যদি কোনও গোষ্ঠীর সাথে মেলে না, তবে অ্যাক্সেস অস্বীকার করা হবে।",
"LabelOpenRSSFeed": "আরএসএস ফিড খুলুন",
"LabelOverwrite": "পুনঃলিখিত",
"LabelPaginationPageXOfY": "{1} টির মধ্যে {0} পৃষ্ঠা",
"LabelPassword": "পাসওয়ার্ড",
"LabelPath": "পথ",
"LabelPermanent": "স্থায়ী",
"LabelPermissionsAccessAllLibraries": "সমস্ত লাইব্রেরি অ্যাক্সেস করতে পারবে",
"LabelPermissionsAccessAllTags": "সমস্ত ট্যাগ অ্যাক্সেস করতে পারবে",
"LabelPermissionsAccessExplicitContent": "স্পষ্ট বিষয়বস্তু অ্যাক্সেস করতে পারে",
"LabelPermissionsCreateEreader": "ইরিডার তৈরি করতে পারেন",
"LabelPermissionsDelete": "মুছে দিতে পারবে",
"LabelPermissionsDownload": "ডাউনলোড করতে পারবে",
"LabelPermissionsUpdate": "আপডেট করতে পারবে",
@@ -465,6 +500,8 @@
"LabelPubDate": "প্রকাশের তারিখ",
"LabelPublishYear": "প্রকাশের বছর",
"LabelPublishedDate": "প্রকাশিত {0}",
"LabelPublishedDecade": "প্রকাশনার দশক",
"LabelPublishedDecades": "প্রকাশনার দশকগুলো",
"LabelPublisher": "প্রকাশক",
"LabelPublishers": "প্রকাশকরা",
"LabelRSSFeedCustomOwnerEmail": "কাস্টম মালিকের ইমেইল",
@@ -484,24 +521,32 @@
"LabelRedo": "পুনরায় করুন",
"LabelRegion": "অঞ্চল",
"LabelReleaseDate": "উন্মোচনের তারিখ",
"LabelRemoveAllMetadataAbs": "সমস্ত metadata.abs ফাইল সরান",
"LabelRemoveAllMetadataJson": "সমস্ত metadata.json ফাইল সরান",
"LabelRemoveCover": "কভার সরান",
"LabelRemoveMetadataFile": "লাইব্রেরি আইটেম ফোল্ডারে মেটাডেটা ফাইল সরান",
"LabelRemoveMetadataFileHelp": "আপনার {0} ফোল্ডারের সমস্ত metadata.json এবং metadata.abs ফাইলগুলি সরান।",
"LabelRowsPerPage": "প্রতি পৃষ্ঠায় সারি",
"LabelSearchTerm": "অনুসন্ধান শব্দ",
"LabelSearchTitle": "অনুসন্ধান শিরোনাম",
"LabelSearchTitleOrASIN": "অনুসন্ধান শিরোনাম বা ASIN",
"LabelSeason": "সেশন",
"LabelSeasonNumber": "মরসুম #{0}",
"LabelSelectAll": "সব নির্বাচন করুন",
"LabelSelectAllEpisodes": "সমস্ত পর্ব নির্বাচন করুন",
"LabelSelectEpisodesShowing": "দেখানো {0}টি পর্ব নির্বাচন করুন",
"LabelSelectUsers": "ব্যবহারকারী নির্বাচন করুন",
"LabelSendEbookToDevice": "ই-বই পাঠান...",
"LabelSequence": "ক্রম",
"LabelSerial": "ধারাবাহিক",
"LabelSeries": "সিরিজ",
"LabelSeriesName": "সিরিজের নাম",
"LabelSeriesProgress": "সিরিজের অগ্রগতি",
"LabelServerLogLevel": "সার্ভার লগ লেভেল",
"LabelServerYearReview": "সার্ভারের বাৎসরিক পর্যালোচনা ({0})",
"LabelSetEbookAsPrimary": "প্রাথমিক হিসাবে সেট করুন",
"LabelSetEbookAsSupplementary": "পরিপূরক হিসেবে সেট করুন",
"LabelSettingsAllowIframe": "আইফ্রেমে এম্বেড করার অনুমতি দিন",
"LabelSettingsAudiobooksOnly": "শুধুমাত্র অডিও বই",
"LabelSettingsAudiobooksOnlyHelp": "এই সেটিংটি সক্ষম করা ই-বই ফাইলগুলিকে উপেক্ষা করবে যদি না সেগুলি একটি অডিওবই ফোল্ডারের মধ্যে থাকে যে ক্ষেত্রে সেগুলিকে সম্পূরক ই-বই হিসাবে সেট করা হবে",
"LabelSettingsBookshelfViewHelp": "কাঠের তাক সহ স্কুমরফিক ডিজাইন",
@@ -523,6 +568,9 @@
"LabelSettingsHideSingleBookSeriesHelp": "যে সিরিজগুলোতে একটি বই আছে সেগুলো সিরিজের পাতা এবং নীড় পেজের তাক থেকে লুকিয়ে রাখা হবে।",
"LabelSettingsHomePageBookshelfView": "নীড় পেজে বুকশেলফ ভিউ ব্যবহার করুন",
"LabelSettingsLibraryBookshelfView": "লাইব্রেরি বুকশেলফ ভিউ ব্যবহার করুন",
"LabelSettingsLibraryMarkAsFinishedPercentComplete": "শতকরা সম্পূর্ণ এর চেয়ে বেশি",
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "বাকি সময় (সেকেন্ড) এর চেয়ে কম",
"LabelSettingsLibraryMarkAsFinishedWhen": "মিডিয়া আইটেমকে সমাপ্ত হিসাবে চিহ্নিত করুন যখন",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "কন্টিনিউ সিরিজে আগের বইগুলো এড়িয়ে যান",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "কন্টিনিউ সিরিজের নীড় পেজ শেল্ফ দেখায় যে সিরিজে শুরু হয়নি এমন প্রথম বই যার অন্তত একটি বই শেষ হয়েছে এবং কোনো বই চলছে না। এই সেটিংটি সক্ষম করলে শুরু না হওয়া প্রথম বইটির পরিবর্তে সবচেয়ে দূরের সম্পূর্ণ বই থেকে সিরিজ চলতে থাকবে।",
"LabelSettingsParseSubtitles": "সাবটাইটেল পার্স করুন",
@@ -541,6 +589,7 @@
"LabelSettingsStoreMetadataWithItemHelp": "ডিফল্টরূপে মেটাডেটা ফাইলগুলি /মেটাডাটা/আইটেমগুলি -এ সংরক্ষণ করা হয়, এই সেটিংটি সক্ষম করলে মেটাডেটা ফাইলগুলি আপনার লাইব্রেরি আইটেম ফোল্ডারে সংরক্ষণ করা হবে",
"LabelSettingsTimeFormat": "সময় বিন্যাস",
"LabelShare": "শেয়ার করুন",
"LabelShareDownloadableHelp": "শেয়ার লিঙ্ক সহ ব্যবহারকারীদের লাইব্রেরি আইটেমের একটি জিপ ফাইল ডাউনলোড করার অনুমতি দিন।",
"LabelShareOpen": "শেয়ার খোলা",
"LabelShareURL": "শেয়ার ইউআরএল",
"LabelShowAll": "সব দেখান",
@@ -549,6 +598,8 @@
"LabelSize": "আকার",
"LabelSleepTimer": "স্লিপ টাইমার",
"LabelSlug": "স্লাগ",
"LabelSortAscending": "আরোহী",
"LabelSortDescending": "অবরোহী",
"LabelStart": "শুরু",
"LabelStartTime": "শুরুর সময়",
"LabelStarted": "শুরু হয়েছে",
@@ -587,6 +638,7 @@
"LabelTimeDurationXMinutes": "{0} মিনিট",
"LabelTimeDurationXSeconds": "{0} সেকেন্ড",
"LabelTimeInMinutes": "মিনিটে সময়",
"LabelTimeLeft": "{0} বাকি",
"LabelTimeListened": "সময় শোনা হয়েছে",
"LabelTimeListenedToday": "আজ শোনার সময়",
"LabelTimeRemaining": "{0}টি অবশিষ্ট",
@@ -594,6 +646,7 @@
"LabelTitle": "শিরোনাম",
"LabelToolsEmbedMetadata": "মেটাডেটা এম্বেড করুন",
"LabelToolsEmbedMetadataDescription": "কভার ইমেজ এবং অধ্যায় সহ অডিও ফাইলগুলিতে মেটাডেটা এম্বেড করুন।",
"LabelToolsM4bEncoder": "M4B এনকোডার",
"LabelToolsMakeM4b": "M4B অডিওবুক ফাইল তৈরি করুন",
"LabelToolsMakeM4bDescription": "এমবেডেড মেটাডেটা, কভার ইমেজ এবং অধ্যায় সহ একটি .M4B অডিওবুক ফাইল তৈরি করুন।",
"LabelToolsSplitM4b": "M4B কে MP3 তে বিভক্ত করুন",
@@ -606,6 +659,7 @@
"LabelTracksMultiTrack": "মাল্টি-ট্র্যাক",
"LabelTracksNone": "কোন ট্র্যাক নেই",
"LabelTracksSingleTrack": "একক-ট্র্যাক",
"LabelTrailer": "আনুগমিক",
"LabelType": "টাইপ",
"LabelUnabridged": "অসংলগ্ন",
"LabelUndo": "পূর্বাবস্থা",
@@ -617,10 +671,13 @@
"LabelUpdateDetailsHelp": "একটি মিল থাকা অবস্থায় নির্বাচিত বইগুলির বিদ্যমান বিবরণ ওভাররাইট করার অনুমতি দিন",
"LabelUpdatedAt": "আপডেট করা হয়েছে",
"LabelUploaderDragAndDrop": "ফাইল বা ফোল্ডার টেনে আনুন এবং ফেলে দিন",
"LabelUploaderDragAndDropFilesOnly": "ফাইল টেনে আনুন",
"LabelUploaderDropFiles": "ফাইলগুলো ফেলে দিন",
"LabelUploaderItemFetchMetadataHelp": "স্বয়ংক্রিয়ভাবে শিরোনাম, লেখক এবং সিরিজ আনুন",
"LabelUseAdvancedOptions": "উন্নত বিকল্প ব্যবহার করুন",
"LabelUseChapterTrack": "অধ্যায় ট্র্যাক ব্যবহার করুন",
"LabelUseFullTrack": "সম্পূর্ণ ট্র্যাক ব্যবহার করুন",
"LabelUseZeroForUnlimited": "অসীমের জন্য 0 ব্যবহার করুন",
"LabelUser": "ব্যবহারকারী",
"LabelUsername": "ব্যবহারকারীর নাম",
"LabelValue": "মান",
@@ -630,6 +687,8 @@
"LabelViewPlayerSettings": "প্লেয়ার সেটিংস দেখুন",
"LabelViewQueue": "প্লেয়ার সারি দেখুন",
"LabelVolume": "ভলিউম",
"LabelWebRedirectURLsDescription": "লগইন করার পরে ওয়েব অ্যাপে পুনঃনির্দেশের অনুমতি দেওয়ার জন্য আপনার OAuth প্রদানকারীতে এই URLগুলোকে অনুমোদন করুন:",
"LabelWebRedirectURLsSubfolder": "রিডাইরেক্ট URL এর জন্য সাবফোল্ডার",
"LabelWeekdaysToRun": "চলতে হবে সপ্তাহের দিন",
"LabelXBooks": "{0}টি বই",
"LabelXItems": "{0}টি আইটেম",
@@ -667,6 +726,7 @@
"MessageConfirmDeleteMetadataProvider": "আপনি কি নিশ্চিতভাবে কাস্টম মেটাডেটা প্রদানকারী \"{0}\" মুছতে চান?",
"MessageConfirmDeleteNotification": "আপনি কি নিশ্চিতভাবে এই বিজ্ঞপ্তিটি মুছতে চান?",
"MessageConfirmDeleteSession": "আপনি কি নিশ্চিত আপনি এই অধিবেশন মুছে দিতে চান?",
"MessageConfirmEmbedMetadataInAudioFiles": "আপনি কি {0}টি অডিও ফাইলে মেটাডেটা এম্বেড করার বিষয়ে নিশ্চিত?",
"MessageConfirmForceReScan": "আপনি কি নিশ্চিত যে আপনি জোর করে পুনরায় স্ক্যান করতে চান?",
"MessageConfirmMarkAllEpisodesFinished": "আপনি কি নিশ্চিত যে আপনি সমস্ত পর্ব সমাপ্ত হিসাবে চিহ্নিত করতে চান?",
"MessageConfirmMarkAllEpisodesNotFinished": "আপনি কি নিশ্চিত যে আপনি সমস্ত পর্বকে শেষ হয়নি বলে চিহ্নিত করতে চান?",
@@ -678,6 +738,7 @@
"MessageConfirmPurgeCache": "ক্যাশে পরিষ্কারক <code>/metadata/cache</code>-এ সম্পূর্ণ ডিরেক্টরি মুছে ফেলবে। <br /><br />আপনি কি নিশ্চিত আপনি ক্যাশে ডিরেক্টরি সরাতে চান?",
"MessageConfirmPurgeItemsCache": "আইটেম ক্যাশে পরিষ্কারক <code>/metadata/cache/items</code>-এ সম্পূর্ণ ডিরেক্টরি মুছে ফেলবে।<br />আপনি কি নিশ্চিত?",
"MessageConfirmQuickEmbed": "সতর্কতা! দ্রুত এম্বেড আপনার অডিও ফাইলের ব্যাকআপ করবে না। নিশ্চিত করুন যে আপনার অডিও ফাইলগুলির একটি ব্যাকআপ আছে। <br><br>আপনি কি চালিয়ে যেতে চান?",
"MessageConfirmQuickMatchEpisodes": "একটি মিল পাওয়া গেলে দ্রুত ম্যাচিং পর্বগুলি বিস্তারিত ওভাররাইট করবে। শুধুমাত্র অতুলনীয় পর্ব আপডেট করা হবে। আপনি কি নিশ্চিত?",
"MessageConfirmReScanLibraryItems": "আপনি কি নিশ্চিত যে আপনি {0}টি আইটেম পুনরায় স্ক্যান করতে চান?",
"MessageConfirmRemoveAllChapters": "আপনি কি নিশ্চিত যে আপনি সমস্ত অধ্যায় সরাতে চান?",
"MessageConfirmRemoveAuthor": "আপনি কি নিশ্চিত যে আপনি লেখক \"{0}\" অপসারণ করতে চান?",
@@ -685,6 +746,7 @@
"MessageConfirmRemoveEpisode": "আপনি কি নিশ্চিত আপনি \"{0}\" পর্বটি সরাতে চান?",
"MessageConfirmRemoveEpisodes": "আপনি কি নিশ্চিত যে আপনি {0}টি পর্ব সরাতে চান?",
"MessageConfirmRemoveListeningSessions": "আপনি কি নিশ্চিত যে আপনি {0}টি শোনার সেশন সরাতে চান?",
"MessageConfirmRemoveMetadataFiles": "আপনি কি আপনার লাইব্রেরি আইটেম ফোল্ডারে থাকা সমস্ত মেটাডেটা {0} ফাইল মুছে ফেলার বিষয়ে নিশ্চিত?",
"MessageConfirmRemoveNarrator": "আপনি কি \"{0}\" বর্ণনাকারীকে সরানোর বিষয়ে নিশ্চিত?",
"MessageConfirmRemovePlaylist": "আপনি কি নিশ্চিত যে আপনি আপনার প্লেলিস্ট \"{0}\" সরাতে চান?",
"MessageConfirmRenameGenre": "আপনি কি নিশ্চিত যে আপনি সমস্ত আইটেমের জন্য \"{0}\" ধারার নাম পরিবর্তন করে \"{1}\" করতে চান?",
@@ -700,6 +762,7 @@
"MessageDragFilesIntoTrackOrder": "সঠিক ট্র্যাক অর্ডারে ফাইল টেনে আনুন",
"MessageEmbedFailed": "এম্বেড ব্যর্থ হয়েছে!",
"MessageEmbedFinished": "এম্বেড করা শেষ!",
"MessageEmbedQueue": "মেটাডেটা এম্বেডের জন্য সারিবদ্ধ ({0} সারিতে)",
"MessageEpisodesQueuedForDownload": "{0} পর্ব(গুলি) ডাউনলোডের জন্য সারিবদ্ধ",
"MessageEreaderDevices": "ই-বুক সরবরাহ নিশ্চিত করতে, আপনাকে নীচে তালিকাভুক্ত প্রতিটি ডিভাইসের জন্য একটি বৈধ প্রেরক হিসাবে উপরের ইমেল ঠিকানাটি যুক্ত করতে হতে পারে।",
"MessageFeedURLWillBe": "ফিড URL হবে {0}",
@@ -710,7 +773,6 @@
"MessageItemsSelected": "{0}টি আইটেম নির্বাচিত",
"MessageItemsUpdated": "{0}টি আইটেম আপডেট করা হয়েছে",
"MessageJoinUsOn": "আমাদের সাথে যোগ দিন",
"MessageListeningSessionsInTheLastYear": "গত বছরে {0}টি শোনার সেশন",
"MessageLoading": "লোড হচ্ছে.।",
"MessageLoadingFolders": "ফোল্ডার লোড হচ্ছে...",
"MessageLogsDescription": "লগগুলি JSON ফাইল হিসাবে <code>/metadata/logs</code>-এ সংরক্ষণ করা হয়। ক্র্যাশ লগগুলি <code>/metadata/logs/crash_logs.txt</code>-এ সংরক্ষণ করা হয়।",
@@ -744,6 +806,7 @@
"MessageNoLogs": "কোনও লগ নেই",
"MessageNoMediaProgress": "মিডিয়া অগ্রগতি নেই",
"MessageNoNotifications": "কোনো বিজ্ঞপ্তি নেই",
"MessageNoPodcastFeed": "অবৈধ পডকাস্ট: কোনো ফিড নেই",
"MessageNoPodcastsFound": "কোন পডকাস্ট পাওয়া যায়নি",
"MessageNoResults": "কোন ফলাফল নেই",
"MessageNoSearchResultsFor": "\"{0}\" এর জন্য কোন অনুসন্ধান ফলাফল নেই",
@@ -760,6 +823,10 @@
"MessagePlaylistCreateFromCollection": "সংগ্রহ থেকে প্লেলিস্ট তৈরি করুন",
"MessagePleaseWait": "অনুগ্রহ করে অপেক্ষা করুন..।",
"MessagePodcastHasNoRSSFeedForMatching": "পডকাস্টের সাথে মিলের জন্য ব্যবহার করার জন্য কোন RSS ফিড ইউআরএল নেই",
"MessagePodcastSearchField": "অনুসন্ধান শব্দ বা RSS ফিড URL লিখুন",
"MessageQuickEmbedInProgress": "দ্রুত এম্বেড করা হচ্ছে",
"MessageQuickEmbedQueue": "দ্রুত এম্বেড করার জন্য সারিবদ্ধ ({0} সারিতে)",
"MessageQuickMatchAllEpisodes": "দ্রুত ম্যাচ সব পর্ব",
"MessageQuickMatchDescription": "খালি আইটেমের বিশদ বিবরণ এবং '{0}' থেকে প্রথম ম্যাচের ফলাফলের সাথে কভার করুন। সার্ভার সেটিং সক্ষম না থাকলে বিশদ ওভাররাইট করে না।",
"MessageRemoveChapter": "অধ্যায় সরান",
"MessageRemoveEpisodes": "{0}টি পর্ব(গুলি) সরান",
@@ -802,6 +869,9 @@
"MessageTaskOpmlImportFeedPodcastExists": "পডকাস্ট আগে থেকেই পাথে বিদ্যমান",
"MessageTaskOpmlImportFeedPodcastFailed": "পডকাস্ট তৈরি করতে ব্যর্থ",
"MessageTaskOpmlImportFinished": "{0}টি পডকাস্ট যোগ করা হয়েছে",
"MessageTaskOpmlParseFailed": "OPML ফাইল পার্স করতে ব্যর্থ হয়েছে",
"MessageTaskOpmlParseFastFail": "অবৈধ OPML ফাইল <opml> ট্যাগ পাওয়া যায়নি বা একটি <outline> ট্যাগ পাওয়া যায়নি",
"MessageTaskOpmlParseNoneFound": "OPML ফাইলে কোনো ফিড পাওয়া যায়নি",
"MessageTaskScanItemsAdded": "{0}টি করা হয়েছে",
"MessageTaskScanItemsMissing": "{0}টি অনুপস্থিত",
"MessageTaskScanItemsUpdated": "{0} টি আপডেট করা হয়েছে",
@@ -826,6 +896,10 @@
"NoteUploaderFoldersWithMediaFiles": "মিডিয়া ফাইল সহ ফোল্ডারগুলি আলাদা লাইব্রেরি আইটেম হিসাবে পরিচালনা করা হবে।",
"NoteUploaderOnlyAudioFiles": "যদি শুধুমাত্র অডিও ফাইল আপলোড করা হয় তবে প্রতিটি অডিও ফাইল একটি পৃথক অডিওবুক হিসাবে পরিচালনা করা হবে।",
"NoteUploaderUnsupportedFiles": "অসমর্থিত ফাইলগুলি উপেক্ষা করা হয়। একটি ফোল্ডার বেছে নেওয়া বা ফেলে দেওয়ার সময়, আইটেম ফোল্ডারে নেই এমন অন্যান্য ফাইলগুলি উপেক্ষা করা হয়।",
"NotificationOnBackupCompletedDescription": "ব্যাকআপ সম্পূর্ণ হলে ট্রিগার হবে",
"NotificationOnBackupFailedDescription": "ব্যাকআপ ব্যর্থ হলে ট্রিগার হবে",
"NotificationOnEpisodeDownloadedDescription": "একটি পডকাস্ট পর্ব স্বয়ংক্রিয়ভাবে ডাউনলোড হলে ট্রিগার হবে",
"NotificationOnTestDescription": "বিজ্ঞপ্তি সিস্টেম পরীক্ষার জন্য ইভেন্ট",
"PlaceholderNewCollection": "নতুন সংগ্রহের নাম",
"PlaceholderNewFolderPath": "নতুন ফোল্ডার পথ",
"PlaceholderNewPlaylist": "নতুন প্লেলিস্টের নাম",
@@ -851,6 +925,7 @@
"StatsYearInReview": "বাৎসরিক পর্যালোচনা",
"ToastAccountUpdateSuccess": "অ্যাকাউন্ট আপডেট করা হয়েছে",
"ToastAppriseUrlRequired": "একটি Apprise ইউআরএল লিখতে হবে",
"ToastAsinRequired": "ASIN প্রয়োজন",
"ToastAuthorImageRemoveSuccess": "লেখকের ছবি সরানো হয়েছে",
"ToastAuthorNotFound": "লেখক \"{0}\" খুঁজে পাওয়া যায়নি",
"ToastAuthorRemoveSuccess": "লেখক সরানো হয়েছে",
@@ -870,20 +945,20 @@
"ToastBackupUploadSuccess": "ব্যাকআপ আপলোড হয়েছে",
"ToastBatchDeleteFailed": "ব্যাচ মুছে ফেলতে ব্যর্থ হয়েছে",
"ToastBatchDeleteSuccess": "ব্যাচ মুছে ফেলা সফল হয়েছে",
"ToastBatchQuickMatchFailed": "ব্যাচ কুইক ম্যাচ ব্যর্থ!",
"ToastBatchQuickMatchStarted": "{0}টি বইয়ের ব্যাচ কুইক ম্যাচ শুরু হয়েছে!",
"ToastBatchUpdateFailed": "ব্যাচ আপডেট ব্যর্থ হয়েছে",
"ToastBatchUpdateSuccess": "ব্যাচ আপডেট সাফল্য",
"ToastBookmarkCreateFailed": "বুকমার্ক তৈরি করতে ব্যর্থ",
"ToastBookmarkCreateSuccess": "বুকমার্ক যোগ করা হয়েছে",
"ToastBookmarkRemoveSuccess": "বুকমার্ক সরানো হয়েছে",
"ToastBookmarkUpdateSuccess": "বুকমার্ক আপডেট করা হয়েছে",
"ToastCachePurgeFailed": "ক্যাশে পরিষ্কার করতে ব্যর্থ হয়েছে",
"ToastCachePurgeSuccess": "ক্যাশে সফলভাবে পরিষ্কার করা হয়েছে",
"ToastChaptersHaveErrors": "অধ্যায়ে ত্রুটি আছে",
"ToastChaptersMustHaveTitles": "অধ্যায়ের শিরোনাম থাকতে হবে",
"ToastChaptersRemoved": "অধ্যায়গুলো মুছে ফেলা হয়েছে",
"ToastChaptersUpdated": "অধ্যায় আপডেট করা হয়েছে",
"ToastCollectionItemsAddFailed": "আইটেম(গুলি) সংগ্রহে যোগ করা ব্যর্থ হয়েছে",
"ToastCollectionItemsAddSuccess": "আইটেম(গুলি) সংগ্রহে যোগ করা সফল হয়েছে",
"ToastCollectionItemsRemoveSuccess": "আইটেম(গুলি) সংগ্রহ থেকে সরানো হয়েছে",
"ToastCollectionRemoveSuccess": "সংগ্রহ সরানো হয়েছে",
"ToastCollectionUpdateSuccess": "সংগ্রহ আপডেট করা হয়েছে",
"ToastCoverUpdateFailed": "কভার আপডেট ব্যর্থ হয়েছে",
@@ -898,11 +973,14 @@
"ToastEncodeCancelSucces": "এনকোড বাতিল করা হয়েছে",
"ToastEpisodeDownloadQueueClearFailed": "সারি সাফ করতে ব্যর্থ হয়েছে",
"ToastEpisodeDownloadQueueClearSuccess": "পর্ব ডাউনলোড সারি পরিষ্কার করা হয়েছে",
"ToastEpisodeUpdateSuccess": "{0}টি পর্ব আপডেট করা হয়েছে",
"ToastErrorCannotShare": "এই ডিভাইসে স্থানীয়ভাবে শেয়ার করা যাবে না",
"ToastFailedToLoadData": "ডেটা লোড করা যায়নি",
"ToastFailedToMatch": "মেলাতে ব্যর্থ হয়েছে",
"ToastFailedToShare": "শেয়ার করতে ব্যর্থ",
"ToastFailedToUpdate": "আপডেট করতে ব্যর্থ হয়েছে",
"ToastInvalidImageUrl": "অকার্যকর ছবির ইউআরএল",
"ToastInvalidMaxEpisodesToDownload": "ডাউনলোড করার জন্য অবৈধ সর্বোচ্চ পর্ব",
"ToastInvalidUrl": "অকার্যকর ইউআরএল",
"ToastItemCoverUpdateSuccess": "আইটেম কভার আপডেট করা হয়েছে",
"ToastItemDeletedFailed": "আইটেম মুছে ফেলতে ব্যর্থ",
@@ -920,14 +998,22 @@
"ToastLibraryScanFailedToStart": "স্ক্যান শুরু করতে ব্যর্থ",
"ToastLibraryScanStarted": "লাইব্রেরি স্ক্যান শুরু হয়েছে",
"ToastLibraryUpdateSuccess": "লাইব্রেরি \"{0}\" আপডেট করা হয়েছে",
"ToastMatchAllAuthorsFailed": "সমস্ত লেখকের সাথে মিলতে ব্যর্থ হয়েছে",
"ToastMetadataFilesRemovedError": "মেটাডেটা সরানোর সময় ত্রুটি {0} ফাইল",
"ToastMetadataFilesRemovedNoneFound": "কোনো মেটাডেটা নেই।লাইব্রেরিতে {0} ফাইল পাওয়া গেছে",
"ToastMetadataFilesRemovedNoneRemoved": "কোনো মেটাডেটা নেই।{0} ফাইল সরানো হয়েছে",
"ToastMetadataFilesRemovedSuccess": "{0} মেটাডেটা৷{1} ফাইল সরানো হয়েছে",
"ToastMustHaveAtLeastOnePath": "অন্তত একটি পথ থাকতে হবে",
"ToastNameEmailRequired": "নাম এবং ইমেইল আবশ্যক",
"ToastNameRequired": "নাম আবশ্যক",
"ToastNewEpisodesFound": "{0}টি নতুন পর্ব পাওয়া গেছে",
"ToastNewUserCreatedFailed": "অ্যাকাউন্ট তৈরি করতে ব্যর্থ: \"{0}\"",
"ToastNewUserCreatedSuccess": "নতুন একাউন্ট তৈরি হয়েছে",
"ToastNewUserLibraryError": "অন্তত একটি লাইব্রেরি নির্বাচন করতে হবে",
"ToastNewUserPasswordError": "অন্তত একটি পাসওয়ার্ড থাকতে হবে, শুধুমাত্র রুট ব্যবহারকারীর একটি খালি পাসওয়ার্ড থাকতে পারে",
"ToastNewUserTagError": "অন্তত একটি ট্যাগ নির্বাচন করতে হবে",
"ToastNewUserUsernameError": "একটি ব্যবহারকারীর নাম লিখুন",
"ToastNoNewEpisodesFound": "কোন নতুন পর্ব পাওয়া যায়নি",
"ToastNoUpdatesNecessary": "কোন আপডেটের প্রয়োজন নেই",
"ToastNotificationCreateFailed": "বিজ্ঞপ্তি তৈরি করতে ব্যর্থ",
"ToastNotificationDeleteFailed": "বিজ্ঞপ্তি মুছে ফেলতে ব্যর্থ",
@@ -946,6 +1032,7 @@
"ToastPodcastGetFeedFailed": "পডকাস্ট ফিড পেতে ব্যর্থ হয়েছে",
"ToastPodcastNoEpisodesInFeed": "আরএসএস ফিডে কোনো পর্ব পাওয়া যায়নি",
"ToastPodcastNoRssFeed": "পডকাস্টের কোন আরএসএস ফিড নেই",
"ToastProgressIsNotBeingSynced": "অগ্রগতি সিঙ্ক হচ্ছে না, প্লেব্যাক পুনরায় চালু করুন",
"ToastProviderCreatedFailed": "প্রদানকারী যোগ করতে ব্যর্থ হয়েছে",
"ToastProviderCreatedSuccess": "নতুন প্রদানকারী যোগ করা হয়েছে",
"ToastProviderNameAndUrlRequired": "নাম এবং ইউআরএল আবশ্যক",
@@ -972,6 +1059,7 @@
"ToastSessionCloseFailed": "অধিবেশন বন্ধ করতে ব্যর্থ হয়েছে",
"ToastSessionDeleteFailed": "সেশন মুছে ফেলতে ব্যর্থ",
"ToastSessionDeleteSuccess": "সেশন মুছে ফেলা হয়েছে",
"ToastSleepTimerDone": "স্লিপ টাইমার হয়ে গেছে... zZzzZz",
"ToastSlugMustChange": "স্লাগে অবৈধ অক্ষর রয়েছে",
"ToastSlugRequired": "স্লাগ আবশ্যক",
"ToastSocketConnected": "সকেট সংযুক্ত",

1026
client/strings/ca.json Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -88,6 +88,8 @@
"ButtonSaveTracklist": "Uložit seznam skladeb",
"ButtonScan": "Prohledat",
"ButtonScanLibrary": "Prohledat Knihovnu",
"ButtonScrollLeft": "Posunout vlevo",
"ButtonScrollRight": "Posunout vpravo",
"ButtonSearch": "Hledat",
"ButtonSelectFolderPath": "Vybrat cestu ke složce",
"ButtonSeries": "Série",
@@ -190,6 +192,7 @@
"HeaderSettingsExperimental": "Experimentální funkce",
"HeaderSettingsGeneral": "Obecné",
"HeaderSettingsScanner": "Skener",
"HeaderSettingsWebClient": "Webový klient",
"HeaderSleepTimer": "Časovač vypnutí",
"HeaderStatsLargestItems": "Největší položky",
"HeaderStatsLongestItems": "Nejdelší položky (hod.)",
@@ -231,7 +234,7 @@
"LabelAppend": "Připojit",
"LabelAudioBitrate": "Bitový tok zvuku (např. 128k)",
"LabelAudioChannels": "Zvukové kanály (1 nebo 2)",
"LabelAudioCodec": "Kodek audia",
"LabelAudioCodec": "Audio Kodek",
"LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (jméno a příjmení)",
"LabelAuthorLastFirst": "Autor (příjmení a jméno)",
@@ -264,6 +267,7 @@
"LabelChapters": "Kapitoly",
"LabelChaptersFound": "Kapitoly nalezeny",
"LabelClickForMoreInfo": "Klikněte pro více informací",
"LabelClickToUseCurrentValue": "Klikni pro použití aktuální hodnoty",
"LabelClosePlayer": "Zavřít přehrávač",
"LabelCodec": "Kodek",
"LabelCollapseSeries": "Sbalit sérii",
@@ -296,6 +300,7 @@
"LabelDiscover": "Objevit",
"LabelDownload": "Stáhnout",
"LabelDownloadNEpisodes": "Stáhnout {0} epizody",
"LabelDownloadable": "Ke stažení",
"LabelDuration": "Délka trvání",
"LabelDurationComparisonExactMatch": "(přesná shoda)",
"LabelDurationComparisonLonger": "({0} delší)",
@@ -313,12 +318,25 @@
"LabelEmailSettingsTestAddress": "Testovací adresa",
"LabelEmbeddedCover": "Vložená obálka",
"LabelEnable": "Povolit",
"LabelEncodingBackupLocation": "Záloha původních audio souborů bude uložena v:",
"LabelEncodingChaptersNotEmbedded": "Kapitoly nejsou vloženy ve vícestopých audioknihách.",
"LabelEncodingClearItemCache": "Nezapomeňte pravidelně promazávat mezipaměť položek.",
"LabelEncodingFinishedM4B": "Výsledné M4B bude uloženo do složky s audioknihou v:",
"LabelEncodingInfoEmbedded": "Metadata budou vložena do audio stop ve složce s audioknihou.",
"LabelEncodingStartedNavigation": "Po spuštění úlohy můžete opustit tuto stránku.",
"LabelEncodingTimeWarning": "Encoding může zabrat až 30 minut.",
"LabelEncodingWarningAdvancedSettings": "Varování: Neměňte toto nastavení pokud neznáte možnosti encodingu ffmpeg.",
"LabelEncodingWatcherDisabled": "Pokud máte zakázaný watcher, budete po skončení muset znovu naskenovat tuto audioknihu.",
"LabelEnd": "Konec",
"LabelEndOfChapter": "Konec kapitoly",
"LabelEpisode": "Epizoda",
"LabelEpisodeNotLinkedToRssFeed": "Epizoda není propojená s RSS feed",
"LabelEpisodeNumber": "Epizoda #{0}",
"LabelEpisodeTitle": "Název epizody",
"LabelEpisodeType": "Typ epizody",
"LabelEpisodeUrlFromRssFeed": "URL epizody z RSS feed",
"LabelEpisodes": "Epizody",
"LabelEpisodic": "Epizodické",
"LabelExample": "Příklad",
"LabelExpandSeries": "Rozbalit série",
"LabelExpandSubSeries": "Rozbalit podsérie",
@@ -346,6 +364,7 @@
"LabelFontScale": "Měřítko písma",
"LabelFontStrikethrough": "Přeškrtnutí",
"LabelFormat": "Formát",
"LabelFull": "Plné",
"LabelGenre": "Žánr",
"LabelGenres": "Žánry",
"LabelHardDeleteFile": "Trvale smazat soubor",
@@ -388,6 +407,7 @@
"LabelLess": "Méně",
"LabelLibrariesAccessibleToUser": "Knihovny přístupné uživateli",
"LabelLibrary": "Knihovna",
"LabelLibraryFilterSublistEmpty": "Žádné {0}",
"LabelLibraryItem": "Položka knihovny",
"LabelLibraryName": "Název knihovny",
"LabelLimit": "Omezit",
@@ -400,6 +420,10 @@
"LabelLowestPriority": "Nejnižší priorita",
"LabelMatchExistingUsersBy": "Přiřadit stávající uživatele podle",
"LabelMatchExistingUsersByDescription": "Slouží k propojení stávajících uživatelů. Po propojení budou uživatelé přiřazeni k jedinečnému ID od poskytovatele SSO.",
"LabelMaxEpisodesToDownload": "Maximální # epizod pro stažení. Použijte 0 pro bez omezení.",
"LabelMaxEpisodesToDownloadPerCheck": "Maximální počet nových epizod ke stažení při jedné kontrole",
"LabelMaxEpisodesToKeep": "Maximální počet epizod k zachování",
"LabelMaxEpisodesToKeepHelp": "Hodnotou 0 není nastaven žádný maximální limit. Po automatickém stažení nové epizody se odstraní nejstarší epizoda, pokud máte více než X epizod. Při každém novém stažení se odstraní pouze 1 epizoda.",
"LabelMediaPlayer": "Přehrávač médií",
"LabelMediaType": "Typ média",
"LabelMetaTag": "Metaznačka",
@@ -445,12 +469,14 @@
"LabelOpenIDGroupClaimDescription": "Název požadavku OpenID, který obsahuje seznam uživatelských skupin. Běžně se označuje jako <code>groups</code>. <b>Je-li nakonfigurováno</b>, plikace automaticky přiřadí role na základě členství uživatele ve skupinách, pokud jsou tyto skupiny v požadavku pojmenovány case-insensitive 'admin', 'user' nebo 'guest'. Požadavek by měl obsahovat seznam, a pokud uživatel patří do více skupin, aplikace přiřadí roli odpovídající nejvyšší úrovni práva přístupu. Pokud žádná skupina není shodná, bude přístup odepřen.",
"LabelOpenRSSFeed": "Otevřít RSS kanál",
"LabelOverwrite": "Přepsat",
"LabelPaginationPageXOfY": "Strana {0} z {1}",
"LabelPassword": "Heslo",
"LabelPath": "Cesta",
"LabelPermanent": "Trvalé",
"LabelPermissionsAccessAllLibraries": "Má přístup ke všem knihovnám",
"LabelPermissionsAccessAllTags": "Má přístup ke všem značkám",
"LabelPermissionsAccessExplicitContent": "Má přístup k explicitnímu obsahu",
"LabelPermissionsCreateEreader": "Může vytvořit Ereader",
"LabelPermissionsDelete": "Může mazat",
"LabelPermissionsDownload": "Může stahovat",
"LabelPermissionsUpdate": "Může aktualizovat",
@@ -474,6 +500,8 @@
"LabelPubDate": "Datum vydání",
"LabelPublishYear": "Rok vydání",
"LabelPublishedDate": "Vydáno {0}",
"LabelPublishedDecade": "Publikováno (dekáda)",
"LabelPublishedDecades": "Publikováno (dekády)",
"LabelPublisher": "Vydavatel",
"LabelPublishers": "Vydavatelé",
"LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka",
@@ -493,24 +521,32 @@
"LabelRedo": "Přepracovat",
"LabelRegion": "Region",
"LabelReleaseDate": "Datum vydání",
"LabelRemoveAllMetadataAbs": "Odebrat všechny soubory metadata.abs",
"LabelRemoveAllMetadataJson": "Smazat všechny soubory metadata.json",
"LabelRemoveCover": "Odstranit obálku",
"LabelRemoveMetadataFile": "Odstranit soubory metadat ve složkách položek knihovny",
"LabelRemoveMetadataFileHelp": "Odstraníte všechny soubory metadata.json a metadata.abs ve svých složkách {0}.",
"LabelRowsPerPage": "Řádky na stránku",
"LabelSearchTerm": "Vyhledat termín",
"LabelSearchTitle": "Vyhledat název",
"LabelSearchTitleOrASIN": "Vyhledat název nebo ASIN",
"LabelSeason": "Sezóna",
"LabelSeasonNumber": "Sezóna č.{0}",
"LabelSelectAll": "Vybrat vše",
"LabelSelectAllEpisodes": "Vybrat všechny epizody",
"LabelSelectEpisodesShowing": "Vyberte {0} epizody, které se zobrazují",
"LabelSelectUsers": "Vybrat uživatele",
"LabelSendEbookToDevice": "Odeslat e-knihu do...",
"LabelSequence": "Sekvence",
"LabelSerial": "Sériové",
"LabelSeries": "Série",
"LabelSeriesName": "Název série",
"LabelSeriesProgress": "Průběh série",
"LabelServerLogLevel": "Úroveň protokolu serveru",
"LabelServerYearReview": "Přehled roku na serveru ({0})",
"LabelSetEbookAsPrimary": "Nastavit jako primární",
"LabelSetEbookAsSupplementary": "Nastavit jako doplňkové",
"LabelSettingsAllowIframe": "Povolit vložení do rámce iframe",
"LabelSettingsAudiobooksOnly": "Pouze audioknihy",
"LabelSettingsAudiobooksOnlyHelp": "Povolením tohoto nastavení budou soubory e-knih ignorovány, pokud nejsou ve složce audioknih, v takovém případě budou nastaveny jako doplňkové e-knihy",
"LabelSettingsBookshelfViewHelp": "Skeumorfní design s dřevěnými policemi",
@@ -532,9 +568,12 @@
"LabelSettingsHideSingleBookSeriesHelp": "Série, které mají jedinou knihu, budou skryty na stránce série a na domovské stránce.",
"LabelSettingsHomePageBookshelfView": "Domovská stránka používá zobrazení police s knihami",
"LabelSettingsLibraryBookshelfView": "Knihovna používá zobrazení police s knihami",
"LabelSettingsLibraryMarkAsFinishedPercentComplete": "Procento dokončení je vyšší než",
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Zbývající čas je kratší než (sekund)",
"LabelSettingsLibraryMarkAsFinishedWhen": "Označit položku médií jako dokončenou, když",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Přeskočit předchozí knihy v pokračování série",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Polička Pokračovat v sérii na domovské stránce zobrazuje první nezačatou knihu v sériích, které mají alespoň jednu knihu dokončenou a žádnou rozečtenou. Povolením tohoto nastavení budou série pokračovat od poslední dokončené knihy namísto první nezačaté knihy.",
"LabelSettingsParseSubtitles": "Analzyovat podtitul",
"LabelSettingsParseSubtitles": "Analyzovat podtitul",
"LabelSettingsParseSubtitlesHelp": "Rozparsovat podtitul z názvů složek audioknih.<br>Podtiul musí být oddělen znakem \" - \"<br>tj. \"Název knihy - Zde Podtitul\" má podtitul \"Zde podtitul\"",
"LabelSettingsPreferMatchedMetadata": "Preferovat spárovaná metadata",
"LabelSettingsPreferMatchedMetadataHelp": "Spárovaná data budou mít při použití funkce Rychlé párování přednost před údaji o položce. Ve výchozím nastavení funkce Rychlé párování pouze doplní chybějící údaje.",
@@ -550,12 +589,17 @@
"LabelSettingsStoreMetadataWithItemHelp": "Ve výchozím nastavení jsou soubory metadat uloženy v adresáři /metadata/items, povolením tohoto nastavení budou soubory metadat uloženy ve složkách položek knihovny",
"LabelSettingsTimeFormat": "Formát času",
"LabelShare": "Sdílet",
"LabelShareDownloadableHelp": "Umožňuje uživatelům s odkazem na sdílení stáhnout soubor zip.",
"LabelShareOpen": "Otevřít sdílení",
"LabelShareURL": "Sdílet URL",
"LabelShowAll": "Zobrazit vše",
"LabelShowSeconds": "Zobrazit sekundy",
"LabelShowSubtitles": "Zobrazit titulky",
"LabelSize": "Velikost",
"LabelSleepTimer": "Časovač vypnutí",
"LabelSlug": "URL název",
"LabelSortAscending": "Vzestupně",
"LabelSortDescending": "Sestupně",
"LabelStart": "Spustit",
"LabelStartTime": "Čas Spuštění",
"LabelStarted": "Spuštěno",
@@ -594,6 +638,7 @@
"LabelTimeDurationXMinutes": "{0} minut",
"LabelTimeDurationXSeconds": "{0} sekund",
"LabelTimeInMinutes": "Čas v minutách",
"LabelTimeLeft": "{0} zbývá",
"LabelTimeListened": "Čas poslechu",
"LabelTimeListenedToday": "Čas poslechu dnes",
"LabelTimeRemaining": "{0} zbývá",
@@ -601,6 +646,7 @@
"LabelTitle": "Název",
"LabelToolsEmbedMetadata": "Vložit metadata",
"LabelToolsEmbedMetadataDescription": "Vložit metadata do zvukových souborů včetně obálky a kapitol.",
"LabelToolsM4bEncoder": "Enkodér M4B",
"LabelToolsMakeM4b": "Vytvořit soubor audioknihy M4B",
"LabelToolsMakeM4bDescription": "Vygenerovat soubor audioknihy M4B s vloženými metadaty, obálkou a kapitolami.",
"LabelToolsSplitM4b": "Rozdělit M4B na MP3",
@@ -613,6 +659,7 @@
"LabelTracksMultiTrack": "Více stop",
"LabelTracksNone": "Žádné stopy",
"LabelTracksSingleTrack": "Jedna stopa",
"LabelTrailer": "Upoutávka",
"LabelType": "Typ",
"LabelUnabridged": "Nezkráceno",
"LabelUndo": "Zpět",
@@ -624,10 +671,13 @@
"LabelUpdateDetailsHelp": "Povolit přepsání existujících údajů o vybraných knihách, když je nalezena shoda",
"LabelUpdatedAt": "Aktualizováno v",
"LabelUploaderDragAndDrop": "Přetáhnout soubory nebo složky",
"LabelUploaderDragAndDropFilesOnly": "Přetáhnout a upustit soubory",
"LabelUploaderDropFiles": "Odstranit soubory",
"LabelUploaderItemFetchMetadataHelp": "Automaticky načíst název, autora a sérii",
"LabelUseAdvancedOptions": "Použít pokročilé možnosti",
"LabelUseChapterTrack": "Použít stopu kapitoly",
"LabelUseFullTrack": "Použít celou stopu",
"LabelUseZeroForUnlimited": "Použijte 0 pro neomezené",
"LabelUser": "Uživatel",
"LabelUsername": "Uživatelské jméno",
"LabelValue": "Hodnota",
@@ -637,6 +687,8 @@
"LabelViewPlayerSettings": "Zobrazit nastavení přehrávače",
"LabelViewQueue": "Zobrazit frontu přehrávače",
"LabelVolume": "Hlasitost",
"LabelWebRedirectURLsDescription": "Autorizujte tyto adresy URL ve zprostředkovateli OAuth, abyste po přihlášení umožnili přesměrování zpět do webové aplikace:",
"LabelWebRedirectURLsSubfolder": "Podsložka pro přesměrování adres URL",
"LabelWeekdaysToRun": "Dny v týdnu ke spuštění",
"LabelXBooks": "{0} knih",
"LabelXItems": "{0} položky",
@@ -674,6 +726,7 @@
"MessageConfirmDeleteMetadataProvider": "Opravdu chcete vymazat vlastního poskytovatele metadat \"{0}\"?",
"MessageConfirmDeleteNotification": "Opravdu chcete vymazat tuto notifikaci?",
"MessageConfirmDeleteSession": "Opravdu chcete smazat tuto relaci?",
"MessageConfirmEmbedMetadataInAudioFiles": "Jste si jisti, že chcete vložit metadata do {0} zvukových souborů?",
"MessageConfirmForceReScan": "Opravdu chcete vynutit opětovné prohledání?",
"MessageConfirmMarkAllEpisodesFinished": "Opravdu chcete označit všechny epizody jako dokončené?",
"MessageConfirmMarkAllEpisodesNotFinished": "Opravdu chcete označit všechny epizody jako nedokončené?",
@@ -681,9 +734,11 @@
"MessageConfirmMarkItemNotFinished": "Opravdu chcete označit \"{0}\" jako nedokončené?",
"MessageConfirmMarkSeriesFinished": "Opravdu chcete označit všechny knihy z této série jako dokončené?",
"MessageConfirmMarkSeriesNotFinished": "Opravdu chcete označit všechny knihy z této série jako nedokončené?",
"MessageConfirmNotificationTestTrigger": "Spustit toto oznámení s testovacími daty?",
"MessageConfirmPurgeCache": "Vyčistit mezipaměť odstraní celý adresář na adrese <code>/metadata/cache</code>. <br /><br />Určitě chcete odstranit adresář mezipaměti?",
"MessageConfirmPurgeItemsCache": "Vyčištění mezipaměti položek odstraní celý adresář <code>/metadata/cache/items</code>.<br />Jste si jistí?",
"MessageConfirmQuickEmbed": "Varování! Rychlé vložení nezálohuje vaše zvukové soubory. Ujistěte se, že máte zálohu zvukových souborů. <br><br>Chcete pokračovat?",
"MessageConfirmQuickMatchEpisodes": "Pokud je nalezena shoda při rychlém párování epizod, dojde k přepsání podrobností. Aktualizovány budou pouze nespárované epizody. Jste si jisti?",
"MessageConfirmReScanLibraryItems": "Opravdu chcete znovu prohledat {0} položky?",
"MessageConfirmRemoveAllChapters": "Opravdu chcete odstranit všechny kapitoly?",
"MessageConfirmRemoveAuthor": "Opravdu chcete odstranit autora \"{0}\"?",
@@ -691,6 +746,7 @@
"MessageConfirmRemoveEpisode": "Opravdu chcete odstranit epizodu \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Opravdu chcete odstranit {0} epizody?",
"MessageConfirmRemoveListeningSessions": "Opravdu chcete odebrat {0} poslechových relací?",
"MessageConfirmRemoveMetadataFiles": "Jste si jisti, že chcete odstranit všechny metadata.{0} soubory ve složkách s položkami ve vaší knihovně?",
"MessageConfirmRemoveNarrator": "Opravdu chcete odebrat předčítání \"{0}\"?",
"MessageConfirmRemovePlaylist": "Opravdu chcete odstranit svůj playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Opravdu chcete přejmenovat žánr \"{0}\" na \"{1}\" pro všechny položky?",
@@ -702,10 +758,12 @@
"MessageConfirmResetProgress": "Opravdu chcete zahodit svůj pokrok?",
"MessageConfirmSendEbookToDevice": "Opravdu chcete odeslat e-knihu {0} {1}\" do zařízení \"{2}\"?",
"MessageConfirmUnlinkOpenId": "Opravdu chcete odpojit tohoto uživatele z OpenID?",
"MessageDaysListenedInTheLastYear": "{0} poslechových dní v minulém roce",
"MessageDownloadingEpisode": "Stahuji epizodu",
"MessageDragFilesIntoTrackOrder": "Přetáhněte soubory do správného pořadí stop",
"MessageEmbedFailed": "Vložení selhalo!",
"MessageEmbedFinished": "Vložení dokončeno!",
"MessageEmbedQueue": "Zařazeno do fronty pro vložení metadat ({0} ve frontě)",
"MessageEpisodesQueuedForDownload": "{0} Epizody zařazené do fronty ke stažení",
"MessageEreaderDevices": "Aby bylo zajištěno doručení elektronických knih, může být nutné přidat výše uvedenou e-mailovou adresu jako platného odesílatele pro každé zařízení uvedené níže.",
"MessageFeedURLWillBe": "URL zdroje bude {0}",
@@ -716,7 +774,6 @@
"MessageItemsSelected": "{0} vybraných položek",
"MessageItemsUpdated": "{0} položky byly aktualizovány",
"MessageJoinUsOn": "Přidejte se k nám",
"MessageListeningSessionsInTheLastYear": "{0} poslechových relací za poslední rok",
"MessageLoading": "Načítá se...",
"MessageLoadingFolders": "Načítám složky...",
"MessageLogsDescription": "Protokoly se ukládají do souborů JSON v <code>/metadata/logs</code>. Protokoly o pádech jsou uloženy v <code>/metadata/logs/crash_logs.txt</code>.",
@@ -750,6 +807,7 @@
"MessageNoLogs": "Žádné protokoly",
"MessageNoMediaProgress": "Žádný průběh médií",
"MessageNoNotifications": "Žádná oznámení",
"MessageNoPodcastFeed": "Neplatný podcast: Žádný kanál",
"MessageNoPodcastsFound": "Nebyly nalezeny žádné podcasty",
"MessageNoResults": "Žádné výsledky",
"MessageNoSearchResultsFor": "Nebyly nalezeny žádné výsledky hledání pro \"{0}\"",
@@ -766,7 +824,11 @@
"MessagePlaylistCreateFromCollection": "Vytvořit seznam skladeb z kolekce",
"MessagePleaseWait": "Čekejte prosím...",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nemá žádnou adresu URL kanálu RSS, kterou by mohl použít pro porovnávání",
"MessageQuickMatchDescription": "Vyplňte prázdné detaily položky a obálku prvním výsledkem shody z '{0}'. Nepřepisuje podrobnosti, pokud není povoleno nastavení serveru \"Preferovat párování metadata\".",
"MessagePodcastSearchField": "Zadejte hledaný pojem pro RSS feed URL",
"MessageQuickEmbedInProgress": "Probíhá rychlé vkládání",
"MessageQuickEmbedQueue": "Zařazeno do fronty pro rychlé vložení ({0} ve frontě)",
"MessageQuickMatchAllEpisodes": "Rychlá shoda všech epizod",
"MessageQuickMatchDescription": "Vyplnit prázdné detaily položky a obálky prvním výsledkem shody z '{0}'. Nepřepisuje detaily, pokud není povoleno nastavení serveru 'Preferovat shodná metadata'.",
"MessageRemoveChapter": "Odstranit kapitolu",
"MessageRemoveEpisodes": "Odstranit {0} epizodu",
"MessageRemoveFromPlayerQueue": "Odstranit z fronty přehrávače",
@@ -775,6 +837,7 @@
"MessageResetChaptersConfirm": "Opravdu chcete resetovat kapitoly a vrátit zpět provedené změny?",
"MessageRestoreBackupConfirm": "Opravdu chcete obnovit zálohu vytvořenou dne",
"MessageRestoreBackupWarning": "Obnovení zálohy přepíše celou databázi umístěnou v /config a obálku obrázků v /metadata/items & /metadata/authors.<br /><br />Backups nezmění žádné soubory ve složkách knihovny. Pokud jste povolili nastavení serveru pro ukládání obrázků obalu a metadat do složek knihovny, nebudou zálohovány ani přepsány.<br /><br />Všichni klienti používající váš server budou automaticky obnoveni.",
"MessageScheduleLibraryScanNote": "Většině uživatelů se doporučuje ponechat tuto funkci vypnutou a ponechat zapnuté nastavení sledování složek. Sledování složek automaticky zjistí změny ve složkách vaší knihovny. Sledování složek nefunguje pro každý souborový systém (jako je NFS), takže místo toho lze použít plánované skenování knihoven.",
"MessageSearchResultsFor": "Výsledky hledání pro",
"MessageSelected": "{0} vybráno",
"MessageServerCouldNotBeReached": "Server je nedostupný",
@@ -784,7 +847,7 @@
"MessageShareURLWillBe": "Sdílené URL bude <strong>{0}</strong>",
"MessageStartPlaybackAtTime": "Spustit přehrávání pro \"{0}\" v {1}?",
"MessageTaskAudioFileNotWritable": "Nelze zapisovat do audio souboru \"{0}\"",
"MessageTaskCanceledByUser": "Task zrušen uživatelem",
"MessageTaskCanceledByUser": "Příkaz zrušen uživatelem",
"MessageTaskDownloadingEpisodeDescription": "Stahování epizody \"{0}\"",
"MessageTaskEmbeddingMetadata": "Vkládání metadat",
"MessageTaskEmbeddingMetadataDescription": "Vkládání metadat do audioknihy \"{0}\"",
@@ -797,14 +860,20 @@
"MessageTaskFailedToMergeAudioFiles": "Spojení audio souborů selhalo",
"MessageTaskFailedToMoveM4bFile": "Přesunutí m4b souboru selhalo",
"MessageTaskFailedToWriteMetadataFile": "Zápis souboru metadat selhal",
"MessageTaskNoFilesToScan": "Žádné soubory ke skenování",
"MessageTaskMatchingBooksInLibrary": "Párování knih v knihovně „{0}“",
"MessageTaskNoFilesToScan": "Žádné soubory k prohledání",
"MessageTaskOpmlImport": "Import OPML",
"MessageTaskOpmlImportDescription": "Vytváření podcastů z {0} RSS feedů",
"MessageTaskOpmlImportFeed": "Importní zdroj OPML",
"MessageTaskOpmlImportFeedDescription": "Importování RSS feedu \"{0}\"",
"MessageTaskOpmlImportFeedFailed": "Nepodařilo se získat kanál podcastu",
"MessageTaskOpmlImportFeedPodcastDescription": "Vytváření podcastu \"{0}\"",
"MessageTaskOpmlImportFeedPodcastExists": "Podcast se stejnou cestou již existuje",
"MessageTaskOpmlImportFeedPodcastFailed": "Vytváření podcastu selhalo",
"MessageTaskOpmlImportFinished": "Přidáno {0} podcastů",
"MessageTaskOpmlParseFailed": "Selhalo parsování OPML souboru",
"MessageTaskOpmlParseFastFail": "Neplatný OPML soubor <opml> tag nenalezen NEBO <outline> tag nenalezen",
"MessageTaskOpmlParseNoneFound": "Feed nebyl nalezen v OPML souboru",
"MessageTaskScanItemsAdded": "{0} přidáno",
"MessageTaskScanItemsMissing": "{0} chybí",
"MessageTaskScanItemsUpdated": "{0} aktualizováno",
@@ -812,7 +881,7 @@
"MessageTaskScanningFileChanges": "Skenování změn souborů v \"{0}\"",
"MessageTaskScanningLibrary": "Skenování \"{0}\" knihovny",
"MessageTaskTargetDirectoryNotWritable": "Do cílové složky nelze zapisovat",
"MessageThinking": "Přemýšlení...",
"MessageThinking": "Přemýšlím...",
"MessageUploaderItemFailed": "Nahrávání selhalo",
"MessageUploaderItemSuccess": "Úspěšně nahráno!",
"MessageUploading": "Nahrávám...",
@@ -828,7 +897,11 @@
"NoteRSSFeedPodcastAppsPubDate": "Upozornění: 1 nebo více epizod nemá datum vydání. Některé podcastové aplikace to vyžadují.",
"NoteUploaderFoldersWithMediaFiles": "Se složkami s multimediálními soubory bude zacházeno jako se samostatnými položkami knihovny.",
"NoteUploaderOnlyAudioFiles": "Pokud nahráváte pouze zvukové soubory, bude s každým zvukovým souborem zacházeno jako se samostatnou audioknihou.",
"NoteUploaderUnsupportedFiles": "Nepodporované soubory jsou ignorovány. Při výběru nebo přetažení složky jsou ostatní soubory, které nejsou ve složce položek, ignorovány.",
"NoteUploaderUnsupportedFiles": "Nepodporované soubory jsou ignorovány. Při výběru nebo přetažení složky jsou ostatní soubory, které nejsou ve složce, ignorovány.",
"NotificationOnBackupCompletedDescription": "Spuštěno po dokončení zálohování",
"NotificationOnBackupFailedDescription": "Spuštěno pokud zálohování selže",
"NotificationOnEpisodeDownloadedDescription": "Spuštěno při automatickém stažení epizody podcastu",
"NotificationOnTestDescription": "Akce pro otestování upozorňovacího systému",
"PlaceholderNewCollection": "Nový název kolekce",
"PlaceholderNewFolderPath": "Nová cesta ke složce",
"PlaceholderNewPlaylist": "Nový název seznamu přehrávání",
@@ -839,18 +912,22 @@
"StatsBooksAdditional": "Některé další zahrnují…",
"StatsBooksFinished": "dokončené knihy",
"StatsBooksFinishedThisYear": "Některé knihy dokončené tento rok…",
"StatsBooksListenedTo": "knih poslechnuto",
"StatsCollectionGrewTo": "Vaše kolekce knih se rozrostla na…",
"StatsSessions": "sezení",
"StatsSessions": "sezóna",
"StatsSpentListening": "stráveno posloucháním",
"StatsTopAuthor": "TOP AUTOR",
"StatsTopAuthors": "TOP AUTOŘI",
"StatsTopGenre": "TOP ŽÁNR",
"StatsTopGenres": "TOP ŽÁNRY",
"StatsTopMonth": "TOP MĚSÍC",
"StatsTopNarrator": "NEJLEPŠÍ VYPRAVĚČ",
"StatsTopNarrators": "NEJLEPŠÍ VYPRAVĚČI",
"StatsTotalDuration": "S celkovou dobou…",
"StatsYearInReview": "ROK V PŘEHLEDU",
"ToastAccountUpdateSuccess": "Účet aktualizován",
"ToastAppriseUrlRequired": "Je nutné zadat Apprise URL",
"ToastAsinRequired": "ASIN vyžadován",
"ToastAuthorImageRemoveSuccess": "Obrázek autora odstraněn",
"ToastAuthorNotFound": "Author \"{0}\" nenalezen",
"ToastAuthorRemoveSuccess": "Autor odstraněn",
@@ -870,21 +947,24 @@
"ToastBackupUploadSuccess": "Záloha nahrána",
"ToastBatchDeleteFailed": "Hromadné smazání selhalo",
"ToastBatchDeleteSuccess": "Hromadné smazání proběhlo úspěšně",
"ToastBatchQuickMatchFailed": "Rychlá schoda dávky se nezdařila!",
"ToastBatchQuickMatchStarted": "Začala rychlá shoda {0} knih!",
"ToastBatchUpdateFailed": "Dávková aktualizace se nezdařila",
"ToastBatchUpdateSuccess": "Dávková aktualizace proběhla úspěšně",
"ToastBookmarkCreateFailed": "Vytvoření záložky se nezdařilo",
"ToastBookmarkCreateSuccess": "Přidána záložka",
"ToastBookmarkRemoveSuccess": "Záložka odstraněna",
"ToastBookmarkUpdateSuccess": "Záložka aktualizována",
"ToastCachePurgeFailed": "Nepodařilo se vyčistit mezipaměť",
"ToastCachePurgeSuccess": "Vyrovnávací paměť úspěšně vyčištěna",
"ToastChaptersHaveErrors": "Kapitoly obsahují chyby",
"ToastChaptersMustHaveTitles": "Kapitoly musí mít názvy",
"ToastChaptersRemoved": "Kapitoly odstraněny",
"ToastCollectionItemsRemoveSuccess": "Položky odstraněny z kolekce",
"ToastChaptersUpdated": "Kapitola aktualizována",
"ToastCollectionItemsAddFailed": "Přidávání položek do kolekce selhalo",
"ToastCollectionRemoveSuccess": "Kolekce odstraněna",
"ToastCollectionUpdateSuccess": "Kolekce aktualizována",
"ToastCoverUpdateFailed": "Aktualizace obálky selhala",
"ToastDateTimeInvalidOrIncomplete": "Datum a čas jsou chybné nebo nekompletní",
"ToastDeleteFileFailed": "Nepodařilo se smazat soubor",
"ToastDeleteFileSuccess": "Soubor smazán",
"ToastDeviceAddFailed": "Přidání zařízení selhalo",
@@ -892,12 +972,18 @@
"ToastDeviceTestEmailFailed": "Odeslání testovacího emailu selhalo",
"ToastDeviceTestEmailSuccess": "Testovací email byl odeslán",
"ToastEmailSettingsUpdateSuccess": "Nastavení emailu aktualizována",
"ToastEncodeCancelFailed": "Chyba zrušení kódování",
"ToastEncodeCancelSucces": "Kódování zrušeno",
"ToastEpisodeDownloadQueueClearFailed": "Vyčištění fronty selhalo",
"ToastEpisodeDownloadQueueClearSuccess": "Fronta stahování epizod je prázdná",
"ToastEpisodeUpdateSuccess": "{0} epizod aktualizováno",
"ToastErrorCannotShare": "Na tomto zařízení nelze nativně sdílet",
"ToastFailedToLoadData": "Nepodařilo se načíst data",
"ToastFailedToMatch": "Nepodařilo se spárovat",
"ToastFailedToShare": "Sdílení selhalo",
"ToastFailedToUpdate": "Aktualizace selhala",
"ToastInvalidImageUrl": "Neplatná URL obrázku",
"ToastInvalidMaxEpisodesToDownload": "Neplatný maximální počet epizod ke stažení",
"ToastInvalidUrl": "Neplatná URL",
"ToastItemCoverUpdateSuccess": "Obálka předmětu byl aktualizována",
"ToastItemDeletedFailed": "Smazání položky selhalo",
@@ -915,28 +1001,84 @@
"ToastLibraryScanFailedToStart": "Nepodařilo se spustit kontrolu",
"ToastLibraryScanStarted": "Kontrola knihovny spuštěna",
"ToastLibraryUpdateSuccess": "Knihovna \"{0}\" aktualizována",
"ToastMatchAllAuthorsFailed": "Nepodařilo se přiřadit všechny autory",
"ToastMetadataFilesRemovedError": "Při odstraňování souborů metadat.{0} došlo k chybě",
"ToastMetadataFilesRemovedNoneFound": "Žádná metadata.{0} nebyla nalezena v knihovně",
"ToastMetadataFilesRemovedNoneRemoved": "Žádná metadata.{0} počet odstraněných souborů",
"ToastMetadataFilesRemovedSuccess": "{0} metadata.{1} soubor odstraněn",
"ToastMustHaveAtLeastOnePath": "Musí mít minimálně jednu cestu",
"ToastNameEmailRequired": "Jméno a email jsou vyžadovány",
"ToastNameRequired": "Jméno je vyžadováno",
"ToastNewEpisodesFound": "{0} nových epizod bylo nalezeno",
"ToastNewUserCreatedFailed": "Chyba při vytváření účtu: \"{0}\"",
"ToastNewUserCreatedSuccess": "Vytvořen nový účet",
"ToastNewUserLibraryError": "Musíte vybrat alespoň jednu knihovnu",
"ToastNewUserPasswordError": "Musí mít heslo, pouze uživatel root může mít prázdné heslo",
"ToastNewUserTagError": "Musíte vybrat alespoň jeden tag",
"ToastNewUserUsernameError": "Zadej uživatelské jméno",
"ToastNoNewEpisodesFound": "Nebyla nalezena žádná nová epizoda",
"ToastNoRSSFeed": "Podcast nemá RSS Feed",
"ToastNoUpdatesNecessary": "Nejsou potřeba žádné aktualizace",
"ToastNotificationCreateFailed": "Chyba při vytváření upozornění",
"ToastNotificationDeleteFailed": "Chyba při odstranění upozornění",
"ToastNotificationFailedMaximum": "Maximální počet chybných pokusů >= 0",
"ToastNotificationQueueMaximum": "Maximální počet upozornění ve frontě musí být >= 0",
"ToastNotificationSettingsUpdateSuccess": "Nastavení upozornění aktualizováno",
"ToastNotificationTestTriggerFailed": "Chyba při spuštění testovacího upozornění",
"ToastNotificationTestTriggerSuccess": "Spuštěno testovací upozornění",
"ToastNotificationUpdateSuccess": "Upozornění aktualizováno",
"ToastPlaylistCreateFailed": "Vytvoření seznamu přehrávání se nezdařilo",
"ToastPlaylistCreateSuccess": "Seznam přehrávání vytvořen",
"ToastPlaylistRemoveSuccess": "Seznam přehrávání odstraněn",
"ToastPlaylistUpdateSuccess": "Seznam přehrávání aktualizován",
"ToastPodcastCreateFailed": "Vytvoření podcastu se nezdařilo",
"ToastPodcastCreateSuccess": "Podcast byl úspěšně vytvořen",
"ToastPodcastGetFeedFailed": "Chyba při získání podcastového feedu",
"ToastPodcastNoEpisodesInFeed": "Žádné epizody nenalezeny v RSS feedu",
"ToastPodcastNoRssFeed": "Podcast nemá RSS feed",
"ToastProgressIsNotBeingSynced": "Progres není synchronizován, restartujte přehrávání",
"ToastProviderCreatedFailed": "Chyba při zadání poskytovatele",
"ToastProviderCreatedSuccess": "Nový poskytovatel přidán",
"ToastProviderNameAndUrlRequired": "Jméno a Url jsou vyžadovány",
"ToastProviderRemoveSuccess": "Poskytovatel odstraněn",
"ToastRSSFeedCloseFailed": "Nepodařilo se zavřít RSS kanál",
"ToastRSSFeedCloseSuccess": "RSS kanál uzavřen",
"ToastRemoveFailed": "Chyba při odstranění",
"ToastRemoveItemFromCollectionFailed": "Nepodařilo se odebrat položku z kolekce",
"ToastRemoveItemFromCollectionSuccess": "Položka odstraněna z kolekce",
"ToastRemoveItemsWithIssuesFailed": "Chyba při odstranění položek v knihovně s chybami",
"ToastRemoveItemsWithIssuesSuccess": "Odstraněny položky knihovny s chybami",
"ToastRenameFailed": "Chyba při přejmenování",
"ToastRescanFailed": "Znovu prohledání selhalo z důvodu {0}",
"ToastRescanRemoved": "Znova skenování komplení - položka byla odsraněna",
"ToastRescanUpToDate": "Znovu prohledání kompletní - položka aktualizována",
"ToastRescanUpdated": "Znovu skenování komplení - položka byla aktualizována",
"ToastScanFailed": "Prohledání položek knihovny selhalo",
"ToastSelectAtLeastOneUser": "Vyberte alespoň jednoho uživatele",
"ToastSendEbookToDeviceFailed": "Odeslání e-knihy do zařízení se nezdařilo",
"ToastSendEbookToDeviceSuccess": "E-kniha odeslána do zařízení \"{0}\"",
"ToastSeriesUpdateFailed": "Aktualizace série se nezdařila",
"ToastSeriesUpdateSuccess": "Aktualizace série byla úspěšná",
"ToastServerSettingsUpdateSuccess": "Nastavení serveru aktualizováno",
"ToastSessionCloseFailed": "Chyba při ukončení",
"ToastSessionDeleteFailed": "Nepodařilo se smazat relaci",
"ToastSessionDeleteSuccess": "Relace smazána",
"ToastSleepTimerDone": "Uspání knížky ... zZzzZz",
"ToastSlugMustChange": "Slug (URL) obsahuje chybné znaky",
"ToastSlugRequired": "Slug (URL) je vyžadována",
"ToastSocketConnected": "Socket připojen",
"ToastSocketDisconnected": "Socket odpojen",
"ToastSocketFailedToConnect": "Socket se nepodařilo připojit",
"ToastSortingPrefixesEmptyError": "Musí mít alespoň 1 třídicí předponu",
"ToastSortingPrefixesUpdateSuccess": "Aktualizovány předpony třídění ({0} položek)",
"ToastTitleRequired": "Titul je vyžadován",
"ToastUnknownError": "Neznámý error",
"ToastUnlinkOpenIdFailed": "Chyba při odpárování uživatele z OpenID",
"ToastUnlinkOpenIdSuccess": "Uživatel odpárován z uživatele z OpenID",
"ToastUserDeleteFailed": "Nepodařilo se smazat uživatele",
"ToastUserDeleteSuccess": "Uživatel smazán"
"ToastUserDeleteSuccess": "Uživatel smazán",
"ToastUserPasswordChangeSuccess": "Heslo bylo změněno úspěšně",
"ToastUserPasswordMismatch": "Hesla se neschodují",
"ToastUserPasswordMustChange": "Nové heslo se musí lišit od předchozího",
"ToastUserRootRequireName": "Musíte zadat uživatelské jméno root"
}

View File

@@ -19,6 +19,7 @@
"ButtonChooseFiles": "Vælg filer",
"ButtonClearFilter": "Ryd filter",
"ButtonCloseFeed": "Luk feed",
"ButtonCloseSession": "Luk Åben Session",
"ButtonCollections": "Samlinger",
"ButtonConfigureScanner": "Konfigurer scanner",
"ButtonCreate": "Opret",
@@ -29,11 +30,15 @@
"ButtonEditChapters": "Rediger kapitler",
"ButtonEditPodcast": "Rediger podcast",
"ButtonEnable": "Aktiver",
"ButtonForceReScan": "Tvungen genindlæsning",
"ButtonFireAndFail": "Affyring Og Fejl",
"ButtonFireOnTest": "Affyring vedTest begivenhed",
"ButtonForceReScan": "Tving genindlæsning",
"ButtonFullPath": "Fuld sti",
"ButtonHide": "Skjul",
"ButtonHome": "Hjem",
"ButtonIssues": "Problemer",
"ButtonJumpBackward": "Hop Tilbage",
"ButtonJumpForward": "Hop Fremad",
"ButtonLatest": "Seneste",
"ButtonLibrary": "Bibliotek",
"ButtonLogout": "Log ud",
@@ -43,20 +48,32 @@
"ButtonMatchAllAuthors": "Match alle forfattere",
"ButtonMatchBooks": "Match bøger",
"ButtonNevermind": "Glem det",
"ButtonOk": "OK",
"ButtonNext": "Næste",
"ButtonNextChapter": "Næste Kapitel",
"ButtonNextItemInQueue": "Næste Element i Køen",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Åbn feed",
"ButtonOpenManager": "Åbn manager",
"ButtonPause": "Pause",
"ButtonPlay": "Afspil",
"ButtonPlayAll": "Afspil Alle",
"ButtonPlaying": "Afspiller",
"ButtonPlaylists": "Afspilningslister",
"ButtonPrevious": "Sidste",
"ButtonPreviousChapter": "Sidste Kapitel",
"ButtonProbeAudioFile": "Undersøg Lydfil",
"ButtonPurgeAllCache": "Ryd al cache",
"ButtonPurgeItemsCache": "Ryd elementcache",
"ButtonQueueAddItem": "Tilføj til kø",
"ButtonQueueRemoveItem": "Fjern fra kø",
"ButtonQuickEmbed": "Hurtig Indlejring",
"ButtonQuickEmbedMetadata": "Hurtig Indlejring af Metadata",
"ButtonQuickMatch": "Hurtig Match",
"ButtonReScan": "Gen-scan",
"ButtonRead": "Læs",
"ButtonReadLess": "Se mindre",
"ButtonReadMore": "Se mere",
"ButtonRefresh": "Genindlæs",
"ButtonRemove": "Fjern",
"ButtonRemoveAll": "Fjern Alle",
"ButtonRemoveAllLibraryItems": "Fjern Alle Bibliotekselementer",
@@ -64,31 +81,46 @@
"ButtonRemoveFromContinueReading": "Fjern fra Fortsæt Læsning",
"ButtonRemoveSeriesFromContinueSeries": "Fjern Serie fra Fortsæt Serie",
"ButtonReset": "Nulstil",
"ButtonResetToDefault": "Nulstil til standard",
"ButtonRestore": "Gendan",
"ButtonSave": "Gem",
"ButtonSaveAndClose": "Gem & Luk",
"ButtonSaveTracklist": "Gem Sporliste",
"ButtonScan": "Scan",
"ButtonScanLibrary": "Scan Bibliotek",
"ButtonScrollLeft": "Rul til Venstre",
"ButtonScrollRight": "Rul til Højre",
"ButtonSearch": "Søg",
"ButtonSelectFolderPath": "Vælg Mappen Sti",
"ButtonSeries": "Serier",
"ButtonSetChaptersFromTracks": "Sæt kapitler fra spor",
"ButtonShare": "Del",
"ButtonShiftTimes": "Skift Tider",
"ButtonShow": "Vis",
"ButtonStartM4BEncode": "Start M4B Kode",
"ButtonStartMetadataEmbed": "Start Metadata Indlejring",
"ButtonStats": "Statistik",
"ButtonSubmit": "Send",
"ButtonTest": "Test",
"ButtonUnlinkOpenId": "Afkobl OpenID",
"ButtonUpload": "Upload",
"ButtonUploadBackup": "Upload Backup",
"ButtonUploadCover": "Upload Omslag",
"ButtonUploadOPMLFile": "Upload OPML Fil",
"ButtonUserDelete": "Slet bruger {0}",
"ButtonUserEdit": "Rediger bruger {0}",
"ButtonViewAll": "Vis Alle",
"ButtonYes": "Ja",
"ErrorUploadFetchMetadataAPI": "Fejl henter metadata",
"ErrorUploadFetchMetadataNoResults": "Kunne ikke hente metadata - prøv at uploade title og/eller forfatter",
"ErrorUploadLacksTitle": "Skal have en title",
"HeaderAccount": "Konto",
"HeaderAddCustomMetadataProvider": "Tilføj Brugerdefineret Metadataudbyder",
"HeaderAdvanced": "Avanceret",
"HeaderAppriseNotificationSettings": "Apprise Notifikationsindstillinger",
"HeaderAudioTracks": "Lydspor",
"HeaderAudiobookTools": "Audiobog Filhåndteringsværktøjer",
"HeaderAuthentication": "Autentificering",
"HeaderBackups": "Sikkerhedskopier",
"HeaderChangePassword": "Skift Adgangskode",
"HeaderChapters": "Kapitler",
@@ -97,9 +129,12 @@
"HeaderCollectionItems": "Samlingselementer",
"HeaderCover": "Omslag",
"HeaderCurrentDownloads": "Nuværende Downloads",
"HeaderCustomMessageOnLogin": "Brugerdefineret Besked ved Login",
"HeaderCustomMetadataProviders": "Brugerdefineret Metadataudbyder",
"HeaderDetails": "Detaljer",
"HeaderDownloadQueue": "Download Kø",
"HeaderEbookFiles": "E-bogsfiler",
"HeaderEmail": "Email",
"HeaderEmailSettings": "Email Indstillinger",
"HeaderEpisodes": "Episoder",
"HeaderEreaderDevices": "E-læser Enheder",
@@ -117,33 +152,47 @@
"HeaderListeningSessions": "Lyttesessioner",
"HeaderListeningStats": "Lyttestatistik",
"HeaderLogin": "Log ind",
"HeaderLogs": "Logs",
"HeaderManageGenres": "Administrer Genrer",
"HeaderManageTags": "Administrer Tags",
"HeaderMapDetails": "Kort Detaljer",
"HeaderMatch": "Match",
"HeaderMetadataOrderOfPrecedence": "Metadata-prioritet",
"HeaderMetadataToEmbed": "Metadata til indlejring",
"HeaderNewAccount": "Ny Konto",
"HeaderNewLibrary": "Nyt Bibliotek",
"HeaderNotificationCreate": "Opret Notifikation",
"HeaderNotificationUpdate": "Updater Notifikation",
"HeaderNotifications": "Meddelelser",
"HeaderOpenIDConnectAuthentication": "OpenID Connect-autentificering",
"HeaderOpenListeningSessions": "Åbne lyttesessioner",
"HeaderOpenRSSFeed": "Åbn RSS Feed",
"HeaderOtherFiles": "Andre Filer",
"HeaderPasswordAuthentication": "Adgangskodeautentificering",
"HeaderPermissions": "Tilladelser",
"HeaderPlayerQueue": "Afspilningskø",
"HeaderPlayerSettings": "Afspiller Indstillinger",
"HeaderPlaylist": "Afspilningsliste",
"HeaderPlaylistItems": "Afspilningsliste Elementer",
"HeaderPodcastsToAdd": "Podcasts til Tilføjelse",
"HeaderPreviewCover": "Forhåndsvis Omslag",
"HeaderRSSFeedGeneral": "RSS Detaljer",
"HeaderRSSFeedIsOpen": "RSS Feed er Åben",
"HeaderRSSFeeds": "RSS-Feeds",
"HeaderRemoveEpisode": "Fjern Episode",
"HeaderRemoveEpisodes": "Fjern {0} Episoder",
"HeaderSavedMediaProgress": "Gemt Medieforløb",
"HeaderSchedule": "Planlæg",
"HeaderScheduleEpisodeDownloads": "Planlæg Automatisk Episode-Download",
"HeaderScheduleLibraryScans": "Planlæg Automatiske Biblioteksscanninger",
"HeaderSession": "Session",
"HeaderSetBackupSchedule": "Indstil Sikkerhedskopieringsplan",
"HeaderSettings": "Indstillinger",
"HeaderSettingsDisplay": "Skærm",
"HeaderSettingsExperimental": "Eksperimentelle Funktioner",
"HeaderSettingsGeneral": "Generelt",
"HeaderSettingsScanner": "Scanner",
"HeaderSettingsWebClient": "Webklient",
"HeaderSleepTimer": "Søvntimer",
"HeaderStatsLargestItems": "Største Elementer",
"HeaderStatsLongestItems": "Længste Elementer (timer)",
@@ -158,7 +207,12 @@
"HeaderUpdateDetails": "Opdater Detaljer",
"HeaderUpdateLibrary": "Opdater Bibliotek",
"HeaderUsers": "Brugere",
"HeaderYearReview": "Gennemgang af År {0}",
"HeaderYourStats": "Dine Statistikker",
"LabelAbridged": "Forkortet",
"LabelAbridgedChecked": "Forkortet (kontrolleret)",
"LabelAbridgedUnchecked": "Uforkortet (ikke kontrolleret)",
"LabelAccessibleBy": "Tilgængelig af",
"LabelAccountType": "Kontotype",
"LabelAccountTypeAdmin": "Administrator",
"LabelAccountTypeGuest": "Gæst",
@@ -168,32 +222,56 @@
"LabelAddToCollectionBatch": "Tilføj {0} Bøger til Samling",
"LabelAddToPlaylist": "Tilføj til Afspilningsliste",
"LabelAddToPlaylistBatch": "Tilføj {0} Elementer til Afspilningsliste",
"LabelAddedAt": "Tilføjet Kl.",
"LabelAddedAt": "Tilføjet",
"LabelAddedDate": "Tilføjet {0}",
"LabelAdminUsersOnly": "Kun Administratorbrugere",
"LabelAll": "Alle",
"LabelAllUsers": "Alle Brugere",
"LabelAllUsersExcludingGuests": "Alle bruger eksklusiv gæster",
"LabelAllUsersIncludingGuests": "Alle bruger inklusiv gæster",
"LabelAlreadyInYourLibrary": "Allerede i dit bibliotek",
"LabelApiToken": "API Token",
"LabelAppend": "Tilføj",
"LabelAudioBitrate": "Lydbitrate (f.eks. 128k)",
"LabelAudioChannels": "Lydkanaler (1 eller 2)",
"LabelAudioCodec": "Lydkodek",
"LabelAuthor": "Forfatter",
"LabelAuthorFirstLast": "Forfatter (Fornavn Efternavn)",
"LabelAuthorLastFirst": "Forfatter (Efternavn, Fornavn)",
"LabelAuthors": "Forfattere",
"LabelAutoDownloadEpisodes": "Auto Download Episoder",
"LabelAutoFetchMetadata": "Automatisk Hent Metadata",
"LabelAutoFetchMetadataHelp": "Henter metadata for titler, forfatter og serier for at strømligne uploading. Ekstra metadata har måske brug for at blive matchet efter upload.",
"LabelAutoLaunch": "Åben Automatisk",
"LabelAutoLaunchDescription": "Viderestil automatisk til login-udbyderen ved navigation til login-siden (manuel overstyring via <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Registrer Automatisk",
"LabelAutoRegisterDescription": "Automatisk oprettelse af nye brugere efter login",
"LabelBackToUser": "Tilbage til Bruger",
"LabelBackupAudioFiles": "Sikkerhedskopier lydfiler",
"LabelBackupLocation": "Backup Placering",
"LabelBackupsEnableAutomaticBackups": "Aktivér automatisk sikkerhedskopiering",
"LabelBackupsEnableAutomaticBackupsHelp": "Sikkerhedskopier gemt i /metadata/backups",
"LabelBackupsMaxBackupSize": "Maksimal sikkerhedskopistørrelse (i GB)",
"LabelBackupsMaxBackupSize": "Maksimal sikkerhedskopistørrelse (i GB) (0 for ubegrænset)",
"LabelBackupsMaxBackupSizeHelp": "Som en beskyttelse mod fejlkonfiguration fejler sikkerhedskopier, hvis de overstiger den konfigurerede størrelse.",
"LabelBackupsNumberToKeep": "Antal sikkerhedskopier at beholde",
"LabelBackupsNumberToKeepHelp": "Kun 1 sikkerhedskopi fjernes ad gangen, så hvis du allerede har flere sikkerhedskopier end dette, skal du fjerne dem manuelt.",
"LabelBitrate": "Bitrate",
"LabelBonus": "Bonus",
"LabelBooks": "Bøger",
"LabelButtonText": "Knap tekst",
"LabelByAuthor": "af {0}",
"LabelChangePassword": "Ændre Adgangskode",
"LabelChannels": "Kanaler",
"LabelChapterCount": "{0} Kapitler",
"LabelChapterTitle": "Kapitel Titel",
"LabelChapters": "Kapitler",
"LabelChaptersFound": "fundne kapitler",
"LabelClickForMoreInfo": "Klik for mere info",
"LabelClickToUseCurrentValue": "Klik for at bruge nuværende værdi",
"LabelClosePlayer": "Luk afspiller",
"LabelCodec": "Kodeks",
"LabelCollapseSeries": "Fold Serier Sammen",
"LabelCollapseSubSeries": "Fold underserie sammen",
"LabelCollection": "Samling",
"LabelCollections": "Samlinger",
"LabelComplete": "Fuldfør",
@@ -209,58 +287,100 @@
"LabelCurrently": "Aktuelt:",
"LabelCustomCronExpression": "Brugerdefineret Cron Udtryk:",
"LabelDatetime": "Dato og Tid",
"LabelDays": "Dage",
"LabelDeleteFromFileSystemCheckbox": "Slet fra filsystem (afmarker kun for at fjerne fra databasen)",
"LabelDescription": "Beskrivelse",
"LabelDeselectAll": "Fravælg Alle",
"LabelDevice": "Enheds",
"LabelDeviceInfo": "Enhedsinformation",
"LabelDeviceIsAvailableTo": "Enhed er tilgængelig for...",
"LabelDirectory": "Mappe",
"LabelDiscFromFilename": "Disk fra Filnavn",
"LabelDiscFromMetadata": "Disk fra Metadata",
"LabelDiscover": "Opdag",
"LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download {0} episoder",
"LabelDownloadable": "Downloadbar",
"LabelDuration": "Varighed",
"LabelDurationComparisonExactMatch": "(præcis match)",
"LabelDurationComparisonLonger": "({0} længere)",
"LabelDurationComparisonShorter": "({0} kortere)",
"LabelDurationFound": "Fundet varighed:",
"LabelEbook": "E-bog",
"LabelEbooks": "E-bøger",
"LabelEdit": "Rediger",
"LabelEmail": "E-mail",
"LabelEmailSettingsFromAddress": "Fra Adresse",
"LabelEmailSettingsRejectUnauthorized": "Afvis uautoriserede certifikater",
"LabelEmailSettingsRejectUnauthorizedHelp": "Deaktivering af SSL certifikat validering kan udsætte din forbindelse for sikkerhedsrisici, eksempelvis man-in-the-middle angreb. Deaktiver kun denne indstilling hvis du forstår de potentielle implikationer og stoler på den mailserver du forbinder til.",
"LabelEmailSettingsSecure": "Sikker",
"LabelEmailSettingsSecureHelp": "Hvis sandt, vil forbindelsen bruge TLS ved tilslutning til serveren. Hvis falsk, bruges TLS, hvis serveren understøtter STARTTLS-udvidelsen. I de fleste tilfælde skal denne værdi sættes til sandt, hvis du tilslutter til port 465. Til port 587 eller 25 skal du holde det falsk. (fra nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Test Adresse",
"LabelEmbeddedCover": "Indlejret Omslag",
"LabelEnable": "Aktivér",
"LabelEncodingBackupLocation": "En sikkerhedskopi af dine originale lydfiler vil blive gemt under:",
"LabelEncodingChaptersNotEmbedded": "Kapitler er ikke indlejret i multi spors lydbøger.",
"LabelEncodingClearItemCache": "Sørg for periodisk at rense indholdscachen.",
"LabelEncodingFinishedM4B": "Færdiggjort M4B som vil blive placeret i din lydbogsmappe ved:",
"LabelEncodingInfoEmbedded": "Metadata vil blive indlejret i lydfiler i lydbogsmappen.",
"LabelEncodingStartedNavigation": "Når opgaven er startet kan du navigere væk fra denne side.",
"LabelEncodingTimeWarning": "Indkodning kan tage op til 30 minutter.",
"LabelEncodingWarningAdvancedSettings": "Advarsel: Opdater ikke disse indstillinger med mindre du kender til ffmpeg indkodningsindstillinger.",
"LabelEncodingWatcherDisabled": "Hvis du har watcheren deaktiveret skal du gen-scanne denne lydbog bagefter.",
"LabelEnd": "Slut",
"LabelEndOfChapter": "Slutningen af kapitel",
"LabelEpisode": "Episode",
"LabelEpisode": "Afsnit",
"LabelEpisodeNotLinkedToRssFeed": "Afsnit er ikke koblet til RSS feed",
"LabelEpisodeNumber": "Afsnit #{0}",
"LabelEpisodeTitle": "Episodetitel",
"LabelEpisodeType": "Episodetype",
"LabelEpisodeUrlFromRssFeed": "Afsnit URL fra RSS feed",
"LabelEpisodes": "Afsnit",
"LabelEpisodic": "Afsnit",
"LabelExample": "Eksempel",
"LabelExpandSeries": "Udfold serie",
"LabelExpandSubSeries": "Udfold underserie",
"LabelExplicit": "Eksplisit",
"LabelExplicitChecked": "Eksplicit (markeret)",
"LabelExplicitUnchecked": "Ikke eksplicit (ikke markeret)",
"LabelExportOPML": "Eksport OPML",
"LabelFeedURL": "Feed URL",
"LabelFetchingMetadata": "Henter metadata",
"LabelFile": "Fil",
"LabelFileBirthtime": "Oprettelsestidspunkt for fil",
"LabelFileBornDate": "Født {0}",
"LabelFileModified": "Fil ændret",
"LabelFileModifiedDate": "Opdateret {0}",
"LabelFilename": "Filnavn",
"LabelFilterByUser": "Filtrér efter bruger",
"LabelFindEpisodes": "Find episoder",
"LabelFinished": "Færdig",
"LabelFolder": "Mappe",
"LabelFolders": "Mapper",
"LabelFontBold": "Fed",
"LabelFontBoldness": "Skrift tykkelse",
"LabelFontFamily": "Fontfamilie",
"LabelFontItalic": "Kursiv",
"LabelFontScale": "Skriftstørrelse",
"LabelFontStrikethrough": "Gennemstreget",
"LabelFormat": "Format",
"LabelFull": "Fuld",
"LabelGenre": "Genre",
"LabelGenres": "Genrer",
"LabelHardDeleteFile": "Permanent slet fil",
"LabelHasEbook": "Har e-bog",
"LabelHasSupplementaryEbook": "Har supplerende e-bog",
"LabelHideSubtitles": "Skjul undertitler",
"LabelHighestPriority": "Højeste prioritet",
"LabelHost": "Vært",
"LabelHour": "Time",
"LabelHours": "Timer",
"LabelIcon": "Ikon",
"LabelImageURLFromTheWeb": "Billede URL fra nettet",
"LabelInProgress": "I gang",
"LabelIncludeInTracklist": "Inkluder i afspilningsliste",
"LabelIncomplete": "Ufuldstændig",
"LabelInterval": "Interval",
"LabelIntervalCustomDailyWeekly": "Tilpasset dagligt/ugentligt",
"LabelIntervalEvery12Hours": "Hver 12. time",
"LabelIntervalEvery15Minutes": "Hver 15. minut",
@@ -271,8 +391,11 @@
"LabelIntervalEveryHour": "Hver time",
"LabelInvert": "Inverter",
"LabelItem": "Element",
"LabelJumpBackwardAmount": "Spring bagud mængde",
"LabelJumpForwardAmount": "Spring fremad mængde",
"LabelLanguage": "Sprog",
"LabelLanguageDefaultServer": "Standard server sprog",
"LabelLanguages": "Sprog",
"LabelLastBookAdded": "Senest tilføjede bog",
"LabelLastBookUpdated": "Senest opdaterede bog",
"LabelLastSeen": "Sidst set",
@@ -284,6 +407,7 @@
"LabelLess": "Mindre",
"LabelLibrariesAccessibleToUser": "Biblioteker tilgængelige for bruger",
"LabelLibrary": "Bibliotek",
"LabelLibraryFilterSublistEmpty": "Nej {0}",
"LabelLibraryItem": "Bibliotekselement",
"LabelLibraryName": "Biblioteksnavn",
"LabelLimit": "Grænse",
@@ -293,13 +417,26 @@
"LabelLogLevelInfo": "Information",
"LabelLogLevelWarn": "Advarsel",
"LabelLookForNewEpisodesAfterDate": "Søg efter nye episoder efter denne dato",
"LabelLowestPriority": "Laveste prioritet",
"LabelMatchExistingUsersBy": "Match eksisterende brugere ved",
"LabelMatchExistingUsersByDescription": "Anvendt for at forbinde brugere. Når forbundet, brugere vil blive matchet ved unikt id fra din SSO udbyder",
"LabelMaxEpisodesToDownload": "Max # afsnit for at downloade. Anvend 0 for ubegrænset.",
"LabelMaxEpisodesToDownloadPerCheck": "Max # afsnit til at downloade per check",
"LabelMaxEpisodesToKeep": "Max # afsnit at beholde",
"LabelMaxEpisodesToKeepHelp": "Værdi af 0 sætter intet maks begrænsning. After et nyt afsnit er automatisk downloaded vil det ældste afsnit blive slettet hvis du har mere end X afsnit. Dette vil kun slette 1 afsnit for hvert nye download.",
"LabelMediaPlayer": "Medieafspiller",
"LabelMediaType": "Medietype",
"LabelMetaTag": "Meta-tag",
"LabelMetaTags": "Meta-tags",
"LabelMetadataOrderOfPrecedenceDescription": "Højeste prioritet metadata kilder vil overskrive de lavest prioriterede metadata kilder",
"LabelMetadataProvider": "Metadataudbyder",
"LabelMinute": "Minut",
"LabelMinutes": "Minutter",
"LabelMissing": "Mangler",
"LabelMissingEbook": "Har ingen ebog",
"LabelMissingSupplementaryEbook": "Har ingen tillægsbog",
"LabelMobileRedirectURIs": "Godkendte mobil redirect URI'er",
"LabelMobileRedirectURIsDescription": "Dete vil whiteliste en gyldig omdirigerings URL for mobile apps. Den standarde er <code>audiobookshelf://oauth</code> som du kan fjerne eller supplere med flere URI'er for tredjeparts app integration. Anvend en stjerne (<code>*</code>) som den eneste indstilling for at tilade en hvilkensomhelst URI.",
"LabelMore": "Mere",
"LabelMoreInfo": "Mere info",
"LabelName": "Navn",
@@ -311,6 +448,7 @@
"LabelNewestEpisodes": "Nyeste episoder",
"LabelNextBackupDate": "Næste sikkerhedskopi dato",
"LabelNextScheduledRun": "Næste planlagte kørsel",
"LabelNoCustomMetadataProviders": "Ingen brugerdefinerede metadata udbydere",
"LabelNoEpisodesSelected": "Ingen episoder valgt",
"LabelNotFinished": "Ikke færdig",
"LabelNotStarted": "Ikke påbegyndt",
@@ -325,31 +463,47 @@
"LabelNotificationsMaxQueueSize": "Maksimal køstørrelse for meddelelseshændelser",
"LabelNotificationsMaxQueueSizeHelp": "Hændelser begrænses til at udløse en gang pr. sekund. Hændelser ignoreres, hvis køen er fyldt. Dette forhindrer meddelelsesspam.",
"LabelNumberOfBooks": "Antal bøger",
"LabelNumberOfEpisodes": "Antal episoder",
"LabelNumberOfEpisodes": "# afsnit",
"LabelOpenIDAdvancedPermsClaimDescription": "Navnet af OpenID claimet som indeholder avancerede brugerhandlinger inden i applikationen som vil gælde for ikke administrative roller (<b>hvis konfigureret</b>). Hvis et claim mangler fra svaret vil adgang til ABS blive nægtet. Hvis en enkelt indstilling/option mangler, vil det bliver behandlet som <code>false</code>. Sørg for at identity provider's claim matcher den forventede struktur:",
"LabelOpenIDClaims": "Efterlad de følgende indstillinger tomme for at deaktivere avancerede grupper og adgangsstyring for automatisk at tilføje dem til 'User' gruppen.",
"LabelOpenIDGroupClaimDescription": "Navnet af det OpenID claim som skal indeholde brugerens grupper. Mest kendt som <code>groups</code>. <b>hvis konfigureret</b>, vil applikationen automatiske tildele roller baseret p[ brugerens gruppemedlemsskaber, givet disse grupper er navngivet (uden forbehold for store og små bogstaver) 'admin', 'user' eller 'guest' i claimet. Claimet burde indeholde en liste (og hvis brugeren tilhøre flere grupper) som applikationen vil tildele roller med højeste adgangsnvieau. Hvis ingen grupper matcher vil adgang blive nægtet.",
"LabelOpenRSSFeed": "Åbn RSS-feed",
"LabelOverwrite": "Overskriv",
"LabelPaginationPageXOfY": "Side {0} af {1}",
"LabelPassword": "Kodeord",
"LabelPath": "Sti",
"LabelPermanent": "Permanent",
"LabelPermissionsAccessAllLibraries": "Kan få adgang til alle biblioteker",
"LabelPermissionsAccessAllTags": "Kan få adgang til alle tags",
"LabelPermissionsAccessExplicitContent": "Kan få adgang til eksplicit indhold",
"LabelPermissionsCreateEreader": "Kan oprette elæser",
"LabelPermissionsDelete": "Kan slette",
"LabelPermissionsDownload": "Kan downloade",
"LabelPermissionsUpdate": "Kan opdatere",
"LabelPermissionsUpload": "Kan uploade",
"LabelPersonalYearReview": "Dit år i review ({0})",
"LabelPhotoPathURL": "Foto sti/URL",
"LabelPlayMethod": "Afspilningsmetode",
"LabelPlayerChapterNumberMarker": "{0} af {1}",
"LabelPlaylists": "Afspilningslister",
"LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "Podcast søgeområde",
"LabelPodcastType": "Podcast type",
"LabelPodcasts": "Podcast",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Præfikser der skal ignoreres (skal ikke skelne mellem store og små bogstaver)",
"LabelPreventIndexing": "Forhindrer, at dit feed bliver indekseret af iTunes og Google podcastkataloger",
"LabelPrimaryEbook": "Primær e-bog",
"LabelProgress": "Fremskridt",
"LabelProvider": "Udbyder",
"LabelProviderAuthorizationValue": "Authorization Header værdi",
"LabelPubDate": "Udgivelsesdato",
"LabelPublishYear": "Udgivelsesår",
"LabelPublishedDate": "Publiceret {0}",
"LabelPublishedDecade": "Publiceret årti",
"LabelPublishedDecades": "Publiceret årtier",
"LabelPublisher": "Forlag",
"LabelPublishers": "Forlag",
"LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail",
"LabelRSSFeedCustomOwnerName": "Brugerdefineret ejerens navn",
"LabelRSSFeedOpen": "Åben RSS-feed",
@@ -357,27 +511,42 @@
"LabelRSSFeedSlug": "RSS-feed-slug",
"LabelRSSFeedURL": "RSS-feed-URL",
"LabelRandomly": "Tilfældigt",
"LabelReAddSeriesToContinueListening": "Gentilføj serier til Fortsæt Lytning",
"LabelRead": "Læst",
"LabelReadAgain": "Læs igen",
"LabelReadAgain": "Læs Igen",
"LabelReadEbookWithoutProgress": "Læs e-bog uden at følge fremskridt",
"LabelRecentSeries": "Seneste serier",
"LabelRecentlyAdded": "Senest tilføjet",
"LabelRecommended": "Anbefalet",
"LabelRedo": "Gøre igen",
"LabelRegion": "Region",
"LabelReleaseDate": "Udgivelsesdato",
"LabelRemoveAllMetadataAbs": "Fjern alle metadata.abs filer",
"LabelRemoveAllMetadataJson": "Fjern alle metadata.json filer",
"LabelRemoveCover": "Fjern omslag",
"LabelRemoveMetadataFile": "Fjern alle metadata filer i biblioteksmapper",
"LabelRemoveMetadataFileHelp": "Fjern alle metadata.json og metadata.abs filer i dine {0} mapper.",
"LabelRowsPerPage": "Rækker per side",
"LabelSearchTerm": "Søgeterm",
"LabelSearchTitle": "Søg efter titel",
"LabelSearchTitleOrASIN": "Søg efter titel eller ASIN",
"LabelSeason": "Sæson",
"LabelSeasonNumber": "Sæson {0}",
"LabelSelectAll": "Vælg alle",
"LabelSelectAllEpisodes": "Vælg alle episoder",
"LabelSelectEpisodesShowing": "Vælg {0} episoder vist",
"LabelSelectUsers": "Valgte brugere",
"LabelSendEbookToDevice": "Send e-bog til...",
"LabelSequence": "Sekvens",
"LabelSerial": "Seriel",
"LabelSeries": "Serie",
"LabelSeriesName": "Serienavn",
"LabelSeriesProgress": "Seriefremskridt",
"LabelServerLogLevel": "Server log niveau",
"LabelServerYearReview": "Server år i review ({0})",
"LabelSetEbookAsPrimary": "Indstil som primær",
"LabelSetEbookAsSupplementary": "Indstil som supplerende",
"LabelSettingsAllowIframe": "Tillad embedding i en iframe",
"LabelSettingsAudiobooksOnly": "Kun lydbøger",
"LabelSettingsAudiobooksOnlyHelp": "Aktivering af denne indstilling vil ignorere e-bogsfiler, medmindre de er inde i en lydbogmappe, hvor de vil blive indstillet som supplerende e-bøger",
"LabelSettingsBookshelfViewHelp": "Skeumorfisk design med træhylder",
@@ -389,6 +558,8 @@
"LabelSettingsEnableWatcher": "Aktiver overvågning",
"LabelSettingsEnableWatcherForLibrary": "Aktiver mappeovervågning for bibliotek",
"LabelSettingsEnableWatcherHelp": "Aktiverer automatisk tilføjelse/opdatering af elementer, når filændringer registreres. *Kræver servergenstart",
"LabelSettingsEpubsAllowScriptedContent": "Tillad scriptet indhold i epub",
"LabelSettingsEpubsAllowScriptedContentHelp": "Tillad epub filer at køre scripts. Det anbefales at holde denne indstilling deaktiveret med mindre du stoler på kilderne af epub filerne.",
"LabelSettingsExperimentalFeatures": "Eksperimentelle funktioner",
"LabelSettingsExperimentalFeaturesHelp": "Funktioner under udvikling, der kunne bruge din feedback og hjælp til test. Klik for at åbne Github-diskussionen.",
"LabelSettingsFindCovers": "Find omslag",
@@ -397,6 +568,11 @@
"LabelSettingsHideSingleBookSeriesHelp": "Serier med en enkelt bog vil blive skjult fra serie-siden og hjemmesidehylder.",
"LabelSettingsHomePageBookshelfView": "Brug bogreolvisning på startside",
"LabelSettingsLibraryBookshelfView": "Brug bogreolvisning i biblioteket",
"LabelSettingsLibraryMarkAsFinishedPercentComplete": "Procent gennemført er større end",
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Tid tilbage er mindre end (sekunder)",
"LabelSettingsLibraryMarkAsFinishedWhen": "Marker medie indhold som færdigt når",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Spring til tidligere bøger i Fortsæt serie",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Fortsæt Serien siden hylde viser de første bøger som ikke er startet i serier med mindst en bog som ikke er startet og ingen bøger i gang. Aktivering af denne indstilling vil fortsætte serien fra den sidst gennemførte bog modsat den først ikke startede bog.",
"LabelSettingsParseSubtitles": "Fortolk undertekster",
"LabelSettingsParseSubtitlesHelp": "Udtræk undertekster fra lydbogsmappenavne.<br>Undertitler skal adskilles af \" - \"<br>f.eks. \"Bogtitel - En undertitel her\" har undertitlen \"En undertitel her\"",
"LabelSettingsPreferMatchedMetadata": "Foretræk matchede metadata",
@@ -412,9 +588,19 @@
"LabelSettingsStoreMetadataWithItem": "Gem metadata med element",
"LabelSettingsStoreMetadataWithItemHelp": "Som standard gemmes metadatafiler i /metadata/items, aktivering af denne indstilling vil gemme metadatafiler i dine bibliotekselementmapper",
"LabelSettingsTimeFormat": "Tidsformat",
"LabelShare": "Del",
"LabelShareDownloadableHelp": "Tillad brugere at dele link til at downloade en zip fil af dette biblioteksindhold.",
"LabelShareOpen": "Del åben",
"LabelShareURL": "Del URL",
"LabelShowAll": "Vis alle",
"LabelShowSeconds": "Vis sekunder",
"LabelShowSubtitles": "Vis undertitler",
"LabelSize": "Størrelse",
"LabelSleepTimer": "Søvntimer",
"LabelSlug": "Snegl",
"LabelSortAscending": "Stigende",
"LabelSortDescending": "Faldende",
"LabelStart": "Start",
"LabelStartTime": "Starttid",
"LabelStarted": "Startet",
"LabelStartedAt": "Startet klokken",
@@ -440,10 +626,19 @@
"LabelTagsAccessibleToUser": "Mærker tilgængelige for bruger",
"LabelTagsNotAccessibleToUser": "Mærker ikke tilgængelige for bruger",
"LabelTasks": "Kører opgaver",
"LabelTextEditorBulletedList": "Punktopstilling",
"LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Nummeropstilling",
"LabelTextEditorUnlink": "Aflink",
"LabelTheme": "Tema",
"LabelThemeDark": "Mørk",
"LabelThemeLight": "Lys",
"LabelTimeBase": "Tidsbase",
"LabelTimeDurationXHours": "{0} timer",
"LabelTimeDurationXMinutes": "{0} minutter",
"LabelTimeDurationXSeconds": "{0} sekunder",
"LabelTimeInMinutes": "Tid i minutter",
"LabelTimeLeft": "{0} tilbage",
"LabelTimeListened": "Tid hørt",
"LabelTimeListenedToday": "Tid hørt i dag",
"LabelTimeRemaining": "{0} tilbage",
@@ -451,6 +646,7 @@
"LabelTitle": "Titel",
"LabelToolsEmbedMetadata": "Indlejre metadata",
"LabelToolsEmbedMetadataDescription": "Indlejr metadata i lydfiler, inklusive omslag og kapitler.",
"LabelToolsM4bEncoder": "M4B indkoder",
"LabelToolsMakeM4b": "Lav M4B lydbogsfil",
"LabelToolsMakeM4bDescription": "Generer en .M4B lydbogsfil med indlejret metadata, omslag og kapitler.",
"LabelToolsSplitM4b": "Opdel M4B til MP3'er",
@@ -463,25 +659,41 @@
"LabelTracksMultiTrack": "Flerspors",
"LabelTracksNone": "Ingen spor",
"LabelTracksSingleTrack": "Enkeltspors",
"LabelTrailer": "Trailer",
"LabelType": "Type",
"LabelUnabridged": "Uforkortet",
"LabelUndo": "Undo",
"LabelUnknown": "Ukendt",
"LabelUnknownPublishDate": "Ukendt publiceringsdato",
"LabelUpdateCover": "Opdater omslag",
"LabelUpdateCoverHelp": "Tillad overskrivning af eksisterende omslag for de valgte bøger, når der findes en match",
"LabelUpdateDetails": "Opdater detaljer",
"LabelUpdateDetailsHelp": "Tillad overskrivning af eksisterende detaljer for de valgte bøger, når der findes en match",
"LabelUpdatedAt": "Opdateret ved",
"LabelUploaderDragAndDrop": "Træk og slip filer eller mapper",
"LabelUploaderDragAndDropFilesOnly": "Træk og slip filer",
"LabelUploaderDropFiles": "Smid filer",
"LabelUploaderItemFetchMetadataHelp": "Automatisk hent titel, forfatter og serie",
"LabelUseAdvancedOptions": "Anvend avancerede indstillinger",
"LabelUseChapterTrack": "Brug kapitel-spor",
"LabelUseFullTrack": "Brug fuldt spor",
"LabelUseZeroForUnlimited": "Anvend 0 for ubegrænset",
"LabelUser": "Bruger",
"LabelUsername": "Brugernavn",
"LabelValue": "Værdi",
"LabelVersion": "Version",
"LabelViewBookmarks": "Se bogmærker",
"LabelViewChapters": "Se kapitler",
"LabelViewPlayerSettings": "Vis afspiller indstillinger",
"LabelViewQueue": "Se afspilningskø",
"LabelVolume": "Volumen",
"LabelWebRedirectURLsDescription": "Godkend disse URL'er i din OAuth udgiver for at tillade omdirigering tilbage til hjemmesiden efter login:",
"LabelWebRedirectURLsSubfolder": "Undermapper for omdirigerings URL'er",
"LabelWeekdaysToRun": "Ugedage til kørsel",
"LabelXBooks": "{0} bøger",
"LabelXItems": "{0} genstande",
"LabelYearReviewHide": "Skjul år i review",
"LabelYearReviewShow": "Vis år i review",
"LabelYourAudiobookDuration": "Din lydbogsvarighed",
"LabelYourBookmarks": "Dine bogmærker",
"LabelYourPlaylists": "Dine spillelister",
@@ -489,10 +701,14 @@
"MessageAddToPlayerQueue": "Tilføj til afspilningskø",
"MessageAppriseDescription": "For at bruge denne funktion skal du have en instans af <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> kørende eller en API, der håndterer de samme anmodninger. <br /> Apprise API-webadressen skal være den fulde URL-sti for at sende underretningen, f.eks. hvis din API-instans er tilgængelig på <code>http://192.168.1.1:8337</code>, så skal du bruge <code>http://192.168.1.1:8337/notify</code>.",
"MessageBackupsDescription": "Backups inkluderer brugere, brugerfremskridt, biblioteksvareoplysninger, serverindstillinger og billeder gemt i <code>/metadata/items</code> og <code>/metadata/authors</code>. Backups inkluderer <strong>ikke</strong> nogen filer gemt i dine biblioteksmapper.",
"MessageBackupsLocationEditNote": "Note: Opdatering af backup sti vil ikke fjerne eller modificere eksisterende backups",
"MessageBackupsLocationNoEditNote": "Note: Backup sti er sat igennem miljøvariabel og kan ikke ændres her.",
"MessageBackupsLocationPathEmpty": "Backup sti kan ikke være tom",
"MessageBatchQuickMatchDescription": "Quick Match vil forsøge at tilføje manglende omslag og metadata til de valgte elementer. Aktivér indstillingerne nedenfor for at tillade Quick Match at overskrive eksisterende omslag og/eller metadata.",
"MessageBookshelfNoCollections": "Du har ikke oprettet nogen samlinger endnu",
"MessageBookshelfNoRSSFeeds": "Ingen RSS-feeds er åbne",
"MessageBookshelfNoResultsForFilter": "Ingen resultater for filter \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Intet resultat for query",
"MessageBookshelfNoSeries": "Du har ingen serier",
"MessageChapterEndIsAfter": "Kapitelslutningen er efter slutningen af din lydbog",
"MessageChapterErrorFirstNotZero": "Første kapitel skal starte ved 0",
@@ -502,19 +718,35 @@
"MessageCheckingCron": "Tjekker cron...",
"MessageConfirmCloseFeed": "Er du sikker på, at du vil lukke dette feed?",
"MessageConfirmDeleteBackup": "Er du sikker på, at du vil slette backup for {0}?",
"MessageConfirmDeleteDevice": "Er du sikker på at du vil fjerne elæser enhed \"{0}\"?",
"MessageConfirmDeleteFile": "Dette vil slette filen fra dit filsystem. Er du sikker?",
"MessageConfirmDeleteLibrary": "Er du sikker på, at du vil slette biblioteket permanent \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "Dette vil slette biblioteksgenstanden fra databasen og dit filsystem. Er du sikker?",
"MessageConfirmDeleteLibraryItems": "Dette vil slette {0} biblioteksgenstande fra din database og filsystem. Er du sikker?",
"MessageConfirmDeleteMetadataProvider": "Er du sikker på at du vil fjerne brugerdefineret metadata udgiver \"{0}\"?",
"MessageConfirmDeleteNotification": "Er du sikker på at du vil fjerne denne notifikation?",
"MessageConfirmDeleteSession": "Er du sikker på, at du vil slette denne session?",
"MessageConfirmEmbedMetadataInAudioFiles": "Er du sikker på at du vil indlejre metadata i {0} lydbogsfiler?",
"MessageConfirmForceReScan": "Er du sikker på, at du vil tvinge en genindlæsning?",
"MessageConfirmMarkAllEpisodesFinished": "Er du sikker på, at du vil markere alle episoder som afsluttet?",
"MessageConfirmMarkAllEpisodesNotFinished": "Er du sikker på, at du vil markere alle episoder som ikke afsluttet?",
"MessageConfirmMarkItemFinished": "Er du sikker på at du vil markere \"{0}\" som færdig?",
"MessageConfirmMarkItemNotFinished": "Er du sikker på at du vil markere \"{0}\" som ikke færdige?",
"MessageConfirmMarkSeriesFinished": "Er du sikker på, at du vil markere alle bøger i denne serie som afsluttet?",
"MessageConfirmMarkSeriesNotFinished": "Er du sikker på, at du vil markere alle bøger i denne serie som ikke afsluttet?",
"MessageConfirmNotificationTestTrigger": "Trigger denne notifikation med testdata?",
"MessageConfirmPurgeCache": "Rensning af cache vil slette hele mappen ved <code>/metadata/cache</code>.<br /><br />Er dy sikker på at du vil fjerne cache mappen?",
"MessageConfirmPurgeItemsCache": "Rensning af cache vil slette hele mappen ved <code>/metadata/cache/items</code>.<br />Er du sikker?",
"MessageConfirmQuickEmbed": "Advarsel! Hurtigindlejring vil ikke backe dine lydfiler op. S'rg for at du har en backup af dine lydfiler. <br /><br />Vil du fortsætte?",
"MessageConfirmQuickMatchEpisodes": "Hurtig match af afsnit vil overskrive detaljer givet et match kan findes. Kun ikke-matchede vil blive opdateret. Er du sikker?",
"MessageConfirmReScanLibraryItems": "Er du sikker på at du vil genscanne {0} genstande?",
"MessageConfirmRemoveAllChapters": "Er du sikker på, at du vil fjerne alle kapitler?",
"MessageConfirmRemoveAuthor": "Er du sikker på, at du vil fjerne forfatteren \"{0}\"?",
"MessageConfirmRemoveCollection": "Er du sikker på, at du vil fjerne samlingen \"{0}\"?",
"MessageConfirmRemoveEpisode": "Er du sikker på, at du vil fjerne episoden \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Er du sikker på, at du vil fjerne {0} episoder?",
"MessageConfirmRemoveListeningSessions": "Er du sikker på at du vil fjerne {0} lytte sessioner?",
"MessageConfirmRemoveMetadataFiles": "Er du sikker på at du vil fjerne alle metadata.{0} filer i dine biblioteksfoldere?",
"MessageConfirmRemoveNarrator": "Er du sikker på, at du vil fjerne fortælleren \"{0}\"?",
"MessageConfirmRemovePlaylist": "Er du sikker på, at du vil fjerne din spilleliste \"{0}\"?",
"MessageConfirmRenameGenre": "Er du sikker på, at du vil omdøbe genre \"{0}\" til \"{1}\" for alle elementer?",
@@ -523,11 +755,17 @@
"MessageConfirmRenameTag": "Er du sikker på, at du vil omdøbe tag \"{0}\" til \"{1}\" for alle elementer?",
"MessageConfirmRenameTagMergeNote": "Bemærk: Dette tag findes allerede, så de vil blive fusioneret.",
"MessageConfirmRenameTagWarning": "Advarsel! Et lignende tag med en anden skrivemåde eksisterer allerede \"{0}\".",
"MessageConfirmResetProgress": "Er du sikker på at du vil nulstille dit fremskridt?",
"MessageConfirmSendEbookToDevice": "Er du sikker på, at du vil sende {0} e-bog \"{1}\" til enhed \"{2}\"?",
"MessageConfirmUnlinkOpenId": "Er du sikker på at du vil fjerne linket mellem denne bruger og OpenID?",
"MessageDaysListenedInTheLastYear": "{0} dage lyttet i løbet af det sidste år",
"MessageDownloadingEpisode": "Downloader episode",
"MessageDragFilesIntoTrackOrder": "Træk filer ind i korrekt spororden",
"MessageEmbedFailed": "Indlejring fejlede!",
"MessageEmbedFinished": "Indlejring færdig!",
"MessageEmbedQueue": "Sat i kø for metadata indlejring ({0} i kø)",
"MessageEpisodesQueuedForDownload": "{0} episoder er sat i kø til download",
"MessageEreaderDevices": "For at sikre levering af ebøger, skal du eventuelt tilføje mailadressen som en gyldig afsender for hver enhed angivet forneden.",
"MessageFeedURLWillBe": "Feed-URL vil være {0}",
"MessageFetching": "Henter...",
"MessageForceReScanDescription": "vil scanne alle filer igen som en frisk scanning. Lydfilens ID3-tags, OPF-filer og tekstfiler scannes som nye.",
@@ -536,9 +774,9 @@
"MessageItemsSelected": "{0} elementer valgt",
"MessageItemsUpdated": "{0} elementer opdateret",
"MessageJoinUsOn": "Deltag i os på",
"MessageListeningSessionsInTheLastYear": "{0} lyttesessioner i det sidste år",
"MessageLoading": "Indlæser...",
"MessageLoadingFolders": "Indlæser mapper...",
"MessageLogsDescription": "Logfiler er gemt i <code>/metadata/logs</code> som JSON filer. Crash log er gemt i <code>/metadata/logs/crash_logs.txt</code>.",
"MessageM4BFailed": "M4B mislykkedes!",
"MessageM4BFinished": "M4B afsluttet!",
"MessageMapChapterTitles": "Tilknyt kapiteloverskrifter til dine eksisterende lydbogskapitler uden at justere tidsstempler",
@@ -555,6 +793,7 @@
"MessageNoCollections": "Ingen samlinger",
"MessageNoCoversFound": "Ingen omslag fundet",
"MessageNoDescription": "Ingen beskrivelse",
"MessageNoDevices": "Ingen enheder",
"MessageNoDownloadsInProgress": "Ingen downloads i gang lige nu",
"MessageNoDownloadsQueued": "Ingen downloads i kø",
"MessageNoEpisodeMatchesFound": "Ingen episode-matcher fundet",
@@ -568,6 +807,7 @@
"MessageNoLogs": "Ingen logfiler",
"MessageNoMediaProgress": "Ingen medieforløb",
"MessageNoNotifications": "Ingen meddelelser",
"MessageNoPodcastFeed": "Invalid podcast: Intet feed",
"MessageNoPodcastsFound": "Ingen podcasts fundet",
"MessageNoResults": "Ingen resultater",
"MessageNoSearchResultsFor": "Ingen søgeresultater for \"{0}\"",
@@ -577,11 +817,17 @@
"MessageNoUpdatesWereNecessary": "Ingen opdateringer var nødvendige",
"MessageNoUserPlaylists": "Du har ingen afspilningslister",
"MessageNotYetImplemented": "Endnu ikke implementeret",
"MessageOpmlPreviewNote": "Note: Dette er en forhåndsvisning af den indlæste OPML fil. Podcast titel vil blive taget fra RSS feedet.",
"MessageOr": "eller",
"MessagePauseChapter": "Pause kapitelafspilning",
"MessagePlayChapter": "Lyt til begyndelsen af kapitlet",
"MessagePlaylistCreateFromCollection": "Opret afspilningsliste fra samling",
"MessagePleaseWait": "Vent venligst...",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast har ingen RSS-feed-URL at bruge til matchning",
"MessagePodcastSearchField": "Indtast søgeterm eller RSS URL",
"MessageQuickEmbedInProgress": "Hurtig indlejring igang",
"MessageQuickEmbedQueue": "I kø for hurtigindlejring ({0} i kø)",
"MessageQuickMatchAllEpisodes": "Hurtig match alle afsnit",
"MessageQuickMatchDescription": "Udfyld tomme elementoplysninger og omslag med første matchresultat fra '{0}'. Overskriver ikke oplysninger, medmindre serverindstillingen 'Foretræk matchet metadata' er aktiveret.",
"MessageRemoveChapter": "Fjern kapitel",
"MessageRemoveEpisodes": "Fjern {0} episode(r)",
@@ -591,10 +837,50 @@
"MessageResetChaptersConfirm": "Er du sikker på, at du vil nulstille kapitler og annullere ændringerne, du har foretaget?",
"MessageRestoreBackupConfirm": "Er du sikker på, at du vil gendanne sikkerhedskopien oprettet den",
"MessageRestoreBackupWarning": "Gendannelse af en sikkerhedskopi vil overskrive hele databasen, som er placeret på /config, og omslagsbilleder i /metadata/items & /metadata/authors.<br /><br />Sikkerhedskopier ændrer ikke nogen filer i dine biblioteksmapper. Hvis du har aktiveret serverindstillinger for at gemme omslagskunst og metadata i dine biblioteksmapper, sikkerhedskopieres eller overskrives disse ikke.<br /><br />Alle klienter, der bruger din server, opdateres automatisk.",
"MessageScheduleLibraryScanNote": "For de fleste brugere, er det anbefalet at efterlade denne funktion deaktiveret for at holde mappe lurer indstilling aktiveret. Mappe lureren vil automatisk opdage ændringer i biblioteksmapper. Mappe lureren virker ikke for alle filsystemer (så som NFS) så schedulerede biblioteksscans vil blive anvendt.",
"MessageSearchResultsFor": "Søgeresultater for",
"MessageSelected": "{0} valgt",
"MessageServerCouldNotBeReached": "Serveren kunne ikke nås",
"MessageSetChaptersFromTracksDescription": "Indstil kapitler ved at bruge hver lydfil som et kapitel og kapiteloverskrift som lydfilnavn",
"MessageShareExpirationWillBe": "Udløb vil være <strong>{0}</strong>",
"MessageShareExpiresIn": "Udløber om {0}",
"MessageShareURLWillBe": "Del URL vil være <strong>{0}</strong>",
"MessageStartPlaybackAtTime": "Start afspilning for \"{0}\" kl. {1}?",
"MessageTaskAudioFileNotWritable": "Lydbogsfil \"{0}\" er ikke skrivebar",
"MessageTaskCanceledByUser": "Opgave annulleret af bruger",
"MessageTaskDownloadingEpisodeDescription": "Download afsnit \"{0}\"",
"MessageTaskEmbeddingMetadata": "Indlejring af metadata",
"MessageTaskEmbeddingMetadataDescription": "Indlejring af metadata i lydbog \"{0}\"",
"MessageTaskEncodingM4b": "Indkodning M4B",
"MessageTaskEncodingM4bDescription": "Indkodning lydog \"{0}\" ind i en enkelt M4B fil",
"MessageTaskFailed": "Fejlet",
"MessageTaskFailedToBackupAudioFile": "Fejlede backup af lydbogsfil \"{0}\"",
"MessageTaskFailedToCreateCacheDirectory": "Fejlede at oprette cache mappe",
"MessageTaskFailedToEmbedMetadataInFile": "Fejlede at indkode metadata i fil \"{0}\"",
"MessageTaskFailedToMergeAudioFiles": "Fejlede at sammenflette lydbogsfiler",
"MessageTaskFailedToMoveM4bFile": "Fejlede i at flytte M4B fil",
"MessageTaskFailedToWriteMetadataFile": "Fejlede i at skrive metadata fil",
"MessageTaskMatchingBooksInLibrary": "Matchede bøger i bibliotek \"{0}\"",
"MessageTaskNoFilesToScan": "Ingen filer at scanne",
"MessageTaskOpmlImport": "OPML import",
"MessageTaskOpmlImportDescription": "Oprettelse af podcasts fra {0} RSS feeds",
"MessageTaskOpmlImportFeed": "OPML importering fejlede",
"MessageTaskOpmlImportFeedDescription": "Importering af RSS feed \"{0}\"",
"MessageTaskOpmlImportFeedFailed": "Fejlede at hente podcast feed",
"MessageTaskOpmlImportFeedPodcastDescription": "Opretter podcast \"{0}\"",
"MessageTaskOpmlImportFeedPodcastExists": "Podcast ligger allerede på filsti",
"MessageTaskOpmlImportFeedPodcastFailed": "Fejlede i at oprette podcast",
"MessageTaskOpmlImportFinished": "Tilføjede {0} podcasts",
"MessageTaskOpmlParseFailed": "Fejlede i at læse OPML fil",
"MessageTaskOpmlParseFastFail": "Forkert OPML <opml> tag ikke fundet ELLER et <outline> tag var ikke fundet",
"MessageTaskOpmlParseNoneFound": "Ingen feeds fundet i OPML fil",
"MessageTaskScanItemsAdded": "{0} tilføjet",
"MessageTaskScanItemsMissing": "{0} mangler",
"MessageTaskScanItemsUpdated": "{0} opdateret",
"MessageTaskScanNoChangesNeeded": "Ingen ændringer nødvendigt",
"MessageTaskScanningFileChanges": "Scanner filændringer i \"{0}\"",
"MessageTaskScanningLibrary": "Scanning af \"{0}\" bibliotek",
"MessageTaskTargetDirectoryNotWritable": "Mål sti er ikke skrivebar",
"MessageThinking": "Tænker...",
"MessageUploaderItemFailed": "Fejl ved upload",
"MessageUploaderItemSuccess": "Uploadet med succes!",
@@ -612,40 +898,102 @@
"NoteUploaderFoldersWithMediaFiles": "Mapper med mediefiler håndteres som separate bibliotekselementer.",
"NoteUploaderOnlyAudioFiles": "Hvis du kun uploader lydfiler, håndteres hver lydfil som en separat lydbog.",
"NoteUploaderUnsupportedFiles": "Ikke-understøttede filer ignoreres. Når du vælger eller slipper en mappe, ignoreres andre filer, der ikke er i en emnemappe.",
"NotificationOnBackupCompletedDescription": "Udløst når backup er færdig",
"NotificationOnBackupFailedDescription": "Udløst når backup fejler",
"NotificationOnEpisodeDownloadedDescription": "Udløst når et podcast afsnit er automatisk downloadet",
"NotificationOnTestDescription": "Event for test af notifikationssystemet",
"PlaceholderNewCollection": "Nyt samlingnavn",
"PlaceholderNewFolderPath": "Ny mappes sti",
"PlaceholderNewPlaylist": "Nyt afspilningslistnavn",
"PlaceholderSearch": "Søg..",
"PlaceholderSearchEpisode": "Søg efter episode..",
"StatsAuthorsAdded": "forfattere tilføjet",
"StatsBooksAdded": "bøger tilføjet",
"StatsBooksAdditional": "Nogle tilføjelser inkludere…",
"StatsBooksFinished": "bøger færdige",
"StatsBooksFinishedThisYear": "Nogle bøger færdiggjort i år.…",
"StatsBooksListenedTo": "bøger lyttet til",
"StatsCollectionGrewTo": "Din bog kollektion voksede til…",
"StatsSessions": "sessioner",
"StatsSpentListening": "brugt at lytte",
"StatsTopAuthor": "TOP FORFATTER",
"StatsTopAuthors": "TOP FORFATTERE",
"StatsTopGenre": "TOP GENRE",
"StatsTopGenres": "TOP GENRER",
"StatsTopMonth": "TOP MÅNED",
"StatsTopNarrator": "TOP OPLÆSER",
"StatsTopNarrators": "TOP OPLÆSERE",
"StatsTotalDuration": "Med den totale varighed af…",
"StatsYearInReview": "ÅR I REVIEW",
"ToastAccountUpdateSuccess": "Konto opdateret",
"ToastAppriseUrlRequired": "Skal indtaste en Apprise URL",
"ToastAsinRequired": "ASIN er påkrævet",
"ToastAuthorImageRemoveSuccess": "Forfatterbillede fjernet",
"ToastAuthorNotFound": "Forfatter \"{0}\" ikke fundet",
"ToastAuthorRemoveSuccess": "Forfatter fjernet",
"ToastAuthorSearchNotFound": "Forfatter ikke fundet",
"ToastAuthorUpdateMerged": "Forfatter fusioneret",
"ToastAuthorUpdateSuccess": "Forfatter opdateret",
"ToastAuthorUpdateSuccessNoImageFound": "Forfatter opdateret (ingen billede fundet)",
"ToastBackupAppliedSuccess": "Backup indlæst",
"ToastBackupCreateFailed": "Mislykkedes oprettelse af sikkerhedskopi",
"ToastBackupCreateSuccess": "Sikkerhedskopi oprettet",
"ToastBackupDeleteFailed": "Mislykkedes sletning af sikkerhedskopi",
"ToastBackupDeleteSuccess": "Sikkerhedskopi slettet",
"ToastBackupInvalidMaxKeep": "Forkert antal backups at beholde",
"ToastBackupInvalidMaxSize": "Forkert maks backup størrelse",
"ToastBackupRestoreFailed": "Mislykkedes gendannelse af sikkerhedskopi",
"ToastBackupUploadFailed": "Mislykkedes upload af sikkerhedskopi",
"ToastBackupUploadSuccess": "Sikkerhedskopi uploadet",
"ToastBatchDeleteFailed": "Batch slet fejlede",
"ToastBatchDeleteSuccess": "Batch slet succes",
"ToastBatchQuickMatchFailed": "Batch Hurtig Match fejlede!",
"ToastBatchQuickMatchStarted": "Batch Hurtig Match af {0} bøger startet!",
"ToastBatchUpdateFailed": "Mislykkedes batchopdatering",
"ToastBatchUpdateSuccess": "Batchopdatering lykkedes",
"ToastBookmarkCreateFailed": "Mislykkedes oprettelse af bogmærke",
"ToastBookmarkCreateSuccess": "Bogmærke tilføjet",
"ToastBookmarkRemoveSuccess": "Bogmærke fjernet",
"ToastBookmarkUpdateSuccess": "Bogmærke opdateret",
"ToastCachePurgeFailed": "Fejlede at opryde cache",
"ToastCachePurgeSuccess": "Cache ryddet op i succesfuldt",
"ToastChaptersHaveErrors": "Kapitler har fejl",
"ToastChaptersMustHaveTitles": "Kapitler skal have titler",
"ToastCollectionItemsRemoveSuccess": "Element(er) fjernet fra samlingen",
"ToastChaptersRemoved": "Kapitler fjernet",
"ToastChaptersUpdated": "Kapitler opdateret",
"ToastCollectionItemsAddFailed": "Genstand(e) tilføjet til kollektion fejlet",
"ToastCollectionRemoveSuccess": "Samling fjernet",
"ToastCollectionUpdateSuccess": "Samling opdateret",
"ToastCoverUpdateFailed": "Cover opdatering fejlede",
"ToastDateTimeInvalidOrIncomplete": "Dato og tid er forkert eller ufærdig",
"ToastDeleteFileFailed": "Slet fil fejlede",
"ToastDeleteFileSuccess": "Fil slettet",
"ToastDeviceAddFailed": "Fejlede at tilføje enhed",
"ToastDeviceNameAlreadyExists": "Elæser enhed med det navn eksistere allerede",
"ToastDeviceTestEmailFailed": "Fejlede at sende test mail",
"ToastDeviceTestEmailSuccess": "Test mail sendt",
"ToastEmailSettingsUpdateSuccess": "Mail indstillinger opdateret",
"ToastEncodeCancelFailed": "Fejlede at afbryde indkodning",
"ToastEncodeCancelSucces": "Indkodning afbrudt",
"ToastEpisodeDownloadQueueClearFailed": "Fejlede at rydde op i kø",
"ToastEpisodeDownloadQueueClearSuccess": "Afsnit download kø renset",
"ToastEpisodeUpdateSuccess": "{0} afsnit opdateret",
"ToastErrorCannotShare": "Kan ikke dele på denne enhed",
"ToastFailedToLoadData": "Fejlede at indlæse data",
"ToastFailedToMatch": "Fejlet match",
"ToastFailedToShare": "Fejlet deling",
"ToastFailedToUpdate": "Fejlet opdatering",
"ToastInvalidImageUrl": "Forkert billede URL",
"ToastInvalidMaxEpisodesToDownload": "Forkert maks afsnit at hente",
"ToastInvalidUrl": "Forkert URL",
"ToastItemCoverUpdateSuccess": "Varens omslag opdateret",
"ToastItemDeletedFailed": "Fejlede at slette genstand",
"ToastItemDeletedSuccess": "Genstand slettet",
"ToastItemDetailsUpdateSuccess": "Varedetaljer opdateret",
"ToastItemMarkedAsFinishedFailed": "Mislykkedes markering som afsluttet",
"ToastItemMarkedAsFinishedSuccess": "Vare markeret som afsluttet",
"ToastItemMarkedAsNotFinishedFailed": "Mislykkedes markering som ikke afsluttet",
"ToastItemMarkedAsNotFinishedSuccess": "Vare markeret som ikke afsluttet",
"ToastItemUpdateSuccess": "Genstand opdateret",
"ToastLibraryCreateFailed": "Mislykkedes oprettelse af bibliotek",
"ToastLibraryCreateSuccess": "Bibliotek \"{0}\" oprettet",
"ToastLibraryDeleteFailed": "Mislykkedes sletning af bibliotek",
@@ -653,25 +1001,84 @@
"ToastLibraryScanFailedToStart": "Mislykkedes start af skanning",
"ToastLibraryScanStarted": "Biblioteksskanning startet",
"ToastLibraryUpdateSuccess": "Bibliotek \"{0}\" opdateret",
"ToastMatchAllAuthorsFailed": "Fejlede at matche alle forfattere",
"ToastMetadataFilesRemovedError": "Fejlet at fjerne metadata.{0} filer",
"ToastMetadataFilesRemovedNoneFound": "Ingen metadata.{0} filer fundet i bibliotek",
"ToastMetadataFilesRemovedNoneRemoved": "Ingen metadata.{0} filer slettet",
"ToastMetadataFilesRemovedSuccess": "{0} metadata.{1} filer slettet",
"ToastMustHaveAtLeastOnePath": "Skal have mindst en sti",
"ToastNameEmailRequired": "Navn og email påkrævet",
"ToastNameRequired": "Navn påkrævet",
"ToastNewEpisodesFound": "{0} nye afsnit fundet",
"ToastNewUserCreatedFailed": "Fejlede at oprette konto: \"{0}\"",
"ToastNewUserCreatedSuccess": "Ny konto oprettet",
"ToastNewUserLibraryError": "Skal vælge mindst et bibliotek",
"ToastNewUserPasswordError": "Skal have et password, kun root brugeren kan have et tomt password",
"ToastNewUserTagError": "Skal vælge mindst et tag",
"ToastNewUserUsernameError": "Angiv brugernavn",
"ToastNoNewEpisodesFound": "Ingen nye afsnit fundet",
"ToastNoRSSFeed": "Podcast har ingen RSS feed",
"ToastNoUpdatesNecessary": "Ingen opdateringer nødvendige",
"ToastNotificationCreateFailed": "Fejlede at oprette notifikation",
"ToastNotificationDeleteFailed": "Fejlede at slette notifikation",
"ToastNotificationFailedMaximum": "Maks forsøg skal være >= 0",
"ToastNotificationQueueMaximum": "Maks notifikationskø skal være >= 0",
"ToastNotificationSettingsUpdateSuccess": "Notifikationsindstillinger opdateret",
"ToastNotificationTestTriggerFailed": "Fejlede at oprette en test notifikation",
"ToastNotificationTestTriggerSuccess": "Test notifikation oprettet",
"ToastNotificationUpdateSuccess": "Notifikation opdateret",
"ToastPlaylistCreateFailed": "Mislykkedes oprettelse af afspilningsliste",
"ToastPlaylistCreateSuccess": "Afspilningsliste oprettet",
"ToastPlaylistRemoveSuccess": "Afspilningsliste fjernet",
"ToastPlaylistUpdateSuccess": "Afspilningsliste opdateret",
"ToastPodcastCreateFailed": "Mislykkedes oprettelse af podcast",
"ToastPodcastCreateSuccess": "Podcast oprettet med succes",
"ToastPodcastGetFeedFailed": "Fejlede at hente podcast feed",
"ToastPodcastNoEpisodesInFeed": "Ingen nye afsnit fundet i RSS feed",
"ToastPodcastNoRssFeed": "Podcast har ingen RSS feed",
"ToastProgressIsNotBeingSynced": "Fremskridt ikke synkroniseret, genstart afspilning",
"ToastProviderCreatedFailed": "Fejlede at tilføje udbyder",
"ToastProviderCreatedSuccess": "Ny udbyder tilføjet",
"ToastProviderNameAndUrlRequired": "Navn og URL påkrævet",
"ToastProviderRemoveSuccess": "Udbyder fjernet",
"ToastRSSFeedCloseFailed": "Mislykkedes lukning af RSS-feed",
"ToastRSSFeedCloseSuccess": "RSS-feed lukket",
"ToastRemoveFailed": "Fejlede at slette",
"ToastRemoveItemFromCollectionFailed": "Mislykkedes fjernelse af element fra samling",
"ToastRemoveItemFromCollectionSuccess": "Element fjernet fra samling",
"ToastRemoveItemsWithIssuesFailed": "Fejlede at slette genstande med fejl",
"ToastRemoveItemsWithIssuesSuccess": "Slettede genstande med fejl",
"ToastRenameFailed": "Fejlede at omdøbe",
"ToastRescanFailed": "Genscan fejlede for {0}",
"ToastRescanRemoved": "Genscan gennemført, genstand blev fjernet",
"ToastRescanUpToDate": "Genscan gennemført, genstand var opdateret",
"ToastRescanUpdated": "Genscan gennemført, genstand blev opdateret",
"ToastScanFailed": "Fejlede at scanne biblioteksgenstand",
"ToastSelectAtLeastOneUser": "Vælg mindst en bruger",
"ToastSendEbookToDeviceFailed": "Mislykkedes afsendelse af e-bog til enhed",
"ToastSendEbookToDeviceSuccess": "E-bog afsendt til enhed \"{0}\"",
"ToastSeriesUpdateFailed": "Mislykkedes opdatering af serie",
"ToastSeriesUpdateSuccess": "Serieopdatering lykkedes",
"ToastServerSettingsUpdateSuccess": "Server indstillinger opdateret",
"ToastSessionCloseFailed": "Luk session fejlede",
"ToastSessionDeleteFailed": "Mislykkedes sletning af session",
"ToastSessionDeleteSuccess": "Session slettet",
"ToastSleepTimerDone": "Sleep timer færdig... zZzzZz",
"ToastSlugMustChange": "Snegl indeholder ugyldige karakterer",
"ToastSlugRequired": "Snegl påkrævet",
"ToastSocketConnected": "Socket forbundet",
"ToastSocketDisconnected": "Socket afbrudt",
"ToastSocketFailedToConnect": "Socket kunne ikke oprettes",
"ToastSortingPrefixesEmptyError": "Skal indeholde mindst 1 sorteringspræfiks",
"ToastSortingPrefixesUpdateSuccess": "Sortering af præfiks opdateret ({0} genstande)",
"ToastTitleRequired": "Titel påkrævet",
"ToastUnknownError": "Ukendt fejl",
"ToastUnlinkOpenIdFailed": "Fejlede i af afkoble bruger fra OpenID",
"ToastUnlinkOpenIdSuccess": "Bruger afkoblet fra OpenID",
"ToastUserDeleteFailed": "Mislykkedes sletning af bruger",
"ToastUserDeleteSuccess": "Bruger slettet"
"ToastUserDeleteSuccess": "Bruger slettet",
"ToastUserPasswordChangeSuccess": "Password ændret",
"ToastUserPasswordMismatch": "Passwords passer ikke sammen",
"ToastUserPasswordMustChange": "Nyt password må ikke være det gamle",
"ToastUserRootRequireName": "Skal indholde et root brugernavn"
}

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