Compare commits

...

136 Commits

Author SHA1 Message Date
advplyr 24923c0009 Version bump v2.13.3 2024-09-02 17:09:34 -05:00
advplyr a9036c9738 Merge pull request #3360 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-09-02 16:53:30 -05:00
Hosted Weblate f9f7fbed33 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/
2024-09-02 23:50:30 +02:00
thehijacker 53b5bee736 Translated using Weblate (Slovenian)
Currently translated at 100.0% (974 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-09-02 23:50:29 +02:00
Kamil Pomykała d0b3726905 Translated using Weblate (Polish)
Currently translated at 81.8% (797 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2024-09-02 23:50:28 +02:00
Andrej Kralj 7a6864507e Translated using Weblate (Slovenian)
Currently translated at 100.0% (974 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-09-02 23:50:27 +02:00
Soaibuzzaman e20563f2e1 Translated using Weblate (Bengali)
Currently translated at 82.0% (799 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bn/
2024-09-02 23:50:26 +02:00
advplyr fea5f8f3d4 Update:Batch edit page show confirmation before navigating away with unsaved changes #3369 2024-09-02 16:50:22 -05:00
advplyr f9bb529b85 Fix:Unlink OpenID button translation string 2024-09-02 16:15:26 -05:00
advplyr 60e348fcc1 Fix:Updating root user #3366 2024-09-02 16:12:57 -05:00
advplyr f194c5be0e Merge pull request #3368 from nichwall/fix_tag_permissions
Fix tag permissions
2024-09-02 15:58:05 -05:00
advplyr 47712e63f1 Update user default permissions 2024-09-02 15:55:25 -05:00
Nicholas Wallace 790c1fb34a Allow update of default permission keys missing for user 2024-09-02 10:28:03 -07:00
Nicholas Wallace 9cca731acc Add: missing default user permission property 2024-09-02 10:08:17 -07:00
advplyr 48f232790a Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-09-01 15:41:19 -05:00
advplyr 3c55aa5f43 Version bump v2.13.2 2024-09-01 15:41:11 -05:00
advplyr 8c1edb30a6 Merge pull request #3356 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-09-01 15:35:29 -05:00
Andrej Kralj 5e64af4448 Translated using Weblate (Slovenian)
Currently translated at 45.9% (448 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-09-01 22:32:04 +02:00
advplyr 9f60017cfe Update:Remove oldSeries model 2024-09-01 15:26:43 -05:00
advplyr b6a86d11d2 Fix:Toasts for item details updated 2024-09-01 15:11:06 -05:00
advplyr db86bfd63d Fix:New authors not setting lastFirst column, updates for new Series model 2024-09-01 15:08:56 -05:00
advplyr 7ff72a8920 Merge pull request #3355 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-09-01 10:19:57 -05:00
Andrej Kralj 2c4f86d148 Translated using Weblate (Slovenian)
Currently translated at 26.2% (256 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-09-01 15:17:26 +00:00
advplyr 1a9f26e804 Version bump v2.13.1 2024-09-01 07:45:46 -05:00
advplyr 42f8194bde Add:Slovenian language option 2024-09-01 07:45:32 -05:00
Weblate (bot) 8634b7058c Translations update from Hosted Weblate (#3351)
* Translated using Weblate (Bengali)

Currently translated at 78.8% (768 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bn/

* Translated using Weblate (Gujarati)

Currently translated at 16.1% (157 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/gu/

* Translated using Weblate (Hungarian)

Currently translated at 75.3% (734 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/

* Translated using Weblate (French)

Currently translated at 92.6% (902 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/

* Translated using Weblate (French)

Currently translated at 92.6% (902 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/

* Translated using Weblate (Russian)

Currently translated at 83.7% (816 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/

* Translated using Weblate (French)

Currently translated at 93.8% (914 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/

* Translated using Weblate (French)

Currently translated at 93.8% (914 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/

* Translated using Weblate (Russian)

Currently translated at 84.3% (822 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/

* Translated using Weblate (French)

Currently translated at 94.1% (917 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/

* Translated using Weblate (Russian)

Currently translated at 85.3% (831 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/

* Translated using Weblate (French)

Currently translated at 94.4% (920 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/

* Translated using Weblate (French)

Currently translated at 94.4% (920 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/

* Translated using Weblate (Russian)

Currently translated at 85.9% (837 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/

* Translated using Weblate (French)

Currently translated at 94.8% (924 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/

* Translated using Weblate (French)

Currently translated at 94.8% (924 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/

* Translated using Weblate (Russian)

Currently translated at 86.2% (840 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/

* Translated using Weblate (French)

Currently translated at 96.8% (943 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/

* Translated using Weblate (French)

Currently translated at 96.8% (943 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/

* Translated using Weblate (Russian)

Currently translated at 86.8% (846 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/

* Translated using Weblate (German)

Currently translated at 100.0% (974 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/

* Translated using Weblate (French)

Currently translated at 100.0% (974 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/

* Translated using Weblate (French)

Currently translated at 100.0% (974 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/

* Translated using Weblate (Russian)

Currently translated at 100.0% (974 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/

* Added translation using Weblate (Slovenian)

* Translated using Weblate (Croatian)

Currently translated at 100.0% (974 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/

* Translated using Weblate (Spanish)

Currently translated at 99.7% (972 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/

* Translated using Weblate (Croatian)

Currently translated at 100.0% (974 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/

* Translated using Weblate (Slovenian)

Currently translated at 21.1% (206 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/

---------

Co-authored-by: Nicholas W <nicholaslwallace@gmail.com>
Co-authored-by: Pierrick Guillaume <pierguill@gmail.com>
Co-authored-by: Charlie <Machou@users.noreply.hosted.weblate.org>
Co-authored-by: Dmitry <dmitry@naboychenko.ru>
Co-authored-by: Valentin <valentin.bartschies@gmail.com>
Co-authored-by: biuklija <ivan@biuklija.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Andrej Kralj <andrej.kralj@gmail.com>
2024-09-01 07:38:57 -05:00
advplyr fc276b330a Fix:Server crash when uploading or adding new podcast #3353 2024-09-01 07:35:05 -05:00
advplyr 5b22d7430a Version bump v2.13.0 2024-08-31 15:39:50 -05:00
advplyr 8883debc74 Merge pull request #3350 from nichwall/untranslated_strings_cleanup
Delete untranslated strings
2024-08-31 15:09:57 -05:00
Nicholas Wallace c92cb08f6f Delete untranslated strings 2024-08-31 13:04:09 -07:00
advplyr 1254b668de Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-08-31 15:03:50 -05:00
advplyr 48b703bf9f Update:Global search author card and library stat author name links to author page 2024-08-31 15:03:42 -05:00
advplyr 064679c057 Update:Author number of books sort fallsback to sort on name when num books is the same 2024-08-31 14:59:42 -05:00
Weblate (bot) ba23d258e7 Translations update from Hosted Weblate (#3342)
* Translated using Weblate (Croatian)

Currently translated at 69.8% (611 of 875 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/

* Translated using Weblate (Croatian)

Currently translated at 92.1% (806 of 875 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/

* Update translation files

Updated by "Cleanup translation files" add-on in Weblate.

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/

* Translated using Weblate (German)

Currently translated at 94.5% (921 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/

* Translated using Weblate (Spanish)

Currently translated at 90.9% (886 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/

* Translated using Weblate (French)

Currently translated at 90.8% (885 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/

* Translated using Weblate (Croatian)

Currently translated at 100.0% (974 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/

* Translated using Weblate (French)

Currently translated at 92.4% (900 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/

* Translated using Weblate (French)

Currently translated at 93.5% (911 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/

---------

Co-authored-by: biuklija <ivan@biuklija.com>
Co-authored-by: Mario <leet31337@web.de>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: Charlie <Machou@users.noreply.hosted.weblate.org>
2024-08-31 14:32:40 -05:00
Nicholas W 98cd19d440 Config issue workflow (#3348)
* Intial: issue comments workflow

* Update: formatting

* Additional common search terms
2024-08-31 13:35:14 -05:00
advplyr 4c8b91e9d9 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-08-31 13:27:54 -05:00
advplyr ba742563c2 Remove old Author object & fix issue deleting empty authors 2024-08-31 13:27:48 -05:00
Nicholas W f0e70ed27b Translation strings added (#3304)
* Update: `pages/items/_id` toast messages

* Update: account modal strings

* Update: audio file data modal strings

* Update: sleep timer set string

* Update: loading indicator string

* Update: lazy book card strings

* Reorder keys

* Fix: syntax error in LazyBookCard

* Fix: json ordering

* Fix: fix double message definition

* Update: login form toast strings

* Update: batch delete toast

* Update: collection add toast messages

* Replace: toasts in BookShelfToolbar

* Update: playlist edit toasts

* Update: Details tab

* Add: title required string

* Update: ereader toasts

* Update: author toasts, title and name required toasts

* Clean up "no updates" strings

* Change: slug strings

* Update: cover modal toasts

* Change: cancel encode toasts

* Change: failed to share toasts

* Simplify: "renameFail" and "removeFail" toasts

* Fix: ordering

* Change: chapters remove toast

* Update: notification strings

* Revert: loading indicator (error in browser)

* Update: collectionBooksTable toast

* Update: "failed to get" strings

* Update: backup strings

* Update: custom provider strings

* Update: sessions strings

* Update: email strings

* Update sort display translation strings, update podcast episode queue strings to use translation

* Fix loading indicator please wait translation

* Consolidate translations and reduce number of toasts

---------

Co-authored-by: advplyr <advplyr@protonmail.com>
2024-08-30 17:47:49 -05:00
advplyr acc4bdbc5e Add:Podcast latest page includes Mark as Finished button #3321 2024-08-29 17:27:52 -05:00
advplyr c45c82306e Remove old library, folder and librarysettings model 2024-08-28 17:26:23 -05:00
advplyr fd827b2214 Merge pull request #3265 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-08-28 04:55:44 -05:00
biuklija df1c157994 Translated using Weblate (Croatian)
Currently translated at 65.8% (576 of 875 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-08-27 21:56:31 +00:00
biuklija a92e417581 Translated using Weblate (Croatian)
Currently translated at 65.6% (574 of 875 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-08-27 21:56:30 +00:00
biuklija 6ad0719880 Translated using Weblate (Croatian)
Currently translated at 65.6% (574 of 875 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-08-27 21:56:30 +00:00
biuklija 5383d0b5f7 Translated using Weblate (Croatian)
Currently translated at 65.6% (574 of 875 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-08-27 21:56:29 +00:00
Tom Redd b3cefc075d Translated using Weblate (Norwegian Bokmål)
Currently translated at 82.2% (720 of 875 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2024-08-27 21:56:29 +00:00
Christian Wia ac62d18007 Translated using Weblate (French)
Currently translated at 99.8% (874 of 875 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-08-27 21:56:28 +00:00
Ahetek fe14c26782 Translated using Weblate (Polish)
Currently translated at 90.7% (794 of 875 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2024-08-27 21:56:28 +00:00
Mario b33a3cabf9 Translated using Weblate (German)
Currently translated at 100.0% (875 of 875 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-08-27 21:56:27 +00:00
gallegonovato 6224163ecd Translated using Weblate (Spanish)
Currently translated at 100.0% (875 of 875 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-08-27 21:56:27 +00:00
Mario 05aabb2843 Translated using Weblate (German)
Currently translated at 100.0% (874 of 874 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-08-27 21:56:26 +00:00
gallegonovato 7d2d5f6bf4 Translated using Weblate (Spanish)
Currently translated at 100.0% (874 of 874 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-08-27 21:56:26 +00:00
Illia Pyshniak c938685679 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (872 of 872 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2024-08-27 21:56:25 +00:00
lecoq e6ecc28001 Translated using Weblate (French)
Currently translated at 99.8% (871 of 872 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-08-27 21:56:24 +00:00
kuci-JK 93fa6ba466 Translated using Weblate (Czech)
Currently translated at 96.6% (843 of 872 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2024-08-27 21:56:24 +00:00
Fredrik Lindqvist a8f459e4fa Translated using Weblate (Swedish)
Currently translated at 83.6% (729 of 872 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2024-08-27 21:56:24 +00:00
gallegonovato 2441bb1cec Translated using Weblate (Spanish)
Currently translated at 100.0% (872 of 872 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-08-27 21:56:23 +00:00
Vito0912 25cc24fca5 Translated using Weblate (German)
Currently translated at 100.0% (872 of 872 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-08-27 21:56:23 +00:00
Vito0912 ff4cbc6d5f Translated using Weblate (German)
Currently translated at 100.0% (872 of 872 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-08-27 21:56:22 +00:00
Charlie f79bfae95d Translated using Weblate (French)
Currently translated at 99.8% (871 of 872 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-08-27 21:56:22 +00:00
SunSpring 2f99efcc60 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (872 of 872 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-08-27 21:56:21 +00:00
burghy86 45b13571a5 Translated using Weblate (Italian)
Currently translated at 100.0% (872 of 872 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2024-08-27 21:56:21 +00:00
Mario 04da8812df Translated using Weblate (German)
Currently translated at 99.8% (871 of 872 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-08-27 21:56:20 +00:00
Vito0912 840304ee04 Translated using Weblate (German)
Currently translated at 99.8% (871 of 872 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-08-27 21:56:20 +00:00
Mario 41bd9a9358 Translated using Weblate (German)
Currently translated at 99.8% (871 of 872 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-08-27 21:56:19 +00:00
Charlie 1e0a9918fd Translated using Weblate (French)
Currently translated at 99.8% (871 of 872 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-08-27 21:56:19 +00:00
gallegonovato 799acf5db8 Translated using Weblate (Spanish)
Currently translated at 100.0% (872 of 872 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-08-27 21:56:18 +00:00
advplyr 1326d29fad Merge pull request #3332 from itzexor/memorystore-2
memorystore: simplify, refactor, re-enable
2024-08-27 16:56:07 -05:00
advplyr 9b35530956 Fix memorystore constructor validation 2024-08-27 16:53:18 -05:00
advplyr 0ae054c5d7 Update tools endpoint status codes 2024-08-26 17:02:29 -05:00
advplyr c72eac9987 Fix:Check if book is already being merged before allowing to start #3331 2024-08-25 17:13:09 -05:00
advplyr 159ccd807f Updates to migrate off of old library model 2024-08-24 16:09:54 -05:00
advplyr 5d13faef33 Updates to LibraryController to use new Library model
- Additional validation on API endpoints
- Removed success toast when reorder libraries
2024-08-24 15:38:15 -05:00
advplyr e0de59a4b6 Merge pull request #3329 from mikiher/embed-single-file
Fix embed and convert for single file library items
2024-08-24 13:27:43 -05:00
advplyr 519a1b0eaf Merge pull request #3328 from mikiher/aspect-ratio-card-width
Update series and collection width to account for book aspect ratio
2024-08-24 13:24:38 -05:00
mikiher 4d8e1b7cef Fix embed and convert for single file library items 2024-08-24 12:01:00 +03:00
mikiher 6d3e096e08 Update series and collection width to account for book aspect ratio 2024-08-24 08:49:40 +03:00
advplyr 38edcdca4b Updates to use new Library model 2024-08-23 16:59:51 -05:00
advplyr 8774e6be71 Update:Create library endpoint to create using new model, adding additional validation 2024-08-22 17:39:28 -05:00
James Ross ec197b2e13 memorystore: simplify, refactor, re-enable
Removes a lot of unused (in ABS) functionality, refactors to ES6
style class, and re-enables this custom implementation with check
period and ttl of 1 day, and 1000 max entries.

The class now only implments the required (as per express-session docs)
methods and removes optional methods, except touch() which allows the
TTL of an entry to be refreshed without affecting its LRU recency.

There is no longer a way to stop the prune timer, but I don't belive
the function was ever being called beforehand. The session store's
lifetime is the same as the application's, and since it is unref()'d
should not cause any shutdown issues.
2024-08-22 03:55:51 +00:00
advplyr 1c0d6e9c67 Merge pull request #3313 from mikiher/author-image-path
Update AuthorController to handle invalid image paths and log a warning
2024-08-21 17:46:39 -05:00
advplyr 7d711da381 Merge pull request #3305 from nichwall/update_api_linting_workflow
Update api linting workflow
2024-08-21 17:43:05 -05:00
advplyr f66cea9829 Merge pull request #3312 from nichwall/close_comics_during_scan
Close comics during scan
2024-08-21 17:41:06 -05:00
advplyr 5f572face5 Merge pull request #3311 from nichwall/backup_restore_clear_cache
Backup restore clear cache
2024-08-21 17:38:16 -05:00
advplyr 88a4cf9f12 Merge pull request #3319 from chancez/pr/chancez/ios_safari_content_type_workaround
Fix Content-Type header when browser user-agent is from an Apple mobile device
2024-08-21 17:20:14 -05:00
Chance Zibolski 0b860e0d40 Fix Content-Type header when browser user-agent is from an Apple mobile device
Fixes #3310

Signed-off-by: Chance Zibolski <chance.zibolski@gmail.com>
2024-08-20 19:01:14 -07:00
advplyr 149bb3e5b2 Fix:Audible book match not falling back to search after failed ASIN #3314 2024-08-20 17:04:48 -05:00
advplyr 7a7a779824 Update podcast audio file meta tag to use album-artist for author and fallback to artist tag #3315 2024-08-20 16:41:17 -05:00
mikiher 20a3657063 Update AuthorController to handle invalid image paths and log a warning 2024-08-20 10:51:24 +03:00
Nicholas Wallace 9c87c3a095 Free memory after extracting comic 2024-08-19 22:05:25 -07:00
Nicholas Wallace 4de65b4369 Autoformat parseComicMetadata 2024-08-19 21:00:16 -07:00
Nicholas Wallace 996c78d760 Add: clear metadata cache when restoring backup 2024-08-19 19:32:53 -07:00
Nicholas Wallace ccdc3d60c4 Change: CacheManager use ensureDir 2024-08-19 19:25:01 -07:00
Nicholas Wallace 8be08882d8 Update formatting in CacheManager 2024-08-19 19:23:41 -07:00
advplyr 26d2c5a8f0 Remove oldUser object require 2024-08-19 17:35:00 -05:00
advplyr bae39e3a2d Remove oldUser object require 2024-08-19 17:31:29 -05:00
advplyr bb1a72269a Remove old User object with old MediaProgress & AudioBookmark 2024-08-19 17:26:17 -05:00
Nicholas Wallace 9674cfd258 Add: explicit permissions for OpenAPI linting workflow 2024-08-18 19:08:04 -07:00
Nicholas Wallace 627ddd2f70 Fix: OpenAPI lint workflow trigger 2024-08-18 19:07:18 -07:00
Nicholas W 27b3a44147 Add: Backup notification (#3225)
* Formatting updates

* Add: backup completion notification

* Fix: comment for backup

* Add: backup size units to notification

* Add: failed backup notification

* Add: calls to failed backup notification

* Update: notification OpenAPI spec

* Update notifications to first check if any are active for an event, update JS docs

---------

Co-authored-by: advplyr <advplyr@protonmail.com>
2024-08-18 14:32:05 -05:00
advplyr 5308fd8b46 Update:Create & update API endpoints to create with new data model 2024-08-17 17:18:40 -05:00
advplyr 1b914d5d4f Update:Log local auth login attempts for failed and successful #2533 #2579 2024-08-17 15:02:59 -05:00
advplyr 9e0f17f7c6 Merge pull request #3294 from mikiher/menu-keyboard-navigation-refactor
Refactor menu keyoboard navigation into mixin
2024-08-17 14:08:20 -05:00
advplyr 1320b6d785 Add:Next chapter button plays next item in queue #3299 2024-08-17 13:32:00 -05:00
mikiher f1ddbeadaf Refactor menu keyoboard navigation into mixin 2024-08-17 06:08:32 +03:00
advplyr f9f89e1e51 Update material symbols icon font
- only include Material Symbols Rounded
- Replace some ligatures with codepoint so loading isnt as ugly/shifting
2024-08-16 16:57:17 -05:00
advplyr bbf214fa4c Update LibraryItem debug logs to show objects and display libraryFiles changes differently 2024-08-15 17:05:18 -05:00
advplyr f1582177e1 Merge pull request #3271 from faush01/feature/nobreak_in_stats
no line breaks in size text
2024-08-15 16:14:02 -05:00
advplyr d5712a564c Update client/pages/library/_library/stats.vue 2024-08-15 16:10:53 -05:00
advplyr 1c274862d8 Merge pull request #3288 from mikiher/fix-collapse-subseries
Fix: add back expand/collapse sub series in selected series page
2024-08-15 16:09:24 -05:00
advplyr 663c9e0fa9 Fix podcast filter user permissions query 2024-08-15 15:54:03 -05:00
mikiher bcb0bc75c9 Fix: add back expand/collapse sub series in selected series page 2024-08-15 10:27:02 +03:00
advplyr 603823d6ea Merge pull request #3278 from mikiher/revert-to-ffbinaries
Go back to downloading binaries from ffbinaries.com
2024-08-14 16:43:01 -05:00
advplyr 20c04d3ed3 Simplify Logger source 2024-08-14 16:36:10 -05:00
mikiher 02e5d608d0 Go back to downloading binaries from ffbinaries.com 2024-08-13 09:25:39 +03:00
advplyr e53ac6566b Update API JS docs 2024-08-11 17:01:25 -05:00
advplyr 2472b86284 Update:Express middleware sets req.user to new data model, openid permissions functions moved to new data model 2024-08-11 16:07:29 -05:00
advplyr 29a15858f4 Update ApiCacheManager unit test for userNew 2024-08-11 15:19:28 -05:00
advplyr afc16358ca Update more API endpoints to use new user model 2024-08-11 15:15:34 -05:00
advplyr 9facf77ff1 Update remove old sync local sessions endpoint & update MeController routes to use new user model 2024-08-11 13:09:53 -05:00
advplyr 1923854202 Update bookmarks API endpoints to use new user model 2024-08-11 12:16:45 -05:00
advplyr 9cd92c7b7f Update API media progress endpoints to use new user model. Merge book & episode endpoints 2024-08-11 11:53:30 -05:00
Shaun 8e0b723207 no line breaks in size text 2024-08-11 21:51:38 +10:00
advplyr 68ef3a07a7 Update controllers to use new user model 2024-08-10 17:15:21 -05:00
advplyr 202ceb02b5 Update:Auth to use new user model
- Express requests include userNew to start migrating API controllers to new user model
2024-08-10 15:46:04 -05:00
advplyr 59370cae81 Update:Docker source skip binary manager check #3266 2024-08-10 12:37:41 -05:00
advplyr 52a3bc224a Version bump v2.12.3 2024-08-09 16:59:19 -05:00
advplyr 54d67e5216 Merge pull request #3245 from ic1415/LibraryItemController
Update LibraryItemController.js
2024-08-09 16:48:30 -05:00
advplyr b55d8250cc Download log update 2024-08-09 16:48:21 -05:00
advplyr 3a1e9abd68 Revert unicode sqlite extension to fix db corruption #3241 2024-08-09 16:41:52 -05:00
advplyr c5ba40a178 Merge pull request #3262 from Vito0912/lang/add-year-in-review
lang/localization of "year in review"
2024-08-09 16:26:12 -05:00
Vito0912 f0c6dccadb Added max width 2024-08-09 19:24:43 +02:00
Vito0912 e701d1ab6a now? please 2024-08-09 18:59:20 +02:00
Vito0912 e10c8093c9 localization of year in review 2024-08-09 18:48:29 +02:00
ic1415 ef2d736b20 Update LibraryItemController.js
standardizing log messages for all download cases
2024-08-05 16:33:56 -04:00
ic1415 f3a453be20 Update LibraryItemController.js
Additional logging for single file downloads coming from download function
2024-08-05 16:19:28 -04:00
212 changed files with 7910 additions and 7961 deletions
+55
View File
@@ -0,0 +1,55 @@
name: Add issue comments by label
on:
issues:
types:
- labeled
jobs:
help-wanted:
if: github.event.label.name == 'help wanted'
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Help wanted comment
run: gh issue comment "$NUMBER" --body "$BODY"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
BODY: >
This issue is not able to be completed due to limited bandwidth or access to the required test hardware.
This issue is available for anyone to work on.
config-issue:
if: github.event.label.name == 'config-issue'
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Config issue comment
run: gh issue close "$NUMBER" --reason "not planned" --comment "$BODY"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
BODY: >
After reviewing this issue, this appears to be a problem with your setup and not Audiobookshelf. This issue is being closed to keep the issue tracker focused on Audiobookshelf itself. Please reach out on the Audiobookshelf Discord for community support.
Some common search terms to help you find the solution to your problem:
- Reverse proxy
- Enabling websockets
- SSL (https vs http)
- Configuring a static IP
- `localhost` versus IP address
- hairpin NAT
- VPN
- firewall ports
- public versus private network
- bridge versus host mode
- Docker networking
- DNS (such as EAI_AGAIN errors)
After you have followed these steps, please post the solution or steps you followed to fix the problem to help others in the future, or show that it is a problem with Audiobookshelf so we can reopen the issue.
+7 -5
View File
@@ -1,13 +1,15 @@
name: API linting name: API linting
# Run on pull requests or pushes when there is a change to the OpenAPI file # Run on pull requests or pushes when there is a change to any OpenAPI files in docs/
on: on:
pull_request:
push: push:
paths: paths:
- docs/ - 'docs/**'
pull_request:
paths: # This action only needs read permissions
- docs/ permissions:
contents: read
jobs: jobs:
build: build:
+1 -28
View File
@@ -2,14 +2,7 @@
font-family: 'Material Symbols Rounded'; font-family: 'Material Symbols Rounded';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url(~static/fonts/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].woff2) format('woff2'); src: url(~static/fonts/MaterialSymbolsRounded.woff2) format('woff2');
}
@font-face {
font-family: 'Material Symbols Outlined';
font-style: normal;
font-weight: 400;
src: url(~static/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2) format('woff2');
} }
.material-symbols { .material-symbols {
@@ -32,26 +25,6 @@
'FILL' 1 'FILL' 1
} }
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
font-weight: normal;
font-style: normal;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
vertical-align: top;
}
.material-symbols-outlined.fill {
font-variation-settings:
'FILL' 1
}
/* cyrillic-ext */ /* cyrillic-ext */
@font-face { @font-face {
font-family: 'Source Sans Pro'; font-family: 'Source Sans Pro';
+7 -7
View File
@@ -16,7 +16,7 @@
<div class="flex-grow" /> <div class="flex-grow" />
<ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center"> <ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
<span class="material-symbols-outlined text-2xl text-warning text-opacity-50"> cast </span> <span class="material-symbols text-2xl text-warning text-opacity-50"> cast </span>
</ui-tooltip> </ui-tooltip>
<div v-if="isChromecastInitialized" class="w-6 min-w-6 h-6 ml-2 mr-1 sm:mx-2 cursor-pointer"> <div v-if="isChromecastInitialized" class="w-6 min-w-6 h-6 ml-2 mr-1 sm:mx-2 cursor-pointer">
<google-cast-launcher></google-cast-launcher> <google-cast-launcher></google-cast-launcher>
@@ -26,19 +26,19 @@
<nuxt-link v-if="currentLibrary" to="/config/stats" class="hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1"> <nuxt-link v-if="currentLibrary" to="/config/stats" class="hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
<ui-tooltip :text="$strings.HeaderYourStats" direction="bottom" class="flex items-center"> <ui-tooltip :text="$strings.HeaderYourStats" direction="bottom" class="flex items-center">
<span class="material-symbols text-2xl" aria-label="User Stats" role="button">equalizer</span> <span class="material-symbols text-2xl" aria-label="User Stats" role="button">&#xe01d;</span>
</ui-tooltip> </ui-tooltip>
</nuxt-link> </nuxt-link>
<nuxt-link v-if="userCanUpload && currentLibrary" to="/upload" class="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1"> <nuxt-link v-if="userCanUpload && currentLibrary" to="/upload" class="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
<ui-tooltip :text="$strings.ButtonUpload" direction="bottom" class="flex items-center"> <ui-tooltip :text="$strings.ButtonUpload" direction="bottom" class="flex items-center">
<span class="material-symbols text-2xl" aria-label="Upload Media" role="button">upload</span> <span class="material-symbols text-2xl" aria-label="Upload Media" role="button">&#xf09b;</span>
</ui-tooltip> </ui-tooltip>
</nuxt-link> </nuxt-link>
<nuxt-link v-if="userIsAdminOrUp" to="/config" class="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1"> <nuxt-link v-if="userIsAdminOrUp" to="/config" class="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
<ui-tooltip :text="$strings.HeaderSettings" direction="bottom" class="flex items-center"> <ui-tooltip :text="$strings.HeaderSettings" direction="bottom" class="flex items-center">
<span class="material-symbols text-2xl" aria-label="System Settings" role="button">settings</span> <span class="material-symbols text-2xl" aria-label="System Settings" role="button">&#xe8b8;</span>
</ui-tooltip> </ui-tooltip>
</nuxt-link> </nuxt-link>
@@ -47,7 +47,7 @@
<span class="block truncate">{{ username }}</span> <span class="block truncate">{{ username }}</span>
</span> </span>
<span class="h-full md:ml-3 md:absolute inset-y-0 md:right-0 flex items-center justify-center md:pr-2 pointer-events-none"> <span class="h-full md:ml-3 md:absolute inset-y-0 md:right-0 flex items-center justify-center md:pr-2 pointer-events-none">
<span class="material-symbols text-xl text-gray-100">person</span> <span class="material-symbols text-xl text-gray-100">&#xe7fd;</span>
</span> </span>
</nuxt-link> </nuxt-link>
</div> </div>
@@ -332,13 +332,13 @@ export default {
libraryItemIds: this.selectedMediaItems.map((i) => i.id) libraryItemIds: this.selectedMediaItems.map((i) => i.id)
}) })
.then(() => { .then(() => {
this.$toast.success('Batch delete success') this.$toast.success(this.$strings.ToastBatchDeleteSuccess)
this.$store.commit('globals/resetSelectedMediaItems', []) this.$store.commit('globals/resetSelectedMediaItems', [])
this.$eventBus.$emit('bookshelf_clear_selection') this.$eventBus.$emit('bookshelf_clear_selection')
}) })
.catch((error) => { .catch((error) => {
console.error('Batch delete failed', error) console.error('Batch delete failed', error)
this.$toast.error('Batch delete failed') this.$toast.error(this.$strings.ToastBatchDeleteFailed)
}) })
.finally(() => { .finally(() => {
this.$store.commit('setProcessingBatch', false) this.$store.commit('setProcessingBatch', false)
+39 -8
View File
@@ -24,11 +24,11 @@
</nuxt-link> </nuxt-link>
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="flex-grow h-full flex justify-center items-center" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> <nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="flex-grow h-full flex justify-center items-center" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p v-if="isPlaylistsPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonPlaylists }}</p> <p v-if="isPlaylistsPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonPlaylists }}</p>
<span v-else class="material-symbols-outlined text-lg">queue_music</span> <span v-else class="material-symbols text-lg">&#xe03d;</span>
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p> <p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
<span v-else class="material-symbols-outlined text-lg">collections_bookmark</span> <span v-else class="material-symbols text-lg">&#xe431;</span>
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p> <p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
@@ -159,6 +159,7 @@ export default {
} }
this.addSubtitlesMenuItem(items) this.addSubtitlesMenuItem(items)
this.addCollapseSubSeriesMenuItem(items)
return items return items
}, },
@@ -371,6 +372,21 @@ export default {
} }
} }
}, },
addCollapseSubSeriesMenuItem(items) {
if (this.selectedSeries && this.isBookLibrary && !this.isBatchSelecting) {
if (this.settings.collapseBookSeries) {
items.push({
text: this.$strings.LabelExpandSubSeries,
action: 'expand-sub-series'
})
} else {
items.push({
text: this.$strings.LabelCollapseSubSeries,
action: 'collapse-sub-series'
})
}
}
},
handleSubtitlesAction(action) { handleSubtitlesAction(action) {
if (action === 'show-subtitles') { if (action === 'show-subtitles') {
this.settings.showSubtitles = true this.settings.showSubtitles = true
@@ -397,6 +413,19 @@ export default {
} }
return false return false
}, },
handleCollapseSubSeriesAction(action) {
if (action === 'collapse-sub-series') {
this.settings.collapseBookSeries = true
this.updateCollapseSubSeries()
return true
}
if (action === 'expand-sub-series') {
this.settings.collapseBookSeries = false
this.updateCollapseSubSeries()
return true
}
return false
},
contextMenuAction({ action }) { contextMenuAction({ action }) {
if (action === 'export-opml') { if (action === 'export-opml') {
this.exportOPML() this.exportOPML()
@@ -427,6 +456,8 @@ export default {
this.markSeriesFinished() this.markSeriesFinished()
} else if (this.handleSubtitlesAction(action)) { } else if (this.handleSubtitlesAction(action)) {
return return
} else if (this.handleCollapseSubSeriesAction(action)) {
return
} }
}, },
showOpenSeriesRSSFeed() { showOpenSeriesRSSFeed() {
@@ -442,11 +473,11 @@ export default {
this.$axios this.$axios
.$get(`/api/me/series/${this.seriesId}/readd-to-continue-listening`) .$get(`/api/me/series/${this.seriesId}/readd-to-continue-listening`)
.then(() => { .then(() => {
this.$toast.success('Series re-added to continue listening') this.$toast.success(this.$strings.ToastItemUpdateSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to re-add series to continue listening', error) console.error('Failed to re-add series to continue listening', error)
this.$toast.error('Failed to re-add series to continue listening') this.$toast.error(this.$strings.ToastItemUpdateFailed)
}) })
.finally(() => { .finally(() => {
this.processingSeries = false this.processingSeries = false
@@ -473,7 +504,7 @@ export default {
}) })
if (!response) { if (!response) {
console.error(`Author ${author.name} not found`) console.error(`Author ${author.name} not found`)
this.$toast.error(`Author ${author.name} not found`) this.$toast.error(this.$getString('ToastAuthorNotFound', [author.name]))
} else if (response.updated) { } else if (response.updated) {
if (response.author.imagePath) console.log(`Author ${response.author.name} was updated`) if (response.author.imagePath) console.log(`Author ${response.author.name} was updated`)
else console.log(`Author ${response.author.name} was updated (no image found)`) else console.log(`Author ${response.author.name} was updated (no image found)`)
@@ -491,13 +522,13 @@ export default {
this.$axios this.$axios
.$delete(`/api/libraries/${this.currentLibraryId}/issues`) .$delete(`/api/libraries/${this.currentLibraryId}/issues`)
.then(() => { .then(() => {
this.$toast.success('Removed library items with issues') this.$toast.success(this.$strings.ToastRemoveItemsWithIssuesSuccess)
this.$router.push(`/library/${this.currentLibraryId}/bookshelf`) this.$router.push(`/library/${this.currentLibraryId}/bookshelf`)
this.$store.dispatch('libraries/fetch', this.currentLibraryId) this.$store.dispatch('libraries/fetch', this.currentLibraryId)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove library items with issues', error) console.error('Failed to remove library items with issues', error)
this.$toast.error('Failed to remove library items with issues') this.$toast.error(this.$strings.ToastRemoveItemsWithIssuesFailed)
}) })
.finally(() => { .finally(() => {
this.processingIssues = false this.processingIssues = false
@@ -553,7 +584,7 @@ export default {
updateCollapseSeries() { updateCollapseSeries() {
this.saveSettings() this.saveSettings()
}, },
updateCollapseBookSeries() { updateCollapseSubSeries() {
this.saveSettings() this.saveSettings()
}, },
updateShowSubtitles() { updateShowSubtitles() {
+39 -1
View File
@@ -43,12 +43,14 @@
:sleep-timer-remaining="sleepTimerRemaining" :sleep-timer-remaining="sleepTimerRemaining"
:sleep-timer-type="sleepTimerType" :sleep-timer-type="sleepTimerType"
:is-podcast="isPodcast" :is-podcast="isPodcast"
:hasNextItemInQueue="hasNextItemInQueue"
@playPause="playPause" @playPause="playPause"
@jumpForward="jumpForward" @jumpForward="jumpForward"
@jumpBackward="jumpBackward" @jumpBackward="jumpBackward"
@setVolume="setVolume" @setVolume="setVolume"
@setPlaybackRate="setPlaybackRate" @setPlaybackRate="setPlaybackRate"
@seek="seek" @seek="seek"
@nextItemInQueue="playNextItemInQueue"
@close="closePlayer" @close="closePlayer"
@showBookmarks="showBookmarks" @showBookmarks="showBookmarks"
@showSleepTimer="showSleepTimerModal = true" @showSleepTimer="showSleepTimerModal = true"
@@ -60,7 +62,7 @@
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-type="sleepTimerType" :remaining="sleepTimerRemaining" :has-chapters="!!chapters.length" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" /> <modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-type="sleepTimerType" :remaining="sleepTimerRemaining" :has-chapters="!!chapters.length" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" :library-item-id="libraryItemId" /> <modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" />
<modals-player-settings-modal v-model="showPlayerSettingsModal" /> <modals-player-settings-modal v-model="showPlayerSettingsModal" />
</div> </div>
@@ -176,6 +178,16 @@ export default {
if (!this.isMusic) return null if (!this.isMusic) return null
return this.mediaMetadata.artists.join(', ') return this.mediaMetadata.artists.join(', ')
}, },
hasNextItemInQueue() {
return this.currentPlayerQueueIndex < this.playerQueueItems.length - 1
},
currentPlayerQueueIndex() {
if (!this.libraryItemId) return -1
return this.playerQueueItems.findIndex((i) => {
if (this.streamEpisode?.id) return i.episodeId === this.streamEpisode.id
return i.libraryItemId === this.libraryItemId
})
},
playerQueueItems() { playerQueueItems() {
return this.$store.state.playerQueueItems || [] return this.$store.state.playerQueueItems || []
} }
@@ -460,6 +472,30 @@ export default {
this.playerHandler.switchPlayer() this.playerHandler.switchPlayer()
} }
}, },
playNextItemInQueue() {
if (this.hasNextItemInQueue) {
this.playQueueItem({ index: this.currentPlayerQueueIndex + 1 })
}
},
/**
* @param {{ index: number }} payload
*/
playQueueItem(payload) {
if (payload?.index === undefined) {
console.error('playQueueItem: No index provided')
return
}
if (!this.playerQueueItems[payload.index]) {
console.error('playQueueItem: No item found at index', payload.index)
return
}
const item = this.playerQueueItems[payload.index]
this.playLibraryItem({
libraryItemId: item.libraryItemId,
episodeId: item.episodeId || null,
queueItems: this.playerQueueItems
})
},
async playLibraryItem(payload) { async playLibraryItem(payload) {
const libraryItemId = payload.libraryItemId const libraryItemId = payload.libraryItemId
const episodeId = payload.episodeId || null const episodeId = payload.episodeId || null
@@ -512,6 +548,7 @@ export default {
this.$eventBus.$on('cast-session-active', this.castSessionActive) this.$eventBus.$on('cast-session-active', this.castSessionActive)
this.$eventBus.$on('playback-seek', this.seek) this.$eventBus.$on('playback-seek', this.seek)
this.$eventBus.$on('playback-time-update', this.playbackTimeUpdate) this.$eventBus.$on('playback-time-update', this.playbackTimeUpdate)
this.$eventBus.$on('play-queue-item', this.playQueueItem)
this.$eventBus.$on('play-item', this.playLibraryItem) this.$eventBus.$on('play-item', this.playLibraryItem)
this.$eventBus.$on('pause-item', this.pauseItem) this.$eventBus.$on('pause-item', this.pauseItem)
}, },
@@ -519,6 +556,7 @@ export default {
this.$eventBus.$off('cast-session-active', this.castSessionActive) this.$eventBus.$off('cast-session-active', this.castSessionActive)
this.$eventBus.$off('playback-seek', this.seek) this.$eventBus.$off('playback-seek', this.seek)
this.$eventBus.$off('playback-time-update', this.playbackTimeUpdate) this.$eventBus.$off('playback-time-update', this.playbackTimeUpdate)
this.$eventBus.$off('play-queue-item', this.playQueueItem)
this.$eventBus.$off('play-item', this.playLibraryItem) this.$eventBus.$off('play-item', this.playLibraryItem)
this.$eventBus.$off('pause-item', this.pauseItem) this.$eventBus.$off('pause-item', this.pauseItem)
} }
+7 -7
View File
@@ -15,7 +15,7 @@
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" 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="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" 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="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-symbols text-2xl">format_list_bulleted</span> <span class="material-symbols text-2xl">&#xe241;</span>
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p> <p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p>
@@ -43,7 +43,7 @@
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-symbols-outlined text-2xl">collections_bookmark</span> <span class="material-symbols text-2xl">&#xe431;</span>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p> <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
@@ -51,7 +51,7 @@
</nuxt-link> </nuxt-link>
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-symbols text-2.5xl">queue_music</span> <span class="material-symbols text-2.5xl">&#xe03d;</span>
<p class="pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p> <p class="pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
@@ -72,7 +72,7 @@
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/narrators`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isNarratorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/narrators`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isNarratorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-symbols text-2xl">record_voice_over</span> <span class="material-symbols text-2xl">&#xe91f;</span>
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.LabelNarrators }}</p> <p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.LabelNarrators }}</p>
@@ -80,7 +80,7 @@
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isBookLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/stats`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isStatsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="isBookLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/stats`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isStatsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-symbols text-2xl">monitoring</span> <span class="material-symbols text-2xl">&#xf190;</span>
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonStats }}</p> <p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonStats }}</p>
@@ -96,7 +96,7 @@
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isMusicLibrary" :to="`/library/${currentLibraryId}/bookshelf/albums`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isMusicAlbumsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="isMusicLibrary" :to="`/library/${currentLibraryId}/bookshelf/albums`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isMusicAlbumsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-symbols-outlined text-xl">album</span> <span class="material-symbols text-xl">album</span>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">Albums</p> <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">Albums</p>
@@ -104,7 +104,7 @@
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-symbols text-2xl">file_download</span> <span class="material-symbols text-2xl">&#xf090;</span>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonDownloadQueue }}</p> <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonDownloadQueue }}</p>
+19 -36
View File
@@ -201,23 +201,6 @@ export default {
// This method returns immediately without waiting for the DOM to update // This method returns immediately without waiting for the DOM to update
return this.coverWidth return this.coverWidth
}, },
/*
cardHeight() {
// This method returns immediately without waiting for the DOM to update
return this.coverHeight + this.detailsHeight
},
detailsHeight() {
if (!this.isAlternativeBookshelfView) return 0
const lineHeight = 1.5
const remSize = 16
const baseHeight = this.sizeMultiplier * lineHeight * remSize
const titleHeight = 0.9 * baseHeight
const line2Height = 0.8 * baseHeight
const line3Height = this.displaySortLine ? 0.8 * baseHeight : 0
const marginHeight = 8 * 2 * this.sizeMultiplier // py-2
return titleHeight + line2Height + line3Height + marginHeight
},
*/
sizeMultiplier() { sizeMultiplier() {
return this.store.getters['user/getSizeMultiplier'] return this.store.getters['user/getSizeMultiplier']
}, },
@@ -363,14 +346,14 @@ export default {
}, },
displaySortLine() { displaySortLine() {
if (this.collapsedSeries) return null if (this.collapsedSeries) return null
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat) if (this.orderBy === 'mtimeMs') return this.$getString('LabelFileModifiedDate', [this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)])
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat) if (this.orderBy === 'birthtimeMs') return this.$getString('LabelFileBornDate', [this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)])
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt, this.dateFormat) if (this.orderBy === 'addedAt') return this.$getString('LabelAddedDate', [this.$formatDate(this._libraryItem.addedAt, this.dateFormat)])
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false) if (this.orderBy === 'media.duration') return this.$strings.LabelDuration + ': ' + this.$elapsedPrettyExtended(this.media.duration, false)
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size) if (this.orderBy === 'size') return this.$strings.LabelSize + ': ' + this.$bytesPretty(this._libraryItem.size)
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes` if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} ` + this.$strings.LabelEpisodes
if (this.orderBy === 'media.metadata.publishedYear') { if (this.orderBy === 'media.metadata.publishedYear') {
if (this.mediaMetadata.publishedYear) return 'Published ' + this.mediaMetadata.publishedYear if (this.mediaMetadata.publishedYear) return this.$getString('LabelPublishedDate', [this.mediaMetadata.publishedYear])
return '\u00A0' return '\u00A0'
} }
return null return null
@@ -727,7 +710,7 @@ export default {
toggleFinished(confirmed = false) { toggleFinished(confirmed = false) {
if (!this.itemIsFinished && this.userProgressPercent > 0 && !confirmed) { if (!this.itemIsFinished && this.userProgressPercent > 0 && !confirmed) {
const payload = { const payload = {
message: `Are you sure you want to mark "${this.displayTitle}" as finished?`, message: this.$getString('MessageConfirmMarkItemFinished', [this.displayTitle]),
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.toggleFinished(true) this.toggleFinished(true)
@@ -772,18 +755,18 @@ export default {
.then((data) => { .then((data) => {
var result = data.result var result = data.result
if (!result) { if (!result) {
this.$toast.error(`Re-Scan Failed for "${this.title}"`) this.$toast.error(this.$getString('ToastRescanFailed', [this.displayTitle]))
} else if (result === 'UPDATED') { } else if (result === 'UPDATED') {
this.$toast.success(`Re-Scan complete item was updated`) this.$toast.success(this.$strings.ToastRescanUpdated)
} else if (result === 'UPTODATE') { } else if (result === 'UPTODATE') {
this.$toast.success(`Re-Scan complete item was up to date`) this.$toast.success(this.$strings.ToastRescanUpToDate)
} else if (result === 'REMOVED') { } else if (result === 'REMOVED') {
this.$toast.error(`Re-Scan complete item was removed`) this.$toast.error(this.$strings.ToastRescanRemoved)
} }
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to scan library item', error) console.error('Failed to scan library item', error)
this.$toast.error('Failed to scan library item') this.$toast.error(this.$strings.ToastScanFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@@ -840,7 +823,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove series from home', error) console.error('Failed to remove series from home', error)
this.$toast.error('Failed to update user') this.$toast.error(this.$strings.ToastFailedToUpdateUser)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@@ -858,7 +841,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to hide item from home', error) console.error('Failed to hide item from home', error)
this.$toast.error('Failed to update user') this.$toast.error(this.$strings.ToastFailedToUpdateUser)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@@ -873,7 +856,7 @@ export default {
episodeId: this.recentEpisode.id, episodeId: this.recentEpisode.id,
title: this.recentEpisode.title, title: this.recentEpisode.title,
subtitle: this.mediaMetadata.title, subtitle: this.mediaMetadata.title,
caption: this.recentEpisode.publishedAt ? `Published ${this.$formatDate(this.recentEpisode.publishedAt, this.dateFormat)}` : 'Unknown publish date', caption: this.recentEpisode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(this.recentEpisode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
duration: this.recentEpisode.audioFile.duration || null, duration: this.recentEpisode.audioFile.duration || null,
coverPath: this.media.coverPath || null coverPath: this.media.coverPath || null
} }
@@ -923,11 +906,11 @@ export default {
axios axios
.$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`) .$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`)
.then(() => { .then(() => {
this.$toast.success('Item deleted') this.$toast.success(this.$strings.ToastItemDeletedSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to delete item', error) console.error('Failed to delete item', error)
this.$toast.error('Failed to delete item') this.$toast.error(this.$strings.ToastItemDeletedFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@@ -1033,7 +1016,7 @@ export default {
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: this.mediaMetadata.title, subtitle: this.mediaMetadata.title,
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
duration: episode.audioFile.duration || null, duration: episode.audioFile.duration || null,
coverPath: this.media.coverPath || null coverPath: this.media.coverPath || null
}) })
+1 -13
View File
@@ -57,23 +57,11 @@ export default {
return this.store.getters['libraries/getBookCoverAspectRatio'] return this.store.getters['libraries/getBookCoverAspectRatio']
}, },
cardWidth() { cardWidth() {
return this.width || this.coverHeight * 2 return this.width || (this.coverHeight / this.bookCoverAspectRatio) * 2
}, },
coverHeight() { coverHeight() {
return this.height * this.sizeMultiplier return this.height * this.sizeMultiplier
}, },
cardHeight() {
return this.coverHeight + this.bottomTextHeight
},
bottomTextHeight() {
if (!this.isAlternativeBookshelfView) return 0 // bottom text appears on top of the divider
const lineHeight = 1.5
const remSize = 16
const baseHeight = this.sizeMultiplier * lineHeight * remSize
const titleHeight = this.labelFontSize * baseHeight
const paddingHeight = 4 * 2 * this.sizeMultiplier // py-1
return titleHeight + paddingHeight
},
labelFontSize() { labelFontSize() {
if (this.width < 160) return 0.75 if (this.width < 160) return 0.75
return 0.9 return 0.9
+2 -2
View File
@@ -65,7 +65,7 @@ export default {
return this.store.getters['libraries/getBookCoverAspectRatio'] return this.store.getters['libraries/getBookCoverAspectRatio']
}, },
cardWidth() { cardWidth() {
return this.width || this.coverHeight * 2 return this.width || (this.coverHeight / this.bookCoverAspectRatio) * 2
}, },
coverHeight() { coverHeight() {
return this.height * this.sizeMultiplier return this.height * this.sizeMultiplier
@@ -96,7 +96,7 @@ export default {
displaySortLine() { displaySortLine() {
switch (this.orderBy) { switch (this.orderBy) {
case 'addedAt': case 'addedAt':
return `${this.$strings.LabelAdded} ${this.$formatDate(this.addedAt, this.dateFormat)}` return this.$getString('LabelAddedDate', [this.$formatDate(this.addedAt, this.dateFormat)])
case 'totalDuration': case 'totalDuration':
return `${this.$strings.LabelDuration} ${this.$elapsedPrettyExtended(this.totalDuration, false)}` return `${this.$strings.LabelDuration} ${this.$elapsedPrettyExtended(this.totalDuration, false)}`
case 'lastBookUpdated': case 'lastBookUpdated':
+1 -1
View File
@@ -3,7 +3,7 @@
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(name)}`"> <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(name)}`">
<div cy-id="card" :style="{ width: cardWidth + 'px', height: cardHeight + 'px', fontSize: sizeMultiplier + 'rem' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden"> <div cy-id="card" :style="{ width: cardWidth + 'px', height: cardHeight + 'px', fontSize: sizeMultiplier + 'rem' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
<div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-40"> <div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-40">
<span class="material-symbols-outlined text-[10em]">record_voice_over</span> <span class="material-symbols text-[10em]">&#xe91f;</span>
</div> </div>
<!-- Narrator name & num books overlay --> <!-- Narrator name & num books overlay -->
@@ -1,7 +1,7 @@
<template> <template>
<div class="flex h-full px-1 overflow-hidden"> <div class="flex h-full px-1 overflow-hidden">
<div class="w-10 h-10 flex items-center justify-center"> <div class="w-10 h-10 flex items-center justify-center">
<span class="material-symbols text-2xl text-gray-200">record_voice_over</span> <span class="material-symbols text-2xl text-gray-200">&#xe91f;</span>
</div> </div>
<div class="flex-grow px-2 narratorSearchCardContent h-full"> <div class="flex-grow px-2 narratorSearchCardContent h-full">
<p class="truncate text-sm">{{ narrator }}</p> <p class="truncate text-sm">{{ narrator }}</p>
+13 -15
View File
@@ -4,11 +4,11 @@
<p class="text-base md:text-lg font-semibold pr-4">{{ eventName }}</p> <p class="text-base md:text-lg font-semibold pr-4">{{ eventName }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" @click.stop="fireTestEventAndSucceed">Fire onTest Event</ui-btn> <ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" @click.stop="fireTestEventAndSucceed">{{ this.$strings.ButtonFireOnTest }}</ui-btn>
<ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" color="red-600" @click.stop="fireTestEventAndFail">Fire & Fail</ui-btn> <ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" color="red-600" @click.stop="fireTestEventAndFail">{{ this.$strings.ButtonFireAndFail }}</ui-btn>
<!-- <ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" @click.stop="rapidFireTestEvents">Rapid Fire</ui-btn> --> <!-- <ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" @click.stop="rapidFireTestEvents">Rapid Fire</ui-btn> -->
<ui-btn v-else-if="notification.enabled" :loading="sendingTest" small class="mr-2" @click.stop="sendTestClick">Test</ui-btn> <ui-btn v-else-if="notification.enabled" :loading="sendingTest" small class="mr-2" @click.stop="sendTestClick">{{ this.$strings.ButtonTest }}</ui-btn>
<ui-btn v-else :loading="enabling" small color="success" class="mr-2" @click="enableNotification">Enable</ui-btn> <ui-btn v-else :loading="enabling" small color="success" class="mr-2" @click="enableNotification">{{ this.$strings.ButtonEnable }}</ui-btn>
<ui-icon-btn :size="7" icon-font-size="1.1rem" icon="edit" class="mr-2" @click="editNotification" /> <ui-icon-btn :size="7" icon-font-size="1.1rem" icon="edit" class="mr-2" @click="editNotification" />
<ui-icon-btn bg-color="error" :size="7" icon-font-size="1.2rem" icon="delete" @click="deleteNotificationClick" /> <ui-icon-btn bg-color="error" :size="7" icon-font-size="1.2rem" icon="delete" @click="deleteNotificationClick" />
@@ -65,12 +65,12 @@ export default {
this.$axios this.$axios
.$get(`/api/notifications/test?fail=${intentionallyFail ? 1 : 0}`) .$get(`/api/notifications/test?fail=${intentionallyFail ? 1 : 0}`)
.then(() => { .then(() => {
this.$toast.success('Triggered onTest Event') this.$toast.success(this.$strings.ToastNotificationTestTriggerSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
const errorMsg = error.response ? error.response.data : null const errorMsg = error.response ? error.response.data : null
this.$toast.error(`Failed: ${errorMsg}` || 'Failed to trigger onTest event') this.$toast.error(`Failed: ${errorMsg}` || this.$strings.ToastNotificationTestTriggerFailed)
}) })
.finally(() => { .finally(() => {
this.testing = false this.testing = false
@@ -91,7 +91,7 @@ export default {
// End testing functions // End testing functions
sendTestClick() { sendTestClick() {
const payload = { const payload = {
message: `Trigger this notification with test data?`, message: this.$strings.MessageConfirmNotificationTestTrigger,
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.sendTest() this.sendTest()
@@ -106,12 +106,12 @@ export default {
this.$axios this.$axios
.$get(`/api/notifications/${this.notification.id}/test`) .$get(`/api/notifications/${this.notification.id}/test`)
.then(() => { .then(() => {
this.$toast.success('Triggered test notification') this.$toast.success(this.$strings.ToastNotificationTestTriggerSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
const errorMsg = error.response ? error.response.data : null const errorMsg = error.response ? error.response.data : null
this.$toast.error(`Failed: ${errorMsg}` || 'Failed to trigger test notification') this.$toast.error(`Failed: ${errorMsg}` || this.$strings.ToastNotificationTestTriggerFailed)
}) })
.finally(() => { .finally(() => {
this.sendingTest = false this.sendingTest = false
@@ -127,11 +127,10 @@ export default {
.$patch(`/api/notifications/${this.notification.id}`, payload) .$patch(`/api/notifications/${this.notification.id}`, payload)
.then((updatedSettings) => { .then((updatedSettings) => {
this.$emit('update', updatedSettings) this.$emit('update', updatedSettings)
this.$toast.success('Notification enabled')
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update notification', error) console.error('Failed to update notification', error)
this.$toast.error('Failed to update notification') this.$toast.error(this.$strings.ToastNotificationUpdateFailed)
}) })
.finally(() => { .finally(() => {
this.enabling = false this.enabling = false
@@ -139,7 +138,7 @@ export default {
}, },
deleteNotificationClick() { deleteNotificationClick() {
const payload = { const payload = {
message: `Are you sure you want to delete this notification?`, message: this.$strings.MessageConfirmDeleteNotification,
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.deleteNotification() this.deleteNotification()
@@ -155,11 +154,10 @@ export default {
.$delete(`/api/notifications/${this.notification.id}`) .$delete(`/api/notifications/${this.notification.id}`)
.then((updatedSettings) => { .then((updatedSettings) => {
this.$emit('update', updatedSettings) this.$emit('update', updatedSettings)
this.$toast.success('Deleted notification')
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
this.$toast.error('Failed to delete notification') this.$toast.error(this.$strings.ToastNotificationDeleteFailed)
}) })
.finally(() => { .finally(() => {
this.deleting = false this.deleting = false
@@ -171,4 +169,4 @@ export default {
}, },
mounted() {} mounted() {}
} }
</script> </script>
+2 -2
View File
@@ -5,7 +5,7 @@
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" /> <ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
</form> </form>
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear"> <div 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">search</span> <span v-if="!search" class="material-symbols" style="font-size: 1.2rem">&#xe8b6;</span>
<span v-else class="material-symbols" style="font-size: 1.2rem">close</span> <span v-else class="material-symbols" style="font-size: 1.2rem">close</span>
</div> </div>
</div> </div>
@@ -42,7 +42,7 @@
<p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelAuthors }}</p> <p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelAuthors }}</p>
<template v-for="item in authorResults"> <template v-for="item in authorResults">
<li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption"> <li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(item.id)}`"> <nuxt-link :to="`/author/${item.id}`">
<cards-author-search-card :author="item" /> <cards-author-search-card :author="item" />
</nuxt-link> </nuxt-link>
</li> </li>
+13 -11
View File
@@ -111,7 +111,7 @@
</div> </div>
<div class="flex pt-4 px-2"> <div class="flex pt-4 px-2">
<ui-btn v-if="hasOpenIDLink" small :loading="unlinkingFromOpenID" color="primary" type="button" class="mr-2" @click.stop="unlinkOpenID">Unlink OpenID</ui-btn> <ui-btn v-if="hasOpenIDLink" small :loading="unlinkingFromOpenID" color="primary" type="button" class="mr-2" @click.stop="unlinkOpenID">{{ $strings.ButtonUnlinkOpenId }}</ui-btn>
<ui-btn v-if="isEditingRoot" small class="flex items-center" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn> <ui-btn v-if="isEditingRoot" small class="flex items-center" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn> <ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
@@ -212,19 +212,19 @@ export default {
}, },
unlinkOpenID() { unlinkOpenID() {
const payload = { const payload = {
message: 'Are you sure you want to unlink this user from OpenID?', message: this.$strings.MessageConfirmUnlinkOpenId,
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.unlinkingFromOpenID = true this.unlinkingFromOpenID = true
this.$axios this.$axios
.$patch(`/api/users/${this.account.id}/openid-unlink`) .$patch(`/api/users/${this.account.id}/openid-unlink`)
.then(() => { .then(() => {
this.$toast.success('User unlinked from OpenID') this.$toast.success(this.$strings.ToastUnlinkOpenIdSuccess)
this.show = false this.show = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to unlink user from OpenID', error) console.error('Failed to unlink user from OpenID', error)
this.$toast.error('Failed to unlink user from OpenID') this.$toast.error(this.$strings.ToastUnlinkOpenIdFailed)
}) })
.finally(() => { .finally(() => {
this.unlinkingFromOpenID = false this.unlinkingFromOpenID = false
@@ -265,15 +265,15 @@ export default {
}, },
submitForm() { submitForm() {
if (!this.newUser.username) { if (!this.newUser.username) {
this.$toast.error('Enter a username') this.$toast.error(this.$strings.ToastNewUserUsernameError)
return return
} }
if (!this.newUser.permissions.accessAllLibraries && !this.newUser.librariesAccessible.length) { if (!this.newUser.permissions.accessAllLibraries && !this.newUser.librariesAccessible.length) {
this.$toast.error('Must select at least one library') this.$toast.error(this.$strings.ToastNewUserLibraryError)
return return
} }
if (!this.newUser.permissions.accessAllTags && !this.newUser.itemTagsSelected.length) { if (!this.newUser.permissions.accessAllTags && !this.newUser.itemTagsSelected.length) {
this.$toast.error('Must select at least one tag') this.$toast.error(this.$strings.ToastNewUserTagError)
return return
} }
@@ -313,12 +313,12 @@ export default {
this.processing = false this.processing = false
console.error('Failed to update account', error) console.error('Failed to update account', error)
var errMsg = error.response ? error.response.data || '' : '' var errMsg = error.response ? error.response.data || '' : ''
this.$toast.error(errMsg || 'Failed to update account') this.$toast.error(errMsg || this.$strings.ToastFailedToUpdateAccount)
}) })
}, },
submitCreateAccount() { submitCreateAccount() {
if (!this.newUser.password) { if (!this.newUser.password) {
this.$toast.error('Must have a password, only root user can have an empty password') this.$toast.error(this.$strings.ToastNewUserPasswordError)
return return
} }
@@ -329,9 +329,9 @@ export default {
.then((data) => { .then((data) => {
this.processing = false this.processing = false
if (data.error) { if (data.error) {
this.$toast.error(`Failed to create account: ${data.error}`) this.$toast.error(this.$strings.ToastNewUserCreatedFailed + ': ' + data.error)
} else { } else {
this.$toast.success('New account created') this.$toast.success(this.$strings.ToastNewUserCreatedSuccess)
this.show = false this.show = false
} }
}) })
@@ -351,6 +351,7 @@ export default {
update: type === 'admin', update: type === 'admin',
delete: type === 'admin', delete: type === 'admin',
upload: type === 'admin', upload: type === 'admin',
accessExplicitContent: true,
accessAllLibraries: true, accessAllLibraries: true,
accessAllTags: true, accessAllTags: true,
selectedTagsNotAccessible: false selectedTagsNotAccessible: false
@@ -385,6 +386,7 @@ export default {
upload: false, upload: false,
accessAllLibraries: true, accessAllLibraries: true,
accessAllTags: true, accessAllTags: true,
accessExplicitContent: true,
selectedTagsNotAccessible: false selectedTagsNotAccessible: false
}, },
librariesAccessible: [], librariesAccessible: [],
@@ -2,7 +2,7 @@
<modals-modal ref="modal" v-model="show" name="custom-metadata-provider" :width="600" :height="'unset'" :processing="processing"> <modals-modal ref="modal" v-model="show" name="custom-metadata-provider" :width="600" :height="'unset'" :processing="processing">
<template #outer> <template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden"> <div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
<p class="text-3xl text-white truncate">Add custom metadata provider</p> <p class="text-3xl text-white truncate">{{ $strings.HeaderAddCustomMetadataProvider }}</p>
</div> </div>
</template> </template>
<form @submit.prevent="submitForm"> <form @submit.prevent="submitForm">
@@ -20,7 +20,7 @@
<ui-text-input-with-label v-model="newUrl" label="URL" /> <ui-text-input-with-label v-model="newUrl" label="URL" />
</div> </div>
<div class="w-full mb-2 p-1"> <div class="w-full mb-2 p-1">
<ui-text-input-with-label v-model="newAuthHeaderValue" :label="'Authorization Header Value'" type="password" /> <ui-text-input-with-label v-model="newAuthHeaderValue" :label="$strings.LabelProviderAuthorizationValue" type="password" />
</div> </div>
<div class="flex px-1 pt-4"> <div class="flex px-1 pt-4">
<div class="flex-grow" /> <div class="flex-grow" />
@@ -67,7 +67,7 @@ export default {
methods: { methods: {
submitForm() { submitForm() {
if (!this.newName || !this.newUrl) { if (!this.newName || !this.newUrl) {
this.$toast.error('Must add name and url') this.$toast.error(this.$strings.ToastProviderNameAndUrlRequired)
return return
} }
@@ -81,13 +81,13 @@ export default {
}) })
.then((data) => { .then((data) => {
this.$emit('added', data.provider) this.$emit('added', data.provider)
this.$toast.success('New provider added') this.$toast.success(this.$strings.ToastProviderCreatedSuccess)
this.show = false this.show = false
}) })
.catch((error) => { .catch((error) => {
const errorMsg = error.response?.data || 'Unknown error' const errorMsg = error.response?.data || 'Unknown error'
console.error('Failed to add provider', error) console.error('Failed to add provider', error)
this.$toast.error('Failed to add provider: ' + errorMsg) this.$toast.error(this.$strings.ToastProviderCreatedFailed + ': ' + errorMsg)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@@ -4,7 +4,7 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<p class="text-base text-gray-200 truncate">{{ metadata.filename }}</p> <p class="text-base text-gray-200 truncate">{{ metadata.filename }}</p>
<ui-btn v-if="ffprobeData" small class="ml-2" @click="ffprobeData = null">{{ $strings.ButtonReset }}</ui-btn> <ui-btn v-if="ffprobeData" small class="ml-2" @click="ffprobeData = null">{{ $strings.ButtonReset }}</ui-btn>
<ui-btn v-else-if="userIsAdminOrUp" small :loading="probingFile" class="ml-2" @click="getFFProbeData">Probe Audio File</ui-btn> <ui-btn v-else-if="userIsAdminOrUp" small :loading="probingFile" class="ml-2" @click="getFFProbeData">{{ $strings.ButtonProbeAudioFile }}</ui-btn>
</div> </div>
<div class="w-full h-px bg-white bg-opacity-10 my-4" /> <div class="w-full h-px bg-white bg-opacity-10 my-4" />
@@ -159,7 +159,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to get ffprobe data', error) console.error('Failed to get ffprobe data', error)
this.$toast.error('FFProbe failed') this.$toast.error(this.$strings.ToastFailedToLoadData)
}) })
.finally(() => { .finally(() => {
this.probingFile = false this.probingFile = false
@@ -9,7 +9,7 @@
<widgets-cron-expression-builder ref="expressionBuilder" v-model="newCronExpression" @input="expressionUpdated" /> <widgets-cron-expression-builder ref="expressionBuilder" v-model="newCronExpression" @input="expressionUpdated" />
<div class="flex items-center justify-end"> <div class="flex items-center justify-end">
<ui-btn :disabled="!isUpdated" @click="submit">{{ isUpdated ? $strings.ButtonSave : $strings.MessageNoUpdateNecessary }}</ui-btn> <ui-btn :disabled="!isUpdated" @click="submit">{{ isUpdated ? $strings.ButtonSave : $strings.MessageNoUpdatesWereNecessary }}</ui-btn>
</div> </div>
</div> </div>
</modals-modal> </modals-modal>
+1 -1
View File
@@ -94,7 +94,7 @@ export default {
this.$toast.success(this.$strings.ToastBookmarkRemoveSuccess) this.$toast.success(this.$strings.ToastBookmarkRemoveSuccess)
}) })
.catch((error) => { .catch((error) => {
this.$toast.error(this.$strings.ToastBookmarkRemoveFailed) this.$toast.error(this.$strings.ToastRemoveFailed)
console.error(error) console.error(error)
}) })
this.show = false this.show = false
@@ -8,7 +8,7 @@
<div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop> <div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop>
<div class="flex"> <div class="flex">
<div class="flex-grow p-1 min-w-48 sm:min-w-64 md:min-w-80"> <div class="flex-grow p-1 min-w-48 sm:min-w-64 md:min-w-80">
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!isNewSeries" :label="$strings.LabelSeriesName" /> <ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!isNewSeries" :label="$strings.LabelSeriesName" @input="seriesNameInputHandler" />
</div> </div>
<div class="w-24 sm:w-28 md:w-40 p-1"> <div class="w-24 sm:w-28 md:w-40 p-1">
<ui-text-input-with-label ref="sequenceInput" v-model="selectedSeries.sequence" :label="$strings.LabelSequence" /> <ui-text-input-with-label ref="sequenceInput" v-model="selectedSeries.sequence" :label="$strings.LabelSequence" />
@@ -66,6 +66,11 @@ export default {
} }
}, },
methods: { methods: {
seriesNameInputHandler() {
if (this.$refs.sequenceInput) {
this.$refs.sequenceInput.setFocus()
}
},
setInputFocus() { setInputFocus() {
if (this.isNewSeries) { if (this.isNewSeries) {
// Focus on series input if new series // Focus on series input if new series
@@ -134,4 +139,4 @@ export default {
}, },
mounted() {} mounted() {}
} }
</script> </script>
@@ -100,7 +100,7 @@
<div class="flex items-center"> <div class="flex items-center">
<ui-btn v-if="!isOpenSession && !isMediaItemShareSession" small color="error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn> <ui-btn v-if="!isOpenSession && !isMediaItemShareSession" small color="error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn>
<ui-btn v-else-if="!isMediaItemShareSession" small color="error" @click.stop="closeSessionClick">Close Open Session</ui-btn> <ui-btn v-else-if="!isMediaItemShareSession" small color="error" @click.stop="closeSessionClick">{{ $strings.ButtonCloseSession }}</ui-btn>
</div> </div>
</div> </div>
</modals-modal> </modals-modal>
@@ -206,14 +206,13 @@ export default {
this.$axios this.$axios
.$post(`/api/session/${this._session.id}/close`) .$post(`/api/session/${this._session.id}/close`)
.then(() => { .then(() => {
this.$toast.success('Session closed')
this.show = false this.show = false
this.$emit('closedSession') this.$emit('closedSession')
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to close session', error) console.error('Failed to close session', error)
const errMsg = error.response?.data || '' const errMsg = error.response?.data || ''
this.$toast.error(errMsg || 'Failed to close open session') this.$toast.error(errMsg || this.$strings.ToastSessionCloseFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
+1 -1
View File
@@ -165,7 +165,7 @@ export default {
}, },
openShare() { openShare() {
if (!this.newShareSlug) { if (!this.newShareSlug) {
this.$toast.error('Slug is required') this.$toast.error(this.$strings.ToastSlugRequired)
return return
} }
const payload = { const payload = {
+1 -1
View File
@@ -15,7 +15,7 @@
</template> </template>
<form class="flex items-center justify-center px-6 py-3" @submit.prevent="submitCustomTime"> <form class="flex items-center justify-center px-6 py-3" @submit.prevent="submitCustomTime">
<ui-text-input v-model="customTime" type="number" step="any" min="0.1" :placeholder="$strings.LabelTimeInMinutes" class="w-48" /> <ui-text-input v-model="customTime" type="number" step="any" min="0.1" :placeholder="$strings.LabelTimeInMinutes" class="w-48" />
<ui-btn color="success" type="submit" :padding-x="0" class="h-9 w-12 flex items-center justify-center ml-1">Set</ui-btn> <ui-btn color="success" type="submit" :padding-x="0" class="h-9 w-18 flex items-center justify-center ml-1">{{ $strings.ButtonSubmit }}</ui-btn>
</form> </form>
</div> </div>
<div v-if="timerSet" class="w-full p-4"> <div v-if="timerSet" class="w-full p-4">
@@ -78,14 +78,13 @@ export default {
if (data.error) { if (data.error) {
this.$toast.error(data.error) this.$toast.error(data.error)
} else { } else {
this.$toast.success('Cover Uploaded')
this.resetCoverPreview() this.resetCoverPreview()
} }
this.processingUpload = false this.processingUpload = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
var errorMsg = error.response && error.response.data ? error.response.data : 'Unknown Error' var errorMsg = error.response && error.response.data ? error.response.data : this.$strings.ToastUnknownError
this.$toast.error(errorMsg) this.$toast.error(errorMsg)
this.processingUpload = false this.processingUpload = false
}) })
@@ -95,7 +94,7 @@ export default {
var success = await this.$axios.$post(`/api/${this.entity}/${this.entityId}/cover`, { url: this.imageUrl }).catch((error) => { var success = await this.$axios.$post(`/api/${this.entity}/${this.entityId}/cover`, { url: this.imageUrl }).catch((error) => {
console.error('Failed to download cover from url', error) console.error('Failed to download cover from url', error)
var errorMsg = error.response && error.response.data ? error.response.data : 'Unknown Error' var errorMsg = error.response && error.response.data ? error.response.data : this.$strings.ToastUnknownError
this.$toast.error(errorMsg) this.$toast.error(errorMsg)
return false return false
}) })
@@ -104,4 +103,4 @@ export default {
} }
} }
} }
</script> </script>
+14 -12
View File
@@ -116,12 +116,12 @@ export default {
this.$axios this.$axios
.$delete(`/api/authors/${this.authorId}`) .$delete(`/api/authors/${this.authorId}`)
.then(() => { .then(() => {
this.$toast.success('Author removed') this.$toast.success(this.$strings.ToastAuthorRemoveSuccess)
this.show = false this.show = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove author', error) console.error('Failed to remove author', error)
this.$toast.error('Failed to remove author') this.$toast.error(this.$strings.ToastRemoveFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@@ -141,7 +141,7 @@ export default {
} }
}) })
if (!Object.keys(updatePayload).length) { if (!Object.keys(updatePayload).length) {
this.$toast.info(this.$strings.MessageNoUpdateNecessary) this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
return return
} }
this.processing = true this.processing = true
@@ -158,7 +158,7 @@ export default {
} else if (result.merged) { } else if (result.merged) {
this.$toast.success(this.$strings.ToastAuthorUpdateMerged) this.$toast.success(this.$strings.ToastAuthorUpdateMerged)
this.show = false this.show = false
} else this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary) } else this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
} }
this.processing = false this.processing = false
}, },
@@ -174,7 +174,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
this.$toast.error(this.$strings.ToastAuthorImageRemoveFailed) this.$toast.error(this.$strings.ToastRemoveFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@@ -182,7 +182,7 @@ export default {
}, },
submitUploadCover() { submitUploadCover() {
if (!this.imageUrl?.startsWith('http:') && !this.imageUrl?.startsWith('https:')) { if (!this.imageUrl?.startsWith('http:') && !this.imageUrl?.startsWith('https:')) {
this.$toast.error('Invalid image url') this.$toast.error(this.$strings.ToastInvalidImageUrl)
return return
} }
@@ -194,14 +194,14 @@ export default {
.$post(`/api/authors/${this.authorId}/image`, updatePayload) .$post(`/api/authors/${this.authorId}/image`, updatePayload)
.then((data) => { .then((data) => {
this.imageUrl = '' this.imageUrl = ''
this.$toast.success('Author image updated') this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
this.authorCopy.updatedAt = data.author.updatedAt this.authorCopy.updatedAt = data.author.updatedAt
this.authorCopy.imagePath = data.author.imagePath this.authorCopy.imagePath = data.author.imagePath
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
this.$toast.error(error.response.data || 'Failed to remove author image') this.$toast.error(error.response.data || this.$strings.ToastRemoveFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@@ -209,7 +209,7 @@ export default {
}, },
async searchAuthor() { async searchAuthor() {
if (!this.authorCopy.name && !this.authorCopy.asin) { if (!this.authorCopy.name && !this.authorCopy.asin) {
this.$toast.error('Must enter an author name') this.$toast.error(this.$strings.ToastNameRequired)
return return
} }
this.processing = true this.processing = true
@@ -228,17 +228,19 @@ export default {
return null return null
}) })
if (!response) { if (!response) {
this.$toast.error('Author not found') this.$toast.error(this.$strings.ToastAuthorSearchNotFound)
} else if (response.updated) { } else if (response.updated) {
if (response.author.imagePath) { if (response.author.imagePath) {
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess) this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
} else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound) } else {
this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
}
this.authorCopy = { this.authorCopy = {
...response.author ...response.author
} }
} else { } else {
this.$toast.info('No updates were made for Author') this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
} }
this.processing = false this.processing = false
} }
@@ -143,7 +143,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove books from collection', error) console.error('Failed to remove books from collection', error)
this.$toast.error(this.$strings.ToastCollectionItemsRemoveFailed) this.$toast.error(this.$strings.ToastRemoveFailed)
this.processing = false this.processing = false
}) })
} else { } else {
@@ -157,7 +157,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove book from collection', error) console.error('Failed to remove book from collection', error)
this.$toast.error(this.$strings.ToastCollectionItemsRemoveFailed) this.$toast.error(this.$strings.ToastRemoveFailed)
this.processing = false this.processing = false
}) })
} }
@@ -172,12 +172,12 @@ export default {
.$post(`/api/collections/${collection.id}/batch/add`, { books: this.selectedBookIds }) .$post(`/api/collections/${collection.id}/batch/add`, { books: this.selectedBookIds })
.then((updatedCollection) => { .then((updatedCollection) => {
console.log(`Books added to collection`, updatedCollection) console.log(`Books added to collection`, updatedCollection)
this.$toast.success('Books added to collection') this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
this.processing = false this.processing = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to add books to collection', error) console.error('Failed to add books to collection', error)
this.$toast.error('Failed to add books to collection') this.$toast.error(this.$strings.ToastCollectionItemsAddFailed)
this.processing = false this.processing = false
}) })
} else { } else {
@@ -187,12 +187,12 @@ export default {
.$post(`/api/collections/${collection.id}/book`, { id: this.selectedLibraryItemId }) .$post(`/api/collections/${collection.id}/book`, { id: this.selectedLibraryItemId })
.then((updatedCollection) => { .then((updatedCollection) => {
console.log(`Book added to collection`, updatedCollection) console.log(`Book added to collection`, updatedCollection)
this.$toast.success('Book added to collection') this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
this.processing = false this.processing = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to add book to collection', error) console.error('Failed to add book to collection', error)
this.$toast.error('Failed to add book to collection') this.$toast.error(this.$strings.ToastCollectionItemsAddFailed)
this.processing = false this.processing = false
}) })
} }
@@ -221,7 +221,7 @@ export default {
.catch((error) => { .catch((error) => {
console.error('Failed to create collection', error) console.error('Failed to create collection', error)
var errMsg = error.response ? error.response.data || '' : '' var errMsg = error.response ? error.response.data || '' : ''
this.$toast.error(`Failed to create collection: ${errMsg}`) this.$toast.error(this.$strings.ToastCollectionCreateFailed + ': ' + errMsg)
this.processing = false this.processing = false
}) })
} }
@@ -106,7 +106,7 @@ export default {
.catch((error) => { .catch((error) => {
console.error('Failed to remove collection', error) console.error('Failed to remove collection', error)
this.processing = false this.processing = false
this.$toast.error(this.$strings.ToastCollectionRemoveFailed) this.$toast.error(this.$strings.ToastRemoveFailed)
}) })
} }
}, },
@@ -115,7 +115,7 @@ export default {
return return
} }
if (!this.newCollectionName) { if (!this.newCollectionName) {
return this.$toast.error('Collection must have a name') return this.$toast.error(this.$strings.ToastNameRequired)
} }
this.processing = true this.processing = true
@@ -125,12 +125,12 @@ export default {
this.$refs.ereaderEmailInput.blur() this.$refs.ereaderEmailInput.blur()
if (!this.newDevice.name?.trim() || !this.newDevice.email?.trim()) { if (!this.newDevice.name?.trim() || !this.newDevice.email?.trim()) {
this.$toast.error('Name and email required') this.$toast.error(this.$strings.ToastNameEmailRequired)
return return
} }
if (this.newDevice.availabilityOption === 'specificUsers' && !this.newDevice.users.length) { if (this.newDevice.availabilityOption === 'specificUsers' && !this.newDevice.users.length) {
this.$toast.error('Must select at least one user') this.$toast.error(this.$strings.ToastSelectAtLeastOneUser)
return return
} }
if (this.newDevice.availabilityOption !== 'specificUsers') { if (this.newDevice.availabilityOption !== 'specificUsers') {
@@ -142,14 +142,14 @@ export default {
if (!this.ereaderDevice) { if (!this.ereaderDevice) {
if (this.existingDevices.some((d) => d.name === this.newDevice.name)) { if (this.existingDevices.some((d) => d.name === this.newDevice.name)) {
this.$toast.error('Ereader device with that name already exists') this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
return return
} }
this.submitCreate() this.submitCreate()
} else { } else {
if (this.ereaderDevice.name !== this.newDevice.name && this.existingDevices.some((d) => d.name === this.newDevice.name)) { if (this.ereaderDevice.name !== this.newDevice.name && this.existingDevices.some((d) => d.name === this.newDevice.name)) {
this.$toast.error('Ereader device with that name already exists') this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
return return
} }
@@ -174,12 +174,11 @@ export default {
.$post(`/api/emails/ereader-devices`, payload) .$post(`/api/emails/ereader-devices`, payload)
.then((data) => { .then((data) => {
this.$emit('update', data.ereaderDevices) this.$emit('update', data.ereaderDevices)
this.$toast.success('Device updated')
this.show = false this.show = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update device', error) console.error('Failed to update device', error)
this.$toast.error('Failed to update device') this.$toast.error(this.$strings.ToastDeviceUpdateFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@@ -201,12 +200,11 @@ export default {
.$post('/api/emails/ereader-devices', payload) .$post('/api/emails/ereader-devices', payload)
.then((data) => { .then((data) => {
this.$emit('update', data.ereaderDevices || []) this.$emit('update', data.ereaderDevices || [])
this.$toast.success('Device added')
this.show = false this.show = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to add device', error) console.error('Failed to add device', error)
this.$toast.error('Failed to add device') this.$toast.error(this.$strings.ToastDeviceAddFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
+5 -10
View File
@@ -194,7 +194,6 @@ export default {
if (data.error) { if (data.error) {
this.$toast.error(data.error) this.$toast.error(data.error)
} else { } else {
this.$toast.success('Cover Uploaded')
this.resetCoverPreview() this.resetCoverPreview()
} }
this.processingUpload = false this.processingUpload = false
@@ -204,7 +203,7 @@ export default {
if (error.response && error.response.data) { if (error.response && error.response.data) {
this.$toast.error(error.response.data) this.$toast.error(error.response.data)
} else { } else {
this.$toast.error('Oops, something went wrong...') this.$toast.error(this.$strings.ToastUnknownError)
} }
this.processingUpload = false this.processingUpload = false
}) })
@@ -255,7 +254,7 @@ export default {
}, },
async updateCover(cover) { async updateCover(cover) {
if (!cover.startsWith('http:') && !cover.startsWith('https:')) { if (!cover.startsWith('http:') && !cover.startsWith('https:')) {
this.$toast.error('Invalid URL') this.$toast.error(this.$strings.ToastInvalidUrl)
return return
} }
@@ -264,11 +263,10 @@ export default {
.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover }) .$post(`/api/items/${this.libraryItemId}/cover`, { url: cover })
.then(() => { .then(() => {
this.imageUrl = '' this.imageUrl = ''
this.$toast.success('Update Successful')
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update cover', error) console.error('Failed to update cover', error)
this.$toast.error(error.response?.data || 'Failed to update cover') this.$toast.error(error.response?.data || this.$strings.ToastCoverUpdateFailed)
}) })
.finally(() => { .finally(() => {
this.isProcessing = false this.isProcessing = false
@@ -308,12 +306,9 @@ export default {
this.isProcessing = true this.isProcessing = true
this.$axios this.$axios
.$patch(`/api/items/${this.libraryItemId}/cover`, { cover: coverFile.metadata.path }) .$patch(`/api/items/${this.libraryItemId}/cover`, { cover: coverFile.metadata.path })
.then(() => {
this.$toast.success('Update Successful')
})
.catch((error) => { .catch((error) => {
console.error('Failed to set local cover', error) console.error('Failed to set local cover', error)
this.$toast.error(error.response?.data || 'Failed to set cover') this.$toast.error(error.response?.data || this.$strings.ToastCoverUpdateFailed)
}) })
.finally(() => { .finally(() => {
this.isProcessing = false this.isProcessing = false
@@ -321,4 +316,4 @@ export default {
} }
} }
} }
</script> </script>
+11 -11
View File
@@ -92,7 +92,7 @@ export default {
var { title, author } = this.$refs.itemDetailsEdit.getTitleAndAuthorName() var { title, author } = this.$refs.itemDetailsEdit.getTitleAndAuthorName()
if (!title) { if (!title) {
this.$toast.error('Must have a title for quick match') this.$toast.error(this.$strings.ToastTitleRequired)
return return
} }
this.quickMatching = true this.quickMatching = true
@@ -108,9 +108,9 @@ export default {
if (res.warning) { if (res.warning) {
this.$toast.warning(res.warning) this.$toast.warning(res.warning)
} else if (res.updated) { } else if (res.updated) {
this.$toast.success('Item details updated') this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
} else { } else {
this.$toast.info('No updates were made') this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
} }
}) })
.catch((error) => { .catch((error) => {
@@ -128,18 +128,18 @@ export default {
this.rescanning = false this.rescanning = false
var result = data.result var result = data.result
if (!result) { if (!result) {
this.$toast.error(`Re-Scan Failed for "${this.title}"`) this.$toast.error(this.$getString('ToastRescanFailed', [this.title]))
} else if (result === 'UPDATED') { } else if (result === 'UPDATED') {
this.$toast.success(`Re-Scan complete item was updated`) this.$toast.success(this.$strings.ToastRescanUpdated)
} else if (result === 'UPTODATE') { } else if (result === 'UPTODATE') {
this.$toast.success(`Re-Scan complete item was up to date`) this.$toast.success(this.$strings.ToastRescanUpToDate)
} else if (result === 'REMOVED') { } else if (result === 'REMOVED') {
this.$toast.error(`Re-Scan complete item was removed`) this.$toast.error(this.$strings.ToastRescanRemoved)
} }
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to scan library item', error) console.error('Failed to scan library item', error)
this.$toast.error('Failed to scan library item') this.$toast.error(this.$strings.ToastScanFailed)
this.rescanning = false this.rescanning = false
}) })
}, },
@@ -156,7 +156,7 @@ export default {
} }
var updatedDetails = this.$refs.itemDetailsEdit.getDetails() var updatedDetails = this.$refs.itemDetailsEdit.getDetails()
if (!updatedDetails.hasChanges) { if (!updatedDetails.hasChanges) {
this.$toast.info('No changes were made') this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
return false return false
} }
return this.updateDetails(updatedDetails) return this.updateDetails(updatedDetails)
@@ -170,7 +170,7 @@ export default {
this.isProcessing = false this.isProcessing = false
if (updateResult) { if (updateResult) {
if (updateResult.updated) { if (updateResult.updated) {
this.$toast.success('Item details updated') this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
return true return true
} else { } else {
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary) this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
@@ -217,4 +217,4 @@ export default {
height: calc(100% - 80px); height: calc(100% - 80px);
max-height: calc(100% - 80px); max-height: calc(100% - 80px);
} }
</style> </style>
+2 -2
View File
@@ -397,7 +397,7 @@ export default {
}, },
submitSearch() { submitSearch() {
if (!this.searchTitle) { if (!this.searchTitle) {
this.$toast.warning('Search title is required') this.$toast.warning(this.$strings.ToastTitleRequired)
return return
} }
this.persistProvider() this.persistProvider()
@@ -618,7 +618,7 @@ export default {
if (updateResult.updated) { if (updateResult.updated) {
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess) this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
} else { } else {
this.$toast.info(this.$strings.ToastItemDetailsUpdateUnneeded) this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
} }
this.clearSelectedMatch() this.clearSelectedMatch()
this.$emit('selectTab', 'details') this.$emit('selectTab', 'details')
@@ -163,7 +163,7 @@ export default {
this.isProcessing = false this.isProcessing = false
if (updateResult) { if (updateResult) {
if (updateResult.updated) { if (updateResult.updated) {
this.$toast.success('Item details updated') this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
return true return true
} else { } else {
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary) this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
@@ -156,7 +156,7 @@ export default {
}, },
validate() { validate() {
if (!this.libraryCopy.name) { if (!this.libraryCopy.name) {
this.$toast.error('Library must have a name') this.$toast.error(this.$strings.ToastNameRequired)
return false return false
} }
if (!this.libraryCopy.folders.length) { if (!this.libraryCopy.folders.length) {
@@ -205,7 +205,7 @@ export default {
submitUpdateLibrary() { submitUpdateLibrary() {
var newLibraryPayload = this.getLibraryUpdatePayload() var newLibraryPayload = this.getLibraryUpdatePayload()
if (!Object.keys(newLibraryPayload).length) { if (!Object.keys(newLibraryPayload).length) {
this.$toast.info('No updates are necessary') this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
return return
} }
@@ -264,4 +264,4 @@ export default {
.tab.tab-selected { .tab.tab-selected {
height: 41px; height: 41px;
} }
</style> </style>
@@ -162,7 +162,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to get filesystem paths', error) console.error('Failed to get filesystem paths', error)
this.$toast.error('Failed to get filesystem paths') this.$toast.error(this.$strings.ToastFailedToLoadData)
return [] return []
}) })
.finally(() => { .finally(() => {
@@ -78,4 +78,4 @@ export default {
}, },
mounted() {} mounted() {}
} }
</script> </script>
@@ -86,7 +86,7 @@ export default {
return this.selectedEventData && this.selectedEventData.requiresLibrary return this.selectedEventData && this.selectedEventData.requiresLibrary
}, },
title() { title() {
return this.isNew ? 'Create Notification' : 'Update Notification' return this.isNew ? this.$strings.HeaderNotificationCreate : this.$strings.HeaderNotificationUpdate
}, },
availableVariables() { availableVariables() {
return this.selectedEventData ? this.selectedEventData.variables || null : null return this.selectedEventData ? this.selectedEventData.variables || null : null
@@ -104,9 +104,9 @@ export default {
}, },
submitForm() { submitForm() {
this.$refs.urlsInput?.forceBlur() this.$refs.urlsInput?.forceBlur()
if (!this.newNotification.urls.length) { if (!this.newNotification.urls.length) {
this.$toast.error('Must enter an Apprise URL') this.$toast.error(this.$strings.ToastAppriseUrlRequired)
return return
} }
@@ -127,12 +127,12 @@ export default {
.$patch(`/api/notifications/${payload.id}`, payload) .$patch(`/api/notifications/${payload.id}`, payload)
.then((updatedSettings) => { .then((updatedSettings) => {
this.$emit('update', updatedSettings) this.$emit('update', updatedSettings)
this.$toast.success('Notification updated') this.$toast.success(this.$strings.ToastNotificationUpdateSuccess)
this.show = false this.show = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update notification', error) console.error('Failed to update notification', error)
this.$toast.error('Failed to update notification') this.$toast.error(this.$strings.ToastNotificationUpdateFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@@ -149,12 +149,11 @@ export default {
.$post('/api/notifications', payload) .$post('/api/notifications', payload)
.then((updatedSettings) => { .then((updatedSettings) => {
this.$emit('update', updatedSettings) this.$emit('update', updatedSettings)
this.$toast.success('Notification created')
this.show = false this.show = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to create notification', error) console.error('Failed to create notification', error)
this.$toast.error('Failed to create notification') this.$toast.error(this.$strings.ToastNotificationCreateFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@@ -13,7 +13,7 @@
<div class="flex-grow" /> <div class="flex-grow" />
<ui-checkbox v-model="playerQueueAutoPlay" label="Auto Play" medium checkbox-bg="primary" border-color="gray-600" label-class="pl-2 mb-px" /> <ui-checkbox v-model="playerQueueAutoPlay" label="Auto Play" medium checkbox-bg="primary" border-color="gray-600" label-class="pl-2 mb-px" />
</div> </div>
<modals-player-queue-item-row v-for="(item, index) in playerQueueItems" :key="index" :item="item" :index="index" @play="playItem" @remove="removeItem" /> <modals-player-queue-item-row v-for="(item, index) in playerQueueItems" :key="index" :item="item" :index="index" @play="playItem(index)" @remove="removeItem" />
</div> </div>
</div> </div>
</modals-modal> </modals-modal>
@@ -22,8 +22,7 @@
<script> <script>
export default { export default {
props: { props: {
value: Boolean, value: Boolean
libraryItemId: String
}, },
data() { data() {
return {} return {}
@@ -50,11 +49,9 @@ export default {
} }
}, },
methods: { methods: {
playItem(item) { playItem(index) {
this.$eventBus.$emit('play-item', { this.$eventBus.$emit('play-queue-item', {
libraryItemId: item.libraryItemId, index
episodeId: item.episodeId || null,
queueItems: this.playerQueueItems
}) })
this.show = false this.show = false
}, },
@@ -63,4 +60,4 @@ export default {
} }
} }
} }
</script> </script>
@@ -130,12 +130,12 @@ export default {
.$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects }) .$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects })
.then((updatedPlaylist) => { .then((updatedPlaylist) => {
console.log(`Items removed from playlist`, updatedPlaylist) console.log(`Items removed from playlist`, updatedPlaylist)
this.$toast.success('Playlist item(s) removed') this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
this.processing = false this.processing = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove items from playlist', error) console.error('Failed to remove items from playlist', error)
this.$toast.error('Failed to remove playlist item(s)') this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
this.processing = false this.processing = false
}) })
}, },
@@ -148,12 +148,12 @@ export default {
.$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects }) .$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects })
.then((updatedPlaylist) => { .then((updatedPlaylist) => {
console.log(`Items added to playlist`, updatedPlaylist) console.log(`Items added to playlist`, updatedPlaylist)
this.$toast.success('Items added to playlist') this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
this.processing = false this.processing = false
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to add items to playlist', error) console.error('Failed to add items to playlist', error)
this.$toast.error('Failed to add items to playlist') this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
this.processing = false this.processing = false
}) })
}, },
@@ -174,14 +174,14 @@ export default {
.$post('/api/playlists', newPlaylist) .$post('/api/playlists', newPlaylist)
.then((data) => { .then((data) => {
console.log('New playlist created', data) console.log('New playlist created', data)
this.$toast.success(`Playlist "${data.name}" created`) this.$toast.success(this.$strings.ToastPlaylistCreateSuccess + ': ' + data.name)
this.processing = false this.processing = false
this.newPlaylistName = '' this.newPlaylistName = ''
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to create playlist', error) console.error('Failed to create playlist', error)
var errMsg = error.response ? error.response.data || '' : '' var errMsg = error.response ? error.response.data || '' : ''
this.$toast.error(`Failed to create playlist: ${errMsg}`) this.$toast.error(this.$strings.ToastPlaylistCreateFailed + ': ' + errMsg)
this.processing = false this.processing = false
}) })
} }
@@ -86,7 +86,7 @@ export default {
.catch((error) => { .catch((error) => {
console.error('Failed to remove playlist', error) console.error('Failed to remove playlist', error)
this.processing = false this.processing = false
this.$toast.error(this.$strings.ToastPlaylistRemoveFailed) this.$toast.error(this.$strings.ToastRemoveFailed)
}) })
} }
}, },
@@ -95,7 +95,7 @@ export default {
return return
} }
if (!this.newPlaylistName) { if (!this.newPlaylistName) {
return this.$toast.error('Playlist must have a name') return this.$toast.error(this.$strings.ToastNameRequired)
} }
this.processing = true this.processing = true
@@ -142,7 +142,7 @@ export default {
const updatedDetails = this.getUpdatePayload() const updatedDetails = this.getUpdatePayload()
if (!Object.keys(updatedDetails).length) { if (!Object.keys(updatedDetails).length) {
this.$toast.info('No changes were made') this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
return false return false
} }
return this.updateDetails(updatedDetails) return this.updateDetails(updatedDetails)
@@ -105,7 +105,7 @@ export default {
} }
const updatePayload = this.getUpdatePayload(episodeData) const updatePayload = this.getUpdatePayload(episodeData)
if (!Object.keys(updatePayload).length) { if (!Object.keys(updatePayload).length) {
return this.$toast.info('No updates are necessary') return this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
} }
console.log('Episode update payload', updatePayload) console.log('Episode update payload', updatePayload)
@@ -126,7 +126,7 @@ export default {
}, },
submitForm() { submitForm() {
if (!this.episodeTitle || !this.episodeTitle.length) { if (!this.episodeTitle || !this.episodeTitle.length) {
this.$toast.error('Must enter an episode title') this.$toast.error(this.$strings.ToastTitleRequired)
return return
} }
this.searchedTitle = this.episodeTitle this.searchedTitle = this.episodeTitle
@@ -121,14 +121,14 @@ export default {
methods: { methods: {
openFeed() { openFeed() {
if (!this.newFeedSlug) { if (!this.newFeedSlug) {
this.$toast.error('Must set a feed slug') this.$toast.error(this.$strings.ToastSlugRequired)
return return
} }
const sanitized = this.$sanitizeSlug(this.newFeedSlug) const sanitized = this.$sanitizeSlug(this.newFeedSlug)
if (this.newFeedSlug !== sanitized) { if (this.newFeedSlug !== sanitized) {
this.newFeedSlug = sanitized this.newFeedSlug = sanitized
this.$toast.warning('Slug had to be modified - Run again') this.$toast.warning(this.$strings.ToastSlugMustChange)
return return
} }
@@ -20,8 +20,8 @@
<span class="material-symbols text-2xl sm:text-3xl">forward_media</span> <span class="material-symbols text-2xl sm:text-3xl">forward_media</span>
</button> </button>
</ui-tooltip> </ui-tooltip>
<ui-tooltip direction="top" :text="$strings.ButtonNextChapter" class="ml-4 lg:ml-8"> <ui-tooltip direction="top" :text="hasNextLabel" class="ml-4 lg:ml-8">
<button :aria-label="$strings.ButtonNextChapter" :disabled="!hasNextChapter" :class="hasNextChapter ? 'text-gray-300' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter"> <button :aria-label="hasNextLabel" :disabled="!hasNext" class="text-gray-300 disabled:text-gray-500" @mousedown.prevent @mouseup.prevent @click.stop="next">
<span class="material-symbols text-2xl sm:text-3xl">last_page</span> <span class="material-symbols text-2xl sm:text-3xl">last_page</span>
</button> </button>
</ui-tooltip> </ui-tooltip>
@@ -43,7 +43,8 @@ export default {
seekLoading: Boolean, seekLoading: Boolean,
playbackRate: Number, playbackRate: Number,
paused: Boolean, paused: Boolean,
hasNextChapter: Boolean hasNextChapter: Boolean,
hasNextItemInQueue: Boolean
}, },
data() { data() {
return {} return {}
@@ -62,6 +63,13 @@ export default {
}, },
jumpBackwardText() { jumpBackwardText() {
return this.getJumpText('jumpBackwardAmount', this.$strings.ButtonJumpBackward) return this.getJumpText('jumpBackwardAmount', this.$strings.ButtonJumpBackward)
},
hasNextLabel() {
if (this.hasNextItemInQueue && !this.hasNextChapter) return this.$strings.ButtonNextItemInQueue
return this.$strings.ButtonNextChapter
},
hasNext() {
return this.hasNextItemInQueue || this.hasNextChapter
} }
}, },
methods: { methods: {
@@ -71,9 +79,9 @@ export default {
prevChapter() { prevChapter() {
this.$emit('prevChapter') this.$emit('prevChapter')
}, },
nextChapter() { next() {
if (!this.hasNextChapter) return if (!this.hasNext) return
this.$emit('nextChapter') this.$emit('next')
}, },
jumpBackward() { jumpBackward() {
this.$emit('jumpBackward') this.$emit('jumpBackward')
+11 -7
View File
@@ -43,7 +43,7 @@
</ui-tooltip> </ui-tooltip>
</div> </div>
<player-playback-controls :loading="loading" :seek-loading="seekLoading" :playback-rate.sync="playbackRate" :paused="paused" :has-next-chapter="hasNextChapter" @prevChapter="prevChapter" @nextChapter="nextChapter" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setPlaybackRate="setPlaybackRate" @playPause="playPause" /> <player-playback-controls :loading="loading" :seek-loading="seekLoading" :playback-rate.sync="playbackRate" :paused="paused" :hasNextChapter="hasNextChapter" :hasNextItemInQueue="hasNextItemInQueue" @prevChapter="prevChapter" @next="goToNext" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setPlaybackRate="setPlaybackRate" @playPause="playPause" />
</div> </div>
<player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" :current-chapter="currentChapter" :playback-rate="playbackRate" @seek="seek" /> <player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" :current-chapter="currentChapter" :playback-rate="playbackRate" @seek="seek" />
@@ -82,7 +82,8 @@ export default {
sleepTimerType: String, sleepTimerType: String,
isPodcast: Boolean, isPodcast: Boolean,
hideBookmarks: Boolean, hideBookmarks: Boolean,
hideSleepTimer: Boolean hideSleepTimer: Boolean,
hasNextItemInQueue: Boolean
}, },
data() { data() {
return { return {
@@ -145,7 +146,7 @@ export default {
return Math.round((100 * time) / duration) return Math.round((100 * time) / duration)
}, },
currentChapterName() { currentChapterName() {
return this.currentChapter ? this.currentChapter.title : '' return this.currentChapter?.title || ''
}, },
currentChapterDuration() { currentChapterDuration() {
if (!this.currentChapter) return 0 if (!this.currentChapter) return 0
@@ -278,10 +279,13 @@ export default {
this.seek(this.currentChapter.start) this.seek(this.currentChapter.start)
} }
}, },
nextChapter() { goToNext() {
if (!this.currentChapter || !this.hasNextChapter) return if (this.hasNextChapter) {
var nextChapter = this.chapters[this.currentChapterIndex + 1] const nextChapter = this.chapters[this.currentChapterIndex + 1]
this.seek(nextChapter.start) this.seek(nextChapter.start)
} else if (this.hasNextItemInQueue) {
this.$emit('nextItemInQueue')
}
}, },
setStreamReady() { setStreamReady() {
if (this.$refs.trackbar) this.$refs.trackbar.setPercentageReady(1) if (this.$refs.trackbar) this.$refs.trackbar.setPercentageReady(1)
+3 -3
View File
@@ -29,7 +29,7 @@
</div> </div>
<div class="flex p-2"> <div class="flex p-2">
<span class="material-symbols-outlined text-5xl pt-1">insert_drive_file</span> <span class="material-symbols text-5xl pt-1">insert_drive_file</span>
<div class="px-1"> <div class="px-1">
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalSizeNum) }}</p> <p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalSizeNum) }}</p>
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelSize }} ({{ totalSizeMod }})</p> <p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelSize }} ({{ totalSizeMod }})</p>
@@ -37,7 +37,7 @@
</div> </div>
<div class="flex p-2"> <div class="flex p-2">
<span class="material-symbols-outlined text-5xl pt-1">audio_file</span> <span class="material-symbols text-5xl pt-1">audio_file</span>
<div class="px-1"> <div class="px-1">
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(numAudioTracks) }}</p> <p class="text-4.5xl leading-none font-bold">{{ $formatNumber(numAudioTracks) }}</p>
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAudioTracks }}</p> <p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAudioTracks }}</p>
@@ -103,4 +103,4 @@ export default {
methods: {}, methods: {},
mounted() {} mounted() {}
} }
</script> </script>
+17 -15
View File
@@ -73,7 +73,7 @@ export default {
const addIcon = (icon, color, fontSize, x, y) => { const addIcon = (icon, color, fontSize, x, y) => {
ctx.fillStyle = color ctx.fillStyle = color
ctx.font = `${fontSize} Material Symbols Outlined` ctx.font = `${fontSize} Material Symbols Rounded`
ctx.fillText(icon, x, y) ctx.fillText(icon, x, y)
} }
@@ -132,6 +132,8 @@ export default {
ctx.restore() ctx.restore()
} }
const twoColumnWidth = 210
ctx.globalAlpha = 1 ctx.globalAlpha = 1
ctx.textBaseline = 'middle' ctx.textBaseline = 'middle'
@@ -150,12 +152,12 @@ export default {
// Top text // Top text
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28) addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51) addText(`${this.year} ${this.$strings.StatsYearInReview}`, '18px', 'bold', 'white', '1px', 65, 51)
// Top left box // Top left box
createRoundedRect(50, 100, 340, 160) createRoundedRect(50, 100, 340, 160)
addText(this.yearStats.numBooksFinished, '64px', 'bold', 'white', '0px', 160, 165) addText(this.yearStats.numBooksFinished, '64px', 'bold', 'white', '0px', 160, 165)
addText('books finished', '28px', 'normal', tanColor, '0px', 160, 210) addText(this.$strings.StatsBooksFinished, '28px', 'normal', tanColor, '0px', 160, 210, twoColumnWidth)
const readIconPath = new Path2D() const readIconPath = new Path2D()
readIconPath.addPath(new Path2D('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'), { a: 2, d: 2, e: 100, f: 160 }) readIconPath.addPath(new Path2D('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'), { a: 2, d: 2, e: 100, f: 160 })
ctx.fillStyle = '#ffffff' ctx.fillStyle = '#ffffff'
@@ -164,40 +166,40 @@ export default {
// Box top right // Box top right
createRoundedRect(410, 100, 340, 160) createRoundedRect(410, 100, 340, 160)
addText(this.$elapsedPrettyExtended(this.yearStats.totalListeningTime, true, false), '40px', 'bold', 'white', '0px', 500, 165) addText(this.$elapsedPrettyExtended(this.yearStats.totalListeningTime, true, false), '40px', 'bold', 'white', '0px', 500, 165)
addText('spent listening', '28px', 'normal', tanColor, '0px', 500, 205) addText(this.$strings.StatsSpentListening, '28px', 'normal', tanColor, '0px', 500, 205, twoColumnWidth)
addIcon('watch_later', 'white', '52px', 440, 180) addIcon('watch_later', 'white', '52px', 440, 180)
// Box bottom left // Box bottom left
createRoundedRect(50, 280, 340, 160) createRoundedRect(50, 280, 340, 160)
addText(this.yearStats.totalListeningSessions, '64px', 'bold', 'white', '0px', 160, 345) addText(this.yearStats.totalListeningSessions, '64px', 'bold', 'white', '0px', 160, 345)
addText('sessions', '28px', 'normal', tanColor, '1px', 160, 390) addText(this.$strings.StatsSessions, '28px', 'normal', tanColor, '1px', 160, 390, twoColumnWidth)
addIcon('headphones', 'white', '52px', 95, 360) addIcon('headphones', 'white', '52px', 95, 360)
// Box bottom right // Box bottom right
createRoundedRect(410, 280, 340, 160) createRoundedRect(410, 280, 340, 160)
addText(this.yearStats.numBooksListened, '64px', 'bold', 'white', '0px', 500, 345) addText(this.yearStats.numBooksListened, '64px', 'bold', 'white', '0px', 500, 345)
addText('books listened to', '28px', 'normal', tanColor, '0px', 500, 390) addText(this.$strings.StatsBooksListenedTo, '28px', 'normal', tanColor, '0px', 500, 390, twoColumnWidth)
addIcon('local_library', 'white', '52px', 440, 360) addIcon('local_library', 'white', '52px', 440, 360)
if (!this.variant) { if (!this.variant) {
// Text stats // Text stats
const topNarrator = this.yearStats.mostListenedNarrator const topNarrator = this.yearStats.mostListenedNarrator
if (topNarrator) { if (topNarrator) {
addText('TOP NARRATOR', '24px', 'normal', tanColor, '1px', 70, 520) addText(this.$strings.StatsTopNarrator, '24px', 'normal', tanColor, '1px', 70, 520, 330)
addText(topNarrator.name, '36px', 'bolder', 'white', '0px', 70, 564, 330) addText(topNarrator.name, '36px', 'bolder', 'white', '0px', 70, 564, 330)
addText(this.$elapsedPrettyExtended(topNarrator.time, true, false), '24px', 'lighter', 'white', '1px', 70, 599) addText(this.$elapsedPrettyExtended(topNarrator.time, true, false), '24px', 'lighter', 'white', '1px', 70, 599)
} }
const topGenre = this.yearStats.topGenres[0] const topGenre = this.yearStats.topGenres[0]
if (topGenre) { if (topGenre) {
addText('TOP GENRE', '24px', 'normal', tanColor, '1px', 430, 520) addText(this.$strings.StatsTopGenre, '24px', 'normal', tanColor, '1px', 430, 520, 330)
addText(topGenre.genre, '36px', 'bolder', 'white', '0px', 430, 564, 330) addText(topGenre.genre, '36px', 'bolder', 'white', '0px', 430, 564, 330)
addText(this.$elapsedPrettyExtended(topGenre.time, true, false), '24px', 'lighter', 'white', '1px', 430, 599) addText(this.$elapsedPrettyExtended(topGenre.time, true, false), '24px', 'lighter', 'white', '1px', 430, 599)
} }
const topAuthor = this.yearStats.topAuthors[0] const topAuthor = this.yearStats.topAuthors[0]
if (topAuthor) { if (topAuthor) {
addText('TOP AUTHOR', '24px', 'normal', tanColor, '1px', 70, 670) addText(this.$strings.StatsTopAuthor, '24px', 'normal', tanColor, '1px', 70, 670, 330)
addText(topAuthor.name, '36px', 'bolder', 'white', '0px', 70, 714, 330) addText(topAuthor.name, '36px', 'bolder', 'white', '0px', 70, 714, 330)
addText(this.$elapsedPrettyExtended(topAuthor.time, true, false), '24px', 'lighter', 'white', '1px', 70, 749) addText(this.$elapsedPrettyExtended(topAuthor.time, true, false), '24px', 'lighter', 'white', '1px', 70, 749)
} }
@@ -205,7 +207,7 @@ export default {
if (this.yearStats.mostListenedMonth?.time) { if (this.yearStats.mostListenedMonth?.time) {
const jsdate = new Date(this.year, this.yearStats.mostListenedMonth.month, 1) const jsdate = new Date(this.year, this.yearStats.mostListenedMonth.month, 1)
const monthName = this.$formatJsDate(jsdate, 'LLLL') const monthName = this.$formatJsDate(jsdate, 'LLLL')
addText('TOP MONTH', '24px', 'normal', tanColor, '1px', 430, 670) addText(this.$strings.StatsTopMonth, '24px', 'normal', tanColor, '1px', 430, 670, 330)
addText(monthName, '36px', 'bolder', 'white', '0px', 430, 714, 330) addText(monthName, '36px', 'bolder', 'white', '0px', 430, 714, 330)
addText(this.$elapsedPrettyExtended(this.yearStats.mostListenedMonth.time, true, false), '24px', 'lighter', 'white', '1px', 430, 749) addText(this.$elapsedPrettyExtended(this.yearStats.mostListenedMonth.time, true, false), '24px', 'lighter', 'white', '1px', 430, 749)
} }
@@ -214,7 +216,7 @@ export default {
finishedBookCoverImgs = Object.values(finishedBookCoverImgs) finishedBookCoverImgs = Object.values(finishedBookCoverImgs)
if (finishedBookCoverImgs.length > 0) { if (finishedBookCoverImgs.length > 0) {
ctx.textAlign = 'center' ctx.textAlign = 'center'
addText('Some books finished this year...', '28px', 'normal', tanColor, '0px', canvas.width / 2, 530) addText(this.$strings.StatsBooksFinishedThisYear, '28px', 'normal', tanColor, '0px', canvas.width / 2, 530)
for (let i = 0; i < Math.min(5, finishedBookCoverImgs.length); i++) { for (let i = 0; i < Math.min(5, finishedBookCoverImgs.length); i++) {
let imgToAdd = finishedBookCoverImgs[i] let imgToAdd = finishedBookCoverImgs[i]
@@ -224,14 +226,14 @@ export default {
} else if (this.variant === 2) { } else if (this.variant === 2) {
// Text stats // Text stats
if (this.yearStats.topAuthors.length) { if (this.yearStats.topAuthors.length) {
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 524) addText(this.$strings.StatsTopAuthors, '24px', 'normal', tanColor, '1px', 70, 524)
for (let i = 0; i < this.yearStats.topAuthors.length; i++) { for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 584 + i * 60, 330) addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 584 + i * 60, 330)
} }
} }
if (this.yearStats.topGenres.length) { if (this.yearStats.topGenres.length) {
addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 524) addText(this.$strings.StatsTopGenres, '24px', 'normal', tanColor, '1px', 430, 524)
for (let i = 0; i < this.yearStats.topGenres.length; i++) { for (let i = 0; i < this.yearStats.topGenres.length; i++) {
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 584 + i * 60, 330) addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 584 + i * 60, 330)
} }
@@ -259,11 +261,11 @@ export default {
.catch((error) => { .catch((error) => {
console.error('Failed to share', error) console.error('Failed to share', error)
if (error.name !== 'AbortError') { if (error.name !== 'AbortError') {
this.$toast.error('Failed to share: ' + error.message) this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
} }
}) })
} else { } else {
this.$toast.error('Cannot share natively on this device') this.$toast.error(this.$strings.ToastErrorCannotShare)
} }
}) })
}, },
@@ -2,7 +2,7 @@
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-1 sm:p-4 mb-4"> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-1 sm:p-4 mb-4">
<!-- hack to get icon fonts loaded on init --> <!-- hack to get icon fonts loaded on init -->
<div class="h-0 w-0 overflow-hidden opacity-0"> <div class="h-0 w-0 overflow-hidden opacity-0">
<span class="material-symbols-outlined">close</span> <span class="material-symbols">close</span>
<span class="abs-icons icon-audiobookshelf" /> <span class="abs-icons icon-audiobookshelf" />
</div> </div>
@@ -38,7 +38,7 @@
<!-- next button --> <!-- next button -->
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++"> <ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span> <span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
<span class="material-symbols-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span> <span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn> </ui-btn>
</div> </div>
<stats-year-in-review ref="yearInReview" :variant="yearInReviewVariant" :year="yearInReviewYear" :processing.sync="processingYearInReview" /> <stats-year-in-review ref="yearInReview" :variant="yearInReviewVariant" :year="yearInReviewYear" :processing.sync="processingYearInReview" />
@@ -74,7 +74,7 @@
<!-- next button --> <!-- next button -->
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++"> <ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span> <span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
<span class="material-symbols-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span> <span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn> </ui-btn>
</div> </div>
</div> </div>
@@ -138,4 +138,4 @@ export default {
} }
} }
} }
</script> </script>
+15 -13
View File
@@ -123,6 +123,8 @@ export default {
ctx.restore() ctx.restore()
} }
const threeColumnTextWidth = 200
ctx.globalAlpha = 1 ctx.globalAlpha = 1
ctx.textBaseline = 'middle' ctx.textBaseline = 'middle'
@@ -141,33 +143,33 @@ export default {
// Top text // Top text
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28) addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51) addText(`${this.year} ${this.$strings.StatsYearInReview}`, '18px', 'bold', 'white', '1px', 65, 51)
// Top left box // Top left box
createRoundedRect(40, 100, 230, 100) createRoundedRect(40, 100, 230, 100)
ctx.textAlign = 'center' ctx.textAlign = 'center'
addText(this.yearStats.numBooksAdded, '48px', 'bold', 'white', '0px', 155, 140) addText(this.yearStats.numBooksAdded, '48px', 'bold', 'white', '0px', 155, 140)
addText('books added', '18px', 'normal', tanColor, '0px', 155, 170) addText(this.$strings.StatsBooksAdded, '18px', 'normal', tanColor, '0px', 155, 170, threeColumnTextWidth)
// Box top right // Box top right
createRoundedRect(285, 100, 230, 100) createRoundedRect(285, 100, 230, 100)
addText(this.yearStats.numAuthorsAdded, '48px', 'bold', 'white', '0px', 400, 140) addText(this.yearStats.numAuthorsAdded, '48px', 'bold', 'white', '0px', 400, 140)
addText('authors added', '18px', 'normal', tanColor, '0px', 400, 170) addText(this.$strings.StatsAuthorsAdded, '18px', 'normal', tanColor, '0px', 400, 170, threeColumnTextWidth)
// Box bottom left // Box bottom left
createRoundedRect(530, 100, 230, 100) createRoundedRect(530, 100, 230, 100)
addText(this.yearStats.numListeningSessions, '48px', 'bold', 'white', '0px', 645, 140) addText(this.yearStats.numListeningSessions, '48px', 'bold', 'white', '0px', 645, 140)
addText('sessions', '18px', 'normal', tanColor, '1px', 645, 170) addText(this.$strings.StatsSessions, '18px', 'normal', tanColor, '1px', 645, 170, threeColumnTextWidth)
// Text stats // Text stats
if (this.yearStats.totalBooksAddedSize) { if (this.yearStats.totalBooksAddedSize) {
addText('Your book collection grew to...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 260) addText(this.$strings.StatsCollectionGrewTo, '24px', 'normal', tanColor, '0px', canvas.width / 2, 260)
addText(this.$bytesPretty(this.yearStats.totalBooksSize), '36px', 'bolder', 'white', '0px', canvas.width / 2, 300) addText(this.$bytesPretty(this.yearStats.totalBooksSize), '36px', 'bolder', 'white', '0px', canvas.width / 2, 300)
addText('+' + this.$bytesPretty(this.yearStats.totalBooksAddedSize), '20px', 'lighter', 'white', '0px', canvas.width / 2, 330) addText('+' + this.$bytesPretty(this.yearStats.totalBooksAddedSize), '20px', 'lighter', 'white', '0px', canvas.width / 2, 330)
} }
if (this.yearStats.totalBooksAddedDuration) { if (this.yearStats.totalBooksAddedDuration) {
addText('With a total duration of...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 400) addText(this.$strings.StatsTotalDuration, '24px', 'normal', tanColor, '0px', canvas.width / 2, 400)
addText(this.$elapsedPrettyExtended(this.yearStats.totalBooksDuration, true, false), '36px', 'bolder', 'white', '0px', canvas.width / 2, 440) addText(this.$elapsedPrettyExtended(this.yearStats.totalBooksDuration, true, false), '36px', 'bolder', 'white', '0px', canvas.width / 2, 440)
addText('+' + this.$elapsedPrettyExtended(this.yearStats.totalBooksAddedDuration, true, false), '20px', 'lighter', 'white', '0px', canvas.width / 2, 470) addText('+' + this.$elapsedPrettyExtended(this.yearStats.totalBooksAddedDuration, true, false), '20px', 'lighter', 'white', '0px', canvas.width / 2, 470)
} }
@@ -176,7 +178,7 @@ export default {
// Bottom images // Bottom images
imgsToAdd = Object.values(imgsToAdd) imgsToAdd = Object.values(imgsToAdd)
if (imgsToAdd.length > 0) { if (imgsToAdd.length > 0) {
addText('Some additions include...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 540) addText(this.$strings.StatsBooksAdditional, '24px', 'normal', tanColor, '0px', canvas.width / 2, 540)
for (let i = 0; i < Math.min(5, imgsToAdd.length); i++) { for (let i = 0; i < Math.min(5, imgsToAdd.length); i++) {
let imgToAdd = imgsToAdd[i] let imgToAdd = imgsToAdd[i]
@@ -187,14 +189,14 @@ export default {
// Text stats // Text stats
ctx.textAlign = 'left' ctx.textAlign = 'left'
if (this.yearStats.topAuthors.length) { if (this.yearStats.topAuthors.length) {
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549) addText(this.$strings.StatsTopAuthors, '24px', 'normal', tanColor, '1px', 70, 549, 330)
for (let i = 0; i < this.yearStats.topAuthors.length; i++) { for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330) addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
} }
} }
if (this.yearStats.topNarrators.length) { if (this.yearStats.topNarrators.length) {
addText('TOP NARRATORS', '24px', 'normal', tanColor, '1px', 430, 549) addText(this.$strings.StatsTopNarrators, '24px', 'normal', tanColor, '1px', 430, 549)
for (let i = 0; i < this.yearStats.topNarrators.length; i++) { for (let i = 0; i < this.yearStats.topNarrators.length; i++) {
addText(this.yearStats.topNarrators[i].name, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330) addText(this.yearStats.topNarrators[i].name, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
} }
@@ -203,14 +205,14 @@ export default {
// Text stats // Text stats
ctx.textAlign = 'left' ctx.textAlign = 'left'
if (this.yearStats.topAuthors.length) { if (this.yearStats.topAuthors.length) {
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549) addText(this.$strings.StatsTopAuthors, '24px', 'normal', tanColor, '1px', 70, 549, 330)
for (let i = 0; i < this.yearStats.topAuthors.length; i++) { for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330) addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
} }
} }
if (this.yearStats.topGenres.length) { if (this.yearStats.topGenres.length) {
addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 549) addText(this.$strings.StatsTopGenres, '24px', 'normal', tanColor, '1px', 430, 549)
for (let i = 0; i < this.yearStats.topGenres.length; i++) { for (let i = 0; i < this.yearStats.topGenres.length; i++) {
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330) addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
} }
@@ -235,11 +237,11 @@ export default {
.catch((error) => { .catch((error) => {
console.error('Failed to share', error) console.error('Failed to share', error)
if (error.name !== 'AbortError') { if (error.name !== 'AbortError') {
this.$toast.error('Failed to share: ' + error.message) this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
} }
}) })
} else { } else {
this.$toast.error('Cannot share natively on this device') this.$toast.error(this.$strings.ToastErrorCannotShare)
} }
}) })
}, },
@@ -64,7 +64,7 @@ export default {
const addIcon = (icon, color, fontSize, x, y) => { const addIcon = (icon, color, fontSize, x, y) => {
ctx.fillStyle = color ctx.fillStyle = color
ctx.font = `${fontSize} Material Symbols Outlined` ctx.font = `${fontSize} Material Symbols Rounded`
ctx.fillText(icon, x, y) ctx.fillText(icon, x, y)
} }
@@ -113,6 +113,8 @@ export default {
ctx.restore() ctx.restore()
} }
const twoColumnWidth = 180
ctx.globalAlpha = 1 ctx.globalAlpha = 1
ctx.textBaseline = 'middle' ctx.textBaseline = 'middle'
@@ -131,12 +133,12 @@ export default {
// Top text // Top text
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28) addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51) addText(`${this.year} ${this.$strings.StatsYearInReview}`, '18px', 'bold', 'white', '1px', 65, 51)
// Top left box // Top left box
createRoundedRect(15, 75, 280, 110) createRoundedRect(15, 75, 280, 110)
addText(this.yearStats.numBooksFinished, '48px', 'bold', 'white', '0px', 105, 120) addText(this.yearStats.numBooksFinished, '48px', 'bold', 'white', '0px', 105, 120)
addText('books finished', '20px', 'normal', tanColor, '0px', 105, 155) addText(this.$strings.StatsBooksFinished, '20px', 'normal', tanColor, '0px', 105, 155, twoColumnWidth)
const readIconPath = new Path2D() const readIconPath = new Path2D()
readIconPath.addPath(new Path2D('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'), { a: 1.5, d: 1.5, e: 55, f: 115 }) readIconPath.addPath(new Path2D('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'), { a: 1.5, d: 1.5, e: 55, f: 115 })
ctx.fillStyle = '#ffffff' ctx.fillStyle = '#ffffff'
@@ -144,7 +146,7 @@ export default {
createRoundedRect(305, 75, 280, 110) createRoundedRect(305, 75, 280, 110)
addText(this.yearStats.numBooksListened, '48px', 'bold', 'white', '0px', 400, 120) addText(this.yearStats.numBooksListened, '48px', 'bold', 'white', '0px', 400, 120)
addText('books listened to', '20px', 'normal', tanColor, '0px', 400, 155) addText(this.$strings.StatsBooksListenedTo, '20px', 'normal', tanColor, '0px', 400, 155, twoColumnWidth)
addIcon('local_library', 'white', '42px', 345, 130) addIcon('local_library', 'white', '42px', 345, 130)
this.canvas = canvas this.canvas = canvas
@@ -165,11 +167,11 @@ export default {
.catch((error) => { .catch((error) => {
console.error('Failed to share', error) console.error('Failed to share', error)
if (error.name !== 'AbortError') { if (error.name !== 'AbortError') {
this.$toast.error('Failed to share: ' + error.message) this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
} }
}) })
} else { } else {
this.$toast.error('Cannot share natively on this device') this.$toast.error(this.$strings.ToastErrorCannotShare)
} }
}) })
}, },
+2 -2
View File
@@ -23,7 +23,7 @@
<div class="w-full flex flex-row items-center justify-center"> <div class="w-full flex flex-row items-center justify-center">
<ui-btn v-if="backup.serverVersion && backup.key" small color="primary" @click="applyBackup(backup)">{{ $strings.ButtonRestore }}</ui-btn> <ui-btn v-if="backup.serverVersion && backup.key" small color="primary" @click="applyBackup(backup)">{{ $strings.ButtonRestore }}</ui-btn>
<ui-tooltip v-else text="This backup was created with an old version of audiobookshelf no longer supported" direction="bottom" class="mx-2 flex items-center"> <ui-tooltip v-else text="This backup was created with an old version of audiobookshelf no longer supported" direction="bottom" class="mx-2 flex items-center">
<span class="material-symbols-outlined text-2xl text-error">error_outline</span> <span class="material-symbols text-2xl text-error">error_outline</span>
</ui-tooltip> </ui-tooltip>
<button aria-label="Download Backup" class="inline-flex material-symbols text-xl mx-1 mt-1 text-white/70 hover:text-white/100" @click.stop="downloadBackup(backup)">download</button> <button aria-label="Download Backup" class="inline-flex material-symbols text-xl mx-1 mt-1 text-white/70 hover:text-white/100" @click.stop="downloadBackup(backup)">download</button>
@@ -186,7 +186,7 @@ export default {
mounted() { mounted() {
this.loadBackups() this.loadBackups()
if (this.$route.query.backup) { if (this.$route.query.backup) {
this.$toast.success('Backup applied successfully') this.$toast.success(this.$strings.ToastBackupAppliedSuccess)
} }
} }
} }
+1 -1
View File
@@ -6,7 +6,7 @@
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="userCanUpdate" small :to="`/audiobook/${libraryItemId}/chapters`" color="primary" class="mr-2" @click="clickEditChapters">{{ $strings.ButtonEditChapters }}</ui-btn> <ui-btn v-if="userCanUpdate" small :to="`/audiobook/${libraryItemId}/chapters`" color="primary" class="mr-2" @click="clickEditChapters">{{ $strings.ButtonEditChapters }}</ui-btn>
<div v-if="!keepOpen" class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="expanded ? 'transform rotate-180' : ''"> <div v-if="!keepOpen" class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="expanded ? 'transform rotate-180' : ''">
<span class="material-symbols text-4xl">expand_more</span> <span class="material-symbols text-4xl">&#xe313;</span>
</div> </div>
</div> </div>
<transition name="slide"> <transition name="slide">
@@ -78,7 +78,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update collection', error) console.error('Failed to update collection', error)
this.$toast.error('Failed to save collection books order') this.$toast.error(this.$strings.ToastCollectionUpdateFailed)
}) })
}, },
editBook(book) { editBook(book) {
@@ -110,4 +110,4 @@ export default {
.collection-book-leave-active { .collection-book-leave-active {
position: absolute; position: absolute;
} }
</style> </style>
@@ -45,7 +45,7 @@ export default {
methods: { methods: {
removeProvider(provider) { removeProvider(provider) {
const payload = { const payload = {
message: `Are you sure you want remove custom metadata provider "${provider.name}"?`, message: this.$getString('MessageConfirmDeleteMetadataProvider', [provider.name]),
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.$emit('update:processing', true) this.$emit('update:processing', true)
@@ -53,12 +53,12 @@ export default {
this.$axios this.$axios
.$delete(`/api/custom-metadata-providers/${provider.id}`) .$delete(`/api/custom-metadata-providers/${provider.id}`)
.then(() => { .then(() => {
this.$toast.success('Provider removed') this.$toast.success(this.$strings.ToastProviderRemoveSuccess)
this.$emit('removed', provider.id) this.$emit('removed', provider.id)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove provider', error) console.error('Failed to remove provider', error)
this.$toast.error('Failed to remove provider') this.$toast.error(this.$strings.ToastRemoveFailed)
}) })
.finally(() => { .finally(() => {
this.$emit('update:processing', false) this.$emit('update:processing', false)
+3 -3
View File
@@ -8,7 +8,7 @@
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="toggleFullPath">{{ $strings.ButtonFullPath }}</ui-btn> <ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="toggleFullPath">{{ $strings.ButtonFullPath }}</ui-btn>
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''"> <div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''">
<span class="material-symbols text-4xl">expand_more</span> <span class="material-symbols text-4xl">&#xe313;</span>
</div> </div>
</div> </div>
<transition name="slide"> <transition name="slide">
@@ -18,7 +18,7 @@
<th class="text-left px-4">{{ $strings.LabelPath }}</th> <th class="text-left px-4">{{ $strings.LabelPath }}</th>
<th class="text-left w-24 min-w-24">{{ $strings.LabelSize }}</th> <th class="text-left w-24 min-w-24">{{ $strings.LabelSize }}</th>
<th class="text-left px-4 w-24"> <th class="text-left px-4 w-24">
{{ $strings.LabelRead }} <ui-tooltip :text="$strings.LabelReadEbookWithoutProgress" direction="top" class="inline-block"><span class="material-symbols-outlined text-sm align-middle">info</span></ui-tooltip> {{ $strings.LabelRead }} <ui-tooltip :text="$strings.LabelReadEbookWithoutProgress" direction="top" class="inline-block"><span class="material-symbols text-sm align-middle">info</span></ui-tooltip>
</th> </th>
<th v-if="showMoreColumn" class="text-center w-16"></th> <th v-if="showMoreColumn" class="text-center w-16"></th>
</tr> </tr>
@@ -92,4 +92,4 @@ export default {
} }
} }
} }
</script> </script>
@@ -1,7 +1,7 @@
<template> <template>
<tr> <tr>
<td class="px-4"> <td class="px-4">
{{ showFullPath ? file.metadata.path : file.metadata.relPath }} <ui-tooltip :text="$strings.LabelPrimaryEbook" class="inline-block"><span v-if="isPrimary" class="material-symbols-outlined text-success align-text-bottom">check_circle</span></ui-tooltip> {{ showFullPath ? file.metadata.path : file.metadata.relPath }} <ui-tooltip :text="$strings.LabelPrimaryEbook" class="inline-block"><span v-if="isPrimary" class="material-symbols text-success align-text-bottom">check_circle</span></ui-tooltip>
</td> </td>
<td> <td>
{{ $bytesPretty(file.metadata.size) }} {{ $bytesPretty(file.metadata.size) }}
@@ -8,7 +8,7 @@
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="toggleFullPath">{{ $strings.ButtonFullPath }}</ui-btn> <ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="toggleFullPath">{{ $strings.ButtonFullPath }}</ui-btn>
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''"> <div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''">
<span class="material-symbols text-4xl">expand_more</span> <span class="material-symbols text-4xl">&#xe313;</span>
</div> </div>
</div> </div>
<transition name="slide"> <transition name="slide">
@@ -103,4 +103,4 @@ export default {
this.showFiles = this.expanded this.showFiles = this.expanded
} }
} }
</script> </script>
@@ -92,7 +92,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update playlist', error) console.error('Failed to update playlist', error)
this.$toast.error('Failed to save playlist items order') this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
}) })
}, },
init() { init() {
@@ -119,4 +119,4 @@ export default {
.playlist-item-leave-active { .playlist-item-leave-active {
position: absolute; position: absolute;
} }
</style> </style>
+2 -2
View File
@@ -11,7 +11,7 @@
<ui-btn small color="primary">{{ $strings.ButtonManageTracks }}</ui-btn> <ui-btn small color="primary">{{ $strings.ButtonManageTracks }}</ui-btn>
</nuxt-link> </nuxt-link>
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''"> <div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''">
<span class="material-symbols text-4xl">expand_more</span> <span class="material-symbols text-4xl">&#xe313;</span>
</div> </div>
</div> </div>
<transition name="slide"> <transition name="slide">
@@ -92,4 +92,4 @@ export default {
} }
} }
} }
</script> </script>
+1 -5
View File
@@ -44,7 +44,7 @@
<div v-if="user.type !== 'root' || userIsRoot" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-opacity-100 cursor-pointer" @click.stop="editUser(user)"> <div v-if="user.type !== 'root' || userIsRoot" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-opacity-100 cursor-pointer" @click.stop="editUser(user)">
<button type="button" :aria-label="$getString('ButtonUserEdit', [user.username])" class="material-symbols text-base">edit</button> <button type="button" :aria-label="$getString('ButtonUserEdit', [user.username])" class="material-symbols text-base">edit</button>
</div> </div>
<div v-show="user.type !== 'root'" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="deleteUserClick(user)"> <div v-show="user.type !== 'root' && user.id !== currentUserId" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="deleteUserClick(user)">
<button type="button" :aria-label="$getString('ButtonUserDelete', [user.username])" class="material-symbols text-base">delete</button> <button type="button" :aria-label="$getString('ButtonUserDelete', [user.username])" class="material-symbols text-base">delete</button>
</div> </div>
</div> </div>
@@ -157,10 +157,6 @@ export default {
this.init() this.init()
}, },
beforeDestroy() { beforeDestroy() {
if (this.$refs.accountModal) {
this.$refs.accountModal.close()
}
if (this.$root.socket) { if (this.$root.socket) {
this.$root.socket.off('user_added', this.addUpdateUser) this.$root.socket.off('user_added', this.addUpdateUser)
this.$root.socket.off('user_updated', this.addUpdateUser) this.$root.socket.off('user_updated', this.addUpdateUser)
@@ -76,8 +76,7 @@ export default {
var newOrder = libraryOrderData.map((lib) => lib.id).join(',') var newOrder = libraryOrderData.map((lib) => lib.id).join(',')
if (currOrder !== newOrder) { if (currOrder !== newOrder) {
this.$axios.$post('/api/libraries/order', libraryOrderData).then((response) => { this.$axios.$post('/api/libraries/order', libraryOrderData).then((response) => {
if (response.libraries && response.libraries.length) { if (response.libraries?.length) {
this.$toast.success('Library order saved', { timeout: 1500 })
this.$store.commit('libraries/set', response.libraries) this.$store.commit('libraries/set', response.libraries)
} }
}) })
@@ -110,4 +109,4 @@ export default {
this.$store.commit('libraries/removeListener', 'libraries-table') this.$store.commit('libraries/removeListener', 'libraries-table')
} }
} }
</script> </script>
@@ -218,12 +218,12 @@ export default {
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess) this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
} else { } else {
console.log(`Item removed from playlist`, updatedPlaylist) console.log(`Item removed from playlist`, updatedPlaylist)
this.$toast.success('Item removed from playlist') this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
} }
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove item from playlist', error) console.error('Failed to remove item from playlist', error)
this.$toast.error('Failed to remove item from playlist') this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
}) })
.finally(() => { .finally(() => {
this.processingRemove = false this.processingRemove = false
@@ -182,7 +182,7 @@ export default {
toggleFinished(confirmed = false) { toggleFinished(confirmed = false) {
if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) { if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) {
const payload = { const payload = {
message: `Are you sure you want to mark "${this.title}" as finished?`, message: `Are you sure you want to mark "${this.episodeTitle}" as finished?`,
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.toggleFinished(true) this.toggleFinished(true)
@@ -233,4 +233,4 @@ export default {
}, },
mounted() {} mounted() {}
} }
</script> </script>
@@ -246,7 +246,7 @@ export default {
message: newIsFinished ? this.$strings.MessageConfirmMarkAllEpisodesFinished : this.$strings.MessageConfirmMarkAllEpisodesNotFinished, message: newIsFinished ? this.$strings.MessageConfirmMarkAllEpisodesFinished : this.$strings.MessageConfirmMarkAllEpisodesNotFinished,
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.batchUpdateEpisodesFinished(this.episodesSorted, newIsFinished) this.batchUpdateEpisodesFinished(this.episodesCopy, newIsFinished)
} }
}, },
type: 'yesNo' type: 'yesNo'
@@ -270,7 +270,7 @@ export default {
if (data.numEpisodesUpdated) { if (data.numEpisodesUpdated) {
this.$toast.success(`${data.numEpisodesUpdated} episodes updated`) this.$toast.success(`${data.numEpisodesUpdated} episodes updated`)
} else { } else {
this.$toast.info('No changes were made') this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
} }
}) })
.catch((error) => { .catch((error) => {
@@ -295,7 +295,7 @@ export default {
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: this.mediaMetadata.title, subtitle: this.mediaMetadata.title,
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
duration: episode.audioFile.duration || null, duration: episode.audioFile.duration || null,
coverPath: this.media.coverPath || null coverPath: this.media.coverPath || null
} }
@@ -305,6 +305,7 @@ export default {
this.batchUpdateEpisodesFinished(this.selectedEpisodes, !this.selectedIsFinished) this.batchUpdateEpisodesFinished(this.selectedEpisodes, !this.selectedIsFinished)
}, },
batchUpdateEpisodesFinished(episodes, newIsFinished) { batchUpdateEpisodesFinished(episodes, newIsFinished) {
if (!episodes.length) return
this.processing = true this.processing = true
const updateProgressPayloads = episodes.map((episode) => { const updateProgressPayloads = episodes.map((episode) => {
@@ -371,7 +372,7 @@ export default {
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: this.mediaMetadata.title, subtitle: this.mediaMetadata.title,
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
duration: episode.audioFile.duration || null, duration: episode.audioFile.duration || null,
coverPath: this.media.coverPath || null coverPath: this.media.coverPath || null
}) })
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj"> <div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
<slot :disabled="disabled" :showMenu="showMenu" :clickShowMenu="clickShowMenu" :processing="processing"> <slot :disabled="disabled" :showMenu="showMenu" :clickShowMenu="clickShowMenu" :processing="processing">
<button v-if="!processing" type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu"> <button v-if="!processing" type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
<span class="material-symbols text-2xl" :class="iconClass">more_vert</span> <span class="material-symbols text-2xl" :class="iconClass">&#xe5d4;</span>
</button> </button>
<div v-else class="h-full w-full flex items-center justify-center"> <div v-else class="h-full w-full flex items-center justify-center">
<widgets-loading-spinner /> <widgets-loading-spinner />
+2 -2
View File
@@ -5,7 +5,7 @@
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" /> <path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg> </svg>
</div> </div>
<span v-else :class="outlined ? 'material-symbols-outlined' : 'material-symbols'" :style="{ fontSize }">{{ icon }}</span> <span v-else :class="outlined ? 'material-symbols' : 'material-symbols fill'" :style="{ fontSize }" v-html="icon" />
</button> </button>
</template> </template>
@@ -86,4 +86,4 @@ button.icon-btn:disabled::before {
button.icon-btn:disabled span { button.icon-btn:disabled span {
color: #777; color: #777;
} }
</style> </style>
+8 -2
View File
@@ -4,13 +4,13 @@
<div ref="wrapper" class="relative"> <div ref="wrapper" class="relative">
<form @submit.prevent="submitForm"> <form @submit.prevent="submitForm">
<div ref="inputWrapper" class="input-wrapper flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-2" :class="disabled ? 'pointer-events-none bg-black-300 text-gray-400' : 'bg-primary'"> <div ref="inputWrapper" class="input-wrapper flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-2" :class="disabled ? 'pointer-events-none bg-black-300 text-gray-400' : 'bg-primary'">
<input ref="input" v-model="textInput" :disabled="disabled" :readonly="!editable" class="h-full w-full bg-transparent focus:outline-none px-1" @focus="inputFocus" @blur="inputBlur" /> <input ref="input" v-model="textInput" :disabled="disabled" :readonly="!editable" class="h-full w-full bg-transparent focus:outline-none px-1" @focus="inputFocus" @blur="inputBlur" @keydown="keydownHandler" />
</div> </div>
</form> </form>
<ul ref="menu" v-show="isFocused && itemsToShow.length" class="absolute z-50 mt-0 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label"> <ul ref="menu" v-show="isFocused && itemsToShow.length" class="absolute z-50 mt-0 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in itemsToShow"> <template v-for="item in itemsToShow">
<li :key="item" class="text-gray-50 select-none relative py-2 pr-3 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent> <li :key="item" class="text-gray-50 select-none relative py-2 pr-3 cursor-pointer hover:bg-black-400" :class="isMenuItemSelected(item) ? 'text-yellow-300' : ''" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
<div class="flex items-center"> <div class="flex items-center">
<span class="font-normal ml-3 block truncate">{{ item }}</span> <span class="font-normal ml-3 block truncate">{{ item }}</span>
</div> </div>
@@ -30,7 +30,10 @@
</template> </template>
<script> <script>
import menuKeyboardNavigationMixin from '@/mixins/menuKeyboardNavigation'
export default { export default {
mixins: [menuKeyboardNavigationMixin],
props: { props: {
value: [String, Number], value: [String, Number],
disabled: Boolean, disabled: Boolean,
@@ -81,6 +84,9 @@ export default {
} }
}, },
methods: { methods: {
keydownHandler(e) {
this.menuNavigationHandler(e)
},
setFocus() { setFocus() {
if (this.$refs.input && this.editable) this.$refs.input.focus() if (this.$refs.input && this.editable) this.$refs.input.focus()
}, },
+8 -3
View File
@@ -7,7 +7,7 @@
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div> <div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div> <div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
</div> </div>
<div class="text-gray-200 text-xs font-light mt-2 text-center">{{ text }}</div> <div class="text-gray-200 text-xs font-light mt-2 text-center">{{ message }}</div>
</div> </div>
</div> </div>
</template> </template>
@@ -17,7 +17,12 @@ export default {
props: { props: {
text: { text: {
type: String, type: String,
default: 'Please Wait...' default: null
}
},
computed: {
message() {
return this.text || this.$strings.MessagePleaseWait
} }
} }
} }
@@ -67,4 +72,4 @@ export default {
transform: translate(24px, 0); transform: translate(24px, 0);
} }
} }
</style> </style>
+6 -51
View File
@@ -37,7 +37,10 @@
</template> </template>
<script> <script>
import menuKeyboardNavigationMixin from '@/mixins/menuKeyboardNavigation'
export default { export default {
mixins: [menuKeyboardNavigationMixin],
props: { props: {
value: { value: {
type: Array, type: Array,
@@ -63,8 +66,7 @@ export default {
typingTimeout: null, typingTimeout: null,
isFocused: false, isFocused: false,
menu: null, menu: null,
filteredItems: null, filteredItems: null
selectedMenuItemIndex: null
} }
}, },
watch: { watch: {
@@ -119,34 +121,8 @@ export default {
this.filteredItems = results || [] this.filteredItems = results || []
}, },
keydownInput(event) { keydownInput(event) {
let items = this.itemsToShow this.menuNavigationHandler(event)
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault()
if (!items.length) return
if (event.key === 'ArrowDown') {
if (this.selectedMenuItemIndex === null) {
this.selectedMenuItemIndex = 0
} else {
this.selectedMenuItemIndex = Math.min(this.selectedMenuItemIndex + 1, items.length - 1)
}
} else if (event.key === 'ArrowUp') {
if (this.selectedMenuItemIndex === null) {
this.selectedMenuItemIndex = items.length - 1
} else {
this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0)
}
}
this.recalcScroll()
return
} else if (event.key === 'Enter') {
if (this.selectedMenuItemIndex !== null) {
this.clickedOption(event, items[this.selectedMenuItemIndex])
} else {
this.submitForm()
}
return
}
this.selectedMenuItemIndex = null
clearTimeout(this.typingTimeout) clearTimeout(this.typingTimeout)
this.typingTimeout = setTimeout(() => { this.typingTimeout = setTimeout(() => {
this.search() this.search()
@@ -161,24 +137,6 @@ export default {
this.recalcMenuPos() this.recalcMenuPos()
}, 50) }, 50)
}, },
recalcScroll() {
if (!this.menu) return
var menuItems = this.menu.querySelectorAll('li')
if (!menuItems.length) return
var selectedItem = menuItems[this.selectedMenuItemIndex]
if (!selectedItem) return
var menuHeight = this.menu.offsetHeight
var itemHeight = selectedItem.offsetHeight
var itemTop = selectedItem.offsetTop
var itemBottom = itemTop + itemHeight
if (itemBottom > this.menu.scrollTop + menuHeight) {
let menuPaddingBottom = parseFloat(window.getComputedStyle(this.menu).paddingBottom)
this.menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom
} else if (itemTop < this.menu.scrollTop) {
let menuPaddingTop = parseFloat(window.getComputedStyle(this.menu).paddingTop)
this.menu.scrollTop = itemTop - menuPaddingTop
}
},
recalcMenuPos() { recalcMenuPos() {
if (!this.menu || !this.$refs.inputWrapper) return if (!this.menu || !this.$refs.inputWrapper) return
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect() var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
@@ -317,9 +275,6 @@ export default {
this.textInput = null this.textInput = null
this.currentSearch = null this.currentSearch = null
this.selectedMenuItemIndex = null this.selectedMenuItemIndex = null
this.$nextTick(() => {
this.blur()
})
}, },
submitForm() { submitForm() {
if (!this.textInput) return if (!this.textInput) return
+6 -52
View File
@@ -20,7 +20,7 @@
<ul ref="menu" v-show="showMenu" class="absolute z-60 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label"> <ul ref="menu" v-show="showMenu" class="absolute z-60 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in itemsToShow"> <template v-for="item in itemsToShow">
<li :key="item.id" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="itemsToShow[selectedMenuItemIndex] === item ? 'text-yellow-300' : ''" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent> <li :key="item.id" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="isMenuItemSelected(item) ? 'text-yellow-300' : ''" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
<div class="flex items-center"> <div class="flex items-center">
<span class="font-normal ml-3 block truncate">{{ item.name }}</span> <span class="font-normal ml-3 block truncate">{{ item.name }}</span>
</div> </div>
@@ -40,7 +40,10 @@
</template> </template>
<script> <script>
import menuKeyboardNavigationMixin from '@/mixins/menuKeyboardNavigation'
export default { export default {
mixins: [menuKeyboardNavigationMixin],
props: { props: {
value: { value: {
type: Array, type: Array,
@@ -63,8 +66,7 @@ export default {
typingTimeout: null, typingTimeout: null,
isFocused: false, isFocused: false,
menu: null, menu: null,
items: [], items: []
selectedMenuItemIndex: null
} }
}, },
watch: { watch: {
@@ -124,34 +126,7 @@ export default {
this.items = results || [] this.items = results || []
}, },
keydownInput(event) { keydownInput(event) {
let items = this.itemsToShow this.menuNavigationHandler(event)
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault()
if (!items.length) return
if (event.key === 'ArrowDown') {
if (this.selectedMenuItemIndex === null) {
this.selectedMenuItemIndex = 0
} else {
this.selectedMenuItemIndex = Math.min(this.selectedMenuItemIndex + 1, items.length - 1)
}
} else if (event.key === 'ArrowUp') {
if (this.selectedMenuItemIndex === null) {
this.selectedMenuItemIndex = items.length - 1
} else {
this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0)
}
}
this.recalcScroll()
return
} else if (event.key === 'Enter') {
if (this.selectedMenuItemIndex !== null) {
this.clickedOption(event, items[this.selectedMenuItemIndex])
} else {
this.submitForm()
}
return
}
this.selectedMenuItemIndex = null
clearTimeout(this.typingTimeout) clearTimeout(this.typingTimeout)
this.typingTimeout = setTimeout(() => { this.typingTimeout = setTimeout(() => {
this.search() this.search()
@@ -166,24 +141,6 @@ export default {
this.recalcMenuPos() this.recalcMenuPos()
}, 50) }, 50)
}, },
recalcScroll() {
if (!this.menu) return
var menuItems = this.menu.querySelectorAll('li')
if (!menuItems.length) return
var selectedItem = menuItems[this.selectedMenuItemIndex]
if (!selectedItem) return
var menuHeight = this.menu.offsetHeight
var itemHeight = selectedItem.offsetHeight
var itemTop = selectedItem.offsetTop
var itemBottom = itemTop + itemHeight
if (itemBottom > this.menu.scrollTop + menuHeight) {
let menuPaddingBottom = parseFloat(window.getComputedStyle(this.menu).paddingBottom)
this.menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom
} else if (itemTop < this.menu.scrollTop) {
let menuPaddingTop = parseFloat(window.getComputedStyle(this.menu).paddingTop)
this.menu.scrollTop = itemTop - menuPaddingTop
}
},
recalcMenuPos() { recalcMenuPos() {
if (!this.menu || !this.$refs.inputWrapper) return if (!this.menu || !this.$refs.inputWrapper) return
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect() var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
@@ -323,9 +280,6 @@ export default {
this.textInput = null this.textInput = null
this.currentSearch = null this.currentSearch = null
this.selectedMenuItemIndex = null this.selectedMenuItemIndex = null
this.$nextTick(() => {
this.blur()
})
}, },
submitForm() { submitForm() {
if (!this.textInput) return if (!this.textInput) return
+2 -2
View File
@@ -24,12 +24,12 @@ export default {
computed: {}, computed: {},
methods: { methods: {
clickBtn(e) { clickBtn(e) {
e.stopPropagation()
if (this.disabled) { if (this.disabled) {
e.preventDefault() e.preventDefault()
return return
} }
this.$emit('click') this.$emit('click')
e.stopPropagation()
} }
}, },
mounted() {} mounted() {}
@@ -54,4 +54,4 @@ button.icon-btn:hover:not(:disabled)::before {
button.icon-btn:disabled::before { button.icon-btn:disabled::before {
background-color: rgba(0, 0, 0, 0.2); background-color: rgba(0, 0, 0, 0.2);
} }
</style> </style>
+2 -2
View File
@@ -23,10 +23,10 @@
<span class="material-symbols text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span> <span class="material-symbols text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
</div> </div>
<div v-if="type === 'password' && isHovering" class="absolute top-0 right-0 h-full px-4 flex items-center justify-center"> <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-outlined text-gray-400 cursor-pointer text-lg" @click.stop.prevent="showPassword = !showPassword">{{ !showPassword ? 'visibility' : 'visibility_off' }}</span> <span class="material-symbols text-gray-400 cursor-pointer text-lg" @click.stop.prevent="showPassword = !showPassword">{{ !showPassword ? 'visibility' : 'visibility_off' }}</span>
</div> </div>
<div v-else-if="showCopy" class="absolute top-0 right-0 h-full px-4 flex items-center justify-center"> <div v-else-if="showCopy" class="absolute top-0 right-0 h-full px-4 flex items-center justify-center">
<span class="material-symbols-outlined text-gray-400 cursor-pointer text-lg" @click.stop.prevent="copyToClipboard">{{ !hasCopied ? 'content_copy' : 'done' }}</span> <span class="material-symbols text-gray-400 cursor-pointer text-lg" @click.stop.prevent="copyToClipboard">{{ !hasCopied ? 'content_copy' : 'done' }}</span>
</div> </div>
</div> </div>
</template> </template>
+2 -2
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="w-full bg-opacity-5 border border-opacity-60 rounded-lg flex items-center relative py-4 pl-16" :class="wrapperClass"> <div class="w-full bg-opacity-5 border border-opacity-60 rounded-lg flex items-center relative py-4 pl-16" :class="wrapperClass">
<div class="absolute top-0 left-4 h-full flex items-center"> <div class="absolute top-0 left-4 h-full flex items-center">
<span class="material-symbols-outlined text-2xl">{{ icon }}</span> <span class="material-symbols text-2xl">{{ icon }}</span>
</div> </div>
<slot /> <slot />
</div> </div>
@@ -30,4 +30,4 @@ export default {
methods: {}, methods: {},
mounted() {} mounted() {}
} }
</script> </script>
+23 -16
View File
@@ -3,67 +3,67 @@
<form class="w-full h-full px-2 md:px-4 py-6" @submit.prevent="submitForm"> <form class="w-full h-full px-2 md:px-4 py-6" @submit.prevent="submitForm">
<div class="flex flex-wrap -mx-1"> <div class="flex flex-wrap -mx-1">
<div class="w-full md:w-1/2 px-1"> <div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" /> <ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" @input="handleInputChange" />
</div> </div>
<div class="flex-grow px-1 mt-2 md:mt-0"> <div class="flex-grow px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" :label="$strings.LabelSubtitle" /> <ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" :label="$strings.LabelSubtitle" @input="handleInputChange" />
</div> </div>
</div> </div>
<div class="flex flex-wrap mt-2 -mx-1"> <div class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-3/4 px-1"> <div class="w-full md:w-3/4 px-1">
<!-- Authors filter only contains authors in this library, uses filter data --> <!-- Authors filter only contains authors in this library, uses filter data -->
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" :label="$strings.LabelAuthors" filter-key="authors" /> <ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" :label="$strings.LabelAuthors" filter-key="authors" @input="handleInputChange" />
</div> </div>
<div class="flex-grow px-1 mt-2 md:mt-0"> <div class="flex-grow px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" :label="$strings.LabelPublishYear" /> <ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" :label="$strings.LabelPublishYear" @input="handleInputChange" />
</div> </div>
</div> </div>
<div class="flex mt-2 -mx-1"> <div class="flex mt-2 -mx-1">
<div class="flex-grow px-1"> <div class="flex-grow px-1">
<widgets-series-input-widget v-model="details.series" /> <widgets-series-input-widget v-model="details.series" @input="handleInputChange" />
</div> </div>
</div> </div>
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" /> <ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" />
<div class="flex flex-wrap mt-2 -mx-1"> <div class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-1/2 px-1"> <div class="w-full md:w-1/2 px-1">
<ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" /> <ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" @input="handleInputChange" />
</div> </div>
<div class="flex-grow px-1 mt-2 md:mt-0"> <div class="flex-grow px-1 mt-2 md:mt-0">
<ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" /> <ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" @input="handleInputChange" />
</div> </div>
</div> </div>
<div class="flex flex-wrap mt-2 -mx-1"> <div class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-1/2 px-1"> <div class="w-full md:w-1/2 px-1">
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" :label="$strings.LabelNarrators" :items="narrators" /> <ui-multi-select ref="narratorsSelect" v-model="details.narrators" :label="$strings.LabelNarrators" :items="narrators" @input="handleInputChange" />
</div> </div>
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0"> <div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" /> <ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" @input="handleInputChange" />
</div> </div>
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0"> <div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" /> <ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" @input="handleInputChange" />
</div> </div>
</div> </div>
<div class="flex flex-wrap mt-2 -mx-1"> <div class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-1/4 px-1"> <div class="w-full md:w-1/4 px-1">
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" /> <ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" @input="handleInputChange" />
</div> </div>
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0"> <div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" /> <ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" @input="handleInputChange" />
</div> </div>
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0"> <div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
<div class="flex justify-center"> <div class="flex justify-center">
<ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" /> <ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" @input="handleInputChange" />
</div> </div>
</div> </div>
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0"> <div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
<div class="flex justify-center"> <div class="flex justify-center">
<ui-checkbox v-model="details.abridged" :label="$strings.LabelAbridged" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" /> <ui-checkbox v-model="details.abridged" :label="$strings.LabelAbridged" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" @input="handleInputChange" />
</div> </div>
</div> </div>
</div> </div>
@@ -132,6 +132,12 @@ export default {
} }
}, },
methods: { methods: {
handleInputChange() {
this.$emit('change', {
libraryItemId: this.libraryItem.id,
hasChanges: this.checkForChanges().hasChanges
})
},
getDetails() { getDetails() {
this.forceBlur() this.forceBlur()
return this.checkForChanges() return this.checkForChanges()
@@ -172,6 +178,7 @@ export default {
} }
} }
} }
this.handleInputChange()
}, },
forceBlur() { forceBlur() {
if (this.$refs.titleInput) this.$refs.titleInput.blur() if (this.$refs.titleInput) this.$refs.titleInput.blur()
@@ -286,4 +293,4 @@ export default {
}, },
mounted() {} mounted() {}
} }
</script> </script>
@@ -1,9 +1,9 @@
<template> <template>
<div> <div>
<div class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent> <div 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">remove</span> <span class="material-symbols" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize" aria-label="Decrease Cover Size" role="button">&#xe15b;</span>
<p class="px-2 font-mono" style="font-size: 1rem">{{ bookCoverWidth }}</p> <p class="px-2 font-mono" style="font-size: 1rem">{{ bookCoverWidth }}</p>
<span class="material-symbols" :class="selectedSizeIndex === availableSizes.length - 1 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="increaseSize" aria-label="Increase Cover Size" role="button">add</span> <span class="material-symbols" :class="selectedSizeIndex === availableSizes.length - 1 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="increaseSize" aria-label="Increase Cover Size" role="button">&#xe145;</span>
</div> </div>
</div> </div>
</template> </template>
@@ -30,14 +30,14 @@
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<widgets-loading-spinner v-if="isValidating" class="mr-2" /> <widgets-loading-spinner v-if="isValidating" class="mr-2" />
<span v-else class="material-symbols-outlined mr-2 text-xl" :class="isValid ? 'text-success' : 'text-error'">{{ isValid ? 'check_circle_outline' : 'error_outline' }}</span> <span v-else class="material-symbols mr-2 text-xl" :class="isValid ? 'text-success' : 'text-error'">{{ isValid ? 'check_circle_outline' : 'error_outline' }}</span>
<p v-if="isValidating" class="text-gray-300 text-base md:text-lg text-center">{{ $strings.MessageCheckingCron }}</p> <p v-if="isValidating" class="text-gray-300 text-base md:text-lg text-center">{{ $strings.MessageCheckingCron }}</p>
<p v-else-if="customCronError" class="text-error text-base md:text-lg text-center">{{ customCronError }}</p> <p v-else-if="customCronError" class="text-error text-base md:text-lg text-center">{{ customCronError }}</p>
<p v-else class="text-success text-base md:text-lg text-center">{{ $strings.MessageValidCronExpression }}</p> <p v-else class="text-success text-base md:text-lg text-center">{{ $strings.MessageValidCronExpression }}</p>
</div> </div>
</template> </template>
<div v-if="cronExpression && isValid" class="flex items-center justify-center text-yellow-400 mt-2"> <div v-if="cronExpression && isValid" class="flex items-center justify-center text-yellow-400 mt-2">
<span class="material-symbols-outlined mr-2 text-xl">event</span> <span class="material-symbols mr-2 text-xl">event</span>
<p>{{ $strings.LabelNextScheduledRun }}: {{ nextRun }}</p> <p>{{ $strings.LabelNextScheduledRun }}: {{ nextRun }}</p>
</div> </div>
</div> </div>
@@ -3,45 +3,45 @@
<form class="w-full h-full px-4 py-6" @submit.prevent="submitForm"> <form class="w-full h-full px-4 py-6" @submit.prevent="submitForm">
<div class="flex -mx-1"> <div class="flex -mx-1">
<div class="w-1/2 px-1"> <div class="w-1/2 px-1">
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" /> <ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" @input="handleInputChange" />
</div> </div>
<div class="flex-grow px-1"> <div class="flex-grow px-1">
<ui-text-input-with-label ref="authorInput" v-model="details.author" :label="$strings.LabelAuthor" /> <ui-text-input-with-label ref="authorInput" v-model="details.author" :label="$strings.LabelAuthor" @input="handleInputChange" />
</div> </div>
</div> </div>
<ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" :label="$strings.LabelRSSFeedURL" class="mt-2" /> <ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" :label="$strings.LabelRSSFeedURL" class="mt-2" @input="handleInputChange" />
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" /> <ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" />
<div class="flex mt-2 -mx-1"> <div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1"> <div class="w-1/2 px-1">
<ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" /> <ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" @input="handleInputChange" />
</div> </div>
<div class="flex-grow px-1"> <div class="flex-grow px-1">
<ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" /> <ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" @input="handleInputChange" />
</div> </div>
</div> </div>
<div class="flex mt-2 -mx-1"> <div class="flex mt-2 -mx-1">
<div class="w-1/4 px-1"> <div class="w-1/4 px-1">
<ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" :label="$strings.LabelReleaseDate" /> <ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" :label="$strings.LabelReleaseDate" @input="handleInputChange" />
</div> </div>
<div class="w-1/4 px-1"> <div class="w-1/4 px-1">
<ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" /> <ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" @input="handleInputChange" />
</div> </div>
<div class="w-1/4 px-1"> <div class="w-1/4 px-1">
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" /> <ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" @input="handleInputChange" />
</div> </div>
<div class="flex-grow px-1 pt-6"> <div class="flex-grow px-1 pt-6">
<div class="flex justify-center"> <div class="flex justify-center">
<ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" /> <ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" @input="handleInputChange" />
</div> </div>
</div> </div>
</div> </div>
<div class="flex mt-2 -mx-1"> <div class="flex mt-2 -mx-1">
<div class="w-1/4 px-1"> <div class="w-1/4 px-1">
<ui-dropdown :label="$strings.LabelPodcastType" v-model="details.type" :items="podcastTypes" small class="max-w-52" /> <ui-dropdown :label="$strings.LabelPodcastType" v-model="details.type" :items="podcastTypes" small class="max-w-52" @input="handleInputChange" />
</div> </div>
</div> </div>
</form> </form>
@@ -105,6 +105,12 @@ export default {
} }
}, },
methods: { methods: {
handleInputChange() {
this.$emit('change', {
libraryItemId: this.libraryItem.id,
hasChanges: this.checkForChanges().hasChanges
})
},
getDetails() { getDetails() {
this.forceBlur() this.forceBlur()
return this.checkForChanges() return this.checkForChanges()
@@ -136,6 +142,8 @@ export default {
} }
} }
} }
this.handleInputChange()
}, },
forceBlur() { forceBlur() {
if (this.$refs.titleInput) this.$refs.titleInput.blur() if (this.$refs.titleInput) this.$refs.titleInput.blur()
+83
View File
@@ -0,0 +1,83 @@
/**
* Mixin for keyboard navigation in dropdown menus.
* This can be used in any component that has a dropdown menu with <li> items.
* The following example shows how to use this mixin in your component:
* <template>
* <div>
* <input type="text" @keydown="menuNavigationHandler">
* <ul ref="menu">
* <li v-for="(item, index) in itemsToShow" :key="index" :class="isMenuItemSelected(item) ? ... : ''" @click="clickedOption($event, item)">
* {{ item }}
* </li>
* </ul>
* </div>
* </template>
*
* This mixin assumes the following are defined in your component:
* itemsToShow: Array of items to show in the dropdown
* clickedOption: Event handler for when an item is clicked
* submitForm: Event handler for when the form is submitted
*
* It also assumes you have a ref="menu" on the menu element.
*/
export default {
data() {
return {
selectedMenuItemIndex: null
}
},
methods: {
menuNavigationHandler(event) {
let items = this.itemsToShow
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault()
if (!items.length) return
if (event.key === 'ArrowDown') {
if (this.selectedMenuItemIndex === null) {
this.selectedMenuItemIndex = 0
} else {
this.selectedMenuItemIndex = Math.min(this.selectedMenuItemIndex + 1, items.length - 1)
}
} else if (event.key === 'ArrowUp') {
if (this.selectedMenuItemIndex === null) {
this.selectedMenuItemIndex = items.length - 1
} else {
this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0)
}
}
this.recalcScroll()
} else if (event.key === 'Enter') {
event.preventDefault()
if (this.selectedMenuItemIndex !== null) {
this.clickedOption(event, items[this.selectedMenuItemIndex])
} else {
this.submitForm()
}
} else {
this.selectedMenuItemIndex = null
}
},
recalcScroll() {
const menu = this.$refs.menu
if (!menu) return
var menuItems = menu.querySelectorAll('li')
if (!menuItems.length) return
var selectedItem = menuItems[this.selectedMenuItemIndex]
if (!selectedItem) return
var menuHeight = menu.offsetHeight
var itemHeight = selectedItem.offsetHeight
var itemTop = selectedItem.offsetTop
var itemBottom = itemTop + itemHeight
if (itemBottom > menu.scrollTop + menuHeight) {
let menuPaddingBottom = parseFloat(window.getComputedStyle(menu).paddingBottom)
menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom
} else if (itemTop < menu.scrollTop) {
let menuPaddingTop = parseFloat(window.getComputedStyle(menu).paddingTop)
menu.scrollTop = itemTop - menuPaddingTop
}
},
isMenuItemSelected(item) {
return this.selectedMenuItemIndex !== null && this.itemsToShow[this.selectedMenuItemIndex] === item
}
}
}
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.12.2", "version": "2.13.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.12.2", "version": "2.13.3",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@nuxtjs/axios": "^5.13.6", "@nuxtjs/axios": "^5.13.6",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.12.2", "version": "2.13.3",
"buildNumber": 1, "buildNumber": 1,
"description": "Self-hosted audiobook and podcast client", "description": "Self-hosted audiobook and podcast client",
"main": "index.js", "main": "index.js",
+6 -6
View File
@@ -117,10 +117,10 @@ export default {
}, },
submitChangePassword() { submitChangePassword() {
if (this.newPassword !== this.confirmPassword) { if (this.newPassword !== this.confirmPassword) {
return this.$toast.error('New password and confirm password do not match') return this.$toast.error(this.$strings.ToastUserPasswordMismatch)
} }
if (this.password === this.newPassword) { if (this.password === this.newPassword) {
return this.$toast.error('Password and New Password cannot be the same') return this.$toast.error(this.$strings.ToastUserPasswordMustChange)
} }
this.changingPassword = true this.changingPassword = true
this.$axios this.$axios
@@ -130,16 +130,16 @@ export default {
}) })
.then((res) => { .then((res) => {
if (res.success) { if (res.success) {
this.$toast.success('Password Changed Successfully') this.$toast.success(this.$strings.ToastUserPasswordChangeSuccess)
this.resetForm() this.resetForm()
} else { } else {
this.$toast.error(res.error || 'Unknown Error') this.$toast.error(res.error || this.$strings.ToastUnknownError)
} }
this.changingPassword = false this.changingPassword = false
}) })
.catch((error) => { .catch((error) => {
console.error(error) console.error(error)
this.$toast.error('Api call failed') this.$toast.error(this.$strings.ToastUnknownError)
this.changingPassword = false this.changingPassword = false
}) })
} }
@@ -148,4 +148,4 @@ export default {
this.selectedLanguage = this.$languageCodes.current this.selectedLanguage = this.$languageCodes.current
} }
} }
</script> </script>
+9 -9
View File
@@ -71,7 +71,7 @@
<div class="flex items-center"> <div class="flex items-center">
<ui-tooltip :text="$strings.MessageRemoveChapter" direction="bottom"> <ui-tooltip :text="$strings.MessageRemoveChapter" direction="bottom">
<button v-if="newChapters.length > 1" class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-error transform hover:scale-110 duration-150" @click="removeChapter(chapter)"> <button v-if="newChapters.length > 1" class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-error transform hover:scale-110 duration-150" @click="removeChapter(chapter)">
<span class="material-symbols-outlined text-base">remove</span> <span class="material-symbols text-base">remove</span>
</button> </button>
</ui-tooltip> </ui-tooltip>
@@ -84,14 +84,14 @@
<ui-tooltip :text="selectedChapterId === chapter.id && isPlayingChapter ? $strings.MessagePauseChapter : $strings.MessagePlayChapter" direction="bottom"> <ui-tooltip :text="selectedChapterId === chapter.id && isPlayingChapter ? $strings.MessagePauseChapter : $strings.MessagePlayChapter" direction="bottom">
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150" @click="playChapter(chapter)"> <button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150" @click="playChapter(chapter)">
<widgets-loading-spinner v-if="selectedChapterId === chapter.id && isLoadingChapter" /> <widgets-loading-spinner v-if="selectedChapterId === chapter.id && isLoadingChapter" />
<span v-else-if="selectedChapterId === chapter.id && isPlayingChapter" class="material-symbols-outlined text-base">pause</span> <span v-else-if="selectedChapterId === chapter.id && isPlayingChapter" class="material-symbols text-base">pause</span>
<span v-else class="material-symbols-outlined text-base">play_arrow</span> <span v-else class="material-symbols text-base">play_arrow</span>
</button> </button>
</ui-tooltip> </ui-tooltip>
<ui-tooltip v-if="chapter.error" :text="chapter.error" direction="left"> <ui-tooltip v-if="chapter.error" :text="chapter.error" direction="left">
<button class="w-7 h-7 rounded-full flex items-center justify-center text-error"> <button class="w-7 h-7 rounded-full flex items-center justify-center text-error">
<span class="material-symbols-outlined text-lg">error_outline</span> <span class="material-symbols text-lg">error_outline</span>
</button> </button>
</ui-tooltip> </ui-tooltip>
</div> </div>
@@ -106,7 +106,7 @@
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn small @click="setChaptersFromTracks">{{ $strings.ButtonSetChaptersFromTracks }}</ui-btn> <ui-btn small @click="setChaptersFromTracks">{{ $strings.ButtonSetChaptersFromTracks }}</ui-btn>
<ui-tooltip :text="$strings.MessageSetChaptersFromTracksDescription" direction="top" class="flex items-center mx-1 cursor-default"> <ui-tooltip :text="$strings.MessageSetChaptersFromTracksDescription" direction="top" class="flex items-center mx-1 cursor-default">
<span class="material-symbols-outlined text-xl text-gray-200">info</span> <span class="material-symbols text-xl text-gray-200">info</span>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2"> <div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
@@ -189,7 +189,7 @@
<div class="flex items-center pt-2"> <div class="flex items-center pt-2">
<ui-btn small color="primary" class="mr-1" @click="applyChapterNamesOnly">{{ $strings.ButtonMapChapterTitles }}</ui-btn> <ui-btn small color="primary" class="mr-1" @click="applyChapterNamesOnly">{{ $strings.ButtonMapChapterTitles }}</ui-btn>
<ui-tooltip :text="$strings.MessageMapChapterTitles" direction="top" class="flex items-center"> <ui-tooltip :text="$strings.MessageMapChapterTitles" direction="top" class="flex items-center">
<span class="material-symbols-outlined text-xl text-gray-200">info</span> <span class="material-symbols text-xl text-gray-200">info</span>
</ui-tooltip> </ui-tooltip>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn small color="success" @click="applyChapterData">{{ $strings.ButtonApplyChapters }}</ui-btn> <ui-btn small color="success" @click="applyChapterData">{{ $strings.ButtonApplyChapters }}</ui-btn>
@@ -560,7 +560,7 @@ export default {
.catch((error) => { .catch((error) => {
this.findingChapters = false this.findingChapters = false
console.error('Failed to get chapter data', error) console.error('Failed to get chapter data', error)
this.$toast.error('Failed to find chapters') this.$toast.error(this.$strings.ToastFailedToLoadData)
this.showFindChaptersModal = false this.showFindChaptersModal = false
}) })
}, },
@@ -611,7 +611,7 @@ export default {
.$post(`/api/items/${this.libraryItem.id}/chapters`, payload) .$post(`/api/items/${this.libraryItem.id}/chapters`, payload)
.then((data) => { .then((data) => {
if (data.updated) { if (data.updated) {
this.$toast.success('Chapters removed') this.$toast.success(this.$strings.ToastChaptersRemoved)
if (this.previousRoute) { if (this.previousRoute) {
this.$router.push(this.previousRoute) this.$router.push(this.previousRoute)
} else { } else {
@@ -623,7 +623,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove chapters', error) console.error('Failed to remove chapters', error)
this.$toast.error('Failed to remove chapters') this.$toast.error(this.$strings.ToastRemoveFailed)
}) })
.finally(() => { .finally(() => {
this.saving = false this.saving = false
+2 -2
View File
@@ -331,11 +331,11 @@ export default {
this.$axios this.$axios
.$delete(`/api/tools/item/${this.libraryItemId}/encode-m4b`) .$delete(`/api/tools/item/${this.libraryItemId}/encode-m4b`)
.then(() => { .then(() => {
this.$toast.success('Encode canceled') this.$toast.success(this.$strings.ToastEncodeCancelSucces)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to cancel encode', error) console.error('Failed to cancel encode', error)
this.$toast.error('Failed to cancel encode') this.$toast.error(this.$strings.ToastEncodeCancelFailed)
}) })
.finally(() => { .finally(() => {
this.isCancelingEncode = false this.isCancelingEncode = false
+40 -38
View File
@@ -97,8 +97,8 @@
<div class="flex justify-center flex-wrap"> <div class="flex justify-center flex-wrap">
<template v-for="libraryItem in libraryItemCopies"> <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 :key="libraryItem.id" class="w-full max-w-3xl border border-black-300 p-6 -ml-px -mt-px">
<widgets-book-details-edit v-if="libraryItem.mediaType === 'book'" :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" /> <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" /> <widgets-podcast-details-edit v-else :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" @change="handleItemChange" />
</div> </div>
</template> </template>
</div> </div>
@@ -108,7 +108,7 @@
<div :class="isScrollable ? 'fixed left-0 box-shadow-lg-up bg-primary' : ''" class="w-full h-20 px-4 flex items-center border-t border-bg z-40" :style="{ bottom: streamLibraryItem ? '165px' : '0px' }"> <div :class="isScrollable ? 'fixed left-0 box-shadow-lg-up bg-primary' : ''" class="w-full h-20 px-4 flex items-center border-t border-bg z-40" :style="{ bottom: streamLibraryItem ? '165px' : '0px' }">
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn color="success" :padding-x="8" class="text-lg" :loading="isProcessing" @click.prevent="saveClick">{{ $strings.ButtonSave }}</ui-btn> <ui-btn color="success" :padding-x="8" class="text-lg" :loading="isProcessing" :disabled="!hasChanges" @click.prevent="saveClick">{{ $strings.ButtonSave }}</ui-btn>
</div> </div>
</div> </div>
</template> </template>
@@ -170,7 +170,8 @@ export default {
abridged: false abridged: false
}, },
appendableKeys: ['authors', 'genres', 'tags', 'narrators', 'series'], appendableKeys: ['authors', 'genres', 'tags', 'narrators', 'series'],
openMapOptions: false openMapOptions: false,
itemsWithChanges: []
} }
}, },
computed: { computed: {
@@ -221,9 +222,19 @@ export default {
}, },
hasSelectedBatchUsage() { hasSelectedBatchUsage() {
return Object.values(this.selectedBatchUsage).some((b) => !!b) return Object.values(this.selectedBatchUsage).some((b) => !!b)
},
hasChanges() {
return this.itemsWithChanges.length > 0
} }
}, },
methods: { methods: {
handleItemChange(itemChange) {
if (!itemChange.hasChanges) {
this.itemsWithChanges = this.itemsWithChanges.filter((id) => id !== itemChange.libraryItemId)
} else if (!this.itemsWithChanges.includes(itemChange.libraryItemId)) {
this.itemsWithChanges.push(itemChange.libraryItemId)
}
},
blurBatchForm() { blurBatchForm() {
if (this.$refs.seriesSelect && this.$refs.seriesSelect.isFocused) { if (this.$refs.seriesSelect && this.$refs.seriesSelect.isFocused) {
this.$refs.seriesSelect.forceBlur() this.$refs.seriesSelect.forceBlur()
@@ -283,38 +294,10 @@ export default {
removedSeriesItem(item) {}, removedSeriesItem(item) {},
newNarratorItem(item) {}, newNarratorItem(item) {},
removedNarratorItem(item) {}, removedNarratorItem(item) {},
newTagItem(item) { newTagItem(item) {},
// if (item && !this.newTagItems.includes(item)) { removedTagItem(item) {},
// this.newTagItems.push(item) newGenreItem(item) {},
// } removedGenreItem(item) {},
},
removedTagItem(item) {
// If newly added, remove if not used on any other items
// if (item && this.newTagItems.includes(item)) {
// var usedByOtherAb = this.libraryItemCopies.find((ab) => {
// return ab.tags && ab.tags.includes(item)
// })
// if (!usedByOtherAb) {
// this.newTagItems = this.newTagItems.filter((t) => t !== item)
// }
// }
},
newGenreItem(item) {
// if (item && !this.newGenreItems.includes(item)) {
// this.newGenreItems.push(item)
// }
},
removedGenreItem(item) {
// If newly added, remove if not used on any other items
// if (item && this.newGenreItems.includes(item)) {
// var usedByOtherAb = this.libraryItemCopies.find((ab) => {
// return ab.book.genres && ab.book.genres.includes(item)
// })
// if (!usedByOtherAb) {
// this.newGenreItems = this.newGenreItems.filter((t) => t !== item)
// }
// }
},
init() { init() {
// TODO: Better deep cloning of library items // TODO: Better deep cloning of library items
this.libraryItemCopies = this.libraryItems.map((li) => { this.libraryItemCopies = this.libraryItems.map((li) => {
@@ -366,7 +349,7 @@ export default {
} }
} }
if (!updates.length) { if (!updates.length) {
return this.$toast.warning('No updates were made') return this.$toast.warning(this.$strings.ToastNoUpdatesNecessary)
} }
console.log('Pushing updates', updates) console.log('Pushing updates', updates)
@@ -376,6 +359,7 @@ export default {
.then((data) => { .then((data) => {
this.isProcessing = false this.isProcessing = false
if (data.updates) { if (data.updates) {
this.itemsWithChanges = []
this.$toast.success(`Successfully updated ${data.updates} items`) this.$toast.success(`Successfully updated ${data.updates} items`)
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf`) this.$router.replace(`/library/${this.currentLibraryId}/bookshelf`)
} else { } else {
@@ -387,10 +371,28 @@ export default {
this.$toast.error('Failed to batch update') this.$toast.error('Failed to batch update')
this.isProcessing = false this.isProcessing = false
}) })
},
beforeUnload(e) {
if (!e || !this.hasChanges) return
e.preventDefault()
e.returnValue = ''
}
},
beforeRouteLeave(to, from, next) {
if (this.hasChanges) {
next(false)
window.location = to.path
} else {
next()
} }
}, },
mounted() { mounted() {
this.init() this.init()
window.addEventListener('beforeunload', this.beforeUnload)
},
beforeDestroy() {
window.removeEventListener('beforeunload', this.beforeUnload)
} }
} }
</script> </script>
@@ -406,4 +408,4 @@ export default {
transform: translateY(-100%); transform: translateY(-100%);
transition: all 150ms ease-in 0s; transition: all 150ms ease-in 0s;
} }
</style> </style>
+6 -6
View File
@@ -3,7 +3,7 @@
<app-settings-content :header-text="$strings.HeaderBackups" :description="$strings.MessageBackupsDescription"> <app-settings-content :header-text="$strings.HeaderBackups" :description="$strings.MessageBackupsDescription">
<div v-if="backupLocation" class="mb-4 max-w-full overflow-hidden"> <div v-if="backupLocation" class="mb-4 max-w-full overflow-hidden">
<div class="flex items-center mb-0.5"> <div class="flex items-center mb-0.5">
<span class="material-symbols-outlined text-2xl text-black-50 mr-2">folder</span> <span class="material-symbols text-2xl text-black-50 mr-2">folder</span>
<span class="text-white text-opacity-60 uppercase text-sm whitespace-nowrap">{{ $strings.LabelBackupLocation }}:</span> <span class="text-white text-opacity-60 uppercase text-sm whitespace-nowrap">{{ $strings.LabelBackupLocation }}:</span>
</div> </div>
<div v-if="!showEditBackupPath" class="inline-flex items-center w-full overflow-hidden"> <div v-if="!showEditBackupPath" class="inline-flex items-center w-full overflow-hidden">
@@ -33,7 +33,7 @@
<div v-if="enableBackups" class="mb-6"> <div v-if="enableBackups" class="mb-6">
<div class="flex items-center pl-0 sm:pl-6 mb-2"> <div class="flex items-center pl-0 sm:pl-6 mb-2">
<span class="material-symbols-outlined text-xl sm:text-2xl text-black-50 mr-2">schedule</span> <span class="material-symbols text-xl sm:text-2xl text-black-50 mr-2">schedule</span>
<div class="w-32 min-w-32 sm:w-40 sm:min-w-40"> <div class="w-32 min-w-32 sm:w-40 sm:min-w-40">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.HeaderSchedule }}:</span> <span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.HeaderSchedule }}:</span>
</div> </div>
@@ -44,7 +44,7 @@
</div> </div>
<div v-if="nextBackupDate" class="flex items-center pl-0 sm:pl-6 py-0.5"> <div v-if="nextBackupDate" class="flex items-center pl-0 sm:pl-6 py-0.5">
<span class="material-symbols-outlined text-xl sm:text-2xl text-black-50 mr-2">event</span> <span class="material-symbols text-xl sm:text-2xl text-black-50 mr-2">event</span>
<div class="w-32 min-w-32 sm:w-40 sm:min-w-40"> <div class="w-32 min-w-32 sm:w-40 sm:min-w-40">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNextBackupDate }}:</span> <span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNextBackupDate }}:</span>
</div> </div>
@@ -162,7 +162,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to save backup path', error) console.error('Failed to save backup path', error)
const errorMsg = error.response?.data || 'Failed to save backup path' const errorMsg = error.response?.data || this.$strings.ToastBackupPathUpdateFailed
this.$toast.error(errorMsg) this.$toast.error(errorMsg)
}) })
.finally(() => { .finally(() => {
@@ -171,11 +171,11 @@ export default {
}, },
updateBackupsSettings() { updateBackupsSettings() {
if (isNaN(this.maxBackupSize) || this.maxBackupSize < 0) { if (isNaN(this.maxBackupSize) || this.maxBackupSize < 0) {
this.$toast.error('Invalid maximum backup size') this.$toast.error(this.$strings.ToastBackupInvalidMaxSize)
return return
} }
if (isNaN(this.backupsToKeep) || this.backupsToKeep <= 0 || this.backupsToKeep > 99) { if (isNaN(this.backupsToKeep) || this.backupsToKeep <= 0 || this.backupsToKeep > 99) {
this.$toast.error('Invalid number of backups to keep') this.$toast.error(this.$strings.ToastBackupInvalidMaxKeep)
return return
} }
const updatePayload = { const updatePayload = {
+7 -8
View File
@@ -109,7 +109,7 @@
</tr> </tr>
</table> </table>
<div v-else-if="!loading" class="text-center py-4"> <div v-else-if="!loading" class="text-center py-4">
<p class="text-lg text-gray-100">No Devices</p> <p class="text-lg text-gray-100">{{ $strings.MessageNoDevices }}</p>
</div> </div>
</app-settings-content> </app-settings-content>
@@ -199,7 +199,7 @@ export default {
}, },
deleteDeviceClick(device) { deleteDeviceClick(device) {
const payload = { const payload = {
message: `Are you sure you want to delete e-reader device "${device.name}"?`, message: this.$getString('MessageConfirmDeleteDevice', [device.name]),
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.deleteDevice(device) this.deleteDevice(device)
@@ -218,11 +218,10 @@ export default {
.$post(`/api/emails/ereader-devices`, payload) .$post(`/api/emails/ereader-devices`, payload)
.then((data) => { .then((data) => {
this.ereaderDevicesUpdated(data.ereaderDevices) this.ereaderDevicesUpdated(data.ereaderDevices)
this.$toast.success('Device deleted')
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to delete device', error) console.error('Failed to delete device', error)
this.$toast.error('Failed to delete device') this.$toast.error(this.$strings.ToastRemoveFailed)
}) })
.finally(() => { .finally(() => {
this.deletingDeviceName = null this.deletingDeviceName = null
@@ -246,11 +245,11 @@ export default {
this.$axios this.$axios
.$post('/api/emails/test') .$post('/api/emails/test')
.then(() => { .then(() => {
this.$toast.success('Test Email Sent') this.$toast.success(this.$strings.ToastDeviceTestEmailSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to send test email', error) console.error('Failed to send test email', error)
const errorMsg = error.response.data || 'Failed to send test email' const errorMsg = error.response.data || this.$strings.ToastDeviceTestEmailFailed
this.$toast.error(errorMsg) this.$toast.error(errorMsg)
}) })
.finally(() => { .finally(() => {
@@ -289,11 +288,11 @@ export default {
this.newSettings = { this.newSettings = {
...data.settings ...data.settings
} }
this.$toast.success('Email settings updated') this.$toast.success(this.$strings.ToastEmailSettingsUpdateSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update email settings', error) console.error('Failed to update email settings', error)
this.$toast.error('Failed to update email settings') this.$toast.error(this.$strings.ToastEmailSettingsUpdateFailed)
}) })
.finally(() => { .finally(() => {
this.savingSettings = false this.savingSettings = false
@@ -130,7 +130,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to rename genre', error) console.error('Failed to rename genre', error)
this.$toast.error('Failed to rename genre') this.$toast.error(this.$strings.ToastRenameFailed)
}) })
.finally(() => { .finally(() => {
this.loading = false this.loading = false
@@ -147,7 +147,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove genre', error) console.error('Failed to remove genre', error)
this.$toast.error('Failed to remove genre') this.$toast.error(this.$strings.ToastRemoveFailed)
}) })
.finally(() => { .finally(() => {
this.loading = false this.loading = false
@@ -126,7 +126,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to rename tag', error) console.error('Failed to rename tag', error)
this.$toast.error('Failed to rename tag') this.$toast.error(this.$strings.ToastRenameFailed)
}) })
.finally(() => { .finally(() => {
this.loading = false this.loading = false
@@ -143,7 +143,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove tag', error) console.error('Failed to remove tag', error)
this.$toast.error('Failed to remove tag') this.$toast.error(this.$strings.ToastRemoveFailed)
}) })
.finally(() => { .finally(() => {
this.loading = false this.loading = false
+4 -4
View File
@@ -105,12 +105,12 @@ export default {
} }
if (isNaN(this.maxNotificationQueue) || this.maxNotificationQueue <= 0) { if (isNaN(this.maxNotificationQueue) || this.maxNotificationQueue <= 0) {
this.$toast.error('Max notification queue must be >= 0') this.$toast.error(this.$strings.ToastNotificationQueueMaximum)
return false return false
} }
if (isNaN(this.maxFailedAttempts) || this.maxFailedAttempts <= 0) { if (isNaN(this.maxFailedAttempts) || this.maxFailedAttempts <= 0) {
this.$toast.error('Max failed attempts must be >= 0') this.$toast.error(this.$strings.ToastNotificationFailedMaximum)
return false return false
} }
@@ -128,11 +128,11 @@ export default {
this.$axios this.$axios
.$patch('/api/notifications', updatePayload) .$patch('/api/notifications', updatePayload)
.then(() => { .then(() => {
this.$toast.success('Notification settings updated') this.$toast.success(this.$strings.ToastNotificationSettingsUpdateSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update notification settings', error) console.error('Failed to update notification settings', error)
this.$toast.error('Failed to update notification settings') this.$toast.error(this.$strings.ToastNotificationSettingsUpdateFailed)
}) })
.finally(() => { .finally(() => {
this.savingSettings = false this.savingSettings = false
+5 -5
View File
@@ -290,7 +290,6 @@ export default {
this.$axios this.$axios
.$post(`/api/sessions/batch/delete`, payload) .$post(`/api/sessions/batch/delete`, payload)
.then(() => { .then(() => {
this.$toast.success('Sessions removed')
if (isAllSessions) { if (isAllSessions) {
// If all sessions were removed from the current page then go to the previous page // If all sessions were removed from the current page then go to the previous page
if (this.currentPage > 0) { if (this.currentPage > 0) {
@@ -303,7 +302,7 @@ export default {
} }
}) })
.catch((error) => { .catch((error) => {
const errorMsg = error.response?.data || 'Failed to remove sessions' const errorMsg = error.response?.data || this.$strings.ToastRemoveFailed
this.$toast.error(errorMsg) this.$toast.error(errorMsg)
}) })
.finally(() => { .finally(() => {
@@ -358,12 +357,13 @@ export default {
}) })
if (!libraryItem) { if (!libraryItem) {
this.$toast.error('Failed to get library item') this.$toast.error(this.$strings.ToastFailedToLoadData)
this.processingGoToTimestamp = false this.processingGoToTimestamp = false
return return
} }
if (session.episodeId && !libraryItem.media.episodes.some((ep) => ep.id === session.episodeId)) { if (session.episodeId && !libraryItem.media.episodes.some((ep) => ep.id === session.episodeId)) {
this.$toast.error('Failed to get podcast episode') console.error('Episode not found in library item', session.episodeId, libraryItem.media.episodes)
this.$toast.error(this.$strings.ToastFailedToLoadData)
this.processingGoToTimestamp = false this.processingGoToTimestamp = false
return return
} }
@@ -377,7 +377,7 @@ export default {
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: libraryItem.media.metadata.title, subtitle: libraryItem.media.metadata.title,
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
duration: episode.audioFile.duration || null, duration: episode.audioFile.duration || null,
coverPath: libraryItem.media.coverPath || null coverPath: libraryItem.media.coverPath || null
} }
+2 -2
View File
@@ -20,7 +20,7 @@
<div class="flex p-2"> <div class="flex p-2">
<div class="hidden sm:block"> <div class="hidden sm:block">
<span class="hidden sm:block material-symbols-outlined text-5xl lg:text-6xl">event</span> <span class="hidden sm:block material-symbols text-5xl lg:text-6xl">event</span>
</div> </div>
<div class="px-1"> <div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ $formatNumber(totalDaysListened) }}</p> <p class="text-4xl md:text-5xl font-bold">{{ $formatNumber(totalDaysListened) }}</p>
@@ -30,7 +30,7 @@
<div class="flex p-2"> <div class="flex p-2">
<div class="hidden sm:block"> <div class="hidden sm:block">
<span class="material-symbols-outlined text-5xl lg:text-6xl">watch_later</span> <span class="material-symbols text-5xl lg:text-6xl">watch_later</span>
</div> </div>
<div class="px-1"> <div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ $formatNumber(totalMinutesListening) }}</p> <p class="text-4xl md:text-5xl font-bold">{{ $formatNumber(totalMinutesListening) }}</p>
+4 -3
View File
@@ -127,12 +127,13 @@ export default {
}) })
if (!libraryItem) { if (!libraryItem) {
this.$toast.error('Failed to get library item') this.$toast.error(this.$strings.ToastFailedToLoadData)
this.processingGoToTimestamp = false this.processingGoToTimestamp = false
return return
} }
if (session.episodeId && !libraryItem.media.episodes.some((ep) => ep.id === session.episodeId)) { if (session.episodeId && !libraryItem.media.episodes.some((ep) => ep.id === session.episodeId)) {
this.$toast.error('Failed to get podcast episode') console.error('Episode not found in library item', session.episodeId, libraryItem.media.episodes)
this.$toast.error(this.$strings.ToastFailedToLoadData)
this.processingGoToTimestamp = false this.processingGoToTimestamp = false
return return
} }
@@ -146,7 +147,7 @@ export default {
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: libraryItem.media.metadata.title, subtitle: libraryItem.media.metadata.title,
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
duration: episode.audioFile.duration || null, duration: episode.audioFile.duration || null,
coverPath: libraryItem.media.coverPath || null coverPath: libraryItem.media.coverPath || null
} }
+7 -2
View File
@@ -39,6 +39,11 @@ export default {
this.showAccountModal = true this.showAccountModal = true
} }
}, },
mounted() {} mounted() {},
beforeDestroy() {
if (this.$refs.accountModal) {
this.$refs.accountModal.close()
}
}
} }
</script> </script>
+14 -17
View File
@@ -80,14 +80,14 @@
<p class="text-gray-400 text-xs pt-1">{{ $strings.LabelStarted }} {{ $formatDate(userProgressStartedAt, dateFormat) }}</p> <p class="text-gray-400 text-xs pt-1">{{ $strings.LabelStarted }} {{ $formatDate(userProgressStartedAt, dateFormat) }}</p>
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick"> <div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
<span class="material-symbols text-sm">close</span> <span class="material-symbols text-sm">&#xe5cd;</span>
</div> </div>
</div> </div>
<!-- Icon buttons --> <!-- Icon buttons -->
<div class="flex items-center justify-center md:justify-start pt-4"> <div class="flex items-center justify-center md:justify-start pt-4">
<ui-btn v-if="showPlayButton" :disabled="isStreaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playItem"> <ui-btn v-if="showPlayButton" :disabled="isStreaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playItem">
<span v-show="!isStreaming" class="material-symbols fill text-2xl -ml-2 pr-1 text-white">play_arrow</span> <span v-show="!isStreaming" class="material-symbols fill text-2xl -ml-2 pr-1 text-white">&#xe037;</span>
{{ isStreaming ? $strings.ButtonPlaying : $strings.ButtonPlay }} {{ isStreaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
</ui-btn> </ui-btn>
@@ -106,7 +106,7 @@
</ui-btn> </ui-btn>
<ui-tooltip v-if="userCanUpdate" :text="$strings.LabelEdit" direction="top"> <ui-tooltip v-if="userCanUpdate" :text="$strings.LabelEdit" direction="top">
<ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" /> <ui-icon-btn icon="&#xe3c9;" outlined class="mx-0.5" @click="editClick" />
</ui-tooltip> </ui-tooltip>
<ui-tooltip v-if="!isPodcast && !isMusic" :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top"> <ui-tooltip v-if="!isPodcast && !isMusic" :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
@@ -121,7 +121,7 @@
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="148" @action="contextMenuAction"> <ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="148" @action="contextMenuAction">
<template #default="{ showMenu, clickShowMenu, disabled }"> <template #default="{ showMenu, clickShowMenu, disabled }">
<button type="button" :disabled="disabled" class="mx-0.5 icon-btn bg-primary border border-gray-600 w-9 h-9 rounded-md flex items-center justify-center relative" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu"> <button type="button" :disabled="disabled" class="mx-0.5 icon-btn bg-primary border border-gray-600 w-9 h-9 rounded-md flex items-center justify-center relative" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
<span class="material-symbols text-2xl">more_horiz</span> <span class="material-symbols text-2xl">&#xe5d3;</span>
</button> </button>
</template> </template>
</ui-context-menu-dropdown> </ui-context-menu-dropdown>
@@ -129,9 +129,7 @@
<div class="my-4 w-full"> <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> <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>
<button v-if="isDescriptionClamped" class="py-0.5 flex items-center text-slate-300 hover:text-white" @click="showFullDescription = !showFullDescription"> <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>
{{ showFullDescription ? $strings.ButtonReadLess : $strings.ButtonReadMore }} <span class="material-symbols text-xl pl-1">{{ showFullDescription ? 'expand_less' : 'expand_more' }}</span>
</button>
</div> </div>
<tables-chapters-table v-if="chapters.length" :library-item="libraryItem" class="mt-6" /> <tables-chapters-table v-if="chapters.length" :library-item="libraryItem" class="mt-6" />
@@ -486,23 +484,23 @@ export default {
this.$axios this.$axios
.$get(`/api/podcasts/${this.libraryItemId}/clear-queue`) .$get(`/api/podcasts/${this.libraryItemId}/clear-queue`)
.then(() => { .then(() => {
this.$toast.success('Episode download queue cleared') this.$toast.success(this.$strings.ToastEpisodeDownloadQueueClearSuccess)
this.episodeDownloadQueued = [] this.episodeDownloadQueued = []
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to clear queue', error) console.error('Failed to clear queue', error)
this.$toast.error('Failed to clear queue') this.$toast.error(this.$strings.ToastEpisodeDownloadQueueClearFailed)
}) })
} }
}, },
async findEpisodesClick() { async findEpisodesClick() {
if (!this.mediaMetadata.feedUrl) { if (!this.mediaMetadata.feedUrl) {
return this.$toast.error('Podcast does not have an RSS Feed') return this.$toast.error(this.$strings.ToastNoRSSFeed)
} }
this.fetchingRSSFeed = true this.fetchingRSSFeed = true
var payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: this.mediaMetadata.feedUrl }).catch((error) => { var payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: this.mediaMetadata.feedUrl }).catch((error) => {
console.error('Failed to get feed', error) console.error('Failed to get feed', error)
this.$toast.error('Failed to get podcast feed') this.$toast.error(this.$strings.ToastPodcastGetFeedFailed)
return null return null
}) })
this.fetchingRSSFeed = false this.fetchingRSSFeed = false
@@ -511,7 +509,7 @@ export default {
console.log('Podcast feed', payload) console.log('Podcast feed', payload)
const podcastfeed = payload.podcast const podcastfeed = payload.podcast
if (!podcastfeed.episodes || !podcastfeed.episodes.length) { if (!podcastfeed.episodes || !podcastfeed.episodes.length) {
this.$toast.info('No episodes found in RSS feed') this.$toast.info(this.$strings.ToastPodcastNoEpisodesInFeed)
return return
} }
@@ -580,7 +578,7 @@ export default {
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: this.title, subtitle: this.title,
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
duration: episode.audioFile.duration || null, duration: episode.audioFile.duration || null,
coverPath: this.libraryItem.media.coverPath || null coverPath: this.libraryItem.media.coverPath || null
}) })
@@ -624,13 +622,12 @@ export default {
}, },
clearProgressClick() { clearProgressClick() {
if (!this.userMediaProgress) return if (!this.userMediaProgress) return
if (confirm(`Are you sure you want to reset your progress?`)) { if (confirm(this.$strings.MessageConfirmResetProgress)) {
this.resettingProgress = true this.resettingProgress = true
this.$axios this.$axios
.$delete(`/api/me/progress/${this.userMediaProgress.id}`) .$delete(`/api/me/progress/${this.userMediaProgress.id}`)
.then(() => { .then(() => {
console.log('Progress reset complete') console.log('Progress reset complete')
this.$toast.success(`Your progress was reset`)
this.resettingProgress = false this.resettingProgress = false
}) })
.catch((error) => { .catch((error) => {
@@ -724,12 +721,12 @@ export default {
this.$axios this.$axios
.$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`) .$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`)
.then(() => { .then(() => {
this.$toast.success('Item deleted') this.$toast.success(this.$strings.ToastItemDeletedSuccess)
this.$router.replace(`/library/${this.libraryId}`) this.$router.replace(`/library/${this.libraryId}`)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to delete item', error) console.error('Failed to delete item', error)
this.$toast.error('Failed to delete item') this.$toast.error(this.$strings.ToastItemDeleteFailed)
}) })
} }
}, },
@@ -61,6 +61,8 @@ export default {
const bDesc = this.authorSortDesc ? -1 : 1 const bDesc = this.authorSortDesc ? -1 : 1
return this.authors.sort((a, b) => { return this.authors.sort((a, b) => {
if (typeof a[sortProp] === 'number' && typeof b[sortProp] === 'number') { if (typeof a[sortProp] === 'number' && typeof b[sortProp] === 'number') {
// Fallback to name sort if equal
if (a[sortProp] === b[sortProp]) return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }) * bDesc
return a[sortProp] > b[sortProp] ? bDesc : -bDesc return a[sortProp] > b[sortProp] ? bDesc : -bDesc
} }
return a[sortProp]?.localeCompare(b[sortProp], undefined, { sensitivity: 'base' }) * bDesc return a[sortProp]?.localeCompare(b[sortProp], undefined, { sensitivity: 'base' }) * bDesc
+2 -2
View File
@@ -138,7 +138,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove narrator', error) console.error('Failed to remove narrator', error)
this.$toast.error('Failed to remove narrator') this.$toast.error(this.$strings.ToastRemoveFailed)
this.loading = false this.loading = false
}) })
}, },
@@ -158,4 +158,4 @@ export default {
}, },
beforeDestroy() {} beforeDestroy() {}
} }
</script> </script>
@@ -111,7 +111,7 @@ export default {
this.processing = true this.processing = true
const queuePayload = await this.$axios.$get(`/api/libraries/${this.libraryId}/episode-downloads`).catch((error) => { const queuePayload = await this.$axios.$get(`/api/libraries/${this.libraryId}/episode-downloads`).catch((error) => {
console.error('Failed to get download queue', error) console.error('Failed to get download queue', error)
this.$toast.error('Failed to get download queue') this.$toast.error(this.$strings.ToastFailedToLoadData)
return null return null
}) })
this.processing = false this.processing = false
@@ -48,7 +48,7 @@
<p dir="auto" class="text-sm text-gray-200 mb-4 line-clamp-4" v-html="episode.subtitle || episode.description" /> <p dir="auto" class="text-sm text-gray-200 mb-4 line-clamp-4" v-html="episode.subtitle || episode.description" />
<div class="flex items-center"> <div class="flex items-center">
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)"> <button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress?.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)">
<span v-if="episodeIdStreaming === episode.id" class="material-symbols text-2xl" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span> <span v-if="episodeIdStreaming === episode.id" class="material-symbols text-2xl" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
<span v-else class="material-symbols fill text-2xl text-success">play_arrow</span> <span v-else class="material-symbols fill text-2xl text-success">play_arrow</span>
<p class="pl-2 pr-1 text-sm font-semibold">{{ getButtonText(episode) }}</p> <p class="pl-2 pr-1 text-sm font-semibold">{{ getButtonText(episode) }}</p>
@@ -56,9 +56,10 @@
<ui-tooltip v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" :text="playerQueueEpisodeIdMap[episode.id] ? $strings.MessageRemoveFromPlayerQueue : $strings.MessageAddToPlayerQueue" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" direction="top"> <ui-tooltip v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" :text="playerQueueEpisodeIdMap[episode.id] ? $strings.MessageRemoveFromPlayerQueue : $strings.MessageAddToPlayerQueue" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" direction="top">
<ui-icon-btn :icon="playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_play'" borderless @click="queueBtnClick(episode)" /> <ui-icon-btn :icon="playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_play'" borderless @click="queueBtnClick(episode)" />
<!-- <button class="h-8 w-8 flex justify-center items-center mx-2" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" @click.stop="queueBtnClick(episode)"> </ui-tooltip>
<span class="material-symbols-outlined text-2xl">{{ playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_add' }}</span>
</button> --> <ui-tooltip :text="!!episode.progress?.isFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
<ui-read-icon-btn :disabled="episodesProcessingMap[episode.id]" :is-read="!!episode.progress?.isFinished" borderless class="mx-1 mt-0.5" @click="toggleEpisodeFinished(episode)" />
</ui-tooltip> </ui-tooltip>
<ui-tooltip :text="$strings.LabelYourPlaylists" direction="top"> <ui-tooltip :text="$strings.LabelYourPlaylists" direction="top">
@@ -98,6 +99,7 @@ export default {
data() { data() {
return { return {
recentEpisodes: [], recentEpisodes: [],
episodesProcessingMap: {},
totalEpisodes: 0, totalEpisodes: 0,
currentPage: 0, currentPage: 0,
processing: false, processing: false,
@@ -143,6 +145,44 @@ export default {
} }
}, },
methods: { methods: {
async toggleEpisodeFinished(episode, confirmed = false) {
if (this.episodesProcessingMap[episode.id]) {
console.warn('Episode is already processing')
return
}
const isFinished = !!episode.progress?.isFinished
const itemProgressPercent = episode.progress?.progress || 0
if (!isFinished && itemProgressPercent > 0 && !confirmed) {
const payload = {
message: `Are you sure you want to mark "${episode.title}" as finished?`,
callback: (confirmed) => {
if (confirmed) {
this.toggleEpisodeFinished(episode, true)
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
return
}
const updatePayload = {
isFinished: !isFinished
}
this.$set(this.episodesProcessingMap, episode.id, true)
this.$axios
.$patch(`/api/me/progress/${episode.libraryItemId}/${episode.id}`, updatePayload)
.catch((error) => {
console.error('Failed to update progress', error)
this.$toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed)
})
.finally(() => {
this.$set(this.episodesProcessingMap, episode.id, false)
})
},
clickAddToPlaylist(episode) { clickAddToPlaylist(episode) {
// Makeshift libraryItem // Makeshift libraryItem
const libraryItem = { const libraryItem = {
@@ -194,7 +234,7 @@ export default {
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: episode.podcast.metadata.title, subtitle: episode.podcast.metadata.title,
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
duration: episode.duration || null, duration: episode.duration || null,
coverPath: episode.podcast.coverPath || null coverPath: episode.podcast.coverPath || null
}) })
@@ -211,11 +251,10 @@ export default {
this.processing = true this.processing = true
const episodePayload = await this.$axios.$get(`/api/libraries/${this.libraryId}/recent-episodes?limit=25&page=${page}`).catch((error) => { const episodePayload = await this.$axios.$get(`/api/libraries/${this.libraryId}/recent-episodes?limit=25&page=${page}`).catch((error) => {
console.error('Failed to get recent episodes', error) console.error('Failed to get recent episodes', error)
this.$toast.error('Failed to get recent episodes') this.$toast.error(this.$strings.ToastFailedToLoadData)
return null return null
}) })
this.processing = false this.processing = false
console.log('Episodes', episodePayload)
this.recentEpisodes = episodePayload.episodes || [] this.recentEpisodes = episodePayload.episodes || []
this.totalEpisodes = episodePayload.total this.totalEpisodes = episodePayload.total
this.currentPage = page this.currentPage = page
@@ -232,7 +271,7 @@ export default {
episodeId: episode.id, episodeId: episode.id,
title: episode.title, title: episode.title,
subtitle: episode.podcast.metadata.title, subtitle: episode.podcast.metadata.title,
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date', caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
duration: episode.duration || null, duration: episode.duration || null,
coverPath: episode.podcast.coverPath || null coverPath: episode.podcast.coverPath || null
} }
@@ -146,7 +146,7 @@ export default {
this.processing = true this.processing = true
var payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed }).catch((error) => { var payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed }).catch((error) => {
console.error('Failed to get feed', error) console.error('Failed to get feed', error)
this.$toast.error('Failed to get podcast feed') this.$toast.error(this.$strings.ToastPodcastGetFeedFailed)
return null return null
}) })
this.processing = false this.processing = false
@@ -197,7 +197,7 @@ export default {
this.processing = true this.processing = true
const payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: podcast.feedUrl }).catch((error) => { const payload = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: podcast.feedUrl }).catch((error) => {
console.error('Failed to get feed', error) console.error('Failed to get feed', error)
this.$toast.error('Failed to get podcast feed') this.$toast.error(this.$strings.ToastPodcastGetFeedFailed)
return null return null
}) })
this.processing = false this.processing = false
+2 -2
View File
@@ -31,7 +31,7 @@
<div :key="author.id" class="w-full py-2"> <div :key="author.id" class="w-full py-2">
<div class="flex items-center mb-1"> <div class="flex items-center mb-1">
<p class="text-sm text-white text-opacity-70 w-36 pr-2 truncate"> <p class="text-sm text-white text-opacity-70 w-36 pr-2 truncate">
{{ index + 1 }}.&nbsp;&nbsp;&nbsp;&nbsp;<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`" class="hover:underline">{{ author.name }}</nuxt-link> {{ index + 1 }}.&nbsp;&nbsp;&nbsp;&nbsp;<nuxt-link :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}</nuxt-link>
</p> </p>
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden"> <div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * author.count) / mostUsedAuthorCount) + '%' }" /> <div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * author.count) / mostUsedAuthorCount) + '%' }" />
@@ -75,7 +75,7 @@
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * ab.size) / largestItemSize) + '%' }" /> <div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * ab.size) / largestItemSize) + '%' }" />
</div> </div>
<div class="w-4 ml-3"> <div class="w-4 ml-3">
<p class="text-sm font-bold">{{ $bytesPretty(ab.size) }}</p> <p class="text-sm font-bold whitespace-nowrap">{{ $bytesPretty(ab.size) }}</p>
</div> </div>
</div> </div>
</div> </div>
+2 -2
View File
@@ -132,11 +132,11 @@ export default {
methods: { methods: {
async submitServerSetup() { async submitServerSetup() {
if (!this.newRoot.username || !this.newRoot.username.trim()) { if (!this.newRoot.username || !this.newRoot.username.trim()) {
this.$toast.error('Must enter a root username') this.$toast.error(this.$strings.ToastUserRootRequireName)
return return
} }
if (this.newRoot.password !== this.confirmPassword) { if (this.newRoot.password !== this.confirmPassword) {
this.$toast.error('Password mismatch') this.$toast.error(this.$strings.ToastUserPasswordMismatch)
return return
} }
if (!this.newRoot.password) { if (!this.newRoot.password) {

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