Compare commits

...

404 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
mikiher 0498d8cb83 Get book placeholder image from fixture rather than from server 2024-04-19 09:49:19 +03:00
mikiher 129da51f76 Add tailwind.compiled.css to .gitignore 2024-04-18 07:47:10 +03: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
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
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
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
jfrazx 06391b9b37 chore: merge master 2024-01-21 19:15:52 -08: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
jfrazx 4e6b75d650 fix; HTTP/429 when requesting authors information, resolves #1570 2023-10-05 13:48:55 -07:00
256 changed files with 19970 additions and 7290 deletions
-3
View File
@@ -10,6 +10,3 @@ RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
curl tzdata ffmpeg && \
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/
+1 -1
View File
@@ -6,5 +6,5 @@ module.exports.config = {
MetadataPath: Path.resolve('metadata'),
FFmpegPath: '/usr/bin/ffmpeg',
FFProbePath: '/usr/bin/ffprobe',
SkipBinariesCheck: false
SkipBinariesCheck: true
}
+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
+60 -13
View File
@@ -1,40 +1,50 @@
name: 🐞 Bug Report
description: File a bug/issue
title: "[Bug]: "
labels: ["bug", "triage"]
description: File a bug/issue and help us improve Audiobookshelf
title: '[Bug]: '
labels: ['bug', 'triage']
body:
- type: markdown
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
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
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
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
id: what-happened
attributes:
label: Describe the issue
description: What happened & what did you expect to happen
label: What happened?
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:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to reproduce the issue
value: "1. "
value: '1. '
validations:
required: true
- type: markdown
attributes:
value: '## Install Environment'
- type: input
id: version
attributes:
label: Audiobookshelf version
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:
required: true
- type: dropdown
@@ -46,6 +56,43 @@ body:
- Debian/PPA
- Windows Tray App
- Built from source
- Other
- Other (list in "Additional Notes" box)
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
description: Request a feature/enhancement
title: "[Enhancement]: "
labels: ["enhancement"]
title: '[Enhancement]: '
labels: ['enhancement']
body:
- type: markdown
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
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
id: describe
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:
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.
+3 -3
View File
@@ -20,11 +20,11 @@ jobs:
- name: Set up node
uses: actions/setup-node@v4
with:
node-version: "20"
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.2.0
uses: audiobookshelf/audiobookshelf-i18n-updater@v1.3.0
with:
directory: "client/strings/" # Adjust the directory path as needed
directory: 'client/strings/' # Adjust the directory path as needed
+4 -4
View File
@@ -4,7 +4,7 @@ on:
pull_request:
push:
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:
build:
@@ -18,8 +18,8 @@ jobs:
with:
node-version: 20
- name: install pkg
run: npm install -g pkg
- name: install pkg (using yao-pkg fork for targetting node20)
run: npm install -g @yao-pkg/pkg
- name: get client dependencies
working-directory: client
@@ -33,7 +33,7 @@ jobs:
run: npm ci --only=production
- name: build binary
run: pkg -t node18-linux-x64 -o audiobookshelf .
run: pkg -t node20-linux-x64 -o audiobookshelf .
- name: run audiobookshelf
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
+1
View File
@@ -19,3 +19,4 @@
sw.*
.DS_STORE
.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.detectIndentation": true,
"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
View File
@@ -6,7 +6,6 @@ RUN npm ci && npm cache clean --force
RUN npm run generate
### STAGE 1: Build server ###
FROM sandreas/tone:v0.1.5 AS tone
FROM node:20-alpine
ENV NODE_ENV=production
@@ -21,7 +20,6 @@ RUN apk update && \
g++ \
tini
COPY --from=tone /usr/local/bin/tone /usr/local/bin/
COPY --from=build /client/dist /client/dist
COPY index.js package* /
COPY server server
+1 -15
View File
@@ -50,7 +50,6 @@ install_ffmpeg() {
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_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
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
rm ffmpeg-git-amd64-static.tar.xz
# Temp downloading tone library to the ffmpeg dir
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"
echo "Good to go on Ffmpeg... hopefully"
}
setup_config() {
@@ -77,12 +70,6 @@ setup_config() {
echo "Existing config found."
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
if [ ! -d "$DEFAULT_DATA_DIR" ]; then
@@ -98,7 +85,6 @@ setup_config() {
CONFIG_PATH=$DEFAULT_DATA_DIR/config
FFMPEG_PATH=$FFMPEG_INSTALL_DIR/ffmpeg
FFPROBE_PATH=$FFMPEG_INSTALL_DIR/ffprobe
TONE_PATH=$FFMPEG_INSTALL_DIR/tone
PORT=$DEFAULT_PORT
HOST=$DEFAULT_HOST"
+1 -1
View File
@@ -48,7 +48,7 @@ Description: $DESCRIPTION"
echo "$controlfile" > dist/debian/DEBIAN/control;
# Package debian
pkg -t node18-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf .
pkg -t node20-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf .
fakeroot dpkg-deb -Zxz --build dist/debian
+59 -30
View File
@@ -1,41 +1,29 @@
<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 -->
<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">
<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">
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn>
<ui-btn to="/config" color="primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn>
<ui-btn color="success" class="w-52" :loading="isScanningLibrary || tempIsScanning" @click="scan">{{ $strings.ButtonScanLibrary }}</ui-btn>
</div>
</div>
<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>
<!-- Alternate plain view -->
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24">
<template v-for="(shelf, index) in shelves">
<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)">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24e">
<template v-for="(shelf, index) in supportedShelves">
<widgets-item-slider :shelf-id="shelf.id" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" :type="shelf.type" class="bookshelf-row pl-8e my-6e" @selectEntity="(payload) => selectEntity(payload, index)">
<p class="font-semibold text-gray-100">{{ $strings[shelf.labelStringKey] }}</p>
</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>
</div>
<!-- Regular bookshelf view -->
<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)" />
</template>
</div>
@@ -58,10 +46,14 @@ export default {
scannerParseSubtitle: false,
wrapperClientWidth: 0,
shelves: [],
lastItemIndexSelected: -1
lastItemIndexSelected: -1,
tempIsScanning: false
}
},
computed: {
supportedShelves() {
return this.shelves.filter((shelf) => ['book', 'podcast', 'episode', 'series', 'authors', 'narrators'].includes(shelf.type))
},
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
@@ -89,14 +81,16 @@ export default {
return this.coverAspectRatio == 1
},
sizeMultiplier() {
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
return this.bookCoverWidth / baseSize
return this.$store.getters['user/getSizeMultiplier']
},
selectedMediaItems() {
return this.$store.state.globals.selectedMediaItems || []
},
streamLibraryItem() {
return this.$store.state.streamLibraryItem
},
isScanningLibrary() {
return !!this.$store.getters['tasks/getRunningLibraryScanTask'](this.currentLibraryId)
}
},
methods: {
@@ -174,7 +168,7 @@ export default {
},
async fetchCategories() {
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) => {
return data
})
@@ -273,14 +267,15 @@ export default {
this.shelves = shelves
},
scan() {
this.tempIsScanning = true
this.$store
.dispatch('libraries/requestLibraryScan', { libraryId: this.$store.state.libraries.currentLibraryId })
.then(() => {
this.$toast.success('Library scan started')
})
.catch((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) {
@@ -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() {
if (this.$root.socket) {
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_added', this.libraryItemsAdded)
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 {
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_added', this.libraryItemsAdded)
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 {
console.error('Error socket not initialized')
}
+24 -45
View File
@@ -1,67 +1,53 @@
<template>
<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 class="w-full h-full pt-6">
<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-6e">
<div v-if="shelf.type === 'book' || shelf.type === 'podcast'" class="flex items-center">
<template v-for="(entity, index) in shelf.entities">
<cards-lazy-book-card :key="`${entity.id}-${index}`" :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>
</div>
<div v-if="shelf.type === 'episode'" class="flex items-center">
<template v-for="(entity, index) in shelf.entities">
<cards-lazy-book-card
: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"
/>
<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" />
</template>
</div>
<div v-if="shelf.type === 'series'" class="flex items-center">
<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>
</div>
<div v-if="shelf.type === 'tags'" class="flex items-center">
<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>
</div>
<div v-if="shelf.type === 'authors'" class="flex items-center">
<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>
</div>
<div v-if="shelf.type === 'narrators'" class="flex items-center">
<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>
</div>
</div>
</div>
<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="w-full h-full shinyBlack flex items-center justify-center rounded-sm border">
<p class="transform text-sm">{{ $strings[shelf.labelStringKey] }}</p>
<div class="relative">
<div class="relative text-center categoryPlacard transform z-30 top-0 left-4e md:left-8e w-44e rounded-md">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em 0.5em` }">
<p :style="{ fontSize: 0.9 + 'em' }">{{ $strings[shelf.labelStringKey] }}</p>
</div>
</div>
</div>
<div class="bookshelfDividerCategorized h-6 w-full absolute bottom-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 class="bookshelfDividerCategorized h-6e w-full absolute top-0 left-0 right-0 z-20"></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">
<span class="material-icons text-6xl text-white">chevron_right</span>
<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-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>
</template>
@@ -74,9 +60,6 @@ export default {
type: Object,
default: () => {}
},
sizeMultiplier: Number,
bookCoverWidth: Number,
bookCoverAspectRatio: Number,
continueListeningShelf: Boolean
},
data() {
@@ -89,12 +72,8 @@ export default {
}
},
computed: {
bookCoverHeight() {
return this.bookCoverWidth * this.bookCoverAspectRatio
},
shelfHeight() {
if (this.shelf.type === 'narrators') return 148
return this.bookCoverHeight + 48
sizeMultiplier() {
return this.$store.getters['user/getSizeMultiplier']
},
paddingLeft() {
if (window.innerWidth < 768) return 1
@@ -218,13 +197,13 @@ export default {
}
.book-shelf-arrow-right {
height: calc(100% - 24px);
height: calc(100% - 1.5em);
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%);
}
.book-shelf-arrow-left {
height: calc(100% - 24px);
height: calc(100% - 1.5em);
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%);
}
</style>
</style>
+92 -47
View File
@@ -1,8 +1,8 @@
<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">
<div :key="shelf" :id="`shelf-${shelf - 1}`" class="w-full px-4 sm:px-8 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 :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 h-6e" />
</div>
</template>
@@ -10,7 +10,7 @@
<p class="text-center text-2xl mb-4 py-4">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p>
<div v-if="userIsAdminOrUp" class="flex">
<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 v-else-if="!totalShelves && initialized" class="w-full py-16">
@@ -49,10 +49,9 @@ export default {
entityIndexesMounted: [],
entityComponentRefs: {},
currentBookWidth: 0,
pageLoadQueue: [],
isFetchingEntities: false,
scrollTimeout: null,
booksPerFetch: 100,
booksPerFetch: 0,
totalShelves: 0,
bookshelfMarginLeft: 0,
isSelectionMode: false,
@@ -62,7 +61,11 @@ export default {
currScrollTop: 0,
resizeTimeout: null,
mountWindowWidth: 0,
lastItemIndexSelected: -1
lastItemIndexSelected: -1,
tempIsScanning: false,
cardWidth: 0,
cardHeight: 0,
resizeObserver: null
}
},
watch: {
@@ -159,55 +162,46 @@ export default {
return this.$store.getters['libraries/getCurrentLibraryName']
},
bookWidth() {
const coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return coverSize * 1.6
return coverSize
return this.cardWidth
},
bookHeight() {
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return this.bookWidth
return this.bookWidth * 1.6
return this.cardHeight
},
shelfPadding() {
if (this.bookshelfWidth < 640) return 32
return 64
if (this.bookshelfWidth < 640) return 32 * this.sizeMultiplier
return 64 * this.sizeMultiplier
},
totalPadding() {
return this.shelfPadding * 2
},
entityWidth() {
if (this.entityName === 'series' || this.entityName === 'collections') {
if (this.bookWidth * 2 > this.bookshelfWidth - this.shelfPadding) return this.bookWidth * 1.6
return this.bookWidth * 2
}
return this.bookWidth
return this.cardWidth
},
entityHeight() {
return this.bookHeight
return this.cardHeight
},
shelfDividerHeightIndex() {
return 6
shelfPaddingHeight() {
return 16
},
shelfHeight() {
if (this.isAlternativeBookshelfView) {
const isItemEntity = this.entityName === 'series-books' || this.entityName === 'items'
const extraTitleSpace = isItemEntity ? 80 : this.entityName === 'albums' ? 60 : 40
return this.entityHeight + extraTitleSpace * this.sizeMultiplier
}
return this.entityHeight + 40
const dividerHeight = this.isAlternativeBookshelfView ? 0 : 24 // h-6
return this.cardHeight + (this.shelfPaddingHeight + dividerHeight) * this.sizeMultiplier
},
totalEntityCardWidth() {
// Includes margin
return this.entityWidth + 24
return this.entityWidth + 24 * this.sizeMultiplier
},
selectedMediaItems() {
return this.$store.state.globals.selectedMediaItems || []
},
sizeMultiplier() {
const baseSize = this.isCoverSquareAspectRatio ? 192 : 120
return this.entityWidth / baseSize
return this.$store.getters['user/getSizeMultiplier']
},
streamLibraryItem() {
return this.$store.state.streamLibraryItem
},
isScanningLibrary() {
return !!this.$store.getters['tasks/getRunningLibraryScanTask'](this.currentLibraryId)
}
},
methods: {
@@ -318,7 +312,7 @@ export default {
let entityPath = this.entityName === 'series-books' ? 'items' : this.entityName
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) => {
console.error('failed to fetch items', error)
@@ -432,10 +426,14 @@ export default {
rebuild() {
this.initSizeData()
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
var lastBookIndex = Math.min(this.totalEntities, this.booksPerFetch)
this.entityIndexesMounted = []
for (let i = 0; i < lastBookIndex; i++) {
this.entityIndexesMounted.push(i)
if (!this.entities[i]) {
const page = Math.floor(i / this.booksPerFetch)
this.loadPage(page)
}
}
var bookshelfEl = document.getElementById('bookshelf')
if (bookshelfEl) {
@@ -497,7 +495,8 @@ export default {
this.resetEntities()
}
},
settingsUpdated(settings) {
async settingsUpdated(settings) {
await this.cardsHelpers.setCardSize()
const wasUpdated = this.checkUpdateSearchParams()
if (wasUpdated) {
this.resetEntities()
@@ -602,6 +601,44 @@ export default {
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) {
var bookshelf = _bookshelf || document.getElementById('bookshelf')
if (!bookshelf) {
@@ -618,6 +655,13 @@ export default {
this.entitiesPerShelf = Math.max(1, Math.floor((this.bookshelfWidth - this.shelfPadding) / this.totalEntityCardWidth))
this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 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
if (this.totalEntities) {
@@ -626,13 +670,8 @@ export default {
return entitiesPerShelfBefore < this.entitiesPerShelf // Books per shelf has changed
},
async init(bookshelf) {
if (this.entityName === 'series') {
this.booksPerFetch = 50
} else {
this.booksPerFetch = 100
}
this.checkUpdateSearchParams()
this.initSizeData(bookshelf)
this.checkUpdateSearchParams()
this.pagesLoaded[0] = true
await this.fetchEntites(0)
@@ -688,6 +727,8 @@ export default {
this.$root.socket.on('playlist_added', this.playlistAdded)
this.$root.socket.on('playlist_updated', this.playlistUpdated)
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 {
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_updated', this.playlistUpdated)
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 {
console.error('Bookshelf - Socket not initialized')
}
@@ -727,18 +770,20 @@ export default {
}
},
scan() {
this.tempIsScanning = true
this.$store
.dispatch('libraries/requestLibraryScan', { libraryId: this.currentLibraryId })
.then(() => {
this.$toast.success('Library scan started')
})
.catch((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.routeFullPath = window.location.pathname + (window.location.search || '')
@@ -773,6 +818,6 @@ export default {
.bookshelfDivider {
background: rgb(149, 119, 90);
background: var(--bookshelf-divider-bg);
box-shadow: 2px 14px 8px #111111aa;
box-shadow: 0.125em 0.875em 0.5em #111111aa;
}
</style>
</style>
+19 -18
View File
@@ -5,21 +5,21 @@
<covers-book-cover expand-on-click :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
</div>
<div class="flex items-start mb-6 lg:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
<div class="min-w-0">
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
{{ title }}
</nuxt-link>
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center">
<div class="min-w-0 w-full">
<div class="flex items-center">
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
{{ title }}
</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>
<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-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">
<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 v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
<div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div>
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate">
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
</div>
<div v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</div>
</div>
<div class="text-gray-400 flex items-center">
@@ -82,13 +82,11 @@ export default {
sleepTimer: null,
displayTitle: null,
currentPlaybackRate: 1,
syncFailedToast: null
syncFailedToast: null,
coverAspectRatio: 1
}
},
computed: {
coverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
isSquareCover() {
return this.coverAspectRatio === 1
},
@@ -138,7 +136,7 @@ export default {
return this.streamLibraryItem?.mediaType === 'music'
},
isExplicit() {
return this.mediaMetadata.explicit || false
return !!this.mediaMetadata.explicit
},
mediaMetadata() {
return this.media.metadata || {}
@@ -457,6 +455,9 @@ export default {
episodeId,
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(() => {
if (this.$refs.audioPlayer) this.$refs.audioPlayer.checkUpdateChapterTrack()
})
+2 -5
View File
@@ -121,7 +121,7 @@
<p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p>
</div>
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version" />
<modals-changelog-view-modal v-model="showChangelogModal" :versionData="versionData" />
</div>
</template>
@@ -219,9 +219,6 @@ export default {
githubTagUrl() {
return this.versionData.githubTagUrl
},
currentVersionChangelog() {
return this.versionData.currentVersionChangelog || 'No Changelog Available'
},
streamLibraryItem() {
return this.$store.state.streamLibraryItem
},
@@ -245,4 +242,4 @@ export default {
#siderail-buttons-container.player-open {
max-height: calc(100vh - 64px - 48px - 160px);
}
</style>
</style>
+47 -34
View File
@@ -1,38 +1,40 @@
<template>
<nuxt-link :to="`/author/${author.id}`">
<div @mouseover="mouseover" @mouseleave="mouseleave">
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
<!-- Image or placeholder -->
<covers-author-image :author="author" />
<div :style="{ minWidth: cardWidth + 'px', maxWidth: cardWidth + 'px' }">
<nuxt-link :to="`/author/${author.id}`">
<div cy-id="card" @mouseover="mouseover" @mouseleave="mouseleave">
<div cy-id="imageArea" :style="{ height: cardHeight + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
<!-- Image or placeholder -->
<covers-author-image :author="author" />
<!-- 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">
<p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} {{ $strings.LabelBooks }}</p>
</div>
<!-- Author name & num books overlay -->
<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: 0.75 + 'em' }">{{ name }}</p>
<p class="text-center text-gray-200" :style="{ fontSize: 0.65 + 'em' }">{{ numBooks }} {{ $strings.LabelBooks }}</p>
</div>
<!-- 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">
<ui-tooltip :text="$strings.ButtonQuickMatch" direction="bottom">
<span class="material-icons text-lg">search</span>
</ui-tooltip>
</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)">
<ui-tooltip :text="$strings.LabelEdit" direction="bottom">
<span class="material-icons text-lg">edit</span>
</ui-tooltip>
</div>
<!-- Search icon btn -->
<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">
<span class="material-icons" :style="{ fontSize: 1.125 + 'em' }">search</span>
</ui-tooltip>
</div>
<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">
<span class="material-icons" :style="{ fontSize: 1.125 + 'em' }">edit</span>
</ui-tooltip>
</div>
<!-- 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">
<widgets-loading-spinner size="" />
<!-- Loading spinner -->
<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="" />
</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 v-show="nameBelow" class="w-full py-1 px-2">
<p class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
</div>
</div>
</nuxt-link>
</nuxt-link>
</div>
</template>
<script>
@@ -43,12 +45,14 @@ export default {
default: () => {}
},
width: Number,
height: Number,
sizeMultiplier: {
height: {
type: Number,
default: 1
default: 192
},
nameBelow: Boolean
nameBelow: {
type: Boolean,
default: false
}
},
data() {
return {
@@ -57,6 +61,12 @@ export default {
}
},
computed: {
cardWidth() {
return this.width || this.cardHeight * 0.8
},
cardHeight() {
return this.height * this.sizeMultiplier
},
userToken() {
return this.$store.getters['user/getToken']
},
@@ -83,6 +93,9 @@ export default {
},
libraryProvider() {
return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
},
sizeMultiplier() {
return this.$store.getters['user/getSizeMultiplier']
}
},
methods: {
@@ -128,4 +141,4 @@ export default {
this.$eventBus.$off(`searching-author-${this.authorId}`, this.setSearching)
}
}
</script>
</script>
+8 -8
View File
@@ -13,9 +13,9 @@
<div class="flex-grow" />
<p class="text-sm md:text-base">{{ book.publishedYear }}</p>
</div>
<p v-if="book.author" class="text-gray-300 text-xs md:text-sm">by {{ book.author }}</p>
<p v-if="book.narrator" class="text-gray-400 text-xs">Narrated by {{ book.narrator }}</p>
<p v-if="book.duration" class="text-gray-400 text-xs">Runtime: {{ $elapsedPrettyExtended(bookDuration, false) }} {{ bookDurationComparison }}</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">{{ $strings.LabelNarrators }}: {{ book.narrator }}</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-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">
@@ -29,9 +29,9 @@
</div>
<div v-else class="px-4 flex-grow">
<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>
<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 class="text-xs text-gray-400 leading-5">{{ book.trackCount }} Episodes</p>
</div>
@@ -75,11 +75,11 @@ export default {
let differenceInMinutes = currentBookDurationMinutes - this.book.duration
if (differenceInMinutes < 0) {
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) {
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: {
+18 -8
View File
@@ -1,15 +1,15 @@
<template>
<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">
<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` }">
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ groupName }}</p>
</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>
</nuxt-link>
</div>
@@ -24,8 +24,10 @@ export default {
default: () => null
},
width: Number,
height: Number,
bookCoverAspectRatio: Number
height: {
type: Number,
default: 192
}
},
data() {
return {
@@ -33,6 +35,15 @@ export default {
}
},
computed: {
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
cardWidth() {
return this.width || this.cardHeight * 2
},
cardHeight() {
return this.height * this.sizeMultiplier
},
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
@@ -46,8 +57,7 @@ export default {
return `/library/${this.currentLibraryId}/bookshelf?filter=${this.filter}`
},
sizeMultiplier() {
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
return this.width / 240
return this.$store.getters['user/getSizeMultiplier']
},
bookItems() {
return this._group.books || []
@@ -78,4 +88,4 @@ export default {
}
}
}
</script>
</script>
+3 -3
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 !== '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" />
<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 === 'tags') return `<p class="truncate">${this.$strings.LabelTags}: ${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 === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
if (this.matchKey === 'series') return `<p class="truncate">${this.$strings.LabelSeries}: ${html}</p>`
@@ -90,4 +90,4 @@ export default {
flex-direction: column;
justify-content: center;
}
</style>
</style>
+13 -9
View File
@@ -21,15 +21,16 @@
<div v-if="!isPodcast" class="flex items-end">
<ui-text-input-with-label v-model.trim="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
<ui-tooltip :text="$strings.LabelUploaderItemFetchMetadataHelp">
<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">
<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">
<span class="text-base text-white text-opacity-80 font-mono material-icons">sync</span>
</div>
</ui-tooltip>
</div>
<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" />
</div>
</div>
@@ -40,7 +41,10 @@
</div>
<div class="w-1/2 px-2">
<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" />
</div>
</div>
@@ -51,10 +55,10 @@
<tables-uploaded-files-table v-if="item.ignoredFiles.length" :title="$strings.HeaderIgnoredFiles" :files="item.ignoredFiles" />
</template>
<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 v-if="uploadFailed" type="error">
<p class="text-base">{{ $strings.MessageUploaderItemFailed }}</p>
<p class="text-base">"{{ itemData.title }}" {{ $strings.MessageUploaderItemFailed }}</p>
</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">
@@ -70,7 +74,7 @@ export default {
props: {
item: {
type: Object,
default: () => { }
default: () => {}
},
mediaType: String,
processing: Boolean,
@@ -99,7 +103,7 @@ export default {
if (this.isPodcast) return 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)
},
+46 -18
View File
@@ -1,18 +1,22 @@
<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 class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
<covers-preview-cover ref="cover" :src="coverSrc" :width="width" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</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` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
<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="relative" :style="{ height: coverHeight + 'px' }">
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
<covers-preview-cover ref="cover" :src="coverSrc" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</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>
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ artist || '&nbsp;' }}</p>
<div class="relative w-full">
<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>
</template>
@@ -22,8 +26,10 @@ export default {
props: {
index: Number,
width: Number,
height: Number,
bookCoverAspectRatio: Number,
height: {
type: Number,
default: 192
},
bookshelfView: {
type: Number,
default: 0
@@ -42,6 +48,29 @@ export default {
}
},
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() {
const config = this.$config || this.$nuxt.$config
if (!this.album || !this.album.libraryItemId) return `${config.routerBasePath}/book_placeholder.jpg`
@@ -49,11 +78,10 @@ export default {
},
labelFontSize() {
if (this.width < 160) return 0.75
return 0.875
return 0.9
},
sizeMultiplier() {
const baseSize = this.bookCoverAspectRatio === 1 ? 192 : 120
return this.width / baseSize
return this.store.getters['user/getSizeMultiplier']
},
title() {
return this.album ? this.album.title : ''
@@ -111,4 +139,4 @@ export default {
}
}
}
</script>
</script>
+198 -141
View File
@@ -1,129 +1,139 @@
<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">
<!-- When cover image does not fill -->
<div 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 ref="card" :id="`book-card-${index}`" :style="{ minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }" class="absolute rounded-sm z-10 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<div :id="`cover-area-${index}`" class="relative w-full top-0 left-0 rounded overflow-hidden z-10 bg-primary box-shadow-book" :style="{ height: coverHeight + 'px ' }">
<!-- When cover image does not fill -->
<div cy-id="coverBg" v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
<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>
<!-- Alternative bookshelf title/author/sort -->
<div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" dir="auto" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
<div :style="{ fontSize: 0.9 * sizeMultiplier + '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 + 'em' }">
<ui-tooltip v-if="displayTitle" :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
<p ref="displayTitle" class="truncate">{{ displayTitle }}</p>
<widgets-explicit-indicator :explicit="isExplicit" />
<p cy-id="title" ref="displayTitle" class="truncate">{{ displayTitle }}</p>
<widgets-explicit-indicator cy-id="explicitIndicator" v-if="isExplicit" />
</ui-tooltip>
</div>
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || '&nbsp;' }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ 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>
<p cy-id="line2" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displayLineTwo || '&nbsp;' }}</p>
<p cy-id="line3" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displaySortLine }}</p>
</div>
</div>
</template>
@@ -135,15 +145,11 @@ import MoreMenu from '@/components/widgets/MoreMenu'
export default {
props: {
index: Number,
width: {
type: Number,
default: 120
},
width: Number,
height: {
type: Number,
default: 192
},
bookCoverAspectRatio: Number,
bookshelfView: Number,
bookMount: {
// Book can be passed as prop or set with setEntity()
@@ -178,6 +184,39 @@ export default {
}
},
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() {
return this.store.state.serverSettings.dateFormat
},
@@ -277,10 +316,6 @@ export default {
squareAspectRatio() {
return this.bookCoverAspectRatio === 1
},
sizeMultiplier() {
const baseSize = this.squareAspectRatio ? 192 : 120
return this.width / baseSize
},
title() {
return this.mediaMetadata.title || ''
},
@@ -302,7 +337,7 @@ export default {
if (this.recentEpisode) return this.recentEpisode.title
const ignorePrefix = this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix
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() {
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 === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
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
},
episodeProgress() {
@@ -343,11 +381,22 @@ export default {
if (!this.userProgress || this.userProgress.progress) return false
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() {
if (this.useEBookProgress) return Math.max(Math.min(1, this.userProgress.ebookProgress), 0)
return this.userProgress ? Math.max(Math.min(1, this.userProgress.progress), 0) || 0 : 0
let progressPercent = this.itemIsFinished ? 1 : this.booksInSeries ? this.seriesProgressPercent : this.useEBookProgress ? this.userProgress?.ebookProgress || 0 : this.userProgress?.progress || 0
return Math.max(Math.min(1, progressPercent), 0)
},
itemIsFinished() {
if (this.booksInSeries) return this.seriesIsFinished
return this.userProgress ? !!this.userProgress.isFinished : false
},
seriesIsFinished() {
@@ -479,6 +528,12 @@ export default {
func: 'openPlaylists',
text: this.$strings.LabelAddToPlaylist
})
if (this.userIsAdminOrUp) {
items.push({
func: 'openShare',
text: this.$strings.LabelShare
})
}
}
if (this.ebookFormat && this.store.state.libraries.ereaderDevices?.length) {
items.push({
@@ -550,16 +605,16 @@ export default {
return this.$root.socket || this.$nuxt.$root.socket
},
titleFontSize() {
return 0.75 * this.sizeMultiplier
return 0.75
},
authorFontSize() {
return 0.6 * this.sizeMultiplier
return 0.6
},
placeholderCoverPadding() {
return 0.8 * this.sizeMultiplier
return 0.8
},
authorBottom() {
return 0.75 * this.sizeMultiplier
return 0.75
},
titleCleaned() {
if (!this.title) return ''
@@ -583,14 +638,12 @@ export default {
const constants = this.$constants || this.$nuxt.$constants
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() {
if (this.booksInSeries) return null
return this._libraryItem.rssFeed || null
},
mediaItemShare() {
return this._libraryItem.mediaItemShare || null
}
},
methods: {
@@ -833,6 +886,10 @@ export default {
this.store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode: this.recentEpisode }])
this.store.commit('globals/setShowPlaylistsModal', true)
},
openShare() {
this.store.commit('setSelectedLibraryItem', this.libraryItem)
this.store.commit('globals/setShareModal', this.mediaItemShare)
},
deleteLibraryItem() {
const payload = {
message: this.$strings.MessageConfirmDeleteLibraryItem,
+45 -21
View File
@@ -1,24 +1,26 @@
<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 class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
<covers-collection-cover ref="cover" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</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 * 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 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="relative" :style="{ height: coverHeight + 'px' }">
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
<covers-collection-cover ref="cover" :book-items="books" :width="cardWidth" :height="coverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</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>
<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-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 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-8 h-8 py-1 rounded-md text-center">
<p class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ title }}</p>
<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 + 'em' }">{{ title }}</p>
</div>
</div>
</template>
@@ -28,8 +30,10 @@ export default {
props: {
index: Number,
width: Number,
height: Number,
bookCoverAspectRatio: Number,
height: {
type: Number,
default: 192
},
bookshelfView: {
type: Number,
default: 0
@@ -49,13 +53,33 @@ export default {
}
},
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() {
if (this.width < 160) return 0.75
return 0.875
return 0.9
},
sizeMultiplier() {
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
return this.width / 240
return this.store.getters['user/getSizeMultiplier']
},
title() {
return this.collection ? this.collection.name : ''
@@ -119,4 +143,4 @@ export default {
}
}
}
</script>
</script>
+32 -19
View File
@@ -1,21 +1,24 @@
<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 class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
<covers-playlist-cover ref="cover" :items="items" :width="width" :height="height" />
</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 * 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 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="relative" :style="{ height: coverHeight + 'px' }">
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
<covers-playlist-cover ref="cover" :items="items" :width="cardWidth" :height="coverHeight" />
</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>
</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` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
<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' }">
<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-8 h-8 py-1 rounded-md text-center">
<p class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ title }}</p>
<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 + 'em' }">{{ title }}</p>
</div>
</div>
</template>
@@ -25,8 +28,10 @@ export default {
props: {
index: Number,
width: Number,
height: Number,
bookCoverAspectRatio: Number,
height: {
type: Number,
default: 192
},
bookshelfView: {
type: Number,
default: 0
@@ -45,13 +50,21 @@ export default {
}
},
computed: {
bookCoverAspectRatio() {
return this.store.getters['libraries/getBookCoverAspectRatio']
},
cardWidth() {
return this.width || this.coverHeight
},
coverHeight() {
return this.height * this.sizeMultiplier
},
labelFontSize() {
if (this.width < 160) return 0.75
return 0.875
return 0.9
},
sizeMultiplier() {
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6)
return this.width / 120
return this.store.getters['user/getSizeMultiplier']
},
title() {
return this.playlist ? this.playlist.name : ''
@@ -112,4 +125,4 @@ export default {
}
}
}
</script>
</script>
+47 -30
View File
@@ -1,28 +1,32 @@
<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 class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
<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 cy-id="card" ref="card" :id="`series-card-${index}`" :style="{ width: cardWidth + 'px' }" class="absolute rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<div cy-id="covers-area" class="relative" :style="{ height: coverHeight + 'px' }">
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
<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 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 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 + '%' }" />
<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 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` }">
<p cy-id="standardBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ displayTitle }}</p>
</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 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
<div cy-id="detailBottomText" v-else class="relative z-30 left-0 right-0 mx-auto py-1e rounded-md text-center">
<p cy-id="detailBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ displayTitle }}</p>
<p cy-id="detailBottomSortLine" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displaySortLine }}</p>
</div>
</div>
</template>
@@ -32,13 +36,14 @@ export default {
props: {
index: Number,
width: Number,
height: Number,
bookCoverAspectRatio: Number,
height: {
type: Number,
default: 192
},
bookshelfView: {
type: Number,
default: 0
},
isCategorized: Boolean,
seriesMount: {
type: Object,
default: () => null
@@ -56,16 +61,24 @@ export default {
}
},
computed: {
bookCoverAspectRatio() {
return this.store.getters['libraries/getBookCoverAspectRatio']
},
cardWidth() {
return this.width || this.coverHeight * 2
},
coverHeight() {
return this.height * this.sizeMultiplier
},
dateFormat() {
return this.store.state.serverSettings.dateFormat
},
labelFontSize() {
if (this.width < 160) return 0.75
return 0.875
return 0.9
},
sizeMultiplier() {
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
return this.width / 240
return this.store.getters['user/getSizeMultiplier']
},
seriesId() {
return this.series ? this.series.id : ''
@@ -78,7 +91,7 @@ export default {
},
displayTitle() {
if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title
return this.title
return this.title || '\u00A0'
},
displaySortLine() {
switch (this.orderBy) {
@@ -119,9 +132,13 @@ export default {
return this.seriesBookProgress.some((p) => !p.isFinished && p.progress > 0)
},
seriesPercentInProgress() {
let totalFinishedAndInProgress = this.seriesBooksFinished.length
if (this.hasSeriesBookInProgress) totalFinishedAndInProgress += 1
return Math.min(1, Math.max(0, totalFinishedAndInProgress / this.books.length))
if (!this.books.length) return 0
let progressPercent = 0
this.seriesBookProgress.forEach((progress) => {
progressPercent += progress.isFinished ? 1 : progress.progress || 0
})
progressPercent /= this.books.length
return Math.min(1, Math.max(0, progressPercent))
},
isSeriesFinished() {
return this.books.length === this.seriesBooksFinished.length
+25 -15
View File
@@ -1,17 +1,19 @@
<template>
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(narrator.name)}`">
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
<div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-40">
<span class="material-icons-outlined text-[10rem]">record_voice_over</span>
</div>
<div>
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(name)}`">
<div cy-id="card" :style="{ width: cardWidth + 'px', height: cardHeight + 'px', fontSize: sizeMultiplier + 'rem' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
<div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-40">
<span class="material-icons-outlined text-[10em]">record_voice_over</span>
</div>
<!-- Narrator name & num books overlay -->
<div class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2">
<p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
<!-- Narrator name & num books overlay -->
<div class="absolute bottom-0 left-0 w-full py-1e bg-black bg-opacity-60 px-2e">
<p cy-id="name" class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: 0.75 + 'em' }">{{ name }}</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>
</nuxt-link>
</nuxt-link>
</div>
</template>
<script>
@@ -22,16 +24,21 @@ export default {
default: () => {}
},
width: Number,
height: Number,
sizeMultiplier: {
height: {
type: Number,
default: 1
default: 100
}
},
data() {
return {}
},
computed: {
cardWidth() {
return this.cardHeight * 1.5
},
cardHeight() {
return this.height * this.sizeMultiplier
},
name() {
return this.narrator?.name || ''
},
@@ -43,8 +50,11 @@ export default {
},
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
sizeMultiplier() {
return this.$store.getters['user/getSizeMultiplier']
}
},
methods: {}
}
</script>
</script>
@@ -37,12 +37,12 @@
<span class="material-icons text-2xl">arrow_left</span>
</div>
<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>
</li>
<li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="option">
<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>
</li>
<template v-for="item in sublistItems">
@@ -89,6 +89,9 @@ export default {
this.$emit('input', val)
}
},
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
libraryMediaType() {
return this.$store.getters['libraries/getCurrentLibraryMediaType']
},
@@ -106,31 +109,37 @@ export default {
},
{
text: this.$strings.LabelGenre,
textPlural: this.$strings.LabelGenres,
value: 'genres',
sublist: true
},
{
text: this.$strings.LabelTag,
textPlural: this.$strings.LabelTags,
value: 'tags',
sublist: true
},
{
text: this.$strings.LabelAuthor,
textPlural: this.$strings.LabelAuthors,
value: 'authors',
sublist: true
},
{
text: this.$strings.LabelNarrator,
textPlural: this.$strings.LabelNarrators,
value: 'narrators',
sublist: true
},
{
text: this.$strings.LabelPublisher,
textPlural: this.$strings.LabelPublishers,
value: 'publishers',
sublist: true
},
{
text: this.$strings.LabelLanguage,
textPlural: this.$strings.LabelLanguages,
value: 'languages',
sublist: true
},
@@ -142,43 +151,50 @@ export default {
]
},
bookItems() {
return [
const items = [
{
text: this.$strings.LabelAll,
value: 'all'
},
{
text: this.$strings.LabelGenre,
textPlural: this.$strings.LabelGenres,
value: 'genres',
sublist: true
},
{
text: this.$strings.LabelTag,
textPlural: this.$strings.LabelTags,
value: 'tags',
sublist: true
},
{
text: this.$strings.LabelSeries,
textPlural: this.$strings.LabelSeries,
value: 'series',
sublist: true
},
{
text: this.$strings.LabelAuthor,
textPlural: this.$strings.LabelAuthors,
value: 'authors',
sublist: true
},
{
text: this.$strings.LabelNarrator,
textPlural: this.$strings.LabelNarrators,
value: 'narrators',
sublist: true
},
{
text: this.$strings.LabelPublisher,
textPlural: this.$strings.LabelPublishers,
value: 'publishers',
sublist: true
},
{
text: this.$strings.LabelLanguage,
textPlural: this.$strings.LabelLanguages,
value: 'languages',
sublist: true
},
@@ -218,6 +234,14 @@ export default {
sublist: false
}
]
if (this.userIsAdminOrUp) {
items.push({
text: this.$strings.LabelShareOpen,
value: 'share-open',
sublist: false
})
}
return items
},
podcastItems() {
return [
@@ -227,16 +251,19 @@ export default {
},
{
text: this.$strings.LabelGenre,
textPlural: this.$strings.LabelGenres,
value: 'genres',
sublist: true
},
{
text: this.$strings.LabelTag,
textPlural: this.$strings.LabelTags,
value: 'tags',
sublist: true
},
{
text: this.$strings.LabelLanguage,
textPlural: this.$strings.LabelLanguages,
value: 'languages',
sublist: true
},
@@ -255,11 +282,13 @@ export default {
},
{
text: this.$strings.LabelGenre,
textPlural: this.$strings.LabelGenres,
value: 'genres',
sublist: true
},
{
text: this.$strings.LabelTag,
textPlural: this.$strings.LabelTags,
value: 'tags',
sublist: true
},
@@ -279,6 +308,13 @@ export default {
selectedItemSublist() {
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() {
if (!this.selected) return ''
const parts = this.selected.split('.')
@@ -505,4 +541,4 @@ export default {
.libraryFilterMenu {
max-height: calc(100vh - 125px);
}
</style>
</style>
+1 -1
View File
@@ -84,4 +84,4 @@ export default {
},
mounted() {}
}
</script>
</script>
+7 -5
View File
@@ -101,9 +101,14 @@ export default {
},
fullCoverUrl() {
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)
},
rawCoverUrl() {
if (!this.libraryItem) return null
const store = this.$store || this.$nuxt.$store
return store.getters['globals/getLibraryItemCoverSrc'](this.libraryItem, this.placeholderUrl, true)
},
cover() {
return this.media.coverPath || this.placeholderUrl
},
@@ -126,9 +131,6 @@ export default {
authorBottom() {
return 0.75 * this.sizeMultiplier
},
userToken() {
return this.$store.getters['user/getToken']
},
resolution() {
return `${this.naturalWidth}x${this.naturalHeight}px`
}
@@ -136,7 +138,7 @@ export default {
methods: {
clickCover() {
if (this.expandOnClick && this.libraryItem) {
this.$store.commit('globals/setRawCoverPreviewModal', this.libraryItem.id)
this.$store.commit('globals/setRawCoverPreviewModal', this.rawCoverUrl)
}
},
setCoverBg() {
+1 -1
View File
@@ -65,7 +65,7 @@ export default {
return 0.8 * this.sizeMultiplier
},
resolution() {
return `${this.naturalWidth}x${this.naturalHeight}px`
return `${this.naturalWidth}×${this.naturalHeight}px`
},
placeholderUrl() {
const config = this.$config || this.$nuxt.$config
+4 -4
View File
@@ -10,21 +10,21 @@
<div class="w-full p-8">
<div class="flex py-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 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-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 v-show="!isEditingRoot" class="flex py-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 class="px-2 w-52">
<ui-dropdown v-model="newUser.type" :label="$strings.LabelAccountType" :disabled="isEditingRoot" :items="accountTypes" small @input="userTypeUpdated" />
</div>
<!-- <div class="flex-grow" /> -->
<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>
<ui-toggle-switch labeledBy="user-enabled-toggle" v-model="newUser.isActive" :disabled="isEditingRoot" />
@@ -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 class="flex items-center">
<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 class="w-full h-px bg-white bg-opacity-10 my-4" />
@@ -80,8 +80,8 @@
</div>
</div>
<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 class="mb-1 text-xs">{{ _session.userId }}</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 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="mb-1">{{ playMethodName }}</p>
@@ -99,8 +99,8 @@
</div>
<div class="flex items-center">
<ui-btn v-if="!isOpenSession" 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-if="!isOpenSession && !isMediaItemShareSession" small color="error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn>
<ui-btn v-else-if="!isMediaItemShareSession" small color="error" @click.stop="closeSessionClick">Close Open Session</ui-btn>
</div>
</div>
</modals-modal>
@@ -166,6 +166,9 @@ export default {
},
isOpenSession() {
return !!this._session.open
},
isMediaItemShareSession() {
return this._session.mediaPlayer === 'web-share'
}
},
methods: {
@@ -20,14 +20,11 @@ export default {
this.$store.commit('globals/setShowRawCoverPreviewModal', val)
}
},
selectedLibraryItemId() {
return this.$store.state.globals.selectedLibraryItemId
},
rawCoverUrl() {
return this.$store.getters['globals/getLibraryItemCoverSrcById'](this.selectedLibraryItemId, null, true)
return this.$store.state.globals.selectedRawCoverUrl
}
},
methods: {},
mounted() {}
}
</script>
</script>
+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>
+15 -11
View File
@@ -9,7 +9,7 @@
<div class="flex">
<div class="w-40 p-2">
<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">
<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>
@@ -30,9 +30,6 @@
<ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" />
</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">
<ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" :label="$strings.LabelDescription" :rows="8" />
</div>
@@ -106,9 +103,9 @@ export default {
methods: {
init() {
this.imageUrl = ''
this.authorCopy.name = this.author.name
this.authorCopy.asin = this.author.asin
this.authorCopy.description = this.author.description
this.authorCopy = {
...this.author
}
},
removeClick() {
const payload = {
@@ -171,7 +168,9 @@ export default {
.$delete(`/api/authors/${this.authorId}/image`)
.then((data) => {
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) => {
console.error('Failed', error)
@@ -196,7 +195,9 @@ export default {
.then((data) => {
this.imageUrl = ''
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) => {
console.error('Failed', error)
@@ -231,8 +232,11 @@ export default {
} else if (response.updated) {
if (response.author.imagePath) {
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
this.$store.commit('globals/showEditAuthorModal', response.author)
} else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
this.authorCopy = {
...response.author
}
} else {
this.$toast.info('No updates were made for Author')
}
@@ -242,4 +246,4 @@ export default {
mounted() {},
beforeDestroy() {}
}
</script>
</script>
@@ -6,7 +6,9 @@
</div>
</template>
<div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh">
<p class="text-xl font-bold pb-4">Changelog 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>
</modals-modal>
@@ -18,17 +20,9 @@ import { marked } from '@/static/libs/marked/index.js'
export default {
props: {
value: Boolean,
changelog: String,
currentVersion: String
},
watch: {
show: {
immediate: true,
handler(newVal) {
if (newVal) {
this.init()
}
}
versionData: {
type: Object,
default: () => {}
}
},
computed: {
@@ -40,16 +34,27 @@ export default {
this.$emit('input', val)
}
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
},
changelog() {
return this.versionData?.currentVersionChangelog || 'No Changelog Available'
},
compiledMarkedown() {
return marked.parse(this.changelog, { gfm: true, breaks: true })
},
currentVersionPubDate() {
if (!this.versionData?.currentVersionPubDate) return 'Unknown release date'
return `${this.$formatDate(this.versionData.currentVersionPubDate, this.dateFormat)}`
},
currentTagUrl() {
return this.versionData?.currentTagUrl
},
currentVersionNumber() {
return this.currentVersion
return this.$config.version
}
},
methods: {
init() {}
},
methods: {},
mounted() {}
}
</script>
@@ -57,7 +62,7 @@ export default {
<style scoped>
/*
1. we need to manually define styles to apply to the parsed markdown elements,
since we don't have access to the actual elements in this component
since we don't have access to the actual elements in this component
2. v-deep allows these to take effect on the content passed in to the v-html in the div above
*/
@@ -70,4 +75,4 @@ since we don't have access to the actual elements in this component
.custom-text ::v-deep > ul {
@apply list-disc list-inside pb-4;
}
</style>
</style>
@@ -122,7 +122,7 @@ export default {
})
.catch((error) => {
console.error('Failed to get collections', error)
this.$toast.error('Failed to load collections')
this.$toast.error(this.$strings.ToastFailedToLoadData)
})
.finally(() => {
this.processing = false
@@ -46,7 +46,12 @@ export default {
ereaderDevice: {
type: Object,
default: () => null
}
},
users: {
type: Array,
default: () => []
},
loadUsers: Function
},
data() {
return {
@@ -56,8 +61,7 @@ export default {
email: '',
availabilityOption: 'adminAndUp',
users: []
},
users: []
}
}
},
watch: {
@@ -108,25 +112,13 @@ export default {
methods: {
availabilityOptionChanged(option) {
if (option === 'specificUsers' && !this.users.length) {
this.loadUsers()
this.callLoadUsers()
}
},
async loadUsers() {
async callLoadUsers() {
this.processing = true
this.users = await this.$axios
.$get('/api/users')
.then((res) => {
return res.users.sort((a, b) => {
return a.createdAt - b.createdAt
})
})
.catch((error) => {
console.error('Failed', error)
return []
})
.finally(() => {
this.processing = false
})
await this.loadUsers()
this.processing = false
},
submitForm() {
this.$refs.ereaderNameInput.blur()
@@ -226,10 +218,6 @@ export default {
this.newDevice.email = this.ereaderDevice.email
this.newDevice.availabilityOption = this.ereaderDevice.availabilityOption || 'adminOrUp'
this.newDevice.users = this.ereaderDevice.users || []
if (this.newDevice.availabilityOption === 'specificUsers' && !this.users.length) {
this.loadUsers()
}
} else {
this.newDevice.name = ''
this.newDevice.email = ''
@@ -1,10 +1,10 @@
<template>
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
<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">
<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>
@@ -23,7 +23,7 @@ export default {
},
computed: {
media() {
return this.libraryItem ? this.libraryItem.media || {} : {}
return this.libraryItem?.media || {}
},
chapters() {
return this.media.chapters || []
@@ -32,6 +32,15 @@ export default {
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>
+26 -12
View File
@@ -32,7 +32,7 @@
</div>
<p class="text-xl pl-3">{{ $strings.HeaderUpdateDetails }}</p>
</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">
<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">
@@ -42,13 +42,13 @@
<div class="flex py-2">
<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">
<covers-preview-cover :src="selectedMatch.cover" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</a>
</div>
<div v-if="media.coverPath">
<p class="text-center text-gray-200">Current</p>
<div v-if="media.coverPath" class="ml-0.5">
<p class="text-center text-gray-200">{{ $strings.LabelCurrent }}</p>
<a :href="$store.getters['globals/getLibraryItemCoverSrc'](libraryItem, null, true)" target="_blank" class="bg-primary">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrc'](libraryItem, null, true)" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</a>
@@ -79,7 +79,7 @@
<div v-if="selectedMatchOrig.narrator" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.narrator" checkbox-bg="bg" @input="checkboxToggled" />
<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>
</div>
</div>
@@ -122,7 +122,7 @@
<div v-if="selectedMatchOrig.tags" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.tags" checkbox-bg="bg" @input="checkboxToggled" />
<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>
</div>
</div>
@@ -180,14 +180,14 @@
<ui-checkbox v-model="selectedMatchUsage.explicit" checkbox-bg="bg" @input="checkboxToggled" />
<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" />
<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 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" />
<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" />
<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>
@@ -280,6 +280,9 @@ export default {
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
filterData() {
return this.$store.state.libraries.filterData
},
providers() {
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
return this.$store.state.scanners.providers
@@ -305,11 +308,16 @@ export default {
isPodcast() {
return this.mediaType == 'podcast'
},
narrators() {
return this.filterData.narrators || []
},
genres() {
const filterData = this.$store.state.libraries.filterData || {}
const currentGenres = filterData.genres || []
const currentGenres = this.filterData.genres || []
const selectedMatchGenres = this.selectedMatch.genres || []
return [...new Set([...currentGenres, ...selectedMatchGenres])]
},
tags() {
return this.filterData.tags || []
}
},
methods: {
@@ -479,6 +487,12 @@ export default {
// match.genres = match.genres.join(',')
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)
@@ -522,11 +536,11 @@ export default {
)
updatePayload.metadata.authors = authorPayload
} 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') {
updatePayload.metadata.genres = [...this.selectedMatch[key]]
} else if (key === 'tags') {
updatePayload.tags = this.selectedMatch[key].split(',').map((v) => v.trim())
updatePayload.tags = this.selectedMatch[key]
} else if (key === 'itunesId') {
updatePayload.metadata.itunesId = Number(this.selectedMatch[key])
} else {
+3 -12
View File
@@ -2,11 +2,8 @@
<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>
<!-- 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 -->
<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>
<p class="text-lg">{{ $strings.LabelToolsMakeM4b }}</p>
@@ -23,7 +20,7 @@
</div>
<!-- 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>
<p class="text-lg">{{ $strings.LabelToolsEmbedMetadata }}</p>
@@ -111,12 +108,6 @@ export default {
},
isEncodeTaskRunning() {
return this.encodeTask && !this.encodeTask?.isFinished
},
isWindowsInstall() {
return this.Source == 'windows'
},
Source() {
return this.$store.state.Source
}
},
methods: {
@@ -141,4 +132,4 @@ export default {
}
}
}
</script>
</script>
@@ -60,8 +60,19 @@
</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-52" @input="formUpdated" />
<ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-72" menu-max-height="200px" @input="formUpdated" />
</div>
</div>
</template>
@@ -83,6 +94,7 @@ export default {
skipMatchingMediaWithAsin: false,
skipMatchingMediaWithIsbn: false,
audiobooksOnly: false,
epubsAllowScriptedContent: false,
hideSingleBookSeries: false,
onlyShowLaterBooksInContinueSeries: false,
podcastSearchRegion: 'us'
@@ -118,6 +130,7 @@ export default {
skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin,
skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn,
audiobooksOnly: !!this.audiobooksOnly,
epubsAllowScriptedContent: !!this.epubsAllowScriptedContent,
hideSingleBookSeries: !!this.hideSingleBookSeries,
onlyShowLaterBooksInContinueSeries: !!this.onlyShowLaterBooksInContinueSeries,
podcastSearchRegion: this.podcastSearchRegion
@@ -133,6 +146,7 @@ export default {
this.skipMatchingMediaWithAsin = !!this.librarySettings.skipMatchingMediaWithAsin
this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn
this.audiobooksOnly = !!this.librarySettings.audiobooksOnly
this.epubsAllowScriptedContent = !!this.librarySettings.epubsAllowScriptedContent
this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries
this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries
this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us'
@@ -142,4 +156,4 @@ export default {
this.init()
}
}
</script>
</script>
@@ -115,7 +115,7 @@ export default {
})
.catch((error) => {
console.error('Failed to get playlists', error)
this.$toast.error('Failed to load user playlists')
this.$toast.error(this.$strings.ToastFailedToLoadData)
})
.finally(() => {
this.processing = false
+9 -7
View File
@@ -5,10 +5,10 @@
<!-- <span class="material-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
<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 direction="top" :text="$strings.LabelSleepTimer">
<ui-tooltip v-if="!hideSleepTimer" direction="top" :text="$strings.LabelSleepTimer">
<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>
<div v-else class="flex items-center">
@@ -18,7 +18,7 @@
</button>
</ui-tooltip>
<ui-tooltip v-if="!isPodcast" direction="top" :text="$strings.LabelViewBookmarks">
<ui-tooltip v-if="!isPodcast && !hideBookmarks" direction="top" :text="$strings.LabelViewBookmarks">
<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>
</button>
@@ -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 class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto">&nbsp;/&nbsp;{{ progressPercent }}%</p>
<div class="flex-grow" />
<p class="text-xs sm:text-sm text-gray-300 pt-0.5">
{{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400">&nbsp;({{ currentChapterIndex + 1 }} of {{ chapters.length }})</span>
<p class="text-xs sm:text-sm text-gray-300 pt-0.5 px-2 truncate">
{{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400">&nbsp;({{ $getString('LabelPlayerChapterNumberMarker', [currentChapterIndex + 1, chapters.length]) }})</span>
</p>
<div class="flex-grow" />
<p class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
@@ -78,7 +78,9 @@ export default {
},
sleepTimerSet: Boolean,
sleepTimerRemaining: Number,
isPodcast: Boolean
isPodcast: Boolean,
hideBookmarks: Boolean,
hideSleepTimer: Boolean
},
data() {
return {
@@ -368,4 +370,4 @@ export default {
left: 100%;
}
}
</style>
</style>
+12 -2
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 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">
<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" />
@@ -131,4 +131,14 @@ 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,
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)
})
},
@@ -386,4 +386,4 @@ export default {
.pagemenu {
max-height: calc(100% - 48px);
}
</style>
</style>
+11 -4
View File
@@ -46,7 +46,8 @@ export default {
font: 'serif',
fontScale: 100,
lineSpacing: 115,
spread: 'auto'
spread: 'auto',
textStroke: 0
}
}
},
@@ -63,6 +64,9 @@ export default {
libraryItemId() {
return this.libraryItem?.id
},
allowScriptedContent() {
return this.$store.getters['libraries/getLibraryEpubsAllowScriptedContent']
},
hasPrev() {
return !this.rendition?.location?.atStart
},
@@ -106,11 +110,14 @@ export default {
const fontScale = this.ereaderSettings.fontScale / 100
const textStroke = this.ereaderSettings.textStroke / 100
return {
'*': {
color: `${fontColor}!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: {
color: `${fontColor}!important`
@@ -192,7 +199,7 @@ export default {
*/
updateProgress(payload) {
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)
})
},
@@ -316,7 +323,7 @@ export default {
reader.rendition = reader.book.renderTo('viewer', {
width: this.readerWidth,
height: this.readerHeight * 0.8,
allowScriptedContent: true,
allowScriptedContent: this.allowScriptedContent,
spread: 'auto',
snap: true,
manager: 'continuous',
+16 -15
View File
@@ -23,13 +23,10 @@
<div class="flex items-center justify-center">
<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>
<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 class="text-center py-2 text-lg">
<p>{{ page }} / {{ numPages }}</p>
</div> -->
</div>
</template>
@@ -57,7 +54,8 @@ export default {
rotate: 0,
loadedRatio: 0,
page: 1,
numPages: 0
numPages: 0,
pdfDocInitParams: null
}
},
computed: {
@@ -108,14 +106,6 @@ export default {
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
}
return `/api/items/${this.libraryItemId}/ebook`
},
pdfDocInitParams() {
return {
url: this.ebookUrl,
httpHeaders: {
Authorization: `Bearer ${this.userToken}`
}
}
}
},
methods: {
@@ -136,7 +126,7 @@ export default {
ebookLocation: this.page,
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)
})
},
@@ -149,6 +139,7 @@ export default {
this.loadedRatio = progress
},
numPagesLoaded(e) {
if (!e) return
this.numPages = e
},
prev() {
@@ -167,15 +158,25 @@ export default {
resize() {
this.windowWidth = window.innerWidth
this.windowHeight = window.innerHeight
},
init() {
this.pdfDocInitParams = {
url: this.ebookUrl,
httpHeaders: {
Authorization: `Bearer ${this.userToken}`
}
}
}
},
mounted() {
this.windowWidth = window.innerWidth
this.windowHeight = window.innerHeight
window.addEventListener('resize', this.resize)
this.init()
},
beforeDestroy() {
window.removeEventListener('resize', this.resize)
}
}
</script>
</script>
+16 -3
View File
@@ -98,6 +98,12 @@
</div>
<ui-range-input v-model="ereaderSettings.lineSpacing" :min="100" :max="300" :step="5" @input="settingsUpdated" />
</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="w-40">
<p class="text-lg">{{ $strings.LabelLayout }}:</p>
@@ -130,7 +136,9 @@ export default {
font: 'serif',
fontScale: 100,
lineSpacing: 115,
spread: 'auto'
fontBoldness: 100,
spread: 'auto',
textStroke: 0
}
}
},
@@ -378,7 +386,12 @@ export default {
try {
const settings = localStorage.getItem('ereaderSettings')
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()
}
} catch (error) {
@@ -416,4 +429,4 @@ export default {
height: 100%;
}
}
</style>
</style>
+2 -2
View File
@@ -271,7 +271,7 @@ export default {
this.$emit('update:processing', true)
this.yearStats = await this.$axios.$get(`/api/me/stats/year/${this.year}`).catch((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
})
await this.initCanvas()
@@ -282,4 +282,4 @@ export default {
this.init()
}
}
</script>
</script>
@@ -250,7 +250,7 @@ export default {
this.$emit('update:processing', true)
this.yearStats = await this.$axios.$get(`/api/stats/year/${this.year}`).catch((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
})
await this.initCanvas()
@@ -261,4 +261,4 @@ export default {
this.init()
}
}
</script>
</script>
@@ -180,7 +180,7 @@ export default {
this.$emit('update:processing', true)
this.yearStats = await this.$axios.$get(`/api/me/stats/year/${this.year}`).catch((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
})
await this.initCanvas()
@@ -191,4 +191,4 @@ export default {
this.init()
}
}
</script>
</script>
@@ -94,11 +94,11 @@ export default {
this.$axios
.$delete(`/api/items/${this.libraryItemId}/file/${this.track.audioFile.ino}`)
.then(() => {
this.$toast.success('File deleted')
this.$toast.success(this.$strings.ToastDeleteFileSuccess)
})
.catch((error) => {
console.error('Failed to delete file', error)
this.$toast.error('Failed to delete file')
this.$toast.error(this.$strings.ToastDeleteFileFailed)
})
}
},
@@ -112,4 +112,4 @@ export default {
},
mounted() {}
}
</script>
</script>
+2 -2
View File
@@ -171,12 +171,12 @@ export default {
this.$axios
.$get('/api/backups')
.then((data) => {
this.$emit('loaded', data.backupLocation)
this.$emit('loaded', data)
this.setBackups(data.backups || [])
})
.catch((error) => {
console.error('Failed to load backups', error)
this.$toast.error('Failed to load backups')
this.$toast.error(this.$strings.ToastFailedToLoadData)
})
.finally(() => {
this.processing = false
+11 -5
View File
@@ -4,7 +4,7 @@
<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>
<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' : ''">
<span class="material-icons text-4xl">expand_more</span>
</div>
@@ -15,7 +15,7 @@
<th class="text-left w-16"><span class="px-4">Id</span></th>
<th class="text-left">{{ $strings.LabelTitle }}</th>
<th class="text-center">{{ $strings.LabelStart }}</th>
<th class="text-center">{{ $strings.LabelEnd }}</th>
<th class="text-center">{{ $strings.LabelDuration }}</th>
</tr>
<tr v-for="chapter in chapters" :key="chapter.id">
<td class="text-left">
@@ -27,8 +27,8 @@
<td class="font-mono text-center hover:underline cursor-pointer" @click.stop="goToTimestamp(chapter.start)">
{{ $secondsToTimestamp(chapter.start) }}
</td>
<td class="font-mono text-center hover:underline cursor-pointer" @click.stop="goToTimestamp(chapter.start)">
{{ $secondsToTimestamp(chapter.end) }}
<td class="font-mono text-center">
{{ $secondsToTimestamp(Math.max(0, chapter.end - chapter.start)) }}
</td>
</tr>
</table>
@@ -107,8 +107,14 @@ export default {
}
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() {}
}
</script>
</script>
@@ -21,7 +21,7 @@
</tr>
</table>
<div v-else-if="!processing" class="text-center py-8">
<p class="text-lg">No custom metadata providers</p>
<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">
@@ -115,11 +115,11 @@ export default {
this.$axios
.$delete(`/api/items/${this.libraryItemId}/file/${this.file.ino}`)
.then(() => {
this.$toast.success('File deleted')
this.$toast.success(this.$strings.ToastDeleteFileSuccess)
})
.catch((error) => {
console.error('Failed to delete file', error)
this.$toast.error('Failed to delete file')
this.$toast.error(this.$strings.ToastDeleteFileFailed)
})
.finally(() => {
this.processing = false
@@ -136,4 +136,4 @@ export default {
},
mounted() {}
}
</script>
</script>
@@ -89,11 +89,11 @@ export default {
this.$axios
.$delete(`/api/items/${this.libraryItemId}/file/${this.file.ino}`)
.then(() => {
this.$toast.success('File deleted')
this.$toast.success(this.$strings.ToastDeleteFileSuccess)
})
.catch((error) => {
console.error('Failed to delete file', error)
this.$toast.error('Failed to delete file')
this.$toast.error(this.$strings.ToastDeleteFileFailed)
})
}
},
@@ -107,4 +107,4 @@ export default {
},
mounted() {}
}
</script>
</script>
+1 -1
View File
@@ -10,7 +10,7 @@
<th class="w-32 hidden sm:table-cell">{{ $strings.LabelCreatedAt }}</th>
<th class="w-32"></th>
</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>
<div class="flex items-center">
<widgets-online-indicator :value="!!usersOnline[user.id]" />
@@ -38,7 +38,7 @@
<div v-if="userCanUpdate" class="mx-1" :class="isHovering ? '' : 'ml-6'">
<ui-icon-btn icon="edit" borderless @click="clickEdit" />
</div>
<div v-if="userCanDelete" class="mx-1">
<div class="mx-1" :class="isHovering ? '' : 'ml-6'">
<ui-icon-btn icon="close" borderless @click="removeClick" />
</div>
</div>
@@ -75,8 +75,7 @@ export default {
},
computed: {
translateDistance() {
if (!this.userCanUpdate && !this.userCanDelete) return 'translate-x-0'
else if (!this.userCanUpdate || !this.userCanDelete) return '-translate-x-12'
if (!this.userCanUpdate) return '-translate-x-12'
return '-translate-x-24'
},
libraryItem() {
@@ -233,4 +232,4 @@ export default {
},
mounted() {}
}
</script>
</script>
@@ -20,7 +20,7 @@
<td class="px-4">
<div class="flex items-center">
<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>
</td>
<td>
+1 -1
View File
@@ -1,5 +1,5 @@
<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 />
<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">
+6 -2
View File
@@ -13,7 +13,7 @@
</button>
<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">
<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">
@@ -41,7 +41,11 @@ export default {
default: () => []
},
disabled: Boolean,
small: Boolean
small: Boolean,
menuMaxHeight: {
type: String,
default: '224px'
}
},
data() {
return {
+9 -3
View File
@@ -83,15 +83,21 @@ export default {
},
async updateLibrary(library) {
var currLibraryId = this.currentLibraryId
if (currLibraryId === library.id) {
return
}
this.disabled = true
await this.$store.dispatch('libraries/fetch', library.id)
if (this.$route.name.startsWith('config')) {
// No need to refresh
} else if (this.$route.name.startsWith('library')) {
var newRoute = this.$route.path.replace(currLibraryId, library.id)
} else if (this.$route.name.startsWith('library') && this.$route.name !== 'library-library-series-id') {
const newRoute = this.$route.path.replace(currLibraryId, library.id)
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 {
this.$router.push(`/library/${library.id}`)
}
@@ -107,4 +113,4 @@ export default {
.librariesDropdownMenu {
max-height: calc(100vh - 75px);
}
</style>
</style>
+19 -8
View File
@@ -302,6 +302,14 @@ export default {
this.recalcMenuPos()
})
},
resetInput() {
this.textInput = null
this.currentSearch = null
this.selectedMenuItemIndex = null
this.$nextTick(() => {
this.blur()
})
},
insertNewItem(item) {
this.selected.push(item)
this.$emit('input', this.selected)
@@ -316,15 +324,18 @@ export default {
submitForm() {
if (!this.textInput) return
var cleaned = this.textInput.trim()
var matchesItem = this.items.find((i) => {
return i === cleaned
})
if (matchesItem) {
this.clickedOption(null, matchesItem)
const cleaned = this.textInput.trim()
if (!cleaned) {
this.resetInput()
} 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() {
@@ -352,4 +363,4 @@ input:read-only {
color: #aaa;
background-color: #444;
}
</style>
</style>
+34 -2
View File
@@ -1,12 +1,33 @@
<template>
<div ref="wrapper" class="relative">
<input :id="inputId" :name="inputName" ref="input" v-model="inputValue" :type="actualType" :step="step" :min="min" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" dir="auto" class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
<input
:id="inputId"
:name="inputName"
ref="input"
v-model="inputValue"
:type="actualType"
:step="step"
:min="min"
:readonly="readonly"
:disabled="disabled"
:placeholder="placeholder"
dir="auto"
class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full"
:class="classList"
@keyup="keyup"
@change="change"
@focus="focused"
@blur="blurred"
/>
<div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
<span class="material-icons text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
</div>
<div v-if="type === 'password' && isHovering" class="absolute top-0 right-0 h-full px-4 flex items-center justify-center">
<span class="material-icons-outlined text-gray-400 cursor-pointer text-lg" @click.stop.prevent="showPassword = !showPassword">{{ !showPassword ? 'visibility' : 'visibility_off' }}</span>
</div>
<div v-else-if="showCopy" class="absolute top-0 right-0 h-full px-4 flex items-center justify-center">
<span class="material-icons-outlined text-gray-400 cursor-pointer text-lg" @click.stop.prevent="copyToClipboard">{{ !hasCopied ? 'content_copy' : 'done' }}</span>
</div>
</div>
</template>
@@ -34,6 +55,7 @@ export default {
clearable: Boolean,
inputId: String,
inputName: String,
showCopy: Boolean,
step: [String, Number],
min: [String, Number]
},
@@ -41,7 +63,8 @@ export default {
return {
showPassword: false,
isHovering: false,
isFocused: false
isFocused: false,
hasCopied: false
}
},
computed: {
@@ -67,6 +90,15 @@ export default {
}
},
methods: {
copyToClipboard() {
if (this.hasCopied) return
this.$copyToClipboard(this.inputValue).then((success) => {
this.hasCopied = success
setTimeout(() => {
this.hasCopied = false
}, 2000)
})
},
clear() {
this.inputValue = ''
this.$emit('clear')
+3 -3
View File
@@ -1,5 +1,5 @@
<template>
<div class="relative">
<div tabindex="0" @focus="focusDigit('second0')" class="relative">
<div class="rounded text-gray-200 border w-full px-3 py-2" :class="focusedDigit ? 'bg-primary bg-opacity-50 border-gray-300' : 'bg-primary border-gray-600'" @click="clickInput" v-click-outside="clickOutsideObj">
<div class="flex items-center">
<template v-for="(digit, index) in digitDisplay">
@@ -174,7 +174,7 @@ export default {
return this.increaseFocused()
} else if (evt.key === 'ArrowDown') {
return this.decreaseFocused()
} else if (evt.key === 'Enter' || evt.key === 'Escape') {
} else if (evt.key === 'Enter' || evt.key === 'Escape' || evt.key === 'Tab') {
return this.removeFocus()
}
@@ -209,4 +209,4 @@ export default {
.digit-focused {
background-color: #555;
}
</style>
</style>
-112
View File
@@ -1,112 +0,0 @@
<template>
<div class="w-full">
<div class="flex items-center py-3">
<slot />
<div class="flex-grow" />
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
<span class="material-icons text-2xl">chevron_left</span>
</button>
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
<span class="material-icons text-2xl">chevron_right</span>
</button>
</div>
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex space-x-4" :style="{ height: height + 'px' }">
<template v-for="(item, index) in items">
<cards-author-card :key="item.id" :ref="`slider-item-${item.id}`" :index="index" :author="item" :height="cardHeight" :width="cardWidth" class="relative" @edit="editAuthor" @hook:updated="setScrollVars" />
</template>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: () => []
},
height: {
type: Number,
default: 192
},
bookshelfView: {
type: Number,
default: 1
}
},
data() {
return {
isScrollable: false,
canScrollLeft: false,
canScrollRight: false,
clientWidth: 0
}
},
computed: {
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
cardScaleMulitiplier() {
return this.height / 192
},
cardHeight() {
return this.height
},
cardWidth() {
return this.cardHeight / this.bookCoverAspectRatio / 1.25
},
booksPerPage() {
return Math.floor(this.clientWidth / (this.cardWidth + 16))
},
isSelectionMode() {
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
}
},
methods: {
editAuthor(author) {
this.$store.commit('globals/showEditAuthorModal', author)
},
scrolled() {
this.setScrollVars()
},
scrollRight() {
if (!this.canScrollRight) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const maxScrollLeft = slider.scrollWidth - slider.clientWidth
const newScrollLeft = Math.min(maxScrollLeft, slider.scrollLeft + scrollAmount)
slider.scrollLeft = newScrollLeft
},
scrollLeft() {
if (!this.canScrollLeft) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const newScrollLeft = Math.max(0, slider.scrollLeft - scrollAmount)
slider.scrollLeft = newScrollLeft
},
setScrollVars() {
const slider = this.$refs.slider
if (!slider) return
const { scrollLeft, scrollWidth, clientWidth } = slider
const scrollPercent = (scrollLeft + clientWidth) / scrollWidth
this.clientWidth = clientWidth
this.isScrollable = scrollWidth > clientWidth
this.canScrollRight = scrollPercent < 1
this.canScrollLeft = scrollLeft > 0
}
},
updated() {
this.setScrollVars()
},
mounted() {},
beforeDestroy() {}
}
</script>
@@ -2,7 +2,7 @@
<div>
<div class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent>
<span class="material-icons" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize" aria-label="Decrease Cover Size" role="button">remove</span>
<p class="px-2 font-mono">{{ bookCoverWidth }}</p>
<p class="px-2 font-mono" style="font-size: 1rem">{{ bookCoverWidth }}</p>
<span class="material-icons" :class="selectedSizeIndex === availableSizes.length - 1 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="increaseSize" aria-label="Increase Cover Size" role="button">add</span>
</div>
</div>
@@ -48,4 +48,4 @@ export default {
},
mounted() {}
}
</script>
</script>
-161
View File
@@ -1,161 +0,0 @@
<template>
<div class="w-full">
<div class="flex items-center py-3">
<slot />
<div class="flex-grow" />
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
<span class="material-icons text-2xl">chevron_left</span>
</button>
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
<span class="material-icons text-2xl">chevron_right</span>
</button>
</div>
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex space-x-4" :style="{ height: height + 'px' }">
<template v-for="(item, index) in items">
<cards-lazy-book-card
:key="item.recentEpisode.id"
:ref="`slider-episode-${item.recentEpisode.id}`"
:index="index"
:book-mount="item"
:height="cardHeight"
:width="cardWidth"
:book-cover-aspect-ratio="bookCoverAspectRatio"
:bookshelf-view="bookshelfView"
:continue-listening-shelf="continueListeningShelf"
class="relative"
@edit="editEpisode"
@editPodcast="editPodcast"
@select="selectItem"
@hook:updated="setScrollVars"
/>
</template>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: () => []
},
height: {
type: Number,
default: 192
},
bookshelfView: {
type: Number,
default: 1
},
continueListeningShelf: Boolean
},
data() {
return {
isScrollable: false,
canScrollLeft: false,
canScrollRight: false,
clientWidth: 0
}
},
computed: {
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
cardScaleMulitiplier() {
return this.height / 192
},
cardHeight() {
return this.height - 40 * this.cardScaleMulitiplier
},
cardWidth() {
return this.cardHeight / this.bookCoverAspectRatio
},
booksPerPage() {
return Math.floor(this.clientWidth / (this.cardWidth + 16))
},
isSelectionMode() {
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
}
},
methods: {
clearSelectedEntities() {
this.updateSelectionMode(false)
},
editEpisode({ libraryItem, episode }) {
this.$store.commit('setSelectedLibraryItem', libraryItem)
this.$store.commit('globals/setSelectedEpisode', episode)
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
},
editPodcast(libraryItem) {
var itemIds = this.items.map((e) => e.id)
this.$store.commit('setBookshelfBookIds', itemIds)
this.$store.commit('showEditModal', libraryItem)
},
selectItem(payload) {
this.$emit('selectEntity', payload)
},
itemSelectedEvt() {
this.updateSelectionMode(this.isSelectionMode)
},
updateSelectionMode(val) {
const selectedMediaItems = this.$store.state.globals.selectedMediaItems
this.items.forEach((ent) => {
let component = this.$refs[`slider-episode-${ent.recentEpisode.id}`]
if (!component || !component.length) return
component = component[0]
component.setSelectionMode(val)
component.selected = selectedMediaItems.some((i) => i.id === ent.id)
})
},
scrolled() {
this.setScrollVars()
},
scrollRight() {
if (!this.canScrollRight) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const maxScrollLeft = slider.scrollWidth - slider.clientWidth
const newScrollLeft = Math.min(maxScrollLeft, slider.scrollLeft + scrollAmount)
slider.scrollLeft = newScrollLeft
},
scrollLeft() {
if (!this.canScrollLeft) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const newScrollLeft = Math.max(0, slider.scrollLeft - scrollAmount)
slider.scrollLeft = newScrollLeft
},
setScrollVars() {
const slider = this.$refs.slider
if (!slider) return
const { scrollLeft, scrollWidth, clientWidth } = slider
const scrollPercent = (scrollLeft + clientWidth) / scrollWidth
this.clientWidth = clientWidth
this.isScrollable = scrollWidth > clientWidth
this.canScrollRight = scrollPercent < 1
this.canScrollLeft = scrollLeft > 0
}
},
updated() {
this.setScrollVars()
},
mounted() {
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
this.$eventBus.$on('item-selected', this.itemSelectedEvt)
},
beforeDestroy() {
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
this.$eventBus.$off('item-selected', this.itemSelectedEvt)
}
}
</script>
@@ -1,5 +1,5 @@
<template>
<ui-tooltip v-if="explicit" :text="$strings.LabelExplicit" direction="top">
<ui-tooltip :text="$strings.LabelExplicit" direction="top">
<svg xmlns="http://www.w3.org/2000/svg" width="12px" height="12px" viewBox="0 0 512 512" class="ml-1">
<path
fill="white"
@@ -40,9 +40,7 @@
<script>
export default {
props: {
explicit: Boolean
},
props: {},
data() {
return {}
},
+106 -55
View File
@@ -1,33 +1,21 @@
<template>
<div class="w-full">
<div class="flex items-center py-3">
<div class="flex items-center py-3e">
<slot />
<div class="flex-grow" />
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
<span class="material-icons text-2xl">chevron_left</span>
<button cy-id="leftScrollButton" v-if="isScrollable" class="w-8e h-8e mx-1e flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
<span class="material-icons" :style="{ fontSize: 1.5 + 'em' }">chevron_left</span>
</button>
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
<span class="material-icons text-2xl">chevron_right</span>
<button cy-id="rightScrollButton" v-if="isScrollable" class="w-8e h-8e mx-1e flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
<span class="material-icons" :style="{ fontSize: 1.5 + 'em' }">chevron_right</span>
</button>
</div>
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex space-x-4" :style="{ height: height + 'px' }">
<div cy-id="slider" ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex space-x-4e">
<template v-for="(item, index) in items">
<cards-lazy-book-card
:key="item.id + '-' + shelfId + '-' + index"
:ref="`slider-item-${item.id}`"
:index="index"
:book-mount="item"
:height="cardHeight"
:width="cardWidth"
:book-cover-aspect-ratio="bookCoverAspectRatio"
:bookshelf-view="bookshelfView"
:continue-listening-shelf="continueListeningShelf"
class="relative"
@edit="editItem"
@select="selectItem"
@hook:updated="setScrollVars"
/>
<div cy-id="item" ref="item" :key="itemKeyFunc(item)">
<component :is="componentName" :ref="itemRefFunc(item)" :index="index" :[itemPropName]="item" :bookshelf-view="bookshelfView" :continue-listening-shelf="continueListeningShelf" class="relative" @edit="editFunc" @editPodcast="editItem" @select="selectItem" @hook:updated="setScrollVars" />
</div>
</template>
</div>
</div>
@@ -41,49 +29,108 @@ export default {
type: Array,
default: () => []
},
height: {
type: Number,
default: 192
},
bookshelfView: {
type: Number,
default: 1
},
shelfId: String,
continueListeningShelf: Boolean
shelfId: {
type: String,
default: ''
},
continueListeningShelf: {
type: Boolean,
default: false
},
type: {
type: String,
default: 'book'
}
},
data() {
return {
isScrollable: false,
canScrollLeft: false,
canScrollRight: false,
clientWidth: 0
clientWidth: 0,
shelfOptionsByType: {
episode: {
component: 'cards-lazy-book-card',
itemPropName: 'book-mount',
itemIdFunc: (item) => item.recentEpisode.id
},
series: {
component: 'cards-lazy-series-card',
itemPropName: 'series-mount',
itemIdFunc: (item) => item.id
},
authors: {
component: 'cards-author-card',
itemPropName: 'author',
itemIdFunc: (item) => item.id
},
narrators: {
component: 'cards-narrator-card',
itemPropName: 'narrator',
itemIdFunc: (item) => item.name
},
book: {
component: 'cards-lazy-book-card',
itemPropName: 'book-mount',
itemIdFunc: (item) => item.id
},
podcast: {
component: 'cards-lazy-book-card',
itemPropName: 'book-mount',
itemIdFunc: (item) => item.id
}
}
}
},
computed: {
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
cardScaleMulitiplier() {
return this.height / 192
},
cardHeight() {
return this.height - 40 * this.cardScaleMulitiplier
},
cardWidth() {
return this.cardHeight / this.bookCoverAspectRatio
},
booksPerPage() {
return Math.floor(this.clientWidth / (this.cardWidth + 16))
},
isSelectionMode() {
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
},
options() {
return this.shelfOptionsByType[this.type]
},
itemIdFunc() {
return this.options.itemIdFunc
},
itemKeyFunc() {
return (item) => this.itemIdFunc(item) + this.shelfId
},
itemRefFunc() {
return (item) => `slider-item-${this.itemIdFunc(item)}`
},
componentName() {
return this.options.component
},
itemPropName() {
return this.options.itemPropName
},
editFunc() {
switch (this.type) {
case 'episode':
return this.editEpisode
case 'authors':
return this.editAuthor
default:
return this.editItem
}
}
},
methods: {
clearSelectedEntities() {
this.updateSelectionMode(false)
},
editEpisode({ libraryItem, episode }) {
this.$store.commit('setSelectedLibraryItem', libraryItem)
this.$store.commit('globals/setSelectedEpisode', episode)
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
},
editAuthor(author) {
this.$store.commit('globals/showEditAuthorModal', author)
},
editItem(libraryItem) {
var itemIds = this.items.map((e) => e.id)
this.$store.commit('setBookshelfBookIds', itemIds)
@@ -97,9 +144,8 @@ export default {
},
updateSelectionMode(val) {
const selectedMediaItems = this.$store.state.globals.selectedMediaItems
this.items.forEach((item) => {
let component = this.$refs[`slider-item-${item.id}`]
let component = this.$refs[this.itemRefFunc(item)]
if (!component || !component.length) return
component = component[0]
component.setSelectionMode(val)
@@ -113,7 +159,7 @@ export default {
if (!this.canScrollRight) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const scrollAmount = this.clientWidth
const maxScrollLeft = slider.scrollWidth - slider.clientWidth
const newScrollLeft = Math.min(maxScrollLeft, slider.scrollLeft + scrollAmount)
@@ -124,7 +170,7 @@ export default {
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const scrollAmount = this.clientWidth
const newScrollLeft = Math.max(0, slider.scrollLeft - scrollAmount)
slider.scrollLeft = newScrollLeft
@@ -133,11 +179,11 @@ export default {
const slider = this.$refs.slider
if (!slider) return
const { scrollLeft, scrollWidth, clientWidth } = slider
const scrollPercent = (scrollLeft + clientWidth) / scrollWidth
const scrollRemaining = Math.abs(scrollLeft + clientWidth - scrollWidth)
this.clientWidth = clientWidth
this.isScrollable = scrollWidth > clientWidth
this.canScrollRight = scrollPercent < 1
this.canScrollRight = scrollRemaining >= 1
this.canScrollLeft = scrollLeft > 0
}
},
@@ -145,12 +191,17 @@ export default {
this.setScrollVars()
},
mounted() {
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
this.$eventBus.$on('item-selected', this.itemSelectedEvt)
this.setScrollVars()
if (['book', 'podcast', 'episode'].includes(this.type)) {
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
this.$eventBus.$on('item-selected', this.itemSelectedEvt)
}
},
beforeDestroy() {
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
this.$eventBus.$off('item-selected', this.itemSelectedEvt)
if (['book', 'podcast', 'episode'].includes(this.type)) {
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
this.$eventBus.$off('item-selected', this.itemSelectedEvt)
}
}
}
</script>
</script>
+2 -2
View File
@@ -1,5 +1,5 @@
<template>
<div ref="wrapper" class="absolute bg-bg rounded-md py-1 border border-black-200 shadow-lg z-50" v-click-outside="clickOutsideObj" :style="{ width: menuWidth + 'px' }" style="top: 0; left: 0">
<div ref="wrapper" class="absolute bg-bg rounded-md py-1 border border-black-200 shadow-lg z-50" v-click-outside="clickOutsideObj" :style="{ width: menuWidth + 'px' }">
<template v-for="(item, index) in items">
<template v-if="item.subitems">
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-default" :class="{ 'bg-white/5': mouseoverItemIndex == index }" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop>
@@ -94,4 +94,4 @@ export default {
},
beforeDestroy() {}
}
</script>
</script>
@@ -1,100 +0,0 @@
<template>
<div class="w-full">
<div class="flex items-center py-3">
<slot />
<div class="flex-grow" />
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
<span class="material-icons text-2xl">chevron_left</span>
</button>
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
<span class="material-icons text-2xl">chevron_right</span>
</button>
</div>
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex space-x-4" :style="{ height: height + 'px' }">
<template v-for="item in items">
<cards-narrator-card :key="item.name" :ref="`slider-item-${item.name}`" :narrator="item" :height="cardHeight" :width="cardWidth" class="relative" @hook:updated="setScrollVars" />
</template>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: () => []
},
height: {
type: Number,
default: 192
},
bookshelfView: {
type: Number,
default: 1
}
},
data() {
return {
isScrollable: false,
canScrollLeft: false,
canScrollRight: false,
clientWidth: 0
}
},
computed: {
cardHeight() {
return this.height
},
cardWidth() {
return this.cardHeight * 1.5
},
booksPerPage() {
return Math.floor(this.clientWidth / (this.cardWidth + 16))
}
},
methods: {
scrolled() {
this.setScrollVars()
},
scrollRight() {
if (!this.canScrollRight) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const maxScrollLeft = slider.scrollWidth - slider.clientWidth
const newScrollLeft = Math.min(maxScrollLeft, slider.scrollLeft + scrollAmount)
slider.scrollLeft = newScrollLeft
},
scrollLeft() {
if (!this.canScrollLeft) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const newScrollLeft = Math.max(0, slider.scrollLeft - scrollAmount)
slider.scrollLeft = newScrollLeft
},
setScrollVars() {
const slider = this.$refs.slider
if (!slider) return
const { scrollLeft, scrollWidth, clientWidth } = slider
const scrollPercent = (scrollLeft + clientWidth) / scrollWidth
this.clientWidth = clientWidth
this.isScrollable = scrollWidth > clientWidth
this.canScrollRight = scrollPercent < 1
this.canScrollLeft = scrollLeft > 0
}
},
updated() {
this.setScrollVars()
},
mounted() {},
beforeDestroy() {}
}
</script>
-109
View File
@@ -1,109 +0,0 @@
<template>
<div class="w-full">
<div class="flex items-center py-3">
<slot />
<div class="flex-grow" />
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
<span class="material-icons text-2xl">chevron_left</span>
</button>
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
<span class="material-icons text-2xl">chevron_right</span>
</button>
</div>
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex space-x-4" :style="{ height: height + 'px' }">
<template v-for="(item, index) in items">
<cards-lazy-series-card :key="item.id" :ref="`slider-item-${item.id}`" :index="index" :series-mount="item" :height="cardHeight" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" :bookshelf-view="$constants.BookshelfView.DETAIL" class="relative" @hook:updated="setScrollVars" />
</template>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: () => []
},
height: {
type: Number,
default: 192
},
bookshelfView: {
type: Number,
default: 1
}
},
data() {
return {
isScrollable: false,
canScrollLeft: false,
canScrollRight: false,
clientWidth: 0
}
},
computed: {
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
cardScaleMulitiplier() {
return this.height / 192
},
cardHeight() {
return this.height - 40 * this.cardScaleMulitiplier
},
cardWidth() {
return 2 * (this.cardHeight / this.bookCoverAspectRatio)
},
booksPerPage() {
return Math.floor(this.clientWidth / (this.cardWidth + 16))
},
isSelectionMode() {
return this.$store.getters['globals/getIsBatchSelectingMediaItems']
}
},
methods: {
scrolled() {
this.setScrollVars()
},
scrollRight() {
if (!this.canScrollRight) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const maxScrollLeft = slider.scrollWidth - slider.clientWidth
const newScrollLeft = Math.min(maxScrollLeft, slider.scrollLeft + scrollAmount)
slider.scrollLeft = newScrollLeft
},
scrollLeft() {
if (!this.canScrollLeft) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const newScrollLeft = Math.max(0, slider.scrollLeft - scrollAmount)
slider.scrollLeft = newScrollLeft
},
setScrollVars() {
const slider = this.$refs.slider
if (!slider) return
const { scrollLeft, scrollWidth, clientWidth } = slider
const scrollPercent = (scrollLeft + clientWidth) / scrollWidth
this.clientWidth = clientWidth
this.isScrollable = scrollWidth > clientWidth
this.canScrollRight = scrollPercent < 1
this.canScrollLeft = scrollLeft > 0
}
},
updated() {
this.setScrollVars()
},
mounted() {},
beforeDestroy() {}
}
</script>
+11
View File
@@ -0,0 +1,11 @@
const { defineConfig } = require("cypress")
module.exports = defineConfig({
component: {
devServer: {
framework: "nuxt",
bundler: "webpack"
},
specPattern: "cypress/tests/**/*.cy.js"
}
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

+31
View File
@@ -0,0 +1,31 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
Cypress.Commands.overwriteQuery('get', function (originalFn, ...args) {
if (args.length > 0 && typeof args[0] === 'string' && args[0].startsWith('&')) {
args[0] = `[cy-id="${args[0].substring(1)}"]`
}
return originalFn.apply(this, args)
})
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Components App</title>
</head>
<body class="text-white bg-bg">
<div data-cy-root></div>
</body>
</html>
+38
View File
@@ -0,0 +1,38 @@
// ***********************************************************
// This example support/component.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
import '../../assets/app.css'
import './tailwind.compiled.css'
// Import commands.js using ES2015 syntax:
import './commands'
import Vue from 'vue'
import { Constants } from '../../plugins/constants'
import Strings from '../../strings/en-us.json'
import '../../plugins/utils'
import '../../plugins/init.client'
import { mount } from 'cypress/vue2'
//Cypress.Commands.add('mount', mount)
Cypress.Commands.add('mount', (component, options = {}) => {
Vue.prototype.$constants = Constants
Vue.prototype.$strings = Strings
return mount(component, options)
})
// Example use:
// cy.mount(MyComponent)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,196 @@
// Import the necessary dependencies
import AuthorCard from '@/components/cards/AuthorCard.vue'
import AuthorImage from '@/components/covers/AuthorImage.vue'
import Tooltip from '@/components/ui/Tooltip.vue'
import LoadingSpinner from '@/components/widgets/LoadingSpinner.vue'
describe('AuthorCard', () => {
const author = {
id: 1,
name: 'John Doe',
numBooks: 5
}
const propsData = {
author,
nameBelow: false
}
const mocks = {
$strings: {
LabelBooks: 'Books',
ButtonQuickMatch: 'Quick Match'
},
$store: {
getters: {
'user/getUserCanUpdate': true,
'libraries/getLibraryProvider': () => 'audible.us',
'user/getSizeMultiplier': 1
},
state: {
libraries: {
currentLibraryId: 'library-123'
}
}
},
$eventBus: {
$on: () => {},
$off: () => {}
}
}
const stubs = {
'covers-author-image': AuthorImage,
'ui-tooltip': Tooltip,
'widgets-loading-spinner': LoadingSpinner
}
const mountOptions = { propsData, mocks, stubs }
it('renders the component', () => {
cy.mount(AuthorCard, mountOptions)
cy.get('&textInline').should('be.visible')
cy.get('&match').should('be.hidden')
cy.get('&edit').should('be.hidden')
cy.get('&nameBelow').should('be.hidden')
cy.get('&card').should(($el) => {
const width = $el.width()
const height = $el.height()
const defaultHeight = 192
const defaultWidth = defaultHeight * 0.8
expect(width).to.be.closeTo(defaultWidth, 0.01)
expect(height).to.be.closeTo(defaultHeight, 0.01)
})
})
it('renders the component with the author name below', () => {
const updatedPropsData = { ...propsData, nameBelow: true }
cy.mount(AuthorCard, { ...mountOptions, propsData: updatedPropsData })
cy.get('&textInline').should('be.hidden')
cy.get('&match').should('be.hidden')
cy.get('&edit').should('be.hidden')
let nameBelowHeight
cy.get('&nameBelow')
.should('be.visible')
.and('have.text', 'John Doe')
.and(($el) => {
const height = $el.height()
const width = $el.width()
const sizeMultiplier = 1
const defaultFontSize = 16
const defaultLineHeight = 1.5
const fontSizeMultiplier = 0.75
const px2 = 16
const defaultHeight = 192
const defaultWidth = defaultHeight * 0.8
expect(height).to.be.closeTo(defaultFontSize * fontSizeMultiplier * sizeMultiplier * defaultLineHeight, 0.01)
nameBelowHeight = height
expect(width).to.be.closeTo(defaultWidth - px2, 0.01)
})
cy.get('&card').should(($el) => {
const width = $el.width()
const height = $el.height()
const py1 = 8
const defaultHeight = 192
const defaultWidth = defaultHeight * 0.8
expect(width).to.be.closeTo(defaultWidth, 0.01)
expect(height).to.be.closeTo(defaultHeight + nameBelowHeight + py1, 0.01)
})
})
it('renders quick-match and edit buttons on mouse hover', () => {
cy.mount(AuthorCard, mountOptions)
// before mouseover
cy.get('&match').should('be.hidden')
cy.get('&edit').should('be.hidden')
// after mouseover
cy.get('&card').trigger('mouseover')
cy.get('&match').should('be.visible')
cy.get('&edit').should('be.visible')
// after mouseleave
cy.get('&card').trigger('mouseleave')
cy.get('&match').should('be.hidden')
cy.get('&edit').should('be.hidden')
})
it('renders the component with spinner while searching', () => {
const data = () => {
return { searching: true, isHovering: false }
}
cy.mount(AuthorCard, { ...mountOptions, data })
cy.get('&textInline').should('be.hidden')
cy.get('&match').should('be.hidden')
cy.get('&edit').should('be.hidden')
cy.get('&spinner').should('be.visible')
})
it('toasts after quick match with no updates', () => {
const updatedMocks = {
...mocks,
$axios: {
$post: cy.stub().resolves({ updated: false, author: { name: 'John Doe' } })
},
$toast: {
success: cy.spy().as('success'),
error: cy.spy().as('error'),
info: cy.spy().as('info')
}
}
cy.mount(AuthorCard, { ...mountOptions, mocks: updatedMocks })
cy.get('&card').trigger('mouseover')
cy.get('&match').click()
cy.get('&spinner').should('be.hidden')
cy.get('@success').should('not.have.been.called')
cy.get('@error').should('not.have.been.called')
cy.get('@info').should('have.been.called')
})
it('toasts after quick match with updates and no image', () => {
const updatedMocks = {
...mocks,
$axios: {
$post: cy.stub().resolves({ updated: true, author: { name: 'John Doe' } })
},
$toast: {
success: cy.stub().as('success'),
error: cy.spy().as('error'),
info: cy.spy().as('info')
}
}
cy.mount(AuthorCard, { ...mountOptions, mocks: updatedMocks })
cy.get('&card').trigger('mouseover')
cy.get('&match').click()
cy.get('&spinner').should('be.hidden')
cy.get('@success').should('have.been.calledOnceWithExactly', 'Author John Doe was updated (no image found)')
cy.get('@error').should('not.have.been.called')
cy.get('@info').should('not.have.been.called')
})
it('toasts after quick match with updates including image', () => {
const updatedMocks = {
...mocks,
$axios: {
$post: cy.stub().resolves({ updated: true, author: { name: 'John Doe', imagePath: 'path/to/image' } })
},
$toast: {
success: cy.stub().as('success'),
error: cy.spy().as('error'),
info: cy.spy().as('info')
}
}
cy.mount(AuthorCard, { ...mountOptions, mocks: updatedMocks })
cy.get('&card').trigger('mouseover')
cy.get('&match').click()
cy.get('&spinner').should('be.hidden')
cy.get('@success').should('have.been.calledOnceWithExactly', 'Author John Doe was updated')
cy.get('@error').should('not.have.been.called')
cy.get('@info').should('not.have.been.called')
})
})
@@ -0,0 +1,85 @@
import ItemSlider from '@/components/widgets/ItemSlider.vue'
import NarratorCard from '@/components/cards/NarratorCard.vue'
import AuthorCard from '@/components/cards/AuthorCard.vue'
function createMountOptions(shelftype) {
const items = {
narrators: [
{ name: 'John Doe', numBooks: 5 },
{ name: 'Jane Doe', numBooks: 3 },
{ name: 'Jack Doe', numBooks: 1 },
{ name: 'Jill Doe', numBooks: 7 }
],
authors: [
{ id: 1, name: 'John Doe', numBooks: 5 },
{ id: 2, name: 'Jane Doe', numBooks: 3 },
{ id: 3, name: 'Jack Doe', numBooks: 1 },
{ id: 4, name: 'Jill Doe', numBooks: 7 }
]
}
const propsData = {
items: items[shelftype],
shelfId: 'shelf-123',
type: shelftype
}
const stubs = {
'cards-narrator-card': NarratorCard,
'cards-author-card': AuthorCard
}
const mocks = {
$store: {
getters: {
'user/getUserCanUpdate': true,
'user/getSizeMultiplier': 1,
'globals/getIsBatchSelectingMediaItems': false
},
state: {
libraries: {
currentLibraryId: 'library-123'
}
}
},
$eventBus: {
$on: () => {},
$off: () => {}
}
}
const slots = {
default: `<p class="font-semibold text-gray-100">${shelftype}</p>`
}
return { propsData, stubs, mocks, slots }
}
describe('ItemSlider', () => {
let mountOptions = null
beforeEach(() => {})
it('renders a narrators slider', () => {
mountOptions = createMountOptions('narrators')
cy.mount(ItemSlider, mountOptions)
cy.get('&item').should('have.length', 4)
cy.get('&leftScrollButton').should('be.visible').and('not.have.class', 'text-gray-300')
cy.get('&rightScrollButton').should('be.visible').and('have.class', 'text-gray-300')
})
it('renders an authors slider', () => {
mountOptions = createMountOptions('authors')
cy.mount(ItemSlider, mountOptions)
cy.get('&item').should('have.length', 4)
cy.get('&leftScrollButton').should('be.visible').and('not.have.class', 'text-gray-300')
cy.get('&rightScrollButton').should('be.visible').and('have.class', 'text-gray-300')
})
it('hides the scroll button when all items are visible', () => {
mountOptions = createMountOptions('narrators')
mountOptions.propsData.items = mountOptions.propsData.items.slice(0, 2)
cy.mount(ItemSlider, mountOptions)
cy.get('&leftScrollButton').should('not.exist')
cy.get('&rightScrollButton').should('not.exist')
})
})
@@ -0,0 +1,314 @@
import LazyBookCard from '@/components/cards/LazyBookCard'
import Tooltip from '@/components/ui/Tooltip.vue'
import ExplicitIndicator from '@/components/widgets/ExplicitIndicator.vue'
import LoadingSpinner from '@/components/widgets/LoadingSpinner.vue'
import { Constants } from '@/plugins/constants'
function createMountOptions() {
const book = {
id: '1',
libraryId: 'library-123',
mediaType: 'book',
media: {
id: 'book1',
metadata: { title: 'The Fellowship of the Ring', titleIgnorePrefix: 'Fellowship of the Ring', authorName: 'J. R. R. Tolkien' },
numTracks: 1
}
}
const propsData = {
index: 0,
bookMount: book,
bookshelfView: Constants.BookshelfView.DETAIL,
continueListeningShelf: false,
filterBy: null,
sortingIgnorePrefix: false,
orderBy: null
}
const stubs = {
'ui-tooltip': Tooltip,
'widgets-explicit-indicator': ExplicitIndicator,
'widgets-loading-spinner': LoadingSpinner
}
const mocks = {
$config: {
routerBasePath: 'https://my.server.com'
},
$store: {
commit: () => {},
getters: {
'user/getUserCanUpdate': true,
'user/getUserCanDelete': true,
'user/getUserCanDownload': true,
'user/getIsAdminOrUp': true,
'user/getUserMediaProgress': (id) => null,
'user/getUserSetting': (settingName) => false,
'user/getSizeMultiplier': 1,
'libraries/getLibraryProvider': () => 'audible.us',
'libraries/getBookCoverAspectRatio': 1,
'globals/getLibraryItemCoverSrc': () => 'https://my.server.com/book_placeholder.jpg',
getLibraryItemsStreaming: () => null,
getIsMediaQueued: () => false,
getIsStreamingFromDifferentLibrary: () => false
},
state: {
libraries: {
currentLibraryId: 'library-123'
},
processingBatch: false,
serverSettings: {
dateFormat: 'MM/dd/yyyy'
}
}
}
}
return { propsData, stubs, mocks }
}
describe('LazyBookCard', () => {
let mountOptions = null
beforeEach(() => {
mountOptions = createMountOptions()
})
before(() => {
// Put placeholder image is in the browser cache
mountOptions = createMountOptions()
cy.intercept('https://my.server.com/book_placeholder.jpg', { fixture: 'images/book_placeholder.jpg' }).as('bookCover')
cy.mount(LazyBookCard, mountOptions)
cy.wait('@bookCover')
// Put cover1 (aspect ratio 1.6) image in the browser cache
mountOptions = createMountOptions()
mountOptions.mocks.$store.getters['globals/getLibraryItemCoverSrc'] = () => 'https://my.server.com/cover1.jpg'
cy.intercept('https://my.server.com/cover1.jpg', { fixture: 'images/cover1.jpg' }).as('bookCover1')
cy.mount(LazyBookCard, mountOptions)
cy.wait('@bookCover1')
// Put cover2 (aspect ratio 1) image in the browser cache
mountOptions = createMountOptions()
mountOptions.mocks.$store.getters['globals/getLibraryItemCoverSrc'] = () => 'https://my.server.com/cover2.jpg'
cy.intercept('https://my.server.com/cover2.jpg', { fixture: 'images/cover2.jpg' }).as('bookCover2')
cy.mount(LazyBookCard, mountOptions)
cy.wait('@bookCover2')
})
it('renders the component correctly', () => {
cy.mount(LazyBookCard, mountOptions)
cy.get('&titleImageNotReady').should('be.hidden')
cy.get('&coverImage').should('have.css', 'opacity', '1')
cy.get('&coverBg').should('be.hidden')
cy.get('&overlay').should('be.hidden')
cy.get('&detailBottom').should('be.visible')
cy.get('&title').should('have.text', 'The Fellowship of the Ring')
cy.get('&explicitIndicator').should('not.exist')
cy.get('&line2').should('have.text', 'J. R. R. Tolkien')
cy.get('&line3').should('not.exist')
cy.get('seriesSequenceList').should('not.exist')
cy.get('&booksInSeries').should('not.exist')
cy.get('&placeholderTitle').should('be.visible')
cy.get('&placeholderTitleText').should('have.text', 'The Fellowship of the Ring')
cy.get('&placeholderAuthor').should('be.visible')
cy.get('&placeholderAuthorText').should('have.text', 'J. R. R. Tolkien')
cy.get('&progressBar').should('be.hidden')
cy.get('&finishedProgressBar').should('not.exist')
cy.get('&loadingSpinner').should('not.exist')
cy.get('&seriesNameOverlay').should('not.exist')
cy.get('&errorTooltip').should('not.exist')
cy.get('&rssFeed').should('not.exist')
cy.get('&seriesSequence').should('not.exist')
cy.get('&podcastEpisdeNumber').should('not.exist')
// this should actually fail, since the height does not cover
// the detailBottom element, currently rendered outside the card's area,
// and requires complex layout calculations outside of the component.
// todo: fix the component to render the detailBottom element inside the card's area
cy.get('#cover-area-0').should(($el) => {
const width = $el.width()
const height = $el.height()
const defaultHeight = 192
const defaultWidth = defaultHeight
expect(width).to.be.closeTo(defaultWidth, 0.01)
expect(height).to.be.closeTo(defaultHeight, 0.01)
})
})
it('shows overlay on mouseover', () => {
cy.mount(LazyBookCard, mountOptions)
cy.get('#book-card-0').trigger('mouseover')
cy.get('&titleImageNotReady').should('be.hidden')
cy.get('&overlay').should('be.visible')
cy.get('&playButton').should('be.visible')
cy.get('&readButton').should('be.hidden')
cy.get('&editButton').should('be.visible')
cy.get('&selectedRadioButton').should('be.visible').and('have.text', 'radio_button_unchecked')
cy.get('&moreButton').should('be.visible')
cy.get('&ebookFormat').should('not.exist')
})
it('routes to item page when clicked', () => {
mountOptions.mocks.$router = { push: cy.stub().as('routerPush') }
cy.mount(LazyBookCard, mountOptions)
cy.get('#book-card-0').click()
cy.get('&titleImageNotReady').should('be.hidden')
cy.get('@routerPush').should('have.been.calledOnceWithExactly', '/item/1')
})
it('shows titleImageNotReady and sets opacity 0 on coverImage when image not ready', () => {
cy.mount(LazyBookCard, mountOptions)
cy.get('&titleImageNotReady').should('be.visible')
cy.get('&coverImage').should('have.css', 'opacity', '0')
})
it('shows coverBg when coverImage has different aspect ratio', () => {
mountOptions.mocks.$store.getters['globals/getLibraryItemCoverSrc'] = () => 'https://my.server.com/cover1.jpg'
cy.mount(LazyBookCard, mountOptions)
cy.get('&titleImageNotReady').should('be.hidden')
cy.get('&coverBg').should('be.visible')
cy.get('&coverImage').should('have.class', 'object-contain')
})
it('hides coverBg when coverImage has same aspect ratio', () => {
mountOptions.mocks.$store.getters['globals/getLibraryItemCoverSrc'] = () => 'https://my.server.com/cover2.jpg'
cy.mount(LazyBookCard, mountOptions)
cy.get('&titleImageNotReady').should('be.hidden')
cy.get('&coverBg').should('be.hidden')
cy.get('&coverImage').should('have.class', 'object-fill')
})
// The logic for displaying placeholder title and author seems incorrect.
// It is currently based on existence of coverPath, but should be based weater the actual cover image is placeholder or not.
// todo: fix the logic to display placeholder title and author based on the actual cover image.
it('hides placeholderTitle and placeholderAuthor when book has cover', () => {
mountOptions.mocks.$store.getters['globals/getLibraryItemCoverSrc'] = () => 'https://my.server.com/cover1.jpg'
mountOptions.propsData.bookMount.media.coverPath = 'cover1.jpg'
cy.mount(LazyBookCard, mountOptions)
cy.get('&titleImageNotReady').should('be.hidden')
cy.get('&placeholderTitle').should('not.exist')
cy.get('&placeholderAuthor').should('not.exist')
})
it('hides detailBottom when bookShelfView is STANDARD', () => {
mountOptions.propsData.bookshelfView = Constants.BookshelfView.STANDARD
cy.mount(LazyBookCard, mountOptions)
cy.get('&titleImageNotReady').should('be.hidden')
cy.get('&detailBottom').should('not.exist')
})
it('shows explicit indicator when book is explicit', () => {
mountOptions.propsData.bookMount.media.metadata.explicit = true
cy.mount(LazyBookCard, mountOptions)
cy.get('&titleImageNotReady').should('be.hidden')
cy.get('&explicitIndicator').should('be.visible')
})
describe('when collapsedSeries is present', () => {
beforeEach(() => {
mountOptions.propsData.bookMount.collapsedSeries = {
id: 'series-123',
name: 'The Lord of the Rings',
nameIgnorePrefix: 'Lord of the Rings',
numBooks: 3,
libraryItemIds: ['1', '2', '3']
}
})
it('shows the collpased series', () => {
cy.mount(LazyBookCard, mountOptions)
cy.get('&titleImageNotReady').should('be.hidden')
cy.get('&seriesSequenceList').should('not.exist')
cy.get('&booksInSeries').should('be.visible').and('have.text', '3')
cy.get('&title').should('be.visible').and('have.text', 'The Lord of the Rings')
cy.get('&line2').should('be.visible').and('have.text', '\u00a0')
cy.get('&progressBar').should('be.hidden')
})
it('shows the seriesNameOverlay on mouseover', () => {
mountOptions.propsData.bookMount.media.metadata.series = {
id: 'series-456',
name: 'Middle Earth Chronicles',
sequence: 1
}
cy.mount(LazyBookCard, mountOptions)
cy.get('#book-card-0').trigger('mouseover')
cy.get('&titleImageNotReady').should('be.hidden')
cy.get('&seriesNameOverlay').should('be.visible').and('have.text', 'Middle Earth Chronicles')
})
it('shows the seriesSequenceList when collapsed series has a sequence list', () => {
mountOptions.propsData.bookMount.collapsedSeries.seriesSequenceList = '1-3'
cy.mount(LazyBookCard, mountOptions)
cy.get('&titleImageNotReady').should('be.hidden')
cy.get('&seriesSequenceList').should('be.visible').and('have.text', '#1-3')
cy.get('&booksInSeries').should('not.exist')
})
it('routes to the series page when clicked', () => {
mountOptions.mocks.$router = { push: cy.stub().as('routerPush') }
cy.mount(LazyBookCard, mountOptions)
cy.get('#book-card-0').click()
cy.get('&titleImageNotReady').should('be.hidden')
cy.get('@routerPush').should('have.been.calledOnceWithExactly', '/library/library-123/series/series-123')
})
it('shows the series progress bar when series has progress', () => {
mountOptions.mocks.$store.getters['user/getUserMediaProgress'] = (id) => {
switch (id) {
case '1':
return { isFinished: true }
case '2':
return { progress: 0.5 }
default:
return null
}
}
cy.mount(LazyBookCard, mountOptions)
cy.get('&titleImageNotReady').should('be.hidden')
cy.get('&progressBar')
.should('be.visible')
.and('have.class', 'bg-yellow-400')
.and(($el) => {
const width = $el.width()
const defaultHeight = 192
const defaultWidth = defaultHeight
expect(width).to.be.closeTo(((1 + 0.5) / 3) * defaultWidth, 0.01)
})
})
it('shows full green progress bar when all books are finished', () => {
mountOptions.mocks.$store.getters['user/getUserMediaProgress'] = (id) => {
return { isFinished: true }
}
cy.mount(LazyBookCard, mountOptions)
cy.get('&titleImageNotReady').should('be.hidden')
cy.get('&progressBar')
.should('be.visible')
.and('have.class', 'bg-success')
.and(($el) => {
const width = $el.width()
const defaultHeight = 192
const defaultWidth = defaultHeight
expect(width).to.be.equal(defaultWidth)
})
})
})
})
@@ -0,0 +1,221 @@
import LazySeriesCard from '@/components/cards/LazySeriesCard.vue'
import GroupCover from '@/components/covers/GroupCover.vue'
describe('LazySeriesCard', () => {
const series = {
id: 1,
name: 'The Lord of the Rings',
nameIgnorePrefix: 'Lord of the Rings',
books: [
{ id: 1, updatedAt: /* 04/14/2024 */ 1713099600000, addedAt: 1713099600000, media: { coverPath: 'cover1.jpg' }, title: 'The Fellowship of the Ring' },
{ id: 2, updatedAt: /* 04/15/2024 */ 1713186000000, addedAt: 1713186000000, media: { coverPath: 'cover2.jpg' }, title: 'The Two Towers' },
{ id: 3, updatedAt: /* 04/16/2024 */ 1713272400000, addedAt: 1713272400000, media: { coverPath: 'cover3.jpg' }, title: 'The Return of the King' }
],
addedAt: /* 04/17/2024 */ 1713358800000,
totalDuration: /* 7h 30m */ 3600 * 7 + 60 * 30,
rssFeed: 'https://example.com/feed.rss'
}
const propsData = {
index: 0,
bookshelfView: 1,
isCategorized: false,
seriesMount: series,
sortingIgnorePrefix: false,
orderBy: 'addedAt'
}
const stubs = {
'covers-group-cover': GroupCover
}
const mocks = {
$store: {
getters: {
'user/getUserCanUpdate': true,
'user/getUserMediaProgress': (id) => null,
'user/getSizeMultiplier': 1,
'libraries/getBookCoverAspectRatio': 1,
'libraries/getLibraryProvider': () => 'audible.us',
'globals/getLibraryItemCoverSrc': () => 'https://my.server.com/book_placeholder.jpg'
},
state: {
libraries: {
currentLibraryId: 'library-123'
},
serverSettings: {
dateFormat: 'MM/dd/yyyy'
}
}
}
}
before(() => {
cy.intercept('GET', 'https://my.server.com/book_placeholder.jpg', { fixture: 'images/book_placeholder.jpg' }).as('bookCover')
cy.mount(LazySeriesCard, { propsData, stubs, mocks })
cy.wait('@bookCover')
// Now the placeholder image is in the browser cache
})
it('renders the component', () => {
cy.mount(LazySeriesCard, { propsData, stubs, mocks })
cy.get('&covers-area').should(($el) => {
const width = $el.width()
const height = $el.height()
const defailtHeight = 192
const defaultWidth = defailtHeight * 2
expect(width).to.be.closeTo(defaultWidth, 0.01)
expect(height).to.be.closeTo(defailtHeight, 0.01)
})
cy.get('&seriesLengthMarker').should('be.visible').and('have.text', propsData.seriesMount.books.length)
cy.get('&seriesProgressBar').should('not.exist')
cy.get('&hoveringDisplayTitle').should('be.hidden')
cy.get('&rssFeedMarker').should('be.visible')
cy.get('&standardBottomDisplayTitle').should('not.exist')
cy.get('&detailBottomDisplayTitle').should('be.visible')
cy.get('&detailBottomDisplayTitle').should('have.text', 'The Lord of the Rings')
cy.get('&detailBottomSortLine').should('have.text', 'Added 04/17/2024')
})
it('shows series name and hides rss feed marker on mouseover', () => {
cy.mount(LazySeriesCard, { propsData, stubs, mocks })
cy.get('&card').trigger('mouseover')
cy.get('&hoveringDisplayTitle').should('be.visible').should('have.text', 'The Lord of the Rings')
cy.get('&rssFeedMarker').should('not.exist')
})
it('routes properly when clicked', () => {
const updatedMocks = {
...mocks,
$router: {
push: cy.stub().as('routerPush')
}
}
cy.mount(LazySeriesCard, { propsData, stubs, mocks: updatedMocks })
cy.get('&card').click()
cy.get('@routerPush').should('have.been.calledOnceWithExactly', '/library/library-123/series/1')
})
it('shows progress bar when progress is available', () => {
const updatedMocks = {
...mocks,
$store: {
...mocks.$store,
getters: {
...mocks.$store.getters,
'user/getUserMediaProgress': (id) => {
switch (id) {
case 1:
return { isFinished: true }
case 2:
return { progress: 0.5 }
default:
return null
}
}
}
}
}
cy.mount(LazySeriesCard, { propsData, stubs, mocks: updatedMocks })
cy.get('&seriesProgressBar')
.should('be.visible')
.and('have.class', 'bg-yellow-400')
.and(($el) => {
const width = $el.width()
const defailtHeight = 192
const defaultWidth = defailtHeight * 2
expect(width).to.be.closeTo(((1 + 0.5) / 3) * defaultWidth, 0.01)
})
})
it('shows full green progress bar when all books are finished', () => {
const updatedMocks = {
...mocks,
$store: {
...mocks.$store,
getters: {
...mocks.$store.getters,
'user/getUserMediaProgress': (id) => {
return { isFinished: true }
}
}
}
}
cy.mount(LazySeriesCard, { propsData, stubs, mocks: updatedMocks })
cy.get('&seriesProgressBar')
.should('be.visible')
.and('have.class', 'bg-success')
.and(($el) => {
const width = $el.width()
const defailtHeight = 192
const defaultWidth = defailtHeight * 2
expect(width).to.equal(defaultWidth)
})
})
it('hides the rss feed marker when there is no rss feed', () => {
const updatedPropsData = {
...propsData,
seriesMount: { ...series, rssFeed: null }
}
cy.mount(LazySeriesCard, { propsData: updatedPropsData, stubs, mocks })
cy.get('&rssFeedMarker').should('not.exist')
})
it('shows the standard bottom display when bookshelf view is 0', () => {
const updatedPropsData = {
...propsData,
bookshelfView: 0
}
cy.mount(LazySeriesCard, { propsData: updatedPropsData, stubs, mocks })
cy.get('&standardBottomDisplayTitle').should('be.visible')
cy.get('&detailBottomDisplayTitle').should('not.exist')
})
it('shows total duration in sort line when orderBy is totalDuration', () => {
const updatedPropsData = {
...propsData,
orderBy: 'totalDuration'
}
cy.mount(LazySeriesCard, { propsData: updatedPropsData, stubs, mocks })
cy.get('&detailBottomSortLine').should('have.text', 'Duration 7h 30m')
})
it('shows last book updated date in sort line when orderBy is lastBookUpdated', () => {
const updatedPropsData = {
...propsData,
orderBy: 'lastBookUpdated'
}
cy.mount(LazySeriesCard, { propsData: updatedPropsData, stubs, mocks })
cy.get('&detailBottomSortLine').should('have.text', 'Last Book Updated 04/16/2024')
})
it('shows last book added date in sort line when orderBy is lastBookAdded', () => {
const updatedPropsData = {
...propsData,
orderBy: 'lastBookAdded'
}
cy.mount(LazySeriesCard, { propsData: updatedPropsData, stubs, mocks })
cy.get('&detailBottomSortLine').should('have.text', 'Last Book Added 04/16/2024')
})
it('shows nameIgnorePrefix when sortingIgnorePrefix is true', () => {
const updatedPropsData = {
...propsData,
sortingIgnorePrefix: true
}
cy.mount(LazySeriesCard, { propsData: updatedPropsData, stubs, mocks })
cy.get('&detailBottomDisplayTitle').should('have.text', 'Lord of the Rings')
})
})
@@ -0,0 +1,84 @@
import NarratorCard from '@/components/cards/NarratorCard.vue'
describe('<NarratorCard />', () => {
const narrator = {
name: 'John Doe',
numBooks: 5
}
const propsData = {
narrator
}
const mocks = {
$store: {
getters: {
'user/getUserCanUpdate': true,
'user/getSizeMultiplier': 1
},
state: {
libraries: {
currentLibraryId: 'library-123'
}
}
},
$encode: (value) => value
}
it('renders the component', () => {
let mountOptions = { propsData, mocks }
// see: https://on.cypress.io/mounting-vue
cy.mount(NarratorCard, mountOptions)
})
it('renders the narrator name correctly', () => {
let mountOptions = { propsData, mocks }
cy.mount(NarratorCard, mountOptions)
cy.get('&name').should('have.text', 'John Doe')
})
it('renders the number of books correctly', () => {
let mountOptions = { propsData, mocks }
cy.mount(NarratorCard, mountOptions)
cy.get('&numBooks').should('have.text', '5 Books')
})
it('renders 1 book correctly', () => {
let propsData = { narrator: { name: 'John Doe', numBooks: 1 }, width: 200, height: 150 }
let mountOptions = { propsData, mocks }
cy.mount(NarratorCard, mountOptions)
cy.get('&numBooks').should('have.text', '1 Book')
})
it('renders the default name and num-books when narrator is not provided', () => {
let propsData = { width: 200, height: 150 }
let mountOptions = { propsData, mocks }
cy.mount(NarratorCard, mountOptions)
cy.get('&name').should('have.text', '')
cy.get('&numBooks').should('have.text', '0 Books')
})
it('has the correct width and height', () => {
let mountOptions = { propsData, mocks }
cy.mount(NarratorCard, mountOptions)
cy.get('&card').should('have.css', 'width', '150px')
cy.get('&card').should('have.css', 'height', '100px')
})
it('has the correct width and height when not provided', () => {
let propsData = { narrator }
let mountOptions = { propsData, mocks }
cy.mount(NarratorCard, mountOptions)
cy.get('&card').should('have.css', 'width', '150px')
cy.get('&card').should('have.css', 'height', '100px')
})
it('has the correct font sizes', () => {
let mountOptions = { propsData, mocks }
cy.mount(NarratorCard, mountOptions)
const defaultFontSize = 16
cy.get('&name').should('have.css', 'font-size', `${0.75 * defaultFontSize}px`)
cy.get('&numBooks').should('have.css', 'font-size', `${0.65 * defaultFontSize}px`)
})
})
+2 -1
View File
@@ -20,6 +20,7 @@
<modals-batch-quick-match-model />
<modals-rssfeed-open-close-modal />
<modals-raw-cover-preview-modal />
<modals-share-modal />
<prompt-confirm />
<readers-reader />
</div>
@@ -598,4 +599,4 @@ export default {
margin-left: 0px;
}
}
</style>
</style>
+45
View File
@@ -0,0 +1,45 @@
<template>
<div id="page-wrapper" class="text-white max-h-screen h-screen overflow-hidden">
<div class="absolute z-0 top-0 left-0 px-6 py-3">
<div class="flex items-center">
<nuxt-link to="/">
<img src="~static/icon.svg" alt="Audiobookshelf Logo" class="w-10 min-w-10 h-10" />
</nuxt-link>
<nuxt-link to="/">
<h1 class="text-xl ml-4 hover:underline">audiobookshelf</h1>
</nuxt-link>
</div>
</div>
<div class="w-full h-full flex items-center justify-center">
<div class="w-full p-2 sm:p-4 md:p-8">
<div class="w-full p-4">
<div class="text-center">
<h1 class="text-4xl font-semibold text-red-500 mb-4">{{ statusCode }}</h1>
<p class="text-xl font-semibold">{{ message }}</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
layout: 'blank',
props: {
error: {
type: Object,
default: null
}
},
computed: {
statusCode() {
return (this.error && this.error.statusCode) || 500
},
message() {
return this.error.message || 'Unknown error'
}
}
}
</script>
+59 -12
View File
@@ -9,7 +9,8 @@ export default {
data() {
return {
cardsHelpers: {
mountEntityCard: this.mountEntityCard
mountEntityCard: this.mountEntityCard,
setCardSize: this.setCardSize
}
}
},
@@ -21,6 +22,56 @@ export default {
if (this.entityName === 'albums') return Vue.extend(LazyAlbumCard)
return Vue.extend(LazyBookCard)
},
getComponentName() {
if (this.entityName === 'series') return 'cards-lazy-series-card'
if (this.entityName === 'collections') return 'cards-lazy-collection-card'
if (this.entityName === 'playlists') return 'cards-lazy-playlist-card'
if (this.entityName === 'albums') return 'cards-lazy-album-card'
return 'cards-lazy-book-card'
},
async setCardSize() {
this.cardWidth = 0
this.cardHeight = 0
// load a dummy card to get the its width and height
const ComponentClass = this.getComponentClass()
const props = {
index: -1,
bookshelfView: this.bookshelfView,
sortingIgnorePrefix: !!this.sortingIgnorePrefix
}
if (this.entityName === 'items') {
props.filterBy = this.filterBy
props.orderBy = this.orderBy
} else if (this.entityName === 'series') {
props.orderBy = this.seriesSortBy
}
const instance = new ComponentClass({
propsData: props
})
instance.$mount()
this.resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
this.cardWidth = entry.contentRect.width
this.cardHeight = entry.contentRect.height
this.resizeObserver.disconnect()
this.$refs.bookshelf.removeChild(instance.$el)
}
})
instance.$el.style.visibility = 'hidden'
instance.$el.style.position = 'absolute'
this.$refs.bookshelf.appendChild(instance.$el)
this.resizeObserver.observe(instance.$el)
const timeBefore = performance.now()
await new Promise((resolve) => {
const unwatch = this.$watch('cardWidth', (value) => {
if (value) {
unwatch()
resolve()
}
})
})
const timeAfter = performance.now()
},
async mountEntityCard(index) {
var shelf = Math.floor(index / this.entitiesPerShelf)
var shelfEl = document.getElementById(`shelf-${shelf}`)
@@ -34,7 +85,7 @@ export default {
shelfEl.appendChild(bookComponent.$el)
if (this.isSelectionMode) {
bookComponent.setSelectionMode(true)
if (this.selectedMediaItems.some(i => i.id === bookComponent.libraryItemId) || this.isSelectAll) {
if (this.selectedMediaItems.some((i) => i.id === bookComponent.libraryItemId) || this.isSelectAll) {
bookComponent.selected = true
} else {
bookComponent.selected = false
@@ -45,17 +96,10 @@ export default {
bookComponent.isHovering = false
return
}
const shelfOffsetY = 16
const row = index % this.entitiesPerShelf
const shelfOffsetX = row * this.totalEntityCardWidth + this.bookshelfMarginLeft
const ComponentClass = this.getComponentClass()
const props = {
index,
width: this.entityWidth,
height: this.entityHeight,
bookCoverAspectRatio: this.coverAspectRatio,
bookshelfView: this.bookshelfView,
sortingIgnorePrefix: !!this.sortingIgnorePrefix
}
@@ -82,6 +126,9 @@ export default {
this.entityComponentRefs[index] = instance
instance.$mount()
const shelfOffsetY = this.shelfPaddingHeight * this.sizeMultiplier
const row = index % this.entitiesPerShelf
const shelfOffsetX = row * this.totalEntityCardWidth + this.bookshelfMarginLeft
instance.$el.style.transform = `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)`
instance.$el.classList.add('absolute', 'top-0', 'left-0')
shelfEl.appendChild(instance.$el)
@@ -91,10 +138,10 @@ export default {
}
if (this.isSelectionMode) {
instance.setSelectionMode(true)
if (instance.libraryItemId && this.selectedMediaItems.some(i => i.id === instance.libraryItemId) || this.isSelectAll) {
if ((instance.libraryItemId && this.selectedMediaItems.some((i) => i.id === instance.libraryItemId)) || this.isSelectAll) {
instance.selected = true
}
}
},
}
}
}
}
+2
View File
@@ -153,4 +153,6 @@ module.exports = {
* See: [Issue tracker](https://github.com/nuxt-community/tailwindcss-module/issues/480)
*/
devServerHandlers: [],
ignore: ["**/*.test.*", "**/*.cy.*"]
}
+1082 -3
View File
File diff suppressed because it is too large Load Diff
+8 -3
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "2.9.0",
"version": "2.11.0",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast client",
"main": "index.js",
@@ -9,7 +9,10 @@
"dev2": "nuxt --hostname localhost --port 1337",
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate"
"generate": "nuxt generate",
"test": "npm run compile-tailwind && cypress run --component --browser chrome",
"test-visually": "npm run compile-tailwind && cypress open --component --browser chrome",
"compile-tailwind": "tailwindcss -i ./assets/tailwind.css -o ./cypress/support/tailwind.compiled.css"
},
"author": "advplyr",
"license": "ISC",
@@ -21,6 +24,7 @@
"cron-parser": "^4.7.1",
"date-fns": "^2.25.0",
"epubjs": "^0.3.88",
"fast-average-color": "^9.4.0",
"hls.js": "^1.5.7",
"libarchive.js": "^1.3.0",
"nuxt": "^2.17.3",
@@ -33,7 +37,8 @@
"devDependencies": {
"@nuxtjs/pwa": "^3.3.5",
"autoprefixer": "^10.4.7",
"cypress": "^13.7.3",
"postcss": "^8.3.6",
"tailwindcss": "^3.4.1"
}
}
}
+2 -2
View File
@@ -18,7 +18,7 @@
<div class="w-12 hidden lg:block" />
<p class="text-lg mb-4 font-semibold">{{ $strings.HeaderChapters }}</p>
<div class="flex-grow" />
<ui-checkbox v-model="showSecondInputs" checkbox-bg="primary" small label-class="text-sm text-gray-200 pl-1" label="Show seconds" class="mx-2" />
<ui-checkbox v-model="showSecondInputs" checkbox-bg="primary" small label-class="text-sm text-gray-200 pl-1" :label="$strings.LabelShowSeconds" class="mx-2" />
<div class="w-32 hidden lg:block" />
</div>
<div class="flex items-center mb-3 py-1 -mx-1">
@@ -639,4 +639,4 @@ export default {
this.destroyAudioEl()
}
}
</script>
</script>

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