Compare commits

...

733 Commits

Author SHA1 Message Date
advplyr 93114b2181 Version bump v2.11.0 2024-07-07 17:23:57 -05:00
advplyr f6dd3de8e7 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-07-07 17:20:20 -05:00
advplyr 0918391636 Update home page shelves to handle share open/closed sockets 2024-07-07 17:19:44 -05:00
advplyr 972b4f7388 Merge pull request #3133 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-07-07 17:10:58 -05:00
advplyr af92ae4d51 Add link to media item shares guide in share modal 2024-07-07 16:09:32 -05:00
Ahetek 3bc6426cc7 Translated using Weblate (Polish)
Currently translated at 89.5% (745 of 832 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2024-07-07 23:07:25 +02:00
Ahetek acfbbd5aec Translated using Weblate (Polish)
Currently translated at 84.7% (705 of 832 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2024-07-07 23:07:24 +02:00
Vito0912 9b677be12e Translated using Weblate (German)
Currently translated at 99.7% (830 of 832 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-07-07 23:07:24 +02:00
advplyr 2f2ec2ec1f Add book item more menu item for Share, restrict share to admin or up, add admin socket events for open/close shares 2024-07-07 15:51:50 -05:00
advplyr e05ab14ad2 Merge pull request #3128 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-07-06 16:28:54 -05:00
advplyr 9074e9ed88 Merge pull request #3125 from nichwall/changelog_link
Changelog Pub Date
2024-07-06 16:28:22 -05:00
advplyr 1e5cb09ada Update changelog version to link to release, pass versionData into changelog modal 2024-07-06 16:28:36 -05:00
advplyr b0f1827e3c Merge branch 'master' into changelog_link 2024-07-06 16:06:51 -05:00
gallegonovato ae7713bacc Translated using Weblate (Spanish)
Currently translated at 99.8% (831 of 832 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-07-06 21:03:28 +00:00
Jeldrik b6c185eebe Translated using Weblate (German)
Currently translated at 99.2% (824 of 830 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-07-06 21:03:28 +00:00
gallegonovato 5114be0773 Translated using Weblate (Spanish)
Currently translated at 100.0% (830 of 830 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-07-06 21:03:27 +00:00
advplyr 9a4c5a16ef Merge pull request #3111 from mikiher/tone-replacement
Replace tone with ffmpeg for metadata and cover embedding
2024-07-06 16:03:17 -05:00
advplyr e6b1acfb44 Remove tone scripts & references, rename tone-object endpoint, remove node-tone dependency, remove TONE_PATH env 2024-07-06 16:00:48 -05:00
advplyr 1e5787c60d Merge pull request #3126 from mikiher/pkg-replacement
Replace pkg with @yao-pkg/pkg and target node20
2024-07-06 14:33:52 -05:00
mikiher 928b080677 Replace pkg with @yao-pkg/pkg and target node20 2024-07-06 19:43:55 +03:00
Nicholas Wallace 3764ef14a9 Add: publish date of current version to modal 2024-07-06 16:21:06 +00:00
Nicholas Wallace 92aae736c4 Add: current pubdate 2024-07-06 15:57:20 +00:00
advplyr 3a2f786517 Merge pull request #3122 from nichwall/backup_field_prevent_edits_with_env
Prevent backup path edits when ENV is set
2024-07-05 16:10:14 -05:00
advplyr 7c0b4e35d7 Update backups config page to use backupPathEnvSet returned from endpoint, remove from ServerConfig 2024-07-05 16:10:07 -05:00
advplyr 0461b57e6c Merge pull request #3116 from nichwall/email_endpoints
Email endpoints
2024-07-05 15:35:37 -05:00
Nicholas Wallace a1688488e5 Fix: name of backupPathEnvSet variable 2024-07-05 17:58:42 +00:00
Nicholas Wallace 4d24817ced Prevent editing backup path in web interface when env variable set 2024-07-05 17:44:49 +00:00
Nicholas Wallace d46de541d6 Fix: bad variable name 2024-07-05 17:41:07 +00:00
Nicholas Wallace 37f62d22b6 Add: report whether backup path environment is set 2024-07-05 17:27:49 +00:00
advplyr b01ef1c691 Fix library shelf height on sorting by title with ignore prefixes and sorting by published year 2024-07-04 16:07:28 -05:00
advplyr 277ff8a5a5 Add:Book library filter for Share Open 2024-07-04 15:45:47 -05:00
advplyr d5f991ae4a Fix media share player screen height on android browsers 2024-07-04 15:28:44 -05:00
advplyr fed5ff4863 Add:Daily cron that closes stale open playback sessions 2024-07-04 12:00:54 -05:00
advplyr 43217657d7 Update media item shares to close when changing shares on same device 2024-07-04 11:19:29 -05:00
Nicholas Wallace fa1518cb1d Fix: wrong settings path 2024-07-04 03:51:54 +00:00
Nicholas Wallace 6d14ed8a72 Update: bundled spec 2024-07-04 03:48:22 +00:00
Nicholas Wallace b8e17de8b4 Add: EmailController to root.yaml 2024-07-04 03:45:04 +00:00
Nicholas Wallace e60a91379a Rename folder 2024-07-04 03:40:17 +00:00
Nicholas Wallace 046bf52d88 Initial EmailController paths 2024-07-04 03:36:01 +00:00
Nicholas Wallace bfc3c7e7c9 Initial email settings schemas 2024-07-04 03:18:52 +00:00
advplyr dd1d2b7c92 Fix media item share changing share, show error on failed to play 2024-07-03 17:08:30 -05:00
mikiher 8bdee51798 Add unit tests for new ffmpegHelpers functions 2024-07-03 23:50:42 +03:00
advplyr 5858b64fc6 Update:Share audio player page background color gradient using fast-average-color on cover image 2024-07-02 17:40:42 -05:00
advplyr 4baa89c8e1 Fix:Audio player chapter name text overflow 2024-07-02 16:54:39 -05:00
mikiher 1b015beba4 Remove windows restrictions from Tools.vuw 2024-07-02 19:00:03 +03:00
mikiher ebaec23648 Replace tone with ffmpeg in AbMergeManager 2024-07-02 18:25:04 +03:00
advplyr d5e00c8bbd Update:Get personalized home page shelves and get library items endpoint optional includes for media item shares, show public icon on shared book items 2024-07-01 17:26:13 -05:00
advplyr 4732ca8119 Embed track number 2024-07-01 16:57:14 -05:00
advplyr 134c2580c9 Update:Custom error page for nuxt 2024-06-30 16:46:54 -05:00
advplyr 8e286a6070 Open media item share sessions shown on listening sessions page, create device info for share sessions 2024-06-30 16:36:00 -05:00
advplyr d7ace4d1dc Update:Media item share URL allows for sending starting time as query string #1768 2024-06-30 15:31:27 -05:00
mikiher a21b1f3b16 Make required changes for mp3 embedding 2024-06-30 15:45:25 +03:00
advplyr c309856f74 Update:Media item share modal UI/UX and localization #1768 2024-06-29 16:15:55 -05:00
advplyr 31146082f0 Update:Media item share endpoints and audio player #1768
- Add endpoints for getting tracks, getting cover image and updating progress
- Implement share session cookie and caching share playback session
- Audio player UI/UX
2024-06-29 15:05:35 -05:00
mikiher 6fbbc65edf Replace tone with ffmpeg for metadata and cover embedding 2024-06-29 20:04:23 +03:00
advplyr c1349e586a Update share page to show player ui 2024-06-28 17:01:28 -05:00
advplyr 8985ebebe2 Merge pull request #3108 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-06-28 15:50:00 -05:00
J. Lavoie 394a004ff5 Translated using Weblate (Finnish)
Currently translated at 19.2% (158 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2024-06-27 23:36:51 +02:00
J. Lavoie 33e6ad4ad6 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.7% (819 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pt_BR/
2024-06-27 23:36:51 +02:00
J. Lavoie 05a0793a9c Translated using Weblate (Norwegian Bokmål)
Currently translated at 85.3% (701 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2024-06-27 23:36:51 +02:00
J. Lavoie 3a5e9cd865 Translated using Weblate (Italian)
Currently translated at 97.8% (803 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2024-06-27 23:36:51 +02:00
J. Lavoie a7cd79850d Translated using Weblate (French)
Currently translated at 100.0% (821 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-06-27 23:36:51 +02:00
J. Lavoie 386edb0427 Translated using Weblate (Spanish)
Currently translated at 100.0% (821 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-06-27 23:36:51 +02:00
J. Lavoie 6c1e25e964 Translated using Weblate (German)
Currently translated at 100.0% (821 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-06-27 23:36:51 +02:00
J. Lavoie a6a956fc28 Translated using Weblate (Italian)
Currently translated at 97.4% (800 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2024-06-27 23:36:51 +02:00
J. Lavoie fb7d6807e2 Translated using Weblate (French)
Currently translated at 99.7% (819 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-06-27 23:36:51 +02:00
J. Lavoie e9f8ca1c14 Translated using Weblate (German)
Currently translated at 100.0% (821 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-06-27 23:36:51 +02:00
dex girl c669ca5be1 Translated using Weblate (Croatian)
Currently translated at 62.3% (512 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-06-27 23:36:51 +02:00
Allan Nordhøy 6dd0fb4225 Translated using Weblate (Norwegian Bokmål)
Currently translated at 85.3% (701 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2024-06-27 23:36:51 +02:00
Allan Nordhøy 709f9a65fa Translated using Weblate (Norwegian Bokmål)
Currently translated at 85.5% (702 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2024-06-27 23:36:51 +02:00
phewi 3c888d2876 Translated using Weblate (Finnish)
Currently translated at 18.3% (151 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2024-06-27 23:36:51 +02:00
Allan Nordhøy aca39011bb Translated using Weblate (Norwegian Bokmål)
Currently translated at 85.6% (703 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2024-06-27 23:36:51 +02:00
phewi f6fc53d7d8 Translated using Weblate (Finnish)
Currently translated at 7.6% (63 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2024-06-27 23:36:51 +02:00
advplyr 599623570b Merge pull request #3107 from taxilian/bug/oldLibraryItemNull
bug: if oldLibraryItem is null things crash
2024-06-27 16:36:29 -05:00
advplyr 67b47785a0 Update:Author endpoints to use faster db call to get number of books 2024-06-27 16:37:43 -05:00
advplyr 56c0124c13 Fix:Changing author name not updating library item metadata files #3060 2024-06-27 16:32:38 -05:00
Richard Bateman f9e270e4be bug: if oldLibraryItem is null things crash 2024-06-27 14:32:14 -06:00
advplyr 8cadaa57f6 Update share endpoint to return playback session, add get share file endpoint 2024-06-26 17:03:12 -05:00
advplyr 042035051d Merge pull request #3037 from mikiher/bookshelf-refactor-2
Bookshelf and cards refactoring
2024-06-25 15:54:31 -05:00
advplyr 12ce3a6147 Merge pull request #3103 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-06-25 08:31:25 -05:00
mikiher 9bf4bd9bfa Merge branch 'advplyr:master' into bookshelf-refactor-2 2024-06-25 10:03:18 +03:00
mikiher 2819317924 Remove now-unsued slider components 2024-06-25 09:14:33 +03:00
mikiher e06ab594e1 Revert "feat: Add a Show Subtitles option"
This reverts commit 3ef189ed4a.
2024-06-25 08:57:09 +03:00
advplyr 04a65648a3 Merge pull request #3099 from mattbasta/patch-1
Add user agent string to feed requests
2024-06-24 17:19:27 -05:00
advplyr 2673742d8d Update User-Agent strings 2024-06-24 17:14:20 -05:00
advplyr 090c02079d Fix bookshelf view search page showing tags shelf, fix bookshelf row arrow overlay height 2024-06-24 16:44:29 -05:00
advplyr 514fb5f7da Fix ItemSlider component select 2024-06-24 16:37:02 -05:00
advplyr f541bc2159 Fix ItemSlider component on search page 2024-06-24 16:24:30 -05:00
Mario d70810364c Translated using Weblate (German)
Currently translated at 100.0% (821 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-06-24 17:13:23 +02:00
advplyr 09d7880779 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-06-23 14:14:56 -05:00
SunSpring c69e6bff10 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (821 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-06-23 19:13:54 +00:00
chris sollami b49c2e7b82 Translated using Weblate (Italian)
Currently translated at 97.4% (800 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2024-06-23 19:13:53 +00:00
gallegonovato d012b2107d Translated using Weblate (Spanish)
Currently translated at 100.0% (821 of 821 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-06-23 19:13:53 +00:00
Plazec 9294521632 Translated using Weblate (Czech)
Currently translated at 99.2% (813 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2024-06-23 19:13:52 +00:00
Nicholas W 7d05317357 Notification endpoints (#3096)
* Initial notification schema

* Add: notification event and settings schema

* Add: NotificationController

* Update bundled spec

* Fix: `operationId` typos

* Fix: library response to be arroy of objects

* Fix: notification ID is not uuid

* Add: `nullable` notification creation parameters

* Nullable libraryId schema

* Remove: `id` from Notification requestBody

* Fix: `allOf` for `libraryItemSequence`

* Fix: required `id` in wrong body

* Fix: libraryItem typos

* Update: bundled spec
2024-06-23 14:12:10 -05:00
Matt Basta 2843a3b6d7 Add user agent string to feed requests 2024-06-23 12:35:37 -04:00
mikiher 635f22ddfe Reverted default spacing and font-sizing changes, and extended spaing with em-based variants 2024-06-23 19:15:39 +03:00
advplyr 903b685e1a Update jsdocs 2024-06-23 11:01:25 -05:00
advplyr 09bcc1191f Merge pull request #3095 from nichwall/parameter_changes
Change: `requestBody` to `parameter`, allow commas in queries
2024-06-23 07:41:21 -05:00
advplyr d6eae9b43e Add:Create media item shares with expiration #1768 2024-06-22 16:42:13 -05:00
Nicholas Wallace f95d9bd0e9 Change: requestBody to parameter, allow commas in queries 2024-06-21 22:25:22 +00:00
advplyr e52b695f7e Update:Change chapters table End column to a Duration column #3093 2024-06-21 16:58:24 -05:00
advplyr 72c1407aa7 Fix:Automatic library scans using stale copy of library object resulting in reverting saved changes to it #3079 #2894 2024-06-20 17:08:18 -05:00
advplyr 2ec49cbdb1 Update en-us string order 2024-06-19 17:17:40 -05:00
advplyr 331d7a41ab Add:Ability to edit backup location path on backups page #2973
- Added api endpoint PATCH /api/backups/path
- Cleanup backup page UI for mobile screens
2024-06-19 17:14:37 -05:00
advplyr 8498cab842 Merge pull request #3086 from taxilian/bug/itemProgressNull
bug: If !itemProgress unhandled exception syncing user progress
2024-06-19 15:28:46 -05:00
advplyr c170cb3132 Merge pull request #3089 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-06-19 15:18:22 -05:00
Charlie 0c58c9060e Translated using Weblate (French)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-06-19 06:45:05 +02:00
Richard Bateman e3c3903c71 bug: If !itemProgress unhandled exception syncing user progress 2024-06-18 18:52:37 -06:00
advplyr 7bc70effb0 Update:Add server setting for backupPath and allow overriding with BACKUP_PATH env variable #2973 2024-06-18 17:10:49 -05:00
kuci-JK 991da2870f Translated using Weblate (Czech)
Currently translated at 99.3% (814 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2024-06-18 22:08:58 +00:00
JL 52b632d810 Translated using Weblate (Danish)
Currently translated at 86.0% (705 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2024-06-18 22:08:58 +00:00
Plazec 33531ff73b Translated using Weblate (Czech)
Currently translated at 99.2% (813 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2024-06-18 22:08:57 +00:00
Plazec 391a777dde Translated using Weblate (Czech)
Currently translated at 92.1% (755 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2024-06-18 22:08:57 +00:00
pmangro 85e7b63532 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pt_BR/
2024-06-18 22:08:56 +00:00
pmangro b02429cf55 Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.5% (799 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pt_BR/
2024-06-18 22:08:56 +00:00
Smoukus 9e064e670a Translated using Weblate (Croatian)
Currently translated at 59.8% (490 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2024-06-18 22:08:55 +00:00
Valentin 61b3785038 Translated using Weblate (German)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-06-18 22:08:55 +00:00
advplyr a75ad5d659 Add:Finnish language option 2024-06-16 09:42:40 -05:00
advplyr 516a3858c5 Merge pull request #3080 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-06-16 08:52:14 -05:00
Petteri Hjort 364787db72 Translated using Weblate (Finnish)
Currently translated at 5.9% (49 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2024-06-15 23:45:03 +02:00
Vito0912 b2562ede55 Translated using Weblate (German)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-06-15 23:45:03 +02:00
Charlie c441d83d39 Translated using Weblate (French)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-06-15 23:45:03 +02:00
Nicholas W 08c6cc674b Added translation using Weblate (Finnish) 2024-06-15 23:45:03 +02:00
Nicholas W 9c34e4bd14 Translated using Weblate (Chinese (Traditional))
Currently translated at 93.5% (766 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hant/
2024-06-15 23:45:03 +02:00
Dmitry 9b159fc1e6 Translated using Weblate (Russian)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2024-06-15 23:45:03 +02:00
Nicholas W bcc2fa409e Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.5% (791 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pt_BR/
2024-06-15 23:45:03 +02:00
Nicholas W 360d54847c Translated using Weblate (Italian)
Currently translated at 97.4% (798 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2024-06-15 23:45:03 +02:00
Nicholas W b25314b4bd Translated using Weblate (Spanish)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-06-15 23:45:03 +02:00
Nicholas W c87f2a571e Translated using Weblate (Czech)
Currently translated at 88.0% (721 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2024-06-15 23:45:03 +02:00
Dmitry 8be02303f9 Translated using Weblate (Russian)
Currently translated at 97.8% (801 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2024-06-15 23:45:03 +02:00
SunSpring c6b4694b22 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-06-15 23:45:03 +02:00
Illia Pyshniak 4762cdb7d8 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2024-06-15 23:45:03 +02:00
Petras Šukys fe2a07bf4b Translated using Weblate (Lithuanian)
Currently translated at 86.0% (705 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/lt/
2024-06-15 23:45:03 +02:00
DiamondtipDR 9f80900717 Translated using Weblate (Spanish)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-06-15 23:45:03 +02:00
gallegonovato 6b001ad7a1 Translated using Weblate (Spanish)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-06-15 23:45:02 +02:00
Mario 4241544aaf Translated using Weblate (German)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-06-15 23:45:02 +02:00
A L 80bcc71c72 Translated using Weblate (Bulgarian)
Currently translated at 96.0% (787 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bg/
2024-06-15 23:45:02 +02:00
SunSpring 253095dcd6 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-06-15 23:45:02 +02:00
burghy86 0e4109a7c2 Translated using Weblate (Italian)
Currently translated at 97.3% (797 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2024-06-15 23:45:02 +02:00
Daniel Schosser 629741db92 Translated using Weblate (German)
Currently translated at 98.7% (809 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-06-15 23:45:02 +02:00
Mario 79236dd67d Translated using Weblate (German)
Currently translated at 98.7% (809 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-06-15 23:45:02 +02:00
SunSpring bdfb7b9af3 Translated using Weblate (Chinese (Simplified))
Currently translated at 97.5% (799 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-06-15 23:45:02 +02:00
Vito0912 665244f1b2 Translated using Weblate (German)
Currently translated at 97.9% (802 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-06-15 23:45:02 +02:00
gallegonovato b74f13bbd7 Translated using Weblate (Spanish)
Currently translated at 97.1% (796 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-06-15 23:45:02 +02:00
gallegonovato d1ee3af2d9 Translated using Weblate (Spanish)
Currently translated at 95.4% (782 of 819 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-06-15 23:45:02 +02:00
advplyr 38fa4d4169 Merge pull request #3078 from nichwall/time_parsing_fix
Time parsing fix
2024-06-15 16:44:57 -05:00
advplyr 56d3ed5a8e Merge pull request #3054 from nichwall/translation_readme_update
Translation readme update
2024-06-15 16:43:16 -05:00
Nicholas Wallace cadef9b023 Misnamed variable and cumulative length 2024-06-15 01:20:22 +00:00
Nicholas Wallace 34b340f179 Fix: overdrive mediamarkers parse hours 2024-06-15 01:17:07 +00:00
advplyr b89bbd2187 Update:Watcher pending delay to 10s. Increase file mtime check interval to 3s and timeout to 600s. Remove file from pending scan if it times out. 2024-06-14 16:50:09 -05:00
advplyr d6438590d7 Update library series endpoint openapi spec to use query parameters instead of request body 2024-06-13 17:13:55 -05:00
Nicholas W baf5f7fbc3 Initial library endpoints (#3012)
* Fix: extra type in `Author.yaml`

* Fix: formatting

* Initial library schema

* Additional debugging

* Fix: spec relative paths

* Add: ebook file spec

* Fix: response type should be string

* Linting updates

* Add: missing librarySettings

* Temporary fix: Library cron can be null or false

* Author controller updates

* Add: `/api/libraries/{id}` endpoint

* Update library responses

* Add: descriptions

* Fix: queries should be in body

* Fix: `body` should be `requestBody`

* Move: `libraryController` paths, clean up `requestBody`

* Clean up libraryController parameters

* Move: author endpoints to controller

* Add `get` for author images

* Simplify author schema with items

* Remove: unused response type

* Update: formatting

* Update json

* Update requestBody on LibraryController

* LibrarySettings update

* Replace: generic parameter with path specific parameter

* Fix: requestBody descriptions

* Fix: match post operation

* Temporary: nullable Author schemas

* LibraryController items endpoint

* Add: delete library items with issues

* Massive cleanup and violation fixing

* Update bundled spec

* Add: remove library items with issues

* Add: library items endpoint

* Fix: errors

* Fix: base schemas

* Add: series schemas

* Add: library series endpoint

* Fix: oneOf and array issues

* Add: author search region for matching

* Add: series endpoints

* Fix: series issues

* Add library series endpoint and update deprecation

* Fix: series endpoint deprecation

* Fix: `name` in `sortDesc` schema

* Add: workflow for linting spec

* Update OpenAPI readme
2024-06-13 17:09:02 -05:00
advplyr e6a2555f05 Merge pull request #3071 from JBlond/master
Follow-up translation
2024-06-12 16:56:52 -05:00
JBlond 36425e1fab Follow-up translation for
f682a7a283
  a6c5732693
  e9453d4f6c
  11d8669426
  5da4861716
  800cdc129d
2024-06-12 16:05:39 +02:00
advplyr 18efd95759 Merge pull request #3057 from Machou/master
Update fr.json
2024-06-11 17:07:27 -05:00
advplyr f682a7a283 Merge pull request #3063 from nichwall/localization_workflow_update
Update i18n workflow to `1.3.0`
2024-06-11 17:02:31 -05:00
Nicholas Wallace cb968ef4ca Update i18n workflow to 1.3.0 2024-06-10 19:22:03 -07:00
advplyr a6c5732693 Merge pull request #3053 from weblate/weblate-audiobookshelf-test-abs-web-client
Translations update from Hosted Weblate
2024-06-10 16:07:52 -05:00
Machou 7bbdc945d5 Update fr.json 2024-06-09 23:52:39 +02:00
Nicholas Wallace b37431dfaa Update translation guide 2024-06-09 20:10:15 +00:00
Nicholas Wallace a333ebe5b0 Readme formatting update 2024-06-09 20:09:47 +00:00
Nicholas W 4affcd0d89 Translated using Weblate (Chinese (Traditional))
Currently translated at 93.4% (765 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/zh_Hant/
2024-06-09 21:37:57 +02:00
Nicholas W 9d5e6351a4 Translated using Weblate (Vietnamese)
Currently translated at 87.7% (719 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/vi/
2024-06-09 21:37:57 +02:00
Nicholas W 91c25918f1 Translated using Weblate (Ukrainian)
Currently translated at 97.4% (798 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/uk/
2024-06-09 21:37:57 +02:00
Nicholas W bb88b5d861 Translated using Weblate (Swedish)
Currently translated at 88.8% (728 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/sv/
2024-06-09 21:37:57 +02:00
Nicholas W 11818a3576 Translated using Weblate (Russian)
Currently translated at 90.7% (743 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/ru/
2024-06-09 21:37:56 +02:00
Nicholas W f3de134980 Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.3% (789 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/pt_BR/
2024-06-09 21:37:56 +02:00
Nicholas W 9fa5db6976 Translated using Weblate (Polish)
Currently translated at 72.4% (593 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/pl/
2024-06-09 21:37:56 +02:00
Nicholas W 5e9043e5fa Translated using Weblate (Norwegian Bokmål)
Currently translated at 85.4% (700 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/nb_NO/
2024-06-09 21:37:55 +02:00
Nicholas W 84e275174c Translated using Weblate (Dutch)
Currently translated at 84.3% (691 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/nl/
2024-06-09 21:37:55 +02:00
Nicholas W ae90dd358e Translated using Weblate (Lithuanian)
Currently translated at 85.5% (701 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/lt/
2024-06-09 21:37:55 +02:00
Nicholas W 0cfd153694 Translated using Weblate (Italian)
Currently translated at 89.6% (734 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/it/
2024-06-09 21:37:54 +02:00
Nicholas W bf99d3d506 Translated using Weblate (Hungarian)
Currently translated at 91.8% (752 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/hu/
2024-06-09 21:37:54 +02:00
Nicholas W 9e055831fe Translated using Weblate (Croatian)
Currently translated at 59.2% (485 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/hr/
2024-06-09 21:37:52 +02:00
Nicholas W a349784da9 Translated using Weblate (Hindi)
Currently translated at 22.5% (185 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/hi/
2024-06-09 21:37:51 +02:00
Nicholas W 40f9e0f669 Translated using Weblate (Hebrew)
Currently translated at 94.3% (773 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/he/
2024-06-09 21:37:47 +02:00
Nicholas W c253a95127 Translated using Weblate (Gujarati)
Currently translated at 28.6% (235 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/gu/
2024-06-09 21:37:47 +02:00
Nicholas W d70d49b9da Translated using Weblate (French)
Currently translated at 94.0% (770 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/fr/
2024-06-09 21:37:38 +02:00
Nicholas W 16c5e4a398 Translated using Weblate (Estonian)
Currently translated at 93.4% (765 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/et/
2024-06-09 21:37:37 +02:00
Nicholas W d53d16c551 Translated using Weblate (Danish)
Currently translated at 85.9% (704 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/da/
2024-06-09 21:37:37 +02:00
Nicholas W 312be0f639 Translated using Weblate (Czech)
Currently translated at 87.9% (720 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/cs/
2024-06-09 21:37:37 +02:00
Nicholas W 0246dcc10d Translated using Weblate (Bengali)
Currently translated at 95.2% (780 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/bn/
2024-06-09 21:37:36 +02:00
Nicholas W 5aa1b14695 Translated using Weblate (Bulgarian)
Currently translated at 92.4% (757 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/bg/
2024-06-09 21:37:36 +02:00
Nicholas W 2ee24c1ded Translated using Weblate (English)
Currently translated at 24.6% (202 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/en/
2024-06-09 21:37:36 +02:00
Nicholas W 700afeacf0 Translated using Weblate (Chinese (Simplified))
Currently translated at 94.6% (775 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/zh_Hans/
2024-06-09 21:29:44 +02:00
Nicholas W e9453d4f6c Translated using Weblate (German)
Currently translated at 96.7% (792 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/de/
2024-06-09 21:23:08 +02:00
gallegonovato 661db2af26 Translated using Weblate (Spanish)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/es/
2024-06-09 19:17:33 +00:00
Nicholas W efd205716b Translated using Weblate (Italian)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/it/
2024-06-09 19:17:32 +00:00
Nicholas W 84144bb32a Translated using Weblate (Bengali)
Currently translated at 95.3% (781 of 819 strings)

Translation: Audiobookshelf/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/bn/
2024-06-09 19:17:32 +00:00
Anonymous 74a094c6df Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/zh_Hant/
2024-06-09 19:17:31 +00:00
Anonymous aa89aca632 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/zh_Hans/
2024-06-09 19:17:31 +00:00
Anonymous 8ac9a0d7c0 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/vi/
2024-06-09 19:17:30 +00:00
Anonymous 0119d7fcff Translated using Weblate (Ukrainian)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/uk/
2024-06-09 19:17:30 +00:00
Anonymous be513fde4f Translated using Weblate (Swedish)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/sv/
2024-06-09 19:17:29 +00:00
Anonymous 715199d88b Translated using Weblate (Russian)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/ru/
2024-06-09 19:17:29 +00:00
Anonymous 34942a3857 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/pt_BR/
2024-06-09 19:17:28 +00:00
Anonymous d67e916c66 Translated using Weblate (Polish)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/pl/
2024-06-09 19:17:28 +00:00
Anonymous e3e2d4ff99 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/nb_NO/
2024-06-09 19:17:27 +00:00
Anonymous 699615f2f3 Translated using Weblate (Dutch)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/nl/
2024-06-09 19:17:27 +00:00
Anonymous 6d267cac0d Translated using Weblate (Lithuanian)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/lt/
2024-06-09 19:17:26 +00:00
Anonymous 7d719d94ba Translated using Weblate (Italian)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/it/
2024-06-09 19:17:26 +00:00
Anonymous 4bf410fd3e Translated using Weblate (Hungarian)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/hu/
2024-06-09 19:17:25 +00:00
Anonymous 16cd05e187 Translated using Weblate (Croatian)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/hr/
2024-06-09 19:17:25 +00:00
Anonymous c7dcaa0316 Translated using Weblate (Hindi)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/hi/
2024-06-09 19:17:24 +00:00
Anonymous 09cf502e70 Translated using Weblate (Hebrew)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/he/
2024-06-09 19:17:24 +00:00
Anonymous 78ac7c2a28 Translated using Weblate (Gujarati)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/gu/
2024-06-09 19:17:23 +00:00
Anonymous 57acda5592 Translated using Weblate (French)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/fr/
2024-06-09 19:17:23 +00:00
Anonymous d52a168582 Translated using Weblate (Estonian)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/et/
2024-06-09 19:17:22 +00:00
Anonymous 97a9782f31 Translated using Weblate (Spanish)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/es/
2024-06-09 19:17:22 +00:00
Anonymous 11d8669426 Translated using Weblate (German)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/de/
2024-06-09 19:17:21 +00:00
Anonymous 2bceb6654a Translated using Weblate (Danish)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/da/
2024-06-09 19:17:21 +00:00
Anonymous 6feea6a1b0 Translated using Weblate (Czech)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/cs/
2024-06-09 19:17:20 +00:00
Anonymous 139919ab20 Translated using Weblate (Bengali)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/bn/
2024-06-09 19:17:20 +00:00
Anonymous 234234cc5c Translated using Weblate (Bulgarian)
Currently translated at 100.0% (819 of 819 strings)

Translation: Audiobookshelf-test/ABS Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-web-client/bg/
2024-06-09 19:17:19 +00:00
advplyr fcd74ae17b Merge pull request #2188 from jfrazx/fix/match-authors-429
fix: HTTP/429 when requesting authors information, resolves #1570
2024-06-09 13:56:55 -05:00
advplyr c2897f819d Update:findEpisode API endpoint validate title search param is a string 2024-06-09 13:55:53 -05:00
advplyr a018374d26 Update:Validate ASIN for author, chapter and match requests 2024-06-09 13:43:03 -05:00
advplyr ee501f70ed Auto-formatting 2024-06-09 12:51:28 -05:00
jfrazx e9e9a8ba75 chore: merge and resolve 2024-06-09 09:18:42 -07:00
advplyr 5da4861716 Merge pull request #3040 from BimBimSalaBim/master
E-Reader Font Boldness Slider #3020
2024-06-07 17:05:29 -05:00
advplyr 9c7569fa7a Map localStorage ereaderSettings onto defaults 2024-06-07 16:43:12 -05:00
advplyr c8892c3725 Fix:Truncate author in player #3038 2024-06-06 16:56:57 -05:00
advplyr ef05e37a04 Fix:Casting for podcast episodes #3044 2024-06-05 17:02:03 -05:00
advplyr 065aae9a7e Merge pull request #3043 from dbrain/fix-feedurl-copy-paste
Fix ssrfFilter url
2024-06-05 08:26:12 -05:00
Daniel Brain 06202811b4 Fix ssrfFilter url 2024-06-05 20:32:52 +10:00
mikiher 3ef189ed4a feat: Add a Show Subtitles option 2024-06-04 20:07:36 +03:00
mikiher 5f8066e601 Add default line heights converted to em units to tailwind config 2024-06-04 18:11:56 +03:00
Faizan Zafar ace490712e Merge branch 'master' of https://github.com/BimBimSalaBim/audiobookshelf 2024-06-03 18:47:47 -07:00
Faizan Zafar 265cd75691 Update font settings in EpubReader and Reader components to include a "Font Boldness" slider which applies a stroke to the text #3020 2024-06-03 18:47:29 -07:00
Faizan Zafar f43969e429 Update font settings in EpubReader and Reader components to include a "Font Boldness" slider which applies a stroke to the text 2024-06-03 18:44:41 -07:00
Faizan Zafar 9adfdda7da Update font settings in EpubReader and Reader components to include a "Font Boldness" slider which applies a stroke to the text 2024-06-03 18:37:25 -07:00
Faizan Zafar 0715de8147 Update font settings in EpubReader and Reader components to include a "Font Boldness" slider which applies a stroke to the text 2024-06-03 18:32:10 -07:00
advplyr 9c33446449 Update:Support for ENV variables to disable SSRF request filter (DISABLE_SSRF_REQUEST_FILTER=1) #2549 2024-06-03 17:21:18 -05:00
mikiher 651601adf6 Add podcast to supported shelf types 2024-06-03 21:57:40 +03:00
mikiher 2186603039 Major bookshelf refactor 2024-06-03 09:04:03 +03:00
advplyr 2b5c7fb519 Merge pull request #3035 from Machou/master
fr update
2024-06-01 15:10:10 -05:00
Machou 82dcd2d6fb Update fr.json 2024-06-01 21:11:08 +02:00
Machou 3f2925029c Update fr.json 2024-06-01 19:17:29 +02:00
advplyr 4da4cf2885 Fix:Fluent ffmpeg not detecting formats in ffmpegv7 #3029 2024-06-01 11:19:43 -05:00
advplyr ae412f2a57 Fix:PDF reader flickering & disable request progress indicator for ebook progress update events #2279 2024-05-31 16:32:38 -05:00
advplyr 95506bc638 Update:Add more translation strings, remove unused string #3027 2024-05-30 17:03:33 -05:00
advplyr 4b7b10a901 Add translation string for no results for query 2024-05-30 16:28:46 -05:00
advplyr 800cdc129d Merge pull request #2646 from Teekeks/payer-translation
feat(i18n): Added missing translation string in player UI
2024-05-30 16:24:25 -05:00
advplyr fb86b4fc84 Fix chapter marker string, map translations 2024-05-30 16:23:27 -05:00
advplyr 941f3248d8 Add:SMTP email setting to disable certificate verification #3030 2024-05-29 16:59:43 -05:00
advplyr 6edbab863a Update:nodemailer version bump to 6.9.13 2024-05-29 16:23:47 -05:00
advplyr a9a317a378 Merge pull request #3028 from Machou/master
Update fr.json
2024-05-28 17:24:03 -05:00
advplyr 3fd290c518 Remove unused functions, jsdoc updates, auto-formatting 2024-05-28 17:24:02 -05:00
Machou b0924e4ce8 Update fr.json 2024-05-28 03:55:20 +02:00
Machou 24adc8f66f Update fr.json 2024-05-28 03:27:01 +02:00
advplyr 964ef910b6 Version bump v2.10.1 2024-05-27 16:09:32 -05:00
advplyr ba6a88a5bf Fix:Edit author modal resetting form inputs on image change #2965 2024-05-27 16:04:36 -05:00
advplyr 1576164218 Update:Get all user playlists for library API endpoint performance improvement #2852 2024-05-27 15:37:02 -05:00
advplyr 94400f7794 Merge pull request #3023 from Dalabad/patch-1
Update de.json
2024-05-27 13:39:46 -05:00
advplyr 41e1b02f3a Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-05-27 13:22:29 -05:00
advplyr 1337c60cde Fix:Debian pkg crash due to using toSorted that is only available in Node20+ #3024 2024-05-27 13:22:20 -05:00
Daniel Schosser e9b4e07bd8 Update de.json
Revert Apprise string change
2024-05-27 19:14:10 +02:00
advplyr 607fdffc18 Merge pull request #3022 from JBlond/master
Update de strings.
2024-05-27 11:47:24 -05:00
Daniel Schosser 216139119b Update de.json 2024-05-27 15:02:00 +02:00
JBlond 19cbd1f8de Update de strings.
Follow-up for: ce7f891b9b and 6fad4521d4
2024-05-27 11:35:54 +02:00
advplyr bf893a56c9 Version bump v2.10.0 for client 2024-05-26 17:20:48 -05:00
advplyr 3a2f680a51 Version bump v2.10.0 2024-05-26 17:06:22 -05:00
advplyr ce7f891b9b Update:Disable epubs from running scripts by default, add library setting to enable it GHSA-7j99-76cj-q9pg 2024-05-26 16:01:08 -05:00
advplyr 8ec9da143f Merge pull request #3014 from BrianCArnold/UpdateMatchImportTagsAndNarrators
Change Tags and Narrators to work the same as Genres on the Match page.
2024-05-26 14:39:03 -05:00
advplyr 7f28fbb330 Update:Prevent MultiSelect input from adding items that are whitespace & trim whitespace before adding items 2024-05-26 14:37:07 -05:00
advplyr 3111d1860a Merge pull request #3017 from nichwall/playlist_user_permissions
Users can edit playlist in UI
2024-05-26 14:15:25 -05:00
Nicholas Wallace bd3dce26d9 Playlist row can always be deleted 2024-05-26 17:22:47 +00:00
Nicholas Wallace db9ee301e3 Playlist always shows edit/delete key 2024-05-26 17:22:21 +00:00
Brian C. Arnold 7d8fb3bb10 Change Tags and Narrators to work the same as Genres on the Match Import page. 2024-05-26 08:08:07 -04:00
advplyr 6fa49e0aab Fix:Add timeout to provider matching default to 30s #3000 2024-05-25 16:32:02 -05:00
advplyr 30d3e41542 Merge pull request #3009 from nichwall/timePicker_cleanup
Time picker cleanup
2024-05-25 15:03:18 -05:00
advplyr c58d613949 Update client/components/ui/TimePicker.vue 2024-05-25 14:58:46 -05:00
advplyr 38ba7fbec2 Merge pull request #3010 from nichwall/ereader_settings_update
Ereader settings update
2024-05-25 14:45:43 -05:00
advplyr 6fad4521d4 Map translations 2024-05-25 14:44:46 -05:00
advplyr 2f72300636 Update email page to only load users when needed 2024-05-25 14:44:34 -05:00
Nicholas Wallace b9cb54db71 Onscreen keyboard to appear with TimePicker 2024-05-25 19:27:23 +00:00
Nicholas Wallace aaaa314761 Add: information to whitelist email 2024-05-25 18:19:47 +00:00
Nicholas Wallace 4e40dbc3a5 Add: user column to ereaders 2024-05-25 18:12:18 +00:00
Nicholas Wallace ba6a4f1224 Add: TimePicker focusable by tab 2024-05-24 23:49:07 +00:00
Nicholas Wallace 524ed9b677 Tab removes focus from TimePicker 2024-05-24 23:47:38 +00:00
advplyr 5bbcb9cac3 Fix:Embedded chapters sort order #3007 2024-05-24 16:49:39 -05:00
advplyr ff169f3fd0 Merge pull request #3002 from diamondtipdr/patch-1
Update es.json
2024-05-24 11:22:43 -05:00
DiamondtipDR cf7b08c993 Update es.json
Updating spanish translation
2024-05-23 23:44:08 -04:00
advplyr d99a77837b Merge pull request #2920 from rasmuslos/master
Add item sessions endpoint
2024-05-23 16:39:40 -05:00
advplyr 23dcf684d9 Item listening sessions endpoint returns 404 on not found media item 2024-05-23 16:35:36 -05:00
advplyr 9c2ed279df Fix mediaId reference, add JS docs, autoformatting 2024-05-23 16:32:34 -05:00
advplyr 700d7fe68e Fix:Ebook item context menu position on mouseover #2980 2024-05-22 16:51:11 -05:00
advplyr 69833db819 Add:Env variable setting to allow CORS 2024-05-19 14:40:46 -05:00
advplyr ab2026ecea Merge pull request #2988 from Machou/patch-1
Update fr.json
2024-05-18 17:49:04 -05:00
Machou 811fd9018a Update fr.json 2024-05-18 22:15:46 +02:00
advplyr 6d89721371 Fix:Podcast download new episode check to compare both GUID and enclosure URL for existing episodes #2986 2024-05-18 09:33:48 -05:00
advplyr ab3a137db9 Merge pull request #2982 from cor-bee/patch-1
Update uk.json
2024-05-17 16:50:32 -05:00
advplyr a11cf7a90e Fix:Book library author name sort order with multi-author books #2859 2024-05-16 14:56:19 -05:00
advplyr c995816076 Merge pull request #2981 from pmangro/PT-BR]-Updated-strings
[Pt-BR] updated strings
2024-05-16 14:53:33 -05:00
pmangro 94e7fc6434 [PT-BR] String fix 2024-05-16 16:43:36 -03:00
pmangro 3916bfe833 [PT-BR] Updated strings 2024-05-16 16:28:20 -03:00
Illia Pyshniak 3080ada35f Update uk.json 2024-05-16 18:31:48 +03:00
advplyr 4cddc597c1 Fix:Book library collapse series with no-series filter #2976 2024-05-14 17:24:39 -05:00
advplyr ec07bfa940 Merge pull request #2974 from JBlond/master
Update de.json
2024-05-14 08:09:54 -05:00
JBlond d20d4bf8c1 Update de.json 2024-05-14 14:21:25 +02:00
Rasmus Krämer 09e26a9e56 Use new database models, fix function name and use optional path parameter 2024-05-14 10:51:50 +02:00
Rasmus Krämer ef74919f12 Merge branch 'advplyr:master' into master 2024-05-14 10:40:21 +02:00
advplyr 6462a50713 Add more translation strings 2024-05-13 17:25:01 -05:00
advplyr 8c6c43657c Add translation strings for toasts, update data load toasts to use generic failed to load data message 2024-05-13 16:58:41 -05:00
advplyr b8ed56e91e Update:Using translations for scan buttons shown on empty library pages & add loading indicator 2024-05-13 16:31:30 -05:00
advplyr dc0eaa32c9 Merge pull request #2954 from mikiher/series-progress-fixes
Fix series and collapsed series progress to be consistent and show average of book series progress
2024-05-12 13:37:10 -05:00
advplyr 60fc4e20e6 Cleanup inconsistencies with ExplicitIndicator component by removing prop 2024-05-12 13:35:03 -05:00
advplyr 6f43b32214 Merge pull request #2966 from lembata/master
Bulgarian Translation
2024-05-11 18:11:13 -05:00
advplyr 5e8ae79d71 Map translations 2024-05-11 18:09:51 -05:00
noiro 34718aa95d Update pl.json 2024-05-11 21:36:40 +02:00
Alexander Lemberg d731ad1bd7 Removed duplicated key and trailing comma 2024-05-11 18:57:54 +03:00
Alexander Lemberg e7fa698645 Add bg to language list 2024-05-11 16:44:42 +03:00
Alexander Lemberg 851d298916 Update bg.json 2024-05-11 16:19:18 +03:00
Alexander Lemberg 1a27e2bef7 Added Bulgarian Translation 2024-05-11 16:06:19 +03:00
advplyr d64860001b Update devcontainer dev.js file to skip binaries check 2024-05-10 17:58:01 -05:00
advplyr b82ac3d536 Update:Uploader shows item title on success/failure message #2958 2024-05-10 17:32:57 -05:00
advplyr 91be9eb0fc Merge pull request #2963 from JBlond/master
Update de strings
2024-05-10 17:00:10 -05:00
JBlond d61bb0bea0 Update de strings 2024-05-10 17:11:49 +02:00
mikiher 911d72971e Removed incorrect stylesheet reference 2024-05-10 12:44:58 +03:00
mikiher b244cc8d41 Add LazyBookCard tests 2024-05-10 12:43:33 +03:00
mikiher 8cc3bfa95e Add test identifiers to LazyBookCard 2024-05-10 12:39:53 +03:00
advplyr ba3d59c645 Merge pull request #2955 from nichwall/feature_request
Add: new feature request form
2024-05-09 20:05:00 -05:00
Nicholas Wallace e416958b01 Add: new feature request form 2024-05-09 06:10:52 +00:00
mikiher 05c1ced65c Update LazyBookCard progress calculation to handle finished items 2024-05-09 07:42:57 +03:00
mikiher 057bc1a0c0 Fix series progress to show sum of series book progresses 2024-05-09 07:31:00 +03:00
advplyr 32fc224600 Merge pull request #2933 from nichwall/logs_doc_update
Add: logs documentation
2024-05-08 16:32:55 -05:00
advplyr fcecd415c8 Map translation strings 2024-05-08 16:33:58 -05:00
mikiher e384527b67 Simplify progress bar and show correct collapsed series progress 2024-05-08 08:44:55 +03:00
advplyr 672672dd2a JSDoc formatting updates 2024-05-07 17:39:10 -05:00
advplyr fd22a6f51d Merge pull request #2896 from CoffeeKnyte/master
Split the author call in the library stats page to 2 lighter functions
2024-05-07 17:36:57 -05:00
mikiher c674042319 Add libraryItemIds to collapsedSeries objects 2024-05-07 18:19:55 +03:00
mikiher a668921e29 Prettier-only formatting changes 2024-05-07 18:16:32 +03:00
advplyr 04ed4810fd Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-05-06 17:17:41 -05:00
advplyr 941c798d78 Fix:Update author updatedAt when downloading new image, fixes author image refresh #2934 2024-05-06 17:17:35 -05:00
Nicholas Wallace 7f12c71eca Add: logs documentation 2024-05-06 01:47:57 +00:00
advplyr f62d10746d Merge pull request #2930 from nichwall/book_matching_update
Book match tab update
2024-05-05 17:03:42 -05:00
advplyr 13afa12456 Map Select All translations 2024-05-05 17:04:41 -05:00
advplyr 4e1406f612 Merge pull request #2929 from nichwall/email_guide_link
Add: link to guide for email settings
2024-05-05 16:55:48 -05:00
advplyr ce98bcc989 Merge pull request #2927 from nichwall/issue_templates
Issue Bug template updates
2024-05-05 16:54:17 -05:00
advplyr ff5cbae059 Update .github/ISSUE_TEMPLATE/bug.yaml 2024-05-05 16:51:55 -05:00
advplyr 04a7f24bac Update .github/ISSUE_TEMPLATE/bug.yaml 2024-05-05 16:51:17 -05:00
advplyr 68bfcb2e6e Update .github/ISSUE_TEMPLATE/bug.yaml 2024-05-05 16:51:12 -05:00
advplyr 4bd7e21a51 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-05-05 16:39:54 -05:00
advplyr 37932f664a Auto formatting for Server.js 2024-05-05 16:39:38 -05:00
Nicholas Wallace 0081525ed3 Add: space between covers on match tab 2024-05-05 17:31:12 +00:00
Nicholas Wallace 7e13cb6ecf Add: Select All for match tab 2024-05-05 17:27:45 +00:00
Nicholas Wallace 721dd14c1f Add: link to guide for email settings 2024-05-05 17:07:35 +00:00
Nicholas Wallace 047c8ec017 Formatting updates 2024-05-05 16:44:00 +00:00
Nicholas Wallace fa5d2b2020 Fix: label tabbing 2024-05-05 16:37:48 +00:00
Nicholas Wallace dfe6505af0 Fix: label placement 2024-05-05 16:37:11 +00:00
Nicholas Wallace b0e33970b8 Add more fields to bug report template 2024-05-05 16:35:26 +00:00
Rasmus Krämer d9f828c717 Added item sessions endpoint 2024-05-05 13:14:30 +02:00
advplyr 15ca3307bd Merge pull request #2916 from Myticktack/patch-1
Update de.json, add translation for #2914
2024-05-04 12:21:33 -05:00
Daniel Drews fa3b7e2f60 Update de.json, add translation for #2914
Add Translation for "Read more" & "Read less" added by Issue #2914
2024-05-04 17:27:38 +02:00
advplyr a6de76a983 Update:Close edit modal when pressing chapter edit button and already on chapter page #2915 2024-05-03 17:25:30 -05:00
advplyr 724e06e9d2 Update:i18n translation strings for Read more/less #2914 2024-05-03 17:12:49 -05:00
advplyr bf3db1dae0 Fix:Fullscreen cover image modal not updating when changing covers #2900 2024-05-02 17:48:50 -05:00
advplyr 410801347c Fix:Switching library on series item page not redirecting #2902 2024-05-01 17:23:49 -05:00
CoffeeKnyte 5041f80cb0 Added limit variable to getAuthorsWithCount()
- Clarified and updated the comments
- added parameter "limit" to getAuthorsWithCount()
- the limit is set to 10 when called from LibraryController.js
- as per Nichwall's comments
2024-05-01 07:24:42 -04:00
CoffeeKnyte 7229cfce84 Added limit 10 to getAuthorsWithCount() call
As per nichwall's request
2024-05-01 07:20:48 -04:00
advplyr cb1ebd4a17 Update editor config formatting options
Co-authored-by: Arran Hobson Sayers <ahobsonsayers@gmail.com>
2024-04-30 17:45:55 -05:00
advplyr 7929f3dc42 Merge pull request #2853 from mikiher/nuxt-unit-tests
Add client component testing framework and tests
2024-04-30 17:32:24 -05:00
CoffeeKnyte 95cdb23efb split getAuthorsWithCount to 2 lighter functions
getAuthorsWithCount - now only gets the top 10 authors (in that library) by number of books
getAuthorsTotalCount - new function to only get total number of authors (in that library)
2024-04-30 11:14:55 -04:00
CoffeeKnyte 182527bfa8 Update LibraryController.js
used a lighter function to find total author count
2024-04-30 11:09:06 -04:00
mikiher 2eb19d46d5 Move test files to a separate directory 2024-04-30 11:30:00 +03:00
advplyr 10e7f142ec Update:Cover resolution to use unicode multiplication sign instead of x #2888 2024-04-29 17:22:14 -05:00
advplyr c55988102d Merge pull request #2891 from ahobsonsayers/master
Tweaks to custom metadata provider schema
2024-04-29 17:13:04 -05:00
Arran Hobson Sayers d488b17869 Update custom metadata provider schema 2024-04-29 22:50:42 +01:00
advplyr ff27c0b58b Auto formatting 2024-04-29 16:30:30 -05:00
mikiher 2bd532eb9a Put book_placholder.jpg in browser cache 2024-04-29 11:16:49 +03:00
mikiher e5fe31fe26 replace id attribute (which has to be unique across a document) with cy-id (which doesn't) 2024-04-29 08:30:14 +03:00
mikiher ec83eb0a27 Add support for cy.get("&id") (translates to [cy-id="id"]) 2024-04-29 08:03:10 +03:00
mikiher 6236f53b4f Change a couple of element ids to camelCase 2024-04-29 07:59:38 +03:00
advplyr 1b2cf50633 Fix:Catch error with transcodes writing concat file & do not fallback to AAC encode if error message is a failure to find include file 2024-04-27 16:41:57 -05:00
advplyr 3ab638ed61 Fix:Trim whitespace from username when creating new, remove trim from password to allow whitespace #2882 2024-04-26 17:07:19 -05:00
advplyr bd1309b680 Fix:nodemailer transport object only use secure: true when port is 465 #2765 2024-04-25 18:04:02 -05:00
advplyr 00bc50c02d Merge pull request #2877 from v3DJG6GL/v3DJG6GL-itunes-podcast-regions
add iTunes podcast regions for all ABS supported languages
2024-04-24 17:36:21 -05:00
advplyr e8bb92826a UI/UX update podcast search region dropdown max width and height 2024-04-24 17:37:04 -05:00
advplyr a0cc42b385 Fix:UI/UX: Users table show red rows for disabled accounts #2876 2024-04-24 10:01:32 -05:00
v3DJG6GL 7edc7ce861 remove-region-bangladesh 2024-04-24 16:13:19 +02:00
v3DJG6GL 0302ed986e fix sorting 2024-04-24 13:57:51 +02:00
v3DJG6GL babfb6978a add-itunes-podcast-regions
This PR adds iTunes podcast regions for all languages that ABS currently supports.
2024-04-24 13:51:38 +02:00
advplyr 2cb53fafd7 Fix:Audio player cover art aspect ratio changes with library #2870 2024-04-23 17:12:13 -05:00
mikiher 8dbe35e5aa Use absolute positioning for the card element 2024-04-23 19:14:47 +03:00
advplyr bd06b6c716 Update:Decrease breakpoint for hiding volume button on audio player #2868 2024-04-22 17:53:29 -05:00
advplyr 8b27c726d5 Version bump 2.9.0 2024-04-21 16:45:13 -05:00
advplyr 68418c1d3b Merge pull request #2820 from apocer/openid_signing_algorithm
Add option to set Signing Algorithm for OpenID Authentification
2024-04-21 16:07:30 -05:00
advplyr a8af6db3d6 Format update of authentication page for supported algorithms 2024-04-21 16:05:41 -05:00
advplyr af856ce1ec Merge branch 'master' into openid_signing_algorithm 2024-04-21 15:38:33 -05:00
advplyr aae8e7535a Fix:Home page always showing horizontal scrollbar 2024-04-21 15:36:01 -05:00
advplyr 359a2752d8 Fix:Server crash when scanning in invalid epub #2856 2024-04-21 15:07:53 -05:00
advplyr 9102a0045f Merge pull request #2803 from nichwall/vacuum_bundling
OpenAPI Spec, try 2
2024-04-20 14:57:32 -05:00
advplyr b124d61826 Update yaml docs to include BearerAuth 2024-04-20 14:57:38 -05:00
advplyr 8e6ead59ce Update yaml keys to camelCase 2024-04-20 14:55:57 -05:00
advplyr f74d741821 Fix:Server crash when updating media with external cover url that fails to download #2857 2024-04-20 11:34:21 -05:00
mikiher 0498d8cb83 Get book placeholder image from fixture rather than from server 2024-04-19 09:49:19 +03:00
advplyr 15f83986e7 Update library stats previewicons padding 2024-04-18 17:45:47 -05:00
advplyr a57fe42dff Update:Library stats to format numbers using selected language #2861, clean up UI for library stats preview icons 2024-04-18 17:30:06 -05:00
advplyr b03198abd9 Add comments/jsdocs to i18n.js 2024-04-18 17:06:12 -05:00
advplyr ad30977781 Fix:Custom metadata provider including extra curly bracket in query string #2860 2024-04-18 16:16:59 -05:00
mikiher 129da51f76 Add tailwind.compiled.css to .gitignore 2024-04-18 07:47:10 +03:00
advplyr dbe10382fd Update:Podcast episode downloader only takes audio streams #2858 2024-04-17 17:09:36 -05:00
mikiher e5bababeae Add tests for LazySeriesCard.vue 2024-04-17 23:27:31 +03:00
mikiher 9b332f0e66 make $constants, $strings, and utility functions avaiable to Cypress mounted componenets 2024-04-17 23:25:44 +03:00
mikiher a49c5afa46 Fix a couple of stub assertions in AuthorCard.cy.js 2024-04-17 23:22:21 +03:00
advplyr f0caf1a933 Update:Book matches support lowercase letters in audible ASIN #2849 2024-04-16 16:39:57 -05:00
mikiher 9e1c907591 Add NarratorCard and AuthorCard component tests 2024-04-16 00:00:35 +03:00
mikiher d638a328d8 Add cypress npm scripts 2024-04-15 23:58:13 +03:00
mikiher f597798839 Add Cypress config and support files 2024-04-15 23:57:21 +03:00
mikiher 303ef6b7c5 Add Cypress to dev dependencies 2024-04-15 23:54:56 +03:00
advplyr 0f7c99d989 Fix:Retry transcode forcing AAC to handle the bad audible m4bs #2720 2024-04-15 15:14:30 -05:00
advplyr 60c65008dc Fix:Match all books only matching first 100 #2096 2024-04-14 17:19:21 -05:00
advplyr c4fd4ff9de Fix:Update metadata.json when using item metadata utils #2837 2024-04-12 17:34:10 -05:00
advplyr 29fc503503 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-04-11 17:29:31 -05:00
advplyr bca49616e1 Update:Podcast episode audio file ID3 tags use comment and description tag for description instead of subtitle #2843 2024-04-11 17:29:23 -05:00
advplyr cb49c17fc5 Merge pull request #2841 from nichwall/i18n-integration-fix
I18n integration fix
2024-04-11 17:17:13 -05:00
Nicholas Wallace 9e1686232b Example: es.json is fixed 2024-04-11 01:34:32 +00:00
Nicholas Wallace f702358bbd Example: missing key in es.json 2024-04-11 01:33:38 +00:00
Nicholas Wallace 9a0b8de354 Example: bad key in es.json 2024-04-11 01:31:22 +00:00
Nicholas Wallace 6ed6fff6bd Update i18n workflow to 1.2.0 2024-04-11 01:29:00 +00:00
Nicholas Wallace 75007bb371 Fix: i18n-integration not running on PRs 2024-04-11 01:28:40 +00:00
advplyr df9da095ef Map i18n strings to uk.json 2024-04-10 17:27:38 -05:00
advplyr 64c98722c3 Merge pull request #2840 from soaibsafi/master
Add Bengali translation
2024-04-10 17:25:59 -05:00
advplyr 36c1a8b2df Fix bn i18n string keys 2024-04-10 17:23:12 -05:00
soaibsafi 710d6af4b3 Adds Bengali translation 2024-04-10 19:46:39 +02:00
advplyr cd7ecb9933 Update:User permission tags accessible to user are alphabetized #2667 2024-04-09 17:54:09 -05:00
apocer f75f0b8cc8 show dropdown if issuer has list of algorithms 2024-04-09 22:29:06 +02:00
advplyr e60d2a9858 Add:Podcast library filter for languages and show language on podcast item page 2024-04-08 15:48:41 -05:00
advplyr 04993dd63d Update:Show language on book item page w/ link to filter #2834 2024-04-08 15:38:34 -05:00
advplyr 41af913280 Update:Edit item cover tab UI for small screen sizes #2832 2024-04-07 16:24:23 -05:00
advplyr 8dc0f2c67c Fix:Duplicate keys error when the same library item is shown twice in continue series 2024-04-06 17:48:40 -05:00
advplyr fc196180b3 Merge pull request #2805 from rasmuslos/master
Add client name to possible device info lines
2024-04-05 16:50:26 -05:00
advplyr 4a127d35b9 Update:Add client name and version to sessions table and session modal 2024-04-05 16:50:15 -05:00
advplyr 1525fdf4f6 Merge pull request #2821 from lkiesow/series-separator
Separator between multiple series
2024-04-04 17:55:53 -05:00
advplyr 8a29c998da Update item page series comma separated list to not include comma in link 2024-04-04 17:54:43 -05:00
Lars Kiesow f56d9f128f Separator between multiple series
If a book is part of multiple series, this patch adds a separator
between the series on the library item details page. With no separator,
it is not immediately clear that they are separate series.
2024-04-04 21:55:52 +02:00
advplyr c5785e9c20 Update:Increase breakpoint for player to change buttons to two lines #2799 2024-04-03 18:41:41 -05:00
advplyr 0ca91ecfff Merge pull request #2817 from springsunx/patch-2
Update zh-cn.json
2024-04-03 18:15:29 -05:00
basti 304d0f6d43 id_token_signed_respo... should be in new Client 2024-04-03 22:52:49 +02:00
basti 6c9a811472 Add ui and settings for OpenID Signing Algorithm 2024-04-03 16:18:13 +02:00
SunX 116a7fb994 Update zh-cn.json 2024-04-03 09:55:33 +08:00
advplyr 8e46181ba0 Update:Adding tooltips to player controls forward/backward and next/prev #2800 2024-04-02 18:05:44 -05:00
advplyr a336686e42 Merge pull request #2802 from pmangro/master
[PT-BR] OpenID permission strings
2024-04-01 17:14:16 -05:00
Rasmus Krämer c8957fe373 Add client name to possible device info lines 2024-04-01 16:20:09 +02:00
Nicholas Wallace ca7eaf9750 OpenAPI spec readme 2024-04-01 00:44:51 +00:00
Nicholas Wallace 74dd24febf Bundled spec 2024-04-01 00:26:55 +00:00
Nicholas Wallace 7b856474af Rename base document 2024-03-31 22:48:58 +00:00
Nicholas Wallace c7ac12a67a Split schema to sub files 2024-03-31 22:47:14 +00:00
pmangro 3264359771 [PT-BR] OpenID permission strings 2024-03-31 19:44:53 -03:00
advplyr c7cc994532 Fix:Handle enabling/disabling library watchers #2775 2024-03-31 14:57:55 -05:00
Nicholas Wallace afe40be957 Initial large file 2024-03-30 23:47:13 +00:00
advplyr a9c9c447f1 Merge pull request #2769 from Sapd/openid-permissions
OpenID: Integrate permissions (Fixes #2523)
2024-03-30 14:38:32 -05:00
advplyr aa1aeacc09 Map new translation strings 2024-03-30 14:26:55 -05:00
advplyr fc595bd799 Updates to authentication page for mobile screen sizes 2024-03-30 14:25:38 -05:00
advplyr a5d7a81519 Clean up formatting of advanced group/permission claims on authentication page 2024-03-30 14:17:34 -05:00
advplyr 7e8fd91fc5 Update OIDC advanced permissions check to only perform an update on changes
- Update permissions example to use UUIDv4 strings for allowedLibraries
- More validation on advanced permission JSON to ensure arrays are array of strings
- Only set allowedTags and allowedLibraries if the corresponding access all permission is false
2024-03-30 14:04:02 -05:00
advplyr c2ed0b7d3d Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-03-30 11:40:43 -05:00
advplyr aefda8bd51 Fix:Local sessions set date and dayOfWeek using the updatedAt timestamp passed in from the client #2795 2024-03-30 11:40:35 -05:00
advplyr 93bec282d2 Merge pull request #1888 from jorgectf/jorgectf/add-codeql-workflow
Add CodeQL workflow
2024-03-29 16:47:07 -05:00
advplyr 1396a432a4 Merge pull request #2797 from mikiher/rtl-fixes
Add dir="auto" attribute where RTL display is needed
2024-03-29 16:08:50 -05:00
Denis Arnst 90e1283058 OpenID: Allow email_verified null and also check username
Only disallow when email_verified explicitly false
Also check username besides preferred_username, even when its not included in OIDC checks (synology uses username)
2024-03-29 15:11:56 +01:00
Denis Arnst 8cd50d5684 OpenID: Don't downgrade root 2024-03-29 14:51:34 +01:00
advplyr 50bd2648aa Fix:Server crash on matching book with an author name ending in comma #2796 2024-03-28 17:00:07 -05:00
mikiher 33254654d5 Add dir="auto" attribute where it makes sense 2024-03-28 23:56:59 +02:00
Denis Arnst 617b8f4487 OpenID: Rename tags switch 2024-03-28 16:16:26 +01:00
advplyr f9b95bb003 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-03-27 16:11:57 -05:00
advplyr 740640884f Update:Support for comic files with webp images #2792 2024-03-27 16:11:47 -05:00
advplyr 86fea5c667 Merge pull request #2791 from mikiher/abs-windows-dispatch
Workflow to dispatch an ABS windows event on server release
2024-03-27 15:37:52 -05:00
mikiher 33e4b51aee Revert "add dummy pull_request event for the workflow to appear in the list"
This reverts commit 1cf0bd0f01.
2024-03-27 13:38:17 +02:00
mikiher 1cf0bd0f01 add dummy pull_request event for the workflow to appear in the list 2024-03-27 13:30:00 +02:00
mikiher 8ce5a5cdbd Add workflow to dispatch an abs-windows event 2024-03-27 13:18:02 +02:00
advplyr fc26b7af0a Merge pull request #2789 from justcallmelarry/bugfix/corretly-working-limit-for-continue-series-toggle
Fix book limit for the Continue Series shelf (with skip earlier books toggle active)
2024-03-25 15:45:45 -05:00
Lauri Vuorela 2d68fa2c27 fix book limit for the contiue series shelf 2024-03-25 16:32:29 +01:00
advplyr f241cb2280 Merge pull request #2787 from nichwall/translation_faq_link
Translation guide link added to readme
2024-03-24 05:07:08 -05:00
Nicholas Wallace 125346bb5c Translation guide link added to readme 2024-03-24 03:11:00 +00:00
advplyr b60f62cebf Merge pull request #2784 from rasmuslos/patch-1
Fix custom metadata provider crash
2024-03-23 15:27:51 -05:00
advplyr 51ff62356d Merge pull request #2773 from mikiher/fix-library-files-inconsistencies
Fix library files inconsistencies
2024-03-23 15:25:33 -05:00
advplyr f827aa97f8 Update library scanner findLibraryItemByItemToFileInoMatch query to iterate through json objects comparing inodes 2024-03-23 14:56:32 -05:00
mikiher 68276fe30b Fix handling of file moves from root folder to sub folder and back 2024-03-23 18:31:52 +02:00
Rasmus Krämer 961533765f Fix custom metadata provider crash 2024-03-23 14:54:34 +01:00
advplyr c1bbec22f0 Merge pull request #2780 from nichwall/i18n_integration_check
Add i18n integration workflow
2024-03-22 15:21:14 -05:00
Nicholas Wallace 7d0eb215d6 Add integration workflow 2024-03-22 01:28:50 +00:00
advplyr ff5226fa93 Update:Remove unused missing/invalid audiobook parts logic and keys 2024-03-21 14:38:52 -05:00
advplyr 8d7530254c Update:Re-order chapters table infront of audio tracks table on book item page #2778 2024-03-21 14:23:49 -05:00
advplyr 6957b4baf6 Merge pull request #2777 from burghy86/patch-14
Update it.json
2024-03-21 08:41:34 -05:00
burghy86 01c8d42291 Update it.json
month fix string
2024-03-21 12:52:49 +01:00
advplyr 1e21847852 Merge pull request #2772 from cor-bee/master
Add Ukrainian Translation and Podcast Region
2024-03-20 16:39:30 -05:00
mikiher 1bee082720 Update libraryFolderID correctly in scanFolderUpdates 2024-03-20 11:40:50 +02:00
Illia Pyshniak b0a9bed15a Update i18n.js
Add Ukrainian language and podcast region
2024-03-19 23:18:34 +02:00
Illia Pyshniak 1d7434cbbb Create uk.json 2024-03-19 23:12:29 +02:00
Denis Arnst 1646f0ebc2 OpenID: Ignore admin for advanced permissions
Also removed some semicolons
2024-03-19 19:35:34 +01:00
Denis Arnst 50330b0a60 Auth: Add translations 2024-03-19 19:18:47 +01:00
Denis Arnst f661e0835c Auth: Simplify Code 2024-03-19 19:18:38 +01:00
mikiher 9511122bae Fix LibraryItem and Media file update logic for library scans 2024-03-19 19:28:26 +02:00
Denis Arnst 56f1bfef50 Auth/OpenID: Implement Permissions via OpenID
* Ability to set group
* Ability to set more advanced permissions
* Modified TextInputWithLabel to provide an ability to specify a different placeholder then the name
2024-03-19 17:57:24 +01:00
advplyr 8e5b7504ae Merge pull request #2760 from pmangro/Continue-series
[PT-BR] Continue Series
2024-03-17 19:22:58 -05:00
advplyr 0a0006f949 Merge pull request #2756 from arcmagedr/master
Add Hebrew translation json and Hebrew to i18n.js
2024-03-17 19:22:21 -05:00
advplyr 5b836dfa28 Remove duplicate he language code 2024-03-17 19:19:52 -05:00
advplyr 8396900178 Merge pull request #2757 from mikiher/fix-unit-tests-flow
Change unit tests workflow to include conditional checkout steps
2024-03-17 19:14:41 -05:00
pmangro 8f80948211 [PT-BR] Continue Series 2024-03-17 19:09:16 -03:00
arcmagedr 4ad09ec3d8 Merge branch 'advplyr:master' into master 2024-03-17 22:26:49 +02:00
dor be4eb28b21 finished proofing Hebrew translation 2024-03-17 22:25:49 +02:00
mikiher f938fca2c7 Fix bug in workflow_dispatch checkout step 2024-03-17 07:57:28 +02:00
mikiher d562f6a69f Change unit-tests.yml workflow to include conditional checkout step 2024-03-17 07:36:13 +02:00
advplyr 166454ef43 Version bump v2.8.1 2024-03-16 17:15:33 -05:00
advplyr d5c854d606 Update:Add robots.txt and noindex meta tag 2024-03-16 16:35:05 -05:00
advplyr eace46bf55 Merge pull request #2688 from mfcar/mf/loginPage
Update Login Page with Logo and Input Form Styling
2024-03-16 15:57:45 -05:00
advplyr b9ffce166e Login page add overflow scroll for mobile landscape, update z index for logo 2024-03-16 15:55:13 -05:00
advplyr 9713e94aed Reformat login page with logo in top left 2024-03-16 15:41:35 -05:00
advplyr d71bc89c9d Merge branch 'master' into mf/loginPage 2024-03-16 15:24:22 -05:00
advplyr a2b2a2d060 Fix:Applying backup not properly overwriting existing sqlite file
- Fixed resetting api cache on backup
- Added loading indicator in backups table
- Fixed apply backup api not responding with 200 http status code
- Added additional logging and failsafes
2024-03-16 15:12:33 -05:00
dor 752268effb mid point proofing 2024-03-16 21:00:44 +02:00
dor 9e3b3f3e12 add Hebrew translation json and Hebrew to i18n.js 2024-03-16 19:26:22 +02:00
advplyr 88f9533b37 Fix:HLS.js retry fragments #2748 #2720 2024-03-15 17:10:43 -05:00
advplyr 630ece82ad Fix:Chapter modal scroll to current chapter 2024-03-15 14:35:09 -05:00
advplyr 5777184cae Merge pull request #2745 from mikiher/unit-tests-flow
Unit tests flow
2024-03-15 14:20:57 -05:00
mfcar a76da14fb0 Merge branch 'refs/heads/master' into mf/loginPage 2024-03-15 08:14:45 +00:00
mikiher 0c612b4836 Update unit test workflow to include push event 2024-03-15 09:51:40 +02:00
mikiher a1af672c7c Add unit test workflow 2024-03-15 08:50:51 +02:00
advplyr 5fcd23409a Update:dev.js in devcontainer to include the SkipBinariesCheck flag #2741 2024-03-14 16:32:23 -05:00
advplyr 99f0799a11 Update:Adding support for skipping check for ffmpeg/ffprobe binaries with environment variable SKIP_BINARIES_CHECK
- Set SKIP_BINARIES_CHECK=1 env variable to skip
- Or set SkipBinariesCheck: true in dev.js #2741
2024-03-14 16:29:01 -05:00
advplyr 316aeba1b0 Merge pull request #2740 from Schiriki123/master
Add name labels to login form
2024-03-14 15:40:45 -05:00
advplyr bfd4a378f3 Merge pull request #2737 from justcallmelarry/feature/add-toggle-for-skipping-earlier-instalments-in-continue-series
Add library toggle setting for skipping earlier instalments in Continue Series
2024-03-14 14:40:20 -05:00
advplyr 521db90ae0 Update JSDocs, remove unused libraryId replacement 2024-03-14 14:37:24 -05:00
advplyr d02fc2debe Update continue series skip earlier books query attribute to look for finished books, update wording on help text, map translations 2024-03-14 14:27:33 -05:00
advplyr e6c21c5be1 Merge pull request #2742 from mikiher/broken-binary-manager-test
Fix broken BinaryManager.isBinaryGood test
2024-03-14 13:13:59 -05:00
advplyr 91248b496e Merge pull request #2734 from mikiher/fix-sequence-cleanup
Make series sequence cleanup slighlty less aggressive
2024-03-14 13:12:04 -05:00
mikiher f7ae7783bd Fix broken BinaryManager.isBinaryGood test 2024-03-14 19:58:42 +02:00
mikiher ae395497a5 Add tests for cleanSeriesSequence 2024-03-14 19:37:51 +02:00
mikiher 8826d3af62 fix cleanSeriesSequence method to extract first numeric value 2024-03-14 19:36:51 +02:00
Lauri Vuorela 65153fae9d var => let 2024-03-14 09:42:50 +01:00
Lauri Vuorela d4c1bc5dfc use already fetched library settings, only fetch maxSequence if setting is turned on 2024-03-14 09:41:48 +01:00
Schiriki d6f13513ae Add name labels to login form 2024-03-13 23:46:56 +01:00
advplyr 2584c3b432 Merge pull request #2733 from kaldigo/master
Added isbn to CustomProviderAdapter
2024-03-13 17:21:51 -05:00
advplyr b54421412d Merge pull request #2738 from Sapd/auth-fix
Auth: Fix crash on missing logout URL
2024-03-13 17:18:35 -05:00
advplyr e2451a3281 Merge pull request #2732 from den13501/i18n-add-zhTW
Add traditional Chinese(zh-TW) to i18n
2024-03-12 17:47:04 -05:00
advplyr dbf4bd5c3d Merge pull request #2691 from lkiesow/hash-in-filename
Fix file names with URL control characters
2024-03-12 17:40:37 -05:00
Denis Arnst 2a722ab163 Auth: Fix crash on missing logout URL
When using OpenID
Also added debug information on openid errors
2024-03-12 18:07:13 +01:00
Lauri Vuorela c83399c7b5 use the toggle to not show earlier works than the ones already read 2024-03-12 17:04:26 +01:00
Lauri Vuorela a814e45150 add a toggle for the new continue series setting 2024-03-12 17:00:21 +01:00
mikiher 29e9216bb1 Make series sequence cleanup slighlty less aggressive 2024-03-12 13:17:52 +02:00
Kaldigo 94d1732b0d Added isbn to CustomProviderAdapter 2024-03-12 08:18:52 +00:00
-Shiken- 7610084627 Update zh-tw.json
fix
2024-03-12 15:39:01 +08:00
-Shiken- d840905a97 Create zh-tw.json
add traditional Chinese translation text.
2024-03-12 15:36:42 +08:00
-Shiken- 7b1b448795 Add traditional Chinese translation
for traditional user.
2024-03-12 11:29:24 +08:00
advplyr 77559d29bb Merge pull request #2724 from mikiher/fix-library-filter-data-access
Fix library filter data direct access
2024-03-11 17:08:41 -05:00
advplyr c14f9accaf Update functions for #2724 and add jsdocs 2024-03-11 17:07:03 -05:00
advplyr 76a1f48c62 Remove UID/GID from Server constructor 2024-03-11 11:11:13 -05:00
mikiher ae0a9bcf86 Merge branch 'advplyr:master' into fix-library-filter-data-access 2024-03-11 08:33:47 +02:00
advplyr 9e44fe5524 Merge pull request #2721 from mikiher/keyboard-navigation-2
Add keyboard navigation to multi-select components
2024-03-10 09:45:16 -05:00
advplyr 727dad7e19 Update multi select highlight color to yellow, remove console logs 2024-03-10 09:43:24 -05:00
advplyr 0c2de91097 Merge pull request #2726 from nichwall/vietnamese_translations
Vietnamese translations
2024-03-09 20:09:09 -06:00
advplyr 450fa45360 Update Vietnamese datefns locale code 2024-03-09 20:09:08 -06:00
Nicholas Wallace e0dddae2c2 Added missing keys 2024-03-09 23:13:20 +00:00
Nicholas Wallace daa9fccc14 Add: Vietnamese translations 2024-03-09 23:00:01 +00:00
mikiher ad45dadc15 Remove redundant space 2024-03-09 12:07:08 +02:00
mikiher 0e8148001e Fix direct access to Database.libraryFilterData 2024-03-09 11:59:50 +02:00
advplyr fa71f9db2e Merge master 2024-03-08 12:22:29 -06:00
advplyr 0d9d2fa4be Merge pull request #2714 from mikiher/keyboard-navigation
Fix input width in MultiSelect UI components  - replacement solution
2024-03-08 12:20:46 -06:00
advplyr c34e9cde05 Merge branch 'master' into keyboard-navigation 2024-03-08 12:15:07 -06:00
advplyr b934a755b5 Merge branch 'master' into keyboard-navigation-2 2024-03-08 12:04:13 -06:00
mikiher a5772f6b66 Add keyboard navigation to multi-select components 2024-03-08 08:51:05 +02:00
advplyr 153f149d58 Merge pull request #2580 from KeyboardHammer/authorSort
Add sorting to author page
2024-03-07 12:28:55 -06:00
advplyr e50b06183e Merge branch 'master' into authorSort 2024-03-07 12:26:07 -06:00
advplyr 305689d513 Update authors sort 2024-03-07 12:26:04 -06:00
advplyr 4dd140585d Add:Abridged checkbox to batch edit overwrite map details #2695 2024-03-06 15:29:10 -06:00
mikiher cd60d0219f Bring back setInputWidth 2024-03-06 14:02:15 +02:00
mikiher 8ec18e8d7b Merge branch 'keyboard-navigation' of https://github.com/mikiher/audiobookshelf into keyboard-navigation 2024-03-06 13:53:49 +02:00
mikiher 15545654ea Alternative input width fix in MultiSelect components 2024-03-06 13:41:54 +02:00
advplyr 8a0fab2b20 Fix:Resizing page update chapter ticks and track bar #2707 2024-03-05 14:30:39 -06:00
advplyr 6e8c6aa740 Merge pull request #2701 from mikiher/keyboard-navigation
Fix input width in MultiSelect UI components
2024-03-05 13:13:52 -06:00
mikiher 5005aabe5e Fix input width in MultiSelect components 2024-03-03 23:40:47 +02:00
advplyr abc2d28617 Merge pull request #2699 from pmangro/master
[PT-BR] enhance-ebook-filter strings translation
2024-03-03 11:42:56 -06:00
pmangro 7569a14510 Merge pull request #1 from pmangro/enhance-ebook-filter-strings
[PT-BR] enhance-ebook-filter strings translation
2024-03-03 13:33:01 -03:00
pmangro b52341dbcf [PT-BR] enhance-ebook-filter strings translation 2024-03-03 13:32:01 -03:00
advplyr b4eed3bad2 Merge pull request #2694 from mikiher/client-image-caching
Client side cover image caching
2024-03-01 17:48:08 -06:00
mikiher 4fe672f09d Update cover image URLs with timestamp where available 2024-03-01 11:55:53 +02:00
advplyr 49af7eb7b0 Merge pull request #2692 from lkiesow/log-src
Fix log source in log file
2024-02-29 17:01:59 -06:00
advplyr c93c863d82 Merge pull request #2677 from Teekeks/enhance-ebook-filter
feat: Expanded filter to include "has no ebook" and "has no supplementary ebooks" options
2024-02-29 14:00:50 -06:00
advplyr 763bb1b829 Map ebook filter translations 2024-02-29 13:59:00 -06:00
Lars Kiesow 79d32274aa Fix log source in log file
The logger should include a source containing the location where the
logger was called. This works well for logging to `stdout`. Unfortunately,
the file logs contain the locations where the file logging is called
inside of the logger. This is not helpful:

```
{"timestamp":"2023-11-19 16:35:43","source":"Logger.js:114","message":"[oldDbFiles] Processed db data file with 1 entities","levelName":"INFO","level":2}
{"timestamp":"2023-11-19 16:35:43","source":"Logger.js:114","message":"[oldDbFiles] Finished loading db data with 2 entities","levelName":"INFO","level":2}
{"timestamp":"2023-11-19 16:35:43","source":"Logger.js:114","message":"[oldDbFiles] 2 settings loaded","levelName":"INFO","level":2}
```

This patch fixes the issue, ensureing that the actual source location
will be logged:

```
{"timestamp":"2024-02-29 18:12:59.832","source":"DailyLog.js:132","message":"[DailyLog] 2024-02-29: Loaded 20 Logs","levelName":"DEBUG","level":1}
{"timestamp":"2024-02-29 18:12:59.638","source":"Server.js:172","message":"=== Starting Server ===","levelName":"INFO","level":2}
{"timestamp":"2024-02-29 18:12:59.638","source":"Server.js:103","message":"[Server] Init v2.8.0","levelName":"INFO","level":2}
```
2024-02-29 18:16:29 +01:00
Lars Kiesow 987842ed04 Fix file names with URL control characters
This patch ensures that files names like `series #3 xy.jpg` are actually
handled correctly instead of the part after `#` being interpreted as
fragment and being discarded.

I noticed that in a few rare cases the App wouldn't properly display
cover images. It turns out that due the file names containing a `#`, the
file path got corrupted, causing Audiobookshelf to return a 403.
2024-02-29 17:56:55 +01:00
advplyr d2b006b909 Update:Windows binary manager to install ffmpeg/ffprobe 5.1 #1098 2024-02-28 16:16:44 -06:00
mfcar f4a19e48ad Update login page 2024-02-28 19:21:11 +00:00
advplyr 38f12f4795 Fix:Podcast schedule max new episodes to download setting to 0 and fix input blurs #2680 2024-02-27 17:17:33 -06:00
advplyr 7a4f4b1586 Merge pull request #2676 from pmangro/2.8.0.a-PT-BT
[PT-BR] Updated strings
2024-02-27 15:59:09 -06:00
pmangro 20ec54e085 [PT-BR] Updated strings 2024-02-27 14:31:21 -03:00
Teekeks 655bebfec4 feat: Expanded filter to include "has no ebook" and "has no supplementary ebooks" options 2024-02-27 18:30:05 +01:00
advplyr 71e1abd263 Merge pull request #2673 from Machou/master
Update fr.json
2024-02-27 08:56:03 -06:00
Machou 72172dcb33 Update fr.json 2024-02-27 08:56:31 +01:00
advplyr def2988e12 Update:Passport openid-client request timeout set to 10s (default was 3.5s) #2669 2024-02-26 17:20:11 -06:00
mikiher b47793c365 Add cache control header for timestamped cover image requests 2024-02-26 14:00:25 +02:00
advplyr 3a99cc56b7 Update:Debian packager script to use xz compression instead of zstd 2024-02-25 12:56:04 -06:00
advplyr 24c35dede5 Merge pull request #2659 from mikiher/quick-match-dup-authors
Fix dup author addition logic
2024-02-25 08:12:05 -06:00
advplyr 8c4400dff1 Merge pull request #2657 from JBlond/master
Update de strings
2024-02-25 08:08:17 -06:00
advplyr af8dffaa33 Merge pull request #2573 from mikiher/fix-match-update
Merge cover and media update in Match.vue into a single /media API call
2024-02-25 08:07:51 -06:00
advplyr 4a36a3c8e6 Merge branch 'master' into fix-match-update 2024-02-25 08:00:29 -06:00
mikiher e6735e042e Fix dup author addition logic 2024-02-25 09:01:26 +02:00
JBlond c799379a54 Update de strings 2024-02-24 20:23:51 +01:00
advplyr d8b9f08e5a Merge pull request #2641 from Teekeks/year-review-translation
feat(i18n): made "Year in Review" UI elements translatable and added german translation for those
2024-02-23 17:01:43 -06:00
advplyr 608b25de45 Map en-us translations 2024-02-23 16:59:46 -06:00
advplyr 2db8869908 Merge branch 'master' into year-review-translation 2024-02-23 16:56:53 -06:00
advplyr 9500737bbe Merge pull request #2644 from RasmusKoit/estonian-translations
Adds estonian translation
2024-02-23 16:55:41 -06:00
advplyr def2b6425b Update:Username and password inputs on login page trim whitespace #2628 2024-02-22 16:30:41 -06:00
jfrazx 5e8f247e84 chore: merge master 2024-02-22 10:50:49 -08:00
Lena During 761a2ff0bf Merge branch 'advplyr:master' into payer-translation 2024-02-22 19:25:04 +01:00
Teekeks e368ffe29f feat(i18n): added missing translatable string in player ui 2024-02-22 19:20:49 +01:00
mikiher 0f4b11494e Merge branch 'advplyr:master' into fix-match-update 2024-02-22 12:20:49 +02:00
rasmuskoit 46448ce1e9 Adds estonian translation 2024-02-22 09:46:09 +02:00
advplyr fbe12b393f Merge pull request #2639 from pmangro/2.8.0.a-PT-BT
[PT-BR] Terminology adjustments and typo fixes
2024-02-21 18:29:29 -06:00
advplyr ccf59b2c1a Merge pull request #2638 from DownloadableFox/master
Updated and fixed Spanish translation
2024-02-21 18:28:28 -06:00
Lena During d7af3b7788 Merge branch 'master' into year-review-translation 2024-02-22 00:39:49 +01:00
Teekeks 682aca0b2a feat(i18n): made "Year in Review" UI elements translatable and added german translation for those 2024-02-22 00:36:43 +01:00
advplyr 3328ffe1b9 Merge pull request #2636 from megamegax/master
feat(i18n): add Hungarian translation
2024-02-21 16:07:36 -06:00
DownloadableFox c07b7840e2 Updated and fixed Spanish translation 2024-02-20 21:14:55 -05:00
pmangro 9f848b2c64 Update pt-br.json - adjust terminology and typo 2024-02-20 17:28:37 -03:00
pmangro 3d66ec0761 PT-BR size adjustment 2024-02-20 11:42:48 -03:00
advplyr f50920be69 Merge pull request #2635 from lonezel/lonezel-updated-german-translation
Update de.json
2024-02-20 08:37:48 -06:00
advplyr d31add9d5a Merge pull request #2634 from pmangro/2.8.0-PT-BR
Updated pt-br string
2024-02-20 08:36:33 -06:00
Hunyady Mihály a4dcb4f92e feat(i18n): add Hungarian translation 2024-02-20 12:25:43 +01:00
lonezel 2c589c1dbd Update de.json
Translated new strings to german - this is my first ever commit on Github, if I need to change something in my workflow let me know!
2024-02-20 10:19:48 +01:00
pmangro 60ea386c6d Updated pt-br string 2024-02-20 05:57:52 -03:00
advplyr 24be1a0ec5 Merge pull request #2629 from burghy86/patch-13
Update it.json
2024-02-19 08:56:39 -06:00
burghy86 e71a14756b Update it.json
new string update
2024-02-19 15:53:12 +01:00
advplyr 85fecbd1b9 Version bump v2.8.0 2024-02-18 16:43:16 -06:00
advplyr 335d39f317 Update guide link for custom metadata providers 2024-02-18 16:22:10 -06:00
advplyr 973a18d346 Update:Added button to user edit modal for unlinking user from openid #2587 2024-02-18 15:38:45 -06:00
advplyr a43b93d796 Fix:Clear library filter data cache when library item is updated #2597 2024-02-18 14:58:46 -06:00
advplyr acf75abdf1 Update:Match author use closest name match by levenshtein distance #2624 2024-02-18 13:06:51 -06:00
advplyr 58598bfcf2 Update:Clamp author description to 4 lines and add more button #2614 2024-02-18 11:32:24 -06:00
advplyr 7a570439db Update:Clamp item descriptions to 4 lines and show more button #2614 2024-02-18 11:24:36 -06:00
advplyr 6e769d1c20 Merge pull request #2554 from mikiher/ffmpeg-latest
Modify BinaryManager to download version 6.1
2024-02-17 17:42:24 -06:00
advplyr d9e7f5d133 Update BinaryManager JSDocs, move validVersions to required binary objects 2024-02-17 17:40:33 -06:00
advplyr a119b05d85 Merge branch 'master' into ffmpeg-latest 2024-02-17 17:05:51 -06:00
advplyr 7bf7b6bcf9 Merge pull request #2553 from Sapd/sso
OpenID: Implement Logout + Fix state + Fix URL Regex
2024-02-17 17:03:12 -06:00
advplyr e47ea98cdd Fix:Disconnect from socket on logout, remove unnecessary logout function 2024-02-17 16:58:49 -06:00
advplyr bf66e13377 Update jsdocs 2024-02-17 16:06:25 -06:00
advplyr d7aba5629e Remove old login rate limiter 2024-02-17 15:29:06 -06:00
advplyr a5c200ac79 Merge branch 'master' into sso 2024-02-17 14:15:41 -06:00
advplyr fdc1fc1b2a Merge pull request #2491 from liaochuan/liaocl
Add Podcast Search Region
2024-02-17 13:32:23 -06:00
advplyr 42a4b762bd Fix translations sort order and pt-br translations 2024-02-17 13:30:30 -06:00
advplyr 180c328ed1 Update jsdocs for search podcasts 2024-02-17 13:24:49 -06:00
advplyr 2ec52a7a45 Merge branch 'master' into liaocl 2024-02-17 12:56:05 -06:00
advplyr aacf37e32b Fix:Year in Review crashing when listening session has a null genre #2623 2024-02-16 16:16:55 -06:00
advplyr 52323b7eb5 Update:Podcast episode download show ffmpeg progress and print full debug log dump on error 2024-02-16 16:05:02 -06:00
advplyr 5b5613a762 Merge pull request #2617 from ipcintron/pwa
Update pwa icon to use iOS icon
2024-02-16 13:44:57 -06:00
ipcintron de6df0c029 Forgot to modify nuxt.config.js 2024-02-16 12:29:52 -06:00
ipcintron e180b3c171 Renamed the icon to make it clear it is being used for iOS 2024-02-16 12:27:10 -06:00
ipcintron 1364b79cbf Put the icon in the link array for iOS only 2024-02-16 10:10:09 -06:00
ipcintron ef96f3102f Merge branch 'advplyr:master' into pwa 2024-02-16 09:51:10 -06:00
advplyr 06ce3b08f7 Merge pull request #2619 from ipcintron/theme
PWA (iOS) theme color fix
2024-02-16 09:31:11 -06:00
advplyr a13217dddf Fix:Initial language code setting eventBus not yet defined 2024-02-16 09:12:47 -06:00
advplyr ce528d4012 Merge pull request #2620 from pmangro/pmangro-patch-1
PT-BR Strings
2024-02-16 09:05:31 -06:00
pmangro 89207b6d2a Update i18n.js
Added PT-BR
2024-02-16 11:57:54 -03:00
pmangro e9591caf81 Spelling 2024-02-16 11:56:31 -03:00
pmangro 24f1aae6b6 Update pt-br.json
Strings 541-766
2024-02-16 11:44:25 -03:00
ipcintron 04fbc9a22b change theme color 2024-02-16 02:15:27 -06:00
ipcintron 14e31d5690 update pwa icon 2024-02-16 01:32:04 -06:00
advplyr a9e9808183 Fix:trim whitespace from asin for chapter lookup #2605 2024-02-15 17:05:48 -06:00
advplyr af7cb2432b Update:Log uncaught exceptions to crash_logs.txt #706 & cleanup logger 2024-02-15 16:46:19 -06:00
pmangro e0c1364916 Create pt-br.json
540 linhas iniciais
2024-02-15 19:08:05 -03:00
advplyr 04d16fc535 Fix:Audio player buttons to use button el and add aria-label translations #2599 2024-02-14 18:28:19 -06:00
advplyr 44135b3fed Rename StreamContainer to MediaPlayerContainer 2024-02-14 18:12:35 -06:00
advplyr 6111e8f0da Fix:Global search menu for mobile 2024-02-13 18:45:01 -06:00
advplyr 4e3e7b10ce Update:Custom metadata provider adapter sends mediaType as a query param 2024-02-12 17:12:49 -06:00
advplyr ce7f81d676 Merge pull request #2486 from FlyinPancake/dewyer/add-custom-metadata-provider
[Feature] Add support for custom metadata providers through a REST API
2024-02-11 17:04:55 -06:00
advplyr 0cf2f8885e Add custom metadata provider controller, update model, move to item metadata utils 2024-02-11 16:48:16 -06:00
advplyr ddf4b2646c Merge branch 'master' into dewyer/add-custom-metadata-provider 2024-02-11 09:10:29 -06:00
advplyr fe1e0749a2 Update:Listening sessions table rows per page text wrapping 2024-02-08 19:12:59 -06:00
advplyr 2093468c92 Fix:Local playback sessions not persisting the last updatedAt value 2024-02-08 19:12:35 -06:00
mikiher 19af7454f2 Force Update LibraryItem model updatedAt refresh (fixes #2593) 2024-02-07 20:57:50 +02:00
KeyboardHammer d24427aad8 fix property 2024-02-03 22:04:40 -06:00
KeyboardHammer e2bb0cfb7c add sorting to author page 2024-02-03 21:48:35 -06:00
mikiher 2ebdb44826 Merge cover and media update in Match.vue into a single /media API call 2024-02-01 12:03:12 +02:00
advplyr 432e25565e Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-01-31 17:23:20 -06:00
advplyr ebe511404a Remove updateMedia endpoint cover cache purge 2024-01-31 17:23:16 -06:00
advplyr e0a79fb86c Merge pull request #2570 from Weldawadyathink/main
Return png from AudiobookCovers.com
2024-01-30 17:02:14 -06:00
Spenser Bushey 295ca3d9a2 Return png from AudiobookCovers.com
Changes AudiobookCovers.com provider to return the full size png file from the server. The original file url has the incorrect content-type header set, which caused issues downloading new cover images.
2024-01-30 09:15:50 -08:00
advplyr dbad8bdb96 Merge pull request #2567 from ipcintron/lockscreen_cover
added raw cover on lockscreen for iOS
2024-01-29 15:21:44 -06:00
ipcintron 8c703859a0 added raw cover 2024-01-29 13:18:58 -06:00
advplyr bedb260b00 Update:Epub ereader allow scripted content 2024-01-28 16:02:02 -06:00
advplyr b49592301f Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-01-28 16:00:32 -06:00
advplyr c6c67078b8 Update:PWA manifest icon to include PNG #2520 2024-01-28 16:00:26 -06:00
advplyr 9e45ad10f1 Merge pull request #2564 from Teekeks/patch-1
Update de.json
2024-01-28 10:10:13 -06:00
Lena During 24da859975 Update de.json 2024-01-28 16:06:07 +01:00
advplyr 0b6a8a9641 Update:Remove 64x64 app icon from PWA manifest so that only the SVG is available #2520 2024-01-27 10:50:44 -06:00
advplyr e43c4f082e Fix:Rich text editor labels and add translations 2024-01-26 17:22:37 -06:00
advplyr 0b334cf957 Add:Authentication setting to show a custom message on login #2552 2024-01-26 17:08:23 -06:00
advplyr ae387ab397 Merge pull request #2559 from bloodscript/master
German localization optimization
2024-01-26 16:28:02 -06:00
bloodscript 056e62dce8 added plural to metadata order hint 2024-01-26 23:15:27 +01:00
bloodscript 47999214bd corrected misspelling of adress 2024-01-26 23:13:11 +01:00
bloodscript 68473ee345 added missing metadata translation 2024-01-26 23:11:38 +01:00
bloodscript 455f27d443 mainly changed usage formular wording for you to the more commonly used, also corrected some misspellings 2024-01-26 23:09:54 +01:00
bloodscript ba996c3b55 translation wasnt accurate verschluesselung means encryption 2024-01-26 20:12:37 +01:00
mikiher d43a1109c8 Modify BinaryManager to download version 6.1 and remove old dowloaded versions 2024-01-25 17:51:06 +02:00
Denis Arnst c3ba7daa16 Auth: Remove is_rest cookie 2024-01-25 16:05:41 +01:00
Denis Arnst 82048cd4f3 SSO: Also save openid_id_token longer 2024-01-25 15:13:56 +01:00
Denis Arnst 71b0a5cc81 SSO Settings: Fix Redirect URL Regex
Forgot to include subpaths
2024-01-25 11:49:10 +01:00
Denis Arnst edb5ff1e33 SSO: Remove pick function 2024-01-25 11:44:20 +01:00
Denis Arnst d4ed6348ee Auth: Store auth_method longer
Its not unrealistic that someone keeps being logged into the app for more than a year
if not stored longer logout process might not work anymore
2024-01-25 11:20:44 +01:00
Denis Arnst f12ac685e8 /auth/openid: Restructure
- Distingush more explictly between mobile and web flow and simplify logic
- Allow state parameter to be passed in mobile flow
- Additional checks for correct parameters
- Remove unused id_token code
- Enforce S256 and don't allow plain PKCE
2024-01-25 11:13:34 +01:00
advplyr b9ec4068ee Merge pull request #2510 from Torstein-Eide/patch-1
README, add HAproxy example
2024-01-24 16:55:32 -06:00
advplyr 02aabb8f97 Update readme.md 2024-01-24 16:55:21 -06:00
advplyr dcec2154c0 Update readme.md 2024-01-24 16:55:17 -06:00
advplyr bbc1d20396 Update readme.md 2024-01-24 16:55:12 -06:00
advplyr e682213681 Update readme.md 2024-01-24 16:55:06 -06:00
advplyr 0153c0faae Update readme.md 2024-01-24 16:54:54 -06:00
Denis Arnst 87ebf4722b OpenID/SSO: Implement Logout functionality 2024-01-24 22:47:50 +01:00
advplyr 3906dca04e Update:RSS feeds only use chapter titles for episode titles if all audio tracks match chapter times #2543 2024-01-23 17:51:34 -06:00
advplyr 399ba314a3 Update github issue template to include Windows Tray App 2024-01-23 15:48:58 -06:00
jfrazx 70827727aa feat(429): retry 429 request errors 2024-01-22 22:19:05 -08:00
jfrazx 73c21242b4 feat: utilize p-throttle instad of limiter 2024-01-22 20:36:20 -08:00
advplyr 19e1803633 Remove unused import 2024-01-22 17:56:41 -06:00
jfrazx 06391b9b37 chore: merge master 2024-01-21 19:15:52 -08:00
advplyr 71048c7ff0 Remove support for Docker armv7 builds 2024-01-20 16:39:43 -06:00
advplyr 7f350279fa Update to node20
- updates many dependencies
- removes @nuxtjs/tailwindcss and postcss8
- pkg targets are using node18 until node20 targets are available
2024-01-19 17:54:41 -06:00
jfrazx 4c9b2ad08b chore: merge master 2024-01-16 18:31:29 -08:00
jfrazx 79c34d0638 chore: merge master 2024-01-13 11:46:38 -08:00
FlyinPancake 6ef4944d89 Merge branch 'advplyr:master' into dewyer/add-custom-metadata-provider 2024-01-13 01:08:23 +01:00
FlyinPancake 3b531144cf implemented suggestions, extended CMPs with series 2024-01-12 21:45:03 +01:00
Torstein Eide 6ca684603c Fix typos 2024-01-12 14:35:30 +01:00
Torstein Eide cf85d66b2f Add example for HAproxy 2024-01-12 14:26:32 +01:00
mozhu 81020ff34d 播客搜索地区配置增加默认参数 2024-01-05 15:50:20 +08:00
mozhu fea78898a5 移动播客搜索地区配置到媒体库配置 2024-01-05 14:45:35 +08:00
mozhu 1be34564f2 数据绑定错误修改 2024-01-04 15:00:40 +08:00
mozhu 56eff7a236 增加播客搜索地区配置 2024-01-04 11:52:45 +08:00
Barnabas Ratki 12c6a1baa0 Fix log messages 2024-01-03 20:42:35 +01:00
Barnabas Ratki 5ea423072b Small fixes 2024-01-03 20:40:36 +01:00
Barnabas Ratki 08a41e37b4 Add specification 2024-01-03 20:27:42 +01:00
Barnabas Ratki 8027c4a06f Added support for custom metadata providers
WiP but already open to feedback
2024-01-03 20:25:34 +01:00
jfrazx 4e6b75d650 fix; HTTP/429 when requesting authors information, resolves #1570 2023-10-05 13:48:55 -07:00
Jorge 679bdf36b1 Add CodeQL workflow 2023-07-03 09:15:04 +02:00
339 changed files with 39511 additions and 31294 deletions
+1 -4
View File
@@ -1,5 +1,5 @@
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster # [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
ARG VARIANT=16 ARG VARIANT=20
FROM mcr.microsoft.com/devcontainers/javascript-node:0-${VARIANT} as base FROM mcr.microsoft.com/devcontainers/javascript-node:0-${VARIANT} as base
# Setup the node environment # Setup the node environment
@@ -10,6 +10,3 @@ RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \ DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
curl tzdata ffmpeg && \ curl tzdata ffmpeg && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Move tone executable to appropriate directory
COPY --from=sandreas/tone:v0.1.5 /usr/local/bin/tone /usr/local/bin/
+2 -1
View File
@@ -5,5 +5,6 @@ module.exports.config = {
ConfigPath: Path.resolve('config'), ConfigPath: Path.resolve('config'),
MetadataPath: Path.resolve('metadata'), MetadataPath: Path.resolve('metadata'),
FFmpegPath: '/usr/bin/ffmpeg', FFmpegPath: '/usr/bin/ffmpeg',
FFProbePath: '/usr/bin/ffprobe' FFProbePath: '/usr/bin/ffprobe',
SkipBinariesCheck: true
} }
+1 -1
View File
@@ -8,7 +8,7 @@
// Append -bullseye or -buster to pin to an OS version. // Append -bullseye or -buster to pin to an OS version.
// Use -bullseye variants on local arm64/Apple Silicon. // Use -bullseye variants on local arm64/Apple Silicon.
"args": { "args": {
"VARIANT": "16" "VARIANT": "20"
} }
}, },
"mounts": [ "mounts": [
+8
View File
@@ -0,0 +1,8 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true
+61 -13
View File
@@ -1,40 +1,50 @@
name: 🐞 Bug Report name: 🐞 Bug Report
description: File a bug/issue description: File a bug/issue and help us improve Audiobookshelf
title: "[Bug]: " title: '[Bug]: '
labels: ["bug", "triage"] labels: ['bug', 'triage']
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: "### Please first search for your issue and check the [docs](https://audiobookshelf.org/docs)." value: 'Thank you for filing a bug report! 🐛'
- type: markdown - type: markdown
attributes: attributes:
value: "### Mobile app issues report [here](https://github.com/advplyr/audiobookshelf-app/issues/new/choose)." value: 'Please first search for your issue and check the [docs](https://audiobookshelf.org/docs).'
- type: markdown - type: markdown
attributes: attributes:
value: "### Join the [discord server](https://discord.gg/HQgCbd6E75) for questions or if you are not sure about a bug." value: 'Report issues with the mobile app [here](https://github.com/advplyr/audiobookshelf-app/issues/new/choose).'
- type: markdown - type: markdown
attributes: attributes:
value: "## Be as descriptive as you can. Include screenshots, error logs, browser, file types, everything you can think of that might be relevant." value: 'Join the [discord server](https://discord.gg/HQgCbd6E75) for questions or if you are not sure about a bug.'
- type: textarea - type: textarea
id: what-happened id: what-happened
attributes: attributes:
label: Describe the issue label: What happened?
description: What happened & what did you expect to happen placeholder: Tell us what you see!
validations:
required: true
- type: textarea
id: what-was-expected
attributes:
label: What did you expect to happen?
placeholder: Tell us what you expected to see! Be as descriptive as you can and include screenshots if applicable.
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: steps-to-reproduce id: steps-to-reproduce
attributes: attributes:
label: Steps to reproduce the issue label: Steps to reproduce the issue
value: "1. " value: '1. '
validations: validations:
required: true required: true
- type: markdown
attributes:
value: '## Install Environment'
- type: input - type: input
id: version id: version
attributes: attributes:
label: Audiobookshelf version label: Audiobookshelf version
description: Do not put 'Latest version', please put the actual version here description: Do not put 'Latest version', please put the actual version here
placeholder: "e.g. v1.6.60" placeholder: 'e.g. v1.6.60'
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
@@ -44,7 +54,45 @@ body:
options: options:
- Docker - Docker
- Debian/PPA - Debian/PPA
- Windows Tray App
- Built from source - Built from source
- Other - Other (list in "Additional Notes" box)
validations: validations:
required: true required: true
- type: dropdown
id: server-os
attributes:
label: What OS is your Audiobookshelf server hosted from?
options:
- Windows
- macOS
- Linux
- Other (list in "Additional Notes" box)
validations:
required: true
- type: dropdown
id: desktop-browsers
attributes:
label: If the issue is being seen in the UI, what browsers are you seeing the problem on?
options:
- Chrome
- Firefox
- Safari
- Edge
- Firefox for Android
- Chrome for Android
- Safari on iOS
- Other (list in "Additional Notes" box)
- type: textarea
id: logs
attributes:
label: Logs
description: Please include any relevant logs here. This field is automatically formatted into code, so you do not need to include any backticks.
placeholder: Paste logs here
render: shell
- type: textarea
id: additional-notes
attributes:
label: Additional Notes
description: Anything else you want to add?
placeholder: 'e.g. I have tried X, Y, and Z.'
+51 -5
View File
@@ -1,17 +1,63 @@
name: 🚀 Feature Request name: 🚀 Feature Request
description: Request a feature/enhancement description: Request a feature/enhancement
title: "[Enhancement]: " title: '[Enhancement]: '
labels: ["enhancement"] labels: ['enhancement']
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: "### Please first search in both issues & discussions for your enhancement." value: '#### *Mobile app features should be [requested here](https://github.com/advplyr/audiobookshelf-app/issues/new/choose)*.'
- type: markdown - type: markdown
attributes: attributes:
value: "### Mobile app features should be requested [here](https://github.com/advplyr/audiobookshelf-app/issues/new/choose)." value: '## Web/Server Feature Request Description'
- type: markdown
attributes:
value: 'Please first search in both issues & discussions for your enhancement.'
- type: dropdown
id: enhancment-type
attributes:
label: Type of Enhancement
options:
- Server Backend
- Web Interface/Frontend
- Documentation
- type: textarea - type: textarea
id: describe id: describe
attributes: attributes:
label: Describe the feature/enhancement label: Describe the Feature/Enhancement
description: Please help us understand what you want.
placeholder: What is your vision?
validations: validations:
required: true required: true
- type: textarea
id: the-why
attributes:
label: Why would this be helpful?
description: Please help us understand why this would enhance your experience.
placeholder: Explain the "why" or "use case".
validations:
required: true
- type: textarea
id: image
attributes:
label: Future Implementation (Screenshot)
description: Please help us visualize by including a doodle or screenshot.
placeholder: How could this look?
validations:
required: true
- type: markdown
attributes:
value: '## Web/Server Current Implementation'
- type: input
id: version
attributes:
label: Audiobookshelf Server Version
description: Do not put 'Latest version', please put your current version number here
placeholder: 'e.g. v1.6.60'
validations:
required: true
- type: textarea
id: current-image
attributes:
label: Current Implementation (Screenshot)
description: What page were you looking at when you thought of this enhancement?
placeholder: If an image is not applicable, please explain why.
+65
View File
@@ -0,0 +1,65 @@
name: "CodeQL"
on:
push:
branches: [ 'master' ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ 'master' ]
schedule:
- cron: '16 5 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -71,7 +71,7 @@ jobs:
with: with:
tags: ${{ github.event.inputs.tags || steps.meta.outputs.tags }} tags: ${{ github.event.inputs.tags || steps.meta.outputs.tags }}
context: . context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64
push: true push: true
cache-from: type=local,src=/tmp/.buildx-cache cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
+30
View File
@@ -0,0 +1,30 @@
name: Verify all i18n files are alphabetized
on:
pull_request:
paths:
- client/strings/** # Should only check if any strings changed
push:
paths:
- client/strings/** # Should only check if any strings changed
jobs:
update_translations:
runs-on: ubuntu-latest
steps:
# Check out the repository
- name: Checkout repository
uses: actions/checkout@v4
# Set up node to run the javascript
- name: Set up node
uses: actions/setup-node@v4
with:
node-version: '20'
# The only argument is the `directory`, which is where the i18n files are
# stored.
- name: Run Update JSON Files action
uses: audiobookshelf/audiobookshelf-i18n-updater@v1.3.0
with:
directory: 'client/strings/' # Adjust the directory path as needed
+5 -5
View File
@@ -4,7 +4,7 @@ on:
pull_request: pull_request:
push: push:
branches-ignore: branches-ignore:
- 'dependabot/**' # Don't run dependabot branches, as they are already covered by pull requests - 'dependabot/**' # Don't run dependabot branches, as they are already covered by pull requests
jobs: jobs:
build: build:
@@ -16,10 +16,10 @@ jobs:
- name: setup nade - name: setup nade
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 20
- name: install pkg - name: install pkg (using yao-pkg fork for targetting node20)
run: npm install -g pkg run: npm install -g @yao-pkg/pkg
- name: get client dependencies - name: get client dependencies
working-directory: client working-directory: client
@@ -33,7 +33,7 @@ jobs:
run: npm ci --only=production run: npm ci --only=production
- name: build binary - name: build binary
run: pkg -t node18-linux-x64 -o audiobookshelf . run: pkg -t node20-linux-x64 -o audiobookshelf .
- name: run audiobookshelf - name: run audiobookshelf
run: | run: |
+30
View File
@@ -0,0 +1,30 @@
name: API linting
# Run on pull requests or pushes when there is a change to the OpenAPI file
on:
push:
paths:
- docs/
pull_request:
paths:
- docs/
jobs:
build:
runs-on: ubuntu-latest
steps:
# Check out the repository
- name: Checkout
uses: actions/checkout@v4
# Set up node to run the javascript
- name: Set up node
uses: actions/setup-node@v4
# Install Redocly CLI
- name: Install Redocly CLI
run: npm install -g @redocly/cli@latest
# Perform linting for exploded spec
- name: Run linting for exploded spec
run: redocly lint docs/root.yaml --format=github-actions
# Perform linting for bundled spec
- name: Run linting for bundled spec
run: redocly lint docs/openapi.json --format=github-actions
+17
View File
@@ -0,0 +1,17 @@
name: Dispatch an abs-windows event
on:
release:
types: [published]
workflow_dispatch:
jobs:
abs-windows-dispatch:
runs-on: ubuntu-latest
steps:
- name: Send a remote repository dispatch event
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.ABS_WINDOWS_PAT }}
repository: mikiher/audiobookshelf-windows
event-type: build-windows
+37
View File
@@ -0,0 +1,37 @@
name: Run Unit Tests
on:
workflow_dispatch:
inputs:
ref:
description: 'Branch/Tag/SHA to test'
required: true
pull_request:
push:
jobs:
run-unit-tests:
name: Run Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout (push/pull request)
uses: actions/checkout@v4
if: github.event_name != 'workflow_dispatch'
- name: Checkout (workflow_dispatch)
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
if: github.event_name == 'workflow_dispatch'
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
+1
View File
@@ -19,3 +19,4 @@
sw.* sw.*
.DS_STORE .DS_STORE
.idea/* .idea/*
tailwind.compiled.css
+17
View File
@@ -0,0 +1,17 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 400,
"proseWrap": "never",
"trailingComma": "none",
"overrides": [
{
"files": ["*.html"],
"options": {
"singleQuote": false,
"wrapAttributes": false,
"sortAttributes": false
}
}
]
}
+7
View File
@@ -0,0 +1,7 @@
{
"recommendations": [
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode",
"octref.vetur"
]
}
+7 -1
View File
@@ -17,5 +17,11 @@
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.detectIndentation": true, "editor.detectIndentation": true,
"editor.tabSize": 2, "editor.tabSize": 2,
"javascript.format.semicolons": "remove" "javascript.format.semicolons": "remove",
"[javascript][json][jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "octref.vetur"
}
} }
+2 -4
View File
@@ -1,13 +1,12 @@
### STAGE 0: Build client ### ### STAGE 0: Build client ###
FROM node:16-alpine AS build FROM node:20-alpine AS build
WORKDIR /client WORKDIR /client
COPY /client /client COPY /client /client
RUN npm ci && npm cache clean --force RUN npm ci && npm cache clean --force
RUN npm run generate RUN npm run generate
### STAGE 1: Build server ### ### STAGE 1: Build server ###
FROM sandreas/tone:v0.1.5 AS tone FROM node:20-alpine
FROM node:16-alpine
ENV NODE_ENV=production ENV NODE_ENV=production
@@ -21,7 +20,6 @@ RUN apk update && \
g++ \ g++ \
tini tini
COPY --from=tone /usr/local/bin/tone /usr/local/bin/
COPY --from=build /client/dist /client/dist COPY --from=build /client/dist /client/dist
COPY index.js package* / COPY index.js package* /
COPY server server COPY server server
+1 -15
View File
@@ -50,7 +50,6 @@ install_ffmpeg() {
echo "Starting FFMPEG Install" echo "Starting FFMPEG Install"
WGET="wget https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz --output-document=ffmpeg-git-amd64-static.tar.xz" WGET="wget https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz --output-document=ffmpeg-git-amd64-static.tar.xz"
WGET_TONE="wget https://github.com/sandreas/tone/releases/download/v0.1.5/tone-0.1.5-linux-x64.tar.gz --output-document=tone-0.1.5-linux-x64.tar.gz"
if ! cd "$FFMPEG_INSTALL_DIR"; then if ! cd "$FFMPEG_INSTALL_DIR"; then
echo "Creating ffmpeg install dir at $FFMPEG_INSTALL_DIR" echo "Creating ffmpeg install dir at $FFMPEG_INSTALL_DIR"
@@ -63,13 +62,7 @@ install_ffmpeg() {
tar xvf ffmpeg-git-amd64-static.tar.xz --strip-components=1 --no-same-owner tar xvf ffmpeg-git-amd64-static.tar.xz --strip-components=1 --no-same-owner
rm ffmpeg-git-amd64-static.tar.xz rm ffmpeg-git-amd64-static.tar.xz
# Temp downloading tone library to the ffmpeg dir echo "Good to go on Ffmpeg... hopefully"
echo "Getting tone.."
$WGET_TONE
tar xvf tone-0.1.5-linux-x64.tar.gz --strip-components=1 --no-same-owner
rm tone-0.1.5-linux-x64.tar.gz
echo "Good to go on Ffmpeg (& tone)... hopefully"
} }
setup_config() { setup_config() {
@@ -77,12 +70,6 @@ setup_config() {
echo "Existing config found." echo "Existing config found."
cat $CONFIG_PATH cat $CONFIG_PATH
# TONE_PATH variable added in 2.1.6, if it doesnt exist then add it
if ! grep -q "TONE_PATH" "$CONFIG_PATH"; then
echo "Adding TONE_PATH to existing config"
echo "TONE_PATH=$FFMPEG_INSTALL_DIR/tone" >> "$CONFIG_PATH"
fi
else else
if [ ! -d "$DEFAULT_DATA_DIR" ]; then if [ ! -d "$DEFAULT_DATA_DIR" ]; then
@@ -98,7 +85,6 @@ setup_config() {
CONFIG_PATH=$DEFAULT_DATA_DIR/config CONFIG_PATH=$DEFAULT_DATA_DIR/config
FFMPEG_PATH=$FFMPEG_INSTALL_DIR/ffmpeg FFMPEG_PATH=$FFMPEG_INSTALL_DIR/ffmpeg
FFPROBE_PATH=$FFMPEG_INSTALL_DIR/ffprobe FFPROBE_PATH=$FFMPEG_INSTALL_DIR/ffprobe
TONE_PATH=$FFMPEG_INSTALL_DIR/tone
PORT=$DEFAULT_PORT PORT=$DEFAULT_PORT
HOST=$DEFAULT_HOST" HOST=$DEFAULT_HOST"
+2 -3
View File
@@ -48,11 +48,10 @@ Description: $DESCRIPTION"
echo "$controlfile" > dist/debian/DEBIAN/control; echo "$controlfile" > dist/debian/DEBIAN/control;
# Package debian # Package debian
pkg -t node16-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf . pkg -t node20-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf .
fakeroot dpkg-deb --build dist/debian fakeroot dpkg-deb -Zxz --build dist/debian
mv dist/debian.deb "dist/$OUTPUT_FILE" mv dist/debian.deb "dist/$OUTPUT_FILE"
chmod +x "dist/$OUTPUT_FILE"
echo "Finished! Filename: $OUTPUT_FILE" echo "Finished! Filename: $OUTPUT_FILE"
+1 -32
View File
@@ -30,8 +30,7 @@
} }
.bookshelf-row { .bookshelf-row {
/* Sidebar width + scrollbar width */ width: calc(100vw - (100vw - 100%));
width: calc(100vw - 88px);
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@@ -217,36 +216,6 @@ Bookshelf Label
filter: blur(20px); filter: blur(20px);
} }
.episode-subtitle {
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
line-height: 16px;
/* fallback */
max-height: 32px;
/* fallback */
-webkit-line-clamp: 2;
/* number of lines to show */
-webkit-box-orient: vertical;
}
.episode-subtitle-long {
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
line-height: 16px;
/* fallback */
max-height: 72px;
/* fallback */
-webkit-line-clamp: 6;
/* number of lines to show */
-webkit-box-orient: vertical;
}
/* Padding for toastification toasts in the top right to not cover appbar/toolbar */ /* Padding for toastification toasts in the top right to not cover appbar/toolbar */
.app-bar-and-toolbar .Vue-Toastification__container.top-right { .app-bar-and-toolbar .Vue-Toastification__container.top-right {
padding-top: 104px; padding-top: 104px;
+1
View File
@@ -24,6 +24,7 @@
word-wrap: normal; word-wrap: normal;
direction: ltr; direction: ltr;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
vertical-align: top;
} }
.material-icons:not([class*="text-"]) { .material-icons:not([class*="text-"]) {
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+59 -30
View File
@@ -1,41 +1,29 @@
<template> <template>
<div id="bookshelf" ref="wrapper" class="w-full max-w-full h-full overflow-y-scroll relative"> <div id="bookshelf" ref="wrapper" class="w-full max-w-full h-full overflow-y-scroll relative" :style="{ fontSize: sizeMultiplier + 'rem' }">
<!-- Cover size widget --> <!-- Cover size widget -->
<widgets-cover-size-widget class="fixed right-4 z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" /> <widgets-cover-size-widget class="fixed right-4 z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" />
<div v-if="loaded && !shelves.length && !search" class="w-full flex flex-col items-center justify-center py-12"> <div v-if="loaded && !shelves.length && !search" class="w-full flex flex-col items-center justify-center py-12">
<p class="text-center text-2xl mb-4 py-4">{{ libraryName }} Library is empty!</p> <p class="text-center text-2xl mb-4 py-4">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p>
<div v-if="userIsAdminOrUp" class="flex"> <div v-if="userIsAdminOrUp" class="flex">
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn> <ui-btn to="/config" color="primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn>
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn> <ui-btn color="success" class="w-52" :loading="isScanningLibrary || tempIsScanning" @click="scan">{{ $strings.ButtonScanLibrary }}</ui-btn>
</div> </div>
</div> </div>
<div v-else-if="loaded && !shelves.length && search" class="w-full h-40 flex items-center justify-center"> <div v-else-if="loaded && !shelves.length && search" class="w-full h-40 flex items-center justify-center">
<p class="text-center text-xl py-4">No results for query</p> <p class="text-center text-xl py-4">{{ $strings.MessageBookshelfNoResultsForQuery }}</p>
</div> </div>
<!-- Alternate plain view --> <!-- Alternate plain view -->
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24"> <div v-else-if="isAlternativeBookshelfView" class="w-full mb-24e">
<template v-for="(shelf, index) in shelves"> <template v-for="(shelf, index) in supportedShelves">
<widgets-item-slider v-if="shelf.type === 'book' || shelf.type === 'podcast'" :shelf-id="shelf.id" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)"> <widgets-item-slider :shelf-id="shelf.id" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" :type="shelf.type" class="bookshelf-row pl-8e my-6e" @selectEntity="(payload) => selectEntity(payload, index)">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p> <p class="font-semibold text-gray-100">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-item-slider> </widgets-item-slider>
<widgets-episode-slider v-else-if="shelf.type === 'episode'" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-episode-slider>
<widgets-series-slider v-else-if="shelf.type === 'series'" :key="index + '.'" :items="shelf.entities" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-series-slider>
<widgets-authors-slider v-else-if="shelf.type === 'authors'" :key="index + '.'" :items="shelf.entities" :height="192 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-authors-slider>
<widgets-narrators-slider v-else-if="shelf.type === 'narrators'" :key="index + '.'" :items="shelf.entities" :height="100 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-narrators-slider>
</template> </template>
</div> </div>
<!-- Regular bookshelf view --> <!-- Regular bookshelf view -->
<div v-else class="w-full"> <div v-else class="w-full">
<template v-for="(shelf, index) in shelves"> <template v-for="(shelf, index) in supportedShelves">
<app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" @selectEntity="(payload) => selectEntity(payload, index)" /> <app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" @selectEntity="(payload) => selectEntity(payload, index)" />
</template> </template>
</div> </div>
@@ -58,10 +46,14 @@ export default {
scannerParseSubtitle: false, scannerParseSubtitle: false,
wrapperClientWidth: 0, wrapperClientWidth: 0,
shelves: [], shelves: [],
lastItemIndexSelected: -1 lastItemIndexSelected: -1,
tempIsScanning: false
} }
}, },
computed: { computed: {
supportedShelves() {
return this.shelves.filter((shelf) => ['book', 'podcast', 'episode', 'series', 'authors', 'narrators'].includes(shelf.type))
},
userIsAdminOrUp() { userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp'] return this.$store.getters['user/getIsAdminOrUp']
}, },
@@ -89,14 +81,16 @@ export default {
return this.coverAspectRatio == 1 return this.coverAspectRatio == 1
}, },
sizeMultiplier() { sizeMultiplier() {
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120 return this.$store.getters['user/getSizeMultiplier']
return this.bookCoverWidth / baseSize
}, },
selectedMediaItems() { selectedMediaItems() {
return this.$store.state.globals.selectedMediaItems || [] return this.$store.state.globals.selectedMediaItems || []
}, },
streamLibraryItem() { streamLibraryItem() {
return this.$store.state.streamLibraryItem return this.$store.state.streamLibraryItem
},
isScanningLibrary() {
return !!this.$store.getters['tasks/getRunningLibraryScanTask'](this.currentLibraryId)
} }
}, },
methods: { methods: {
@@ -174,7 +168,7 @@ export default {
}, },
async fetchCategories() { async fetchCategories() {
const categories = await this.$axios const categories = await this.$axios
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete`) .$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete,share`)
.then((data) => { .then((data) => {
return data return data
}) })
@@ -273,14 +267,15 @@ export default {
this.shelves = shelves this.shelves = shelves
}, },
scan() { scan() {
this.tempIsScanning = true
this.$store this.$store
.dispatch('libraries/requestLibraryScan', { libraryId: this.$store.state.libraries.currentLibraryId }) .dispatch('libraries/requestLibraryScan', { libraryId: this.$store.state.libraries.currentLibraryId })
.then(() => {
this.$toast.success('Library scan started')
})
.catch((error) => { .catch((error) => {
console.error('Failed to start scan', error) console.error('Failed to start scan', error)
this.$toast.error('Failed to start scan') this.$toast.error(this.$strings.ToastLibraryScanFailedToStart)
})
.finally(() => {
this.tempIsScanning = false
}) })
}, },
userUpdated(user) { userUpdated(user) {
@@ -413,6 +408,36 @@ export default {
} }
}) })
}, },
shareOpen(mediaItemShare) {
this.shelves.forEach((shelf) => {
if (shelf.type == 'book') {
shelf.entities = shelf.entities.map((ent) => {
if (ent.media.id === mediaItemShare.mediaItemId) {
return {
...ent,
mediaItemShare
}
}
return ent
})
}
})
},
shareClosed(mediaItemShare) {
this.shelves.forEach((shelf) => {
if (shelf.type == 'book') {
shelf.entities = shelf.entities.map((ent) => {
if (ent.media.id === mediaItemShare.mediaItemId) {
return {
...ent,
mediaItemShare: null
}
}
return ent
})
}
})
},
initListeners() { initListeners() {
if (this.$root.socket) { if (this.$root.socket) {
this.$root.socket.on('user_updated', this.userUpdated) this.$root.socket.on('user_updated', this.userUpdated)
@@ -424,6 +449,8 @@ export default {
this.$root.socket.on('items_updated', this.libraryItemsUpdated) this.$root.socket.on('items_updated', this.libraryItemsUpdated)
this.$root.socket.on('items_added', this.libraryItemsAdded) this.$root.socket.on('items_added', this.libraryItemsAdded)
this.$root.socket.on('episode_added', this.episodeAdded) this.$root.socket.on('episode_added', this.episodeAdded)
this.$root.socket.on('share_open', this.shareOpen)
this.$root.socket.on('share_closed', this.shareClosed)
} else { } else {
console.error('Error socket not initialized') console.error('Error socket not initialized')
} }
@@ -439,6 +466,8 @@ export default {
this.$root.socket.off('items_updated', this.libraryItemsUpdated) this.$root.socket.off('items_updated', this.libraryItemsUpdated)
this.$root.socket.off('items_added', this.libraryItemsAdded) this.$root.socket.off('items_added', this.libraryItemsAdded)
this.$root.socket.off('episode_added', this.episodeAdded) this.$root.socket.off('episode_added', this.episodeAdded)
this.$root.socket.off('share_open', this.shareOpen)
this.$root.socket.off('share_closed', this.shareClosed)
} else { } else {
console.error('Error socket not initialized') console.error('Error socket not initialized')
} }
+23 -44
View File
@@ -1,67 +1,53 @@
<template> <template>
<div class="relative"> <div class="relative">
<div ref="shelf" class="w-full max-w-full bookshelf-row categorizedBookshelfRow relative overflow-x-scroll overflow-y-hidden z-10" :style="{ paddingLeft: paddingLeft * sizeMultiplier + 'rem', height: shelfHeight + 'px' }" @scroll="scrolled"> <div ref="shelf" class="w-full max-w-full bookshelf-row categorizedBookshelfRow relative overflow-x-scroll no-scroll overflow-y-hidden z-10" :style="{ paddingLeft: paddingLeft + 'em' }" @scroll="scrolled">
<div class="w-full h-full pt-6"> <div class="w-full h-full pt-6e">
<div v-if="shelf.type === 'book' || shelf.type === 'podcast'" class="flex items-center"> <div v-if="shelf.type === 'book' || shelf.type === 'podcast'" class="flex items-center">
<template v-for="(entity, index) in shelf.entities"> <template v-for="(entity, index) in shelf.entities">
<cards-lazy-book-card :key="entity.id" :ref="`shelf-book-${entity.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" :continue-listening-shelf="continueListeningShelf" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editItem" /> <cards-lazy-book-card :key="entity.id" :ref="`shelf-book-${entity.id}`" :index="index" :book-mount="entity" :continue-listening-shelf="continueListeningShelf" class="relative mx-2e" @hook:updated="updatedBookCard" @select="selectItem" @edit="editItem" />
</template> </template>
</div> </div>
<div v-if="shelf.type === 'episode'" class="flex items-center"> <div v-if="shelf.type === 'episode'" class="flex items-center">
<template v-for="(entity, index) in shelf.entities"> <template v-for="(entity, index) in shelf.entities">
<cards-lazy-book-card <cards-lazy-book-card :key="entity.recentEpisode.id" :ref="`shelf-episode-${entity.recentEpisode.id}`" :index="index" :book-mount="entity" :continue-listening-shelf="continueListeningShelf" class="relative mx-2e" @hook:updated="updatedBookCard" @select="selectItem" @editPodcast="editItem" @edit="editEpisode" />
:key="entity.recentEpisode.id"
:ref="`shelf-episode-${entity.recentEpisode.id}`"
:index="index"
:width="bookCoverWidth"
:height="bookCoverHeight"
:book-cover-aspect-ratio="bookCoverAspectRatio"
:book-mount="entity"
:continue-listening-shelf="continueListeningShelf"
class="relative mx-2"
@hook:updated="updatedBookCard"
@select="selectItem"
@editPodcast="editItem"
@edit="editEpisode"
/>
</template> </template>
</div> </div>
<div v-if="shelf.type === 'series'" class="flex items-center"> <div v-if="shelf.type === 'series'" class="flex items-center">
<template v-for="entity in shelf.entities"> <template v-for="entity in shelf.entities">
<cards-lazy-series-card :key="entity.name" :series-mount="entity" :height="bookCoverHeight" :width="bookCoverWidth * 2" :book-cover-aspect-ratio="bookCoverAspectRatio" class="relative mx-2" @hook:updated="updatedBookCard" /> <cards-lazy-series-card :key="entity.name" :series-mount="entity" class="relative mx-2e" @hook:updated="updatedBookCard" />
</template> </template>
</div> </div>
<div v-if="shelf.type === 'tags'" class="flex items-center"> <div v-if="shelf.type === 'tags'" class="flex items-center">
<template v-for="entity in shelf.entities"> <template v-for="entity in shelf.entities">
<cards-group-card :key="entity.name" :group="entity" :height="bookCoverHeight" :width="bookCoverWidth * 2" :book-cover-aspect-ratio="bookCoverAspectRatio" class="relative mx-2" @hook:updated="updatedBookCard" /> <cards-group-card :key="entity.name" :group="entity" class="relative mx-2e" @hook:updated="updatedBookCard" />
</template> </template>
</div> </div>
<div v-if="shelf.type === 'authors'" class="flex items-center"> <div v-if="shelf.type === 'authors'" class="flex items-center">
<template v-for="entity in shelf.entities"> <template v-for="entity in shelf.entities">
<cards-author-card :key="entity.id" :width="bookCoverWidth / 1.25" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" @edit="editAuthor" /> <cards-author-card :key="entity.id" :author="entity" @hook:updated="updatedBookCard" class="mx-2e" @edit="editAuthor" />
</template> </template>
</div> </div>
<div v-if="shelf.type === 'narrators'" class="flex items-center"> <div v-if="shelf.type === 'narrators'" class="flex items-center">
<template v-for="entity in shelf.entities"> <template v-for="entity in shelf.entities">
<cards-narrator-card :key="entity.name" :width="150" :height="100" :narrator="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" /> <cards-narrator-card :key="entity.name" :narrator="entity" @hook:updated="updatedBookCard" class="mx-2e" />
</template> </template>
</div> </div>
</div> </div>
</div> </div>
<div class="relative">
<div class="absolute text-center categoryPlacard transform z-30 bottom-px left-4 md:left-8 w-44 rounded-md" style="height: 22px"> <div class="relative text-center categoryPlacard transform z-30 top-0 left-4e md:left-8e w-44e rounded-md">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border"> <div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em 0.5em` }">
<p class="transform text-sm">{{ $strings[shelf.labelStringKey] }}</p> <p :style="{ fontSize: 0.9 + 'em' }">{{ $strings[shelf.labelStringKey] }}</p>
</div>
</div> </div>
</div>
<div class="bookshelfDividerCategorized h-6 w-full absolute bottom-0 left-0 right-0 z-20"></div> <div class="bookshelfDividerCategorized h-6e w-full absolute top-0 left-0 right-0 z-20"></div>
<div v-show="canScrollLeft && !isScrolling" class="hidden sm:flex absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-30" @click="scrollLeft">
<span class="material-icons text-6xl text-white">chevron_left</span>
</div> </div>
<div v-show="canScrollRight && !isScrolling" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-30" @click="scrollRight"> <div v-show="canScrollLeft && !isScrolling" class="hidden sm:flex absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollLeft">
<span class="material-icons text-6xl text-white">chevron_right</span> <span class="material-icons text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_left</span>
</div>
<div v-show="canScrollRight && !isScrolling" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollRight">
<span class="material-icons text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_right</span>
</div> </div>
</div> </div>
</template> </template>
@@ -74,9 +60,6 @@ export default {
type: Object, type: Object,
default: () => {} default: () => {}
}, },
sizeMultiplier: Number,
bookCoverWidth: Number,
bookCoverAspectRatio: Number,
continueListeningShelf: Boolean continueListeningShelf: Boolean
}, },
data() { data() {
@@ -89,12 +72,8 @@ export default {
} }
}, },
computed: { computed: {
bookCoverHeight() { sizeMultiplier() {
return this.bookCoverWidth * this.bookCoverAspectRatio return this.$store.getters['user/getSizeMultiplier']
},
shelfHeight() {
if (this.shelf.type === 'narrators') return 148
return this.bookCoverHeight + 48
}, },
paddingLeft() { paddingLeft() {
if (window.innerWidth < 768) return 1 if (window.innerWidth < 768) return 1
@@ -218,12 +197,12 @@ export default {
} }
.book-shelf-arrow-right { .book-shelf-arrow-right {
height: calc(100% - 24px); height: calc(100% - 1.5em);
background: rgb(48, 48, 48); background: rgb(48, 48, 48);
background: linear-gradient(90deg, rgba(48, 48, 48, 0) 0%, rgba(25, 25, 25, 0.25) 8%, rgba(17, 17, 17, 0.4) 28%, rgba(17, 17, 17, 0.6) 71%, rgba(10, 10, 10, 0.6) 86%, rgba(0, 0, 0, 0.7) 100%); background: linear-gradient(90deg, rgba(48, 48, 48, 0) 0%, rgba(25, 25, 25, 0.25) 8%, rgba(17, 17, 17, 0.4) 28%, rgba(17, 17, 17, 0.6) 71%, rgba(10, 10, 10, 0.6) 86%, rgba(0, 0, 0, 0.7) 100%);
} }
.book-shelf-arrow-left { .book-shelf-arrow-left {
height: calc(100% - 24px); height: calc(100% - 1.5em);
background: rgb(48, 48, 48); background: rgb(48, 48, 48);
background: linear-gradient(-90deg, rgba(48, 48, 48, 0) 0%, rgba(25, 25, 25, 0.25) 8%, rgba(17, 17, 17, 0.4) 28%, rgba(17, 17, 17, 0.6) 71%, rgba(10, 10, 10, 0.6) 86%, rgba(0, 0, 0, 0.7) 100%); background: linear-gradient(-90deg, rgba(48, 48, 48, 0) 0%, rgba(25, 25, 25, 0.25) 8%, rgba(17, 17, 17, 0.4) 28%, rgba(17, 17, 17, 0.6) 71%, rgba(10, 10, 10, 0.6) 86%, rgba(0, 0, 0, 0.7) 100%);
} }
@@ -98,6 +98,9 @@
<template v-else-if="page === 'authors'"> <template v-else-if="page === 'authors'">
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="userCanUpdate && authors && authors.length && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn> <ui-btn v-if="userCanUpdate && authors && authors.length && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
<!-- author sort select -->
<controls-sort-select v-if="authors && authors.length" v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" />
</template> </template>
</div> </div>
</div> </div>
@@ -183,6 +186,30 @@ export default {
} }
] ]
}, },
authorSortItems() {
return [
{
text: this.$strings.LabelAuthorFirstLast,
value: 'name'
},
{
text: this.$strings.LabelAuthorLastFirst,
value: 'lastFirst'
},
{
text: this.$strings.LabelNumberOfBooks,
value: 'numBooks'
},
{
text: this.$strings.LabelAddedAt,
value: 'addedAt'
},
{
text: this.$strings.LabelUpdatedAt,
value: 'updatedAt'
}
]
},
userIsAdminOrUp() { userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp'] return this.$store.getters['user/getIsAdminOrUp']
}, },
@@ -455,6 +482,9 @@ export default {
updateCollapseBookSeries() { updateCollapseBookSeries() {
this.saveSettings() this.saveSettings()
}, },
updateAuthorSort() {
this.saveSettings()
},
saveSettings() { saveSettings() {
this.$store.dispatch('user/updateUserSettings', this.settings) this.$store.dispatch('user/updateUserSettings', this.settings)
}, },
+91 -46
View File
@@ -1,8 +1,8 @@
<template> <template>
<div id="bookshelf" class="w-full overflow-y-auto"> <div id="bookshelf" ref="bookshelf" class="w-full overflow-y-auto" :style="{ fontSize: sizeMultiplier + 'rem' }">
<template v-for="shelf in totalShelves"> <template v-for="shelf in totalShelves">
<div :key="shelf" :id="`shelf-${shelf - 1}`" class="w-full px-4 sm:px-8 relative" :class="{ bookshelfRow: !isAlternativeBookshelfView }" :style="{ height: shelfHeight + 'px' }"> <div :key="shelf" :id="`shelf-${shelf - 1}`" class="w-full px-4e sm:px-8e relative" :class="{ bookshelfRow: !isAlternativeBookshelfView }" :style="{ height: shelfHeight + 'px' }">
<div v-if="!isAlternativeBookshelfView" class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-20" :class="`h-${shelfDividerHeightIndex}`" /> <div v-if="!isAlternativeBookshelfView" class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-20 h-6e" />
</div> </div>
</template> </template>
@@ -10,7 +10,7 @@
<p class="text-center text-2xl mb-4 py-4">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p> <p class="text-center text-2xl mb-4 py-4">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p>
<div v-if="userIsAdminOrUp" class="flex"> <div v-if="userIsAdminOrUp" class="flex">
<ui-btn to="/config" color="primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn> <ui-btn to="/config" color="primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn>
<ui-btn color="success" class="w-52" @click="scan">{{ $strings.ButtonScanLibrary }}</ui-btn> <ui-btn color="success" class="w-52" :loading="isScanningLibrary || tempIsScanning" @click="scan">{{ $strings.ButtonScanLibrary }}</ui-btn>
</div> </div>
</div> </div>
<div v-else-if="!totalShelves && initialized" class="w-full py-16"> <div v-else-if="!totalShelves && initialized" class="w-full py-16">
@@ -49,10 +49,9 @@ export default {
entityIndexesMounted: [], entityIndexesMounted: [],
entityComponentRefs: {}, entityComponentRefs: {},
currentBookWidth: 0, currentBookWidth: 0,
pageLoadQueue: [],
isFetchingEntities: false, isFetchingEntities: false,
scrollTimeout: null, scrollTimeout: null,
booksPerFetch: 100, booksPerFetch: 0,
totalShelves: 0, totalShelves: 0,
bookshelfMarginLeft: 0, bookshelfMarginLeft: 0,
isSelectionMode: false, isSelectionMode: false,
@@ -62,7 +61,11 @@ export default {
currScrollTop: 0, currScrollTop: 0,
resizeTimeout: null, resizeTimeout: null,
mountWindowWidth: 0, mountWindowWidth: 0,
lastItemIndexSelected: -1 lastItemIndexSelected: -1,
tempIsScanning: false,
cardWidth: 0,
cardHeight: 0,
resizeObserver: null
} }
}, },
watch: { watch: {
@@ -159,55 +162,46 @@ export default {
return this.$store.getters['libraries/getCurrentLibraryName'] return this.$store.getters['libraries/getCurrentLibraryName']
}, },
bookWidth() { bookWidth() {
const coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize') return this.cardWidth
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return coverSize * 1.6
return coverSize
}, },
bookHeight() { bookHeight() {
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return this.bookWidth return this.cardHeight
return this.bookWidth * 1.6
}, },
shelfPadding() { shelfPadding() {
if (this.bookshelfWidth < 640) return 32 if (this.bookshelfWidth < 640) return 32 * this.sizeMultiplier
return 64 return 64 * this.sizeMultiplier
}, },
totalPadding() { totalPadding() {
return this.shelfPadding * 2 return this.shelfPadding * 2
}, },
entityWidth() { entityWidth() {
if (this.entityName === 'series' || this.entityName === 'collections') { return this.cardWidth
if (this.bookWidth * 2 > this.bookshelfWidth - this.shelfPadding) return this.bookWidth * 1.6
return this.bookWidth * 2
}
return this.bookWidth
}, },
entityHeight() { entityHeight() {
return this.bookHeight return this.cardHeight
}, },
shelfDividerHeightIndex() { shelfPaddingHeight() {
return 6 return 16
}, },
shelfHeight() { shelfHeight() {
if (this.isAlternativeBookshelfView) { const dividerHeight = this.isAlternativeBookshelfView ? 0 : 24 // h-6
const isItemEntity = this.entityName === 'series-books' || this.entityName === 'items' return this.cardHeight + (this.shelfPaddingHeight + dividerHeight) * this.sizeMultiplier
const extraTitleSpace = isItemEntity ? 80 : this.entityName === 'albums' ? 60 : 40
return this.entityHeight + extraTitleSpace * this.sizeMultiplier
}
return this.entityHeight + 40
}, },
totalEntityCardWidth() { totalEntityCardWidth() {
// Includes margin // Includes margin
return this.entityWidth + 24 return this.entityWidth + 24 * this.sizeMultiplier
}, },
selectedMediaItems() { selectedMediaItems() {
return this.$store.state.globals.selectedMediaItems || [] return this.$store.state.globals.selectedMediaItems || []
}, },
sizeMultiplier() { sizeMultiplier() {
const baseSize = this.isCoverSquareAspectRatio ? 192 : 120 return this.$store.getters['user/getSizeMultiplier']
return this.entityWidth / baseSize
}, },
streamLibraryItem() { streamLibraryItem() {
return this.$store.state.streamLibraryItem return this.$store.state.streamLibraryItem
},
isScanningLibrary() {
return !!this.$store.getters['tasks/getRunningLibraryScanTask'](this.currentLibraryId)
} }
}, },
methods: { methods: {
@@ -318,7 +312,7 @@ export default {
let entityPath = this.entityName === 'series-books' ? 'items' : this.entityName let entityPath = this.entityName === 'series-books' ? 'items' : this.entityName
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : '' const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&include=rssfeed,numEpisodesIncomplete` const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&include=rssfeed,numEpisodesIncomplete,share`
const payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => { const payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
console.error('failed to fetch items', error) console.error('failed to fetch items', error)
@@ -432,10 +426,14 @@ export default {
rebuild() { rebuild() {
this.initSizeData() this.initSizeData()
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf) var lastBookIndex = Math.min(this.totalEntities, this.booksPerFetch)
this.entityIndexesMounted = [] this.entityIndexesMounted = []
for (let i = 0; i < lastBookIndex; i++) { for (let i = 0; i < lastBookIndex; i++) {
this.entityIndexesMounted.push(i) this.entityIndexesMounted.push(i)
if (!this.entities[i]) {
const page = Math.floor(i / this.booksPerFetch)
this.loadPage(page)
}
} }
var bookshelfEl = document.getElementById('bookshelf') var bookshelfEl = document.getElementById('bookshelf')
if (bookshelfEl) { if (bookshelfEl) {
@@ -497,7 +495,8 @@ export default {
this.resetEntities() this.resetEntities()
} }
}, },
settingsUpdated(settings) { async settingsUpdated(settings) {
await this.cardsHelpers.setCardSize()
const wasUpdated = this.checkUpdateSearchParams() const wasUpdated = this.checkUpdateSearchParams()
if (wasUpdated) { if (wasUpdated) {
this.resetEntities() this.resetEntities()
@@ -602,6 +601,44 @@ export default {
this.executeRebuild() this.executeRebuild()
} }
}, },
shareOpen(mediaItemShare) {
if (this.entityName === 'items' || this.entityName === 'series-books') {
var indexOf = this.entities.findIndex((ent) => ent?.media?.id === mediaItemShare.mediaItemId)
if (indexOf >= 0) {
if (this.entityComponentRefs[indexOf]) {
const libraryItem = { ...this.entityComponentRefs[indexOf].libraryItem }
libraryItem.mediaItemShare = mediaItemShare
this.entityComponentRefs[indexOf].setEntity?.(libraryItem)
}
}
}
},
shareClosed(mediaItemShare) {
if (this.entityName === 'items' || this.entityName === 'series-books') {
var indexOf = this.entities.findIndex((ent) => ent?.media?.id === mediaItemShare.mediaItemId)
if (indexOf >= 0) {
if (this.entityComponentRefs[indexOf]) {
const libraryItem = { ...this.entityComponentRefs[indexOf].libraryItem }
libraryItem.mediaItemShare = null
this.entityComponentRefs[indexOf].setEntity?.(libraryItem)
}
}
}
},
updatePagesLoaded() {
let numPages = Math.ceil(this.totalEntities / this.booksPerFetch)
for (let page = 0; page < numPages; page++) {
let numEntities = Math.min(this.totalEntities - page * this.booksPerFetch, this.booksPerFetch)
this.pagesLoaded[page] = true
for (let i = 0; i < numEntities; i++) {
const index = page * this.booksPerFetch + i
if (!this.entities[index]) {
this.pagesLoaded[page] = false
break
}
}
}
},
initSizeData(_bookshelf) { initSizeData(_bookshelf) {
var bookshelf = _bookshelf || document.getElementById('bookshelf') var bookshelf = _bookshelf || document.getElementById('bookshelf')
if (!bookshelf) { if (!bookshelf) {
@@ -618,6 +655,13 @@ export default {
this.entitiesPerShelf = Math.max(1, Math.floor((this.bookshelfWidth - this.shelfPadding) / this.totalEntityCardWidth)) this.entitiesPerShelf = Math.max(1, Math.floor((this.bookshelfWidth - this.shelfPadding) / this.totalEntityCardWidth))
this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2 this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2
this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2 this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2
const booksPerFetch = this.entitiesPerShelf * this.shelvesPerPage
if (booksPerFetch !== this.booksPerFetch) {
this.booksPerFetch = booksPerFetch
if (this.totalEntities) {
this.updatePagesLoaded()
}
}
this.currentBookWidth = this.bookWidth this.currentBookWidth = this.bookWidth
if (this.totalEntities) { if (this.totalEntities) {
@@ -626,13 +670,8 @@ export default {
return entitiesPerShelfBefore < this.entitiesPerShelf // Books per shelf has changed return entitiesPerShelfBefore < this.entitiesPerShelf // Books per shelf has changed
}, },
async init(bookshelf) { async init(bookshelf) {
if (this.entityName === 'series') {
this.booksPerFetch = 50
} else {
this.booksPerFetch = 100
}
this.checkUpdateSearchParams()
this.initSizeData(bookshelf) this.initSizeData(bookshelf)
this.checkUpdateSearchParams()
this.pagesLoaded[0] = true this.pagesLoaded[0] = true
await this.fetchEntites(0) await this.fetchEntites(0)
@@ -688,6 +727,8 @@ export default {
this.$root.socket.on('playlist_added', this.playlistAdded) this.$root.socket.on('playlist_added', this.playlistAdded)
this.$root.socket.on('playlist_updated', this.playlistUpdated) this.$root.socket.on('playlist_updated', this.playlistUpdated)
this.$root.socket.on('playlist_removed', this.playlistRemoved) this.$root.socket.on('playlist_removed', this.playlistRemoved)
this.$root.socket.on('share_open', this.shareOpen)
this.$root.socket.on('share_closed', this.shareClosed)
} else { } else {
console.error('Bookshelf - Socket not initialized') console.error('Bookshelf - Socket not initialized')
} }
@@ -715,6 +756,8 @@ export default {
this.$root.socket.off('playlist_added', this.playlistAdded) this.$root.socket.off('playlist_added', this.playlistAdded)
this.$root.socket.off('playlist_updated', this.playlistUpdated) this.$root.socket.off('playlist_updated', this.playlistUpdated)
this.$root.socket.off('playlist_removed', this.playlistRemoved) this.$root.socket.off('playlist_removed', this.playlistRemoved)
this.$root.socket.off('share_open', this.shareOpen)
this.$root.socket.off('share_closed', this.shareClosed)
} else { } else {
console.error('Bookshelf - Socket not initialized') console.error('Bookshelf - Socket not initialized')
} }
@@ -727,18 +770,20 @@ export default {
} }
}, },
scan() { scan() {
this.tempIsScanning = true
this.$store this.$store
.dispatch('libraries/requestLibraryScan', { libraryId: this.currentLibraryId }) .dispatch('libraries/requestLibraryScan', { libraryId: this.currentLibraryId })
.then(() => {
this.$toast.success('Library scan started')
})
.catch((error) => { .catch((error) => {
console.error('Failed to start scan', error) console.error('Failed to start scan', error)
this.$toast.error('Failed to start scan') this.$toast.error(this.$strings.ToastLibraryScanFailedToStart)
})
.finally(() => {
this.tempIsScanning = false
}) })
} }
}, },
mounted() { async mounted() {
await this.cardsHelpers.setCardSize()
this.initListeners() this.initListeners()
this.routeFullPath = window.location.pathname + (window.location.search || '') this.routeFullPath = window.location.pathname + (window.location.search || '')
@@ -773,6 +818,6 @@ export default {
.bookshelfDivider { .bookshelfDivider {
background: rgb(149, 119, 90); background: rgb(149, 119, 90);
background: var(--bookshelf-divider-bg); background: var(--bookshelf-divider-bg);
box-shadow: 2px 14px 8px #111111aa; box-shadow: 0.125em 0.875em 0.5em #111111aa;
} }
</style> </style>
@@ -1,25 +1,25 @@
<template> <template>
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 md:h-40 z-50 bg-primary px-2 md:px-4 pb-1 md:pb-4 pt-2"> <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 id="videoDock" />
<div class="absolute left-2 top-2 md:left-4 cursor-pointer"> <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" /> <covers-book-cover expand-on-click :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
</div> </div>
<div class="flex items-start mb-6 md: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="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
<div class="min-w-0"> <div class="min-w-0 w-full">
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate"> <div class="flex items-center">
{{ title }} <nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
</nuxt-link> {{ title }}
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center"> </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">
<span class="material-icons text-sm">person</span> <span class="material-icons text-sm">person</span>
<div class="flex items-center"> <div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
<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="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">
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base"> <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>
<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>
<div v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</div>
<widgets-explicit-indicator :explicit="isExplicit"></widgets-explicit-indicator>
</div> </div>
<div v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</div>
</div> </div>
<div class="text-gray-400 flex items-center"> <div class="text-gray-400 flex items-center">
@@ -29,7 +29,7 @@
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-tooltip direction="top" :text="$strings.LabelClosePlayer"> <ui-tooltip direction="top" :text="$strings.LabelClosePlayer">
<span class="material-icons sm:px-2 py-1 md:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</span> <button :aria-label="$strings.LabelClosePlayer" class="material-icons sm:px-2 py-1 lg:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</button>
</ui-tooltip> </ui-tooltip>
</div> </div>
<player-ui <player-ui
@@ -82,13 +82,11 @@ export default {
sleepTimer: null, sleepTimer: null,
displayTitle: null, displayTitle: null,
currentPlaybackRate: 1, currentPlaybackRate: 1,
syncFailedToast: null syncFailedToast: null,
coverAspectRatio: 1
} }
}, },
computed: { computed: {
coverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
isSquareCover() { isSquareCover() {
return this.coverAspectRatio === 1 return this.coverAspectRatio === 1
}, },
@@ -138,7 +136,7 @@ export default {
return this.streamLibraryItem?.mediaType === 'music' return this.streamLibraryItem?.mediaType === 'music'
}, },
isExplicit() { isExplicit() {
return this.mediaMetadata.explicit || false return !!this.mediaMetadata.explicit
}, },
mediaMetadata() { mediaMetadata() {
return this.media.metadata || {} return this.media.metadata || {}
@@ -380,7 +378,7 @@ export default {
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === data.stream) { if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === data.stream) {
if (!data.numSegments) return if (!data.numSegments) return
var chunks = data.chunks var chunks = data.chunks
console.log(`[StreamContainer] Stream Progress ${data.percent}`) console.log(`[MediaPlayerContainer] Stream Progress ${data.percent}`)
if (this.$refs.audioPlayer) { if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments) this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
} else { } else {
@@ -397,17 +395,17 @@ export default {
this.playerHandler.prepareOpenSession(session, this.currentPlaybackRate) this.playerHandler.prepareOpenSession(session, this.currentPlaybackRate)
}, },
streamOpen(session) { streamOpen(session) {
console.log(`[StreamContainer] Stream session open`, session) console.log(`[MediaPlayerContainer] Stream session open`, session)
}, },
streamClosed(streamId) { streamClosed(streamId) {
// Stream was closed from the server // Stream was closed from the server
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) { if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) {
console.warn('[StreamContainer] Closing stream due to request from server') console.warn('[MediaPlayerContainer] Closing stream due to request from server')
this.playerHandler.closePlayer() this.playerHandler.closePlayer()
} }
}, },
streamReady() { streamReady() {
console.log(`[StreamContainer] Stream Ready`) console.log(`[MediaPlayerContainer] Stream Ready`)
if (this.$refs.audioPlayer) { if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.setStreamReady() this.$refs.audioPlayer.setStreamReady()
} else { } else {
@@ -417,7 +415,7 @@ export default {
streamError(streamId) { streamError(streamId) {
// Stream had critical error from the server // Stream had critical error from the server
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) { if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) {
console.warn('[StreamContainer] Closing stream due to stream error from server') console.warn('[MediaPlayerContainer] Closing stream due to stream error from server')
this.playerHandler.closePlayer() this.playerHandler.closePlayer()
} }
}, },
@@ -457,6 +455,9 @@ export default {
episodeId, episodeId,
queueItems: payload.queueItems || [] queueItems: payload.queueItems || []
}) })
// Set cover aspect ratio for this item's library since the library may change
this.coverAspectRatio = this.$store.getters['libraries/getBookCoverAspectRatio']
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.audioPlayer) this.$refs.audioPlayer.checkUpdateChapterTrack() if (this.$refs.audioPlayer) this.$refs.audioPlayer.checkUpdateChapterTrack()
}) })
@@ -496,7 +497,7 @@ export default {
</script> </script>
<style> <style>
#streamContainer { #mediaPlayerContainer {
box-shadow: 0px -6px 8px #1111113f; box-shadow: 0px -6px 8px #1111113f;
} }
</style> </style>
+2 -1
View File
@@ -1,6 +1,7 @@
<template> <template>
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8"> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-2 sm:p-4 mb-8">
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<slot name="header-prefix"></slot>
<h1 class="text-xl">{{ headerText }}</h1> <h1 class="text-xl">{{ headerText }}</h1>
<slot name="header-items"></slot> <slot name="header-items"></slot>
+1 -4
View File
@@ -121,7 +121,7 @@
<p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p> <p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p>
</div> </div>
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version" /> <modals-changelog-view-modal v-model="showChangelogModal" :versionData="versionData" />
</div> </div>
</template> </template>
@@ -219,9 +219,6 @@ export default {
githubTagUrl() { githubTagUrl() {
return this.versionData.githubTagUrl return this.versionData.githubTagUrl
}, },
currentVersionChangelog() {
return this.versionData.currentVersionChangelog || 'No Changelog Available'
},
streamLibraryItem() { streamLibraryItem() {
return this.$store.state.streamLibraryItem return this.$store.state.streamLibraryItem
}, },
+46 -33
View File
@@ -1,38 +1,40 @@
<template> <template>
<nuxt-link :to="`/author/${author.id}`"> <div :style="{ minWidth: cardWidth + 'px', maxWidth: cardWidth + 'px' }">
<div @mouseover="mouseover" @mouseleave="mouseleave"> <nuxt-link :to="`/author/${author.id}`">
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden"> <div cy-id="card" @mouseover="mouseover" @mouseleave="mouseleave">
<!-- Image or placeholder --> <div cy-id="imageArea" :style="{ height: cardHeight + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
<covers-author-image :author="author" /> <!-- Image or placeholder -->
<covers-author-image :author="author" />
<!-- Author name & num books overlay --> <!-- Author name & num books overlay -->
<div v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2"> <div cy-id="textInline" v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1e bg-black bg-opacity-60 px-2e">
<p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p> <p class="text-center font-semibold truncate" :style="{ fontSize: 0.75 + 'em' }">{{ name }}</p>
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} {{ $strings.LabelBooks }}</p> <p class="text-center text-gray-200" :style="{ fontSize: 0.65 + 'em' }">{{ numBooks }} {{ $strings.LabelBooks }}</p>
</div> </div>
<!-- Search icon btn --> <!-- Search icon btn -->
<div v-show="!searching && isHovering && userCanUpdate" class="absolute top-0 left-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="searchAuthor"> <div cy-id="match" v-show="!searching && isHovering && userCanUpdate" class="absolute top-0 left-0 p-2e cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="searchAuthor">
<ui-tooltip :text="$strings.ButtonQuickMatch" direction="bottom"> <ui-tooltip :text="$strings.ButtonQuickMatch" direction="bottom">
<span class="material-icons text-lg">search</span> <span class="material-icons" :style="{ fontSize: 1.125 + 'em' }">search</span>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div v-show="isHovering && !searching && userCanUpdate" class="absolute top-0 right-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="$emit('edit', author)"> <div cy-id="edit" v-show="isHovering && !searching && userCanUpdate" class="absolute top-0 right-0 p-2e cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="$emit('edit', author)">
<ui-tooltip :text="$strings.LabelEdit" direction="bottom"> <ui-tooltip :text="$strings.LabelEdit" direction="bottom">
<span class="material-icons text-lg">edit</span> <span class="material-icons" :style="{ fontSize: 1.125 + 'em' }">edit</span>
</ui-tooltip> </ui-tooltip>
</div> </div>
<!-- Loading spinner --> <!-- Loading spinner -->
<div v-show="searching" class="absolute top-0 left-0 z-10 w-full h-full bg-black bg-opacity-50 flex items-center justify-center"> <div cy-id="spinner" v-show="searching" class="absolute top-0 left-0 z-10 w-full h-full bg-black bg-opacity-50 flex items-center justify-center">
<widgets-loading-spinner size="" /> <widgets-loading-spinner size="" />
</div>
</div>
<div cy-id="nameBelow" v-show="nameBelow" class="w-full py-1e px-2e">
<p class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: 0.75 + 'em' }">{{ name }}</p>
</div> </div>
</div> </div>
<div v-show="nameBelow" class="w-full py-1 px-2"> </nuxt-link>
<p class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p> </div>
</div>
</div>
</nuxt-link>
</template> </template>
<script> <script>
@@ -43,12 +45,14 @@ export default {
default: () => {} default: () => {}
}, },
width: Number, width: Number,
height: Number, height: {
sizeMultiplier: {
type: Number, type: Number,
default: 1 default: 192
}, },
nameBelow: Boolean nameBelow: {
type: Boolean,
default: false
}
}, },
data() { data() {
return { return {
@@ -57,6 +61,12 @@ export default {
} }
}, },
computed: { computed: {
cardWidth() {
return this.width || this.cardHeight * 0.8
},
cardHeight() {
return this.height * this.sizeMultiplier
},
userToken() { userToken() {
return this.$store.getters['user/getToken'] return this.$store.getters['user/getToken']
}, },
@@ -83,6 +93,9 @@ export default {
}, },
libraryProvider() { libraryProvider() {
return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google' return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
},
sizeMultiplier() {
return this.$store.getters['user/getSizeMultiplier']
} }
}, },
methods: { methods: {
+8 -8
View File
@@ -13,9 +13,9 @@
<div class="flex-grow" /> <div class="flex-grow" />
<p class="text-sm md:text-base">{{ book.publishedYear }}</p> <p class="text-sm md:text-base">{{ book.publishedYear }}</p>
</div> </div>
<p v-if="book.author" class="text-gray-300 text-xs md:text-sm">by {{ book.author }}</p> <p v-if="book.author" class="text-gray-300 text-xs md:text-sm">{{ $getString('LabelByAuthor', [book.author]) }}</p>
<p v-if="book.narrator" class="text-gray-400 text-xs">Narrated by {{ book.narrator }}</p> <p v-if="book.narrator" class="text-gray-400 text-xs">{{ $strings.LabelNarrators }}: {{ book.narrator }}</p>
<p v-if="book.duration" class="text-gray-400 text-xs">Runtime: {{ $elapsedPrettyExtended(bookDuration, false) }} {{ bookDurationComparison }}</p> <p v-if="book.duration" class="text-gray-400 text-xs">{{ $strings.LabelDuration }}: {{ $elapsedPrettyExtended(bookDuration, false) }} {{ bookDurationComparison }}</p>
<div v-if="book.series?.length" class="flex py-1 -mx-1"> <div v-if="book.series?.length" class="flex py-1 -mx-1">
<div v-for="(series, index) in book.series" :key="index" class="bg-white bg-opacity-10 rounded-full px-1 py-0.5 mx-1"> <div v-for="(series, index) in book.series" :key="index" class="bg-white bg-opacity-10 rounded-full px-1 py-0.5 mx-1">
<p class="leading-3 text-xs text-gray-400"> <p class="leading-3 text-xs text-gray-400">
@@ -29,9 +29,9 @@
</div> </div>
<div v-else class="px-4 flex-grow"> <div v-else class="px-4 flex-grow">
<h1> <h1>
<div class="flex items-center">{{ book.title }}<widgets-explicit-indicator :explicit="book.explicit" /></div> <div class="flex items-center">{{ book.title }}<widgets-explicit-indicator v-if="book.explicit" /></div>
</h1> </h1>
<p class="text-base text-gray-300 whitespace-nowrap truncate">by {{ book.author }}</p> <p class="text-base text-gray-300 whitespace-nowrap truncate">{{ $getString('LabelByAuthor', [book.author]) }}</p>
<p v-if="book.genres" class="text-xs text-gray-400 leading-5">{{ book.genres.join(', ') }}</p> <p v-if="book.genres" class="text-xs text-gray-400 leading-5">{{ book.genres.join(', ') }}</p>
<p class="text-xs text-gray-400 leading-5">{{ book.trackCount }} Episodes</p> <p class="text-xs text-gray-400 leading-5">{{ book.trackCount }} Episodes</p>
</div> </div>
@@ -75,11 +75,11 @@ export default {
let differenceInMinutes = currentBookDurationMinutes - this.book.duration let differenceInMinutes = currentBookDurationMinutes - this.book.duration
if (differenceInMinutes < 0) { if (differenceInMinutes < 0) {
differenceInMinutes = Math.abs(differenceInMinutes) differenceInMinutes = Math.abs(differenceInMinutes)
return `(${this.$elapsedPrettyExtended(differenceInMinutes * 60, false, false)} shorter)` return this.$getString('LabelDurationComparisonLonger', [this.$elapsedPrettyExtended(differenceInMinutes * 60, false, false)])
} else if (differenceInMinutes > 0) { } else if (differenceInMinutes > 0) {
return `(${this.$elapsedPrettyExtended(differenceInMinutes * 60, false, false)} longer)` return this.$getString('LabelDurationComparisonShorter', [this.$elapsedPrettyExtended(differenceInMinutes * 60, false, false)])
} }
return '(exact match)' return this.$strings.LabelDurationComparisonExactMatch
} }
}, },
methods: { methods: {
+17 -7
View File
@@ -1,15 +1,15 @@
<template> <template>
<div class="relative"> <div class="relative">
<div class="rounded-sm h-full relative" :style="{ width: width + 'px', height: height + 'px' }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard"> <div class="rounded-sm h-full relative" :style="{ width: cardWidth + 'px', height: cardHeight + 'px' }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard">
<nuxt-link :to="groupTo" class="cursor-pointer"> <nuxt-link :to="groupTo" class="cursor-pointer">
<div class="w-full h-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'"> <div class="w-full h-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'">
<covers-group-cover ref="groupcover" :id="groupEncode" :name="groupName" :type="groupType" :book-items="bookItems" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-group-cover ref="groupcover" :id="groupEncode" :name="groupName" :type="groupType" :book-items="bookItems" :width="cardWidth" :height="cardHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }"> <div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ groupName }}</p> <p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ groupName }}</p>
</div> </div>
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ bookItems.length }}</div> <div class="absolute z-10 top-1.5e right-1.5e rounded-md leading-3e p-1e font-semibold text-white flex items-center justify-center" :style="{ fontSize: 0.8 + 'em' }" style="background-color: #cd9d49dd">{{ bookItems.length }}</div>
</div> </div>
</nuxt-link> </nuxt-link>
</div> </div>
@@ -24,8 +24,10 @@ export default {
default: () => null default: () => null
}, },
width: Number, width: Number,
height: Number, height: {
bookCoverAspectRatio: Number type: Number,
default: 192
}
}, },
data() { data() {
return { return {
@@ -33,6 +35,15 @@ export default {
} }
}, },
computed: { computed: {
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
cardWidth() {
return this.width || this.cardHeight * 2
},
cardHeight() {
return this.height * this.sizeMultiplier
},
currentLibraryId() { currentLibraryId() {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
}, },
@@ -46,8 +57,7 @@ export default {
return `/library/${this.currentLibraryId}/bookshelf?filter=${this.filter}` return `/library/${this.currentLibraryId}/bookshelf?filter=${this.filter}`
}, },
sizeMultiplier() { sizeMultiplier() {
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2) return this.$store.getters['user/getSizeMultiplier']
return this.width / 240
}, },
bookItems() { bookItems() {
return this._group.books || [] return this._group.books || []
+2 -2
View File
@@ -7,7 +7,7 @@
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-gray-300" v-html="matchHtml" /> <p v-if="matchKey === 'subtitle'" class="truncate text-xs text-gray-300" v-html="matchHtml" />
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p> <p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">{{ $getString('LabelByAuthor', [authorName]) }}</p>
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" /> <p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin' || matchKey === 'episode' || matchKey === 'narrators'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" /> <div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin' || matchKey === 'episode' || matchKey === 'narrators'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
@@ -69,7 +69,7 @@ export default {
if (this.matchKey === 'episode') return `<p class="truncate">${this.$strings.LabelEpisode}: ${html}</p>` if (this.matchKey === 'episode') return `<p class="truncate">${this.$strings.LabelEpisode}: ${html}</p>`
if (this.matchKey === 'tags') return `<p class="truncate">${this.$strings.LabelTags}: ${html}</p>` if (this.matchKey === 'tags') return `<p class="truncate">${this.$strings.LabelTags}: ${html}</p>`
if (this.matchKey === 'subtitle') return `<p class="truncate">${html}</p>` if (this.matchKey === 'subtitle') return `<p class="truncate">${html}</p>`
if (this.matchKey === 'authors') return `by ${html}` if (this.matchKey === 'authors') this.$getString('LabelByAuthor', [html])
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>` if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>` if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
if (this.matchKey === 'series') return `<p class="truncate">${this.$strings.LabelSeries}: ${html}</p>` if (this.matchKey === 'series') return `<p class="truncate">${this.$strings.LabelSeries}: ${html}</p>`
+13 -9
View File
@@ -21,15 +21,16 @@
<div v-if="!isPodcast" class="flex items-end"> <div v-if="!isPodcast" class="flex items-end">
<ui-text-input-with-label v-model.trim="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" /> <ui-text-input-with-label v-model.trim="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
<ui-tooltip :text="$strings.LabelUploaderItemFetchMetadataHelp"> <ui-tooltip :text="$strings.LabelUploaderItemFetchMetadataHelp">
<div <div class="ml-2 mb-1 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-primary cursor-pointer" @click="fetchMetadata">
class="ml-2 mb-1 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-primary cursor-pointer"
@click="fetchMetadata">
<span class="text-base text-white text-opacity-80 font-mono material-icons">sync</span> <span class="text-base text-white text-opacity-80 font-mono material-icons">sync</span>
</div> </div>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div v-else class="w-full"> <div v-else class="w-full">
<p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p> <p class="px-1 text-sm font-semibold">
{{ $strings.LabelDirectory }}
<em class="font-normal text-xs pl-2">(auto)</em>
</p>
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs" /> <ui-text-input :value="directory" disabled class="w-full font-mono text-xs" />
</div> </div>
</div> </div>
@@ -40,7 +41,10 @@
</div> </div>
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
<div class="w-full"> <div class="w-full">
<label class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></label> <label class="px-1 text-sm font-semibold">
{{ $strings.LabelDirectory }}
<em class="font-normal text-xs pl-2">(auto)</em>
</label>
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs h-10" /> <ui-text-input :value="directory" disabled class="w-full font-mono text-xs h-10" />
</div> </div>
</div> </div>
@@ -51,10 +55,10 @@
<tables-uploaded-files-table v-if="item.ignoredFiles.length" :title="$strings.HeaderIgnoredFiles" :files="item.ignoredFiles" /> <tables-uploaded-files-table v-if="item.ignoredFiles.length" :title="$strings.HeaderIgnoredFiles" :files="item.ignoredFiles" />
</template> </template>
<widgets-alert v-if="uploadSuccess" type="success"> <widgets-alert v-if="uploadSuccess" type="success">
<p class="text-base">{{ $strings.MessageUploaderItemSuccess }}</p> <p class="text-base">"{{ itemData.title }}" {{ $strings.MessageUploaderItemSuccess }}</p>
</widgets-alert> </widgets-alert>
<widgets-alert v-if="uploadFailed" type="error"> <widgets-alert v-if="uploadFailed" type="error">
<p class="text-base">{{ $strings.MessageUploaderItemFailed }}</p> <p class="text-base">"{{ itemData.title }}" {{ $strings.MessageUploaderItemFailed }}</p>
</widgets-alert> </widgets-alert>
<div v-if="isNonInteractable" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20"> <div v-if="isNonInteractable" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20">
@@ -70,7 +74,7 @@ export default {
props: { props: {
item: { item: {
type: Object, type: Object,
default: () => { } default: () => {}
}, },
mediaType: String, mediaType: String,
processing: Boolean, processing: Boolean,
@@ -99,7 +103,7 @@ export default {
if (this.isPodcast) return this.itemData.title if (this.isPodcast) return this.itemData.title
const outputPathParts = [this.itemData.author, this.itemData.series, this.itemData.title] const outputPathParts = [this.itemData.author, this.itemData.series, this.itemData.title]
const cleanedOutputPathParts = outputPathParts.filter(Boolean).map(part => this.$sanitizeFilename(part)) const cleanedOutputPathParts = outputPathParts.filter(Boolean).map((part) => this.$sanitizeFilename(part))
return Path.join(...cleanedOutputPathParts) return Path.join(...cleanedOutputPathParts)
}, },
+45 -17
View File
@@ -1,18 +1,22 @@
<template> <template>
<div ref="card" :id="`album-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard"> <div ref="card" :id="`album-card-${index}`" :style="{ width: cardWidth + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" /> <div class="relative" :style="{ height: coverHeight + 'px' }">
<div class="w-full h-full bg-primary relative rounded overflow-hidden"> <div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<covers-preview-cover ref="cover" :src="coverSrc" :width="width" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <div class="w-full h-full bg-primary relative rounded overflow-hidden">
</div> <covers-preview-cover ref="cover" :src="coverSrc" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div> </div>
</div> </div>
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p> <div class="relative w-full">
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ artist || '&nbsp;' }}</p> <div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6e h-6e rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em ${0.5}em` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
</div>
</div>
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8e h-8e py-1e rounded-md text-center">
<p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ artist || '&nbsp;' }}</p>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -22,8 +26,10 @@ export default {
props: { props: {
index: Number, index: Number,
width: Number, width: Number,
height: Number, height: {
bookCoverAspectRatio: Number, type: Number,
default: 192
},
bookshelfView: { bookshelfView: {
type: Number, type: Number,
default: 0 default: 0
@@ -42,6 +48,29 @@ export default {
} }
}, },
computed: { computed: {
bookCoverAspectRatio() {
return this.store.getters['libraries/getBookCoverAspectRatio']
},
cardWidth() {
return this.width || this.coverHeight
},
coverHeight() {
return this.height * this.sizeMultiplier
},
/*
cardHeight() {
return this.coverHeight + this.bottomTextHeight
},
bottomTextHeight() {
if (!this.isAlternativeBookshelfView) return 0
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
},
*/
coverSrc() { coverSrc() {
const config = this.$config || this.$nuxt.$config const config = this.$config || this.$nuxt.$config
if (!this.album || !this.album.libraryItemId) return `${config.routerBasePath}/book_placeholder.jpg` if (!this.album || !this.album.libraryItemId) return `${config.routerBasePath}/book_placeholder.jpg`
@@ -49,11 +78,10 @@ export default {
}, },
labelFontSize() { labelFontSize() {
if (this.width < 160) return 0.75 if (this.width < 160) return 0.75
return 0.875 return 0.9
}, },
sizeMultiplier() { sizeMultiplier() {
const baseSize = this.bookCoverAspectRatio === 1 ? 192 : 120 return this.store.getters['user/getSizeMultiplier']
return this.width / baseSize
}, },
title() { title() {
return this.album ? this.album.title : '' return this.album ? this.album.title : ''
+201 -160
View File
@@ -1,129 +1,139 @@
<template> <template>
<div ref="card" :id="`book-card-${index}`" :style="{ minWidth: width + 'px', maxWidth: width + 'px', height: height + 'px' }" class="rounded-sm z-10 bg-primary cursor-pointer box-shadow-book" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard"> <div ref="card" :id="`book-card-${index}`" :style="{ minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }" class="absolute rounded-sm z-10 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<!-- When cover image does not fill --> <div :id="`cover-area-${index}`" class="relative w-full top-0 left-0 rounded overflow-hidden z-10 bg-primary box-shadow-book" :style="{ height: coverHeight + 'px ' }">
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary"> <!-- When cover image does not fill -->
<div class="absolute cover-bg" ref="coverBg" /> <div cy-id="coverBg" v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
<div class="absolute cover-bg" ref="coverBg" />
</div>
<div cy-id="seriesSequenceList" v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #78350f">
<p :style="{ fontSize: 0.8 + 'em' }">#{{ seriesSequenceList }}</p>
</div>
<div cy-id="booksInSeries" v-else-if="booksInSeries" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #cd9d49dd">
<p :style="{ fontSize: 0.8 + 'em' }">{{ booksInSeries }}</p>
</div>
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
<div cy-id="titleImageNotReady" v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: 0.5 + 'em' }">
<p :style="{ fontSize: 0.8 + 'em' }" class="text-gray-300 text-center">{{ title }}</p>
</div>
<!-- Cover Image -->
<img cy-id="coverImage" v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="relative w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
<!-- Placeholder Cover Title & Author -->
<div cy-id="placeholderTitle" v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'em' }">
<div>
<p cy-id="placeholderTitleText" class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'em' }">{{ titleCleaned }}</p>
</div>
</div>
<div cy-id="placeholderAuthor" v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'em', bottom: authorBottom + 'em' }">
<p cy-id="placeholderAuthorText" class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'em' }">{{ authorCleaned }}</p>
</div>
<div v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }" style="background-color: #78350f">
<p :style="{ fontSize: 0.8 + 'em' }">#{{ seriesSequenceList }}</p>
</div>
<div v-else-if="booksInSeries" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }" style="background-color: #cd9d49dd">
<p :style="{ fontSize: 0.8 + 'em' }">{{ booksInSeries }}</p>
</div>
<!-- No progress shown for podcasts (unless showing podcast episode) -->
<div cy-id="progressBar" v-if="!isPodcast || episodeProgress" class="absolute bottom-0 left-0 h-1e shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * userProgressPercent + 'px' }"></div>
<!-- Overlay is not shown if collapsing series in library -->
<div cy-id="overlay" v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen) && !processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded md:block" :class="overlayWrapperClasslist">
<div cy-id="playButton" v-show="showPlayButton" class="h-full flex items-center justify-center pointer-events-none">
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="play">
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'em' }">play_circle_filled</span>
</div>
</div>
<div cy-id="readButton" v-show="showReadButton" class="h-full flex items-center justify-center pointer-events-none">
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="clickReadEBook">
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'em' }">auto_stories</span>
</div>
</div>
<div cy-id="editButton" v-if="userCanUpdate" v-show="!isSelectionMode" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-150 top-0 right-0" :style="{ padding: 0.375 + 'em' }" @click.stop.prevent="editClick">
<span class="material-icons" :style="{ fontSize: 1 + 'em' }">edit</span>
</div>
<!-- Radio button -->
<div cy-id="selectedRadioButton" v-if="!isAuthorBookshelfView" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 + 'em', left: 0.375 + 'em' }" @click.stop.prevent="selectBtnClick">
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 + 'em' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
</div>
<!-- More Menu Icon -->
<div cy-id="moreButton" ref="moreIcon" v-show="!isSelectionMode && moreMenuItems.length" class="md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-150" :style="{ bottom: 0.375 + 'em', right: 0.375 + 'em' }" @click.stop.prevent="clickShowMore">
<span class="material-icons" :style="{ fontSize: 1.2 + 'em' }">more_vert</span>
</div>
<div cy-id="ebookFormat" v-if="ebookFormat" class="absolute" :style="{ bottom: 0.375 + 'em', left: 0.375 + 'em' }">
<span class="text-white/80" :style="{ fontSize: 0.8 + 'em' }">{{ ebookFormat }}</span>
</div>
</div>
<!-- Processing/loading spinner overlay -->
<div cy-id="loadingSpinner" v-if="processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 rounded flex items-center justify-center">
<widgets-loading-spinner size="la-lg" />
</div>
<!-- Series name overlay -->
<div cy-id="seriesNameOverlay" v-if="booksInSeries && libraryItem && isHovering" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-60 rounded flex items-center justify-center" :style="{ padding: 1 + 'em' }">
<p v-if="seriesName" class="text-gray-200 text-center" :style="{ fontSize: 1.1 + 'em' }">{{ seriesName }}</p>
</div>
<!-- Error widget -->
<ui-tooltip cy-id="ErrorTooltip" v-if="showError" :text="errorText" class="absolute bottom-4e left-0 z-10">
<div :style="{ height: 1.5 + 'em', width: 2.5 + 'em' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
<span class="material-icons text-red-100 pr-1e" :style="{ fontSize: 0.875 + 'em' }">priority_high</span>
</div>
</ui-tooltip>
<!-- rss feed icon -->
<div cy-id="rssFeed" v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" :style="{ padding: 0.375 + 'em' }">
<span class="material-icons" :style="{ fontSize: 1.5 + 'em' }">rss_feed</span>
</div>
<!-- media item shared icon -->
<div cy-id="mediaItemShare" v-if="mediaItemShare && !isSelectionMode && !isHovering" class="absolute text-success left-0 z-10" :style="{ padding: 0.375 + 'em', top: rssFeed ? '2em' : '0px' }">
<span class="material-icons" :style="{ fontSize: 1.5 + 'em' }">public</span>
</div>
<!-- Series sequence -->
<div cy-id="seriesSequence" v-if="seriesSequence && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }">
<p :style="{ fontSize: 0.8 + 'em' }">#{{ seriesSequence }}</p>
</div>
<!-- Podcast Episode # -->
<div cy-id="podcastEpisodeNumber" v-if="recentEpisodeNumber !== null && !isHovering && !isSelectionMode && !processing" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }">
<p :style="{ fontSize: 0.8 + 'em' }">
Episode<span v-if="recentEpisodeNumber"> #{{ recentEpisodeNumber }}</span>
</p>
</div>
<!-- Podcast Num Episodes -->
<div cy-id="numEpisodes" v-else-if="!numEpisodesIncomplete && numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', width: 1.25 + 'em', height: 1.25 + 'em' }">
<p :style="{ fontSize: 0.8 + 'em' }">{{ numEpisodes }}</p>
</div>
<!-- Podcast Num Episodes -->
<div cy-id="numEpisodesIncomplete" v-else-if="numEpisodesIncomplete && !isHovering && !isSelectionMode" class="absolute rounded-full bg-yellow-400 text-black font-semibold box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', width: 1.25 + 'em', height: 1.25 + 'em' }">
<p :style="{ fontSize: 0.8 + 'em' }">{{ numEpisodesIncomplete }}</p>
</div>
</div>
</div> </div>
<!-- Alternative bookshelf title/author/sort --> <!-- Alternative bookshelf title/author/sort -->
<div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }"> <div cy-id="detailBottom" :id="`description-area-${index}`" v-if="isAlternativeBookshelfView || isAuthorBookshelfView" dir="auto" class="relative mt-2e mb-2e left-0 z-50 w-full">
<div :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }"> <div :style="{ fontSize: 0.9 + 'em' }">
<ui-tooltip :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center"> <ui-tooltip v-if="displayTitle" :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
<p ref="displayTitle" class="truncate">{{ displayTitle }}</p> <p cy-id="title" ref="displayTitle" class="truncate">{{ displayTitle }}</p>
<widgets-explicit-indicator :explicit="isExplicit" /> <widgets-explicit-indicator cy-id="explicitIndicator" v-if="isExplicit" />
</ui-tooltip> </ui-tooltip>
</div> </div>
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || '&nbsp;' }}</p> <p cy-id="line2" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displayLineTwo || '&nbsp;' }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p> <p cy-id="line3" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displaySortLine }}</p>
</div>
<div v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }" style="background-color: #78350f">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequenceList }}</p>
</div>
<div v-else-if="booksInSeries" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }" style="background-color: #cd9d49dd">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ booksInSeries }}</p>
</div>
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
<div v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="text-gray-300 text-center">{{ title }}</p>
</div>
<!-- Cover Image -->
<img v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
<!-- Placeholder Cover Title & Author -->
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
<div>
<p class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">
{{ titleCleaned }}
</p>
</div>
</div>
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
<p class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
</div>
</div>
<!-- No progress shown for collapsed series in library and podcasts (unless showing podcast episode) -->
<div v-if="!booksInSeries && (!isPodcast || episodeProgress)" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
<!-- Finished progress bar for collapsed series -->
<div v-else-if="booksInSeries && seriesIsFinished" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b bg-success" :style="{ width: width * userProgressPercent + 'px' }"></div>
<!-- Overlay is not shown if collapsing series in library -->
<div v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen) && !processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded hidden md:block" :class="overlayWrapperClasslist">
<div v-show="showPlayButton" class="h-full flex items-center justify-center pointer-events-none">
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="play">
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
</div>
</div>
<div v-show="showReadButton" class="h-full flex items-center justify-center pointer-events-none">
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="clickReadEBook">
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">auto_stories</span>
</div>
</div>
<div v-if="userCanUpdate" v-show="!isSelectionMode" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-150 top-0 right-0" :style="{ padding: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="editClick">
<span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">edit</span>
</div>
<!-- Radio button -->
<div v-if="!isAuthorBookshelfView" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="selectBtnClick">
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
</div>
<!-- More Menu Icon -->
<div ref="moreIcon" v-show="!isSelectionMode && moreMenuItems.length" class="hidden md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-150" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
</div>
<div v-if="ebookFormat" class="absolute" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }">
<span class="text-white/80" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ ebookFormat }}</span>
</div>
</div>
<!-- Processing/loading spinner overlay -->
<div v-if="processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 rounded flex items-center justify-center">
<widgets-loading-spinner size="la-lg" />
</div>
<!-- Series name overlay -->
<div v-if="booksInSeries && libraryItem && isHovering" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-60 rounded flex items-center justify-center" :style="{ padding: sizeMultiplier + 'rem' }">
<p v-if="seriesName" class="text-gray-200 text-center" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">{{ seriesName }}</p>
</div>
<!-- Error widget -->
<ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0 z-10">
<div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
</div>
</ui-tooltip>
<div v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" :style="{ padding: 0.375 * sizeMultiplier + 'rem' }">
<span class="material-icons" :style="{ fontSize: sizeMultiplier * 1.5 + 'rem' }">rss_feed</span>
</div>
<!-- Series sequence -->
<div v-if="seriesSequence && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequence }}</p>
</div>
<!-- Podcast Episode # -->
<div v-if="recentEpisodeNumber !== null && !isHovering && !isSelectionMode && !processing" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">
Episode<span v-if="recentEpisodeNumber"> #{{ recentEpisodeNumber }}</span>
</p>
</div>
<!-- Podcast Num Episodes -->
<div v-else-if="!numEpisodesIncomplete && numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodes }}</p>
</div>
<!-- Podcast Num Episodes -->
<div v-else-if="numEpisodesIncomplete && !isHovering && !isSelectionMode" class="absolute rounded-full bg-yellow-400 text-black font-semibold box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodesIncomplete }}</p>
</div> </div>
</div> </div>
</template> </template>
@@ -135,15 +145,11 @@ import MoreMenu from '@/components/widgets/MoreMenu'
export default { export default {
props: { props: {
index: Number, index: Number,
width: { width: Number,
type: Number,
default: 120
},
height: { height: {
type: Number, type: Number,
default: 192 default: 192
}, },
bookCoverAspectRatio: Number,
bookshelfView: Number, bookshelfView: Number,
bookMount: { bookMount: {
// Book can be passed as prop or set with setEntity() // Book can be passed as prop or set with setEntity()
@@ -178,6 +184,39 @@ export default {
} }
}, },
computed: { computed: {
bookCoverAspectRatio() {
return this.store.getters['libraries/getBookCoverAspectRatio']
},
coverWidth() {
return this.width || this.coverHeight / this.bookCoverAspectRatio
},
coverHeight() {
return this.height * this.sizeMultiplier
},
cardWidth() {
// 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']
},
dateFormat() { dateFormat() {
return this.store.state.serverSettings.dateFormat return this.store.state.serverSettings.dateFormat
}, },
@@ -277,10 +316,6 @@ export default {
squareAspectRatio() { squareAspectRatio() {
return this.bookCoverAspectRatio === 1 return this.bookCoverAspectRatio === 1
}, },
sizeMultiplier() {
const baseSize = this.squareAspectRatio ? 192 : 120
return this.width / baseSize
},
title() { title() {
return this.mediaMetadata.title || '' return this.mediaMetadata.title || ''
}, },
@@ -302,7 +337,7 @@ export default {
if (this.recentEpisode) return this.recentEpisode.title if (this.recentEpisode) return this.recentEpisode.title
const ignorePrefix = this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix const ignorePrefix = this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix
if (this.collapsedSeries) return ignorePrefix ? this.collapsedSeries.nameIgnorePrefix : this.collapsedSeries.name if (this.collapsedSeries) return ignorePrefix ? this.collapsedSeries.nameIgnorePrefix : this.collapsedSeries.name
return ignorePrefix ? this.mediaMetadata.titleIgnorePrefix : this.title return ignorePrefix ? this.mediaMetadata.titleIgnorePrefix || '\u00A0' : this.title || '\u00A0'
}, },
displayLineTwo() { displayLineTwo() {
if (this.recentEpisode) return this.title if (this.recentEpisode) return this.title
@@ -323,7 +358,10 @@ export default {
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false) 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 === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes` if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
if (this.orderBy === 'media.metadata.publishedYear' && this.mediaMetadata.publishedYear) return 'Published ' + this.mediaMetadata.publishedYear if (this.orderBy === 'media.metadata.publishedYear') {
if (this.mediaMetadata.publishedYear) return 'Published ' + this.mediaMetadata.publishedYear
return '\u00A0'
}
return null return null
}, },
episodeProgress() { episodeProgress() {
@@ -343,11 +381,22 @@ export default {
if (!this.userProgress || this.userProgress.progress) return false if (!this.userProgress || this.userProgress.progress) return false
return this.userProgress.ebookProgress > 0 return this.userProgress.ebookProgress > 0
}, },
seriesProgressPercent() {
if (!this.libraryItemIdsInSeries.length) return 0
let progressPercent = 0
const useEBookProgress = this.useEBookProgress
this.libraryItemIdsInSeries.forEach((lid) => {
const progress = this.store.getters['user/getUserMediaProgress'](lid)
if (progress) progressPercent += progress.isFinished ? 1 : useEBookProgress ? progress.ebookProgress || 0 : progress.progress || 0
})
return progressPercent / this.libraryItemIdsInSeries.length
},
userProgressPercent() { userProgressPercent() {
if (this.useEBookProgress) return Math.max(Math.min(1, this.userProgress.ebookProgress), 0) let progressPercent = this.itemIsFinished ? 1 : this.booksInSeries ? this.seriesProgressPercent : this.useEBookProgress ? this.userProgress?.ebookProgress || 0 : this.userProgress?.progress || 0
return this.userProgress ? Math.max(Math.min(1, this.userProgress.progress), 0) || 0 : 0 return Math.max(Math.min(1, progressPercent), 0)
}, },
itemIsFinished() { itemIsFinished() {
if (this.booksInSeries) return this.seriesIsFinished
return this.userProgress ? !!this.userProgress.isFinished : false return this.userProgress ? !!this.userProgress.isFinished : false
}, },
seriesIsFinished() { seriesIsFinished() {
@@ -358,7 +407,7 @@ export default {
}, },
showError() { showError() {
if (this.recentEpisode) return false // Dont show podcast error on episode card if (this.recentEpisode) return false // Dont show podcast error on episode card
return this.numInvalidAudioFiles || this.numMissingParts || this.isMissing || this.isInvalid return this.isMissing || this.isInvalid
}, },
libraryItemIdStreaming() { libraryItemIdStreaming() {
return this.store.getters['getLibraryItemIdStreaming'] return this.store.getters['getLibraryItemIdStreaming']
@@ -388,29 +437,13 @@ export default {
isInvalid() { isInvalid() {
return this._libraryItem.isInvalid return this._libraryItem.isInvalid
}, },
numMissingParts() {
if (this.isPodcast) return 0
return this.media.numMissingParts
},
numInvalidAudioFiles() {
if (this.isPodcast) return 0
return this.media.numInvalidAudioFiles
},
errorText() { errorText() {
if (this.isMissing) return 'Item directory is missing!' if (this.isMissing) return 'Item directory is missing!'
else if (this.isInvalid) { else if (this.isInvalid) {
if (this.isPodcast) return 'Podcast has no episodes' if (this.isPodcast) return 'Podcast has no episodes'
return 'Item has no audio tracks & ebook' return 'Item has no audio tracks & ebook'
} }
let txt = '' return 'Unknown Error'
if (this.numMissingParts) {
txt += `${this.numMissingParts} missing parts.`
}
if (this.numInvalidAudioFiles) {
if (txt) txt += ' '
txt += `${this.numInvalidAudioFiles} invalid audio files.`
}
return txt || 'Unknown Error'
}, },
overlayWrapperClasslist() { overlayWrapperClasslist() {
const classes = [] const classes = []
@@ -495,6 +528,12 @@ export default {
func: 'openPlaylists', func: 'openPlaylists',
text: this.$strings.LabelAddToPlaylist text: this.$strings.LabelAddToPlaylist
}) })
if (this.userIsAdminOrUp) {
items.push({
func: 'openShare',
text: this.$strings.LabelShare
})
}
} }
if (this.ebookFormat && this.store.state.libraries.ereaderDevices?.length) { if (this.ebookFormat && this.store.state.libraries.ereaderDevices?.length) {
items.push({ items.push({
@@ -566,16 +605,16 @@ export default {
return this.$root.socket || this.$nuxt.$root.socket return this.$root.socket || this.$nuxt.$root.socket
}, },
titleFontSize() { titleFontSize() {
return 0.75 * this.sizeMultiplier return 0.75
}, },
authorFontSize() { authorFontSize() {
return 0.6 * this.sizeMultiplier return 0.6
}, },
placeholderCoverPadding() { placeholderCoverPadding() {
return 0.8 * this.sizeMultiplier return 0.8
}, },
authorBottom() { authorBottom() {
return 0.75 * this.sizeMultiplier return 0.75
}, },
titleCleaned() { titleCleaned() {
if (!this.title) return '' if (!this.title) return ''
@@ -599,14 +638,12 @@ export default {
const constants = this.$constants || this.$nuxt.$constants const constants = this.$constants || this.$nuxt.$constants
return this.bookshelfView === constants.BookshelfView.AUTHOR return this.bookshelfView === constants.BookshelfView.AUTHOR
}, },
titleDisplayBottomOffset() {
if (!this.isAlternativeBookshelfView && !this.isAuthorBookshelfView) return 0
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
return 4.25 * this.sizeMultiplier
},
rssFeed() { rssFeed() {
if (this.booksInSeries) return null if (this.booksInSeries) return null
return this._libraryItem.rssFeed || null return this._libraryItem.rssFeed || null
},
mediaItemShare() {
return this._libraryItem.mediaItemShare || null
} }
}, },
methods: { methods: {
@@ -849,6 +886,10 @@ export default {
this.store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode: this.recentEpisode }]) this.store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode: this.recentEpisode }])
this.store.commit('globals/setShowPlaylistsModal', true) this.store.commit('globals/setShowPlaylistsModal', true)
}, },
openShare() {
this.store.commit('setSelectedLibraryItem', this.libraryItem)
this.store.commit('globals/setShareModal', this.mediaItemShare)
},
deleteLibraryItem() { deleteLibraryItem() {
const payload = { const payload = {
message: this.$strings.MessageConfirmDeleteLibraryItem, message: this.$strings.MessageConfirmDeleteLibraryItem,
+44 -20
View File
@@ -1,24 +1,26 @@
<template> <template>
<div ref="card" :id="`collection-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard"> <div ref="card" :id="`collection-card-${index}`" :style="{ width: cardWidth + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" /> <div class="relative" :style="{ height: coverHeight + 'px' }">
<div class="w-full h-full bg-primary relative rounded overflow-hidden"> <div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<covers-collection-cover ref="cover" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <div class="w-full h-full bg-primary relative rounded overflow-hidden">
</div> <covers-collection-cover ref="cover" :book-items="books" :width="cardWidth" :height="coverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
<div class="absolute pointer-events-auto" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickEdit">
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
</div> </div>
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
<div class="absolute pointer-events-auto" :style="{ top: 0.5 + 'em', right: 0.5 + 'em' }" @click.stop.prevent="clickEdit">
<span class="material-icons text-white text-opacity-75 hover:text-opacity-100" :style="{ fontSize: 1.25 + 'em' }">edit</span>
</div>
</div>
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 + 'em', left: 0.5 + 'em', fontSize: 1.5 + 'em' }">rss_feed</span>
</div> </div>
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span> <div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6e h-6e rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em ${0.5}em` }">
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }"> <p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div> </div>
</div> </div>
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center"> <div v-else class="relative z-30 left-0 right-0 mx-auto h-8e py-1e rounded-md text-center">
<p class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ title }}</p> <p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
</div> </div>
</div> </div>
</template> </template>
@@ -28,8 +30,10 @@ export default {
props: { props: {
index: Number, index: Number,
width: Number, width: Number,
height: Number, height: {
bookCoverAspectRatio: Number, type: Number,
default: 192
},
bookshelfView: { bookshelfView: {
type: Number, type: Number,
default: 0 default: 0
@@ -49,13 +53,33 @@ export default {
} }
}, },
computed: { computed: {
bookCoverAspectRatio() {
return this.store.getters['libraries/getBookCoverAspectRatio']
},
cardWidth() {
return this.width || this.coverHeight * 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() { labelFontSize() {
if (this.width < 160) return 0.75 if (this.width < 160) return 0.75
return 0.875 return 0.9
}, },
sizeMultiplier() { sizeMultiplier() {
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2) return this.store.getters['user/getSizeMultiplier']
return this.width / 240
}, },
title() { title() {
return this.collection ? this.collection.name : '' return this.collection ? this.collection.name : ''
+31 -18
View File
@@ -1,21 +1,24 @@
<template> <template>
<div ref="card" :id="`playlist-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard"> <div ref="card" :id="`playlist-card-${index}`" :style="{ width: cardWidth + 'px', fontSize: sizeMultiplier + 'rem' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" /> <div class="relative" :style="{ height: coverHeight + 'px' }">
<div class="w-full h-full bg-primary relative rounded overflow-hidden"> <div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<covers-playlist-cover ref="cover" :items="items" :width="width" :height="height" /> <div class="w-full h-full bg-primary relative rounded overflow-hidden">
</div> <covers-playlist-cover ref="cover" :items="items" :width="cardWidth" :height="coverHeight" />
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none"> </div>
<div class="absolute pointer-events-auto" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickEdit"> <div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span> <div class="absolute pointer-events-auto" :style="{ top: 0.5 + 'em', right: 0.5 + 'em' }" @click.stop.prevent="clickEdit">
<span class="material-icons text-white text-opacity-75 hover:text-opacity-100" :style="{ fontSize: 1.25 + 'em' }">edit</span>
</div>
</div> </div>
</div> </div>
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }"> <div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 -bottom-6e left-0 right-0 mx-auto h-6e rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p> <div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em ${0.5}em` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
</div> </div>
</div> </div>
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center"> <div v-else class="relative z-30 left-0 right-0 mx-auto h-8e py-1e rounded-md text-center">
<p class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ title }}</p> <p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
</div> </div>
</div> </div>
</template> </template>
@@ -25,8 +28,10 @@ export default {
props: { props: {
index: Number, index: Number,
width: Number, width: Number,
height: Number, height: {
bookCoverAspectRatio: Number, type: Number,
default: 192
},
bookshelfView: { bookshelfView: {
type: Number, type: Number,
default: 0 default: 0
@@ -45,13 +50,21 @@ export default {
} }
}, },
computed: { computed: {
bookCoverAspectRatio() {
return this.store.getters['libraries/getBookCoverAspectRatio']
},
cardWidth() {
return this.width || this.coverHeight
},
coverHeight() {
return this.height * this.sizeMultiplier
},
labelFontSize() { labelFontSize() {
if (this.width < 160) return 0.75 if (this.width < 160) return 0.75
return 0.875 return 0.9
}, },
sizeMultiplier() { sizeMultiplier() {
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6) return this.store.getters['user/getSizeMultiplier']
return this.width / 120
}, },
title() { title() {
return this.playlist ? this.playlist.name : '' return this.playlist ? this.playlist.name : ''
+47 -30
View File
@@ -1,28 +1,32 @@
<template> <template>
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard"> <div cy-id="card" ref="card" :id="`series-card-${index}`" :style="{ width: cardWidth + 'px' }" class="absolute rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" /> <div cy-id="covers-area" class="relative" :style="{ height: coverHeight + 'px' }">
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0"> <div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="cardWidth" :height="coverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
<div cy-id="seriesLengthMarker" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #cd9d49dd">
<p :style="{ fontSize: 0.8 + 'em' }">{{ books.length }}</p>
</div>
<div cy-id="seriesProgressBar" v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1e shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
<div cy-id="hoveringDisplayTitle" v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: '1em' }">
<p :style="{ fontSize: 1.2 + 'em' }">{{ displayTitle }}</p>
</div>
<span cy-id="rssFeedMarker" v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 + 'em', left: 0.5 + 'em', fontSize: 1.5 + 'em' }">rss_feed</span>
</div> </div>
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ books.length }}</div> <div cy-id="standardBottomText" v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6e h-6e rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em 0.5em` }">
<div v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" /> <p cy-id="standardBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ displayTitle }}</p>
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
</div>
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span>
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
</div> </div>
</div> </div>
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center"> <div cy-id="detailBottomText" v-else class="relative z-30 left-0 right-0 mx-auto py-1e rounded-md text-center">
<p class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ displayTitle }}</p> <p cy-id="detailBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ displayTitle }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p> <p cy-id="detailBottomSortLine" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displaySortLine }}</p>
</div> </div>
</div> </div>
</template> </template>
@@ -32,13 +36,14 @@ export default {
props: { props: {
index: Number, index: Number,
width: Number, width: Number,
height: Number, height: {
bookCoverAspectRatio: Number, type: Number,
default: 192
},
bookshelfView: { bookshelfView: {
type: Number, type: Number,
default: 0 default: 0
}, },
isCategorized: Boolean,
seriesMount: { seriesMount: {
type: Object, type: Object,
default: () => null default: () => null
@@ -56,16 +61,24 @@ export default {
} }
}, },
computed: { computed: {
bookCoverAspectRatio() {
return this.store.getters['libraries/getBookCoverAspectRatio']
},
cardWidth() {
return this.width || this.coverHeight * 2
},
coverHeight() {
return this.height * this.sizeMultiplier
},
dateFormat() { dateFormat() {
return this.store.state.serverSettings.dateFormat return this.store.state.serverSettings.dateFormat
}, },
labelFontSize() { labelFontSize() {
if (this.width < 160) return 0.75 if (this.width < 160) return 0.75
return 0.875 return 0.9
}, },
sizeMultiplier() { sizeMultiplier() {
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2) return this.store.getters['user/getSizeMultiplier']
return this.width / 240
}, },
seriesId() { seriesId() {
return this.series ? this.series.id : '' return this.series ? this.series.id : ''
@@ -78,7 +91,7 @@ export default {
}, },
displayTitle() { displayTitle() {
if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title
return this.title return this.title || '\u00A0'
}, },
displaySortLine() { displaySortLine() {
switch (this.orderBy) { switch (this.orderBy) {
@@ -119,9 +132,13 @@ export default {
return this.seriesBookProgress.some((p) => !p.isFinished && p.progress > 0) return this.seriesBookProgress.some((p) => !p.isFinished && p.progress > 0)
}, },
seriesPercentInProgress() { seriesPercentInProgress() {
let totalFinishedAndInProgress = this.seriesBooksFinished.length if (!this.books.length) return 0
if (this.hasSeriesBookInProgress) totalFinishedAndInProgress += 1 let progressPercent = 0
return Math.min(1, Math.max(0, totalFinishedAndInProgress / this.books.length)) this.seriesBookProgress.forEach((progress) => {
progressPercent += progress.isFinished ? 1 : progress.progress || 0
})
progressPercent /= this.books.length
return Math.min(1, Math.max(0, progressPercent))
}, },
isSeriesFinished() { isSeriesFinished() {
return this.books.length === this.seriesBooksFinished.length return this.books.length === this.seriesBooksFinished.length
+24 -14
View File
@@ -1,17 +1,19 @@
<template> <template>
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(narrator.name)}`"> <div>
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden"> <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(name)}`">
<div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-40"> <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">
<span class="material-icons-outlined text-[10rem]">record_voice_over</span> <div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-40">
</div> <span class="material-icons-outlined text-[10em]">record_voice_over</span>
</div>
<!-- Narrator name & num books overlay --> <!-- Narrator name & num books overlay -->
<div class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2"> <div class="absolute bottom-0 left-0 w-full py-1e bg-black bg-opacity-60 px-2e">
<p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p> <p cy-id="name" class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: 0.75 + 'em' }">{{ name }}</p>
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p> <p cy-id="numBooks" class="text-center text-gray-200" :style="{ fontSize: 0.65 + 'em' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
</div>
</div> </div>
</div> </nuxt-link>
</nuxt-link> </div>
</template> </template>
<script> <script>
@@ -22,16 +24,21 @@ export default {
default: () => {} default: () => {}
}, },
width: Number, width: Number,
height: Number, height: {
sizeMultiplier: {
type: Number, type: Number,
default: 1 default: 100
} }
}, },
data() { data() {
return {} return {}
}, },
computed: { computed: {
cardWidth() {
return this.cardHeight * 1.5
},
cardHeight() {
return this.height * this.sizeMultiplier
},
name() { name() {
return this.narrator?.name || '' return this.narrator?.name || ''
}, },
@@ -43,6 +50,9 @@ export default {
}, },
currentLibraryId() { currentLibraryId() {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
},
sizeMultiplier() {
return this.$store.getters['user/getSizeMultiplier']
} }
}, },
methods: {} methods: {}
@@ -89,6 +89,14 @@
</template> </template>
</div> </div>
</div> </div>
<div v-if="language" 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.LabelLanguage }}</span>
</div>
<div>
<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 || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32"> <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> <span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
@@ -182,6 +190,9 @@ export default {
narrators() { narrators() {
return this.mediaMetadata.narrators || [] return this.mediaMetadata.narrators || []
}, },
language() {
return this.mediaMetadata.language || null
},
durationPretty() { durationPretty() {
if (this.isPodcast) return this.$elapsedPrettyExtended(this.totalPodcastDuration) if (this.isPodcast) return this.$elapsedPrettyExtended(this.totalPodcastDuration)
+10 -8
View File
@@ -1,13 +1,15 @@
<template> <template>
<div class="sm:w-80 w-full relative"> <div class="">
<form @submit.prevent="submitSearch"> <div class="w-full relative sm:w-80">
<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 @submit.prevent="submitSearch">
</form> <ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear"> </form>
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span> <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-else class="material-icons" style="font-size: 1.2rem">close</span> <span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
<span v-else class="material-icons" style="font-size: 1.2rem">close</span>
</div>
</div> </div>
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-40 sm:w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu"> <div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full max-w-64 sm:max-w-80 sm:w-80 bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu">
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label"> <ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<li v-if="isTyping" class="py-2 px-2"> <li v-if="isTyping" class="py-2 px-2">
<p>{{ $strings.MessageThinking }}</p> <p>{{ $strings.MessageThinking }}</p>
@@ -37,12 +37,12 @@
<span class="material-icons text-2xl">arrow_left</span> <span class="material-icons text-2xl">arrow_left</span>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="font-normal block truncate">Back</span> <span class="font-normal block truncate">{{ $strings.ButtonBack }}</span>
</div> </div>
</li> </li>
<li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="option"> <li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="option">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<span class="font-normal block truncate py-2">No {{ sublist }}</span> <span class="font-normal block truncate py-2">{{ $getString('LabelLibraryFilterSublistEmpty', [selectedSublistText]) }}</span>
</div> </div>
</li> </li>
<template v-for="item in sublistItems"> <template v-for="item in sublistItems">
@@ -89,6 +89,9 @@ export default {
this.$emit('input', val) this.$emit('input', val)
} }
}, },
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
libraryMediaType() { libraryMediaType() {
return this.$store.getters['libraries/getCurrentLibraryMediaType'] return this.$store.getters['libraries/getCurrentLibraryMediaType']
}, },
@@ -106,31 +109,37 @@ export default {
}, },
{ {
text: this.$strings.LabelGenre, text: this.$strings.LabelGenre,
textPlural: this.$strings.LabelGenres,
value: 'genres', value: 'genres',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelTag, text: this.$strings.LabelTag,
textPlural: this.$strings.LabelTags,
value: 'tags', value: 'tags',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelAuthor, text: this.$strings.LabelAuthor,
textPlural: this.$strings.LabelAuthors,
value: 'authors', value: 'authors',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelNarrator, text: this.$strings.LabelNarrator,
textPlural: this.$strings.LabelNarrators,
value: 'narrators', value: 'narrators',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelPublisher, text: this.$strings.LabelPublisher,
textPlural: this.$strings.LabelPublishers,
value: 'publishers', value: 'publishers',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelLanguage, text: this.$strings.LabelLanguage,
textPlural: this.$strings.LabelLanguages,
value: 'languages', value: 'languages',
sublist: true sublist: true
}, },
@@ -142,43 +151,50 @@ export default {
] ]
}, },
bookItems() { bookItems() {
return [ const items = [
{ {
text: this.$strings.LabelAll, text: this.$strings.LabelAll,
value: 'all' value: 'all'
}, },
{ {
text: this.$strings.LabelGenre, text: this.$strings.LabelGenre,
textPlural: this.$strings.LabelGenres,
value: 'genres', value: 'genres',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelTag, text: this.$strings.LabelTag,
textPlural: this.$strings.LabelTags,
value: 'tags', value: 'tags',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelSeries, text: this.$strings.LabelSeries,
textPlural: this.$strings.LabelSeries,
value: 'series', value: 'series',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelAuthor, text: this.$strings.LabelAuthor,
textPlural: this.$strings.LabelAuthors,
value: 'authors', value: 'authors',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelNarrator, text: this.$strings.LabelNarrator,
textPlural: this.$strings.LabelNarrators,
value: 'narrators', value: 'narrators',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelPublisher, text: this.$strings.LabelPublisher,
textPlural: this.$strings.LabelPublishers,
value: 'publishers', value: 'publishers',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelLanguage, text: this.$strings.LabelLanguage,
textPlural: this.$strings.LabelLanguages,
value: 'languages', value: 'languages',
sublist: true sublist: true
}, },
@@ -218,6 +234,14 @@ export default {
sublist: false sublist: false
} }
] ]
if (this.userIsAdminOrUp) {
items.push({
text: this.$strings.LabelShareOpen,
value: 'share-open',
sublist: false
})
}
return items
}, },
podcastItems() { podcastItems() {
return [ return [
@@ -227,14 +251,22 @@ export default {
}, },
{ {
text: this.$strings.LabelGenre, text: this.$strings.LabelGenre,
textPlural: this.$strings.LabelGenres,
value: 'genres', value: 'genres',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelTag, text: this.$strings.LabelTag,
textPlural: this.$strings.LabelTags,
value: 'tags', value: 'tags',
sublist: true sublist: true
}, },
{
text: this.$strings.LabelLanguage,
textPlural: this.$strings.LabelLanguages,
value: 'languages',
sublist: true
},
{ {
text: this.$strings.ButtonIssues, text: this.$strings.ButtonIssues,
value: 'issues', value: 'issues',
@@ -250,11 +282,13 @@ export default {
}, },
{ {
text: this.$strings.LabelGenre, text: this.$strings.LabelGenre,
textPlural: this.$strings.LabelGenres,
value: 'genres', value: 'genres',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelTag, text: this.$strings.LabelTag,
textPlural: this.$strings.LabelTags,
value: 'tags', value: 'tags',
sublist: true sublist: true
}, },
@@ -274,6 +308,13 @@ export default {
selectedItemSublist() { selectedItemSublist() {
return this.selected?.includes('.') ? this.selected.split('.')[0] : null return this.selected?.includes('.') ? this.selected.split('.')[0] : null
}, },
selectedSublistText() {
if (!this.sublist) {
return ''
}
const sublistItem = this.selectItems.find((i) => i.value === this.sublist)
return sublistItem?.textPlural || sublistItem?.text || ''
},
selectedText() { selectedText() {
if (!this.selected) return '' if (!this.selected) return ''
const parts = this.selected.split('.') const parts = this.selected.split('.')
@@ -368,9 +409,17 @@ export default {
id: 'ebook', id: 'ebook',
name: this.$strings.LabelHasEbook name: this.$strings.LabelHasEbook
}, },
{
id: 'no-ebook',
name: this.$strings.LabelMissingEbook
},
{ {
id: 'supplementary', id: 'supplementary',
name: this.$strings.LabelHasSupplementaryEbook name: this.$strings.LabelHasSupplementaryEbook
},
{
id: 'no-supplementary',
name: this.$strings.LabelMissingSupplementaryEbook
} }
] ]
}, },
+5 -5
View File
@@ -1,8 +1,8 @@
<template> <template>
<div class="relative" v-click-outside="clickOutside" @mouseover="mouseover" @mouseleave="mouseleave"> <div class="relative" v-click-outside="clickOutside" @mouseover="mouseover" @mouseleave="mouseleave">
<div class="cursor-pointer text-gray-300 hover:text-white" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon"> <button :aria-label="$strings.LabelVolume" class="text-gray-300 hover:text-white" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon">
<span class="material-icons text-2xl sm:text-3xl">{{ volumeIcon }}</span> <span class="material-icons text-2xl sm:text-3xl">{{ volumeIcon }}</span>
</div> </button>
<transition name="menux"> <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 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 ref="volumeTrack" class="h-1 w-full bg-gray-500 my-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack">
@@ -38,8 +38,8 @@ export default {
}, },
set(val) { set(val) {
try { try {
localStorage.setItem("volume", val); localStorage.setItem('volume', val)
} catch(error) { } catch (error) {
console.error('Failed to store volume', err) console.error('Failed to store volume', err)
} }
this.$emit('input', val) this.$emit('input', val)
@@ -146,7 +146,7 @@ export default {
if (this.value === 0) { if (this.value === 0) {
this.isMute = true this.isMute = true
} }
const storageVolume = localStorage.getItem("volume") const storageVolume = localStorage.getItem('volume')
if (storageVolume) { if (storageVolume) {
this.volume = parseFloat(storageVolume) this.volume = parseFloat(storageVolume)
} }
+7 -5
View File
@@ -101,9 +101,14 @@ export default {
}, },
fullCoverUrl() { fullCoverUrl() {
if (!this.libraryItem) return null if (!this.libraryItem) return null
var store = this.$store || this.$nuxt.$store const store = this.$store || this.$nuxt.$store
return store.getters['globals/getLibraryItemCoverSrc'](this.libraryItem, this.placeholderUrl) return store.getters['globals/getLibraryItemCoverSrc'](this.libraryItem, this.placeholderUrl)
}, },
rawCoverUrl() {
if (!this.libraryItem) return null
const store = this.$store || this.$nuxt.$store
return store.getters['globals/getLibraryItemCoverSrc'](this.libraryItem, this.placeholderUrl, true)
},
cover() { cover() {
return this.media.coverPath || this.placeholderUrl return this.media.coverPath || this.placeholderUrl
}, },
@@ -126,9 +131,6 @@ export default {
authorBottom() { authorBottom() {
return 0.75 * this.sizeMultiplier return 0.75 * this.sizeMultiplier
}, },
userToken() {
return this.$store.getters['user/getToken']
},
resolution() { resolution() {
return `${this.naturalWidth}x${this.naturalHeight}px` return `${this.naturalWidth}x${this.naturalHeight}px`
} }
@@ -136,7 +138,7 @@ export default {
methods: { methods: {
clickCover() { clickCover() {
if (this.expandOnClick && this.libraryItem) { if (this.expandOnClick && this.libraryItem) {
this.$store.commit('globals/setRawCoverPreviewModal', this.libraryItem.id) this.$store.commit('globals/setRawCoverPreviewModal', this.rawCoverUrl)
} }
}, },
setCoverBg() { setCoverBg() {
+1 -1
View File
@@ -65,7 +65,7 @@ export default {
return 0.8 * this.sizeMultiplier return 0.8 * this.sizeMultiplier
}, },
resolution() { resolution() {
return `${this.naturalWidth}x${this.naturalHeight}px` return `${this.naturalWidth}×${this.naturalHeight}px`
}, },
placeholderUrl() { placeholderUrl() {
const config = this.$config || this.$nuxt.$config const config = this.$config || this.$nuxt.$config
+37 -7
View File
@@ -10,21 +10,21 @@
<div class="w-full p-8"> <div class="w-full p-8">
<div class="flex py-2"> <div class="flex py-2">
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
<ui-text-input-with-label v-model="newUser.username" :label="$strings.LabelUsername" /> <ui-text-input-with-label v-model.trim="newUser.username" :label="$strings.LabelUsername" />
</div> </div>
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
<ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? $strings.LabelPassword : $strings.LabelChangePassword" type="password" /> <ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? $strings.LabelPassword : $strings.LabelChangePassword" type="password" />
<ui-text-input-with-label v-else v-model="newUser.email" :label="$strings.LabelEmail" /> <ui-text-input-with-label v-else v-model.trim="newUser.email" :label="$strings.LabelEmail" />
</div> </div>
</div> </div>
<div v-show="!isEditingRoot" class="flex py-2"> <div v-show="!isEditingRoot" class="flex py-2">
<div class="w-1/2 px-2"> <div class="w-1/2 px-2">
<ui-text-input-with-label v-model="newUser.email" :label="$strings.LabelEmail" /> <ui-text-input-with-label v-model.trim="newUser.email" :label="$strings.LabelEmail" />
</div> </div>
<div class="px-2 w-52"> <div class="px-2 w-52">
<ui-dropdown v-model="newUser.type" :label="$strings.LabelAccountType" :disabled="isEditingRoot" :items="accountTypes" small @input="userTypeUpdated" /> <ui-dropdown v-model="newUser.type" :label="$strings.LabelAccountType" :disabled="isEditingRoot" :items="accountTypes" small @input="userTypeUpdated" />
</div> </div>
<!-- <div class="flex-grow" /> -->
<div class="flex items-center pt-4 px-2"> <div class="flex items-center pt-4 px-2">
<p class="px-3 font-semibold" id="user-enabled-toggle" :class="isEditingRoot ? 'text-gray-300' : ''">{{ $strings.LabelEnable }}</p> <p class="px-3 font-semibold" id="user-enabled-toggle" :class="isEditingRoot ? 'text-gray-300' : ''">{{ $strings.LabelEnable }}</p>
<ui-toggle-switch labeledBy="user-enabled-toggle" v-model="newUser.isActive" :disabled="isEditingRoot" /> <ui-toggle-switch labeledBy="user-enabled-toggle" v-model="newUser.isActive" :disabled="isEditingRoot" />
@@ -111,7 +111,8 @@
</div> </div>
<div class="flex pt-4 px-2"> <div class="flex pt-4 px-2">
<ui-btn v-if="isEditingRoot" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn> <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="isEditingRoot" small class="flex items-center" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn> <ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
</div> </div>
@@ -136,7 +137,8 @@ export default {
newUser: {}, newUser: {},
isNew: true, isNew: true,
tags: [], tags: [],
loadingTags: false loadingTags: false,
unlinkingFromOpenID: false
} }
}, },
watch: { watch: {
@@ -180,7 +182,7 @@ export default {
return this.isNew ? this.$strings.HeaderNewAccount : this.$strings.HeaderUpdateAccount return this.isNew ? this.$strings.HeaderNewAccount : this.$strings.HeaderUpdateAccount
}, },
isEditingRoot() { isEditingRoot() {
return this.account && this.account.type === 'root' return this.account?.type === 'root'
}, },
libraries() { libraries() {
return this.$store.state.libraries.libraries return this.$store.state.libraries.libraries
@@ -198,6 +200,9 @@ export default {
}, },
tagsSelectionText() { tagsSelectionText() {
return this.newUser.permissions.selectedTagsNotAccessible ? this.$strings.LabelTagsNotAccessibleToUser : this.$strings.LabelTagsAccessibleToUser return this.newUser.permissions.selectedTagsNotAccessible ? this.$strings.LabelTagsNotAccessibleToUser : this.$strings.LabelTagsAccessibleToUser
},
hasOpenIDLink() {
return !!this.account?.hasOpenIDLink
} }
}, },
methods: { methods: {
@@ -205,6 +210,31 @@ export default {
// Force close when navigating - used in UsersTable // Force close when navigating - used in UsersTable
if (this.$refs.modal) this.$refs.modal.setHide() if (this.$refs.modal) this.$refs.modal.setHide()
}, },
unlinkOpenID() {
const payload = {
message: 'Are you sure you want to unlink this user from OpenID?',
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.show = false
})
.catch((error) => {
console.error('Failed to unlink user from OpenID', error)
this.$toast.error('Failed to unlink user from OpenID')
})
.finally(() => {
this.unlinkingFromOpenID = false
})
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
accessAllTagsToggled(val) { accessAllTagsToggled(val) {
if (val) { if (val) {
if (this.newUser.itemTagsSelected?.length) { if (this.newUser.itemTagsSelected?.length) {
@@ -0,0 +1,105 @@
<template>
<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>
</div>
</template>
<form @submit.prevent="submitForm">
<div class="px-4 w-full flex items-center text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="min-height: 400px; max-height: 80vh">
<div class="w-full p-8">
<div class="flex mb-2">
<div class="w-3/4 p-1">
<ui-text-input-with-label v-model="newName" :label="$strings.LabelName" />
</div>
<div class="w-1/4 p-1">
<ui-text-input-with-label value="Book" readonly :label="$strings.LabelMediaType" />
</div>
</div>
<div class="w-full mb-2 p-1">
<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" />
</div>
<div class="flex px-1 pt-4">
<div class="flex-grow" />
<ui-btn color="success" type="submit">{{ $strings.ButtonAdd }}</ui-btn>
</div>
</div>
</div>
</form>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean
},
data() {
return {
processing: false,
newName: '',
newUrl: '',
newAuthHeaderValue: ''
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.init()
}
}
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}
},
methods: {
submitForm() {
if (!this.newName || !this.newUrl) {
this.$toast.error('Must add name and url')
return
}
this.processing = true
this.$axios
.$post('/api/custom-metadata-providers', {
name: this.newName,
url: this.newUrl,
mediaType: 'book', // Currently only supporting book mediaType
authHeaderValue: this.newAuthHeaderValue
})
.then((data) => {
this.$emit('added', data.provider)
this.$toast.success('New provider added')
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)
})
.finally(() => {
this.processing = false
})
},
init() {
this.processing = false
this.newName = ''
this.newUrl = ''
this.newAuthHeaderValue = ''
}
},
mounted() {}
}
</script>
+6 -6
View File
@@ -34,11 +34,6 @@ export default {
data() { data() {
return {} return {}
}, },
watch: {
value(newVal) {
this.$nextTick(this.scrollToChapter)
}
},
computed: { computed: {
show: { show: {
get() { get() {
@@ -53,7 +48,7 @@ export default {
return this.playbackRate return this.playbackRate
}, },
currentChapterId() { currentChapterId() {
return this.currentChapter ? this.currentChapter.id : null return this.currentChapter?.id || null
}, },
currentChapterStart() { currentChapterStart() {
return (this.currentChapter?.start || 0) / this._playbackRate return (this.currentChapter?.start || 0) / this._playbackRate
@@ -74,6 +69,11 @@ export default {
} }
} }
} }
},
updated() {
if (this.value) {
this.$nextTick(this.scrollToChapter)
}
} }
} }
</script> </script>
@@ -8,7 +8,7 @@
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden p-6" style="max-height: 80vh"> <div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden p-6" style="max-height: 80vh">
<div class="flex items-center"> <div class="flex items-center">
<p class="text-base text-gray-200">{{ _session.displayTitle }}</p> <p class="text-base text-gray-200">{{ _session.displayTitle }}</p>
<p v-if="_session.displayAuthor" class="text-xs text-gray-400 px-4">by {{ _session.displayAuthor }}</p> <p v-if="_session.displayAuthor" class="text-xs text-gray-400 px-4">{{ $getString('LabelByAuthor', [_session.displayAuthor]) }}</p>
</div> </div>
<div class="w-full h-px bg-white bg-opacity-10 my-4" /> <div class="w-full h-px bg-white bg-opacity-10 my-4" />
@@ -80,26 +80,27 @@
</div> </div>
</div> </div>
<div class="w-full md:w-1/3"> <div class="w-full md:w-1/3">
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mb-2 mt-6 md:mt-0">{{ $strings.LabelUser }}</p> <p v-if="!isMediaItemShareSession" class="font-semibold uppercase text-xs text-gray-400 tracking-wide mb-2 mt-6 md:mt-0">{{ $strings.LabelUser }}</p>
<p class="mb-1 text-xs">{{ _session.userId }}</p> <p v-if="!isMediaItemShareSession" class="mb-1 text-xs">{{ _session.userId }}</p>
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">{{ $strings.LabelMediaPlayer }}</p> <p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">{{ $strings.LabelMediaPlayer }}</p>
<p class="mb-1">{{ playMethodName }}</p> <p class="mb-1">{{ playMethodName }}</p>
<p class="mb-1">{{ _session.mediaPlayer }}</p> <p class="mb-1">{{ _session.mediaPlayer }}</p>
<p v-if="hasDeviceInfo" class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">{{ $strings.LabelDevice }}</p> <p v-if="hasDeviceInfo" class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">{{ $strings.LabelDevice }}</p>
<p v-if="clientDisplayName" class="mb-1">{{ clientDisplayName }}</p>
<p v-if="deviceInfo.ipAddress" class="mb-1">{{ deviceInfo.ipAddress }}</p> <p v-if="deviceInfo.ipAddress" class="mb-1">{{ deviceInfo.ipAddress }}</p>
<p v-if="osDisplayName" class="mb-1">{{ osDisplayName }}</p> <p v-if="osDisplayName" class="mb-1">{{ osDisplayName }}</p>
<p v-if="deviceInfo.browserName" class="mb-1">{{ deviceInfo.browserName }}</p> <p v-if="deviceInfo.browserName" class="mb-1">{{ deviceInfo.browserName }}</p>
<p v-if="clientDisplayName" class="mb-1">{{ clientDisplayName }}</p> <p v-if="deviceDisplayName" class="mb-1">{{ deviceDisplayName }}</p>
<p v-if="deviceInfo.sdkVersion" class="mb-1">SDK {{ $strings.LabelVersion }}: {{ deviceInfo.sdkVersion }}</p> <p v-if="deviceInfo.sdkVersion" class="mb-1">SDK {{ $strings.LabelVersion }}: {{ deviceInfo.sdkVersion }}</p>
<p v-if="deviceInfo.deviceType" class="mb-1">{{ $strings.LabelType }}: {{ deviceInfo.deviceType }}</p> <p v-if="deviceInfo.deviceType" class="mb-1">{{ $strings.LabelType }}: {{ deviceInfo.deviceType }}</p>
</div> </div>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<ui-btn v-if="!isOpenSession" small color="error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn> <ui-btn v-if="!isOpenSession && !isMediaItemShareSession" small color="error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn>
<ui-btn v-else small color="error" @click.stop="closeSessionClick">Close Open Session</ui-btn> <ui-btn v-else-if="!isMediaItemShareSession" small color="error" @click.stop="closeSessionClick">Close Open Session</ui-btn>
</div> </div>
</div> </div>
</modals-modal> </modals-modal>
@@ -141,10 +142,14 @@ export default {
if (!this.deviceInfo.osName) return null if (!this.deviceInfo.osName) return null
return `${this.deviceInfo.osName} ${this.deviceInfo.osVersion}` return `${this.deviceInfo.osName} ${this.deviceInfo.osVersion}`
}, },
clientDisplayName() { deviceDisplayName() {
if (!this.deviceInfo.manufacturer || !this.deviceInfo.model) return null if (!this.deviceInfo.manufacturer || !this.deviceInfo.model) return null
return `${this.deviceInfo.manufacturer} ${this.deviceInfo.model}` return `${this.deviceInfo.manufacturer} ${this.deviceInfo.model}`
}, },
clientDisplayName() {
if (!this.deviceInfo.clientName) return null
return `${this.deviceInfo.clientName} ${this.deviceInfo.clientVersion || ''}`
},
playMethodName() { playMethodName() {
const playMethod = this._session.playMethod const playMethod = this._session.playMethod
if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play' if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play'
@@ -161,6 +166,9 @@ export default {
}, },
isOpenSession() { isOpenSession() {
return !!this._session.open return !!this._session.open
},
isMediaItemShareSession() {
return this._session.mediaPlayer === 'web-share'
} }
}, },
methods: { methods: {
@@ -20,11 +20,8 @@ export default {
this.$store.commit('globals/setShowRawCoverPreviewModal', val) this.$store.commit('globals/setShowRawCoverPreviewModal', val)
} }
}, },
selectedLibraryItemId() {
return this.$store.state.globals.selectedLibraryItemId
},
rawCoverUrl() { rawCoverUrl() {
return this.$store.getters['globals/getLibraryItemCoverSrcById'](this.selectedLibraryItemId, null, true) return this.$store.state.globals.selectedRawCoverUrl
} }
}, },
methods: {}, methods: {},
+204
View File
@@ -0,0 +1,204 @@
<template>
<modals-modal ref="modal" v-model="show" name="share" :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">{{ $strings.LabelShare }}</p>
</div>
</template>
<div class="px-6 py-8 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<div class="absolute top-0 right-0 p-4">
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/media-item-shares" target="_blank" class="inline-flex">
<span class="material-icons text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
</div>
<template v-if="currentShare">
<div class="w-full py-2">
<label class="px-1 text-sm font-semibold block">{{ $strings.LabelShareURL }}</label>
<ui-text-input v-model="currentShareUrl" show-copy readonly class="text-base h-10" />
</div>
<div class="w-full py-2 px-1">
<p v-if="currentShare.expiresAt" class="text-base">{{ $getString('MessageShareExpiresIn', [currentShareTimeRemaining]) }}</p>
<p v-else>{{ $strings.LabelPermanent }}</p>
</div>
</template>
<template v-else>
<div class="flex flex-col sm:flex-row items-center justify-between space-y-4 sm:space-y-0 sm:space-x-4 mb-4">
<div class="w-full sm:w-48">
<label class="px-1 text-sm font-semibold block">{{ $strings.LabelSlug }}</label>
<ui-text-input v-model="newShareSlug" class="text-base h-10" />
</div>
<div class="flex-grow" />
<div class="w-full sm:w-80">
<label class="px-1 text-sm font-semibold block">{{ $strings.LabelDuration }}</label>
<div class="inline-flex items-center space-x-2">
<div>
<ui-icon-btn icon="remove" :size="10" @click="clickMinus" />
</div>
<ui-text-input v-model="newShareDuration" type="number" text-center no-spinner class="text-center max-w-12 min-w-12 h-10 text-base" />
<div>
<ui-icon-btn icon="add" :size="10" @click="clickPlus" />
</div>
<div class="w-28">
<ui-dropdown v-model="shareDurationUnit" :items="durationUnits" />
</div>
</div>
</div>
</div>
<p class="text-sm text-gray-300 py-1 px-1" v-html="$getString('MessageShareURLWillBe', [demoShareUrl])" />
<p class="text-sm text-gray-300 py-1 px-1" v-html="$getString('MessageShareExpirationWillBe', [expirationDateString])" />
</template>
<div class="flex items-center pt-6">
<div class="flex-grow" />
<ui-btn v-if="currentShare" color="error" small @click="deleteShare">{{ $strings.ButtonDelete }}</ui-btn>
<ui-btn v-if="!currentShare" color="success" small @click="openShare">{{ $strings.ButtonShare }}</ui-btn>
</div>
</div>
</modals-modal>
</template>
<script>
export default {
props: {},
data() {
return {
processing: false,
newShareSlug: '',
newShareDuration: 0,
currentShare: null,
shareDurationUnit: 'minutes',
durationUnits: [
{
text: this.$strings.LabelMinutes,
value: 'minutes'
},
{
text: this.$strings.LabelHours,
value: 'hours'
},
{
text: this.$strings.LabelDays,
value: 'days'
}
]
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.init()
}
}
}
},
computed: {
show: {
get() {
return this.$store.state.globals.showShareModal
},
set(val) {
this.$store.commit('globals/setShowShareModal', val)
}
},
mediaItemShare() {
return this.$store.state.globals.selectedMediaItemShare
},
libraryItem() {
return this.$store.state.selectedLibraryItem
},
user() {
return this.$store.state.user.user
},
demoShareUrl() {
return `${window.origin}/share/${this.newShareSlug}`
},
currentShareUrl() {
if (!this.currentShare) return ''
return `${window.origin}/share/${this.currentShare.slug}`
},
currentShareTimeRemaining() {
if (!this.currentShare) return 'Error'
if (!this.currentShare.expiresAt) return this.$strings.LabelPermanent
const msRemaining = new Date(this.currentShare.expiresAt).valueOf() - Date.now()
if (msRemaining <= 0) return 'Expired'
return this.$elapsedPrettyExtended(msRemaining / 1000, true, false)
},
expireDurationSeconds() {
let shareDuration = Number(this.newShareDuration)
if (!shareDuration || isNaN(shareDuration)) return 0
return this.newShareDuration * (this.shareDurationUnit === 'minutes' ? 60 : this.shareDurationUnit === 'hours' ? 3600 : 86400)
},
expirationDateString() {
if (!this.expireDurationSeconds) return this.$strings.LabelPermanent
const dateMs = Date.now() + this.expireDurationSeconds * 1000
return this.$formatDatetime(dateMs, this.$store.state.serverSettings.dateFormat, this.$store.state.serverSettings.timeFormat)
}
},
methods: {
clickPlus() {
this.newShareDuration++
},
clickMinus() {
if (this.newShareDuration > 0) {
this.newShareDuration--
}
},
deleteShare() {
if (!this.currentShare) return
this.processing = true
this.$axios
.$delete(`/api/share/mediaitem/${this.currentShare.id}`)
.then(() => {
this.currentShare = null
this.$emit('removed')
})
.catch((error) => {
console.error('deleteShare', error)
let errorMsg = error.response?.data || 'Failed to delete share'
this.$toast.error(errorMsg)
})
.finally(() => {
this.processing = false
})
},
openShare() {
if (!this.newShareSlug) {
this.$toast.error('Slug is required')
return
}
const payload = {
slug: this.newShareSlug,
mediaItemType: 'book',
mediaItemId: this.libraryItem.media.id,
expiresAt: this.expireDurationSeconds ? Date.now() + this.expireDurationSeconds * 1000 : 0
}
this.processing = true
this.$axios
.$post(`/api/share/mediaitem`, payload)
.then((data) => {
this.currentShare = data
this.$emit('opened', data)
})
.catch((error) => {
console.error('openShare', error)
let errorMsg = error.response?.data || 'Failed to share item'
this.$toast.error(errorMsg)
})
.finally(() => {
this.processing = false
})
},
init() {
this.newShareSlug = this.$randomId(10)
if (this.mediaItemShare) {
this.currentShare = { ...this.mediaItemShare }
} else {
this.currentShare = null
}
}
},
mounted() {}
}
</script>
+14 -10
View File
@@ -9,7 +9,7 @@
<div class="flex"> <div class="flex">
<div class="w-40 p-2"> <div class="w-40 p-2">
<div class="w-full h-45 relative"> <div class="w-full h-45 relative">
<covers-author-image :author="author" /> <covers-author-image :author="authorCopy" />
<div v-if="userCanDelete && !processing && author.imagePath" class="absolute top-0 left-0 w-full h-full opacity-0 hover:opacity-100"> <div v-if="userCanDelete && !processing && author.imagePath" class="absolute top-0 left-0 w-full h-full opacity-0 hover:opacity-100">
<span class="absolute top-2 right-2 material-icons text-error transform hover:scale-125 transition-transform cursor-pointer text-lg" @click="removeCover">delete</span> <span class="absolute top-2 right-2 material-icons text-error transform hover:scale-125 transition-transform cursor-pointer text-lg" @click="removeCover">delete</span>
</div> </div>
@@ -30,9 +30,6 @@
<ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" /> <ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" />
</div> </div>
</div> </div>
<!-- <div class="p-2">
<ui-text-input-with-label v-model="authorCopy.imagePath" :disabled="processing" :label="$strings.LabelPhotoPathURL" />
</div> -->
<div class="p-2"> <div class="p-2">
<ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" :label="$strings.LabelDescription" :rows="8" /> <ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" :label="$strings.LabelDescription" :rows="8" />
</div> </div>
@@ -106,9 +103,9 @@ export default {
methods: { methods: {
init() { init() {
this.imageUrl = '' this.imageUrl = ''
this.authorCopy.name = this.author.name this.authorCopy = {
this.authorCopy.asin = this.author.asin ...this.author
this.authorCopy.description = this.author.description }
}, },
removeClick() { removeClick() {
const payload = { const payload = {
@@ -171,7 +168,9 @@ export default {
.$delete(`/api/authors/${this.authorId}/image`) .$delete(`/api/authors/${this.authorId}/image`)
.then((data) => { .then((data) => {
this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess) this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess)
this.$store.commit('globals/showEditAuthorModal', data.author)
this.authorCopy.updatedAt = data.author.updatedAt
this.authorCopy.imagePath = data.author.imagePath
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
@@ -196,7 +195,9 @@ export default {
.then((data) => { .then((data) => {
this.imageUrl = '' this.imageUrl = ''
this.$toast.success('Author image updated') this.$toast.success('Author image updated')
this.$store.commit('globals/showEditAuthorModal', data.author)
this.authorCopy.updatedAt = data.author.updatedAt
this.authorCopy.imagePath = data.author.imagePath
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
@@ -231,8 +232,11 @@ export default {
} else if (response.updated) { } else if (response.updated) {
if (response.author.imagePath) { if (response.author.imagePath) {
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess) this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
this.$store.commit('globals/showEditAuthorModal', response.author)
} else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound) } else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
this.authorCopy = {
...response.author
}
} else { } else {
this.$toast.info('No updates were made for Author') this.$toast.info('No updates were made for Author')
} }
@@ -6,7 +6,9 @@
</div> </div>
</template> </template>
<div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh"> <div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh">
<p class="text-xl font-bold pb-4">Changelog v{{ currentVersionNumber }}</p> <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" /> <div class="custom-text" v-html="compiledMarkedown" />
</div> </div>
</modals-modal> </modals-modal>
@@ -18,17 +20,9 @@ import { marked } from '@/static/libs/marked/index.js'
export default { export default {
props: { props: {
value: Boolean, value: Boolean,
changelog: String, versionData: {
currentVersion: String type: Object,
}, default: () => {}
watch: {
show: {
immediate: true,
handler(newVal) {
if (newVal) {
this.init()
}
}
} }
}, },
computed: { computed: {
@@ -40,16 +34,27 @@ export default {
this.$emit('input', val) this.$emit('input', val)
} }
}, },
dateFormat() {
return this.$store.state.serverSettings.dateFormat
},
changelog() {
return this.versionData?.currentVersionChangelog || 'No Changelog Available'
},
compiledMarkedown() { compiledMarkedown() {
return marked.parse(this.changelog, { gfm: true, breaks: true }) 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() { currentVersionNumber() {
return this.currentVersion return this.$config.version
} }
}, },
methods: { methods: {},
init() {}
},
mounted() {} mounted() {}
} }
</script> </script>
@@ -122,7 +122,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to get collections', error) console.error('Failed to get collections', error)
this.$toast.error('Failed to load collections') this.$toast.error(this.$strings.ToastFailedToLoadData)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@@ -46,7 +46,12 @@ export default {
ereaderDevice: { ereaderDevice: {
type: Object, type: Object,
default: () => null default: () => null
} },
users: {
type: Array,
default: () => []
},
loadUsers: Function
}, },
data() { data() {
return { return {
@@ -56,8 +61,7 @@ export default {
email: '', email: '',
availabilityOption: 'adminAndUp', availabilityOption: 'adminAndUp',
users: [] users: []
}, }
users: []
} }
}, },
watch: { watch: {
@@ -108,25 +112,13 @@ export default {
methods: { methods: {
availabilityOptionChanged(option) { availabilityOptionChanged(option) {
if (option === 'specificUsers' && !this.users.length) { if (option === 'specificUsers' && !this.users.length) {
this.loadUsers() this.callLoadUsers()
} }
}, },
async loadUsers() { async callLoadUsers() {
this.processing = true this.processing = true
this.users = await this.$axios await this.loadUsers()
.$get('/api/users') this.processing = false
.then((res) => {
return res.users.sort((a, b) => {
return a.createdAt - b.createdAt
})
})
.catch((error) => {
console.error('Failed', error)
return []
})
.finally(() => {
this.processing = false
})
}, },
submitForm() { submitForm() {
this.$refs.ereaderNameInput.blur() this.$refs.ereaderNameInput.blur()
@@ -226,10 +218,6 @@ export default {
this.newDevice.email = this.ereaderDevice.email this.newDevice.email = this.ereaderDevice.email
this.newDevice.availabilityOption = this.ereaderDevice.availabilityOption || 'adminOrUp' this.newDevice.availabilityOption = this.ereaderDevice.availabilityOption || 'adminOrUp'
this.newDevice.users = this.ereaderDevice.users || [] this.newDevice.users = this.ereaderDevice.users || []
if (this.newDevice.availabilityOption === 'specificUsers' && !this.users.length) {
this.loadUsers()
}
} else { } else {
this.newDevice.name = '' this.newDevice.name = ''
this.newDevice.email = '' this.newDevice.email = ''
@@ -1,10 +1,10 @@
<template> <template>
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6"> <div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
<div class="w-full mb-4"> <div class="w-full mb-4">
<tables-chapters-table v-if="chapters.length" :library-item="libraryItem" keep-open /> <tables-chapters-table v-if="chapters.length" :library-item="libraryItem" keep-open @close="closeModal" />
<div v-if="!chapters.length" class="py-4 text-center"> <div v-if="!chapters.length" class="py-4 text-center">
<p class="mb-8 text-xl">{{ $strings.MessageNoChapters }}</p> <p class="mb-8 text-xl">{{ $strings.MessageNoChapters }}</p>
<ui-btn v-if="userCanUpdate" :to="`/audiobook/${libraryItem.id}/chapters`">{{ $strings.ButtonAddChapters }}</ui-btn> <ui-btn v-if="userCanUpdate" :to="`/audiobook/${libraryItem.id}/chapters`" @click="clickAddChapters">{{ $strings.ButtonAddChapters }}</ui-btn>
</div> </div>
</div> </div>
</div> </div>
@@ -23,7 +23,7 @@ export default {
}, },
computed: { computed: {
media() { media() {
return this.libraryItem ? this.libraryItem.media || {} : {} return this.libraryItem?.media || {}
}, },
chapters() { chapters() {
return this.media.chapters || [] return this.media.chapters || []
@@ -32,6 +32,15 @@ export default {
return this.$store.getters['user/getUserCanUpdate'] return this.$store.getters['user/getUserCanUpdate']
} }
}, },
methods: {} methods: {
closeModal() {
this.$emit('close')
},
clickAddChapters() {
if (this.$route.name === 'audiobook-id-chapters' && this.$route.params?.id === this.libraryItem?.id) {
this.closeModal()
}
}
}
} }
</script> </script>
+9 -9
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative"> <div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
<div class="flex flex-wrap mb-4"> <div class="flex flex-col sm:flex-row mb-4">
<div class="relative"> <div class="relative self-center">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, libraryItemUpdatedAt, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, libraryItemUpdatedAt, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<!-- book cover overlay --> <!-- book cover overlay -->
@@ -14,7 +14,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-2 md:mt-0"> <div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-6 md:mt-0">
<div class="flex items-center"> <div class="flex items-center">
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 md:min-w-32"> <div v-if="userCanUpload" class="w-10 md:w-40 pr-2 md:min-w-32">
<ui-file-input ref="fileInput" @change="fileUploadSelected"> <ui-file-input ref="fileInput" @change="fileUploadSelected">
@@ -49,20 +49,20 @@
</div> </div>
</div> </div>
<form @submit.prevent="submitSearchForm"> <form @submit.prevent="submitSearchForm">
<div class="flex items-center justify-start -mx-1 h-20"> <div class="flex flex-wrap sm:flex-nowrap items-center justify-start -mx-1">
<div class="w-48 px-1"> <div class="w-48 flex-grow p-1">
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small /> <ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small />
</div> </div>
<div class="w-72 px-1"> <div class="w-72 flex-grow p-1">
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" /> <ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" />
</div> </div>
<div v-show="provider != 'itunes' && provider != 'audiobookcovers'" class="w-72 px-1"> <div v-show="provider != 'itunes' && provider != 'audiobookcovers'" class="w-72 flex-grow p-1">
<ui-text-input-with-label v-model="searchAuthor" :label="$strings.LabelAuthor" /> <ui-text-input-with-label v-model="searchAuthor" :label="$strings.LabelAuthor" />
</div> </div>
<ui-btn class="mt-5 ml-1" type="submit">{{ $strings.ButtonSearch }}</ui-btn> <ui-btn class="mt-5 ml-1 md:min-w-24" :padding-x="4" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
</div> </div>
</form> </form>
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-80 overflow-y-scroll mt-2 max-w-full"> <div v-if="hasSearched" class="flex items-center flex-wrap justify-center sm:max-h-80 sm:overflow-y-scroll mt-2 max-w-full">
<p v-if="!coversFound.length">{{ $strings.MessageNoCoversFound }}</p> <p v-if="!coversFound.length">{{ $strings.MessageNoCoversFound }}</p>
<template v-for="cover in coversFound"> <template v-for="cover in coversFound">
<div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === coverPath ? 'border-yellow-300' : ''" @click="updateCover(cover)"> <div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === coverPath ? 'border-yellow-300' : ''" @click="updateCover(cover)">
@@ -29,7 +29,7 @@
<td class="text-center w-20 min-w-20"> <td class="text-center w-20 min-w-20">
<p>{{ episode.episode }}</p> <p>{{ episode.episode }}</p>
</td> </td>
<td> <td dir="auto">
{{ episode.title }} {{ episode.title }}
</td> </td>
<td class="font-mono text-center"> <td class="font-mono text-center">
+50 -33
View File
@@ -32,7 +32,7 @@
</div> </div>
<p class="text-xl pl-3">{{ $strings.HeaderUpdateDetails }}</p> <p class="text-xl pl-3">{{ $strings.HeaderUpdateDetails }}</p>
</div> </div>
<ui-checkbox v-model="selectAll" checkbox-bg="bg" @input="selectAllToggled" /> <ui-checkbox v-model="selectAll" :label="$strings.LabelSelectAll" checkbox-bg="bg" @input="selectAllToggled" />
<form @submit.prevent="submitMatchUpdate"> <form @submit.prevent="submitMatchUpdate">
<div v-if="selectedMatchOrig.cover" class="flex flex-wrap md:flex-nowrap items-center justify-center"> <div v-if="selectedMatchOrig.cover" class="flex flex-wrap md:flex-nowrap items-center justify-center">
<div class="flex flex-grow items-center py-2"> <div class="flex flex-grow items-center py-2">
@@ -42,15 +42,15 @@
<div class="flex py-2"> <div class="flex py-2">
<div> <div>
<p class="text-center text-gray-200">New</p> <p class="text-center text-gray-200">{{ $strings.LabelNew }}</p>
<a :href="selectedMatch.cover" target="_blank" class="bg-primary"> <a :href="selectedMatch.cover" target="_blank" class="bg-primary">
<covers-preview-cover :src="selectedMatch.cover" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-preview-cover :src="selectedMatch.cover" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</a> </a>
</div> </div>
<div v-if="media.coverPath"> <div v-if="media.coverPath" class="ml-0.5">
<p class="text-center text-gray-200">Current</p> <p class="text-center text-gray-200">{{ $strings.LabelCurrent }}</p>
<a :href="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" target="_blank" class="bg-primary"> <a :href="$store.getters['globals/getLibraryItemCoverSrc'](libraryItem, null, true)" target="_blank" class="bg-primary">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrc'](libraryItem, null, true)" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</a> </a>
</div> </div>
</div> </div>
@@ -79,7 +79,7 @@
<div v-if="selectedMatchOrig.narrator" class="flex items-center py-2"> <div v-if="selectedMatchOrig.narrator" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.narrator" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.narrator" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.narrator" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" /> <ui-multi-select v-model="selectedMatch.narrator" :items="narrators" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" />
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.narratorName || '' }}</p> <p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.narratorName || '' }}</p>
</div> </div>
</div> </div>
@@ -122,7 +122,7 @@
<div v-if="selectedMatchOrig.tags" class="flex items-center py-2"> <div v-if="selectedMatchOrig.tags" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.tags" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.tags" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" /> <ui-multi-select v-model="selectedMatch.tags" :items="tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" />
<p v-if="media.tags" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ media.tags.join(', ') }}</p> <p v-if="media.tags" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ media.tags.join(', ') }}</p>
</div> </div>
</div> </div>
@@ -180,14 +180,14 @@
<ui-checkbox v-model="selectedMatchUsage.explicit" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.explicit" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4" :class="{ 'pt-4': mediaMetadata.explicit != null }"> <div class="flex-grow ml-4" :class="{ 'pt-4': mediaMetadata.explicit != null }">
<ui-checkbox v-model="selectedMatch.explicit" :label="$strings.LabelExplicit" :disabled="!selectedMatchUsage.explicit" :checkbox-bg="!selectedMatchUsage.explicit ? 'bg' : 'primary'" border-color="gray-600" label-class="pl-2 text-base font-semibold" /> <ui-checkbox v-model="selectedMatch.explicit" :label="$strings.LabelExplicit" :disabled="!selectedMatchUsage.explicit" :checkbox-bg="!selectedMatchUsage.explicit ? 'bg' : 'primary'" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
<p v-if="mediaMetadata.explicit != null" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.explicit ? 'Explicit (checked)' : 'Not Explicit (unchecked)' }}</p> <p v-if="mediaMetadata.explicit != null" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.explicit ? $strings.LabelExplicitChecked : $strings.LabelExplicitUnchecked }}</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.abridged != null" class="flex items-center pb-2" :class="{ 'pt-2': mediaMetadata.abridged == null }"> <div v-if="selectedMatchOrig.abridged != null" class="flex items-center pb-2" :class="{ 'pt-2': mediaMetadata.abridged == null }">
<ui-checkbox v-model="selectedMatchUsage.abridged" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.abridged" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4" :class="{ 'pt-4': mediaMetadata.abridged != null }"> <div class="flex-grow ml-4" :class="{ 'pt-4': mediaMetadata.abridged != null }">
<ui-checkbox v-model="selectedMatch.abridged" :label="$strings.LabelAbridged" :disabled="!selectedMatchUsage.abridged" :checkbox-bg="!selectedMatchUsage.abridged ? 'bg' : 'primary'" border-color="gray-600" label-class="pl-2 text-base font-semibold" /> <ui-checkbox v-model="selectedMatch.abridged" :label="$strings.LabelAbridged" :disabled="!selectedMatchUsage.abridged" :checkbox-bg="!selectedMatchUsage.abridged ? 'bg' : 'primary'" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
<p v-if="mediaMetadata.abridged != null" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.abridged ? 'Abridged (checked)' : 'Unabridged (unchecked)' }}</p> <p v-if="mediaMetadata.abridged != null" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.abridged ? $strings.LabelAbridgedChecked : $strings.LabelAbridgedUnchecked }}</p>
</div> </div>
</div> </div>
@@ -280,6 +280,9 @@ export default {
bookCoverAspectRatio() { bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio'] return this.$store.getters['libraries/getBookCoverAspectRatio']
}, },
filterData() {
return this.$store.state.libraries.filterData
},
providers() { providers() {
if (this.isPodcast) return this.$store.state.scanners.podcastProviders if (this.isPodcast) return this.$store.state.scanners.podcastProviders
return this.$store.state.scanners.providers return this.$store.state.scanners.providers
@@ -305,11 +308,16 @@ export default {
isPodcast() { isPodcast() {
return this.mediaType == 'podcast' return this.mediaType == 'podcast'
}, },
narrators() {
return this.filterData.narrators || []
},
genres() { genres() {
const filterData = this.$store.state.libraries.filterData || {} const currentGenres = this.filterData.genres || []
const currentGenres = filterData.genres || []
const selectedMatchGenres = this.selectedMatch.genres || [] const selectedMatchGenres = this.selectedMatch.genres || []
return [...new Set([...currentGenres, ...selectedMatchGenres])] return [...new Set([...currentGenres, ...selectedMatchGenres])]
},
tags() {
return this.filterData.tags || []
} }
}, },
methods: { methods: {
@@ -328,6 +336,17 @@ export default {
console.error('PersistProvider', error) console.error('PersistProvider', error)
} }
}, },
getDefaultBookProvider() {
let provider = localStorage.getItem('book-provider')
if (!provider) return 'google'
// Validate book provider
if (!this.$store.getters['scanners/checkBookProviderExists'](provider)) {
console.error('Stored book provider does not exist', provider)
localStorage.removeItem('book-provider')
return 'google'
}
return provider
},
getSearchQuery() { getSearchQuery() {
if (this.isPodcast) return `term=${encodeURIComponent(this.searchTitle)}` if (this.isPodcast) return `term=${encodeURIComponent(this.searchTitle)}`
var searchQuery = `provider=${this.provider}&fallbackTitleOnly=1&title=${encodeURIComponent(this.searchTitle)}` var searchQuery = `provider=${this.provider}&fallbackTitleOnly=1&title=${encodeURIComponent(this.searchTitle)}`
@@ -434,7 +453,9 @@ export default {
this.searchTitle = this.libraryItem.media.metadata.title this.searchTitle = this.libraryItem.media.metadata.title
this.searchAuthor = this.libraryItem.media.metadata.authorName || '' this.searchAuthor = this.libraryItem.media.metadata.authorName || ''
if (this.isPodcast) this.provider = 'itunes' if (this.isPodcast) this.provider = 'itunes'
else this.provider = localStorage.getItem('book-provider') || 'google' else {
this.provider = this.getDefaultBookProvider()
}
// Prefer using ASIN if set and using audible provider // Prefer using ASIN if set and using audible provider
if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) { if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) {
@@ -466,6 +487,12 @@ export default {
// match.genres = match.genres.join(',') // match.genres = match.genres.join(',')
match.genres = match.genres.split(',').map((g) => g.trim()) match.genres = match.genres.split(',').map((g) => g.trim())
} }
if (match.tags && !Array.isArray(match.tags)) {
match.tags = match.tags.split(',').map((g) => g.trim())
}
if (match.narrator && !Array.isArray(match.narrator)) {
match.narrator = match.narrator.split(',').map((g) => g.trim())
}
} }
console.log('Select Match', match) console.log('Select Match', match)
@@ -495,7 +522,10 @@ export default {
} else if (key === 'author' && !this.isPodcast) { } else if (key === 'author' && !this.isPodcast) {
var authors = this.selectedMatch[key] var authors = this.selectedMatch[key]
if (!Array.isArray(authors)) { if (!Array.isArray(authors)) {
authors = authors.split(',').map((au) => au.trim()) authors = authors
.split(',')
.map((au) => au.trim())
.filter((au) => !!au)
} }
var authorPayload = [] var authorPayload = []
authors.forEach((authorName) => authors.forEach((authorName) =>
@@ -506,11 +536,11 @@ export default {
) )
updatePayload.metadata.authors = authorPayload updatePayload.metadata.authors = authorPayload
} else if (key === 'narrator') { } else if (key === 'narrator') {
updatePayload.metadata.narrators = this.selectedMatch[key].split(',').map((v) => v.trim()) updatePayload.metadata.narrators = this.selectedMatch[key]
} else if (key === 'genres') { } else if (key === 'genres') {
updatePayload.metadata.genres = [...this.selectedMatch[key]] updatePayload.metadata.genres = [...this.selectedMatch[key]]
} else if (key === 'tags') { } else if (key === 'tags') {
updatePayload.tags = this.selectedMatch[key].split(',').map((v) => v.trim()) updatePayload.tags = this.selectedMatch[key]
} else if (key === 'itunesId') { } else if (key === 'itunesId') {
updatePayload.metadata.itunesId = Number(this.selectedMatch[key]) updatePayload.metadata.itunesId = Number(this.selectedMatch[key])
} else { } else {
@@ -533,24 +563,11 @@ export default {
// Persist in local storage // Persist in local storage
localStorage.setItem('selectedMatchUsage', JSON.stringify(this.selectedMatchUsage)) localStorage.setItem('selectedMatchUsage', JSON.stringify(this.selectedMatchUsage))
if (updatePayload.metadata.cover) {
const coverPayload = {
url: updatePayload.metadata.cover
}
const success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, coverPayload).catch((error) => {
console.error('Failed to update', error)
return false
})
if (success) {
this.$toast.success(this.$strings.ToastItemCoverUpdateSuccess)
} else {
this.$toast.error(this.$strings.ToastItemCoverUpdateFailed)
}
console.log('Updated cover')
delete updatePayload.metadata.cover
}
if (Object.keys(updatePayload).length) { if (Object.keys(updatePayload).length) {
if (updatePayload.metadata.cover) {
updatePayload.url = updatePayload.metadata.cover
delete updatePayload.metadata.cover
}
const mediaUpdatePayload = updatePayload const mediaUpdatePayload = updatePayload
const updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, mediaUpdatePayload).catch((error) => { const updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, mediaUpdatePayload).catch((error) => {
console.error('Failed to update', error) console.error('Failed to update', error)
@@ -20,7 +20,7 @@
</ui-tooltip> </ui-tooltip>
</div> </div>
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2"> <div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
<ui-text-input ref="maxEpisodesInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" /> <ui-text-input ref="maxEpisodesToDownloadInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" />
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded."> <ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded.">
<p class="pl-4 text-base"> <p class="pl-4 text-base">
Max new episodes to download per check Max new episodes to download per check
@@ -129,9 +129,12 @@ export default {
return return
} }
} }
if (this.$refs.maxEpisodesInput && this.$refs.maxEpisodesInput.isFocused) {
if (this.$refs.maxEpisodesInput?.isFocused) {
this.$refs.maxEpisodesInput.blur() this.$refs.maxEpisodesInput.blur()
return }
if (this.$refs.maxEpisodesToDownloadInput?.isFocused) {
this.$refs.maxEpisodesToDownloadInput.blur()
} }
const updatePayload = { const updatePayload = {
@@ -140,9 +143,11 @@ export default {
if (this.enableAutoDownloadEpisodes) { if (this.enableAutoDownloadEpisodes) {
updatePayload.autoDownloadSchedule = this.cronExpression updatePayload.autoDownloadSchedule = this.cronExpression
} }
this.newMaxEpisodesToKeep = Number(this.newMaxEpisodesToKeep)
if (this.newMaxEpisodesToKeep !== this.maxEpisodesToKeep) { if (this.newMaxEpisodesToKeep !== this.maxEpisodesToKeep) {
updatePayload.maxEpisodesToKeep = this.newMaxEpisodesToKeep updatePayload.maxEpisodesToKeep = this.newMaxEpisodesToKeep
} }
this.newMaxNewEpisodesToDownload = Number(this.newMaxNewEpisodesToDownload)
if (this.newMaxNewEpisodesToDownload !== this.maxNewEpisodesToDownload) { if (this.newMaxNewEpisodesToDownload !== this.maxNewEpisodesToDownload) {
updatePayload.maxNewEpisodesToDownload = this.newMaxNewEpisodesToDownload updatePayload.maxNewEpisodesToDownload = this.newMaxNewEpisodesToDownload
} }
+2 -11
View File
@@ -2,11 +2,8 @@
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6"> <div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6">
<p class="text-xl font-semibold mb-2">{{ $strings.HeaderAudiobookTools }}</p> <p class="text-xl font-semibold mb-2">{{ $strings.HeaderAudiobookTools }}</p>
<!-- alert for windows install -->
<widgets-alert v-if="isWindowsInstall" type="warning" class="my-8 text-base">Not supported for the Windows install yet</widgets-alert>
<!-- Merge to m4b --> <!-- Merge to m4b -->
<div v-if="showM4bDownload && !isWindowsInstall" class="w-full border border-black-200 p-4 my-8"> <div v-if="showM4bDownload" class="w-full border border-black-200 p-4 my-8">
<div class="flex flex-wrap items-center"> <div class="flex flex-wrap items-center">
<div> <div>
<p class="text-lg">{{ $strings.LabelToolsMakeM4b }}</p> <p class="text-lg">{{ $strings.LabelToolsMakeM4b }}</p>
@@ -23,7 +20,7 @@
</div> </div>
<!-- Embed Metadata --> <!-- Embed Metadata -->
<div v-if="mediaTracks.length && !isWindowsInstall" class="w-full border border-black-200 p-4 my-8"> <div v-if="mediaTracks.length" class="w-full border border-black-200 p-4 my-8">
<div class="flex items-center"> <div class="flex items-center">
<div> <div>
<p class="text-lg">{{ $strings.LabelToolsEmbedMetadata }}</p> <p class="text-lg">{{ $strings.LabelToolsEmbedMetadata }}</p>
@@ -111,12 +108,6 @@ export default {
}, },
isEncodeTaskRunning() { isEncodeTaskRunning() {
return this.encodeTask && !this.encodeTask?.isFinished return this.encodeTask && !this.encodeTask?.isFinished
},
isWindowsInstall() {
return this.Source == 'windows'
},
Source() {
return this.$store.state.Source
} }
}, },
methods: { methods: {
@@ -127,6 +127,7 @@ export default {
skipMatchingMediaWithIsbn: false, skipMatchingMediaWithIsbn: false,
autoScanCronExpression: null, autoScanCronExpression: null,
hideSingleBookSeries: false, hideSingleBookSeries: false,
onlyShowLaterBooksInContinueSeries: false,
metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
} }
} }
@@ -49,6 +49,31 @@
</ui-tooltip> </ui-tooltip>
</div> </div>
</div> </div>
<div v-if="isBookLibrary" class="py-3">
<div class="flex items-center">
<ui-toggle-switch v-model="onlyShowLaterBooksInContinueSeries" @input="formUpdated" />
<ui-tooltip :text="$strings.LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp">
<p class="pl-4 text-base">
{{ $strings.LabelSettingsOnlyShowLaterBooksInContinueSeries }}
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div>
</div>
<div v-if="isBookLibrary" class="py-3">
<div class="flex items-center">
<ui-toggle-switch v-model="epubsAllowScriptedContent" @input="formUpdated" />
<ui-tooltip :text="$strings.LabelSettingsEpubsAllowScriptedContentHelp">
<p class="pl-4 text-base">
{{ $strings.LabelSettingsEpubsAllowScriptedContent }}
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div>
</div>
<div v-if="isPodcastLibrary" class="py-3">
<ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-72" menu-max-height="200px" @input="formUpdated" />
</div>
</div> </div>
</template> </template>
@@ -69,7 +94,10 @@ export default {
skipMatchingMediaWithAsin: false, skipMatchingMediaWithAsin: false,
skipMatchingMediaWithIsbn: false, skipMatchingMediaWithIsbn: false,
audiobooksOnly: false, audiobooksOnly: false,
hideSingleBookSeries: false epubsAllowScriptedContent: false,
hideSingleBookSeries: false,
onlyShowLaterBooksInContinueSeries: false,
podcastSearchRegion: 'us'
} }
}, },
computed: { computed: {
@@ -85,6 +113,9 @@ export default {
isBookLibrary() { isBookLibrary() {
return this.mediaType === 'book' return this.mediaType === 'book'
}, },
isPodcastLibrary() {
return this.mediaType === 'podcast'
},
providers() { providers() {
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
return this.$store.state.scanners.providers return this.$store.state.scanners.providers
@@ -99,7 +130,10 @@ export default {
skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin, skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin,
skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn, skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn,
audiobooksOnly: !!this.audiobooksOnly, audiobooksOnly: !!this.audiobooksOnly,
hideSingleBookSeries: !!this.hideSingleBookSeries epubsAllowScriptedContent: !!this.epubsAllowScriptedContent,
hideSingleBookSeries: !!this.hideSingleBookSeries,
onlyShowLaterBooksInContinueSeries: !!this.onlyShowLaterBooksInContinueSeries,
podcastSearchRegion: this.podcastSearchRegion
} }
} }
}, },
@@ -112,7 +146,10 @@ export default {
this.skipMatchingMediaWithAsin = !!this.librarySettings.skipMatchingMediaWithAsin this.skipMatchingMediaWithAsin = !!this.librarySettings.skipMatchingMediaWithAsin
this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn
this.audiobooksOnly = !!this.librarySettings.audiobooksOnly this.audiobooksOnly = !!this.librarySettings.audiobooksOnly
this.epubsAllowScriptedContent = !!this.librarySettings.epubsAllowScriptedContent
this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries
this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries
this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us'
} }
}, },
mounted() { mounted() {
@@ -115,7 +115,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to get playlists', error) console.error('Failed to get playlists', error)
this.$toast.error('Failed to load user playlists') this.$toast.error(this.$strings.ToastFailedToLoadData)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@@ -33,7 +33,7 @@
<div class="break-words">{{ episode.title }}</div> <div class="break-words">{{ episode.title }}</div>
<widgets-podcast-type-indicator :type="episode.episodeType" /> <widgets-podcast-type-indicator :type="episode.episodeType" />
</div> </div>
<p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p> <p v-if="episode.subtitle" class="mb-1 text-sm text-gray-300 line-clamp-2">{{ episode.subtitle }}</p>
<p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p> <p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
</div> </div>
</div> </div>
@@ -15,8 +15,8 @@
<p class="text-xs text-gray-300">{{ podcastAuthor }}</p> <p class="text-xs text-gray-300">{{ podcastAuthor }}</p>
</div> </div>
</div> </div>
<p class="text-lg font-semibold mb-6">{{ title }}</p> <p dir="auto" class="text-lg font-semibold mb-6">{{ title }}</p>
<div v-if="description" class="default-style" v-html="description" /> <div v-if="description" dir="auto" class="default-style" v-html="description" />
<p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p> <p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p>
</div> </div>
</modals-modal> </modals-modal>
@@ -19,7 +19,7 @@
<div class="w-full p-1"> <div class="w-full p-1">
<ui-textarea-with-label v-model="newEpisode.subtitle" :label="$strings.LabelSubtitle" :rows="3" /> <ui-textarea-with-label v-model="newEpisode.subtitle" :label="$strings.LabelSubtitle" :rows="3" />
</div> </div>
<div class="w-full p-1 default-style"> <div class="w-full p-1">
<ui-rich-text-editor :label="$strings.LabelDescription" v-model="newEpisode.description" /> <ui-rich-text-editor :label="$strings.LabelDescription" v-model="newEpisode.description" />
</div> </div>
</div> </div>
@@ -18,7 +18,7 @@
<div v-for="(episode, index) in episodesFound" :key="index" class="w-full py-4 border-b border-white border-opacity-5 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer px-2" @click.stop="selectEpisode(episode)"> <div v-for="(episode, index) in episodesFound" :key="index" class="w-full py-4 border-b border-white border-opacity-5 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer px-2" @click.stop="selectEpisode(episode)">
<p v-if="episode.episode" class="font-semibold text-gray-200">#{{ episode.episode }}</p> <p v-if="episode.episode" class="font-semibold text-gray-200">#{{ episode.episode }}</p>
<p class="break-words mb-1">{{ episode.title }}</p> <p class="break-words mb-1">{{ episode.title }}</p>
<p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p> <p v-if="episode.subtitle" class="mb-1 text-sm text-gray-300 line-clamp-2">{{ episode.subtitle }}</p>
<p class="text-xs text-gray-400">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p> <p class="text-xs text-gray-400">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
</div> </div>
</template> </template>
@@ -1,22 +1,30 @@
<template> <template>
<div class="flex pt-4 pb-2 md:pt-0 md:pb-2"> <div class="flex items-center pt-4 pb-2 lg:pt-0 lg:pb-2">
<div class="flex-grow" /> <div class="flex-grow" />
<template v-if="!loading"> <template v-if="!loading">
<div class="cursor-pointer flex items-center justify-center text-gray-300 mr-4 md:mr-8" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter"> <ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 lg:mr-8">
<span class="material-icons text-2xl sm:text-3xl">first_page</span> <button :aria-label="$strings.ButtonPreviousChapter" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
</div> <span class="material-icons text-2xl sm:text-3xl">first_page</span>
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward"> </button>
<span class="material-icons text-2xl sm:text-3xl">replay_10</span> </ui-tooltip>
</div> <ui-tooltip direction="top" :text="$strings.ButtonJumpBackward">
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause"> <button :aria-label="$strings.ButtonJumpBackward" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
<span class="material-icons text-2xl sm:text-3xl">replay_10</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-icons text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span> <span class="material-icons text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
</div> </button>
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward"> <ui-tooltip direction="top" :text="$strings.ButtonJumpForward">
<span class="material-icons text-2xl sm:text-3xl">forward_10</span> <button :aria-label="$strings.ButtonJumpForward" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
</div> <span class="material-icons text-2xl sm:text-3xl">forward_10</span>
<div class="flex items-center justify-center ml-4 md:ml-8" :class="hasNextChapter ? 'text-gray-300 cursor-pointer' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter"> </button>
<span class="material-icons text-2xl sm:text-3xl">last_page</span> </ui-tooltip>
</div> <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-icons text-2xl sm:text-3xl">last_page</span>
</button>
</ui-tooltip>
<controls-playback-speed-control v-model="playbackRateInput" @input="playbackRateUpdated" @change="playbackRateChanged" /> <controls-playback-speed-control v-model="playbackRateInput" @input="playbackRateUpdated" @change="playbackRateChanged" />
</template> </template>
<template v-else> <template v-else>
+4 -1
View File
@@ -57,7 +57,6 @@ export default {
}, },
watch: { watch: {
duration: { duration: {
immediate: true,
handler() { handler() {
this.setChapterTicks() this.setChapterTicks()
} }
@@ -205,10 +204,14 @@ export default {
}, },
windowResize() { windowResize() {
this.setTrackWidth() this.setTrackWidth()
this.setChapterTicks()
this.updatePlayedTrackWidth()
this.updateBufferTrack()
} }
}, },
mounted() { mounted() {
this.setTrackWidth() this.setTrackWidth()
this.setChapterTicks()
window.addEventListener('resize', this.windowResize) window.addEventListener('resize', this.windowResize)
}, },
beforeDestroy() { beforeDestroy() {
+18 -16
View File
@@ -1,45 +1,45 @@
<template> <template>
<div class="w-full -mt-6"> <div class="w-full -mt-6">
<div class="w-full relative mb-1"> <div class="w-full relative mb-1">
<div class="absolute -top-10 md:top-0 right-0 lg:right-2 flex items-center h-full"> <div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full">
<!-- <span class="material-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> --> <!-- <span class="material-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
<ui-tooltip direction="top" :text="$strings.LabelVolume"> <ui-tooltip direction="top" :text="$strings.LabelVolume">
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden md:block" /> <controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden sm:block" />
</ui-tooltip> </ui-tooltip>
<ui-tooltip direction="top" :text="$strings.LabelSleepTimer"> <ui-tooltip v-if="!hideSleepTimer" direction="top" :text="$strings.LabelSleepTimer">
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')"> <button :aria-label="$strings.LabelSleepTimer" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
<span v-if="!sleepTimerSet" class="material-icons text-2xl">snooze</span> <span v-if="!sleepTimerSet" class="material-icons text-2xl">snooze</span>
<div v-else class="flex items-center"> <div v-else class="flex items-center">
<span class="material-icons text-lg text-warning">snooze</span> <span class="material-icons text-lg text-warning">snooze</span>
<p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p> <p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p>
</div> </div>
</div> </button>
</ui-tooltip> </ui-tooltip>
<ui-tooltip v-if="!isPodcast" direction="top" :text="$strings.LabelViewBookmarks"> <ui-tooltip v-if="!isPodcast && !hideBookmarks" direction="top" :text="$strings.LabelViewBookmarks">
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')"> <button :aria-label="$strings.LabelViewBookmarks" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
<span class="material-icons text-2xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span> <span class="material-icons text-2xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
</div> </button>
</ui-tooltip> </ui-tooltip>
<ui-tooltip v-if="chapters.length" direction="top" :text="$strings.LabelViewChapters"> <ui-tooltip v-if="chapters.length" direction="top" :text="$strings.LabelViewChapters">
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters"> <button :aria-label="$strings.LabelViewChapters" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
<span class="material-icons text-2xl">format_list_bulleted</span> <span class="material-icons text-2xl">format_list_bulleted</span>
</div> </button>
</ui-tooltip> </ui-tooltip>
<ui-tooltip v-if="playerQueueItems.length" direction="top" :text="$strings.LabelViewQueue"> <ui-tooltip v-if="playerQueueItems.length" direction="top" :text="$strings.LabelViewQueue">
<button class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')"> <button :aria-label="$strings.LabelViewQueue" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
<span class="material-icons text-2.5xl sm:text-3xl">playlist_play</span> <span class="material-icons text-2.5xl sm:text-3xl">playlist_play</span>
</button> </button>
</ui-tooltip> </ui-tooltip>
<ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack"> <ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack">
<div class="cursor-pointer text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack"> <button :aria-label="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack" class="text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack">
<span class="material-icons text-2xl sm:text-3xl transform transition-transform" :class="useChapterTrack ? 'rotate-180' : ''">timelapse</span> <span class="material-icons text-2xl sm:text-3xl transform transition-transform" :class="useChapterTrack ? 'rotate-180' : ''">timelapse</span>
</div> </button>
</ui-tooltip> </ui-tooltip>
</div> </div>
@@ -52,8 +52,8 @@
<p ref="currentTimestamp" class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">00:00:00</p> <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> <p class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto">&nbsp;/&nbsp;{{ progressPercent }}%</p>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="text-xs sm:text-sm text-gray-300 pt-0.5"> <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;({{ currentChapterIndex + 1 }} of {{ chapters.length }})</span> {{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400">&nbsp;({{ $getString('LabelPlayerChapterNumberMarker', [currentChapterIndex + 1, chapters.length]) }})</span>
</p> </p>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p> <p class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
@@ -78,7 +78,9 @@ export default {
}, },
sleepTimerSet: Boolean, sleepTimerSet: Boolean,
sleepTimerRemaining: Number, sleepTimerRemaining: Number,
isPodcast: Boolean isPodcast: Boolean,
hideBookmarks: Boolean,
hideSleepTimer: Boolean
}, },
data() { data() {
return { return {
+11 -1
View File
@@ -3,7 +3,7 @@
<div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" /> <div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" />
<div ref="content" class="relative text-white" :style="{ height: modalHeight, width: modalWidth }" v-click-outside="clickedOutside"> <div ref="content" class="relative text-white" :style="{ height: modalHeight, width: modalWidth }" v-click-outside="clickedOutside">
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300"> <div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
<p class="text-lg mb-6 mt-2 px-1" v-html="message" /> <p id="confirm-prompt-message" class="text-lg mb-6 mt-2 px-1" v-html="message" />
<ui-checkbox v-if="checkboxLabel" v-model="checkboxValue" checkbox-bg="bg" :label="checkboxLabel" label-class="pl-2 text-base" class="mb-6 px-1" /> <ui-checkbox v-if="checkboxLabel" v-model="checkboxValue" checkbox-bg="bg" :label="checkboxLabel" label-class="pl-2 text-base" class="mb-6 px-1" />
@@ -132,3 +132,13 @@ export default {
} }
} }
</script> </script>
<style>
#confirm-prompt-message code {
font-size: 1rem;
border-radius: 6px;
background-color: rgb(82, 82, 82);
color: white;
padding: 2px 4px;
}
</style>
+2 -2
View File
@@ -179,7 +179,7 @@ export default {
ebookLocation: this.page, ebookLocation: this.page,
ebookProgress: Math.max(0, Math.min(1, (Number(this.page) - 1) / Number(this.numPages))) ebookProgress: Math.max(0, Math.min(1, (Number(this.page) - 1) / Number(this.numPages)))
} }
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => { this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload, { progress: false }).catch((error) => {
console.error('ComicReader.updateProgress failed:', error) console.error('ComicReader.updateProgress failed:', error)
}) })
}, },
@@ -334,7 +334,7 @@ export default {
} }
}, },
parseFilenames(filenames) { parseFilenames(filenames) {
const acceptableImages = ['.jpeg', '.jpg', '.png'] const acceptableImages = ['.jpeg', '.jpg', '.png', '.webp']
var imageFiles = filenames.filter((f) => { var imageFiles = filenames.filter((f) => {
return acceptableImages.includes((Path.extname(f) || '').toLowerCase()) return acceptableImages.includes((Path.extname(f) || '').toLowerCase())
}) })
+11 -3
View File
@@ -46,7 +46,8 @@ export default {
font: 'serif', font: 'serif',
fontScale: 100, fontScale: 100,
lineSpacing: 115, lineSpacing: 115,
spread: 'auto' spread: 'auto',
textStroke: 0
} }
} }
}, },
@@ -63,6 +64,9 @@ export default {
libraryItemId() { libraryItemId() {
return this.libraryItem?.id return this.libraryItem?.id
}, },
allowScriptedContent() {
return this.$store.getters['libraries/getLibraryEpubsAllowScriptedContent']
},
hasPrev() { hasPrev() {
return !this.rendition?.location?.atStart return !this.rendition?.location?.atStart
}, },
@@ -106,11 +110,14 @@ export default {
const fontScale = this.ereaderSettings.fontScale / 100 const fontScale = this.ereaderSettings.fontScale / 100
const textStroke = this.ereaderSettings.textStroke / 100
return { return {
'*': { '*': {
color: `${fontColor}!important`, color: `${fontColor}!important`,
'background-color': `${backgroundColor}!important`, 'background-color': `${backgroundColor}!important`,
'line-height': lineSpacing * fontScale + 'rem!important' 'line-height': lineSpacing * fontScale + 'rem!important',
'-webkit-text-stroke': textStroke + 'px ' + fontColor + '!important'
}, },
a: { a: {
color: `${fontColor}!important` color: `${fontColor}!important`
@@ -192,7 +199,7 @@ export default {
*/ */
updateProgress(payload) { updateProgress(payload) {
if (!this.keepProgress) return if (!this.keepProgress) return
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => { this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload, { progress: false }).catch((error) => {
console.error('EpubReader.updateProgress failed:', error) console.error('EpubReader.updateProgress failed:', error)
}) })
}, },
@@ -316,6 +323,7 @@ export default {
reader.rendition = reader.book.renderTo('viewer', { reader.rendition = reader.book.renderTo('viewer', {
width: this.readerWidth, width: this.readerWidth,
height: this.readerHeight * 0.8, height: this.readerHeight * 0.8,
allowScriptedContent: this.allowScriptedContent,
spread: 'auto', spread: 'auto',
snap: true, snap: true,
manager: 'continuous', manager: 'continuous',
+15 -14
View File
@@ -23,13 +23,10 @@
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<div :style="{ width: pdfWidth + 'px', height: pdfHeight + 'px' }" class="overflow-auto"> <div :style="{ width: pdfWidth + 'px', height: pdfHeight + 'px' }" class="overflow-auto">
<div v-if="loadedRatio > 0 && loadedRatio < 1" style="background-color: green; color: white; text-align: center" :style="{ width: loadedRatio * 100 + '%' }">{{ Math.floor(loadedRatio * 100) }}%</div> <div v-if="loadedRatio > 0 && loadedRatio < 1" style="background-color: green; color: white; text-align: center" :style="{ width: loadedRatio * 100 + '%' }">{{ Math.floor(loadedRatio * 100) }}%</div>
<pdf ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md" :src="pdfDocInitParams" :page="page" :rotate="rotate" @progress="progressEvt" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event" @loaded="loadedEvt"></pdf> <pdf v-if="pdfDocInitParams" ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md" :src="pdfDocInitParams" :page="page" :rotate="rotate" @progress="progressEvt" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event" @loaded="loadedEvt"></pdf>
</div> </div>
</div> </div>
</div> </div>
<!-- <div class="text-center py-2 text-lg">
<p>{{ page }} / {{ numPages }}</p>
</div> -->
</div> </div>
</template> </template>
@@ -57,7 +54,8 @@ export default {
rotate: 0, rotate: 0,
loadedRatio: 0, loadedRatio: 0,
page: 1, page: 1,
numPages: 0 numPages: 0,
pdfDocInitParams: null
} }
}, },
computed: { computed: {
@@ -108,14 +106,6 @@ export default {
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}` return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
} }
return `/api/items/${this.libraryItemId}/ebook` return `/api/items/${this.libraryItemId}/ebook`
},
pdfDocInitParams() {
return {
url: this.ebookUrl,
httpHeaders: {
Authorization: `Bearer ${this.userToken}`
}
}
} }
}, },
methods: { methods: {
@@ -136,7 +126,7 @@ export default {
ebookLocation: this.page, ebookLocation: this.page,
ebookProgress: Math.max(0, Math.min(1, (Number(this.page) - 1) / Number(this.numPages))) ebookProgress: Math.max(0, Math.min(1, (Number(this.page) - 1) / Number(this.numPages)))
} }
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => { this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload, { progress: false }).catch((error) => {
console.error('EpubReader.updateProgress failed:', error) console.error('EpubReader.updateProgress failed:', error)
}) })
}, },
@@ -149,6 +139,7 @@ export default {
this.loadedRatio = progress this.loadedRatio = progress
}, },
numPagesLoaded(e) { numPagesLoaded(e) {
if (!e) return
this.numPages = e this.numPages = e
}, },
prev() { prev() {
@@ -167,12 +158,22 @@ export default {
resize() { resize() {
this.windowWidth = window.innerWidth this.windowWidth = window.innerWidth
this.windowHeight = window.innerHeight this.windowHeight = window.innerHeight
},
init() {
this.pdfDocInitParams = {
url: this.ebookUrl,
httpHeaders: {
Authorization: `Bearer ${this.userToken}`
}
}
} }
}, },
mounted() { mounted() {
this.windowWidth = window.innerWidth this.windowWidth = window.innerWidth
this.windowHeight = window.innerHeight this.windowHeight = window.innerHeight
window.addEventListener('resize', this.resize) window.addEventListener('resize', this.resize)
this.init()
}, },
beforeDestroy() { beforeDestroy() {
window.removeEventListener('resize', this.resize) window.removeEventListener('resize', this.resize)
+15 -2
View File
@@ -98,6 +98,12 @@
</div> </div>
<ui-range-input v-model="ereaderSettings.lineSpacing" :min="100" :max="300" :step="5" @input="settingsUpdated" /> <ui-range-input v-model="ereaderSettings.lineSpacing" :min="100" :max="300" :step="5" @input="settingsUpdated" />
</div> </div>
<div class="flex items-center mb-4">
<div class="w-40">
<p class="text-lg">{{ $strings.LabelFontBoldness }}:</p>
</div>
<ui-range-input v-model="ereaderSettings.textStroke" :min="0" :max="300" :step="5" @input="settingsUpdated" />
</div>
<div class="flex items-center"> <div class="flex items-center">
<div class="w-40"> <div class="w-40">
<p class="text-lg">{{ $strings.LabelLayout }}:</p> <p class="text-lg">{{ $strings.LabelLayout }}:</p>
@@ -130,7 +136,9 @@ export default {
font: 'serif', font: 'serif',
fontScale: 100, fontScale: 100,
lineSpacing: 115, lineSpacing: 115,
spread: 'auto' fontBoldness: 100,
spread: 'auto',
textStroke: 0
} }
} }
}, },
@@ -378,7 +386,12 @@ export default {
try { try {
const settings = localStorage.getItem('ereaderSettings') const settings = localStorage.getItem('ereaderSettings')
if (settings) { if (settings) {
this.ereaderSettings = JSON.parse(settings) const _ereaderSettings = JSON.parse(settings)
for (const key in this.ereaderSettings) {
if (_ereaderSettings[key] !== undefined) {
this.ereaderSettings[key] = _ereaderSettings[key]
}
}
this.settingsUpdated() this.settingsUpdated()
} }
} catch (error) { } catch (error) {
+16 -16
View File
@@ -1,45 +1,45 @@
<template> <template>
<div class="flex flex-wrap justify-center mt-6"> <div class="flex flex-wrap justify-center mt-6">
<div class="flex px-2"> <div class="flex p-2">
<svg class="h-14 w-14 md:h-18 md:w-18" viewBox="0 0 24 24"> <svg class="h-14 w-14" viewBox="0 0 24 24">
<path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" /> <path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" />
</svg> </svg>
<div class="px-2"> <div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalItems }}</p> <p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalItems) }}</p>
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsInLibrary }}</p> <p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsInLibrary }}</p>
</div> </div>
</div> </div>
<div class="flex px-4"> <div class="flex p-2">
<span class="material-icons text-7xl">show_chart</span> <span class="material-icons text-5xl py-1">show_chart</span>
<div class="px-1"> <div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalTime }}</p> <p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalTime) }}</p>
<p class="text-xs md:text-sm text-white text-opacity-80">{{ useOverallHours ? $strings.LabelStatsOverallHours : $strings.LabelStatsOverallDays }}</p> <p class="text-xs md:text-sm text-white text-opacity-80">{{ useOverallHours ? $strings.LabelStatsOverallHours : $strings.LabelStatsOverallDays }}</p>
</div> </div>
</div> </div>
<div v-if="isBookLibrary" class="flex px-4"> <div v-if="isBookLibrary" class="flex p-2">
<svg class="h-14 w-14 md:h-18 md:w-18" viewBox="0 0 24 24"> <svg class="h-14 w-14" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" /> <path fill="currentColor" d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" />
</svg> </svg>
<div class="px-1"> <div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalAuthors }}</p> <p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalAuthors) }}</p>
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAuthors }}</p> <p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAuthors }}</p>
</div> </div>
</div> </div>
<div class="flex px-4"> <div class="flex p-2">
<span class="material-icons-outlined text-6xl pt-1">insert_drive_file</span> <span class="material-icons-outlined text-5xl pt-1">insert_drive_file</span>
<div class="px-1"> <div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalSizeNum }}</p> <p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalSizeNum) }}</p>
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelSize }} ({{ totalSizeMod }})</p> <p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelSize }} ({{ totalSizeMod }})</p>
</div> </div>
</div> </div>
<div class="flex px-4"> <div class="flex p-2">
<span class="material-icons-outlined text-6xl pt-1">audio_file</span> <span class="material-icons-outlined text-5xl pt-1">audio_file</span>
<div class="px-1"> <div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ numAudioTracks }}</p> <p class="text-4.5xl leading-none font-bold">{{ $formatNumber(numAudioTracks) }}</p>
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAudioTracks }}</p> <p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAudioTracks }}</p>
</div> </div>
</div> </div>
+1 -1
View File
@@ -271,7 +271,7 @@ export default {
this.$emit('update:processing', true) this.$emit('update:processing', true)
this.yearStats = await this.$axios.$get(`/api/me/stats/year/${this.year}`).catch((err) => { this.yearStats = await this.$axios.$get(`/api/me/stats/year/${this.year}`).catch((err) => {
console.error('Failed to load stats for year', err) console.error('Failed to load stats for year', err)
this.$toast.error('Failed to load year stats') this.$toast.error(this.$strings.ToastFailedToLoadData)
return null return null
}) })
await this.initCanvas() await this.initCanvas()
+18 -13
View File
@@ -7,9 +7,10 @@
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<p class="hidden md:block text-xl font-semibold">{{ yearInReviewYear }} Year in Review</p> <p class="hidden md:block text-xl font-semibold">{{ $getString('HeaderYearReview', [yearInReviewYear]) }}</p>
<div class="hidden md:block flex-grow" /> <div class="hidden md:block flex-grow" />
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? 'Hide Year in Review' : 'See Year in Review' }}</ui-btn> <ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide :
$strings.LabelYearReviewShow }}</ui-btn>
</div> </div>
<!-- your year in review --> <!-- your year in review -->
@@ -20,24 +21,27 @@
<!-- previous button --> <!-- previous button -->
<ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--"> <ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--">
<span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span> <span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
<span class="hidden sm:inline-block pr-2">Previous</span> <span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
</ui-btn> </ui-btn>
<!-- share button --> <!-- share button -->
<ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview"> Share </ui-btn> <ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview">{{
$strings.ButtonShare }}
</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="hidden sm:block text-lg font-semibold">Your Year in Review ({{ yearInReviewVariant + 1 }})</p> <p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelPersonalYearReview', [yearInReviewVariant + 1]) }}
</p>
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p> <p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
<!-- refresh button --> <!-- refresh button -->
<ui-btn small :disabled="processingYearInReview" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReview"> <ui-btn small :disabled="processingYearInReview" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReview">
<span class="hidden sm:inline-block">Refresh</span> <span class="hidden sm:inline-block">{{ $strings.ButtonRefresh }}</span>
<span class="material-icons sm:!hidden text-lg py-px">refresh</span> <span class="material-icons sm:!hidden text-lg py-px">refresh</span>
</ui-btn> </ui-btn>
<!-- next button --> <!-- next button -->
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++"> <ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
<span class="hidden sm:inline-block pl-2">Next</span> <span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span> <span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn> </ui-btn>
</div> </div>
@@ -46,7 +50,7 @@
<!-- your year in review short --> <!-- your year in review short -->
<div class="w-full max-w-[800px] mx-auto my-4"> <div class="w-full max-w-[800px] mx-auto my-4">
<!-- share button --> <!-- share button -->
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex sm:hidden items-center font-semibold mb-1" @click="shareYearInReviewShort"> Share </ui-btn> <ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex sm:hidden items-center font-semibold mb-1" @click="shareYearInReviewShort">{{ $strings.ButtonShare }}</ui-btn>
<stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" /> <stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" />
</div> </div>
@@ -56,24 +60,25 @@
<!-- previous button --> <!-- previous button -->
<ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--"> <ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--">
<span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span> <span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
<span class="hidden sm:inline-block pr-2">Previous</span> <span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
</ui-btn> </ui-btn>
<!-- share button --> <!-- share button -->
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer"> Share </ui-btn> <ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer">{{ $strings.ButtonShare }}
</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="hidden sm:block text-lg font-semibold">Server Year in Review ({{ yearInReviewServerVariant + 1 }})</p> <p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelServerYearReview', [yearInReviewServerVariant + 1]) }}</p>
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p> <p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
<!-- refresh button --> <!-- refresh button -->
<ui-btn small :disabled="processingYearInReviewServer" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReviewServer"> <ui-btn small :disabled="processingYearInReviewServer" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReviewServer">
<span class="hidden sm:inline-block">Refresh</span> <span class="hidden sm:inline-block">{{ $strings.ButtonRefresh }}</span>
<span class="material-icons sm:!hidden text-lg py-px">refresh</span> <span class="material-icons sm:!hidden text-lg py-px">refresh</span>
</ui-btn> </ui-btn>
<!-- next button --> <!-- next button -->
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++"> <ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
<span class="hidden sm:inline-block pl-2">Next</span> <span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span> <span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn> </ui-btn>
</div> </div>
@@ -250,7 +250,7 @@ export default {
this.$emit('update:processing', true) this.$emit('update:processing', true)
this.yearStats = await this.$axios.$get(`/api/stats/year/${this.year}`).catch((err) => { this.yearStats = await this.$axios.$get(`/api/stats/year/${this.year}`).catch((err) => {
console.error('Failed to load stats for year', err) console.error('Failed to load stats for year', err)
this.$toast.error('Failed to load year stats') this.$toast.error(this.$strings.ToastFailedToLoadData)
return null return null
}) })
await this.initCanvas() await this.initCanvas()
@@ -180,7 +180,7 @@ export default {
this.$emit('update:processing', true) this.$emit('update:processing', true)
this.yearStats = await this.$axios.$get(`/api/me/stats/year/${this.year}`).catch((err) => { this.yearStats = await this.$axios.$get(`/api/me/stats/year/${this.year}`).catch((err) => {
console.error('Failed to load stats for year', err) console.error('Failed to load stats for year', err)
this.$toast.error('Failed to load year stats') this.$toast.error(this.$strings.ToastFailedToLoadData)
return null return null
}) })
await this.initCanvas() await this.initCanvas()
@@ -94,11 +94,11 @@ export default {
this.$axios this.$axios
.$delete(`/api/items/${this.libraryItemId}/file/${this.track.audioFile.ino}`) .$delete(`/api/items/${this.libraryItemId}/file/${this.track.audioFile.ino}`)
.then(() => { .then(() => {
this.$toast.success('File deleted') this.$toast.success(this.$strings.ToastDeleteFileSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to delete file', error) console.error('Failed to delete file', error)
this.$toast.error('Failed to delete file') this.$toast.error(this.$strings.ToastDeleteFileFailed)
}) })
} }
}, },
+12 -6
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="text-center mt-4"> <div class="text-center mt-4 relative">
<div class="flex py-4"> <div class="flex py-4">
<ui-file-input ref="fileInput" class="mr-2" accept=".audiobookshelf" @change="backupUploaded">{{ $strings.ButtonUploadBackup }}</ui-file-input> <ui-file-input ref="fileInput" class="mr-2" accept=".audiobookshelf" @change="backupUploaded">{{ $strings.ButtonUploadBackup }}</ui-file-input>
<div class="flex-grow" /> <div class="flex-grow" />
@@ -54,6 +54,10 @@
</div> </div>
</div> </div>
</prompt-dialog> </prompt-dialog>
<div v-if="isApplyingBackup" class="absolute inset-0 w-full h-full flex items-center justify-center bg-black/20 rounded-md">
<ui-loading-indicator />
</div>
</div> </div>
</template> </template>
@@ -64,6 +68,7 @@ export default {
showConfirmApply: false, showConfirmApply: false,
selectedBackup: null, selectedBackup: null,
isBackingUp: false, isBackingUp: false,
isApplyingBackup: false,
processing: false, processing: false,
backups: [] backups: []
} }
@@ -85,19 +90,21 @@ export default {
}, },
confirm() { confirm() {
this.showConfirmApply = false this.showConfirmApply = false
this.isApplyingBackup = true
this.$axios this.$axios
.$get(`/api/backups/${this.selectedBackup.id}/apply`) .$get(`/api/backups/${this.selectedBackup.id}/apply`)
.then(() => { .then(() => {
this.isBackingUp = false
location.replace('/config/backups?backup=1') location.replace('/config/backups?backup=1')
}) })
.catch((error) => { .catch((error) => {
this.isBackingUp = false
console.error('Failed to apply backup', error) console.error('Failed to apply backup', error)
const errorMsg = error.response.data || this.$strings.ToastBackupRestoreFailed const errorMsg = error.response.data || this.$strings.ToastBackupRestoreFailed
this.$toast.error(errorMsg) this.$toast.error(errorMsg)
}) })
.finally(() => {
this.isApplyingBackup = false
})
}, },
deleteBackupClick(backup) { deleteBackupClick(backup) {
if (confirm(this.$getString('MessageConfirmDeleteBackup', [this.$formatDatetime(backup.createdAt, this.dateFormat, this.timeFormat)]))) { if (confirm(this.$getString('MessageConfirmDeleteBackup', [this.$formatDatetime(backup.createdAt, this.dateFormat, this.timeFormat)]))) {
@@ -164,12 +171,12 @@ export default {
this.$axios this.$axios
.$get('/api/backups') .$get('/api/backups')
.then((data) => { .then((data) => {
this.$emit('loaded', data.backupLocation) this.$emit('loaded', data)
this.setBackups(data.backups || []) this.setBackups(data.backups || [])
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to load backups', error) console.error('Failed to load backups', error)
this.$toast.error('Failed to load backups') this.$toast.error(this.$strings.ToastFailedToLoadData)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@@ -180,7 +187,6 @@ export default {
this.loadBackups() this.loadBackups()
if (this.$route.query.backup) { if (this.$route.query.backup) {
this.$toast.success('Backup applied successfully') this.$toast.success('Backup applied successfully')
this.$router.replace('/config')
} }
} }
} }
+11 -5
View File
@@ -4,7 +4,7 @@
<p class="pr-4">{{ $strings.HeaderChapters }}</p> <p class="pr-4">{{ $strings.HeaderChapters }}</p>
<span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ chapters.length }}</span> <span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ chapters.length }}</span>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="userCanUpdate" small :to="`/audiobook/${libraryItemId}/chapters`" color="primary" class="mr-2">{{ $strings.ButtonEditChapters }}</ui-btn> <ui-btn v-if="userCanUpdate" small :to="`/audiobook/${libraryItemId}/chapters`" color="primary" class="mr-2" @click="clickEditChapters">{{ $strings.ButtonEditChapters }}</ui-btn>
<div v-if="!keepOpen" class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="expanded ? 'transform rotate-180' : ''"> <div v-if="!keepOpen" class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="expanded ? 'transform rotate-180' : ''">
<span class="material-icons text-4xl">expand_more</span> <span class="material-icons text-4xl">expand_more</span>
</div> </div>
@@ -15,20 +15,20 @@
<th class="text-left w-16"><span class="px-4">Id</span></th> <th class="text-left w-16"><span class="px-4">Id</span></th>
<th class="text-left">{{ $strings.LabelTitle }}</th> <th class="text-left">{{ $strings.LabelTitle }}</th>
<th class="text-center">{{ $strings.LabelStart }}</th> <th class="text-center">{{ $strings.LabelStart }}</th>
<th class="text-center">{{ $strings.LabelEnd }}</th> <th class="text-center">{{ $strings.LabelDuration }}</th>
</tr> </tr>
<tr v-for="chapter in chapters" :key="chapter.id"> <tr v-for="chapter in chapters" :key="chapter.id">
<td class="text-left"> <td class="text-left">
<p class="px-4">{{ chapter.id }}</p> <p class="px-4">{{ chapter.id }}</p>
</td> </td>
<td> <td dir="auto">
{{ chapter.title }} {{ chapter.title }}
</td> </td>
<td class="font-mono text-center hover:underline cursor-pointer" @click.stop="goToTimestamp(chapter.start)"> <td class="font-mono text-center hover:underline cursor-pointer" @click.stop="goToTimestamp(chapter.start)">
{{ $secondsToTimestamp(chapter.start) }} {{ $secondsToTimestamp(chapter.start) }}
</td> </td>
<td class="font-mono text-center hover:underline cursor-pointer" @click.stop="goToTimestamp(chapter.start)"> <td class="font-mono text-center">
{{ $secondsToTimestamp(chapter.end) }} {{ $secondsToTimestamp(Math.max(0, chapter.end - chapter.start)) }}
</td> </td>
</tr> </tr>
</table> </table>
@@ -107,6 +107,12 @@ export default {
} }
this.$store.commit('globals/setConfirmPrompt', payload) this.$store.commit('globals/setConfirmPrompt', payload)
} }
},
clickEditChapters() {
// Used for Chapters tab in modal
if (this.$route.name === 'audiobook-id-chapters' && this.$route.params?.id === this.libraryItem?.id) {
this.$emit('close')
}
} }
}, },
mounted() {} mounted() {}
@@ -0,0 +1,127 @@
<template>
<div class="min-h-40">
<table v-if="providers.length" id="providers">
<tr>
<th>{{ $strings.LabelName }}</th>
<th>URL</th>
<th>Authorization Header Value</th>
<th class="w-12"></th>
</tr>
<tr v-for="provider in providers" :key="provider.id">
<td class="text-sm">{{ provider.name }}</td>
<td class="text-sm">{{ provider.url }}</td>
<td class="text-sm">
<span v-if="provider.authHeaderValue" class="custom-provider-api-key">{{ provider.authHeaderValue }}</span>
</td>
<td class="py-0">
<div class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="removeProvider(provider)">
<button type="button" :aria-label="$strings.ButtonDelete" class="material-icons text-base">delete</button>
</div>
</td>
</tr>
</table>
<div v-else-if="!processing" class="text-center py-8">
<p class="text-lg">{{ $strings.LabelNoCustomMetadataProviders }}</p>
</div>
<div v-if="processing" class="absolute inset-0 h-full flex items-center justify-center bg-black/40 rounded-md">
<ui-loading-indicator />
</div>
</div>
</template>
<script>
export default {
props: {
providers: {
type: Array,
default: () => []
},
processing: Boolean
},
data() {
return {}
},
methods: {
removeProvider(provider) {
const payload = {
message: `Are you sure you want remove custom metadata provider "${provider.name}"?`,
callback: (confirmed) => {
if (confirmed) {
this.$emit('update:processing', true)
this.$axios
.$delete(`/api/custom-metadata-providers/${provider.id}`)
.then(() => {
this.$toast.success('Provider removed')
this.$emit('removed', provider.id)
})
.catch((error) => {
console.error('Failed to remove provider', error)
this.$toast.error('Failed to remove provider')
})
.finally(() => {
this.$emit('update:processing', false)
})
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
}
}
}
</script>
<style>
#providers {
table-layout: fixed;
border-collapse: collapse;
border: 1px solid #474747;
width: 100%;
}
#providers td,
#providers th {
/* border: 1px solid #2e2e2e; */
padding: 8px 8px;
text-align: left;
}
#providers td.py-0 {
padding: 0px 8px;
}
#providers tr:nth-child(even) {
background-color: #373838;
}
#providers tr:nth-child(odd) {
background-color: #2f2f2f;
}
#providers tr:hover {
background-color: #444;
}
#providers th {
font-size: 0.8rem;
font-weight: 600;
padding-top: 5px;
padding-bottom: 5px;
background-color: #272727;
}
.custom-provider-api-key {
padding: 1px;
background-color: #272727;
border-radius: 4px;
color: transparent;
transition: color, background-color 0.5s ease;
}
.custom-provider-api-key:hover {
background-color: transparent;
color: white;
}
</style>
@@ -115,11 +115,11 @@ export default {
this.$axios this.$axios
.$delete(`/api/items/${this.libraryItemId}/file/${this.file.ino}`) .$delete(`/api/items/${this.libraryItemId}/file/${this.file.ino}`)
.then(() => { .then(() => {
this.$toast.success('File deleted') this.$toast.success(this.$strings.ToastDeleteFileSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to delete file', error) console.error('Failed to delete file', error)
this.$toast.error('Failed to delete file') this.$toast.error(this.$strings.ToastDeleteFileFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@@ -89,11 +89,11 @@ export default {
this.$axios this.$axios
.$delete(`/api/items/${this.libraryItemId}/file/${this.file.ino}`) .$delete(`/api/items/${this.libraryItemId}/file/${this.file.ino}`)
.then(() => { .then(() => {
this.$toast.success('File deleted') this.$toast.success(this.$strings.ToastDeleteFileSuccess)
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to delete file', error) console.error('Failed to delete file', error)
this.$toast.error('Failed to delete file') this.$toast.error(this.$strings.ToastDeleteFileFailed)
}) })
} }
}, },
+1 -1
View File
@@ -10,7 +10,7 @@
<th class="w-32 hidden sm:table-cell">{{ $strings.LabelCreatedAt }}</th> <th class="w-32 hidden sm:table-cell">{{ $strings.LabelCreatedAt }}</th>
<th class="w-32"></th> <th class="w-32"></th>
</tr> </tr>
<tr v-for="user in users" :key="user.id" class="cursor-pointer" :class="user.isActive ? '' : 'bg-error bg-opacity-20'" @click="$router.push(`/config/users/${user.id}`)"> <tr v-for="user in users" :key="user.id" class="cursor-pointer" :class="user.isActive ? '' : '!bg-error/10'" @click="$router.push(`/config/users/${user.id}`)">
<td> <td>
<div class="flex items-center"> <div class="flex items-center">
<widgets-online-indicator :value="!!usersOnline[user.id]" /> <widgets-online-indicator :value="!!usersOnline[user.id]" />
@@ -38,7 +38,7 @@
<div v-if="userCanUpdate" class="mx-1" :class="isHovering ? '' : 'ml-6'"> <div v-if="userCanUpdate" class="mx-1" :class="isHovering ? '' : 'ml-6'">
<ui-icon-btn icon="edit" borderless @click="clickEdit" /> <ui-icon-btn icon="edit" borderless @click="clickEdit" />
</div> </div>
<div v-if="userCanDelete" class="mx-1"> <div class="mx-1" :class="isHovering ? '' : 'ml-6'">
<ui-icon-btn icon="close" borderless @click="removeClick" /> <ui-icon-btn icon="close" borderless @click="removeClick" />
</div> </div>
</div> </div>
@@ -75,8 +75,7 @@ export default {
}, },
computed: { computed: {
translateDistance() { translateDistance() {
if (!this.userCanUpdate && !this.userCanDelete) return 'translate-x-0' if (!this.userCanUpdate) return '-translate-x-12'
else if (!this.userCanUpdate || !this.userCanDelete) return '-translate-x-12'
return '-translate-x-24' return '-translate-x-24'
}, },
libraryItem() { libraryItem() {
@@ -20,7 +20,7 @@
<td class="px-4"> <td class="px-4">
<div class="flex items-center"> <div class="flex items-center">
<nuxt-link :to="`/item/${downloadQueued.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ downloadQueued.podcastTitle }}</nuxt-link> <nuxt-link :to="`/item/${downloadQueued.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ downloadQueued.podcastTitle }}</nuxt-link>
<widgets-explicit-indicator :explicit="downloadQueued.podcastExplicit" /> <widgets-explicit-indicator v-if="downloadQueued.podcastExplicit" />
</div> </div>
</td> </td>
<td> <td>
@@ -30,7 +30,7 @@
<widgets-podcast-type-indicator :type="downloadQueued.episodeType" /> <widgets-podcast-type-indicator :type="downloadQueued.episodeType" />
</div> </div>
</td> </td>
<td class="px-4"> <td dir="auto" class="px-4">
{{ downloadQueued.episodeDisplayTitle }} {{ downloadQueued.episodeDisplayTitle }}
</td> </td>
<td class="text-xs"> <td class="text-xs">
@@ -2,13 +2,13 @@
<div :id="`lazy-episode-${index}`" class="w-full h-full cursor-pointer" @mouseover="mouseover" @mouseleave="mouseleave"> <div :id="`lazy-episode-${index}`" class="w-full h-full cursor-pointer" @mouseover="mouseover" @mouseleave="mouseleave">
<div class="flex" @click="clickedEpisode"> <div class="flex" @click="clickedEpisode">
<div class="flex-grow"> <div class="flex-grow">
<div class="flex items-center"> <div dir="auto" class="flex items-center">
<span class="text-sm font-semibold">{{ episodeTitle }}</span> <span class="text-sm font-semibold">{{ episodeTitle }}</span>
<widgets-podcast-type-indicator :type="episodeType" /> <widgets-podcast-type-indicator :type="episodeType" />
</div> </div>
<div class="h-10 flex items-center mt-1.5 mb-0.5"> <div class="h-10 flex items-center mt-1.5 mb-0.5 overflow-hidden">
<p class="text-sm text-gray-200 episode-subtitle" v-html="episodeSubtitle"></p> <p class="text-sm text-gray-200 line-clamp-2" v-html="episodeSubtitle"></p>
</div> </div>
<div class="h-8 flex items-center"> <div class="h-8 flex items-center">
<div class="w-full inline-flex justify-between max-w-xl"> <div class="w-full inline-flex justify-between max-w-xl">
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<nuxt-link v-if="to" :to="to" class="abs-btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList"> <nuxt-link v-if="to" :to="to" class="abs-btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList" @click.native="click">
<slot /> <slot />
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100"> <div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24"> <svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
+6 -2
View File
@@ -13,7 +13,7 @@
</button> </button>
<transition name="menu"> <transition name="menu">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="listbox"> <ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="listbox" :style="{ maxHeight: menuMaxHeight }">
<template v-for="item in itemsToShow"> <template v-for="item in itemsToShow">
<li :key="item.value" class="text-gray-100 relative py-2 cursor-pointer hover:bg-black-400" :id="'listbox-option-' + item.value" role="option" tabindex="0" @keyup.enter="clickedOption(item.value)" @click="clickedOption(item.value)"> <li :key="item.value" class="text-gray-100 relative py-2 cursor-pointer hover:bg-black-400" :id="'listbox-option-' + item.value" role="option" tabindex="0" @keyup.enter="clickedOption(item.value)" @click="clickedOption(item.value)">
<div class="flex items-center"> <div class="flex items-center">
@@ -41,7 +41,11 @@ export default {
default: () => [] default: () => []
}, },
disabled: Boolean, disabled: Boolean,
small: Boolean small: Boolean,
menuMaxHeight: {
type: String,
default: '224px'
}
}, },
data() { data() {
return { return {
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<input ref="fileInput" type="file" :accept="accept" class="hidden" @change="inputChanged" /> <input ref="fileInput" type="file" :accept="accept" class="hidden" @change="inputChanged" />
<ui-btn @click="clickUpload" color="primary" class="hidden md:block" type="text"><slot /></ui-btn> <ui-btn @click="clickUpload" color="primary" class="hidden md:block w-full" type="text"><slot /></ui-btn>
<ui-icon-btn @click="clickUpload" icon="upload" class="block md:hidden" /> <ui-icon-btn @click="clickUpload" icon="upload" class="block md:hidden" />
</div> </div>
</template> </template>
+8 -2
View File
@@ -83,15 +83,21 @@ export default {
}, },
async updateLibrary(library) { async updateLibrary(library) {
var currLibraryId = this.currentLibraryId var currLibraryId = this.currentLibraryId
if (currLibraryId === library.id) {
return
}
this.disabled = true this.disabled = true
await this.$store.dispatch('libraries/fetch', library.id) await this.$store.dispatch('libraries/fetch', library.id)
if (this.$route.name.startsWith('config')) { if (this.$route.name.startsWith('config')) {
// No need to refresh // No need to refresh
} else if (this.$route.name.startsWith('library')) { } else if (this.$route.name.startsWith('library') && this.$route.name !== 'library-library-series-id') {
var newRoute = this.$route.path.replace(currLibraryId, library.id) const newRoute = this.$route.path.replace(currLibraryId, library.id)
this.$router.push(newRoute) this.$router.push(newRoute)
} else if (this.$route.name === 'library-library-series-id' && library.mediaType === 'book') {
// For series item page redirect to root series page
this.$router.push(`/library/${library.id}/bookshelf/series`)
} else { } else {
this.$router.push(`/library/${library.id}`) this.$router.push(`/library/${library.id}`)
} }
+95 -19
View File
@@ -11,13 +11,13 @@
</div> </div>
{{ item }} {{ item }}
</div> </div>
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" style="min-width: 40px; width: 40px" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" /> <input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
</div> </div>
</form> </form>
<ul ref="menu" v-show="showMenu" class="absolute z-60 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label"> <ul ref="menu" v-show="showMenu" class="absolute z-60 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in itemsToShow"> <template v-for="item in itemsToShow">
<li :key="item" class="text-gray-50 select-none relative py-2 pr-9 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-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>
<div class="flex items-center"> <div class="flex items-center">
<span class="font-normal ml-3 block truncate">{{ item }}</span> <span class="font-normal ml-3 block truncate">{{ item }}</span>
</div> </div>
@@ -54,7 +54,7 @@ export default {
menuDisabled: { menuDisabled: {
type: Boolean, type: Boolean,
default: false default: false
}, }
}, },
data() { data() {
return { return {
@@ -62,7 +62,9 @@ export default {
currentSearch: null, currentSearch: null,
typingTimeout: null, typingTimeout: null,
isFocused: false, isFocused: false,
menu: null menu: null,
filteredItems: null,
selectedMenuItemIndex: null
} }
}, },
watch: { watch: {
@@ -91,24 +93,63 @@ export default {
return classes.join(' ') return classes.join(' ')
}, },
itemsToShow() { itemsToShow() {
if (!this.currentSearch || !this.textInput) { if (!this.currentSearch || !this.textInput || !this.filteredItems) {
return this.items return this.items
} }
return this.items.filter((i) => { return this.filteredItems
var iValue = String(i).toLowerCase()
return iValue.includes(this.currentSearch.toLowerCase())
})
} }
}, },
methods: { methods: {
editItem(item) { editItem(item) {
this.$emit('edit', item) this.$emit('edit', item)
}, },
keydownInput() { search() {
if (!this.textInput) {
this.filteredItems = null
return
}
this.currentSearch = this.textInput
const results = this.items.filter((i) => {
var iValue = String(i).toLowerCase()
return iValue.includes(this.currentSearch.toLowerCase())
})
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
clearTimeout(this.typingTimeout) clearTimeout(this.typingTimeout)
this.typingTimeout = setTimeout(() => { this.typingTimeout = setTimeout(() => {
this.currentSearch = this.textInput this.search()
}, 100) }, 100)
this.setInputWidth() this.setInputWidth()
}, },
@@ -120,6 +161,24 @@ export default {
this.recalcMenuPos() this.recalcMenuPos()
}, 50) }, 50)
}, },
recalcScroll() {
if (!this.menu) return
var menuItems = this.menu.querySelectorAll('li')
if (!menuItems.length) return
var selectedItem = menuItems[this.selectedMenuItemIndex]
if (!selectedItem) return
var menuHeight = this.menu.offsetHeight
var itemHeight = selectedItem.offsetHeight
var itemTop = selectedItem.offsetTop
var itemBottom = itemTop + itemHeight
if (itemBottom > this.menu.scrollTop + menuHeight) {
let menuPaddingBottom = parseFloat(window.getComputedStyle(this.menu).paddingBottom)
this.menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom
} else if (itemTop < this.menu.scrollTop) {
let menuPaddingTop = parseFloat(window.getComputedStyle(this.menu).paddingTop)
this.menu.scrollTop = itemTop - menuPaddingTop
}
},
recalcMenuPos() { recalcMenuPos() {
if (!this.menu || !this.$refs.inputWrapper) return if (!this.menu || !this.$refs.inputWrapper) return
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect() var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
@@ -208,7 +267,10 @@ export default {
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()
} }
if (this.$refs.input) this.$refs.input.focus() if (this.$refs.input) {
this.$refs.input.style.width = '24px'
this.$refs.input.focus()
}
var newSelected = null var newSelected = null
if (this.selected.includes(itemValue)) { if (this.selected.includes(itemValue)) {
@@ -219,6 +281,7 @@ export default {
} }
this.textInput = null this.textInput = null
this.currentSearch = null this.currentSearch = null
this.selectedMenuItemIndex = null
this.$emit('input', newSelected) this.$emit('input', newSelected)
this.$nextTick(() => { this.$nextTick(() => {
this.recalcMenuPos() this.recalcMenuPos()
@@ -239,12 +302,21 @@ export default {
this.recalcMenuPos() this.recalcMenuPos()
}) })
}, },
resetInput() {
this.textInput = null
this.currentSearch = null
this.selectedMenuItemIndex = null
this.$nextTick(() => {
this.blur()
})
},
insertNewItem(item) { insertNewItem(item) {
this.selected.push(item) this.selected.push(item)
this.$emit('input', this.selected) this.$emit('input', this.selected)
this.$emit('newItem', item) this.$emit('newItem', item)
this.textInput = null this.textInput = null
this.currentSearch = null this.currentSearch = null
this.selectedMenuItemIndex = null
this.$nextTick(() => { this.$nextTick(() => {
this.blur() this.blur()
}) })
@@ -252,15 +324,19 @@ export default {
submitForm() { submitForm() {
if (!this.textInput) return if (!this.textInput) return
var cleaned = this.textInput.trim() const cleaned = this.textInput.trim()
var matchesItem = this.items.find((i) => { if (!cleaned) {
return i === cleaned this.resetInput()
})
if (matchesItem) {
this.clickedOption(null, matchesItem)
} else { } else {
this.insertNewItem(this.textInput) const matchesItem = this.items.find((i) => i === cleaned)
if (matchesItem) {
this.clickedOption(null, matchesItem)
} else {
this.insertNewItem(cleaned)
}
} }
if (this.$refs.input) this.$refs.input.style.width = '24px'
}, },
scroll() { scroll() {
this.recalcMenuPos() this.recalcMenuPos()
+58 -5
View File
@@ -14,13 +14,13 @@
<div v-if="showEdit && !disabled" class="rounded-full cursor-pointer w-6 h-6 mx-0.5 bg-bg flex items-center justify-center"> <div v-if="showEdit && !disabled" class="rounded-full cursor-pointer w-6 h-6 mx-0.5 bg-bg flex items-center justify-center">
<span class="material-icons text-white hover:text-success pt-px pr-px" style="font-size: 1.1rem" @click.stop="addItem">add</span> <span class="material-icons text-white hover:text-success pt-px pr-px" style="font-size: 1.1rem" @click.stop="addItem">add</span>
</div> </div>
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" style="min-width: 40px; width: 40px" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" /> <input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
</div> </div>
</form> </form>
<ul ref="menu" v-show="showMenu" class="absolute z-60 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label"> <ul ref="menu" v-show="showMenu" class="absolute z-60 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in itemsToShow"> <template v-for="item in itemsToShow">
<li :key="item.id" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" 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="itemsToShow[selectedMenuItemIndex] === item ? 'text-yellow-300' : ''" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
<div class="flex items-center"> <div class="flex items-center">
<span class="font-normal ml-3 block truncate">{{ item.name }}</span> <span class="font-normal ml-3 block truncate">{{ item.name }}</span>
</div> </div>
@@ -63,7 +63,8 @@ export default {
typingTimeout: null, typingTimeout: null,
isFocused: false, isFocused: false,
menu: null, menu: null,
items: [] items: [],
selectedMenuItemIndex: null
} }
}, },
watch: { watch: {
@@ -122,7 +123,35 @@ export default {
this.items = results || [] this.items = results || []
}, },
keydownInput() { 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
clearTimeout(this.typingTimeout) clearTimeout(this.typingTimeout)
this.typingTimeout = setTimeout(() => { this.typingTimeout = setTimeout(() => {
this.search() this.search()
@@ -137,6 +166,24 @@ export default {
this.recalcMenuPos() this.recalcMenuPos()
}, 50) }, 50)
}, },
recalcScroll() {
if (!this.menu) return
var menuItems = this.menu.querySelectorAll('li')
if (!menuItems.length) return
var selectedItem = menuItems[this.selectedMenuItemIndex]
if (!selectedItem) return
var menuHeight = this.menu.offsetHeight
var itemHeight = selectedItem.offsetHeight
var itemTop = selectedItem.offsetTop
var itemBottom = itemTop + itemHeight
if (itemBottom > this.menu.scrollTop + menuHeight) {
let menuPaddingBottom = parseFloat(window.getComputedStyle(this.menu).paddingBottom)
this.menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom
} else if (itemTop < this.menu.scrollTop) {
let menuPaddingTop = parseFloat(window.getComputedStyle(this.menu).paddingTop)
this.menu.scrollTop = itemTop - menuPaddingTop
}
},
recalcMenuPos() { recalcMenuPos() {
if (!this.menu || !this.$refs.inputWrapper) return if (!this.menu || !this.$refs.inputWrapper) return
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect() var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
@@ -228,7 +275,10 @@ export default {
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()
} }
if (this.$refs.input) this.$refs.input.focus() if (this.$refs.input) {
this.$refs.input.style.width = '24px'
this.$refs.input.focus()
}
let newSelected = null let newSelected = null
if (this.getIsSelected(item.id)) { if (this.getIsSelected(item.id)) {
@@ -244,6 +294,7 @@ export default {
} }
this.textInput = null this.textInput = null
this.currentSearch = null this.currentSearch = null
this.selectedMenuItemIndex = null
this.$emit('input', newSelected) this.$emit('input', newSelected)
this.$nextTick(() => { this.$nextTick(() => {
@@ -271,6 +322,7 @@ export default {
this.$emit('newItem', item) this.$emit('newItem', item)
this.textInput = null this.textInput = null
this.currentSearch = null this.currentSearch = null
this.selectedMenuItemIndex = null
this.$nextTick(() => { this.$nextTick(() => {
this.blur() this.blur()
}) })
@@ -291,6 +343,7 @@ export default {
name: this.textInput name: this.textInput
}) })
} }
if (this.$refs.input) this.$refs.input.style.width = '24px'
}, },
scroll() { scroll() {
this.recalcMenuPos() this.recalcMenuPos()
+13 -13
View File
@@ -1,5 +1,5 @@
<template> <template>
<div> <div class="default-style">
<p v-if="label" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }"> <p v-if="label" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
{{ label }} {{ label }}
</p> </p>
@@ -29,31 +29,31 @@ export default {
config() { config() {
return { return {
toolbar: { toolbar: {
getDefaultHTML: () => ` <div class="trix-button-row"> getDefaultHTML: () => `<div class="trix-button-row">
<span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools"> <span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" title="#{lang.bold}" tabindex="-1">#{lang.bold}</button> <button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" title="${this.$strings.LabelFontBold}" tabindex="-1">${this.$strings.LabelFontBold}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" title="#{lang.italic}" tabindex="-1">#{lang.italic}</button> <button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" title="${this.$strings.LabelFontItalic}" tabindex="-1">${this.$strings.LabelFontItalic}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" title="#{lang.strike}" tabindex="-1">#{lang.strike}</button> <button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" title="${this.$strings.LabelFontStrikethrough}" tabindex="-1">${this.$strings.LabelFontStrikethrough}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="#{lang.link}" tabindex="-1">#{lang.link}</button> <button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="${this.$strings.LabelTextEditorLink}" tabindex="-1">${this.$strings.LabelTextEditorLink}</button>
</span> </span>
<span class="trix-button-group trix-button-group--block-tools" data-trix-button-group="block-tools"> <span class="trix-button-group trix-button-group--block-tools" data-trix-button-group="block-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" title="#{lang.bullets}" tabindex="-1">#{lang.bullets}</button> <button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" title="${this.$strings.LabelTextEditorBulletedList}" tabindex="-1">${this.$strings.LabelTextEditorBulletedList}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" title="#{lang.numbers}" tabindex="-1">#{lang.numbers}</button> <button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" title="${this.$strings.LabelTextEditorNumberedList}" tabindex="-1">${this.$strings.LabelTextEditorNumberedList}</button>
</span> </span>
<span class="trix-button-group-spacer"></span> <span class="trix-button-group-spacer"></span>
<span class="trix-button-group trix-button-group--history-tools" data-trix-button-group="history-tools"> <span class="trix-button-group trix-button-group--history-tools" data-trix-button-group="history-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" title="#{lang.undo}" tabindex="-1">#{lang.undo}</button> <button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" title="${this.$strings.LabelUndo}" tabindex="-1">${this.$strings.LabelUndo}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" title="#{lang.redo}" tabindex="-1">#{lang.redo}</button> <button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" title="${this.$strings.LabelRedo}" tabindex="-1">${this.$strings.LabelRedo}</button>
</span> </span>
</div> </div>
<div class="trix-dialogs" data-trix-dialogs> <div class="trix-dialogs" data-trix-dialogs>
<div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href"> <div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
<div class="trix-dialog__link-fields"> <div class="trix-dialog__link-fields">
<input type="url" name="href" class="trix-input trix-input--dialog" placeholder="#{lang.urlPlaceholder}" aria-label="#{lang.url}" required data-trix-input> <input type="url" name="href" class="trix-input trix-input--dialog" placeholder="" aria-label="URL" required data-trix-input>
<div class="trix-button-group"> <div class="trix-button-group">
<input type="button" class="trix-button trix-button--dialog" value="#{lang.link}" data-trix-method="setAttribute"> <input type="button" class="trix-button trix-button--dialog" value="${this.$strings.LabelTextEditorLink}" data-trix-method="setAttribute">
<input type="button" class="trix-button trix-button--dialog" value="#{lang.unlink}" data-trix-method="removeAttribute"> <input type="button" class="trix-button trix-button--dialog" value="${this.$strings.LabelTextEditorUnlink}" data-trix-method="removeAttribute">
</div> </div>
</div> </div>
</div> </div>

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