Compare commits

...

304 Commits

Author SHA1 Message Date
advplyr cf5598aeb9 Version bump v2.14.0 2024-10-05 16:10:07 -05:00
advplyr 8cf3d648ea Merge pull request #3478 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-10-05 15:58:50 -05:00
SunSpring 212311a980 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (990 of 990 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-10-05 06:45:04 +02:00
biuklija c9522dc25d Translated using Weblate (Croatian)
Currently translated at 100.0% (990 of 990 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-10-03 23:34:54 +00:00
kuci-JK 37af753402 Translated using Weblate (Czech)
Currently translated at 88.8% (880 of 990 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2024-10-03 23:34:54 +00:00
advplyr d8c5627cf8 Update audio player volume slider to be vertical 2024-10-03 18:34:43 -05:00
advplyr 4f926b37db Update:Remove server settings update toast on config page 2024-10-02 18:10:51 -05:00
advplyr fefc16bd13 Fix:Use region for author search by name #3470 2024-10-01 16:14:51 -05:00
advplyr 1b1b71a9b6 Merge pull request #3468 from mikiher/nunicode-intergration
Nunicode integration
2024-10-01 15:17:54 -05:00
mikiher 086532652e Fix to NUSQLITE3_PATH in index.js 2024-10-01 17:15:44 +03:00
mikiher 4e8b4720a1 Merge branch 'nunicode-intergration' of https://github.com/mikiher/audiobookshelf into nunicode-intergration 2024-10-01 16:48:14 +03:00
mikiher 4a7ada28fb Switch to nunicode-binaries v1.1 2024-10-01 16:47:40 +03:00
advplyr 1710285674 Merge pull request #3460 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-09-30 17:19:04 -05:00
tonttula a6bb61d998 Translated using Weblate (Finnish)
Currently translated at 44.0% (436 of 990 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2024-09-30 22:11:00 +00:00
gallegonovato 5ec05dfa84 Translated using Weblate (Spanish)
Currently translated at 100.0% (990 of 990 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-09-30 22:11:00 +00:00
Alexander Künzel 83e854aa13 Translated using Weblate (German)
Currently translated at 99.1% (982 of 990 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-09-30 22:10:59 +00:00
thehijacker 634f809159 Translated using Weblate (Slovenian)
Currently translated at 100.0% (990 of 990 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-09-30 22:10:59 +00:00
Hosted Weblate e5cf141834 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-30 22:10:58 +00:00
biuklija 8610b68d3f Translated using Weblate (Croatian)
Currently translated at 100.0% (1007 of 1007 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-09-30 22:10:57 +00:00
Alexander Künzel f3e3bddc94 Translated using Weblate (German)
Currently translated at 99.2% (999 of 1007 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-09-30 22:10:56 +00:00
tonttula 7ef3284cc5 Translated using Weblate (Finnish)
Currently translated at 42.9% (433 of 1007 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2024-09-30 22:10:55 +00:00
Mihály Hunyady 3494586f77 Translated using Weblate (Hungarian)
Currently translated at 81.1% (817 of 1007 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2024-09-30 22:10:55 +00:00
biuklija faaf99e6bb Translated using Weblate (Croatian)
Currently translated at 100.0% (1007 of 1007 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-09-30 22:10:54 +00:00
tonttula 1078ba2111 Translated using Weblate (Finnish)
Currently translated at 35.1% (354 of 1007 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2024-09-30 22:10:53 +00:00
ti777777 2ad69300f5 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 74.9% (755 of 1007 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hant/
2024-09-30 22:10:52 +00:00
Soaibuzzaman d2f3fa7fdf Translated using Weblate (Bengali)
Currently translated at 100.0% (1007 of 1007 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bn/
2024-09-30 22:10:52 +00:00
advplyr 64fcb6270b Fix i18n strings out of order 2024-09-30 17:10:42 -05:00
advplyr 562c30cff4 Replace failed to update toasts with one generic string 2024-09-29 17:53:52 -05:00
advplyr 7108501d24 Add libnusqlite3 to gitignore 2024-09-29 11:37:13 -05:00
mikiher 37eae3406c Remove debug messages 2024-09-29 12:27:30 +03:00
mikiher 501dc938e6 Add Nunicode sqlite extension integration 2024-09-29 09:22:39 +03:00
advplyr c5ecd35fe9 Remove toasts after uploading #3352 2024-09-28 16:41:56 -05:00
advplyr 7cd8d7f44d Update NotificationManager to singleton 2024-09-27 17:33:23 -05:00
advplyr 567a9a4e58 Fix:API /libraries/:library/items validate limit and page are positive integers #3459 2024-09-26 16:48:38 -05:00
advplyr 58f4a0cfbb Merge pull request #3461 from mpgirro/oci-image-source
Add OpenContainers Annotations as Labels to Docker Image
2024-09-26 16:34:23 -05:00
Maximilian Irro e6c0b697aa Add OpenContainer Image Format Annotations as Labels to Docker Image 2024-09-26 21:02:46 +02:00
advplyr 35f60d699d Merge pull request #3427 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-09-25 17:02:36 -05:00
Vili Kangas c219be0970 Translated using Weblate (Finnish)
Currently translated at 31.7% (320 of 1007 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2024-09-25 23:56:14 +02:00
Charlie c72ce843fa Translated using Weblate (French)
Currently translated at 100.0% (1007 of 1007 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-09-25 23:56:14 +02:00
Charlie c606059a3a Translated using Weblate (French)
Currently translated at 100.0% (1007 of 1007 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-09-25 23:56:14 +02:00
Philip Karlsson 049a8bdc6d Translated using Weblate (Swedish)
Currently translated at 70.5% (710 of 1007 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2024-09-25 23:56:14 +02:00
burghy86 9752f744ca Translated using Weblate (Italian)
Currently translated at 96.8% (975 of 1007 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2024-09-25 23:56:14 +02:00
biuklija 4be6fb789c Translated using Weblate (Croatian)
Currently translated at 98.2% (989 of 1007 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-09-25 23:56:14 +02:00
thehijacker afc56e5259 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1007 of 1007 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-09-25 23:56:14 +02:00
SunSpring d47f8521d5 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1007 of 1007 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-09-25 23:56:14 +02:00
gallegonovato 7f853d426a Translated using Weblate (Spanish)
Currently translated at 100.0% (1007 of 1007 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-09-25 23:56:14 +02:00
burghy86 e9008c615d Translated using Weblate (Italian)
Currently translated at 100.0% (975 of 975 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2024-09-25 23:56:14 +02:00
Charlie 01f081ef5a Translated using Weblate (French)
Currently translated at 100.0% (975 of 975 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-09-25 23:56:14 +02:00
Charlie 7ee174e0d5 Translated using Weblate (French)
Currently translated at 100.0% (975 of 975 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-09-25 23:56:14 +02:00
thehijacker 24439f86e0 Translated using Weblate (Slovenian)
Currently translated at 100.0% (975 of 975 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-09-25 23:56:14 +02:00
Reimo Vellemaa fbd3ce3b72 Translated using Weblate (Estonian)
Currently translated at 77.8% (759 of 975 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/et/
2024-09-25 23:56:14 +02:00
Reimo Vellemaa 96f8b54b51 Translated using Weblate (Estonian)
Currently translated at 77.7% (758 of 975 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/et/
2024-09-25 23:56:14 +02:00
SunSpring 9c94a78e29 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (975 of 975 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-09-25 23:56:14 +02:00
RafalHo a14e3dd137 Translated using Weblate (Polish)
Currently translated at 82.2% (802 of 975 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2024-09-25 23:56:14 +02:00
Milo Ivir e37673bd67 Translated using Weblate (Croatian)
Currently translated at 100.0% (975 of 975 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-09-25 23:56:14 +02:00
Milo Ivir 6aa10d20a1 Translated using Weblate (Croatian)
Currently translated at 99.8% (974 of 975 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-09-25 23:56:14 +02:00
Lasse Slotmann 68a92acb7a Translated using Weblate (Danish)
Currently translated at 69.5% (678 of 975 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2024-09-25 23:56:14 +02:00
gallegonovato 8aa7cc9ca5 Translated using Weblate (Spanish)
Currently translated at 99.7% (973 of 975 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-09-25 23:56:14 +02:00
Lasse Slotmann e6c087c3bb Translated using Weblate (Danish)
Currently translated at 69.2% (675 of 975 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2024-09-25 23:56:14 +02:00
Mario 39a2097152 Translated using Weblate (German)
Currently translated at 100.0% (975 of 975 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-09-25 23:56:14 +02:00
Lasse Slotmann 6a8003917e Translated using Weblate (Danish)
Currently translated at 68.6% (669 of 975 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2024-09-25 23:56:14 +02:00
gfbdrgng d5a17ddc8c Translated using Weblate (Russian)
Currently translated at 100.0% (975 of 975 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2024-09-25 23:56:14 +02:00
advplyr 48bbf0d649 Merge pull request #3453 from glorenzen/center-play-button
Center Play Button
2024-09-25 16:56:09 -05:00
advplyr 0bc58c254f Update playback speed to no longer use font-mono, adjust position of popup 2024-09-25 16:49:24 -05:00
Greg Lorenzen b2d41f0583 Move playback speed control next to player volume control 2024-09-24 23:17:26 +00:00
Greg Lorenzen 0d31d20f0f Center align player chapter title 2024-09-24 23:00:19 +00:00
advplyr bb7938f66d Update:When merging embedded chapters from multiple files filter out ~0 duration chapters #3361 2024-09-24 10:54:25 -05:00
advplyr 5b22e945da Update:Format numbers on user listening stats chart #3441 2024-09-23 16:36:56 -05:00
advplyr decde230aa Update:Some logs to include library item id #3440 2024-09-22 14:15:17 -05:00
advplyr 1dec8ae122 Update:Added string localization for tasks #3303 #3352 2024-09-21 14:02:57 -05:00
advplyr 8512d5e693 Update Task object to handle translation keys with subs 2024-09-20 17:18:29 -05:00
advplyr bb481ccfb4 Update:Chapters page populate ASIN input in lookup modal after match #3428 2024-09-19 17:21:41 -05:00
advplyr 12bce48ef5 Update:Home page refetch items when scanning in first items 2024-09-18 15:30:24 -05:00
advplyr 013c7c776e Merge pull request #3436 from mikiher/create-playback-session-race-condition
Change PlaybackSession createFromOld to use upsert instead of create
2024-09-18 15:14:03 -05:00
advplyr 8f96d20a23 Merge pull request #3435 from mikiher/comic-book-extractors
Move to node-unrar-js for cbr and node-stream-zip for cbz
2024-09-18 14:52:32 -05:00
advplyr 1a8811b69a Remove unused requires 2024-09-18 14:26:10 -05:00
mikiher d796849d74 Small change to logging of unhandled rejections 2024-09-18 18:44:16 +03:00
mikiher 942bd0859f Change PlaybackSession createFromOld to use upsert instead of create 2024-09-18 18:01:36 +03:00
mikiher 072028c740 Cleanup empty directiories inside the temp extraction dir 2024-09-18 10:16:46 +03:00
mikiher 0d08aecd56 Move from libarchive to node-unrar-js for cbr and node-stream-zip for cbz 2024-09-18 08:28:15 +03:00
advplyr 22ad16e11b Fix:Server crash on scan for library with no metadataPrecedence set #3434 2024-09-17 16:10:32 -05:00
advplyr 2f49a08c7d Merge pull request #3425 from wommy/postcss-options
added postcssOptions to remove npm warning
2024-09-16 14:22:42 -05:00
wommy fcacda74cb added postcssOptions to remove npm warning 2024-09-15 18:29:23 -04:00
advplyr fa0c90de70 Merge pull request #3422 from mikiher/parse-comic-metadata-try-catch
Catch file extraction errors in parseComicMetadata
2024-09-15 16:10:13 -05:00
advplyr c1197314ac Merge pull request #3397 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-09-15 15:34:29 -05:00
mikiher 0b31792660 catch file extraction errors in parseComicMetadata 2024-09-15 11:48:33 +03:00
advplyr b35fabbe55 Update:Collection & playlist Play button renamed to Play All #3320 2024-09-14 16:04:50 -05:00
biuklija 8cd8a157a6 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/
2024-09-14 23:04:50 +02:00
burghy86 86aece6828 Translated using Weblate (Italian)
Currently translated at 92.8% (904 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2024-09-14 23:04:50 +02:00
gfbdrgng f9edadbafd 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/
2024-09-14 23:04:49 +02:00
biuklija 6a388cd4fe 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/
2024-09-14 23:04:49 +02:00
J. Lavoie 9d17e9ff48 Translated using Weblate (Italian)
Currently translated at 89.6% (873 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2024-09-14 23:04:49 +02:00
J. Lavoie 662b7d01b8 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/
2024-09-14 23:04:49 +02:00
J. Lavoie a19bc4b4e4 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/
2024-09-14 23:04:49 +02:00
Charlie a545aa5c39 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/
2024-09-14 23:04:49 +02:00
advplyr 8493e56b11 Merge pull request #3418 from mikiher/fix-database-version-init
Fix MigrationManager initial run behavior
2024-09-14 10:09:46 -05:00
mikiher 21c77dccce Add server migration scripts to pkg assets 2024-09-14 13:05:21 +03:00
mikiher 55164803b0 Fix migrationMeta database version initial value, and move isDatabaseNew logic inside MigrationManager 2024-09-14 08:01:32 +03:00
advplyr 5c49a8ce6a Merge pull request #3407 from agraubert/patch-1
Default deny explicit content to users
2024-09-13 13:24:12 -05:00
advplyr 854f308eae Merge pull request #3410 from mikiher/library-scan-try-catch
Handle library scan failure gracefully
2024-09-13 13:10:46 -05:00
advplyr 16ba6b53ba Merge pull request #3414 from thatguy7/master
Improved handling of Authors and Series with names containing non-ASCII characters
2024-09-13 12:59:01 -05:00
Oleg Ivasenko 0af29a378a use asciiOnlyToLowerCase to match lower function behaviour of SQLite 2024-09-13 17:09:32 +00:00
Oleg Ivasenko def34a860b when checking if series/author is alread in DB, use case insensitive match only for ASCII names 2024-09-13 16:23:25 +00:00
mikiher f8034e1b78 scanLibrary fail and cancel handling round 2 2024-09-13 09:23:48 +03:00
advplyr 01fbea02f1 Clean out old unused functions, Device updates for replacing DeviceInfo 2024-09-12 16:36:39 -05:00
advplyr 3d9af89e24 Merge pull request #3411 from justcallmelarry/feature/add-duration-when-creating-sessions
Add duration to local sessions on creation
2024-09-12 15:23:10 -05:00
Lauri Vuorela d430d9f3ed add new setDuration and use that 2024-09-12 20:05:08 +02:00
Lauri Vuorela 0c24a1e626 add duration to session when creating 2024-09-12 19:46:08 +02:00
mikiher 1099dbe642 Handle library scan failure gracefully 2024-09-12 18:56:52 +03:00
Aaron Graubert 2df3277dcd Server side change to enable default explicit acces for admins 2024-09-11 23:09:04 -06:00
Aaron Graubert 6ae14213f5 Related ui changes for removing default explicit access 2024-09-11 23:08:00 -06:00
Aaron Graubert 61bd029303 Default deny explicit content to users 2024-09-11 22:42:21 -06:00
advplyr 5b09bd8242 Merge pull request #3374 from wommy/update-nuxt-2.18.1
client update: nuxt 2.17.3 -> 2.18.1
2024-09-11 16:28:08 -05:00
advplyr 703477b157 Merge pull request #3405 from mikiher/logger-fixes
Log non-strings into log file like console.log does
2024-09-11 14:31:05 -05:00
mikiher 03ff5d8ae1 Disregard socketListener.level if level >= FATAL 2024-09-11 22:05:38 +03:00
mikiher 220f7ef7cd Resolve some weird unrelated flakiness in BookFinder test 2024-09-11 21:40:31 +03:00
mikiher 682a99dd43 Log non-strings into log file like console.log does 2024-09-11 19:58:30 +03:00
advplyr fac5de582d Merge pull request #3378 from mikiher/migration-manager
Add db migration management infratructure
2024-09-10 16:50:39 -05:00
advplyr 7cbf9de8ca Update migrations jsdocs 2024-09-10 15:57:07 -05:00
advplyr ce213c3d89 Version bump v2.13.4 2024-09-09 16:15:44 -05:00
advplyr 32cd0360e6 Merge pull request #3371 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-09-09 16:11:21 -05:00
Mario 1ec23a5699 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/
2024-09-09 23:04:25 +02:00
Soaibuzzaman 48330f6432 Translated using Weblate (Bengali)
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/bn/
2024-09-09 23:04:25 +02:00
thehijacker 28358debbc 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-09 23:04:25 +02:00
SunSpring 54b7ed6117 Translated using Weblate (Chinese (Simplified Han script))
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/zh_Hans/
2024-09-09 23:04:25 +02:00
gallegonovato 0cfd2ee63b 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/
2024-09-09 23:04:25 +02:00
thehijacker 37a0990741 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-09 23:04:25 +02:00
advplyr 7a0cd1eb34 Merge pull request #3396 from mikiher/custom-provider-try-catch
Add a try-catch block around custom provider search
2024-09-09 16:04:20 -05:00
advplyr ac3277da09 Merge pull request #3395 from mikiher/quick-match-new-series
Fix crash when quick match adds new series
2024-09-09 16:03:30 -05:00
advplyr 65d1e7be56 Merge pull request #3394 from mikiher/webp-embed
Convert webp images to jpeg during metadata embed
2024-09-09 16:02:17 -05:00
mikiher 80685afa7e Add a try-catch block around custom provider search 2024-09-09 19:23:26 +03:00
mikiher f892453892 Fix crash when quick match adds new series 2024-09-09 18:36:12 +03:00
mikiher 422bb8c31c Convert webp images to jpeg during metadata embed 2024-09-09 15:28:53 +03:00
mikiher 6fb1202c1c Put umzug in server/libs and remove unneeded dependencies from it 2024-09-08 21:33:32 +03:00
advplyr 4ddd2788f0 Fix:Byte conversion to use 1000 instead of 1024 to be accurate with abbrevs #3386 2024-09-07 16:52:42 -05:00
mikiher 8a28029809 Make migration management more robust 2024-09-07 22:24:19 +03:00
advplyr 423a2129d1 Update:Format number for entity total in bookshelf toolbar #3370 2024-09-06 17:01:48 -05:00
advplyr a338097514 Update:Cleanup logging on library item update #3362 2024-09-06 16:58:40 -05:00
advplyr 84b67abb03 Fix:Get all collections API endpoint crashing server #3372 2024-09-05 17:15:38 -05:00
advplyr 5ec8406653 Cleanup Collection model to remove oldCollection references 2024-09-04 18:00:59 -05:00
mikiher b3ce300d32 Fix some packaging and dependency issues 2024-09-04 23:55:16 +03:00
mikiher 3f93b93d9e Add db migration management infratructure 2024-09-04 12:48:10 +03:00
wommy e32c83db63 npm update nuxt: 2.17.3 -> 2.18.1 2024-09-03 19:36:58 -04:00
advplyr 0344a63b48 Clean out old unused objects 2024-09-03 17:04:58 -05:00
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
advplyr e81b3461b2 Version bump v2.12.2 2024-08-08 17:17:22 -05:00
advplyr 9345cb3934 Update version check get changelogs 2024-08-08 17:04:50 -05:00
advplyr eb36a0b3dd Merge pull request #3247 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-08-08 16:58:06 -05:00
advplyr 7e442ecb3d Revert MemoryStore used in expressSession 2024-08-08 16:54:48 -05:00
tonttula f07c5eb725 Translated using Weblate (Finnish)
Currently translated at 32.2% (275 of 853 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2024-08-08 23:54:39 +02:00
SunSpring a486be92cb Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (853 of 853 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-08-08 23:54:39 +02:00
tonttula 4d84060036 Translated using Weblate (Finnish)
Currently translated at 26.2% (224 of 853 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2024-08-08 23:54:39 +02:00
tonttula fc503691fe Translated using Weblate (Finnish)
Currently translated at 25.9% (221 of 853 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2024-08-08 23:54:39 +02:00
Charlie c80dd43a3e Translated using Weblate (French)
Currently translated at 99.7% (851 of 853 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-08-08 23:54:39 +02:00
Charlie a4a62e0c18 Translated using Weblate (French)
Currently translated at 99.7% (851 of 853 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-08-08 23:54:39 +02:00
advplyr 2f98cb9b6d Update:Changelog shows all releases matching minor version and lower than current #3250 2024-08-07 17:56:55 -05:00
advplyr 91dc6eebb0 Merge pull request #3254 from mikiher/unc-path-support
Fix path normalization to support UNC paths
2024-08-07 15:27:53 -05:00
mikiher d72e0a4418 Fix path normalization to support UNC paths 2024-08-07 21:18:53 +03:00
advplyr 2c8ebd43cc Update ApiCacheManager unit test 2024-08-06 17:26:36 -05:00
advplyr 9f561aa296 Update:Skip library api cache for random sort #3249 2024-08-06 17:20:53 -05:00
advplyr 930bacd45d Fix:Podcast episode download request failing due to user-agent string #3246 2024-08-05 16:59:37 -05: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
advplyr 45c97a778d Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-08-05 09:01:53 -05:00
advplyr 6ebc64f73b Version bump v2.12.1 2024-08-05 09:01:45 -05:00
advplyr 52807d0d49 Merge pull request #3233 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-08-05 08:58:13 -05:00
SunSpring a5e18e99bc Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (853 of 853 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-08-05 15:55:41 +02:00
Charlie f545b3e745 Translated using Weblate (French)
Currently translated at 99.7% (851 of 853 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-08-05 15:55:41 +02:00
Vito0912 e0877803e3 Translated using Weblate (German)
Currently translated at 100.0% (853 of 853 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-08-05 15:55:41 +02:00
advplyr 4916887c8d Merge pull request #3236 from devnoname120/arm64/fix-unicode-so-load
Fix unicode.so loading
2024-08-05 08:55:37 -05:00
Paul 20eb573897 Fix unicode.so loading
`unicode.so` is dynamically linked against glibc but Alpine only comes with musl. Installing the compatibility layer fixes it.

Fix https://github.com/advplyr/audiobookshelf/issues/3231
Fix https://github.com/advplyr/audiobookshelf/issues/3234
Close https://github.com/advplyr/audiobookshelf/pull/3235
2024-08-05 13:34:56 +02:00
290 changed files with 16278 additions and 12997 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.
+1
View File
@@ -70,6 +70,7 @@ jobs:
uses: docker/build-push-action@v3
with:
tags: ${{ github.event.inputs.tags || steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
context: .
platforms: linux/amd64,linux/arm64
push: true
+7 -5
View File
@@ -1,13 +1,15 @@
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:
pull_request:
push:
paths:
- docs/
pull_request:
paths:
- docs/
- 'docs/**'
# This action only needs read permissions
permissions:
contents: read
jobs:
build:
+1
View File
@@ -16,6 +16,7 @@
/ffmpeg*
/ffprobe*
/unicode*
/libnusqlite3*
sw.*
.DS_STORE
+25 -8
View File
@@ -11,19 +11,36 @@ FROM node:20-alpine
ENV NODE_ENV=production
RUN apk update && \
apk add --no-cache --update \
curl \
tzdata \
ffmpeg \
make \
python3 \
g++ \
tini
apk add --no-cache --update \
curl \
tzdata \
ffmpeg \
make \
gcompat \
python3 \
g++ \
tini \
unzip
COPY --from=build /client/dist /client/dist
COPY index.js package* /
COPY server server
ARG TARGETPLATFORM
ENV NUSQLITE3_DIR="/usr/local/lib/nusqlite3"
ENV NUSQLITE3_PATH="${NUSQLITE3_DIR}/libnusqlite3.so"
RUN case "$TARGETPLATFORM" in \
"linux/amd64") \
curl -L -o /tmp/library.zip "https://github.com/mikiher/nunicode-sqlite/releases/download/v1.1/libnusqlite3-linux-x64.zip" ;; \
"linux/arm64") \
curl -L -o /tmp/library.zip "https://github.com/mikiher/nunicode-sqlite/releases/download/v1.1/libnusqlite3-linux-arm64.zip" ;; \
*) echo "Unsupported platform: $TARGETPLATFORM" && exit 1 ;; \
esac && \
unzip /tmp/library.zip -d $NUSQLITE3_DIR && \
rm /tmp/library.zip
RUN npm ci --only=production
RUN apk del make python3 g++
+1 -28
View File
@@ -2,14 +2,7 @@
font-family: 'Material Symbols Rounded';
font-style: normal;
font-weight: 400;
src: url(~static/fonts/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].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');
src: url(~static/fonts/MaterialSymbolsRounded.woff2) format('woff2');
}
.material-symbols {
@@ -32,26 +25,6 @@
'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 */
@font-face {
font-family: 'Source Sans Pro';
+7 -8
View File
@@ -16,7 +16,7 @@
<div class="flex-grow" />
<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>
<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>
@@ -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">
<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>
</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">
<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>
</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">
<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>
</nuxt-link>
@@ -47,7 +47,7 @@
<span class="block truncate">{{ username }}</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="material-symbols text-xl text-gray-100">person</span>
<span class="material-symbols text-xl text-gray-100">&#xe7fd;</span>
</span>
</nuxt-link>
</div>
@@ -264,7 +264,6 @@ export default {
libraryItems.forEach((item) => {
let subtitle = ''
if (item.mediaType === 'book') subtitle = item.media.metadata.authors.map((au) => au.name).join(', ')
else if (item.mediaType === 'music') subtitle = item.media.metadata.artists.join(', ')
queueItems.push({
libraryItemId: item.id,
libraryId: item.libraryId,
@@ -332,13 +331,13 @@ export default {
libraryItemIds: this.selectedMediaItems.map((i) => i.id)
})
.then(() => {
this.$toast.success('Batch delete success')
this.$toast.success(this.$strings.ToastBatchDeleteSuccess)
this.$store.commit('globals/resetSelectedMediaItems', [])
this.$eventBus.$emit('bookshelf_clear_selection')
})
.catch((error) => {
console.error('Batch delete failed', error)
this.$toast.error('Batch delete failed')
this.$toast.error(this.$strings.ToastBatchDeleteFailed)
})
.finally(() => {
this.$store.commit('setProcessingBatch', false)
@@ -347,6 +347,13 @@ export default {
libraryItemsAdded(libraryItems) {
console.log('libraryItems added', libraryItems)
// First items added to library
const isThisLibrary = libraryItems.some((li) => li.libraryId === this.currentLibraryId)
if (!this.shelves.length && !this.search && isThisLibrary) {
this.fetchCategories()
return
}
const recentlyAddedShelf = this.shelves.find((shelf) => shelf.id === 'recently-added')
if (!recentlyAddedShelf) return
+42 -15
View File
@@ -24,11 +24,11 @@
</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'">
<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 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>
<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 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>
@@ -50,7 +50,7 @@
{{ seriesName }}
</p>
<div class="w-6 h-6 rounded-full bg-black bg-opacity-30 flex items-center justify-center ml-3">
<span class="font-mono">{{ numShowing }}</span>
<span class="font-mono">{{ $formatNumber(numShowing) }}</span>
</div>
<div class="flex-grow" />
@@ -63,7 +63,7 @@
</template>
<!-- library & collections page -->
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
<p class="hidden md:block">{{ numShowing }} {{ entityName }}</p>
<p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p>
<div class="flex-grow hidden sm:inline-block" />
@@ -80,7 +80,7 @@
<controls-sort-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesSortBy" :descending.sync="settings.seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" />
<!-- issues page remove all button -->
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn>
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ $formatNumber(numShowing) }} {{ entityName }}</ui-btn>
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
</template>
@@ -159,6 +159,7 @@ export default {
}
this.addSubtitlesMenuItem(items)
this.addCollapseSubSeriesMenuItem(items)
return items
},
@@ -245,9 +246,6 @@ export default {
isPodcastLibrary() {
return this.currentLibraryMediaType === 'podcast'
},
isMusicLibrary() {
return this.currentLibraryMediaType === 'music'
},
isLibraryPage() {
return this.page === ''
},
@@ -280,7 +278,6 @@ export default {
},
entityName() {
if (this.isAlbumsPage) return 'Albums'
if (this.isMusicLibrary) return 'Tracks'
if (this.isPodcastLibrary) return this.$strings.LabelPodcasts
if (!this.page) return this.$strings.LabelBooks
@@ -371,6 +368,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) {
if (action === 'show-subtitles') {
this.settings.showSubtitles = true
@@ -397,6 +409,19 @@ export default {
}
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 }) {
if (action === 'export-opml') {
this.exportOPML()
@@ -427,6 +452,8 @@ export default {
this.markSeriesFinished()
} else if (this.handleSubtitlesAction(action)) {
return
} else if (this.handleCollapseSubSeriesAction(action)) {
return
}
},
showOpenSeriesRSSFeed() {
@@ -442,11 +469,11 @@ export default {
this.$axios
.$get(`/api/me/series/${this.seriesId}/readd-to-continue-listening`)
.then(() => {
this.$toast.success('Series re-added to continue listening')
this.$toast.success(this.$strings.ToastItemUpdateSuccess)
})
.catch((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.ToastFailedToUpdate)
})
.finally(() => {
this.processingSeries = false
@@ -473,7 +500,7 @@ export default {
})
if (!response) {
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) {
if (response.author.imagePath) console.log(`Author ${response.author.name} was updated`)
else console.log(`Author ${response.author.name} was updated (no image found)`)
@@ -491,13 +518,13 @@ export default {
this.$axios
.$delete(`/api/libraries/${this.currentLibraryId}/issues`)
.then(() => {
this.$toast.success('Removed library items with issues')
this.$toast.success(this.$strings.ToastRemoveItemsWithIssuesSuccess)
this.$router.push(`/library/${this.currentLibraryId}/bookshelf`)
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
})
.catch((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(() => {
this.processingIssues = false
@@ -553,7 +580,7 @@ export default {
updateCollapseSeries() {
this.saveSettings()
},
updateCollapseBookSeries() {
updateCollapseSubSeries() {
this.saveSettings()
},
updateShowSubtitles() {
+40 -11
View File
@@ -1,10 +1,9 @@
<template>
<div v-if="streamLibraryItem" id="mediaPlayerContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 lg:h-40 z-50 bg-primary px-2 lg:px-4 pb-1 lg:pb-4 pt-2">
<div id="videoDock" />
<div class="absolute left-2 top-2 lg:left-4 cursor-pointer">
<covers-book-cover expand-on-click :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
</div>
<div class="flex items-start mb-6 lg:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
<div class="flex items-start mb-6 lg:mb-0" :class="isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
<div class="min-w-0 w-full">
<div class="flex items-center">
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
@@ -12,10 +11,9 @@
</nuxt-link>
<widgets-explicit-indicator v-if="isExplicit" />
</div>
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5">
<div class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5">
<span class="material-symbols text-sm">person</span>
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
<div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div>
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate">
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
</div>
@@ -43,12 +41,14 @@
:sleep-timer-remaining="sleepTimerRemaining"
:sleep-timer-type="sleepTimerType"
:is-podcast="isPodcast"
:hasNextItemInQueue="hasNextItemInQueue"
@playPause="playPause"
@jumpForward="jumpForward"
@jumpBackward="jumpBackward"
@setVolume="setVolume"
@setPlaybackRate="setPlaybackRate"
@seek="seek"
@nextItemInQueue="playNextItemInQueue"
@close="closePlayer"
@showBookmarks="showBookmarks"
@showSleepTimer="showSleepTimerModal = true"
@@ -60,7 +60,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-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" />
</div>
@@ -138,9 +138,6 @@ export default {
isPodcast() {
return this.streamLibraryItem?.mediaType === 'podcast'
},
isMusic() {
return this.streamLibraryItem?.mediaType === 'music'
},
isExplicit() {
return !!this.mediaMetadata.explicit
},
@@ -172,9 +169,15 @@ export default {
if (!this.isPodcast) return null
return this.mediaMetadata.author || 'Unknown'
},
musicArtists() {
if (!this.isMusic) return null
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() {
return this.$store.state.playerQueueItems || []
@@ -460,6 +463,30 @@ export default {
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) {
const libraryItemId = payload.libraryItemId
const episodeId = payload.episodeId || null
@@ -512,6 +539,7 @@ export default {
this.$eventBus.$on('cast-session-active', this.castSessionActive)
this.$eventBus.$on('playback-seek', this.seek)
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('pause-item', this.pauseItem)
},
@@ -519,6 +547,7 @@ export default {
this.$eventBus.$off('cast-session-active', this.castSessionActive)
this.$eventBus.$off('playback-seek', this.seek)
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('pause-item', this.pauseItem)
}
+6 -20
View File
@@ -15,7 +15,7 @@
</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'">
<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>
@@ -43,7 +43,7 @@
</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'">
<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>
@@ -51,7 +51,7 @@
</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'">
<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>
@@ -72,7 +72,7 @@
</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'">
<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>
@@ -80,7 +80,7 @@
</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'">
<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>
@@ -95,16 +95,8 @@
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</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'">
<span class="material-symbols-outlined text-xl">album</span>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">Albums</p>
<div v-show="isMusicAlbumsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</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'">
<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>
@@ -172,9 +164,6 @@ export default {
isPodcastLibrary() {
return this.currentLibraryMediaType === 'podcast'
},
isMusicLibrary() {
return this.currentLibraryMediaType === 'music'
},
isPodcastDownloadQueuePage() {
return this.$route.name === 'library-library-podcast-download-queue'
},
@@ -184,9 +173,6 @@ export default {
isPodcastLatestPage() {
return this.$route.name === 'library-library-podcast-latest'
},
isMusicAlbumsPage() {
return this.paramId === 'albums'
},
homePage() {
return this.$route.name === 'library-library'
},
@@ -8,6 +8,7 @@
<p class="truncate text-sm">{{ title }}</p>
<p class="truncate text-xs text-gray-300">{{ description }}</p>
<p v-if="specialMessage" class="truncate text-xs text-gray-300">{{ specialMessage }}</p>
<p v-if="isFailed && failedMessage" class="text-xs truncate text-red-500">{{ failedMessage }}</p>
<p v-else-if="!isFinished && cancelingScan" class="text-xs truncate">Canceling...</p>
@@ -26,7 +27,16 @@ export default {
},
data() {
return {
cancelingScan: false
cancelingScan: false,
specialMessage: ''
}
},
watch: {
task: {
immediate: true,
handler() {
this.initTask()
}
}
},
computed: {
@@ -34,14 +44,17 @@ export default {
return this.$store.getters['user/getIsAdminOrUp']
},
title() {
if (this.task.titleKey && this.$strings[this.task.titleKey]) {
return this.$getString(this.task.titleKey, this.task.titleSubs)
}
return this.task.title || 'No Title'
},
description() {
if (this.task.descriptionKey && this.$strings[this.task.descriptionKey]) {
return this.$getString(this.task.descriptionKey, this.task.descriptionSubs)
}
return this.task.description || ''
},
details() {
return this.task.details || 'Unknown'
},
isFinished() {
return !!this.task.isFinished
},
@@ -52,6 +65,9 @@ export default {
return this.isFinished && !this.isFailed
},
failedMessage() {
if (this.task.errorKey && this.$strings[this.task.errorKey]) {
return this.$getString(this.task.errorKey, this.task.errorSubs)
}
return this.task.error || ''
},
action() {
@@ -87,6 +103,21 @@ export default {
}
},
methods: {
initTask() {
// special message for library scan tasks
if (this.task?.data?.scanResults) {
const scanResults = this.task.data.scanResults
const strs = []
if (scanResults.added) strs.push(this.$getString('MessageTaskScanItemsAdded', [scanResults.added]))
if (scanResults.updated) strs.push(this.$getString('MessageTaskScanItemsUpdated', [scanResults.updated]))
if (scanResults.missing) strs.push(this.$getString('MessageTaskScanItemsMissing', [scanResults.missing]))
const changesDetected = strs.length > 0 ? strs.join(', ') : this.$strings.MessageTaskScanNoChangesNeeded
const timeElapsed = scanResults.elapsed ? ` (${this.$elapsedPretty(scanResults.elapsed / 1000, false, true)})` : ''
this.specialMessage = `${changesDetected}${timeElapsed}`
} else {
this.specialMessage = ''
}
},
cancelScan() {
const libraryId = this.task?.data?.libraryId
if (!libraryId) {
+20 -44
View File
@@ -201,23 +201,6 @@ export default {
// This method returns immediately without waiting for the DOM to update
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() {
return this.store.getters['user/getSizeMultiplier']
},
@@ -243,9 +226,6 @@ export default {
isPodcast() {
return this.mediaType === 'podcast' || this.store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast'
},
isMusic() {
return this.mediaType === 'music'
},
isExplicit() {
return this.mediaMetadata.explicit || false
},
@@ -353,7 +333,6 @@ export default {
displayLineTwo() {
if (this.recentEpisode) return this.title
if (this.isPodcast) return this.author
if (this.isMusic) return this.artist
if (this.collapsedSeries) return ''
if (this.isAuthorBookshelfView) {
return this.mediaMetadata.publishedYear || ''
@@ -363,14 +342,14 @@ export default {
},
displaySortLine() {
if (this.collapsedSeries) return null
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt, this.dateFormat)
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
if (this.orderBy === 'mtimeMs') return this.$getString('LabelFileModifiedDate', [this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)])
if (this.orderBy === 'birthtimeMs') return this.$getString('LabelFileBornDate', [this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)])
if (this.orderBy === 'addedAt') return this.$getString('LabelAddedDate', [this.$formatDate(this._libraryItem.addedAt, this.dateFormat)])
if (this.orderBy === 'media.duration') return this.$strings.LabelDuration + ': ' + this.$elapsedPrettyExtended(this.media.duration, false)
if (this.orderBy === 'size') return this.$strings.LabelSize + ': ' + this.$bytesPretty(this._libraryItem.size)
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} ` + this.$strings.LabelEpisodes
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 null
@@ -381,7 +360,6 @@ export default {
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
},
userProgress() {
if (this.isMusic) return null
if (this.episodeProgress) return this.episodeProgress
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
},
@@ -437,7 +415,7 @@ export default {
return !this.isSelectionMode && !this.showPlayButton && this.ebookFormat
},
showPlayButton() {
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode || this.isMusic)
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode)
},
showSmallEBookIcon() {
return !this.isSelectionMode && this.ebookFormat
@@ -481,8 +459,6 @@ export default {
return this.store.getters['user/getIsAdminOrUp']
},
moreMenuItems() {
if (this.isMusic) return []
if (this.recentEpisode) {
const items = [
{
@@ -727,7 +703,7 @@ export default {
toggleFinished(confirmed = false) {
if (!this.itemIsFinished && this.userProgressPercent > 0 && !confirmed) {
const payload = {
message: `Are you sure you want to mark "${this.displayTitle}" as finished?`,
message: this.$getString('MessageConfirmMarkItemFinished', [this.displayTitle]),
callback: (confirmed) => {
if (confirmed) {
this.toggleFinished(true)
@@ -772,18 +748,18 @@ export default {
.then((data) => {
var result = data.result
if (!result) {
this.$toast.error(`Re-Scan Failed for "${this.title}"`)
this.$toast.error(this.$getString('ToastRescanFailed', [this.displayTitle]))
} else if (result === 'UPDATED') {
this.$toast.success(`Re-Scan complete item was updated`)
this.$toast.success(this.$strings.ToastRescanUpdated)
} 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') {
this.$toast.error(`Re-Scan complete item was removed`)
this.$toast.error(this.$strings.ToastRescanRemoved)
}
})
.catch((error) => {
console.error('Failed to scan library item', error)
this.$toast.error('Failed to scan library item')
this.$toast.error(this.$strings.ToastScanFailed)
})
.finally(() => {
this.processing = false
@@ -840,7 +816,7 @@ export default {
})
.catch((error) => {
console.error('Failed to remove series from home', error)
this.$toast.error('Failed to update user')
this.$toast.error(this.$strings.ToastFailedToUpdate)
})
.finally(() => {
this.processing = false
@@ -858,7 +834,7 @@ export default {
})
.catch((error) => {
console.error('Failed to hide item from home', error)
this.$toast.error('Failed to update user')
this.$toast.error(this.$strings.ToastFailedToUpdate)
})
.finally(() => {
this.processing = false
@@ -873,7 +849,7 @@ export default {
episodeId: this.recentEpisode.id,
title: this.recentEpisode.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,
coverPath: this.media.coverPath || null
}
@@ -923,11 +899,11 @@ export default {
axios
.$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`)
.then(() => {
this.$toast.success('Item deleted')
this.$toast.success(this.$strings.ToastItemDeletedSuccess)
})
.catch((error) => {
console.error('Failed to delete item', error)
this.$toast.error('Failed to delete item')
this.$toast.error(this.$strings.ToastItemDeletedFailed)
})
.finally(() => {
this.processing = false
@@ -1033,7 +1009,7 @@ export default {
episodeId: episode.id,
title: episode.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,
coverPath: this.media.coverPath || null
})
+1 -13
View File
@@ -57,23 +57,11 @@ export default {
return this.store.getters['libraries/getBookCoverAspectRatio']
},
cardWidth() {
return this.width || this.coverHeight * 2
return this.width || (this.coverHeight / this.bookCoverAspectRatio) * 2
},
coverHeight() {
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() {
if (this.width < 160) return 0.75
return 0.9
+2 -2
View File
@@ -65,7 +65,7 @@ export default {
return this.store.getters['libraries/getBookCoverAspectRatio']
},
cardWidth() {
return this.width || this.coverHeight * 2
return this.width || (this.coverHeight / this.bookCoverAspectRatio) * 2
},
coverHeight() {
return this.height * this.sizeMultiplier
@@ -96,7 +96,7 @@ export default {
displaySortLine() {
switch (this.orderBy) {
case 'addedAt':
return `${this.$strings.LabelAdded} ${this.$formatDate(this.addedAt, this.dateFormat)}`
return this.$getString('LabelAddedDate', [this.$formatDate(this.addedAt, this.dateFormat)])
case 'totalDuration':
return `${this.$strings.LabelDuration} ${this.$elapsedPrettyExtended(this.totalDuration, false)}`
case 'lastBookUpdated':
+1 -1
View File
@@ -3,7 +3,7 @@
<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 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>
<!-- Narrator name & num books overlay -->
@@ -1,7 +1,7 @@
<template>
<div class="flex h-full px-1 overflow-hidden">
<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 class="flex-grow px-2 narratorSearchCardContent h-full">
<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>
<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" color="red-600" @click.stop="fireTestEventAndFail">Fire & Fail</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">{{ 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-else-if="notification.enabled" :loading="sendingTest" small class="mr-2" @click.stop="sendTestClick">Test</ui-btn>
<ui-btn v-else :loading="enabling" small color="success" class="mr-2" @click="enableNotification">Enable</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">{{ 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 bg-color="error" :size="7" icon-font-size="1.2rem" icon="delete" @click="deleteNotificationClick" />
@@ -65,12 +65,12 @@ export default {
this.$axios
.$get(`/api/notifications/test?fail=${intentionallyFail ? 1 : 0}`)
.then(() => {
this.$toast.success('Triggered onTest Event')
this.$toast.success(this.$strings.ToastNotificationTestTriggerSuccess)
})
.catch((error) => {
console.error('Failed', error)
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(() => {
this.testing = false
@@ -91,7 +91,7 @@ export default {
// End testing functions
sendTestClick() {
const payload = {
message: `Trigger this notification with test data?`,
message: this.$strings.MessageConfirmNotificationTestTrigger,
callback: (confirmed) => {
if (confirmed) {
this.sendTest()
@@ -106,12 +106,12 @@ export default {
this.$axios
.$get(`/api/notifications/${this.notification.id}/test`)
.then(() => {
this.$toast.success('Triggered test notification')
this.$toast.success(this.$strings.ToastNotificationTestTriggerSuccess)
})
.catch((error) => {
console.error('Failed', error)
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(() => {
this.sendingTest = false
@@ -127,11 +127,10 @@ export default {
.$patch(`/api/notifications/${this.notification.id}`, payload)
.then((updatedSettings) => {
this.$emit('update', updatedSettings)
this.$toast.success('Notification enabled')
})
.catch((error) => {
console.error('Failed to update notification', error)
this.$toast.error('Failed to update notification')
this.$toast.error(this.$strings.ToastFailedToUpdate)
})
.finally(() => {
this.enabling = false
@@ -139,7 +138,7 @@ export default {
},
deleteNotificationClick() {
const payload = {
message: `Are you sure you want to delete this notification?`,
message: this.$strings.MessageConfirmDeleteNotification,
callback: (confirmed) => {
if (confirmed) {
this.deleteNotification()
@@ -155,11 +154,10 @@ export default {
.$delete(`/api/notifications/${this.notification.id}`)
.then((updatedSettings) => {
this.$emit('update', updatedSettings)
this.$toast.success('Deleted notification')
})
.catch((error) => {
console.error('Failed', error)
this.$toast.error('Failed to delete notification')
this.$toast.error(this.$strings.ToastNotificationDeleteFailed)
})
.finally(() => {
this.deleting = false
@@ -171,4 +169,4 @@ export default {
},
mounted() {}
}
</script>
</script>
@@ -27,38 +27,6 @@
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=publishers.${$encode(publisher)}`" class="hover:underline">{{ publisher }}</nuxt-link>
</div>
</div>
<div v-if="musicAlbum" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Album</span>
</div>
<div>
{{ musicAlbum }}
</div>
</div>
<div v-if="musicAlbumArtist" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Album Artist</span>
</div>
<div>
{{ musicAlbumArtist }}
</div>
</div>
<div v-if="musicTrackPretty" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Track</span>
</div>
<div>
{{ musicTrackPretty }}
</div>
</div>
<div v-if="musicDiscPretty" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Disc</span>
</div>
<div>
{{ musicDiscPretty }}
</div>
</div>
<div v-if="podcastType" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span>
@@ -97,7 +65,7 @@
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=languages.${$encode(language)}`" class="hover:underline">{{ language }}</nuxt-link>
</div>
</div>
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
<div v-if="tracks.length || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
</div>
@@ -134,10 +102,6 @@ export default {
isPodcast() {
return this.libraryItem.mediaType === 'podcast'
},
audioFile() {
// Music track
return this.media.audioFile
},
media() {
return this.libraryItem.media || {}
},
@@ -168,25 +132,6 @@ export default {
publisher() {
return this.mediaMetadata.publisher || ''
},
musicArtists() {
return this.mediaMetadata.artists || []
},
musicAlbum() {
return this.mediaMetadata.album || ''
},
musicAlbumArtist() {
return this.mediaMetadata.albumArtist || ''
},
musicTrackPretty() {
if (!this.mediaMetadata.trackNumber) return null
if (!this.mediaMetadata.trackTotal) return this.mediaMetadata.trackNumber
return `${this.mediaMetadata.trackNumber} / ${this.mediaMetadata.trackTotal}`
},
musicDiscPretty() {
if (!this.mediaMetadata.discNumber) return null
if (!this.mediaMetadata.discTotal) return this.mediaMetadata.discNumber
return `${this.mediaMetadata.discNumber} / ${this.mediaMetadata.discTotal}`
},
narrators() {
return this.mediaMetadata.narrators || []
},
@@ -220,4 +165,4 @@ export default {
methods: {},
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" />
</form>
<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>
</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>
<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">
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(item.id)}`">
<nuxt-link :to="`/author/${item.id}`">
<cards-author-search-card :author="item" />
</nuxt-link>
</li>
@@ -98,9 +98,6 @@ export default {
isPodcast() {
return this.libraryMediaType === 'podcast'
},
isMusic() {
return this.libraryMediaType === 'music'
},
seriesItems() {
return [
{
@@ -274,35 +271,9 @@ export default {
}
]
},
musicItems() {
return [
{
text: this.$strings.LabelAll,
value: 'all'
},
{
text: this.$strings.LabelGenre,
textPlural: this.$strings.LabelGenres,
value: 'genres',
sublist: true
},
{
text: this.$strings.LabelTag,
textPlural: this.$strings.LabelTags,
value: 'tags',
sublist: true
},
{
text: this.$strings.ButtonIssues,
value: 'issues',
sublist: false
}
]
},
selectItems() {
if (this.isSeries) return this.seriesItems
if (this.isPodcast) return this.podcastItems
if (this.isMusic) return this.musicItems
return this.bookItems
},
selectedItemSublist() {
@@ -56,9 +56,6 @@ export default {
isPodcast() {
return this.libraryMediaType === 'podcast'
},
isMusic() {
return this.libraryMediaType === 'music'
},
podcastItems() {
return [
{
@@ -148,40 +145,10 @@ export default {
}
]
},
musicItems() {
return [
{
text: this.$strings.LabelTitle,
value: 'media.metadata.title'
},
{
text: this.$strings.LabelAddedAt,
value: 'addedAt'
},
{
text: this.$strings.LabelSize,
value: 'size'
},
{
text: this.$strings.LabelDuration,
value: 'media.duration'
},
{
text: this.$strings.LabelFileBirthtime,
value: 'birthtimeMs'
},
{
text: this.$strings.LabelFileModified,
value: 'mtimeMs'
}
]
},
selectItems() {
let items = null
if (this.isPodcast) {
items = this.podcastItems
} else if (this.isMusic) {
items = this.musicItems
} else if (this.$store.getters['user/getUserSetting']('filterBy').startsWith('series.')) {
items = this.seriesItems
} else {
@@ -1,9 +1,9 @@
<template>
<div ref="wrapper" class="relative ml-4 sm:ml-8" v-click-outside="clickOutside">
<div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="setShowMenu(true)">
<span class="font-mono uppercase text-gray-200 text-sm sm:text-base">{{ playbackRate.toFixed(1) }}<span class="text-base">x</span></span>
<span class="text-gray-200 text-sm sm:text-base">{{ playbackRate.toFixed(1) }}<span class="text-base">x</span></span>
</div>
<div v-show="showMenu" class="absolute -top-20 z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }">
<div v-show="showMenu" class="absolute -top-[5.5rem] z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }">
<div class="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }">
<div class="arrow-down" />
</div>
@@ -11,12 +11,12 @@
<template v-for="rate in rates">
<div :key="rate" class="h-full border-black-300 w-11 cursor-pointer border rounded-sm" :class="value === rate ? 'bg-black-100' : 'hover:bg-black hover:bg-opacity-10'" style="min-width: 44px; max-width: 44px" @click="set(rate)">
<div class="w-full h-full flex justify-center items-center">
<p class="text-xs text-center font-mono">{{ rate }}<span class="text-sm">x</span></p>
<p class="text-xs text-center">{{ rate }}<span class="text-sm">x</span></p>
</div>
</div>
</template>
</div>
<div class="w-full py-1 px-4">
<div class="w-full py-1 px-1">
<div class="flex items-center justify-between">
<ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" />
<p class="px-2 text-2xl sm:text-3xl">{{ playbackRate }}<span class="text-2xl">x</span></p>
@@ -41,7 +41,7 @@ export default {
currentPlaybackRate: 0,
MIN_SPEED: 0.5,
MAX_SPEED: 10,
menuLeft: -92,
menuLeft: -96,
arrowLeft: 0
}
},
@@ -89,9 +89,9 @@ export default {
if (boundingBox.left + 110 > window.innerWidth - 10) {
this.menuLeft = window.innerWidth - 230 - boundingBox.left
this.arrowLeft = Math.abs(this.menuLeft) - 92
this.arrowLeft = Math.abs(this.menuLeft) - 96
} else {
this.menuLeft = -92
this.menuLeft = -96
this.arrowLeft = 0
}
},
@@ -109,4 +109,4 @@ export default {
this.currentPlaybackRate = this.playbackRate
}
}
</script>
</script>
+18 -25
View File
@@ -4,10 +4,10 @@
<span class="material-symbols text-2xl sm:text-3xl">{{ volumeIcon }}</span>
</button>
<transition name="menux">
<div v-show="isOpen" class="volumeMenu h-6 absolute bottom-2 w-28 px-2 bg-bg shadow-sm rounded-lg" style="left: -116px">
<div ref="volumeTrack" class="h-1 w-full bg-gray-500 my-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack">
<div class="bg-gray-100 h-full absolute left-0 top-0 pointer-events-none rounded-full" :style="{ width: volume * trackWidth + 'px' }" />
<div class="w-2.5 h-2.5 bg-white shadow-sm rounded-full absolute pointer-events-none" :class="isDragging ? 'transform scale-125 origin-center' : ''" :style="{ left: cursorLeft + 'px', top: '-3px' }" />
<div v-show="isOpen" class="volumeMenu h-28 absolute bottom-2 w-6 py-2 bg-bg shadow-sm rounded-lg" style="top: -116px">
<div ref="volumeTrack" class="w-1 h-full bg-gray-500 mx-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack">
<div class="bg-gray-100 w-full absolute left-0 bottom-0 pointer-events-none rounded-full" :style="{ height: volume * trackHeight + 'px' }" />
<div class="w-2.5 h-2.5 bg-white shadow-sm rounded-full absolute pointer-events-none" :class="isDragging ? 'transform scale-125 origin-center' : ''" :style="{ bottom: cursorBottom + 'px', left: '-3px' }" />
</div>
</div>
</transition>
@@ -24,10 +24,10 @@ export default {
isOpen: false,
isDragging: false,
isHovering: false,
posX: 0,
posY: 0,
lastValue: 0.5,
isMute: false,
trackWidth: 112 - 20,
trackHeight: 112 - 20,
openTimeout: null
}
},
@@ -45,9 +45,9 @@ export default {
this.$emit('input', val)
}
},
cursorLeft() {
var left = this.trackWidth * this.volume
return left - 3
cursorBottom() {
var bottom = this.trackHeight * this.volume
return bottom - 3
},
volumeIcon() {
if (this.volume <= 0) return 'volume_mute'
@@ -89,17 +89,10 @@ export default {
}, 600)
},
mousemove(e) {
var diff = this.posX - e.x
this.posX = e.x
var volShift = 0
if (diff < 0) {
// Volume up
volShift = diff / this.trackWidth
} else {
// volume down
volShift = diff / this.trackWidth
}
var newVol = this.volume - volShift
var diff = this.posY - e.y
this.posY = e.y
var volShift = diff / this.trackHeight
var newVol = this.volume + volShift
newVol = Math.min(Math.max(0, newVol), 1)
this.volume = newVol
e.preventDefault()
@@ -113,8 +106,8 @@ export default {
},
mousedownTrack(e) {
this.isDragging = true
this.posX = e.x
var vol = e.offsetX / this.trackWidth
this.posY = e.y
var vol = 1 - e.offsetY / this.trackHeight
vol = Math.min(Math.max(vol, 0), 1)
this.volume = vol
document.body.addEventListener('mousemove', this.mousemove)
@@ -137,7 +130,7 @@ export default {
this.clickVolumeIcon()
},
clickVolumeTrack(e) {
var vol = e.offsetX / this.trackWidth
var vol = 1 - e.offsetY / this.trackHeight
vol = Math.min(Math.max(vol, 0), 1)
this.volume = vol
}
@@ -147,7 +140,7 @@ export default {
this.isMute = true
}
const storageVolume = localStorage.getItem('volume')
if (storageVolume) {
if (storageVolume && !isNaN(storageVolume)) {
this.volume = parseFloat(storageVolume)
}
},
@@ -157,4 +150,4 @@ export default {
document.body.removeEventListener('mouseup', this.mouseup)
}
}
</script>
</script>
+14 -12
View File
@@ -111,7 +111,7 @@
</div>
<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>
<div class="flex-grow" />
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
@@ -212,19 +212,19 @@ export default {
},
unlinkOpenID() {
const payload = {
message: 'Are you sure you want to unlink this user from OpenID?',
message: this.$strings.MessageConfirmUnlinkOpenId,
callback: (confirmed) => {
if (confirmed) {
this.unlinkingFromOpenID = true
this.$axios
.$patch(`/api/users/${this.account.id}/openid-unlink`)
.then(() => {
this.$toast.success('User unlinked from OpenID')
this.$toast.success(this.$strings.ToastUnlinkOpenIdSuccess)
this.show = false
})
.catch((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(() => {
this.unlinkingFromOpenID = false
@@ -265,15 +265,15 @@ export default {
},
submitForm() {
if (!this.newUser.username) {
this.$toast.error('Enter a username')
this.$toast.error(this.$strings.ToastNewUserUsernameError)
return
}
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
}
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
}
@@ -296,7 +296,7 @@ export default {
.then((data) => {
this.processing = false
if (data.error) {
this.$toast.error(`${this.$strings.ToastAccountUpdateFailed}: ${data.error}`)
this.$toast.error(`${this.$strings.ToastFailedToUpdate}: ${data.error}`)
} else {
console.log('Account updated', data.user)
@@ -313,12 +313,12 @@ export default {
this.processing = false
console.error('Failed to update account', error)
var errMsg = error.response ? error.response.data || '' : ''
this.$toast.error(errMsg || 'Failed to update account')
this.$toast.error(errMsg || this.$strings.ToastFailedToUpdate)
})
},
submitCreateAccount() {
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
}
@@ -329,9 +329,9 @@ export default {
.then((data) => {
this.processing = false
if (data.error) {
this.$toast.error(`Failed to create account: ${data.error}`)
this.$toast.error(this.$strings.ToastNewUserCreatedFailed + ': ' + data.error)
} else {
this.$toast.success('New account created')
this.$toast.success(this.$strings.ToastNewUserCreatedSuccess)
this.show = false
}
})
@@ -351,6 +351,7 @@ export default {
update: type === 'admin',
delete: type === 'admin',
upload: type === 'admin',
accessExplicitContent: type === 'admin',
accessAllLibraries: true,
accessAllTags: true,
selectedTagsNotAccessible: false
@@ -385,6 +386,7 @@ export default {
upload: false,
accessAllLibraries: true,
accessAllTags: true,
accessExplicitContent: false,
selectedTagsNotAccessible: false
},
librariesAccessible: [],
@@ -2,7 +2,7 @@
<modals-modal ref="modal" v-model="show" name="custom-metadata-provider" :width="600" :height="'unset'" :processing="processing">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
<p class="text-3xl text-white truncate">Add custom metadata provider</p>
<p class="text-3xl text-white truncate">{{ $strings.HeaderAddCustomMetadataProvider }}</p>
</div>
</template>
<form @submit.prevent="submitForm">
@@ -20,7 +20,7 @@
<ui-text-input-with-label v-model="newUrl" label="URL" />
</div>
<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 class="flex px-1 pt-4">
<div class="flex-grow" />
@@ -67,7 +67,7 @@ export default {
methods: {
submitForm() {
if (!this.newName || !this.newUrl) {
this.$toast.error('Must add name and url')
this.$toast.error(this.$strings.ToastProviderNameAndUrlRequired)
return
}
@@ -81,13 +81,13 @@ export default {
})
.then((data) => {
this.$emit('added', data.provider)
this.$toast.success('New provider added')
this.$toast.success(this.$strings.ToastProviderCreatedSuccess)
this.show = false
})
.catch((error) => {
const errorMsg = error.response?.data || 'Unknown error'
console.error('Failed to add provider', error)
this.$toast.error('Failed to add provider: ' + errorMsg)
this.$toast.error(this.$strings.ToastProviderCreatedFailed + ': ' + errorMsg)
})
.finally(() => {
this.processing = false
@@ -4,7 +4,7 @@
<div class="flex items-center justify-between">
<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-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 class="w-full h-px bg-white bg-opacity-10 my-4" />
@@ -159,7 +159,7 @@ export default {
})
.catch((error) => {
console.error('Failed to get ffprobe data', error)
this.$toast.error('FFProbe failed')
this.$toast.error(this.$strings.ToastFailedToLoadData)
})
.finally(() => {
this.probingFile = false
@@ -9,7 +9,7 @@
<widgets-cron-expression-builder ref="expressionBuilder" v-model="newCronExpression" @input="expressionUpdated" />
<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>
</modals-modal>
+2 -2
View File
@@ -94,7 +94,7 @@ export default {
this.$toast.success(this.$strings.ToastBookmarkRemoveSuccess)
})
.catch((error) => {
this.$toast.error(this.$strings.ToastBookmarkRemoveFailed)
this.$toast.error(this.$strings.ToastRemoveFailed)
console.error(error)
})
this.show = false
@@ -110,7 +110,7 @@ export default {
this.$toast.success(this.$strings.ToastBookmarkUpdateSuccess)
})
.catch((error) => {
this.$toast.error(this.$strings.ToastBookmarkUpdateFailed)
this.$toast.error(this.$strings.ToastFailedToUpdate)
console.error(error)
})
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="flex">
<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 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" />
@@ -66,6 +66,11 @@ export default {
}
},
methods: {
seriesNameInputHandler() {
if (this.$refs.sequenceInput) {
this.$refs.sequenceInput.setFocus()
}
},
setInputFocus() {
if (this.isNewSeries) {
// Focus on series input if new series
@@ -134,4 +139,4 @@ export default {
},
mounted() {}
}
</script>
</script>
@@ -100,7 +100,7 @@
<div class="flex items-center">
<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>
</modals-modal>
@@ -206,14 +206,13 @@ export default {
this.$axios
.$post(`/api/session/${this._session.id}/close`)
.then(() => {
this.$toast.success('Session closed')
this.show = false
this.$emit('closedSession')
})
.catch((error) => {
console.error('Failed to close session', error)
const errMsg = error.response?.data || ''
this.$toast.error(errMsg || 'Failed to close open session')
this.$toast.error(errMsg || this.$strings.ToastSessionCloseFailed)
})
.finally(() => {
this.processing = false
+1 -1
View File
@@ -165,7 +165,7 @@ export default {
},
openShare() {
if (!this.newShareSlug) {
this.$toast.error('Slug is required')
this.$toast.error(this.$strings.ToastSlugRequired)
return
}
const payload = {
+1 -1
View File
@@ -15,7 +15,7 @@
</template>
<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-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>
</div>
<div v-if="timerSet" class="w-full p-4">
@@ -78,14 +78,13 @@ export default {
if (data.error) {
this.$toast.error(data.error)
} else {
this.$toast.success('Cover Uploaded')
this.resetCoverPreview()
}
this.processingUpload = false
})
.catch((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.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) => {
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)
return false
})
@@ -104,4 +103,4 @@ export default {
}
}
}
</script>
</script>
+15 -13
View File
@@ -116,12 +116,12 @@ export default {
this.$axios
.$delete(`/api/authors/${this.authorId}`)
.then(() => {
this.$toast.success('Author removed')
this.$toast.success(this.$strings.ToastAuthorRemoveSuccess)
this.show = false
})
.catch((error) => {
console.error('Failed to remove author', error)
this.$toast.error('Failed to remove author')
this.$toast.error(this.$strings.ToastRemoveFailed)
})
.finally(() => {
this.processing = false
@@ -141,14 +141,14 @@ export default {
}
})
if (!Object.keys(updatePayload).length) {
this.$toast.info(this.$strings.MessageNoUpdateNecessary)
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
return
}
this.processing = true
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
console.error('Failed', error)
const errorMsg = error.response ? error.response.data : null
this.$toast.error(errorMsg || this.$strings.ToastAuthorUpdateFailed)
this.$toast.error(errorMsg || this.$strings.ToastFailedToUpdate)
return null
})
if (result) {
@@ -158,7 +158,7 @@ export default {
} else if (result.merged) {
this.$toast.success(this.$strings.ToastAuthorUpdateMerged)
this.show = false
} else this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
} else this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
}
this.processing = false
},
@@ -174,7 +174,7 @@ export default {
})
.catch((error) => {
console.error('Failed', error)
this.$toast.error(this.$strings.ToastAuthorImageRemoveFailed)
this.$toast.error(this.$strings.ToastRemoveFailed)
})
.finally(() => {
this.processing = false
@@ -182,7 +182,7 @@ export default {
},
submitUploadCover() {
if (!this.imageUrl?.startsWith('http:') && !this.imageUrl?.startsWith('https:')) {
this.$toast.error('Invalid image url')
this.$toast.error(this.$strings.ToastInvalidImageUrl)
return
}
@@ -194,14 +194,14 @@ export default {
.$post(`/api/authors/${this.authorId}/image`, updatePayload)
.then((data) => {
this.imageUrl = ''
this.$toast.success('Author image updated')
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
this.authorCopy.updatedAt = data.author.updatedAt
this.authorCopy.imagePath = data.author.imagePath
})
.catch((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(() => {
this.processing = false
@@ -209,7 +209,7 @@ export default {
},
async searchAuthor() {
if (!this.authorCopy.name && !this.authorCopy.asin) {
this.$toast.error('Must enter an author name')
this.$toast.error(this.$strings.ToastNameRequired)
return
}
this.processing = true
@@ -228,17 +228,19 @@ export default {
return null
})
if (!response) {
this.$toast.error('Author not found')
this.$toast.error(this.$strings.ToastAuthorSearchNotFound)
} else if (response.updated) {
if (response.author.imagePath) {
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
} else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
} else {
this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
}
this.authorCopy = {
...response.author
}
} else {
this.$toast.info('No updates were made for Author')
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
}
this.processing = false
}
@@ -6,10 +6,15 @@
</div>
</template>
<div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh">
<p class="text-xl font-bold pb-4">
Changelog <a :href="currentTagUrl" target="_blank" class="hover:underline">v{{ currentVersionNumber }}</a> ({{ currentVersionPubDate }})
</p>
<div class="custom-text" v-html="compiledMarkedown" />
<template v-for="release in releasesToShow">
<div :key="release.name">
<p class="text-xl font-bold pb-4">
Changelog <a :href="`https://github.com/advplyr/audiobookshelf/releases/tag/${release.name}`" target="_blank" class="hover:underline">{{ release.name }}</a> ({{ $formatDate(release.pubdate, dateFormat) }})
</p>
<div class="custom-text" v-html="getChangelog(release)" />
</div>
<div v-if="release !== releasesToShow[releasesToShow.length - 1]" class="border-b border-black-300 my-8" />
</template>
</div>
</modals-modal>
</template>
@@ -37,24 +42,15 @@ export default {
dateFormat() {
return this.$store.state.serverSettings.dateFormat
},
changelog() {
return this.versionData?.currentVersionChangelog || 'No Changelog Available'
},
compiledMarkedown() {
return marked.parse(this.changelog, { gfm: true, breaks: true })
},
currentVersionPubDate() {
if (!this.versionData?.currentVersionPubDate) return 'Unknown release date'
return `${this.$formatDate(this.versionData.currentVersionPubDate, this.dateFormat)}`
},
currentTagUrl() {
return this.versionData?.currentTagUrl
},
currentVersionNumber() {
return this.$config.version
releasesToShow() {
return this.versionData?.releasesToShow || []
}
},
methods: {
getChangelog(release) {
return marked.parse(release.changelog || 'No Changelog Available', { gfm: true, breaks: true })
}
},
methods: {},
mounted() {}
}
</script>
@@ -143,7 +143,7 @@ export default {
})
.catch((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
})
} else {
@@ -157,7 +157,7 @@ export default {
})
.catch((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
})
}
@@ -172,12 +172,12 @@ export default {
.$post(`/api/collections/${collection.id}/batch/add`, { books: this.selectedBookIds })
.then((updatedCollection) => {
console.log(`Books added to collection`, updatedCollection)
this.$toast.success('Books added to collection')
this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
this.processing = false
})
.catch((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
})
} else {
@@ -187,12 +187,12 @@ export default {
.$post(`/api/collections/${collection.id}/book`, { id: this.selectedLibraryItemId })
.then((updatedCollection) => {
console.log(`Book added to collection`, updatedCollection)
this.$toast.success('Book added to collection')
this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
this.processing = false
})
.catch((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
})
}
@@ -221,7 +221,7 @@ export default {
.catch((error) => {
console.error('Failed to create collection', error)
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
})
}
@@ -106,7 +106,7 @@ export default {
.catch((error) => {
console.error('Failed to remove collection', error)
this.processing = false
this.$toast.error(this.$strings.ToastCollectionRemoveFailed)
this.$toast.error(this.$strings.ToastRemoveFailed)
})
}
},
@@ -115,7 +115,7 @@ export default {
return
}
if (!this.newCollectionName) {
return this.$toast.error('Collection must have a name')
return this.$toast.error(this.$strings.ToastNameRequired)
}
this.processing = true
@@ -135,7 +135,7 @@ export default {
.catch((error) => {
console.error('Failed to update collection', error)
this.processing = false
this.$toast.error(this.$strings.ToastCollectionUpdateFailed)
this.$toast.error(this.$strings.ToastFailedToUpdate)
})
}
},
@@ -125,12 +125,12 @@ export default {
this.$refs.ereaderEmailInput.blur()
if (!this.newDevice.name?.trim() || !this.newDevice.email?.trim()) {
this.$toast.error('Name and email required')
this.$toast.error(this.$strings.ToastNameEmailRequired)
return
}
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
}
if (this.newDevice.availabilityOption !== 'specificUsers') {
@@ -142,14 +142,14 @@ export default {
if (!this.ereaderDevice) {
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
}
this.submitCreate()
} else {
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
}
@@ -174,12 +174,11 @@ export default {
.$post(`/api/emails/ereader-devices`, payload)
.then((data) => {
this.$emit('update', data.ereaderDevices)
this.$toast.success('Device updated')
this.show = false
})
.catch((error) => {
console.error('Failed to update device', error)
this.$toast.error('Failed to update device')
this.$toast.error(this.$strings.ToastFailedToUpdate)
})
.finally(() => {
this.processing = false
@@ -201,12 +200,11 @@ export default {
.$post('/api/emails/ereader-devices', payload)
.then((data) => {
this.$emit('update', data.ereaderDevices || [])
this.$toast.success('Device added')
this.show = false
})
.catch((error) => {
console.error('Failed to add device', error)
this.$toast.error('Failed to add device')
this.$toast.error(this.$strings.ToastDeviceAddFailed)
})
.finally(() => {
this.processing = false
+5 -10
View File
@@ -194,7 +194,6 @@ export default {
if (data.error) {
this.$toast.error(data.error)
} else {
this.$toast.success('Cover Uploaded')
this.resetCoverPreview()
}
this.processingUpload = false
@@ -204,7 +203,7 @@ export default {
if (error.response && error.response.data) {
this.$toast.error(error.response.data)
} else {
this.$toast.error('Oops, something went wrong...')
this.$toast.error(this.$strings.ToastUnknownError)
}
this.processingUpload = false
})
@@ -255,7 +254,7 @@ export default {
},
async updateCover(cover) {
if (!cover.startsWith('http:') && !cover.startsWith('https:')) {
this.$toast.error('Invalid URL')
this.$toast.error(this.$strings.ToastInvalidUrl)
return
}
@@ -264,11 +263,10 @@ export default {
.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover })
.then(() => {
this.imageUrl = ''
this.$toast.success('Update Successful')
})
.catch((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(() => {
this.isProcessing = false
@@ -308,12 +306,9 @@ export default {
this.isProcessing = true
this.$axios
.$patch(`/api/items/${this.libraryItemId}/cover`, { cover: coverFile.metadata.path })
.then(() => {
this.$toast.success('Update Successful')
})
.catch((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(() => {
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()
if (!title) {
this.$toast.error('Must have a title for quick match')
this.$toast.error(this.$strings.ToastTitleRequired)
return
}
this.quickMatching = true
@@ -108,9 +108,9 @@ export default {
if (res.warning) {
this.$toast.warning(res.warning)
} else if (res.updated) {
this.$toast.success('Item details updated')
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
} else {
this.$toast.info('No updates were made')
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
}
})
.catch((error) => {
@@ -128,18 +128,18 @@ export default {
this.rescanning = false
var result = data.result
if (!result) {
this.$toast.error(`Re-Scan Failed for "${this.title}"`)
this.$toast.error(this.$getString('ToastRescanFailed', [this.title]))
} else if (result === 'UPDATED') {
this.$toast.success(`Re-Scan complete item was updated`)
this.$toast.success(this.$strings.ToastRescanUpdated)
} 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') {
this.$toast.error(`Re-Scan complete item was removed`)
this.$toast.error(this.$strings.ToastRescanRemoved)
}
})
.catch((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
})
},
@@ -156,7 +156,7 @@ export default {
}
var updatedDetails = this.$refs.itemDetailsEdit.getDetails()
if (!updatedDetails.hasChanges) {
this.$toast.info('No changes were made')
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
return false
}
return this.updateDetails(updatedDetails)
@@ -170,7 +170,7 @@ export default {
this.isProcessing = false
if (updateResult) {
if (updateResult.updated) {
this.$toast.success('Item details updated')
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
return true
} else {
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
@@ -217,4 +217,4 @@ export default {
height: calc(100% - 80px);
max-height: calc(100% - 80px);
}
</style>
</style>
+3 -3
View File
@@ -397,7 +397,7 @@ export default {
},
submitSearch() {
if (!this.searchTitle) {
this.$toast.warning('Search title is required')
this.$toast.warning(this.$strings.ToastTitleRequired)
return
}
this.persistProvider()
@@ -618,12 +618,12 @@ export default {
if (updateResult.updated) {
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
} else {
this.$toast.info(this.$strings.ToastItemDetailsUpdateUnneeded)
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
}
this.clearSelectedMatch()
this.$emit('selectTab', 'details')
} else {
this.$toast.error(this.$strings.ToastItemDetailsUpdateFailed)
this.$toast.error(this.$strings.ToastFailedToUpdate)
}
} else {
this.clearSelectedMatch()
@@ -163,7 +163,7 @@ export default {
this.isProcessing = false
if (updateResult) {
if (updateResult.updated) {
this.$toast.success('Item details updated')
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
return true
} else {
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
@@ -156,7 +156,7 @@ export default {
},
validate() {
if (!this.libraryCopy.name) {
this.$toast.error('Library must have a name')
this.$toast.error(this.$strings.ToastNameRequired)
return false
}
if (!this.libraryCopy.folders.length) {
@@ -205,7 +205,7 @@ export default {
submitUpdateLibrary() {
var newLibraryPayload = this.getLibraryUpdatePayload()
if (!Object.keys(newLibraryPayload).length) {
this.$toast.info('No updates are necessary')
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
return
}
@@ -222,7 +222,7 @@ export default {
if (error.response && error.response.data) {
this.$toast.error(error.response.data)
} else {
this.$toast.error(this.$strings.ToastLibraryUpdateFailed)
this.$toast.error(this.$strings.ToastFailedToUpdate)
}
this.processing = false
})
@@ -264,4 +264,4 @@ export default {
.tab.tab-selected {
height: 41px;
}
</style>
</style>
@@ -162,7 +162,7 @@ export default {
})
.catch((error) => {
console.error('Failed to get filesystem paths', error)
this.$toast.error('Failed to get filesystem paths')
this.$toast.error(this.$strings.ToastFailedToLoadData)
return []
})
.finally(() => {
@@ -78,4 +78,4 @@ export default {
},
mounted() {}
}
</script>
</script>
@@ -86,7 +86,7 @@ export default {
return this.selectedEventData && this.selectedEventData.requiresLibrary
},
title() {
return this.isNew ? 'Create Notification' : 'Update Notification'
return this.isNew ? this.$strings.HeaderNotificationCreate : this.$strings.HeaderNotificationUpdate
},
availableVariables() {
return this.selectedEventData ? this.selectedEventData.variables || null : null
@@ -104,9 +104,9 @@ export default {
},
submitForm() {
this.$refs.urlsInput?.forceBlur()
if (!this.newNotification.urls.length) {
this.$toast.error('Must enter an Apprise URL')
this.$toast.error(this.$strings.ToastAppriseUrlRequired)
return
}
@@ -127,12 +127,12 @@ export default {
.$patch(`/api/notifications/${payload.id}`, payload)
.then((updatedSettings) => {
this.$emit('update', updatedSettings)
this.$toast.success('Notification updated')
this.$toast.success(this.$strings.ToastNotificationUpdateSuccess)
this.show = false
})
.catch((error) => {
console.error('Failed to update notification', error)
this.$toast.error('Failed to update notification')
this.$toast.error(this.$strings.ToastFailedToUpdate)
})
.finally(() => {
this.processing = false
@@ -149,12 +149,11 @@ export default {
.$post('/api/notifications', payload)
.then((updatedSettings) => {
this.$emit('update', updatedSettings)
this.$toast.success('Notification created')
this.show = false
})
.catch((error) => {
console.error('Failed to create notification', error)
this.$toast.error('Failed to create notification')
this.$toast.error(this.$strings.ToastNotificationCreateFailed)
})
.finally(() => {
this.processing = false
@@ -13,7 +13,7 @@
<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" />
</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>
</modals-modal>
@@ -22,8 +22,7 @@
<script>
export default {
props: {
value: Boolean,
libraryItemId: String
value: Boolean
},
data() {
return {}
@@ -50,11 +49,9 @@ export default {
}
},
methods: {
playItem(item) {
this.$eventBus.$emit('play-item', {
libraryItemId: item.libraryItemId,
episodeId: item.episodeId || null,
queueItems: this.playerQueueItems
playItem(index) {
this.$eventBus.$emit('play-queue-item', {
index
})
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 })
.then((updatedPlaylist) => {
console.log(`Items removed from playlist`, updatedPlaylist)
this.$toast.success('Playlist item(s) removed')
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
this.processing = false
})
.catch((error) => {
console.error('Failed to remove items from playlist', error)
this.$toast.error('Failed to remove playlist item(s)')
this.$toast.error(this.$strings.ToastFailedToUpdate)
this.processing = false
})
},
@@ -148,12 +148,12 @@ export default {
.$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects })
.then((updatedPlaylist) => {
console.log(`Items added to playlist`, updatedPlaylist)
this.$toast.success('Items added to playlist')
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
this.processing = false
})
.catch((error) => {
console.error('Failed to add items to playlist', error)
this.$toast.error('Failed to add items to playlist')
this.$toast.error(this.$strings.ToastFailedToUpdate)
this.processing = false
})
},
@@ -174,14 +174,14 @@ export default {
.$post('/api/playlists', newPlaylist)
.then((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.newPlaylistName = ''
})
.catch((error) => {
console.error('Failed to create playlist', error)
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
})
}
@@ -86,7 +86,7 @@ export default {
.catch((error) => {
console.error('Failed to remove playlist', error)
this.processing = false
this.$toast.error(this.$strings.ToastPlaylistRemoveFailed)
this.$toast.error(this.$strings.ToastRemoveFailed)
})
}
},
@@ -95,7 +95,7 @@ export default {
return
}
if (!this.newPlaylistName) {
return this.$toast.error('Playlist must have a name')
return this.$toast.error(this.$strings.ToastNameRequired)
}
this.processing = true
@@ -115,7 +115,7 @@ export default {
.catch((error) => {
console.error('Failed to update playlist', error)
this.processing = false
this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
this.$toast.error(this.$strings.ToastFailedToUpdate)
})
}
},
@@ -142,7 +142,7 @@ export default {
const updatedDetails = this.getUpdatePayload()
if (!Object.keys(updatedDetails).length) {
this.$toast.info('No changes were made')
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
return false
}
return this.updateDetails(updatedDetails)
@@ -105,7 +105,7 @@ export default {
}
const updatePayload = this.getUpdatePayload(episodeData)
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)
@@ -126,7 +126,7 @@ export default {
},
submitForm() {
if (!this.episodeTitle || !this.episodeTitle.length) {
this.$toast.error('Must enter an episode title')
this.$toast.error(this.$strings.ToastTitleRequired)
return
}
this.searchedTitle = this.episodeTitle
@@ -121,14 +121,14 @@ export default {
methods: {
openFeed() {
if (!this.newFeedSlug) {
this.$toast.error('Must set a feed slug')
this.$toast.error(this.$strings.ToastSlugRequired)
return
}
const sanitized = this.$sanitizeSlug(this.newFeedSlug)
if (this.newFeedSlug !== sanitized) {
this.newFeedSlug = sanitized
this.$toast.warning('Slug had to be modified - Run again')
this.$toast.warning(this.$strings.ToastSlugMustChange)
return
}
@@ -1,38 +1,37 @@
<template>
<div class="flex items-center pt-4 pb-2 lg:pt-0 lg:pb-2">
<div class="flex-grow" />
<template v-if="!loading">
<ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 lg:mr-8">
<button :aria-label="$strings.ButtonPreviousChapter" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
<span class="material-symbols text-2xl sm:text-3xl">first_page</span>
<div class="flex justify-center pt-4 pb-2 lg:pt-0 lg:pb-2">
<div class="flex items-center justify-center flex-grow">
<template v-if="!loading">
<ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 lg:mr-8">
<button :aria-label="$strings.ButtonPreviousChapter" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
<span class="material-symbols text-2xl sm:text-3xl">first_page</span>
</button>
</ui-tooltip>
<ui-tooltip direction="top" :text="jumpBackwardText">
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
<span class="material-symbols text-2xl sm:text-3xl">replay</span>
</button>
</ui-tooltip>
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
<span class="material-symbols fill text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
</button>
</ui-tooltip>
<ui-tooltip direction="top" :text="jumpBackwardText">
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
<span class="material-symbols text-2xl sm:text-3xl">replay</span>
</button>
</ui-tooltip>
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
<span class="material-symbols fill text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
</button>
<ui-tooltip direction="top" :text="jumpForwardText">
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
<span class="material-symbols text-2xl sm:text-3xl">forward_media</span>
</button>
</ui-tooltip>
<ui-tooltip direction="top" :text="$strings.ButtonNextChapter" 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">
<span class="material-symbols text-2xl sm:text-3xl">last_page</span>
</button>
</ui-tooltip>
<controls-playback-speed-control v-model="playbackRateInput" @input="playbackRateUpdated" @change="playbackRateChanged" />
</template>
<template v-else>
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
<span class="material-symbols text-2xl">autorenew</span>
</div>
</template>
<div class="flex-grow" />
<ui-tooltip direction="top" :text="jumpForwardText">
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
<span class="material-symbols text-2xl sm:text-3xl">forward_media</span>
</button>
</ui-tooltip>
<ui-tooltip direction="top" :text="hasNextLabel" class="ml-4 lg:ml-8">
<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>
</button>
</ui-tooltip>
</template>
<template v-else>
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
<span class="material-symbols text-2xl">autorenew</span>
</div>
</template>
</div>
</div>
</template>
@@ -41,27 +40,26 @@ export default {
props: {
loading: Boolean,
seekLoading: Boolean,
playbackRate: Number,
paused: Boolean,
hasNextChapter: Boolean
hasNextChapter: Boolean,
hasNextItemInQueue: Boolean
},
data() {
return {}
},
computed: {
playbackRateInput: {
get() {
return this.playbackRate
},
set(val) {
this.$emit('update:playbackRate', val)
}
},
jumpForwardText() {
return this.getJumpText('jumpForwardAmount', this.$strings.ButtonJumpForward)
},
jumpBackwardText() {
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: {
@@ -71,9 +69,9 @@ export default {
prevChapter() {
this.$emit('prevChapter')
},
nextChapter() {
if (!this.hasNextChapter) return
this.$emit('nextChapter')
next() {
if (!this.hasNext) return
this.$emit('next')
},
jumpBackward() {
this.$emit('jumpBackward')
@@ -81,15 +79,6 @@ export default {
jumpForward() {
this.$emit('jumpForward')
},
playbackRateUpdated(playbackRate) {
this.$emit('setPlaybackRate', playbackRate)
},
playbackRateChanged(playbackRate) {
this.$emit('setPlaybackRate', playbackRate)
this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => {
console.error('Failed to update settings', err)
})
},
getJumpText(setting, prefix) {
const amount = this.$store.getters['user/getUserSetting'](setting)
if (!amount) return prefix
+33 -35
View File
@@ -2,9 +2,9 @@
<div class="w-full -mt-6">
<div class="w-full relative mb-1">
<div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full">
<!-- <span class="material-symbols text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
<controls-playback-speed-control v-model="playbackRate" @input="setPlaybackRate" @change="playbackRateChanged" class="mx-2 block" />
<ui-tooltip direction="top" :text="$strings.LabelVolume">
<ui-tooltip direction="left" :text="$strings.LabelVolume">
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden sm:block" />
</ui-tooltip>
@@ -13,7 +13,7 @@
<span v-if="!sleepTimerSet" class="material-symbols text-2xl">snooze</span>
<div v-else class="flex items-center">
<span class="material-symbols text-lg text-warning">snooze</span>
<p class="text-sm sm:text-lg text-warning font-mono font-semibold text-center px-0.5 sm:pb-0.5 sm:min-w-8">{{ sleepTimerRemainingString }}</p>
<p class="text-sm sm:text-lg text-warning font-semibold text-center px-0.5 sm:pb-0.5 sm:min-w-8">{{ sleepTimerRemainingString }}</p>
</div>
</button>
</ui-tooltip>
@@ -43,20 +43,24 @@
</ui-tooltip>
</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>
<player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" :current-chapter="currentChapter" :playback-rate="playbackRate" @seek="seek" />
<div class="flex">
<p ref="currentTimestamp" class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">00:00:00</p>
<p class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto">&nbsp;/&nbsp;{{ progressPercent }}%</p>
<div class="flex-grow" />
<p class="text-xs sm:text-sm text-gray-300 pt-0.5 px-2 truncate">
{{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400">&nbsp;({{ $getString('LabelPlayerChapterNumberMarker', [currentChapterIndex + 1, chapters.length]) }})</span>
</p>
<div class="flex-grow" />
<p class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
<div class="relative flex items-center justify-between">
<div class="flex-grow flex items-center">
<p ref="currentTimestamp" class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">00:00:00</p>
<p class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto">&nbsp;/&nbsp;{{ progressPercent }}%</p>
</div>
<div class="absolute left-1/2 transform -translate-x-1/2">
<p class="text-xs sm:text-sm text-gray-300 pt-0.5 px-2 truncate">
{{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400">&nbsp;({{ $getString('LabelPlayerChapterNumberMarker', [currentChapterIndex + 1, chapters.length]) }})</span>
</p>
</div>
<div class="flex-grow flex items-center justify-end">
<p class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
</div>
</div>
<modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :playback-rate="playbackRate" :chapters="chapters" @select="selectChapter" />
@@ -82,7 +86,8 @@ export default {
sleepTimerType: String,
isPodcast: Boolean,
hideBookmarks: Boolean,
hideSleepTimer: Boolean
hideSleepTimer: Boolean,
hasNextItemInQueue: Boolean
},
data() {
return {
@@ -145,7 +150,7 @@ export default {
return Math.round((100 * time) / duration)
},
currentChapterName() {
return this.currentChapter ? this.currentChapter.title : ''
return this.currentChapter?.title || ''
},
currentChapterDuration() {
if (!this.currentChapter) return 0
@@ -177,22 +182,6 @@ export default {
methods: {
toggleFullscreen(isFullscreen) {
this.$store.commit('setPlayerIsFullscreen', isFullscreen)
var videoPlayerEl = document.getElementById('video-player')
if (videoPlayerEl) {
if (isFullscreen) {
videoPlayerEl.style.width = '100vw'
videoPlayerEl.style.height = '100vh'
videoPlayerEl.style.top = '0px'
videoPlayerEl.style.left = '0px'
} else {
videoPlayerEl.style.width = '384px'
videoPlayerEl.style.height = '216px'
videoPlayerEl.style.top = 'unset'
videoPlayerEl.style.bottom = '80px'
videoPlayerEl.style.left = '16px'
}
}
},
setDuration(duration) {
this.duration = duration
@@ -239,6 +228,12 @@ export default {
this.playbackRate = Number((this.playbackRate - 0.1).toFixed(1))
this.setPlaybackRate(this.playbackRate)
},
playbackRateChanged(playbackRate) {
this.setPlaybackRate(playbackRate)
this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => {
console.error('Failed to update settings', err)
})
},
setPlaybackRate(playbackRate) {
this.$emit('setPlaybackRate', playbackRate)
},
@@ -278,10 +273,13 @@ export default {
this.seek(this.currentChapter.start)
}
},
nextChapter() {
if (!this.currentChapter || !this.hasNextChapter) return
var nextChapter = this.chapters[this.currentChapterIndex + 1]
this.seek(nextChapter.start)
goToNext() {
if (this.hasNextChapter) {
const nextChapter = this.chapters[this.currentChapterIndex + 1]
this.seek(nextChapter.start)
} else if (this.hasNextItemInQueue) {
this.$emit('nextItemInQueue')
}
},
setStreamReady() {
if (this.$refs.trackbar) this.$refs.trackbar.setPercentageReady(1)
@@ -35,22 +35,22 @@
<div class="flex justify-between pt-12">
<div>
<p class="text-sm text-center">{{ $strings.LabelStatsWeekListening }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ totalMinutesListeningThisWeek }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ $formatNumber(totalMinutesListeningThisWeek) }}</p>
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
</div>
<div>
<p class="text-sm text-center">{{ $strings.LabelStatsDailyAverage }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ averageMinutesPerDay }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ $formatNumber(averageMinutesPerDay) }}</p>
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
</div>
<div>
<p class="text-sm text-center">{{ $strings.LabelStatsBestDay }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ mostListenedDay }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ $formatNumber(mostListenedDay) }}</p>
<p class="text-sm text-center">{{ $strings.LabelStatsMinutes }}</p>
</div>
<div>
<p class="text-sm text-center">{{ $strings.LabelStatsDays }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ daysInARow }}</p>
<p class="text-5xl font-semibold text-center" style="line-height: 0.85">{{ $formatNumber(daysInARow) }}</p>
<p class="text-sm text-center">{{ $strings.LabelStatsInARow }}</p>
</div>
</div>
+3 -3
View File
@@ -29,7 +29,7 @@
</div>
<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">
<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>
@@ -37,7 +37,7 @@
</div>
<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">
<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>
@@ -103,4 +103,4 @@ export default {
methods: {},
mounted() {}
}
</script>
</script>
+17 -15
View File
@@ -73,7 +73,7 @@ export default {
const addIcon = (icon, color, fontSize, x, y) => {
ctx.fillStyle = color
ctx.font = `${fontSize} Material Symbols Outlined`
ctx.font = `${fontSize} Material Symbols Rounded`
ctx.fillText(icon, x, y)
}
@@ -132,6 +132,8 @@ export default {
ctx.restore()
}
const twoColumnWidth = 210
ctx.globalAlpha = 1
ctx.textBaseline = 'middle'
@@ -150,12 +152,12 @@ export default {
// Top text
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
createRoundedRect(50, 100, 340, 160)
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()
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'
@@ -164,40 +166,40 @@ export default {
// Box top right
createRoundedRect(410, 100, 340, 160)
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)
// Box bottom left
createRoundedRect(50, 280, 340, 160)
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)
// Box bottom right
createRoundedRect(410, 280, 340, 160)
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)
if (!this.variant) {
// Text stats
const topNarrator = this.yearStats.mostListenedNarrator
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(this.$elapsedPrettyExtended(topNarrator.time, true, false), '24px', 'lighter', 'white', '1px', 70, 599)
}
const topGenre = this.yearStats.topGenres[0]
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(this.$elapsedPrettyExtended(topGenre.time, true, false), '24px', 'lighter', 'white', '1px', 430, 599)
}
const topAuthor = this.yearStats.topAuthors[0]
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(this.$elapsedPrettyExtended(topAuthor.time, true, false), '24px', 'lighter', 'white', '1px', 70, 749)
}
@@ -205,7 +207,7 @@ export default {
if (this.yearStats.mostListenedMonth?.time) {
const jsdate = new Date(this.year, this.yearStats.mostListenedMonth.month, 1)
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(this.$elapsedPrettyExtended(this.yearStats.mostListenedMonth.time, true, false), '24px', 'lighter', 'white', '1px', 430, 749)
}
@@ -214,7 +216,7 @@ export default {
finishedBookCoverImgs = Object.values(finishedBookCoverImgs)
if (finishedBookCoverImgs.length > 0) {
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++) {
let imgToAdd = finishedBookCoverImgs[i]
@@ -224,14 +226,14 @@ export default {
} else if (this.variant === 2) {
// Text stats
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++) {
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 584 + i * 60, 330)
}
}
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++) {
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 584 + i * 60, 330)
}
@@ -259,11 +261,11 @@ export default {
.catch((error) => {
console.error('Failed to share', error)
if (error.name !== 'AbortError') {
this.$toast.error('Failed to share: ' + error.message)
this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
}
})
} 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">
<!-- hack to get icon fonts loaded on init -->
<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" />
</div>
@@ -38,7 +38,7 @@
<!-- next button -->
<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="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>
</div>
<stats-year-in-review ref="yearInReview" :variant="yearInReviewVariant" :year="yearInReviewYear" :processing.sync="processingYearInReview" />
@@ -74,7 +74,7 @@
<!-- next button -->
<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="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>
</div>
</div>
@@ -138,4 +138,4 @@ export default {
}
}
}
</script>
</script>
+15 -13
View File
@@ -123,6 +123,8 @@ export default {
ctx.restore()
}
const threeColumnTextWidth = 200
ctx.globalAlpha = 1
ctx.textBaseline = 'middle'
@@ -141,33 +143,33 @@ export default {
// Top text
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
createRoundedRect(40, 100, 230, 100)
ctx.textAlign = 'center'
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
createRoundedRect(285, 100, 230, 100)
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
createRoundedRect(530, 100, 230, 100)
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
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.totalBooksAddedSize), '20px', 'lighter', 'white', '0px', canvas.width / 2, 330)
}
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.totalBooksAddedDuration, true, false), '20px', 'lighter', 'white', '0px', canvas.width / 2, 470)
}
@@ -176,7 +178,7 @@ export default {
// Bottom images
imgsToAdd = Object.values(imgsToAdd)
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++) {
let imgToAdd = imgsToAdd[i]
@@ -187,14 +189,14 @@ export default {
// Text stats
ctx.textAlign = 'left'
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++) {
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
}
}
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++) {
addText(this.yearStats.topNarrators[i].name, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
}
@@ -203,14 +205,14 @@ export default {
// Text stats
ctx.textAlign = 'left'
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++) {
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
}
}
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++) {
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
}
@@ -235,11 +237,11 @@ export default {
.catch((error) => {
console.error('Failed to share', error)
if (error.name !== 'AbortError') {
this.$toast.error('Failed to share: ' + error.message)
this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
}
})
} 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) => {
ctx.fillStyle = color
ctx.font = `${fontSize} Material Symbols Outlined`
ctx.font = `${fontSize} Material Symbols Rounded`
ctx.fillText(icon, x, y)
}
@@ -113,6 +113,8 @@ export default {
ctx.restore()
}
const twoColumnWidth = 180
ctx.globalAlpha = 1
ctx.textBaseline = 'middle'
@@ -131,12 +133,12 @@ export default {
// Top text
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
createRoundedRect(15, 75, 280, 110)
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()
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'
@@ -144,7 +146,7 @@ export default {
createRoundedRect(305, 75, 280, 110)
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)
this.canvas = canvas
@@ -165,11 +167,11 @@ export default {
.catch((error) => {
console.error('Failed to share', error)
if (error.name !== 'AbortError') {
this.$toast.error('Failed to share: ' + error.message)
this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
}
})
} 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">
<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">
<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>
<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() {
this.loadBackups()
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" />
<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' : ''">
<span class="material-symbols text-4xl">expand_more</span>
<span class="material-symbols text-4xl">&#xe313;</span>
</div>
</div>
<transition name="slide">
@@ -78,7 +78,7 @@ export default {
})
.catch((error) => {
console.error('Failed to update collection', error)
this.$toast.error('Failed to save collection books order')
this.$toast.error(this.$strings.ToastFailedToUpdate)
})
},
editBook(book) {
@@ -110,4 +110,4 @@ export default {
.collection-book-leave-active {
position: absolute;
}
</style>
</style>
@@ -45,7 +45,7 @@ export default {
methods: {
removeProvider(provider) {
const payload = {
message: `Are you sure you want remove custom metadata provider "${provider.name}"?`,
message: this.$getString('MessageConfirmDeleteMetadataProvider', [provider.name]),
callback: (confirmed) => {
if (confirmed) {
this.$emit('update:processing', true)
@@ -53,12 +53,12 @@ export default {
this.$axios
.$delete(`/api/custom-metadata-providers/${provider.id}`)
.then(() => {
this.$toast.success('Provider removed')
this.$toast.success(this.$strings.ToastProviderRemoveSuccess)
this.$emit('removed', provider.id)
})
.catch((error) => {
console.error('Failed to remove provider', error)
this.$toast.error('Failed to remove provider')
this.$toast.error(this.$strings.ToastRemoveFailed)
})
.finally(() => {
this.$emit('update:processing', false)
+3 -3
View File
@@ -8,7 +8,7 @@
<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>
<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>
<transition name="slide">
@@ -18,7 +18,7 @@
<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 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 v-if="showMoreColumn" class="text-center w-16"></th>
</tr>
@@ -92,4 +92,4 @@ export default {
}
}
}
</script>
</script>
@@ -1,7 +1,7 @@
<template>
<tr>
<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>
{{ $bytesPretty(file.metadata.size) }}
@@ -8,7 +8,7 @@
<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>
<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>
<transition name="slide">
@@ -103,4 +103,4 @@ export default {
this.showFiles = this.expanded
}
}
</script>
</script>
@@ -92,7 +92,7 @@ export default {
})
.catch((error) => {
console.error('Failed to update playlist', error)
this.$toast.error('Failed to save playlist items order')
this.$toast.error(this.$strings.ToastFailedToUpdate)
})
},
init() {
@@ -119,4 +119,4 @@ export default {
.playlist-item-leave-active {
position: absolute;
}
</style>
</style>
+2 -2
View File
@@ -11,7 +11,7 @@
<ui-btn small color="primary">{{ $strings.ButtonManageTracks }}</ui-btn>
</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' : ''">
<span class="material-symbols text-4xl">expand_more</span>
<span class="material-symbols text-4xl">&#xe313;</span>
</div>
</div>
<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)">
<button type="button" :aria-label="$getString('ButtonUserEdit', [user.username])" class="material-symbols text-base">edit</button>
</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>
</div>
</div>
@@ -157,10 +157,6 @@ export default {
this.init()
},
beforeDestroy() {
if (this.$refs.accountModal) {
this.$refs.accountModal.close()
}
if (this.$root.socket) {
this.$root.socket.off('user_added', this.addUpdateUser)
this.$root.socket.off('user_updated', this.addUpdateUser)
@@ -76,8 +76,7 @@ export default {
var newOrder = libraryOrderData.map((lib) => lib.id).join(',')
if (currOrder !== newOrder) {
this.$axios.$post('/api/libraries/order', libraryOrderData).then((response) => {
if (response.libraries && response.libraries.length) {
this.$toast.success('Library order saved', { timeout: 1500 })
if (response.libraries?.length) {
this.$store.commit('libraries/set', response.libraries)
}
})
@@ -110,4 +109,4 @@ export default {
this.$store.commit('libraries/removeListener', 'libraries-table')
}
}
</script>
</script>
@@ -218,12 +218,12 @@ export default {
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
} else {
console.log(`Item removed from playlist`, updatedPlaylist)
this.$toast.success('Item removed from playlist')
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
}
})
.catch((error) => {
console.error('Failed to remove item from playlist', error)
this.$toast.error('Failed to remove item from playlist')
this.$toast.error(this.$strings.ToastFailedToUpdate)
})
.finally(() => {
this.processingRemove = false
@@ -182,7 +182,7 @@ export default {
toggleFinished(confirmed = false) {
if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) {
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) => {
if (confirmed) {
this.toggleFinished(true)
@@ -233,4 +233,4 @@ export default {
},
mounted() {}
}
</script>
</script>
@@ -246,7 +246,7 @@ export default {
message: newIsFinished ? this.$strings.MessageConfirmMarkAllEpisodesFinished : this.$strings.MessageConfirmMarkAllEpisodesNotFinished,
callback: (confirmed) => {
if (confirmed) {
this.batchUpdateEpisodesFinished(this.episodesSorted, newIsFinished)
this.batchUpdateEpisodesFinished(this.episodesCopy, newIsFinished)
}
},
type: 'yesNo'
@@ -270,7 +270,7 @@ export default {
if (data.numEpisodesUpdated) {
this.$toast.success(`${data.numEpisodesUpdated} episodes updated`)
} else {
this.$toast.info('No changes were made')
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
}
})
.catch((error) => {
@@ -295,7 +295,7 @@ export default {
episodeId: episode.id,
title: episode.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,
coverPath: this.media.coverPath || null
}
@@ -305,6 +305,7 @@ export default {
this.batchUpdateEpisodesFinished(this.selectedEpisodes, !this.selectedIsFinished)
},
batchUpdateEpisodesFinished(episodes, newIsFinished) {
if (!episodes.length) return
this.processing = true
const updateProgressPayloads = episodes.map((episode) => {
@@ -371,7 +372,7 @@ export default {
episodeId: episode.id,
title: episode.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,
coverPath: this.media.coverPath || null
})
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
<slot :disabled="disabled" :showMenu="showMenu" :clickShowMenu="clickShowMenu" :processing="processing">
<button v-if="!processing" type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
<span class="material-symbols text-2xl" :class="iconClass">more_vert</span>
<span class="material-symbols text-2xl" :class="iconClass">&#xe5d4;</span>
</button>
<div v-else class="h-full w-full flex items-center justify-center">
<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" />
</svg>
</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>
</template>
@@ -86,4 +86,4 @@ button.icon-btn:disabled::before {
button.icon-btn:disabled span {
color: #777;
}
</style>
</style>
+8 -2
View File
@@ -4,13 +4,13 @@
<div ref="wrapper" class="relative">
<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'">
<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>
</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">
<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">
<span class="font-normal ml-3 block truncate">{{ item }}</span>
</div>
@@ -30,7 +30,10 @@
</template>
<script>
import menuKeyboardNavigationMixin from '@/mixins/menuKeyboardNavigation'
export default {
mixins: [menuKeyboardNavigationMixin],
props: {
value: [String, Number],
disabled: Boolean,
@@ -81,6 +84,9 @@ export default {
}
},
methods: {
keydownHandler(e) {
this.menuNavigationHandler(e)
},
setFocus() {
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>
<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>
</template>
@@ -17,7 +17,12 @@ export default {
props: {
text: {
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);
}
}
</style>
</style>
+6 -51
View File
@@ -37,7 +37,10 @@
</template>
<script>
import menuKeyboardNavigationMixin from '@/mixins/menuKeyboardNavigation'
export default {
mixins: [menuKeyboardNavigationMixin],
props: {
value: {
type: Array,
@@ -63,8 +66,7 @@ export default {
typingTimeout: null,
isFocused: false,
menu: null,
filteredItems: null,
selectedMenuItemIndex: null
filteredItems: null
}
},
watch: {
@@ -119,34 +121,8 @@ export default {
this.filteredItems = results || []
},
keydownInput(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()
return
} else if (event.key === 'Enter') {
if (this.selectedMenuItemIndex !== null) {
this.clickedOption(event, items[this.selectedMenuItemIndex])
} else {
this.submitForm()
}
return
}
this.selectedMenuItemIndex = null
this.menuNavigationHandler(event)
clearTimeout(this.typingTimeout)
this.typingTimeout = setTimeout(() => {
this.search()
@@ -161,24 +137,6 @@ export default {
this.recalcMenuPos()
}, 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() {
if (!this.menu || !this.$refs.inputWrapper) return
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
@@ -317,9 +275,6 @@ export default {
this.textInput = null
this.currentSearch = null
this.selectedMenuItemIndex = null
this.$nextTick(() => {
this.blur()
})
},
submitForm() {
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">
<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">
<span class="font-normal ml-3 block truncate">{{ item.name }}</span>
</div>
@@ -40,7 +40,10 @@
</template>
<script>
import menuKeyboardNavigationMixin from '@/mixins/menuKeyboardNavigation'
export default {
mixins: [menuKeyboardNavigationMixin],
props: {
value: {
type: Array,
@@ -63,8 +66,7 @@ export default {
typingTimeout: null,
isFocused: false,
menu: null,
items: [],
selectedMenuItemIndex: null
items: []
}
},
watch: {
@@ -124,34 +126,7 @@ export default {
this.items = results || []
},
keydownInput(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()
return
} else if (event.key === 'Enter') {
if (this.selectedMenuItemIndex !== null) {
this.clickedOption(event, items[this.selectedMenuItemIndex])
} else {
this.submitForm()
}
return
}
this.selectedMenuItemIndex = null
this.menuNavigationHandler(event)
clearTimeout(this.typingTimeout)
this.typingTimeout = setTimeout(() => {
this.search()
@@ -166,24 +141,6 @@ export default {
this.recalcMenuPos()
}, 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() {
if (!this.menu || !this.$refs.inputWrapper) return
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
@@ -323,9 +280,6 @@ export default {
this.textInput = null
this.currentSearch = null
this.selectedMenuItemIndex = null
this.$nextTick(() => {
this.blur()
})
},
submitForm() {
if (!this.textInput) return
+2 -2
View File
@@ -24,12 +24,12 @@ export default {
computed: {},
methods: {
clickBtn(e) {
e.stopPropagation()
if (this.disabled) {
e.preventDefault()
return
}
this.$emit('click')
e.stopPropagation()
}
},
mounted() {}
@@ -54,4 +54,4 @@ button.icon-btn:hover:not(:disabled)::before {
button.icon-btn:disabled::before {
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>
</div>
<div v-if="type === 'password' && isHovering" class="absolute top-0 right-0 h-full px-4 flex items-center justify-center">
<span class="material-symbols-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 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>
</template>
+2 -2
View File
@@ -1,7 +1,7 @@
<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="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>
<slot />
</div>
@@ -30,4 +30,4 @@ export default {
methods: {},
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">
<div class="flex flex-wrap -mx-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 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 class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-3/4 px-1">
<!-- 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 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 class="flex mt-2 -mx-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>
<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="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 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 class="flex flex-wrap mt-2 -mx-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 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 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 class="flex flex-wrap mt-2 -mx-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 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 class="flex-grow px-1 pt-6 mt-2 md:mt-0">
<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 class="flex-grow px-1 pt-6 mt-2 md:mt-0">
<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>
@@ -132,6 +132,12 @@ export default {
}
},
methods: {
handleInputChange() {
this.$emit('change', {
libraryItemId: this.libraryItem.id,
hasChanges: this.checkForChanges().hasChanges
})
},
getDetails() {
this.forceBlur()
return this.checkForChanges()
@@ -172,6 +178,7 @@ export default {
}
}
}
this.handleInputChange()
},
forceBlur() {
if (this.$refs.titleInput) this.$refs.titleInput.blur()
@@ -286,4 +293,4 @@ export default {
},
mounted() {}
}
</script>
</script>
@@ -1,9 +1,9 @@
<template>
<div>
<div class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent>
<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>
<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>
</template>
@@ -30,14 +30,14 @@
<div class="flex items-center justify-center">
<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-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>
</div>
</template>
<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>
</div>
</div>
@@ -3,45 +3,45 @@
<form class="w-full h-full px-4 py-6" @submit.prevent="submitForm">
<div class="flex -mx-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 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>
<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="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 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 class="flex mt-2 -mx-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 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 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 class="flex-grow px-1 pt-6">
<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 class="flex mt-2 -mx-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>
</form>
@@ -105,6 +105,12 @@ export default {
}
},
methods: {
handleInputChange() {
this.$emit('change', {
libraryItemId: this.libraryItem.id,
hasChanges: this.checkForChanges().hasChanges
})
},
getDetails() {
this.forceBlur()
return this.checkForChanges()
@@ -136,6 +142,8 @@ export default {
}
}
}
this.handleInputChange()
},
forceBlur() {
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
}
}
}
+6 -4
View File
@@ -129,10 +129,12 @@ module.exports = {
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {
postcss: {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
postcssOptions: {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}
}
},
watchers: {
+3106 -2020
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "2.12.0",
"version": "2.14.0",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast client",
"main": "index.js",
@@ -27,7 +27,7 @@
"fast-average-color": "^9.4.0",
"hls.js": "^1.5.7",
"libarchive.js": "^1.3.0",
"nuxt": "^2.17.3",
"nuxt": "^2.18.1",
"nuxt-socket-io": "^1.1.18",
"trix": "^1.3.1",
"v-click-outside": "^3.1.2",
+6 -6
View File
@@ -117,10 +117,10 @@ export default {
},
submitChangePassword() {
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) {
return this.$toast.error('Password and New Password cannot be the same')
return this.$toast.error(this.$strings.ToastUserPasswordMustChange)
}
this.changingPassword = true
this.$axios
@@ -130,16 +130,16 @@ export default {
})
.then((res) => {
if (res.success) {
this.$toast.success('Password Changed Successfully')
this.$toast.success(this.$strings.ToastUserPasswordChangeSuccess)
this.resetForm()
} else {
this.$toast.error(res.error || 'Unknown Error')
this.$toast.error(res.error || this.$strings.ToastUnknownError)
}
this.changingPassword = false
})
.catch((error) => {
console.error(error)
this.$toast.error('Api call failed')
this.$toast.error(this.$strings.ToastUnknownError)
this.changingPassword = false
})
}
@@ -148,4 +148,4 @@ export default {
this.selectedLanguage = this.$languageCodes.current
}
}
</script>
</script>
+22 -10
View File
@@ -71,7 +71,7 @@
<div class="flex items-center">
<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)">
<span class="material-symbols-outlined text-base">remove</span>
<span class="material-symbols text-base">remove</span>
</button>
</ui-tooltip>
@@ -84,14 +84,14 @@
<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)">
<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 class="material-symbols-outlined text-base">play_arrow</span>
<span v-else-if="selectedChapterId === chapter.id && isPlayingChapter" class="material-symbols text-base">pause</span>
<span v-else class="material-symbols text-base">play_arrow</span>
</button>
</ui-tooltip>
<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">
<span class="material-symbols-outlined text-lg">error_outline</span>
<span class="material-symbols text-lg">error_outline</span>
</button>
</ui-tooltip>
</div>
@@ -106,7 +106,7 @@
<div class="flex-grow" />
<ui-btn small @click="setChaptersFromTracks">{{ $strings.ButtonSetChaptersFromTracks }}</ui-btn>
<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>
</div>
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
@@ -189,7 +189,7 @@
<div class="flex items-center pt-2">
<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">
<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>
<div class="flex-grow" />
<ui-btn small color="success" @click="applyChapterData">{{ $strings.ButtonApplyChapters }}</ui-btn>
@@ -499,7 +499,7 @@ export default {
.catch((error) => {
this.saving = false
console.error('Failed to update chapters', error)
this.$toast.error('Failed to update chapters')
this.$toast.error(this.$strings.ToastFailedToUpdate)
})
},
applyChapterNamesOnly() {
@@ -560,7 +560,7 @@ export default {
.catch((error) => {
this.findingChapters = false
console.error('Failed to get chapter data', error)
this.$toast.error('Failed to find chapters')
this.$toast.error(this.$strings.ToastFailedToLoadData)
this.showFindChaptersModal = false
})
},
@@ -611,7 +611,7 @@ export default {
.$post(`/api/items/${this.libraryItem.id}/chapters`, payload)
.then((data) => {
if (data.updated) {
this.$toast.success('Chapters removed')
this.$toast.success(this.$strings.ToastChaptersRemoved)
if (this.previousRoute) {
this.$router.push(this.previousRoute)
} else {
@@ -623,20 +623,32 @@ export default {
})
.catch((error) => {
console.error('Failed to remove chapters', error)
this.$toast.error('Failed to remove chapters')
this.$toast.error(this.$strings.ToastRemoveFailed)
})
.finally(() => {
this.saving = false
})
},
libraryItemUpdated(libraryItem) {
if (libraryItem.id === this.libraryItem.id) {
if (!!libraryItem.media.metadata.asin && this.mediaMetadata.asin !== libraryItem.media.metadata.asin) {
this.asinInput = libraryItem.media.metadata.asin
}
this.libraryItem = libraryItem
}
}
},
mounted() {
this.regionInput = localStorage.getItem('audibleRegion') || 'US'
this.asinInput = this.mediaMetadata.asin || null
this.initChapters()
this.$eventBus.$on(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
},
beforeDestroy() {
this.destroyAudioEl()
this.$eventBus.$off(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
}
}
</script>
+2 -2
View File
@@ -331,11 +331,11 @@ export default {
this.$axios
.$delete(`/api/tools/item/${this.libraryItemId}/encode-m4b`)
.then(() => {
this.$toast.success('Encode canceled')
this.$toast.success(this.$strings.ToastEncodeCancelSucces)
})
.catch((error) => {
console.error('Failed to cancel encode', error)
this.$toast.error('Failed to cancel encode')
this.$toast.error(this.$strings.ToastEncodeCancelFailed)
})
.finally(() => {
this.isCancelingEncode = false
+40 -38
View File
@@ -97,8 +97,8 @@
<div class="flex justify-center flex-wrap">
<template v-for="libraryItem in libraryItemCopies">
<div :key="libraryItem.id" class="w-full max-w-3xl border border-black-300 p-6 -ml-px -mt-px">
<widgets-book-details-edit v-if="libraryItem.mediaType === 'book'" :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" />
<widgets-podcast-details-edit v-else :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" @change="handleItemChange" />
</div>
</template>
</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="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>
</template>
@@ -170,7 +170,8 @@ export default {
abridged: false
},
appendableKeys: ['authors', 'genres', 'tags', 'narrators', 'series'],
openMapOptions: false
openMapOptions: false,
itemsWithChanges: []
}
},
computed: {
@@ -221,9 +222,19 @@ export default {
},
hasSelectedBatchUsage() {
return Object.values(this.selectedBatchUsage).some((b) => !!b)
},
hasChanges() {
return this.itemsWithChanges.length > 0
}
},
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() {
if (this.$refs.seriesSelect && this.$refs.seriesSelect.isFocused) {
this.$refs.seriesSelect.forceBlur()
@@ -283,38 +294,10 @@ export default {
removedSeriesItem(item) {},
newNarratorItem(item) {},
removedNarratorItem(item) {},
newTagItem(item) {
// if (item && !this.newTagItems.includes(item)) {
// this.newTagItems.push(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)
// }
// }
},
newTagItem(item) {},
removedTagItem(item) {},
newGenreItem(item) {},
removedGenreItem(item) {},
init() {
// TODO: Better deep cloning of library items
this.libraryItemCopies = this.libraryItems.map((li) => {
@@ -366,7 +349,7 @@ export default {
}
}
if (!updates.length) {
return this.$toast.warning('No updates were made')
return this.$toast.warning(this.$strings.ToastNoUpdatesNecessary)
}
console.log('Pushing updates', updates)
@@ -376,6 +359,7 @@ export default {
.then((data) => {
this.isProcessing = false
if (data.updates) {
this.itemsWithChanges = []
this.$toast.success(`Successfully updated ${data.updates} items`)
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf`)
} else {
@@ -387,10 +371,28 @@ export default {
this.$toast.error('Failed to batch update')
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() {
this.init()
window.addEventListener('beforeunload', this.beforeUnload)
},
beforeDestroy() {
window.removeEventListener('beforeunload', this.beforeUnload)
}
}
</script>
@@ -406,4 +408,4 @@ export default {
transform: translateY(-100%);
transition: all 150ms ease-in 0s;
}
</style>
</style>
+1 -1
View File
@@ -16,7 +16,7 @@
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="clickPlay">
<span v-show="!streaming" class="material-symbols fill text-2xl -ml-2 pr-1 text-white">play_arrow</span>
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlayAll }}
</ui-btn>
<!-- RSS feed -->
+1 -1
View File
@@ -317,7 +317,7 @@ export default {
})
.catch((error) => {
console.error('Failed to update server settings', error)
this.$toast.error(this.$strings.ToastServerSettingsUpdateFailed)
this.$toast.error(this.$strings.ToastFailedToUpdate)
})
.finally(() => {
this.savingSettings = false
+6 -6
View File
@@ -3,7 +3,7 @@
<app-settings-content :header-text="$strings.HeaderBackups" :description="$strings.MessageBackupsDescription">
<div v-if="backupLocation" class="mb-4 max-w-full overflow-hidden">
<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>
</div>
<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 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">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.HeaderSchedule }}:</span>
</div>
@@ -44,7 +44,7 @@
</div>
<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">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNextBackupDate }}:</span>
</div>
@@ -162,7 +162,7 @@ export default {
})
.catch((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.ToastFailedToUpdate
this.$toast.error(errorMsg)
})
.finally(() => {
@@ -171,11 +171,11 @@ export default {
},
updateBackupsSettings() {
if (isNaN(this.maxBackupSize) || this.maxBackupSize < 0) {
this.$toast.error('Invalid maximum backup size')
this.$toast.error(this.$strings.ToastBackupInvalidMaxSize)
return
}
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
}
const updatePayload = {
+7 -8
View File
@@ -109,7 +109,7 @@
</tr>
</table>
<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>
</app-settings-content>
@@ -199,7 +199,7 @@ export default {
},
deleteDeviceClick(device) {
const payload = {
message: `Are you sure you want to delete e-reader device "${device.name}"?`,
message: this.$getString('MessageConfirmDeleteDevice', [device.name]),
callback: (confirmed) => {
if (confirmed) {
this.deleteDevice(device)
@@ -218,11 +218,10 @@ export default {
.$post(`/api/emails/ereader-devices`, payload)
.then((data) => {
this.ereaderDevicesUpdated(data.ereaderDevices)
this.$toast.success('Device deleted')
})
.catch((error) => {
console.error('Failed to delete device', error)
this.$toast.error('Failed to delete device')
this.$toast.error(this.$strings.ToastRemoveFailed)
})
.finally(() => {
this.deletingDeviceName = null
@@ -246,11 +245,11 @@ export default {
this.$axios
.$post('/api/emails/test')
.then(() => {
this.$toast.success('Test Email Sent')
this.$toast.success(this.$strings.ToastDeviceTestEmailSuccess)
})
.catch((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)
})
.finally(() => {
@@ -289,11 +288,11 @@ export default {
this.newSettings = {
...data.settings
}
this.$toast.success('Email settings updated')
this.$toast.success(this.$strings.ToastEmailSettingsUpdateSuccess)
})
.catch((error) => {
console.error('Failed to update email settings', error)
this.$toast.error('Failed to update email settings')
this.$toast.error(this.$strings.ToastFailedToUpdate)
})
.finally(() => {
this.savingSettings = false

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