Compare commits

...

446 Commits

Author SHA1 Message Date
advplyr e81b3461b2 Version bump v2.12.2 2024-08-08 17:17:22 -05:00
advplyr 9345cb3934 Update version check get changelogs 2024-08-08 17:04:50 -05:00
advplyr eb36a0b3dd Merge pull request #3247 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-08-08 16:58:06 -05:00
advplyr 7e442ecb3d Revert MemoryStore used in expressSession 2024-08-08 16:54:48 -05:00
tonttula f07c5eb725 Translated using Weblate (Finnish)
Currently translated at 32.2% (275 of 853 strings)

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

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

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

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

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

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

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

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

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

Fix https://github.com/advplyr/audiobookshelf/issues/3231
Fix https://github.com/advplyr/audiobookshelf/issues/3234
Close https://github.com/advplyr/audiobookshelf/pull/3235
2024-08-05 13:34:56 +02:00
advplyr 8ff7b6b6e6 Add server log for process.platform and process.arch #3231 2024-08-04 17:08:55 -05:00
advplyr 06eaee8909 Fix:Binary manager dylib file ext check #3231 2024-08-04 16:51:07 -05:00
advplyr 8f9487ba70 Version bump v2.12.0 2024-08-04 16:36:38 -05:00
advplyr eca51457b7 Update jsdocs and auto-formatting 2024-08-04 16:13:40 -05:00
advplyr 15c6fce648 Remove duplicate dependency ms 2024-08-04 12:52:52 -05:00
advplyr 6c872263c6 Fix:Show changelog when clicking version on config side rail #3232 2024-08-04 12:30:18 -05:00
advplyr 4d3b3d1740 Update:Replace default express-session MemoryStore with stable MemoryStore #2538 2024-08-04 12:00:10 -05:00
advplyr bba8920855 Merge pull request #3228 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-08-04 10:10:02 -05:00
gallegonovato f56b9487ff Translated using Weblate (Spanish)
Currently translated at 100.0% (853 of 853 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-08-04 00:09:30 +02:00
Mario 1946d8296b Translated using Weblate (German)
Currently translated at 100.0% (853 of 853 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-08-04 00:09:29 +02:00
burghy86 41e5d7f820 Translated using Weblate (Italian)
Currently translated at 100.0% (852 of 852 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2024-08-04 00:09:28 +02:00
weblate.user.1274 2507568103 Translated using Weblate (Norwegian Bokmål)
Currently translated at 83.4% (711 of 852 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2024-08-04 00:09:27 +02:00
gallegonovato 19733798fa Translated using Weblate (Spanish)
Currently translated at 100.0% (852 of 852 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-08-04 00:09:26 +02:00
Mario 427d6da360 Translated using Weblate (German)
Currently translated at 100.0% (852 of 852 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-08-04 00:09:25 +02:00
Mario 2b67d3d1c5 Translated using Weblate (German)
Currently translated at 100.0% (850 of 850 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-08-04 00:09:24 +02:00
gallegonovato 6926a40ad6 Translated using Weblate (Spanish)
Currently translated at 100.0% (850 of 850 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-08-04 00:09:24 +02:00
Illia Pyshniak 7a8da5bf3a Translated using Weblate (Ukrainian)
Currently translated at 100.0% (849 of 849 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2024-08-04 00:09:23 +02:00
gallegonovato fc8fa17c6f Translated using Weblate (Spanish)
Currently translated at 100.0% (849 of 849 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-08-04 00:09:22 +02:00
Mario 0a88659a9f Translated using Weblate (German)
Currently translated at 100.0% (849 of 849 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-08-04 00:09:21 +02:00
Illia Pyshniak 9967858c44 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (849 of 849 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2024-08-04 00:09:21 +02:00
Mario e2ce388f90 Translated using Weblate (German)
Currently translated at 100.0% (849 of 849 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-08-04 00:09:20 +02:00
gallegonovato f31649f1d2 Translated using Weblate (Spanish)
Currently translated at 100.0% (849 of 849 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-08-04 00:09:20 +02:00
advplyr a55c167dde Fix:Cleanup media progress when deleting podcasts, remove usage of old user model 2024-08-03 17:09:17 -05:00
advplyr 642cf232ba Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-08-03 15:08:45 -05:00
advplyr 164b4525c4 Replace function for cleaning up user seriesHideFromContinueListening to not use old model 2024-08-03 15:08:03 -05:00
advplyr 39c26d2bee Merge pull request #3217 from ic1415/LibraryItemController
Update LibraryItemController.js
2024-08-02 16:33:23 -05:00
advplyr 2a69955cc1 Update server/controllers/LibraryItemController.js 2024-08-02 16:30:21 -05:00
advplyr 4a5345dd5d Update:devcontainer dev.js default to not skip binaries check, fail gracefully if required binary env variables are not set when skipping 2024-08-01 14:25:57 -05:00
advplyr 1e6dd0e3e0 Add jsdocs for Ffmpeg and tools controller 2024-07-31 17:32:51 -05:00
advplyr 91cca2e358 Merge pull request #3214 from faush01/feature/persist_encoding_options
persist and show the advanced encoding options
2024-07-31 16:52:36 -05:00
advplyr 816a9be618 Merge pull request #3208 from nichwall/release_issue_workflow
Add: workflow to close issues on release
2024-07-31 16:07:24 -05:00
ic1415 9eb0ec76fe Update LibraryItemController.js
update library item controller to log downloaded ebooks to fix #3215
2024-07-31 10:48:41 -04:00
Shaun 49054d5239 persist the advanced encoding options, show the encoding options used with in progress encodes 2024-07-31 16:44:24 +10:00
advplyr 787c4e45a8 Merge pull request #3212 from mikiher/library-fetch
On item pages, fetch the item's library data to the store if it's not available
2024-07-30 16:52:51 -05:00
advplyr 34cb7a4d02 Remove unused loadLibraryFilterData func 2024-07-30 16:49:02 -05:00
advplyr 006241163b Replace setCurrentLibrary calls with fetch to ensure filterData matches library 2024-07-30 16:35:26 -05:00
advplyr 03818fadee Remove unnecessary setCurrentLibrary on mounted item page 2024-07-30 16:20:36 -05:00
mikiher 897c3ea625 on item pages, fetch item's library data if unavailable 2024-07-30 20:02:03 +03:00
Nicholas Wallace 73e4293f04 Fix: label has space in name 2024-07-29 18:08:40 -07:00
Nicholas Wallace 6f5ffcb1f8 Add: workflow to close issues on release 2024-07-29 18:06:05 -07:00
advplyr ed70f3af83 Fix:Material symbols icon to use check instead of checkmark 2024-07-29 17:34:05 -05:00
advplyr 73196f9be8 Update:Match tab support clicking current value to set it #3200 2024-07-29 17:31:52 -05:00
advplyr a77f4e9d77 Merge pull request #3204 from mikiher/embed-permissions
Fix permission issues in embed/merge
2024-07-29 16:49:12 -05:00
mikiher 294490f814 Fix permission issues in embed/merge 2024-07-29 20:19:58 +03:00
advplyr 6183001fca Merge pull request #3199 from mikiher/unaccent
Support accent-insensitive search using SQLean unicode sqlite3 extension
2024-07-28 17:21:32 -05:00
advplyr 3ac604c665 Remove ffmpeg binaries install step from debian preinst script 2024-07-28 16:55:45 -05:00
advplyr e342b07cd0 Update:OPF metadata parser supports namespaces on creator and identifier tags #3201 2024-07-28 14:54:17 -05:00
advplyr b524cbd1b3 Update:Parse epub cover image uses cover specified in opf meta #3201 2024-07-28 14:34:31 -05:00
advplyr 88693d73bd Fix:Shares not working with timeouts longer than 23 days #3164 2024-07-27 17:40:51 -05:00
mikiher 2c453a34ee Remove redundant console.log() message 2024-07-27 23:09:46 +03:00
mikiher 3d2b2e43b1 Set execution permission for downloaded binaries 2024-07-27 23:03:40 +03:00
mikiher c3f3fca896 Remove dependency on libs/ffbinaries from BinaryManager test 2024-07-27 22:44:01 +03:00
mikiher dedf6e5d4b Support accent-insensitive matching using the sqlean sqlite3 unicode extension 2024-07-27 21:56:07 +03:00
mikiher 6c379fc3a7 Add /unicode* to .gitignore 2024-07-27 21:52:33 +03:00
mikiher 329e9c9eb2 BinaryManager support for libraries and downloading from github release assets 2024-07-27 21:51:31 +03:00
advplyr ee53086444 Update chapter modal colors for consistency 2024-07-26 17:36:39 -05:00
advplyr 43d6c6678f Add:Random library sorting option for libraries and series #3166
- Fixed author sort and match button not showing
2024-07-25 16:10:42 -05:00
advplyr 82f136ba79 Merge pull request #3195 from mikiher/remove-match-logic
Simplify ItemSearchCard component by removing matching logic
2024-07-25 15:46:12 -05:00
mikiher e40d3dd64d Simplify ItemSearchCard component 2024-07-25 09:40:18 +03:00
advplyr a5897fd64b Fix:Set series and collection RSS feed cover image using first item with cover #3193 2024-07-24 16:40:45 -05:00
advplyr e786e3c057 Remove references to matrix server 2024-07-23 16:08:47 -05:00
advplyr d347645475 Update:Format numbers on user stats page #3187 2024-07-22 17:43:42 -05:00
advplyr 215b78c162 Merge pull request #3186 from nichwall/author_image_restore
Ensure author and items folder is created before restoring backup
2024-07-22 16:41:55 -05:00
Nicholas Wallace ee271519f9 Ensure author folder is created before extracting files 2024-07-21 18:04:46 +00:00
advplyr b350277bbc Remove unused files 2024-07-21 11:12:17 -05:00
advplyr 604ae080ac Merge pull request #3185 from mikiher/genre-search
Adds genres to gloabl search
2024-07-21 11:07:43 -05:00
advplyr a191dab359 Show numItems and numBooks on search cards 2024-07-21 11:07:54 -05:00
advplyr 3223011b13 Fix:Switching libraries on search page losing q query string 2024-07-21 10:52:23 -05:00
advplyr f746e246e4 Merge pull request #3184 from brendans-bits/optionalDependencies
Moved cypress to `optionalDependencies`
2024-07-21 09:54:05 -05:00
mikiher 0476b68585 fix: Encode search query parameter in search.vue 2024-07-21 14:00:55 +03:00
mikiher ec395bed72 fix: Encode search query parameter in GlobalSearch.vue 2024-07-21 13:53:10 +03:00
mikiher bff56220c2 Adds genres to gloabl search 2024-07-21 11:10:05 +03:00
Brendan 3006405a52 Moved cypress to optionalDependencies 2024-07-21 11:30:50 +10:00
advplyr 9cd0ac80b1 Merge pull request #3182 from mikiher/ffmpeg-progress
Add progress and fixes to m4b and embed tools
2024-07-20 15:31:03 -05:00
mikiher da51d38ba2 Improve documentation and arg names in TrackProgressMonitor.js 2024-07-20 21:42:58 +03:00
advplyr 5ba6459069 Merge pull request #3179 from nichwall/api_spec_opml
Update: podcast opml endpoints
2024-07-20 11:02:15 -05:00
mikiher 75899242fd put MessageEmbedFailed in akphabetic order. 2024-07-20 13:05:03 +03:00
mikiher 7faf42d892 Merge branch 'advplyr:master' into ffmpeg-progress 2024-07-20 12:28:47 +03:00
mikiher 10f5f331d7 Fixes + add progress to m4b and embed tools 2024-07-20 12:28:06 +03:00
Nicholas Wallace b1414388e1 Add: podcast tags to opml endpoints 2024-07-20 02:42:41 +00:00
Nicholas Wallace eb0f5b2e1b Update: podcast opml endpoints 2024-07-20 02:38:56 +00:00
advplyr 7af02ad2e2 Fix:Series bookshelf row padding when using ignore prefixes setting #3169 2024-07-19 17:12:12 -05:00
advplyr 8330dabc46 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-07-18 16:43:10 -05:00
advplyr dbc7ad0b3b Fix:Podcast episode match not properly encoding search query #3177 2024-07-18 16:43:00 -05:00
advplyr c0fd24770e Merge pull request #3170 from ajvgwu/nfo-metadata-language
Parse book language from NFO metadata source
2024-07-18 16:14:48 -05:00
advplyr 4289fe4990 Update Nfo scanner and auto-formatting 2024-07-18 16:09:40 -05:00
advplyr e925e9b23f Update:Increase size of more menu icon on items page 2024-07-18 16:06:37 -05:00
Alex 71cd86fdd5 Merge branch 'advplyr:master' into nfo-metadata-language 2024-07-18 13:26:05 -04:00
advplyr 03be947ad6 Merge pull request #3163 from ajyey/feature/disable-max-backup-size
Adds support for allowing backups of unlimited size
2024-07-17 17:09:37 -05:00
advplyr 96f9084f2e Update:Disable axios progress indicator for sync requests 2024-07-17 17:11:57 -05:00
advplyr bbccfcbd12 Merge pull request #3165 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-07-17 16:57:52 -05:00
Alex 9a697f48db feat: parse language from NFO metadata source 2024-07-17 11:50:27 -04:00
advplyr 37ad1cced2 Fix:Large OPML import timeouts #3118
- Added OPML Api endpoints for /parse and /create, removed old
- Show task for OPML import and create failed tasks for failed feeds
2024-07-16 17:05:52 -05:00
Mario 26db20f63d Translated using Weblate (German)
Currently translated at 100.0% (848 of 848 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-07-16 16:47:20 +02:00
Andrew Leonard ff788e3591 feat: adds better conditional for max backup size 2024-07-16 01:31:12 -04:00
Andrew Leonard 4b482488de feat: remember setting of 0 on server side 2024-07-16 01:30:00 -04:00
Andrew Leonard e230b6640f feat: adds unlimited text to text label 2024-07-16 01:11:20 -04:00
Andrew Leonard 2bc949fae3 feat: adds support for allowing backups of unlimited size 2024-07-15 23:58:05 -04:00
advplyr b1bc472205 Update:Incrase icon font size for more context menu and player loading 2024-07-15 17:38:13 -05:00
gallegonovato 5c7a38c292 Translated using Weblate (Spanish)
Currently translated at 100.0% (848 of 848 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-07-16 00:35:43 +02:00
Petteri Hjort bbd6c51eb6 Translated using Weblate (Finnish)
Currently translated at 21.4% (182 of 848 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2024-07-16 00:35:43 +02:00
Hosted Weblate d17f9b0687 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/
2024-07-16 00:35:43 +02:00
Marcin Martela 4d2bdb6eee Translated using Weblate (Polish)
Currently translated at 90.2% (763 of 845 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2024-07-16 00:35:43 +02:00
Vito0912 b6a1014c72 Translated using Weblate (German)
Currently translated at 100.0% (845 of 845 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-07-16 00:35:43 +02:00
gallegonovato b99885c806 Translated using Weblate (Spanish)
Currently translated at 100.0% (845 of 845 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-07-16 00:35:43 +02:00
tonttula f422c9b820 Translated using Weblate (Finnish)
Currently translated at 21.4% (181 of 844 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2024-07-16 00:35:43 +02:00
Ori Z 0befe91360 Translated using Weblate (Hebrew)
Currently translated at 92.2% (779 of 844 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/he/
2024-07-16 00:35:43 +02:00
gallegonovato da671e3fd5 Translated using Weblate (Spanish)
Currently translated at 100.0% (844 of 844 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-07-16 00:35:43 +02:00
Mario fec94c18aa Translated using Weblate (German)
Currently translated at 100.0% (844 of 844 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-07-16 00:35:43 +02:00
gallegonovato 11c6fc7d90 Translated using Weblate (Spanish)
Currently translated at 100.0% (838 of 838 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-07-16 00:35:43 +02:00
Michał Pomarański 7ea5e7dc95 Translated using Weblate (Polish)
Currently translated at 90.4% (758 of 838 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2024-07-16 00:35:43 +02:00
Mario 2a98e2c361 Translated using Weblate (German)
Currently translated at 100.0% (838 of 838 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-07-16 00:35:43 +02:00
Charlie 7fb499b301 Translated using Weblate (French)
Currently translated at 100.0% (832 of 832 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2024-07-16 00:35:43 +02:00
Lode Smets af9aee76cf Translated using Weblate (Dutch)
Currently translated at 86.7% (722 of 832 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2024-07-16 00:35:43 +02:00
gallegonovato 075ec15f02 Translated using Weblate (Spanish)
Currently translated at 100.0% (832 of 832 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-07-16 00:35:43 +02:00
Valentin 1c650473f8 Translated using Weblate (German)
Currently translated at 100.0% (832 of 832 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-07-16 00:35:43 +02:00
Valentin 0efdf50821 Translated using Weblate (German)
Currently translated at 100.0% (832 of 832 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-07-16 00:35:43 +02:00
Mario df65ef2191 Translated using Weblate (German)
Currently translated at 100.0% (832 of 832 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-07-16 00:35:43 +02:00
Ahetek bc3b1d9565 Translated using Weblate (Polish)
Currently translated at 90.8% (756 of 832 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2024-07-16 00:35:43 +02:00
advplyr 2998d3ba6a Merge pull request #3160 from glorenzen/home-page-ultrawide-support
Add ultrawide support for home page
2024-07-15 16:30:44 -05:00
Greg Lorenzen ea11153032 Update responsive limit for displayed items on personalized shelves on home page 2024-07-15 15:15:06 +00:00
Greg Lorenzen 733f61075f WIP: Add "End of chapter" option for sleep timer (#3151)
* Add SleepTimerTypes for countdown and chapter

* Add functionality for 'end of chapter' sleep timer

* Fix custom time for sleep timer

* Include end of chapter string for sleep timer

* Increase chapter end tolerance to 0.75

* Show sleep time options in modal when timer is active

* Add SleepTimerTypes for countdown and chapter

* Add functionality for 'end of chapter' sleep timer

* Fix custom time for sleep timer

* Include end of chapter string for sleep timer

* Increase chapter end tolerance to 0.75

* Show sleep time options in modal when timer is active

* Sleep timer cleanup

* Localization for sleep timer modal, UI updates

---------

Co-authored-by: advplyr <advplyr@protonmail.com>
2024-07-14 13:56:48 -05:00
Greg Lorenzen 618e69775c Add responsive limit for displayed items on personalized shelves on home page 2024-07-13 22:32:44 -07:00
advplyr eabfa90121 Update:Move library stats page to SideRail #3134 2024-07-13 15:26:07 -05:00
Greg Lorenzen 43b7ccd61a WIP: Add adjustable skip amount (#3113)
* Add playback settings string to en-us

* Add playback settings UI for jump forwards and jump backwards

* Remove jump forwards and jump backwards settings

* Remove jump forwards and jump backwards en-us strings

* Update player UI to include player settings button

* Add label view player settings string

* Add PlayerSettingsModal component

Includes a toggle switch for enabling/disabling the chapter track feature.

* Add player settings modal component to MediaPlayerContainer

* Handle useChapterTrack changes in PlayerUI

* Add jump forwards and jump backwards settings to user store

* Add jump forwards and jump backwards label strings

* Add jump forwards and jump backwards settings to PlayerSettingsModal

* Update jump forwards and jump backwards to handle user state values in PlayerHandler

* Update jump backwards icon in PlayerPlaybackControls

* Add playback settings string to en-us

* Add playback settings UI for jump forwards and jump backwards

* Remove jump forwards and jump backwards settings

* Remove jump forwards and jump backwards en-us strings

* Update player UI to include player settings button

* Add label view player settings string

* Add PlayerSettingsModal component

Includes a toggle switch for enabling/disabling the chapter track feature.

* Add player settings modal component to MediaPlayerContainer

* Handle useChapterTrack changes in PlayerUI

* Add jump forwards and jump backwards settings to user store

* Add jump forwards and jump backwards label strings

* Add jump forwards and jump backwards settings to PlayerSettingsModal

* Update jump forwards and jump backwards to handle user state values in PlayerHandler

* Update jump backwards icon in PlayerPlaybackControls

* Add jump amounts to playback controls tooltips

* Fix merge issues and add new Material Symbols to player ui

* Alphabetize strings in en-us.json

* Update dropdown component with SelectInput to support menu overflowing modal

* Update localization for player settings

* Update en-us strings order

---------

Co-authored-by: advplyr <advplyr@protonmail.com>
2024-07-12 17:52:48 -05:00
advplyr b6875a44cf Merge pull request #3152 from mikiher/fix-opf-fetch-series
Fix OPF fetchSeries so it deduplicates found series
2024-07-12 07:59:10 -05:00
mikiher c0004dd532 Fix fetchSeries so it deduplicates returned series 2024-07-12 12:42:42 +03:00
advplyr 0ee3b89760 Fix:Series and collection RSS feeds keeping correct order #3137 2024-07-11 17:49:05 -05:00
Nicholas W c5e60d30e1 Podcast endpoints (#3140)
* Add: `AudioTrack.yaml`

* Fix: audiotrack example

* Initial: podcast schemas and endpoints

* Update schemas

* Add: podcasts tag

* Update bundled spec
2024-07-11 16:29:35 -05:00
advplyr acaf1ac196 Merge pull request #3138 from glorenzen/update-material-icons
Update material icons
2024-07-10 16:41:21 -05:00
advplyr 8dc4538c95 Remove old material-icons files and classes 2024-07-10 16:41:42 -05:00
advplyr e224fd2595 Update edit library folder icon to fill 2024-07-10 16:38:58 -05:00
advplyr f0a1ea4d6d Merge pull request #3139 from nichwall/email_notification_linting_fixes
Email notification linting fixes
2024-07-09 16:25:31 -05:00
advplyr 10cb8ebf3b Merge pull request #3117 from mikiher/show-subtitles
New feature: Show Subtitles
2024-07-09 16:08:36 -05:00
advplyr 8c4afa1866 Fix typo 2024-07-09 16:09:41 -05:00
advplyr eb5af47bbf Merge branch 'master' into show-subtitles 2024-07-09 15:58:34 -05:00
Nicholas Wallace 4fd93ce64c Update bundled spec 2024-07-09 01:55:55 +00:00
Nicholas Wallace 7ba4e9e66d Add: summary to Notification endpoints 2024-07-09 01:55:35 +00:00
Nicholas Wallace e2e5449d25 Fix: schema reference in EmailController 2024-07-09 01:51:39 +00:00
Greg Lorenzen abc76ca155 Replace Material Icons in LaxyBookCard and ShareModal components 2024-07-08 22:29:36 +00:00
Greg Lorenzen 0fc84a8684 Replace material-icons in YearInReview components 2024-07-08 22:29:20 +00:00
Greg Lorenzen a76600e53b Update play_arrow icons with fill CSS class 2024-07-08 22:00:16 +00:00
Greg Lorenzen e55cf30705 Add material-icons fill class to play icon in LazyBookCard.vue 2024-07-08 16:43:50 +00:00
Greg Lorenzen 2c65b8fd2b Replace material-icons class with material-symbols class in components 2024-07-08 09:39:00 -07:00
Greg Lorenzen 20b8e35132 Revert material-icons CSS classes
Add new material-symbols classes for Material Symbols fonts
2024-07-08 09:39:00 -07:00
Greg Lorenzen 8007225a41 Update font names and file paths for Material Symbols 2024-07-08 09:39:00 -07:00
Greg Lorenzen 63a6da6680 Add Material Symbols outlined and rounded woff files 2024-07-08 09:39:00 -07:00
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
mikiher 31c8cb476a Make menu item localized where they're not 2024-07-06 00:58:25 +03: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
mikiher 79bd6a25d9 Move Collapse Series option to the tollbar's context menu 2024-07-05 00:45:18 +03:00
mikiher 0042604e6d Merge branch 'advplyr:master' into show-subtitles 2024-07-05 00:17:01 +03: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
mikiher 54f2bb1092 Add a Show Subtitles option 2024-07-04 20:35:58 +03:00
mikiher 6b6df619f5 Remove tailwind.compiled.css (auto-generated, added by mistake) 2024-07-04 20:30:29 +03: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
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
335 changed files with 15818 additions and 9260 deletions
-3
View File
@@ -10,6 +10,3 @@ RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \ DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends \
curl tzdata ffmpeg && \ curl tzdata ffmpeg && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Move tone executable to appropriate directory
COPY --from=sandreas/tone:v0.1.5 /usr/local/bin/tone /usr/local/bin/
+1 -1
View File
@@ -6,5 +6,5 @@ module.exports.config = {
MetadataPath: Path.resolve('metadata'), MetadataPath: Path.resolve('metadata'),
FFmpegPath: '/usr/bin/ffmpeg', FFmpegPath: '/usr/bin/ffmpeg',
FFProbePath: '/usr/bin/ffprobe', FFProbePath: '/usr/bin/ffprobe',
SkipBinariesCheck: true SkipBinariesCheck: false
} }
-3
View File
@@ -3,6 +3,3 @@ contact_links:
- name: Discord - name: Discord
url: https://discord.gg/HQgCbd6E75 url: https://discord.gg/HQgCbd6E75
about: Ask questions, get help troubleshooting, and join the Abs community here. about: Ask questions, get help troubleshooting, and join the Abs community here.
- name: Matrix
url: https://matrix.to/#/#audiobookshelf:matrix.org
about: Ask questions, get help troubleshooting, and join the Abs community here.
@@ -0,0 +1,20 @@
name: Close fixed issues on release.
on:
release:
types: [published]
permissions:
contents: read
issues: write
jobs:
comment:
runs-on: ubuntu-latest
steps:
- name: Close issues marked as fixed upon a release.
uses: gcampbell-msft/fixed-pending-release@7fa1b75a0c04bcd4b375110522878e5f6100cff5
with:
label: 'awaiting release'
removeLabel: true
applyToAll: true
message: Fixed in [${releaseTag}](${releaseUrl}).
+3 -3
View File
@@ -20,11 +20,11 @@ jobs:
- name: Set up node - name: Set up node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "20" node-version: '20'
# The only argument is the `directory`, which is where the i18n files are # The only argument is the `directory`, which is where the i18n files are
# stored. # stored.
- name: Run Update JSON Files action - name: Run Update JSON Files action
uses: audiobookshelf/audiobookshelf-i18n-updater@v1.2.0 uses: audiobookshelf/audiobookshelf-i18n-updater@v1.3.0
with: 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: pull_request:
push: push:
branches-ignore: branches-ignore:
- 'dependabot/**' # Don't run dependabot branches, as they are already covered by pull requests - 'dependabot/**' # Don't run dependabot branches, as they are already covered by pull requests
jobs: jobs:
build: build:
@@ -18,8 +18,8 @@ jobs:
with: with:
node-version: 20 node-version: 20
- name: install pkg - name: install pkg (using yao-pkg fork for targetting node20)
run: npm install -g pkg run: npm install -g @yao-pkg/pkg
- name: get client dependencies - name: get client dependencies
working-directory: client working-directory: client
@@ -33,7 +33,7 @@ jobs:
run: npm ci --only=production run: npm ci --only=production
- name: build binary - name: build binary
run: pkg -t node18-linux-x64 -o audiobookshelf . run: pkg -t node20-linux-x64 -o audiobookshelf .
- name: run audiobookshelf - name: run audiobookshelf
run: | run: |
+30
View File
@@ -0,0 +1,30 @@
name: API linting
# Run on pull requests or pushes when there is a change to the OpenAPI file
on:
push:
paths:
- docs/
pull_request:
paths:
- docs/
jobs:
build:
runs-on: ubuntu-latest
steps:
# Check out the repository
- name: Checkout
uses: actions/checkout@v4
# Set up node to run the javascript
- name: Set up node
uses: actions/setup-node@v4
# Install Redocly CLI
- name: Install Redocly CLI
run: npm install -g @redocly/cli@latest
# Perform linting for exploded spec
- name: Run linting for exploded spec
run: redocly lint docs/root.yaml --format=github-actions
# Perform linting for bundled spec
- name: Run linting for bundled spec
run: redocly lint docs/openapi.json --format=github-actions
+1
View File
@@ -15,6 +15,7 @@
/.nyc_output/ /.nyc_output/
/ffmpeg* /ffmpeg*
/ffprobe* /ffprobe*
/unicode*
sw.* sw.*
.DS_STORE .DS_STORE
+1 -2
View File
@@ -6,7 +6,6 @@ RUN npm ci && npm cache clean --force
RUN npm run generate RUN npm run generate
### STAGE 1: Build server ### ### STAGE 1: Build server ###
FROM sandreas/tone:v0.1.5 AS tone
FROM node:20-alpine FROM node:20-alpine
ENV NODE_ENV=production ENV NODE_ENV=production
@@ -17,11 +16,11 @@ RUN apk update && \
tzdata \ tzdata \
ffmpeg \ ffmpeg \
make \ make \
gcompat \
python3 \ python3 \
g++ \ g++ \
tini tini
COPY --from=tone /usr/local/bin/tone /usr/local/bin/
COPY --from=build /client/dist /client/dist COPY --from=build /client/dist /client/dist
COPY index.js package* / COPY index.js package* /
COPY server server COPY server server
-38
View File
@@ -2,7 +2,6 @@
set -e set -e
set -o pipefail set -o pipefail
FFMPEG_INSTALL_DIR="/usr/lib/audiobookshelf-ffmpeg"
DEFAULT_DATA_DIR="/usr/share/audiobookshelf" DEFAULT_DATA_DIR="/usr/share/audiobookshelf"
CONFIG_PATH="/etc/default/audiobookshelf" CONFIG_PATH="/etc/default/audiobookshelf"
DEFAULT_PORT=13378 DEFAULT_PORT=13378
@@ -46,43 +45,11 @@ add_group() {
fi fi
} }
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"
mkdir "$FFMPEG_INSTALL_DIR"
chown -R 'audiobookshelf:audiobookshelf' "$FFMPEG_INSTALL_DIR"
cd "$FFMPEG_INSTALL_DIR"
fi
$WGET
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"
}
setup_config() { setup_config() {
if [ -f "$CONFIG_PATH" ]; then if [ -f "$CONFIG_PATH" ]; then
echo "Existing config found." echo "Existing config found."
cat $CONFIG_PATH cat $CONFIG_PATH
# TONE_PATH variable added in 2.1.6, if it doesnt exist then add it
if ! grep -q "TONE_PATH" "$CONFIG_PATH"; then
echo "Adding TONE_PATH to existing config"
echo "TONE_PATH=$FFMPEG_INSTALL_DIR/tone" >> "$CONFIG_PATH"
fi
else else
if [ ! -d "$DEFAULT_DATA_DIR" ]; then if [ ! -d "$DEFAULT_DATA_DIR" ]; then
@@ -96,9 +63,6 @@ setup_config() {
config_text="METADATA_PATH=$DEFAULT_DATA_DIR/metadata config_text="METADATA_PATH=$DEFAULT_DATA_DIR/metadata
CONFIG_PATH=$DEFAULT_DATA_DIR/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 PORT=$DEFAULT_PORT
HOST=$DEFAULT_HOST" HOST=$DEFAULT_HOST"
@@ -115,5 +79,3 @@ add_group 'audiobookshelf' ''
add_user 'audiobookshelf' '' 'audiobookshelf' 'audiobookshelf user-daemon' '/bin/false' add_user 'audiobookshelf' '' 'audiobookshelf' 'audiobookshelf user-daemon' '/bin/false'
setup_config setup_config
install_ffmpeg
+1 -1
View File
@@ -48,7 +48,7 @@ Description: $DESCRIPTION"
echo "$controlfile" > dist/debian/DEBIAN/control; echo "$controlfile" > dist/debian/DEBIAN/control;
# Package debian # 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 fakeroot dpkg-deb -Zxz --build dist/debian
+15 -12
View File
@@ -1,19 +1,19 @@
@font-face { @font-face {
font-family: 'Material Icons'; font-family: 'Material Symbols Rounded';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url(~static/fonts/MaterialIcons.woff2) format('woff2'); src: url(~static/fonts/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].woff2) format('woff2');
} }
@font-face { @font-face {
font-family: 'Material Icons Outlined'; font-family: 'Material Symbols Outlined';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url(~static/fonts/MaterialIconsOutlined.woff2) format('woff2'); src: url(~static/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2) format('woff2');
} }
.material-icons { .material-symbols {
font-family: 'Material Icons'; font-family: 'Material Symbols Rounded';
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
line-height: 1; line-height: 1;
@@ -27,12 +27,13 @@
vertical-align: top; vertical-align: top;
} }
.material-icons:not([class*="text-"]) { .material-symbols.fill {
font-size: 1.5rem; font-variation-settings:
'FILL' 1
} }
.material-icons-outlined { .material-symbols-outlined {
font-family: 'Material Icons Outlined'; font-family: 'Material Symbols Outlined';
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
line-height: 1; line-height: 1;
@@ -43,10 +44,12 @@
word-wrap: normal; word-wrap: normal;
direction: ltr; direction: ltr;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
vertical-align: top;
} }
.material-icons-outlined:not([class*="text-"]) { .material-symbols-outlined.fill {
font-size: 1.5rem; font-variation-settings:
'FILL' 1
} }
/* cyrillic-ext */ /* cyrillic-ext */
+9 -9
View File
@@ -16,7 +16,7 @@
<div class="flex-grow" /> <div class="flex-grow" />
<ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center"> <ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
<span class="material-icons-outlined text-2xl text-warning text-opacity-50"> cast </span> <span class="material-symbols-outlined text-2xl text-warning text-opacity-50"> cast </span>
</ui-tooltip> </ui-tooltip>
<div v-if="isChromecastInitialized" class="w-6 min-w-6 h-6 ml-2 mr-1 sm:mx-2 cursor-pointer"> <div v-if="isChromecastInitialized" class="w-6 min-w-6 h-6 ml-2 mr-1 sm:mx-2 cursor-pointer">
<google-cast-launcher></google-cast-launcher> <google-cast-launcher></google-cast-launcher>
@@ -26,19 +26,19 @@
<nuxt-link v-if="currentLibrary" to="/config/stats" class="hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1"> <nuxt-link v-if="currentLibrary" to="/config/stats" class="hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
<ui-tooltip :text="$strings.HeaderYourStats" direction="bottom" class="flex items-center"> <ui-tooltip :text="$strings.HeaderYourStats" direction="bottom" class="flex items-center">
<span class="material-icons text-2xl" aria-label="User Stats" role="button">equalizer</span> <span class="material-symbols text-2xl" aria-label="User Stats" role="button">equalizer</span>
</ui-tooltip> </ui-tooltip>
</nuxt-link> </nuxt-link>
<nuxt-link v-if="userCanUpload && currentLibrary" to="/upload" class="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1"> <nuxt-link v-if="userCanUpload && currentLibrary" to="/upload" class="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
<ui-tooltip :text="$strings.ButtonUpload" direction="bottom" class="flex items-center"> <ui-tooltip :text="$strings.ButtonUpload" direction="bottom" class="flex items-center">
<span class="material-icons text-2xl" aria-label="Upload Media" role="button">upload</span> <span class="material-symbols text-2xl" aria-label="Upload Media" role="button">upload</span>
</ui-tooltip> </ui-tooltip>
</nuxt-link> </nuxt-link>
<nuxt-link v-if="userIsAdminOrUp" to="/config" class="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1"> <nuxt-link v-if="userIsAdminOrUp" to="/config" class="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
<ui-tooltip :text="$strings.HeaderSettings" direction="bottom" class="flex items-center"> <ui-tooltip :text="$strings.HeaderSettings" direction="bottom" class="flex items-center">
<span class="material-icons text-2xl" aria-label="System Settings" role="button">settings</span> <span class="material-symbols text-2xl" aria-label="System Settings" role="button">settings</span>
</ui-tooltip> </ui-tooltip>
</nuxt-link> </nuxt-link>
@@ -47,7 +47,7 @@
<span class="block truncate">{{ username }}</span> <span class="block truncate">{{ username }}</span>
</span> </span>
<span class="h-full md:ml-3 md:absolute inset-y-0 md:right-0 flex items-center justify-center md:pr-2 pointer-events-none"> <span class="h-full md:ml-3 md:absolute inset-y-0 md:right-0 flex items-center justify-center md:pr-2 pointer-events-none">
<span class="material-icons text-xl text-gray-100">person</span> <span class="material-symbols text-xl text-gray-100">person</span>
</span> </span>
</nuxt-link> </nuxt-link>
</div> </div>
@@ -55,7 +55,7 @@
<h1 class="text-lg md:text-2xl px-4">{{ $getString('MessageItemsSelected', [numMediaItemsSelected]) }}</h1> <h1 class="text-lg md:text-2xl px-4">{{ $getString('MessageItemsSelected', [numMediaItemsSelected]) }}</h1>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="!isPodcastLibrary && selectedMediaItemsArePlayable" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playSelectedItems"> <ui-btn v-if="!isPodcastLibrary && selectedMediaItemsArePlayable" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playSelectedItems">
<span class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span> <span class="material-symbols fill text-2xl -ml-2 pr-1 text-white">play_arrow</span>
{{ $strings.ButtonPlay }} {{ $strings.ButtonPlay }}
</ui-btn> </ui-btn>
<ui-tooltip v-if="isBookLibrary" :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom"> <ui-tooltip v-if="isBookLibrary" :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
@@ -76,7 +76,7 @@
<ui-context-menu-dropdown v-if="contextMenuItems.length && !processingBatch" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" /> <ui-context-menu-dropdown v-if="contextMenuItems.length && !processingBatch" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" />
<ui-tooltip :text="$strings.LabelDeselectAll" direction="bottom" class="flex items-center"> <ui-tooltip :text="$strings.LabelDeselectAll" direction="bottom" class="flex items-center">
<span class="material-icons text-3xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatch ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span> <span class="material-symbols text-3xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatch ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
</ui-tooltip> </ui-tooltip>
</div> </div>
</div> </div>
@@ -170,13 +170,13 @@ export default {
if (!this.isPodcastLibrary && this.selectedMediaItemsArePlayable) { if (!this.isPodcastLibrary && this.selectedMediaItemsArePlayable) {
options.push({ options.push({
text: 'Quick Embed Metadata', text: this.$strings.ButtonQuickEmbedMetadata,
action: 'quick-embed' action: 'quick-embed'
}) })
} }
options.push({ options.push({
text: 'Re-Scan', text: this.$strings.ButtonReScan,
action: 'rescan' action: 'rescan'
}) })
+57 -22
View File
@@ -1,5 +1,5 @@
<template> <template>
<div id="bookshelf" ref="wrapper" class="w-full max-w-full h-full overflow-y-scroll relative"> <div id="bookshelf" ref="wrapper" class="w-full max-w-full h-full overflow-y-scroll relative" :style="{ fontSize: sizeMultiplier + 'rem' }">
<!-- Cover size widget --> <!-- Cover size widget -->
<widgets-cover-size-widget class="fixed right-4 z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" /> <widgets-cover-size-widget class="fixed right-4 z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" />
@@ -11,31 +11,19 @@
</div> </div>
</div> </div>
<div v-else-if="loaded && !shelves.length && search" class="w-full h-40 flex items-center justify-center"> <div v-else-if="loaded && !shelves.length && search" class="w-full h-40 flex items-center justify-center">
<p class="text-center text-xl py-4">No results for query</p> <p class="text-center text-xl py-4">{{ $strings.MessageBookshelfNoResultsForQuery }}</p>
</div> </div>
<!-- Alternate plain view --> <!-- Alternate plain view -->
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24"> <div v-else-if="isAlternativeBookshelfView" class="w-full mb-24e">
<template v-for="(shelf, index) in shelves"> <template v-for="(shelf, index) in supportedShelves">
<widgets-item-slider v-if="shelf.type === 'book' || shelf.type === 'podcast'" :shelf-id="shelf.id" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)"> <widgets-item-slider :shelf-id="shelf.id" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" :type="shelf.type" class="bookshelf-row pl-8e my-6e" @selectEntity="(payload) => selectEntity(payload, index)">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p> <p class="font-semibold text-gray-100">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-item-slider> </widgets-item-slider>
<widgets-episode-slider v-else-if="shelf.type === 'episode'" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-episode-slider>
<widgets-series-slider v-else-if="shelf.type === 'series'" :key="index + '.'" :items="shelf.entities" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-series-slider>
<widgets-authors-slider v-else-if="shelf.type === 'authors'" :key="index + '.'" :items="shelf.entities" :height="192 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-authors-slider>
<widgets-narrators-slider v-else-if="shelf.type === 'narrators'" :key="index + '.'" :items="shelf.entities" :height="100 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-narrators-slider>
</template> </template>
</div> </div>
<!-- Regular bookshelf view --> <!-- Regular bookshelf view -->
<div v-else class="w-full"> <div v-else class="w-full">
<template v-for="(shelf, index) in shelves"> <template v-for="(shelf, index) in supportedShelves">
<app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" @selectEntity="(payload) => selectEntity(payload, index)" /> <app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" @selectEntity="(payload) => selectEntity(payload, index)" />
</template> </template>
</div> </div>
@@ -63,6 +51,9 @@ export default {
} }
}, },
computed: { computed: {
supportedShelves() {
return this.shelves.filter((shelf) => ['book', 'podcast', 'episode', 'series', 'authors', 'narrators'].includes(shelf.type))
},
userIsAdminOrUp() { userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp'] return this.$store.getters['user/getIsAdminOrUp']
}, },
@@ -90,8 +81,7 @@ export default {
return this.coverAspectRatio == 1 return this.coverAspectRatio == 1
}, },
sizeMultiplier() { sizeMultiplier() {
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120 return this.$store.getters['user/getSizeMultiplier']
return this.bookCoverWidth / baseSize
}, },
selectedMediaItems() { selectedMediaItems() {
return this.$store.state.globals.selectedMediaItems || [] return this.$store.state.globals.selectedMediaItems || []
@@ -177,8 +167,19 @@ export default {
this.loaded = true this.loaded = true
}, },
async fetchCategories() { async fetchCategories() {
// Sets the limit for the number of items to be displayed based on the viewport width.
const viewportWidth = window.innerWidth
let limit
if (viewportWidth >= 3240) {
limit = 15
} else if (viewportWidth >= 2880 && viewportWidth < 3240) {
limit = 12
}
const limitQuery = limit ? `&limit=${limit}` : ''
const categories = await this.$axios const categories = await this.$axios
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete`) .$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete,share${limitQuery}`)
.then((data) => { .then((data) => {
return data return data
}) })
@@ -418,6 +419,36 @@ export default {
} }
}) })
}, },
shareOpen(mediaItemShare) {
this.shelves.forEach((shelf) => {
if (shelf.type == 'book') {
shelf.entities = shelf.entities.map((ent) => {
if (ent.media.id === mediaItemShare.mediaItemId) {
return {
...ent,
mediaItemShare
}
}
return ent
})
}
})
},
shareClosed(mediaItemShare) {
this.shelves.forEach((shelf) => {
if (shelf.type == 'book') {
shelf.entities = shelf.entities.map((ent) => {
if (ent.media.id === mediaItemShare.mediaItemId) {
return {
...ent,
mediaItemShare: null
}
}
return ent
})
}
})
},
initListeners() { initListeners() {
if (this.$root.socket) { if (this.$root.socket) {
this.$root.socket.on('user_updated', this.userUpdated) this.$root.socket.on('user_updated', this.userUpdated)
@@ -429,6 +460,8 @@ export default {
this.$root.socket.on('items_updated', this.libraryItemsUpdated) this.$root.socket.on('items_updated', this.libraryItemsUpdated)
this.$root.socket.on('items_added', this.libraryItemsAdded) this.$root.socket.on('items_added', this.libraryItemsAdded)
this.$root.socket.on('episode_added', this.episodeAdded) this.$root.socket.on('episode_added', this.episodeAdded)
this.$root.socket.on('share_open', this.shareOpen)
this.$root.socket.on('share_closed', this.shareClosed)
} else { } else {
console.error('Error socket not initialized') console.error('Error socket not initialized')
} }
@@ -444,6 +477,8 @@ export default {
this.$root.socket.off('items_updated', this.libraryItemsUpdated) this.$root.socket.off('items_updated', this.libraryItemsUpdated)
this.$root.socket.off('items_added', this.libraryItemsAdded) this.$root.socket.off('items_added', this.libraryItemsAdded)
this.$root.socket.off('episode_added', this.episodeAdded) this.$root.socket.off('episode_added', this.episodeAdded)
this.$root.socket.off('share_open', this.shareOpen)
this.$root.socket.off('share_closed', this.shareClosed)
} else { } else {
console.error('Error socket not initialized') console.error('Error socket not initialized')
} }
+23 -44
View File
@@ -1,67 +1,53 @@
<template> <template>
<div class="relative"> <div class="relative">
<div ref="shelf" class="w-full max-w-full bookshelf-row categorizedBookshelfRow relative overflow-x-scroll overflow-y-hidden z-10" :style="{ paddingLeft: paddingLeft * sizeMultiplier + 'rem', height: shelfHeight + 'px' }" @scroll="scrolled"> <div ref="shelf" class="w-full max-w-full bookshelf-row categorizedBookshelfRow relative overflow-x-scroll no-scroll overflow-y-hidden z-10" :style="{ paddingLeft: paddingLeft + 'em' }" @scroll="scrolled">
<div class="w-full h-full pt-6"> <div class="w-full h-full pt-6e">
<div v-if="shelf.type === 'book' || shelf.type === 'podcast'" class="flex items-center"> <div v-if="shelf.type === 'book' || shelf.type === 'podcast'" class="flex items-center">
<template v-for="(entity, index) in shelf.entities"> <template v-for="(entity, index) in shelf.entities">
<cards-lazy-book-card :key="`${entity.id}-${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> </template>
</div> </div>
<div v-if="shelf.type === 'episode'" class="flex items-center"> <div v-if="shelf.type === 'episode'" class="flex items-center">
<template v-for="(entity, index) in shelf.entities"> <template v-for="(entity, index) in shelf.entities">
<cards-lazy-book-card <cards-lazy-book-card :key="entity.recentEpisode.id" :ref="`shelf-episode-${entity.recentEpisode.id}`" :index="index" :book-mount="entity" :continue-listening-shelf="continueListeningShelf" class="relative mx-2e" @hook:updated="updatedBookCard" @select="selectItem" @editPodcast="editItem" @edit="editEpisode" />
:key="entity.recentEpisode.id"
:ref="`shelf-episode-${entity.recentEpisode.id}`"
:index="index"
:width="bookCoverWidth"
:height="bookCoverHeight"
:book-cover-aspect-ratio="bookCoverAspectRatio"
:book-mount="entity"
:continue-listening-shelf="continueListeningShelf"
class="relative mx-2"
@hook:updated="updatedBookCard"
@select="selectItem"
@editPodcast="editItem"
@edit="editEpisode"
/>
</template> </template>
</div> </div>
<div v-if="shelf.type === 'series'" class="flex items-center"> <div v-if="shelf.type === 'series'" class="flex items-center">
<template v-for="entity in shelf.entities"> <template v-for="entity in shelf.entities">
<cards-lazy-series-card :key="entity.name" :series-mount="entity" :height="bookCoverHeight" :width="bookCoverWidth * 2" :book-cover-aspect-ratio="bookCoverAspectRatio" class="relative mx-2" @hook:updated="updatedBookCard" /> <cards-lazy-series-card :key="entity.name" :series-mount="entity" class="relative mx-2e" @hook:updated="updatedBookCard" />
</template> </template>
</div> </div>
<div v-if="shelf.type === 'tags'" class="flex items-center"> <div v-if="shelf.type === 'tags'" class="flex items-center">
<template v-for="entity in shelf.entities"> <template v-for="entity in shelf.entities">
<cards-group-card :key="entity.name" :group="entity" :height="bookCoverHeight" :width="bookCoverWidth * 2" :book-cover-aspect-ratio="bookCoverAspectRatio" class="relative mx-2" @hook:updated="updatedBookCard" /> <cards-group-card :key="entity.name" :group="entity" class="relative mx-2e" @hook:updated="updatedBookCard" />
</template> </template>
</div> </div>
<div v-if="shelf.type === 'authors'" class="flex items-center"> <div v-if="shelf.type === 'authors'" class="flex items-center">
<template v-for="entity in shelf.entities"> <template v-for="entity in shelf.entities">
<cards-author-card :key="entity.id" :width="bookCoverWidth / 1.25" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" @edit="editAuthor" /> <cards-author-card :key="entity.id" :author="entity" @hook:updated="updatedBookCard" class="mx-2e" @edit="editAuthor" />
</template> </template>
</div> </div>
<div v-if="shelf.type === 'narrators'" class="flex items-center"> <div v-if="shelf.type === 'narrators'" class="flex items-center">
<template v-for="entity in shelf.entities"> <template v-for="entity in shelf.entities">
<cards-narrator-card :key="entity.name" :width="150" :height="100" :narrator="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" /> <cards-narrator-card :key="entity.name" :narrator="entity" @hook:updated="updatedBookCard" class="mx-2e" />
</template> </template>
</div> </div>
</div> </div>
</div> </div>
<div class="relative">
<div class="absolute text-center categoryPlacard transform z-30 bottom-px left-4 md:left-8 w-44 rounded-md" style="height: 22px"> <div class="relative text-center categoryPlacard transform z-30 top-0 left-4e md:left-8e w-44e rounded-md">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border"> <div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em 0.5em` }">
<p class="transform text-sm">{{ $strings[shelf.labelStringKey] }}</p> <p :style="{ fontSize: 0.9 + 'em' }">{{ $strings[shelf.labelStringKey] }}</p>
</div>
</div> </div>
</div>
<div class="bookshelfDividerCategorized h-6 w-full absolute bottom-0 left-0 right-0 z-20"></div> <div class="bookshelfDividerCategorized h-6e w-full absolute top-0 left-0 right-0 z-20"></div>
<div v-show="canScrollLeft && !isScrolling" class="hidden sm:flex absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-30" @click="scrollLeft">
<span class="material-icons text-6xl text-white">chevron_left</span>
</div> </div>
<div v-show="canScrollRight && !isScrolling" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-30" @click="scrollRight"> <div v-show="canScrollLeft && !isScrolling" class="hidden sm:flex absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollLeft">
<span class="material-icons text-6xl text-white">chevron_right</span> <span class="material-symbols text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_left</span>
</div>
<div v-show="canScrollRight && !isScrolling" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollRight">
<span class="material-symbols text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_right</span>
</div> </div>
</div> </div>
</template> </template>
@@ -74,9 +60,6 @@ export default {
type: Object, type: Object,
default: () => {} default: () => {}
}, },
sizeMultiplier: Number,
bookCoverWidth: Number,
bookCoverAspectRatio: Number,
continueListeningShelf: Boolean continueListeningShelf: Boolean
}, },
data() { data() {
@@ -89,12 +72,8 @@ export default {
} }
}, },
computed: { computed: {
bookCoverHeight() { sizeMultiplier() {
return this.bookCoverWidth * this.bookCoverAspectRatio return this.$store.getters['user/getSizeMultiplier']
},
shelfHeight() {
if (this.shelf.type === 'narrators') return 148
return this.bookCoverHeight + 48
}, },
paddingLeft() { paddingLeft() {
if (window.innerWidth < 768) return 1 if (window.innerWidth < 768) return 1
@@ -218,12 +197,12 @@ export default {
} }
.book-shelf-arrow-right { .book-shelf-arrow-right {
height: calc(100% - 24px); height: calc(100% - 1.5em);
background: rgb(48, 48, 48); background: rgb(48, 48, 48);
background: linear-gradient(90deg, rgba(48, 48, 48, 0) 0%, rgba(25, 25, 25, 0.25) 8%, rgba(17, 17, 17, 0.4) 28%, rgba(17, 17, 17, 0.6) 71%, rgba(10, 10, 10, 0.6) 86%, rgba(0, 0, 0, 0.7) 100%); background: linear-gradient(90deg, rgba(48, 48, 48, 0) 0%, rgba(25, 25, 25, 0.25) 8%, rgba(17, 17, 17, 0.4) 28%, rgba(17, 17, 17, 0.6) 71%, rgba(10, 10, 10, 0.6) 86%, rgba(0, 0, 0, 0.7) 100%);
} }
.book-shelf-arrow-left { .book-shelf-arrow-left {
height: calc(100% - 24px); height: calc(100% - 1.5em);
background: rgb(48, 48, 48); background: rgb(48, 48, 48);
background: linear-gradient(-90deg, rgba(48, 48, 48, 0) 0%, rgba(25, 25, 25, 0.25) 8%, rgba(17, 17, 17, 0.4) 28%, rgba(17, 17, 17, 0.6) 71%, rgba(10, 10, 10, 0.6) 86%, rgba(0, 0, 0, 0.7) 100%); background: linear-gradient(-90deg, rgba(48, 48, 48, 0) 0%, rgba(25, 25, 25, 0.25) 8%, rgba(17, 17, 17, 0.4) 28%, rgba(17, 17, 17, 0.6) 71%, rgba(10, 10, 10, 0.6) 86%, rgba(0, 0, 0, 0.7) 100%);
} }
+87 -10
View File
@@ -24,11 +24,11 @@
</nuxt-link> </nuxt-link>
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="flex-grow h-full flex justify-center items-center" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> <nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="flex-grow h-full flex justify-center items-center" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p v-if="isPlaylistsPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonPlaylists }}</p> <p v-if="isPlaylistsPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonPlaylists }}</p>
<span v-else class="material-icons-outlined text-lg">queue_music</span> <span v-else class="material-symbols-outlined text-lg">queue_music</span>
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p> <p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
<span v-else class="material-icons-outlined text-lg">collections_bookmark</span> <span v-else class="material-symbols-outlined text-lg">collections_bookmark</span>
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p> <p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
@@ -53,7 +53,6 @@
<span class="font-mono">{{ numShowing }}</span> <span class="font-mono">{{ numShowing }}</span>
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-checkbox v-if="!isBatchSelecting" v-model="settings.collapseBookSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseBookSeries" />
<!-- RSS feed --> <!-- RSS feed -->
<ui-tooltip v-if="seriesRssFeed" :text="$strings.LabelOpenRSSFeed" direction="top"> <ui-tooltip v-if="seriesRssFeed" :text="$strings.LabelOpenRSSFeed" direction="top">
@@ -68,9 +67,6 @@
<div class="flex-grow hidden sm:inline-block" /> <div class="flex-grow hidden sm:inline-block" />
<!-- collapse series checkbox -->
<ui-checkbox v-if="isLibraryPage && isBookLibrary && !isBatchSelecting" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
<!-- library filter select --> <!-- library filter select -->
<controls-library-filter-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" /> <controls-library-filter-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
@@ -93,14 +89,20 @@
<div class="flex-grow" /> <div class="flex-grow" />
<p>{{ $strings.MessageSearchResultsFor }} "{{ searchQuery }}"</p> <p>{{ $strings.MessageSearchResultsFor }} "{{ searchQuery }}"</p>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
</template> </template>
<!-- authors page --> <!-- authors page -->
<template v-else-if="page === 'authors'"> <template v-else-if="page === 'authors'">
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="userCanUpdate && authors && authors.length && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn> <ui-btn v-if="userCanUpdate && authors?.length && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
<!-- author sort select --> <!-- author sort select -->
<controls-sort-select v-if="authors && authors.length" v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" /> <controls-sort-select v-if="authors?.length" v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" />
</template>
<!-- home page -->
<template v-else-if="isHome">
<div class="flex-grow" />
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
</template> </template>
</div> </div>
</div> </div>
@@ -151,11 +153,13 @@ export default {
if (this.isSeriesRemovedFromContinueListening) { if (this.isSeriesRemovedFromContinueListening) {
items.push({ items.push({
text: 'Re-Add Series to Continue Listening', text: this.$strings.LabelReAddSeriesToContinueListening,
action: 're-add-to-continue-listening' action: 're-add-to-continue-listening'
}) })
} }
this.addSubtitlesMenuItem(items)
return items return items
}, },
seriesSortItems() { seriesSortItems() {
@@ -183,6 +187,10 @@ export default {
{ {
text: this.$strings.LabelTotalDuration, text: this.$strings.LabelTotalDuration,
value: 'totalDuration' value: 'totalDuration'
},
{
text: this.$strings.LabelRandomly,
value: 'random'
} }
] ]
}, },
@@ -318,11 +326,14 @@ export default {
if (this.isPodcastLibrary && this.isLibraryPage && this.userCanDownload) { if (this.isPodcastLibrary && this.isLibraryPage && this.userCanDownload) {
items.push({ items.push({
text: 'Export OPML', text: this.$strings.LabelExportOPML,
action: 'export-opml' action: 'export-opml'
}) })
} }
this.addSubtitlesMenuItem(items)
this.addCollapseSeriesMenuItem(items)
return items return items
}, },
showPlaylists() { showPlaylists() {
@@ -330,9 +341,70 @@ export default {
} }
}, },
methods: { methods: {
addSubtitlesMenuItem(items) {
if (this.isBookLibrary && (!this.page || this.page === 'search')) {
if (this.settings.showSubtitles) {
items.push({
text: this.$strings.LabelHideSubtitles,
action: 'hide-subtitles'
})
} else {
items.push({
text: this.$strings.LabelShowSubtitles,
action: 'show-subtitles'
})
}
}
},
addCollapseSeriesMenuItem(items) {
if (this.isLibraryPage && this.isBookLibrary && !this.isBatchSelecting) {
if (this.settings.collapseSeries) {
items.push({
text: this.$strings.LabelExpandSeries,
action: 'expand-series'
})
} else {
items.push({
text: this.$strings.LabelCollapseSeries,
action: 'collapse-series'
})
}
}
},
handleSubtitlesAction(action) {
if (action === 'show-subtitles') {
this.settings.showSubtitles = true
this.updateShowSubtitles()
return true
}
if (action === 'hide-subtitles') {
this.settings.showSubtitles = false
this.updateShowSubtitles()
return true
}
return false
},
handleCollapseSeriesAction(action) {
if (action === 'collapse-series') {
this.settings.collapseSeries = true
this.updateCollapseSeries()
return true
}
if (action === 'expand-series') {
this.settings.collapseSeries = false
this.updateCollapseSeries()
return true
}
return false
},
contextMenuAction({ action }) { contextMenuAction({ action }) {
if (action === 'export-opml') { if (action === 'export-opml') {
this.exportOPML() this.exportOPML()
return
} else if (this.handleSubtitlesAction(action)) {
return
} else if (this.handleCollapseSeriesAction(action)) {
return
} }
}, },
exportOPML() { exportOPML() {
@@ -353,6 +425,8 @@ export default {
return return
} }
this.markSeriesFinished() this.markSeriesFinished()
} else if (this.handleSubtitlesAction(action)) {
return
} }
}, },
showOpenSeriesRSSFeed() { showOpenSeriesRSSFeed() {
@@ -482,6 +556,9 @@ export default {
updateCollapseBookSeries() { updateCollapseBookSeries() {
this.saveSettings() this.saveSettings()
}, },
updateShowSubtitles() {
this.saveSettings()
},
updateAuthorSort() { updateAuthorSort() {
this.saveSettings() this.saveSettings()
}, },
+5 -11
View File
@@ -2,7 +2,7 @@
<div> <div>
<div class="w-44 fixed left-0 top-16 bg-bg bg-opacity-100 md:bg-opacity-70 shadow-lg border-r border-white border-opacity-5 py-3 transform transition-transform mb-12 overflow-y-auto" :class="wrapperClass + ' ' + (streamLibraryItem ? 'h-[calc(100%-270px)]' : 'h-[calc(100%-110px)]')" v-click-outside="clickOutside"> <div class="w-44 fixed left-0 top-16 bg-bg bg-opacity-100 md:bg-opacity-70 shadow-lg border-r border-white border-opacity-5 py-3 transform transition-transform mb-12 overflow-y-auto" :class="wrapperClass + ' ' + (streamLibraryItem ? 'h-[calc(100%-270px)]' : 'h-[calc(100%-110px)]')" v-click-outside="clickOutside">
<div v-show="isMobilePortrait" class="flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer"> <div v-show="isMobilePortrait" class="flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer">
<span class="material-icons text-2xl">arrow_back</span> <span class="material-symbols text-2xl">arrow_back</span>
</div> </div>
<nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-3 h-12 border-b border-primary border-opacity-30 flex items-center cursor-pointer relative" :class="routeName === route.id ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'"> <nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-3 h-12 border-b border-primary border-opacity-30 flex items-center cursor-pointer relative" :class="routeName === route.id ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
@@ -10,7 +10,7 @@
<div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> <div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> </nuxt-link>
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version" /> <modals-changelog-view-modal v-model="showChangelogModal" :versionData="versionData" />
</div> </div>
<div class="w-44 h-12 px-4 border-t bg-bg border-black border-opacity-20 fixed left-0 flex flex-col justify-center" :class="wrapperClass" :style="{ bottom: streamLibraryItem ? '160px' : '0px' }"> <div class="w-44 h-12 px-4 border-t bg-bg border-black border-opacity-20 fixed left-0 flex flex-col justify-center" :class="wrapperClass" :style="{ bottom: streamLibraryItem ? '160px' : '0px' }">
@@ -19,7 +19,7 @@
<p class="text-xs text-gray-300 italic">{{ Source }}</p> <p class="text-xs text-gray-300 italic">{{ Source }}</p>
</div> </div>
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ latestVersion }}</a> <a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ $config.version }}</a>
</div> </div>
</div> </div>
</template> </template>
@@ -114,9 +114,9 @@ export default {
if (this.currentLibraryId) { if (this.currentLibraryId) {
configRoutes.push({ configRoutes.push({
id: 'config-library-stats', id: 'library-stats',
title: this.$strings.HeaderLibraryStats, title: this.$strings.HeaderLibraryStats,
path: '/config/library-stats' path: `/library/${this.currentLibraryId}/stats`
}) })
configRoutes.push({ configRoutes.push({
id: 'config-stats', id: 'config-stats',
@@ -156,15 +156,9 @@ export default {
hasUpdate() { hasUpdate() {
return !!this.versionData.hasUpdate return !!this.versionData.hasUpdate
}, },
latestVersion() {
return this.versionData.latestVersion
},
githubTagUrl() { githubTagUrl() {
return this.versionData.githubTagUrl return this.versionData.githubTagUrl
}, },
currentVersionChangelog() {
return this.versionData.currentVersionChangelog || 'No Changelog Available'
},
streamLibraryItem() { streamLibraryItem() {
return this.$store.state.streamLibraryItem return this.$store.state.streamLibraryItem
} }
+81 -41
View File
@@ -1,8 +1,8 @@
<template> <template>
<div id="bookshelf" class="w-full overflow-y-auto"> <div id="bookshelf" ref="bookshelf" class="w-full overflow-y-auto" :style="{ fontSize: sizeMultiplier + 'rem' }">
<template v-for="shelf in totalShelves"> <template v-for="shelf in totalShelves">
<div :key="shelf" :id="`shelf-${shelf - 1}`" class="w-full px-4 sm:px-8 relative" :class="{ bookshelfRow: !isAlternativeBookshelfView }" :style="{ height: shelfHeight + 'px' }"> <div :key="shelf" :id="`shelf-${shelf - 1}`" class="w-full px-4e sm:px-8e relative" :class="{ bookshelfRow: !isAlternativeBookshelfView }" :style="{ height: shelfHeight + 'px' }">
<div v-if="!isAlternativeBookshelfView" class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-20" :class="`h-${shelfDividerHeightIndex}`" /> <div v-if="!isAlternativeBookshelfView" class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-20 h-6e" />
</div> </div>
</template> </template>
@@ -49,10 +49,9 @@ export default {
entityIndexesMounted: [], entityIndexesMounted: [],
entityComponentRefs: {}, entityComponentRefs: {},
currentBookWidth: 0, currentBookWidth: 0,
pageLoadQueue: [],
isFetchingEntities: false, isFetchingEntities: false,
scrollTimeout: null, scrollTimeout: null,
booksPerFetch: 100, booksPerFetch: 0,
totalShelves: 0, totalShelves: 0,
bookshelfMarginLeft: 0, bookshelfMarginLeft: 0,
isSelectionMode: false, isSelectionMode: false,
@@ -63,7 +62,10 @@ export default {
resizeTimeout: null, resizeTimeout: null,
mountWindowWidth: 0, mountWindowWidth: 0,
lastItemIndexSelected: -1, lastItemIndexSelected: -1,
tempIsScanning: false tempIsScanning: false,
cardWidth: 0,
cardHeight: 0,
resizeObserver: null
} }
}, },
watch: { watch: {
@@ -160,52 +162,40 @@ export default {
return this.$store.getters['libraries/getCurrentLibraryName'] return this.$store.getters['libraries/getCurrentLibraryName']
}, },
bookWidth() { bookWidth() {
const coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize') return this.cardWidth
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return coverSize * 1.6
return coverSize
}, },
bookHeight() { bookHeight() {
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return this.bookWidth return this.cardHeight
return this.bookWidth * 1.6
}, },
shelfPadding() { shelfPadding() {
if (this.bookshelfWidth < 640) return 32 if (this.bookshelfWidth < 640) return 32 * this.sizeMultiplier
return 64 return 64 * this.sizeMultiplier
}, },
totalPadding() { totalPadding() {
return this.shelfPadding * 2 return this.shelfPadding * 2
}, },
entityWidth() { entityWidth() {
if (this.entityName === 'series' || this.entityName === 'collections') { return this.cardWidth
if (this.bookWidth * 2 > this.bookshelfWidth - this.shelfPadding) return this.bookWidth * 1.6
return this.bookWidth * 2
}
return this.bookWidth
}, },
entityHeight() { entityHeight() {
return this.bookHeight return this.cardHeight
}, },
shelfDividerHeightIndex() { shelfPaddingHeight() {
return 6 return 16
}, },
shelfHeight() { shelfHeight() {
if (this.isAlternativeBookshelfView) { const dividerHeight = this.isAlternativeBookshelfView ? 0 : 24 // h-6
const isItemEntity = this.entityName === 'series-books' || this.entityName === 'items' return this.cardHeight + (this.shelfPaddingHeight + dividerHeight) * this.sizeMultiplier
const extraTitleSpace = isItemEntity ? 80 : this.entityName === 'albums' ? 60 : 40
return this.entityHeight + extraTitleSpace * this.sizeMultiplier
}
return this.entityHeight + 40
}, },
totalEntityCardWidth() { totalEntityCardWidth() {
// Includes margin // Includes margin
return this.entityWidth + 24 return this.entityWidth + 24 * this.sizeMultiplier
}, },
selectedMediaItems() { selectedMediaItems() {
return this.$store.state.globals.selectedMediaItems || [] return this.$store.state.globals.selectedMediaItems || []
}, },
sizeMultiplier() { sizeMultiplier() {
const baseSize = this.isCoverSquareAspectRatio ? 192 : 120 return this.$store.getters['user/getSizeMultiplier']
return this.entityWidth / baseSize
}, },
streamLibraryItem() { streamLibraryItem() {
return this.$store.state.streamLibraryItem return this.$store.state.streamLibraryItem
@@ -322,7 +312,7 @@ export default {
let entityPath = this.entityName === 'series-books' ? 'items' : this.entityName let entityPath = this.entityName === 'series-books' ? 'items' : this.entityName
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : '' const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&include=rssfeed,numEpisodesIncomplete` const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&include=rssfeed,numEpisodesIncomplete,share`
const payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => { const payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
console.error('failed to fetch items', error) console.error('failed to fetch items', error)
@@ -436,10 +426,14 @@ export default {
rebuild() { rebuild() {
this.initSizeData() this.initSizeData()
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf) var lastBookIndex = Math.min(this.totalEntities, this.booksPerFetch)
this.entityIndexesMounted = [] this.entityIndexesMounted = []
for (let i = 0; i < lastBookIndex; i++) { for (let i = 0; i < lastBookIndex; i++) {
this.entityIndexesMounted.push(i) this.entityIndexesMounted.push(i)
if (!this.entities[i]) {
const page = Math.floor(i / this.booksPerFetch)
this.loadPage(page)
}
} }
var bookshelfEl = document.getElementById('bookshelf') var bookshelfEl = document.getElementById('bookshelf')
if (bookshelfEl) { if (bookshelfEl) {
@@ -501,7 +495,8 @@ export default {
this.resetEntities() this.resetEntities()
} }
}, },
settingsUpdated(settings) { async settingsUpdated(settings) {
await this.cardsHelpers.setCardSize()
const wasUpdated = this.checkUpdateSearchParams() const wasUpdated = this.checkUpdateSearchParams()
if (wasUpdated) { if (wasUpdated) {
this.resetEntities() this.resetEntities()
@@ -606,6 +601,44 @@ export default {
this.executeRebuild() this.executeRebuild()
} }
}, },
shareOpen(mediaItemShare) {
if (this.entityName === 'items' || this.entityName === 'series-books') {
var indexOf = this.entities.findIndex((ent) => ent?.media?.id === mediaItemShare.mediaItemId)
if (indexOf >= 0) {
if (this.entityComponentRefs[indexOf]) {
const libraryItem = { ...this.entityComponentRefs[indexOf].libraryItem }
libraryItem.mediaItemShare = mediaItemShare
this.entityComponentRefs[indexOf].setEntity?.(libraryItem)
}
}
}
},
shareClosed(mediaItemShare) {
if (this.entityName === 'items' || this.entityName === 'series-books') {
var indexOf = this.entities.findIndex((ent) => ent?.media?.id === mediaItemShare.mediaItemId)
if (indexOf >= 0) {
if (this.entityComponentRefs[indexOf]) {
const libraryItem = { ...this.entityComponentRefs[indexOf].libraryItem }
libraryItem.mediaItemShare = null
this.entityComponentRefs[indexOf].setEntity?.(libraryItem)
}
}
}
},
updatePagesLoaded() {
let numPages = Math.ceil(this.totalEntities / this.booksPerFetch)
for (let page = 0; page < numPages; page++) {
let numEntities = Math.min(this.totalEntities - page * this.booksPerFetch, this.booksPerFetch)
this.pagesLoaded[page] = true
for (let i = 0; i < numEntities; i++) {
const index = page * this.booksPerFetch + i
if (!this.entities[index]) {
this.pagesLoaded[page] = false
break
}
}
}
},
initSizeData(_bookshelf) { initSizeData(_bookshelf) {
var bookshelf = _bookshelf || document.getElementById('bookshelf') var bookshelf = _bookshelf || document.getElementById('bookshelf')
if (!bookshelf) { if (!bookshelf) {
@@ -622,6 +655,13 @@ export default {
this.entitiesPerShelf = Math.max(1, Math.floor((this.bookshelfWidth - this.shelfPadding) / this.totalEntityCardWidth)) this.entitiesPerShelf = Math.max(1, Math.floor((this.bookshelfWidth - this.shelfPadding) / this.totalEntityCardWidth))
this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2 this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2
this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2 this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2
const booksPerFetch = this.entitiesPerShelf * this.shelvesPerPage
if (booksPerFetch !== this.booksPerFetch) {
this.booksPerFetch = booksPerFetch
if (this.totalEntities) {
this.updatePagesLoaded()
}
}
this.currentBookWidth = this.bookWidth this.currentBookWidth = this.bookWidth
if (this.totalEntities) { if (this.totalEntities) {
@@ -630,13 +670,8 @@ export default {
return entitiesPerShelfBefore < this.entitiesPerShelf // Books per shelf has changed return entitiesPerShelfBefore < this.entitiesPerShelf // Books per shelf has changed
}, },
async init(bookshelf) { async init(bookshelf) {
if (this.entityName === 'series') {
this.booksPerFetch = 50
} else {
this.booksPerFetch = 100
}
this.checkUpdateSearchParams()
this.initSizeData(bookshelf) this.initSizeData(bookshelf)
this.checkUpdateSearchParams()
this.pagesLoaded[0] = true this.pagesLoaded[0] = true
await this.fetchEntites(0) await this.fetchEntites(0)
@@ -692,6 +727,8 @@ export default {
this.$root.socket.on('playlist_added', this.playlistAdded) this.$root.socket.on('playlist_added', this.playlistAdded)
this.$root.socket.on('playlist_updated', this.playlistUpdated) this.$root.socket.on('playlist_updated', this.playlistUpdated)
this.$root.socket.on('playlist_removed', this.playlistRemoved) this.$root.socket.on('playlist_removed', this.playlistRemoved)
this.$root.socket.on('share_open', this.shareOpen)
this.$root.socket.on('share_closed', this.shareClosed)
} else { } else {
console.error('Bookshelf - Socket not initialized') console.error('Bookshelf - Socket not initialized')
} }
@@ -719,6 +756,8 @@ export default {
this.$root.socket.off('playlist_added', this.playlistAdded) this.$root.socket.off('playlist_added', this.playlistAdded)
this.$root.socket.off('playlist_updated', this.playlistUpdated) this.$root.socket.off('playlist_updated', this.playlistUpdated)
this.$root.socket.off('playlist_removed', this.playlistRemoved) this.$root.socket.off('playlist_removed', this.playlistRemoved)
this.$root.socket.off('share_open', this.shareOpen)
this.$root.socket.off('share_closed', this.shareClosed)
} else { } else {
console.error('Bookshelf - Socket not initialized') console.error('Bookshelf - Socket not initialized')
} }
@@ -743,7 +782,8 @@ export default {
}) })
} }
}, },
mounted() { async mounted() {
await this.cardsHelpers.setCardSize()
this.initListeners() this.initListeners()
this.routeFullPath = window.location.pathname + (window.location.search || '') this.routeFullPath = window.location.pathname + (window.location.search || '')
@@ -778,6 +818,6 @@ export default {
.bookshelfDivider { .bookshelfDivider {
background: rgb(149, 119, 90); background: rgb(149, 119, 90);
background: var(--bookshelf-divider-bg); background: var(--bookshelf-divider-bg);
box-shadow: 2px 14px 8px #111111aa; box-shadow: 0.125em 0.875em 0.5em #111111aa;
} }
</style> </style>
+49 -22
View File
@@ -5,43 +5,43 @@
<covers-book-cover expand-on-click :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" /> <covers-book-cover expand-on-click :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
</div> </div>
<div class="flex items-start mb-6 lg:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'"> <div class="flex items-start mb-6 lg:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
<div class="min-w-0"> <div class="min-w-0 w-full">
<div class="flex items-center"> <div class="flex items-center">
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate"> <nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
{{ title }} {{ title }}
</nuxt-link> </nuxt-link>
<widgets-explicit-indicator v-if="isExplicit" /> <widgets-explicit-indicator v-if="isExplicit" />
</div> </div>
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center"> <div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5">
<span class="material-icons text-sm">person</span> <span class="material-symbols text-sm">person</span>
<div class="flex items-center"> <div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div> <div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div>
<div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div> <div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate">
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base"> <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
</div>
<div v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</div>
</div> </div>
<div v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</div>
</div> </div>
<div class="text-gray-400 flex items-center"> <div class="text-gray-400 flex items-center">
<span class="material-icons text-xs">schedule</span> <span class="material-symbols text-xs">schedule</span>
<p class="font-mono text-xs sm:text-sm pl-1 sm:pl-1.5 pb-px">{{ totalDurationPretty }}</p> <p class="font-mono text-xs sm:text-sm pl-1 sm:pl-1.5 pb-px">{{ totalDurationPretty }}</p>
</div> </div>
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-tooltip direction="top" :text="$strings.LabelClosePlayer"> <ui-tooltip direction="top" :text="$strings.LabelClosePlayer">
<button :aria-label="$strings.LabelClosePlayer" class="material-icons sm:px-2 py-1 lg:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</button> <button :aria-label="$strings.LabelClosePlayer" class="material-symbols sm:px-2 py-1 lg:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</button>
</ui-tooltip> </ui-tooltip>
</div> </div>
<player-ui <player-ui
ref="audioPlayer" ref="audioPlayer"
:chapters="chapters" :chapters="chapters"
:current-chapter="currentChapter"
:paused="!isPlaying" :paused="!isPlaying"
:loading="playerLoading" :loading="playerLoading"
:bookmarks="bookmarks" :bookmarks="bookmarks"
:sleep-timer-set="sleepTimerSet" :sleep-timer-set="sleepTimerSet"
:sleep-timer-remaining="sleepTimerRemaining" :sleep-timer-remaining="sleepTimerRemaining"
:sleep-timer-type="sleepTimerType"
:is-podcast="isPodcast" :is-podcast="isPodcast"
@playPause="playPause" @playPause="playPause"
@jumpForward="jumpForward" @jumpForward="jumpForward"
@@ -53,13 +53,16 @@
@showBookmarks="showBookmarks" @showBookmarks="showBookmarks"
@showSleepTimer="showSleepTimerModal = true" @showSleepTimer="showSleepTimerModal = true"
@showPlayerQueueItems="showPlayerQueueItemsModal = true" @showPlayerQueueItems="showPlayerQueueItemsModal = true"
@showPlayerSettings="showPlayerSettingsModal = true"
/> />
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :library-item-id="libraryItemId" @select="selectBookmark" /> <modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :library-item-id="libraryItemId" @select="selectBookmark" />
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-time="sleepTimerTime" :remaining="sleepTimerRemaining" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" /> <modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-type="sleepTimerType" :remaining="sleepTimerRemaining" :has-chapters="!!chapters.length" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" :library-item-id="libraryItemId" /> <modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" :library-item-id="libraryItemId" />
<modals-player-settings-modal v-model="showPlayerSettingsModal" />
</div> </div>
</template> </template>
@@ -78,9 +81,10 @@ export default {
currentTime: 0, currentTime: 0,
showSleepTimerModal: false, showSleepTimerModal: false,
showPlayerQueueItemsModal: false, showPlayerQueueItemsModal: false,
showPlayerSettingsModal: false,
sleepTimerSet: false, sleepTimerSet: false,
sleepTimerTime: 0,
sleepTimerRemaining: 0, sleepTimerRemaining: 0,
sleepTimerType: null,
sleepTimer: null, sleepTimer: null,
displayTitle: null, displayTitle: null,
currentPlaybackRate: 1, currentPlaybackRate: 1,
@@ -147,6 +151,9 @@ export default {
if (this.streamEpisode) return this.streamEpisode.chapters || [] if (this.streamEpisode) return this.streamEpisode.chapters || []
return this.media.chapters || [] return this.media.chapters || []
}, },
currentChapter() {
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
},
title() { title() {
if (this.playerHandler.displayTitle) return this.playerHandler.displayTitle if (this.playerHandler.displayTitle) return this.playerHandler.displayTitle
return this.mediaMetadata.title || 'No Title' return this.mediaMetadata.title || 'No Title'
@@ -206,14 +213,18 @@ export default {
this.$store.commit('setIsPlaying', isPlaying) this.$store.commit('setIsPlaying', isPlaying)
this.updateMediaSessionPlaybackState() this.updateMediaSessionPlaybackState()
}, },
setSleepTimer(seconds) { setSleepTimer(time) {
this.sleepTimerSet = true this.sleepTimerSet = true
this.sleepTimerTime = seconds
this.sleepTimerRemaining = seconds
this.runSleepTimer()
this.showSleepTimerModal = false this.showSleepTimerModal = false
this.sleepTimerType = time.timerType
if (this.sleepTimerType === this.$constants.SleepTimerTypes.COUNTDOWN) {
this.runSleepTimer(time)
}
}, },
runSleepTimer() { runSleepTimer(time) {
this.sleepTimerRemaining = time.seconds
var lastTick = Date.now() var lastTick = Date.now()
clearInterval(this.sleepTimer) clearInterval(this.sleepTimer)
this.sleepTimer = setInterval(() => { this.sleepTimer = setInterval(() => {
@@ -222,12 +233,23 @@ export default {
this.sleepTimerRemaining -= elapsed / 1000 this.sleepTimerRemaining -= elapsed / 1000
if (this.sleepTimerRemaining <= 0) { if (this.sleepTimerRemaining <= 0) {
this.clearSleepTimer() this.sleepTimerEnd()
this.playerHandler.pause()
this.$toast.info('Sleep Timer Done.. zZzzZz')
} }
}, 1000) }, 1000)
}, },
checkChapterEnd(time) {
if (!this.currentChapter) return
const chapterEndTime = this.currentChapter.end
const tolerance = 0.75
if (time >= chapterEndTime - tolerance) {
this.sleepTimerEnd()
}
},
sleepTimerEnd() {
this.clearSleepTimer()
this.playerHandler.pause()
this.$toast.info('Sleep Timer Done.. zZzzZz')
},
cancelSleepTimer() { cancelSleepTimer() {
this.showSleepTimerModal = false this.showSleepTimerModal = false
this.clearSleepTimer() this.clearSleepTimer()
@@ -237,6 +259,7 @@ export default {
this.sleepTimerRemaining = 0 this.sleepTimerRemaining = 0
this.sleepTimer = null this.sleepTimer = null
this.sleepTimerSet = false this.sleepTimerSet = false
this.sleepTimerType = null
}, },
incrementSleepTimer(amount) { incrementSleepTimer(amount) {
if (!this.sleepTimerSet) return if (!this.sleepTimerSet) return
@@ -277,6 +300,10 @@ export default {
if (this.$refs.audioPlayer) { if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.setCurrentTime(time) this.$refs.audioPlayer.setCurrentTime(time)
} }
if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER && this.sleepTimerSet) {
this.checkChapterEnd(time)
}
}, },
setDuration(duration) { setDuration(duration) {
this.totalDuration = duration this.totalDuration = duration
+20 -12
View File
@@ -15,7 +15,7 @@
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-icons text-2xl">format_list_bulleted</span> <span class="material-symbols text-2xl">format_list_bulleted</span>
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p> <p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p>
@@ -43,7 +43,7 @@
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-icons-outlined text-2xl">collections_bookmark</span> <span class="material-symbols-outlined text-2xl">collections_bookmark</span>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p> <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
@@ -51,7 +51,7 @@
</nuxt-link> </nuxt-link>
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-icons text-2.5xl">queue_music</span> <span class="material-symbols text-2.5xl">queue_music</span>
<p class="pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p> <p class="pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
@@ -72,13 +72,21 @@
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/narrators`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isNarratorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/narrators`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isNarratorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-icons text-2xl">record_voice_over</span> <span class="material-symbols text-2xl">record_voice_over</span>
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.LabelNarrators }}</p> <p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.LabelNarrators }}</p>
<div v-show="isNarratorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> <div v-show="isNarratorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isBookLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/stats`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isStatsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-symbols text-2xl">monitoring</span>
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonStats }}</p>
<div v-show="isStatsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="abs-icons icon-podcast text-xl"></span> <span class="abs-icons icon-podcast text-xl"></span>
@@ -88,7 +96,7 @@
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isMusicLibrary" :to="`/library/${currentLibraryId}/bookshelf/albums`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isMusicAlbumsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="isMusicLibrary" :to="`/library/${currentLibraryId}/bookshelf/albums`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isMusicAlbumsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-icons-outlined text-xl">album</span> <span class="material-symbols-outlined text-xl">album</span>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">Albums</p> <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">Albums</p>
@@ -96,15 +104,15 @@
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-icons text-2xl">file_download</span> <span class="material-symbols text-2xl">file_download</span>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonDownloadQueue }}</p> <p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonDownloadQueue }}</p>
<div v-show="isPodcastDownloadQueuePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> <div v-show="isPodcastDownloadQueuePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> </nuxt-link>
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'"> <nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : 'bg-error bg-opacity-20'">
<span class="material-icons text-2xl">warning</span> <span class="material-symbols text-2xl">warning</span>
<p class="pt-1.5 text-center leading-4" style="font-size: 1rem">{{ $strings.ButtonIssues }}</p> <p class="pt-1.5 text-center leading-4" style="font-size: 1rem">{{ $strings.ButtonIssues }}</p>
@@ -121,7 +129,7 @@
<p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p> <p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p>
</div> </div>
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version" /> <modals-changelog-view-modal v-model="showChangelogModal" :versionData="versionData" />
</div> </div>
</template> </template>
@@ -194,6 +202,9 @@ export default {
isPlaylistsPage() { isPlaylistsPage() {
return this.paramId === 'playlists' return this.paramId === 'playlists'
}, },
isStatsPage() {
return this.$route.name === 'library-library-stats'
},
libraryBookshelfPage() { libraryBookshelfPage() {
return this.$route.name === 'library-library-bookshelf-id' return this.$route.name === 'library-library-bookshelf-id'
}, },
@@ -219,9 +230,6 @@ export default {
githubTagUrl() { githubTagUrl() {
return this.versionData.githubTagUrl return this.versionData.githubTagUrl
}, },
currentVersionChangelog() {
return this.versionData.currentVersionChangelog || 'No Changelog Available'
},
streamLibraryItem() { streamLibraryItem() {
return this.$store.state.streamLibraryItem return this.$store.state.streamLibraryItem
}, },
+46 -33
View File
@@ -1,38 +1,40 @@
<template> <template>
<nuxt-link :to="`/author/${author?.id}`"> <div :style="{ minWidth: cardWidth + 'px', maxWidth: cardWidth + 'px' }">
<div cy-id="card" :style="{ width: width + 'px'}" @mouseover="mouseover" @mouseleave="mouseleave"> <nuxt-link :to="`/author/${author.id}`">
<div cy-id="imageArea" :style="{ height: height + 'px' }" class=" bg-primary box-shadow-book rounded-md relative overflow-hidden"> <div cy-id="card" @mouseover="mouseover" @mouseleave="mouseleave">
<!-- Image or placeholder --> <div cy-id="imageArea" :style="{ height: cardHeight + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
<covers-author-image :author="author"/> <!-- Image or placeholder -->
<covers-author-image :author="author" />
<!-- Author name & num books overlay --> <!-- Author name & num books overlay -->
<div cy-id="textInline" v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2"> <div cy-id="textInline" v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1e bg-black bg-opacity-60 px-2e">
<p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p> <p class="text-center font-semibold truncate" :style="{ fontSize: 0.75 + 'em' }">{{ name }}</p>
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} {{ $strings.LabelBooks }}</p> <p class="text-center text-gray-200" :style="{ fontSize: 0.65 + 'em' }">{{ numBooks }} {{ $strings.LabelBooks }}</p>
</div> </div>
<!-- Search icon btn --> <!-- Search icon btn -->
<div cy-id="match" v-show="!searching && isHovering && userCanUpdate" class="absolute top-0 left-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="searchAuthor"> <div cy-id="match" v-show="!searching && isHovering && userCanUpdate" class="absolute top-0 left-0 p-2e cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="searchAuthor">
<ui-tooltip :text="$strings.ButtonQuickMatch" direction="bottom"> <ui-tooltip :text="$strings.ButtonQuickMatch" direction="bottom">
<span class="material-icons text-lg">search</span> <span class="material-symbols" :style="{ fontSize: 1.125 + 'em' }">search</span>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div cy-id="edit" v-show="isHovering && !searching && userCanUpdate" class="absolute top-0 right-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="$emit('edit', author)"> <div cy-id="edit" v-show="isHovering && !searching && userCanUpdate" class="absolute top-0 right-0 p-2e cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="$emit('edit', author)">
<ui-tooltip :text="$strings.LabelEdit" direction="bottom"> <ui-tooltip :text="$strings.LabelEdit" direction="bottom">
<span class="material-icons text-lg">edit</span> <span class="material-symbols" :style="{ fontSize: 1.125 + 'em' }">edit</span>
</ui-tooltip> </ui-tooltip>
</div> </div>
<!-- Loading spinner --> <!-- 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"> <div cy-id="spinner" v-show="searching" class="absolute top-0 left-0 z-10 w-full h-full bg-black bg-opacity-50 flex items-center justify-center">
<widgets-loading-spinner size="" /> <widgets-loading-spinner size="" />
</div>
</div>
<div cy-id="nameBelow" v-show="nameBelow" class="w-full py-1e px-2e">
<p class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: 0.75 + 'em' }">{{ name }}</p>
</div> </div>
</div> </div>
<div cy-id="nameBelow" v-show="nameBelow" class="w-full py-1 px-2"> </nuxt-link>
<p class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p> </div>
</div>
</div>
</nuxt-link>
</template> </template>
<script> <script>
@@ -43,12 +45,14 @@ export default {
default: () => {} default: () => {}
}, },
width: Number, width: Number,
height: Number, height: {
sizeMultiplier: {
type: Number, type: Number,
default: 1 default: 192
}, },
nameBelow: Boolean nameBelow: {
type: Boolean,
default: false
}
}, },
data() { data() {
return { return {
@@ -57,6 +61,12 @@ export default {
} }
}, },
computed: { computed: {
cardWidth() {
return this.width || this.cardHeight * 0.8
},
cardHeight() {
return this.height * this.sizeMultiplier
},
userToken() { userToken() {
return this.$store.getters['user/getToken'] return this.$store.getters['user/getToken']
}, },
@@ -83,6 +93,9 @@ export default {
}, },
libraryProvider() { libraryProvider() {
return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google' return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
},
sizeMultiplier() {
return this.$store.getters['user/getSizeMultiplier']
} }
}, },
methods: { methods: {
+5 -1
View File
@@ -5,6 +5,7 @@
</div> </div>
<div class="flex-grow px-2 authorSearchCardContent h-full"> <div class="flex-grow px-2 authorSearchCardContent h-full">
<p class="truncate text-sm">{{ name }}</p> <p class="truncate text-sm">{{ name }}</p>
<p class="text-xs text-gray-400">{{ $getString('LabelXBooks', [numBooks]) }}</p>
</div> </div>
</div> </div>
</template> </template>
@@ -23,6 +24,9 @@ export default {
computed: { computed: {
name() { name() {
return this.author.name return this.author.name
},
numBooks() {
return this.author.numBooks
} }
}, },
methods: {}, methods: {},
@@ -33,7 +37,7 @@ export default {
<style> <style>
.authorSearchCardContent { .authorSearchCardContent {
width: calc(100% - 80px); width: calc(100% - 80px);
height: 40px; height: 44px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
-254
View File
@@ -1,254 +0,0 @@
<template>
<div ref="wrapper" class="relative pointer-events-none" :style="{ width: standardWidth * 0.8 * 1.1 * scale + 'px', height: standardHeight * 1.1 * scale + 'px', marginBottom: 20 + 'px', marginTop: 15 + 'px' }">
<div ref="card" class="wrap absolute origin-center transform duration-200" :style="{ transform: `scale(${scale * scaleMultiplier}) translateY(${hover2 ? '-40%' : '-50%'})` }">
<div class="perspective">
<div class="book-wrap transform duration-100 pointer-events-auto" :class="hover2 ? 'z-80' : 'rotate'" @mouseover="hover = true" @mouseout="hover = false">
<div class="book book-1 box-shadow-book3d" ref="front"></div>
<div class="title book-1 pointer-events-none" ref="left"></div>
<div class="bottom book-1 pointer-events-none" ref="bottom"></div>
<div class="book-back book-1 pointer-events-none">
<div class="text pointer-events-none">
<h3 class="mb-4">Book Back</h3>
<p>
<span>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Sunt earum doloremque aliquam culpa dolor nostrum consequatur quas dicta? Molestias repellendus minima pariatur libero vel, reiciendis optio magnam rerum, labore corporis.</span>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
src: String,
width: {
type: Number,
default: 200
}
},
data() {
return {
hover: false,
hover2: false,
standardWidth: 200,
standardHeight: 320,
isAttached: true,
pageX: 0,
pageY: 0
}
},
watch: {
src(newVal) {
this.setCover()
},
width(newVal) {
this.init()
},
hover(newVal) {
if (newVal) {
this.unattach()
} else {
this.attach()
}
setTimeout(() => {
this.hover2 = newVal
}, 100)
}
},
computed: {
scaleMultiplier() {
return this.hover2 ? 1.25 : 1
},
scale() {
var scale = this.width / this.standardWidth
return scale
}
},
methods: {
unattach() {
if (this.$refs.card && this.isAttached) {
var bookshelf = document.getElementById('bookshelf')
if (bookshelf) {
var pos = this.$refs.wrapper.getBoundingClientRect()
this.pageX = pos.x
this.pageY = pos.y
document.body.appendChild(this.$refs.card)
this.$refs.card.style.left = this.pageX + 'px'
this.$refs.card.style.top = this.pageY + 'px'
this.$refs.card.style.zIndex = 50
this.isAttached = false
} else if (bookshelf) {
console.log(this.pageX, this.pageY)
this.isAttached = false
}
}
},
attach() {
if (this.$refs.card && !this.isAttached) {
if (this.$refs.wrapper) {
this.isAttached = true
this.$refs.wrapper.appendChild(this.$refs.card)
this.$refs.card.style.left = '0px'
this.$refs.card.style.top = '0px'
}
} else {
console.log('Is attached already', this.isAttached)
}
},
init() {
var standardWidth = this.standardWidth
document.documentElement.style.setProperty('--book-w', standardWidth + 'px')
document.documentElement.style.setProperty('--book-wx', standardWidth + 1 + 'px')
document.documentElement.style.setProperty('--book-h', standardWidth * 1.6 + 'px')
document.documentElement.style.setProperty('--book-d', 40 + 'px')
},
setElBg(el) {
el.style.backgroundImage = `url("${this.src}")`
el.style.backgroundSize = 'cover'
el.style.backgroundPosition = 'center center'
el.style.backgroundRepeat = 'no-repeat'
},
setCover() {
if (this.$refs.front) {
this.setElBg(this.$refs.front)
}
if (this.$refs.bottom) {
this.setElBg(this.$refs.bottom)
this.$refs.bottom.style.backgroundSize = '2000%'
this.$refs.bottom.style.filter = 'blur(1px)'
}
if (this.$refs.left) {
this.setElBg(this.$refs.left)
this.$refs.left.style.backgroundSize = '2000%'
this.$refs.left.style.filter = 'blur(1px)'
}
}
},
mounted() {
this.setCover()
this.init()
}
}
</script>
<style>
/* :root {
--book-w: 200px;
--book-h: 320px;
--book-d: 30px;
--book-wx: 201px;
} */
/*
.wrap {
width: calc(1.1 * var(--book-w));
height: calc(1.1 * var(--book-h));
margin: 0 auto;
}
.perspective {
position: relative;
width: 100%;
height: 100%;
perspective: 600px;
transform-style: preserve-3d;
overflow: hidden;
}
.book-wrap {
height: 100%;
width: 100%;
transform-style: preserve-3d;
transition: 'all ease-out 0.6s';
}
.book {
width: var(--book-w);
height: var(--book-h);
background: url(https://covers.openlibrary.org/b/id/8303020-L.jpg) no-repeat center center;
background-size: cover;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
cursor: pointer;
}
.title {
content: '';
height: var(--book-h);
width: var(--book-d);
position: absolute;
right: 0;
left: calc(var(--book-wx) * -1);
top: 0;
bottom: 0;
margin: auto;
background: #444;
transform: rotateY(-80deg) translateX(-14px);
background: url(https://covers.openlibrary.org/b/id/8303020-L.jpg) no-repeat center center;
background-size: 5000%;
filter: blur(1px);
}
.bottom {
content: '';
height: var(--book-d);
width: var(--book-w);
position: absolute;
right: 0;
bottom: var(--book-h);
top: 0;
left: 0;
margin: auto;
background: #444;
transform: rotateY(0deg) rotateX(90deg) translateY(-15px) translateX(-2.5px) skewX(10deg);
background: url(https://covers.openlibrary.org/b/id/8303020-L.jpg) no-repeat center center;
background-size: 5000%;
filter: blur(1px);
}
.book-back {
width: var(--book-w);
height: var(--book-h);
background-color: #444;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
cursor: pointer;
transform: rotate(180deg) translateZ(-30px) translateX(5px);
}
.book-back .text {
transform: rotateX(180deg);
position: absolute;
bottom: 0px;
padding: 20px;
text-align: left;
font-size: 12px;
}
.book-back .text h3 {
color: #fff;
}
.book-back .text span {
display: block;
margin-bottom: 20px;
color: #fff;
}
.book-wrap.rotate {
transform: rotateY(30deg) rotateX(0deg);
}
.book-wrap.flip {
transform: rotateY(180deg);
} */
</style>
@@ -0,0 +1,36 @@
<template>
<div class="flex h-full px-1 overflow-hidden">
<div class="w-10 h-10 flex items-center justify-center">
<span class="material-symbols text-2xl text-gray-200">category</span>
</div>
<div class="flex-grow px-2 tagSearchCardContent h-full">
<p class="truncate text-sm">{{ genre }}</p>
<p class="text-xs text-gray-400">{{ $getString('LabelXItems', [numItems]) }}</p>
</div>
</div>
</template>
<script>
export default {
props: {
genre: String,
numItems: Number
},
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
<style>
.tagSearchCardContent {
width: calc(100% - 40px);
height: 44px;
display: flex;
flex-direction: column;
justify-content: center;
}
</style>
+17 -7
View File
@@ -1,15 +1,15 @@
<template> <template>
<div class="relative"> <div class="relative">
<div class="rounded-sm h-full relative" :style="{ width: width + 'px', height: height + 'px' }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard"> <div class="rounded-sm h-full relative" :style="{ width: cardWidth + 'px', height: cardHeight + 'px' }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard">
<nuxt-link :to="groupTo" class="cursor-pointer"> <nuxt-link :to="groupTo" class="cursor-pointer">
<div class="w-full h-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'"> <div class="w-full h-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'">
<covers-group-cover ref="groupcover" :id="groupEncode" :name="groupName" :type="groupType" :book-items="bookItems" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-group-cover ref="groupcover" :id="groupEncode" :name="groupName" :type="groupType" :book-items="bookItems" :width="cardWidth" :height="cardHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }"> <div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ groupName }}</p> <p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ groupName }}</p>
</div> </div>
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ bookItems.length }}</div> <div class="absolute z-10 top-1.5e right-1.5e rounded-md leading-3e p-1e font-semibold text-white flex items-center justify-center" :style="{ fontSize: 0.8 + 'em' }" style="background-color: #cd9d49dd">{{ bookItems.length }}</div>
</div> </div>
</nuxt-link> </nuxt-link>
</div> </div>
@@ -24,8 +24,10 @@ export default {
default: () => null default: () => null
}, },
width: Number, width: Number,
height: Number, height: {
bookCoverAspectRatio: Number type: Number,
default: 192
}
}, },
data() { data() {
return { return {
@@ -33,6 +35,15 @@ export default {
} }
}, },
computed: { computed: {
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
cardWidth() {
return this.width || this.cardHeight * 2
},
cardHeight() {
return this.height * this.sizeMultiplier
},
currentLibraryId() { currentLibraryId() {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
}, },
@@ -46,8 +57,7 @@ export default {
return `/library/${this.currentLibraryId}/bookshelf?filter=${this.filter}` return `/library/${this.currentLibraryId}/bookshelf?filter=${this.filter}`
}, },
sizeMultiplier() { sizeMultiplier() {
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2) return this.$store.getters['user/getSizeMultiplier']
return this.width / 240
}, },
bookItems() { bookItems() {
return this._group.books || [] return this._group.books || []
+4 -30
View File
@@ -2,15 +2,9 @@
<div class="flex items-center h-full px-1 overflow-hidden"> <div class="flex items-center h-full px-1 overflow-hidden">
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div class="flex-grow px-2 audiobookSearchCardContent"> <div class="flex-grow px-2 audiobookSearchCardContent">
<p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p> <p class="truncate text-sm">{{ title }}</p>
<p v-else class="truncate text-sm" v-html="matchHtml" /> <p v-if="subtitle" class="truncate text-xs text-gray-300">{{ subtitle }}</p>
<p class="text-xs text-gray-200 truncate">{{ $getString('LabelByAuthor', [authorName]) }}</p>
<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">{{ $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" />
</div> </div>
</div> </div>
</template> </template>
@@ -21,10 +15,7 @@ export default {
libraryItem: { libraryItem: {
type: Object, type: Object,
default: () => {} default: () => {}
}, }
search: String,
matchKey: String,
matchText: String
}, },
data() { data() {
return {} return {}
@@ -58,23 +49,6 @@ export default {
authorName() { authorName() {
if (this.isPodcast) return this.mediaMetadata.author || 'Unknown' if (this.isPodcast) return this.mediaMetadata.author || 'Unknown'
return this.mediaMetadata.authorName || 'Unknown' return this.mediaMetadata.authorName || 'Unknown'
},
matchHtml() {
if (!this.matchText || !this.search) return ''
// This used to highlight the part of the search found
// but with removing commas periods etc this is no longer plausible
const html = this.matchText
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') 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>`
if (this.matchKey === 'narrators') return `<p class="truncate">${this.$strings.LabelNarrator}: ${html}</p>`
return `${html}`
} }
}, },
methods: {}, methods: {},
@@ -1,7 +1,7 @@
<template> <template>
<div class="flex items-center px-1 overflow-hidden"> <div class="flex items-center px-1 overflow-hidden">
<div class="w-8 flex items-center justify-center"> <div class="w-8 flex items-center justify-center">
<span v-if="isFinished" :class="taskIconStatus" class="material-icons text-base">{{ actionIcon }}</span> <span v-if="isFinished" :class="taskIconStatus" class="material-symbols text-base">{{ actionIcon }}</span>
<widgets-loading-spinner v-else /> <widgets-loading-spinner v-else />
</div> </div>
<div class="flex-grow px-2 taskRunningCardContent"> <div class="flex-grow px-2 taskRunningCardContent">
+2 -2
View File
@@ -5,7 +5,7 @@
</div> </div>
<div v-if="!processing && !uploadFailed && !uploadSuccess" class="absolute -top-3 -right-3 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-error cursor-pointer" @click="$emit('remove')"> <div v-if="!processing && !uploadFailed && !uploadSuccess" class="absolute -top-3 -right-3 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-error cursor-pointer" @click="$emit('remove')">
<span class="text-base text-white text-opacity-80 font-mono material-icons">close</span> <span class="text-base text-white text-opacity-80 font-mono material-symbols">close</span>
</div> </div>
<template v-if="!uploadSuccess && !uploadFailed"> <template v-if="!uploadSuccess && !uploadFailed">
@@ -22,7 +22,7 @@
<ui-text-input-with-label v-model.trim="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" /> <ui-text-input-with-label v-model.trim="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
<ui-tooltip :text="$strings.LabelUploaderItemFetchMetadataHelp"> <ui-tooltip :text="$strings.LabelUploaderItemFetchMetadataHelp">
<div 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> <span class="text-base text-white text-opacity-80 font-mono material-symbols">sync</span>
</div> </div>
</ui-tooltip> </ui-tooltip>
</div> </div>
+45 -17
View File
@@ -1,18 +1,22 @@
<template> <template>
<div ref="card" :id="`album-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard"> <div ref="card" :id="`album-card-${index}`" :style="{ width: cardWidth + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" /> <div class="relative" :style="{ height: coverHeight + 'px' }">
<div class="w-full h-full bg-primary relative rounded overflow-hidden"> <div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<covers-preview-cover ref="cover" :src="coverSrc" :width="width" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <div class="w-full h-full bg-primary relative rounded overflow-hidden">
</div> <covers-preview-cover ref="cover" :src="coverSrc" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div> </div>
</div> </div>
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p> <div class="relative w-full">
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ artist || '&nbsp;' }}</p> <div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6e h-6e rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em ${0.5}em` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
</div>
</div>
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8e h-8e py-1e rounded-md text-center">
<p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ artist || '&nbsp;' }}</p>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -22,8 +26,10 @@ export default {
props: { props: {
index: Number, index: Number,
width: Number, width: Number,
height: Number, height: {
bookCoverAspectRatio: Number, type: Number,
default: 192
},
bookshelfView: { bookshelfView: {
type: Number, type: Number,
default: 0 default: 0
@@ -42,6 +48,29 @@ export default {
} }
}, },
computed: { computed: {
bookCoverAspectRatio() {
return this.store.getters['libraries/getBookCoverAspectRatio']
},
cardWidth() {
return this.width || this.coverHeight
},
coverHeight() {
return this.height * this.sizeMultiplier
},
/*
cardHeight() {
return this.coverHeight + this.bottomTextHeight
},
bottomTextHeight() {
if (!this.isAlternativeBookshelfView) return 0
const lineHeight = 1.5
const remSize = 16
const baseHeight = this.sizeMultiplier * lineHeight * remSize
const titleHeight = this.labelFontSize * baseHeight
const paddingHeight = 4 * 2 * this.sizeMultiplier // py-1
return titleHeight + paddingHeight
},
*/
coverSrc() { coverSrc() {
const config = this.$config || this.$nuxt.$config const config = this.$config || this.$nuxt.$config
if (!this.album || !this.album.libraryItemId) return `${config.routerBasePath}/book_placeholder.jpg` if (!this.album || !this.album.libraryItemId) return `${config.routerBasePath}/book_placeholder.jpg`
@@ -49,11 +78,10 @@ export default {
}, },
labelFontSize() { labelFontSize() {
if (this.width < 160) return 0.75 if (this.width < 160) return 0.75
return 0.875 return 0.9
}, },
sizeMultiplier() { sizeMultiplier() {
const baseSize = this.bookCoverAspectRatio === 1 ? 192 : 120 return this.store.getters['user/getSizeMultiplier']
return this.width / baseSize
}, },
title() { title() {
return this.album ? this.album.title : '' return this.album ? this.album.title : ''
+201 -134
View File
@@ -1,125 +1,142 @@
<template> <template>
<div ref="card" :id="`book-card-${index}`" :style="{ minWidth: width + 'px', maxWidth: width + 'px', height: height + 'px' }" class="absolute rounded-sm z-10 bg-primary cursor-pointer box-shadow-book" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard"> <div ref="card" :id="`book-card-${index}`" :style="{ minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }" class="absolute rounded-sm z-10 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<!-- When cover image does not fill --> <div :id="`cover-area-${index}`" class="relative w-full top-0 left-0 rounded overflow-hidden z-10 bg-primary box-shadow-book" :style="{ height: coverHeight + 'px ' }">
<div cy-id="coverBg" v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary"> <!-- When cover image does not fill -->
<div class="absolute cover-bg" ref="coverBg" /> <div cy-id="coverBg" v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
<div class="absolute cover-bg" ref="coverBg" />
</div>
<div cy-id="seriesSequenceList" v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #78350f">
<p :style="{ fontSize: 0.8 + 'em' }">#{{ seriesSequenceList }}</p>
</div>
<div cy-id="booksInSeries" v-else-if="booksInSeries" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #cd9d49dd">
<p :style="{ fontSize: 0.8 + 'em' }">{{ booksInSeries }}</p>
</div>
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
<div cy-id="titleImageNotReady" v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: 0.5 + 'em' }">
<p :style="{ fontSize: 0.8 + 'em' }" class="text-gray-300 text-center">{{ title }}</p>
</div>
<!-- Cover Image -->
<img cy-id="coverImage" v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="relative w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
<!-- Placeholder Cover Title & Author -->
<div cy-id="placeholderTitle" v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'em' }">
<div>
<p cy-id="placeholderTitleText" class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'em' }">{{ titleCleaned }}</p>
</div>
</div>
<div cy-id="placeholderAuthor" v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'em', bottom: authorBottom + 'em' }">
<p cy-id="placeholderAuthorText" class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'em' }">{{ authorCleaned }}</p>
</div>
<div v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }" style="background-color: #78350f">
<p :style="{ fontSize: 0.8 + 'em' }">#{{ seriesSequenceList }}</p>
</div>
<div v-else-if="booksInSeries" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }" style="background-color: #cd9d49dd">
<p :style="{ fontSize: 0.8 + 'em' }">{{ booksInSeries }}</p>
</div>
<!-- No progress shown for podcasts (unless showing podcast episode) -->
<div cy-id="progressBar" v-if="!isPodcast || episodeProgress" class="absolute bottom-0 left-0 h-1e shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * userProgressPercent + 'px' }"></div>
<!-- Overlay is not shown if collapsing series in library -->
<div cy-id="overlay" v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen) && !processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded md:block" :class="overlayWrapperClasslist">
<div cy-id="playButton" v-show="showPlayButton" class="h-full flex items-center justify-center pointer-events-none">
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="play">
<span class="material-symbols fill" :style="{ fontSize: playIconFontSize + 'em' }">play_arrow</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-symbols" :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-symbols" :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-symbols" :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-symbols" :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-symbols 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-symbols" :style="{ fontSize: 1.5 + 'em' }">rss_feed</span>
</div>
<!-- media item shared icon -->
<div cy-id="mediaItemShare" v-if="mediaItemShare && !isSelectionMode && !isHovering" class="absolute text-success left-0 z-10" :style="{ padding: 0.375 + 'em', top: rssFeed ? '2em' : '0px' }">
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">public</span>
</div>
<!-- Series sequence -->
<div cy-id="seriesSequence" v-if="seriesSequence && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }">
<p :style="{ fontSize: 0.8 + 'em' }">#{{ seriesSequence }}</p>
</div>
<!-- Podcast Episode # -->
<div cy-id="podcastEpisodeNumber" v-if="recentEpisodeNumber !== null && !isHovering && !isSelectionMode && !processing" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }">
<p :style="{ fontSize: 0.8 + 'em' }">
Episode<span v-if="recentEpisodeNumber"> #{{ recentEpisodeNumber }}</span>
</p>
</div>
<!-- Podcast Num Episodes -->
<div cy-id="numEpisodes" v-else-if="!numEpisodesIncomplete && numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', width: 1.25 + 'em', height: 1.25 + 'em' }">
<p :style="{ fontSize: 0.8 + 'em' }">{{ numEpisodes }}</p>
</div>
<!-- Podcast Num Episodes -->
<div cy-id="numEpisodesIncomplete" v-else-if="numEpisodesIncomplete && !isHovering && !isSelectionMode" class="absolute rounded-full bg-yellow-400 text-black font-semibold box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', width: 1.25 + 'em', height: 1.25 + 'em' }">
<p :style="{ fontSize: 0.8 + 'em' }">{{ numEpisodesIncomplete }}</p>
</div>
</div>
</div> </div>
<!-- Alternative bookshelf title/author/sort --> <!-- Alternative bookshelf title/author/sort -->
<div cy-id="detailBottom" v-if="isAlternativeBookshelfView || isAuthorBookshelfView" dir="auto" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }"> <div cy-id="detailBottom" :id="`description-area-${index}`" v-if="isAlternativeBookshelfView || isAuthorBookshelfView" dir="auto" class="relative mt-2e mb-2e left-0 z-50 w-full">
<div :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }"> <div :style="{ fontSize: 0.9 + 'em' }">
<ui-tooltip v-if="displayTitle" :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center"> <ui-tooltip v-if="displayTitle" :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
<p cy-id="title" ref="displayTitle" class="truncate">{{ displayTitle }}</p> <p cy-id="title" ref="displayTitle" class="truncate">{{ displayTitle }}</p>
<widgets-explicit-indicator cy-id="explicitIndicator" v-if="isExplicit" /> <widgets-explicit-indicator cy-id="explicitIndicator" v-if="isExplicit" />
</ui-tooltip> </ui-tooltip>
</div> </div>
<p cy-id="line2" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || '&nbsp;' }}</p> <ui-tooltip v-if="showSubtitles" :text="displaySubtitle" :disabled="!displaySubtitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
<p cy-id="line3" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p> <p cy-id="subtitle" class="truncate" ref="displaySubtitle" :style="{ fontSize: 0.6 + 'em' }">{{ displaySubtitle }}</p>
</div> </ui-tooltip>
<p cy-id="line2" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displayLineTwo || '&nbsp;' }}</p>
<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 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }" style="background-color: #78350f"> <p cy-id="line3" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displaySortLine }}</p>
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ 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 * 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 cy-id="titleImageNotReady" 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 cy-id="coverImage" 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 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 + 'rem' }">
<div>
<p cy-id="placeholderTitleText" class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ 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 + 'rem', bottom: authorBottom + 'rem' }">
<p cy-id="placeholderAuthorText" class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
</div>
</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-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * 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 + 'rem' }">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 + 'rem' }">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 * sizeMultiplier + 'rem' }" @click.stop.prevent="editClick">
<span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">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 * 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 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 * 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 cy-id="ebookFormat" 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 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: 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 cy-id="ErrorTooltip" 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 cy-id="rssFeed" 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 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 * 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 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 * 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 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 * 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 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 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodesIncomplete }}</p>
</div> </div>
</div> </div>
</template> </template>
@@ -131,15 +148,11 @@ import MoreMenu from '@/components/widgets/MoreMenu'
export default { export default {
props: { props: {
index: Number, index: Number,
width: { width: Number,
type: Number,
default: 120
},
height: { height: {
type: Number, type: Number,
default: 192 default: 192
}, },
bookCoverAspectRatio: Number,
bookshelfView: Number, bookshelfView: Number,
bookMount: { bookMount: {
// Book can be passed as prop or set with setEntity() // Book can be passed as prop or set with setEntity()
@@ -161,6 +174,7 @@ export default {
selected: false, selected: false,
isSelectionMode: false, isSelectionMode: false,
displayTitleTruncated: false, displayTitleTruncated: false,
displaySubtitleTruncated: false,
showCoverBg: false showCoverBg: false
} }
}, },
@@ -174,6 +188,39 @@ export default {
} }
}, },
computed: { computed: {
bookCoverAspectRatio() {
return this.store.getters['libraries/getBookCoverAspectRatio']
},
coverWidth() {
return this.width || this.coverHeight / this.bookCoverAspectRatio
},
coverHeight() {
return this.height * this.sizeMultiplier
},
cardWidth() {
// This method returns immediately without waiting for the DOM to update
return this.coverWidth
},
/*
cardHeight() {
// This method returns immediately without waiting for the DOM to update
return this.coverHeight + this.detailsHeight
},
detailsHeight() {
if (!this.isAlternativeBookshelfView) return 0
const lineHeight = 1.5
const remSize = 16
const baseHeight = this.sizeMultiplier * lineHeight * remSize
const titleHeight = 0.9 * baseHeight
const line2Height = 0.8 * baseHeight
const line3Height = this.displaySortLine ? 0.8 * baseHeight : 0
const marginHeight = 8 * 2 * this.sizeMultiplier // py-2
return titleHeight + line2Height + line3Height + marginHeight
},
*/
sizeMultiplier() {
return this.store.getters['user/getSizeMultiplier']
},
dateFormat() { dateFormat() {
return this.store.state.serverSettings.dateFormat return this.store.state.serverSettings.dateFormat
}, },
@@ -194,7 +241,7 @@ export default {
return this._libraryItem.mediaType return this._libraryItem.mediaType
}, },
isPodcast() { isPodcast() {
return this.mediaType === 'podcast' return this.mediaType === 'podcast' || this.store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast'
}, },
isMusic() { isMusic() {
return this.mediaType === 'music' return this.mediaType === 'music'
@@ -273,10 +320,6 @@ export default {
squareAspectRatio() { squareAspectRatio() {
return this.bookCoverAspectRatio === 1 return this.bookCoverAspectRatio === 1
}, },
sizeMultiplier() {
const baseSize = this.squareAspectRatio ? 192 : 120
return this.width / baseSize
},
title() { title() {
return this.mediaMetadata.title || '' return this.mediaMetadata.title || ''
}, },
@@ -298,7 +341,14 @@ export default {
if (this.recentEpisode) return this.recentEpisode.title if (this.recentEpisode) return this.recentEpisode.title
const ignorePrefix = this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix const ignorePrefix = this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix
if (this.collapsedSeries) return ignorePrefix ? this.collapsedSeries.nameIgnorePrefix : this.collapsedSeries.name if (this.collapsedSeries) return ignorePrefix ? this.collapsedSeries.nameIgnorePrefix : this.collapsedSeries.name
return ignorePrefix ? this.mediaMetadata.titleIgnorePrefix : this.title return ignorePrefix ? this.mediaMetadata.titleIgnorePrefix || '\u00A0' : this.title || '\u00A0'
},
displaySubtitle() {
if (!this.libraryItem) return '\u00A0'
if (this.collapsedSeries) return this.collapsedSeries.numBooks === 1 ? '1 book' : `${this.collapsedSeries.numBooks} books`
if (this.mediaMetadata.subtitle) return this.mediaMetadata.subtitle
if (this.mediaMetadata.seriesName) return this.mediaMetadata.seriesName
return ''
}, },
displayLineTwo() { displayLineTwo() {
if (this.recentEpisode) return this.title if (this.recentEpisode) return this.title
@@ -319,7 +369,10 @@ export default {
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false) if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size) if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes` if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
if (this.orderBy === 'media.metadata.publishedYear' && this.mediaMetadata.publishedYear) return 'Published ' + this.mediaMetadata.publishedYear if (this.orderBy === 'media.metadata.publishedYear') {
if (this.mediaMetadata.publishedYear) return 'Published ' + this.mediaMetadata.publishedYear
return '\u00A0'
}
return null return null
}, },
episodeProgress() { episodeProgress() {
@@ -486,6 +539,12 @@ export default {
func: 'openPlaylists', func: 'openPlaylists',
text: this.$strings.LabelAddToPlaylist text: this.$strings.LabelAddToPlaylist
}) })
if (this.userIsAdminOrUp) {
items.push({
func: 'openShare',
text: this.$strings.LabelShare
})
}
} }
if (this.ebookFormat && this.store.state.libraries.ereaderDevices?.length) { if (this.ebookFormat && this.store.state.libraries.ereaderDevices?.length) {
items.push({ items.push({
@@ -557,16 +616,16 @@ export default {
return this.$root.socket || this.$nuxt.$root.socket return this.$root.socket || this.$nuxt.$root.socket
}, },
titleFontSize() { titleFontSize() {
return 0.75 * this.sizeMultiplier return 0.75
}, },
authorFontSize() { authorFontSize() {
return 0.6 * this.sizeMultiplier return 0.6
}, },
placeholderCoverPadding() { placeholderCoverPadding() {
return 0.8 * this.sizeMultiplier return 0.8
}, },
authorBottom() { authorBottom() {
return 0.75 * this.sizeMultiplier return 0.75
}, },
titleCleaned() { titleCleaned() {
if (!this.title) return '' if (!this.title) return ''
@@ -590,14 +649,15 @@ export default {
const constants = this.$constants || this.$nuxt.$constants const constants = this.$constants || this.$nuxt.$constants
return this.bookshelfView === constants.BookshelfView.AUTHOR return this.bookshelfView === constants.BookshelfView.AUTHOR
}, },
titleDisplayBottomOffset() {
if (!this.isAlternativeBookshelfView && !this.isAuthorBookshelfView) return 0
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
return 4.25 * this.sizeMultiplier
},
rssFeed() { rssFeed() {
if (this.booksInSeries) return null if (this.booksInSeries) return null
return this._libraryItem.rssFeed || null return this._libraryItem.rssFeed || null
},
mediaItemShare() {
return this._libraryItem.mediaItemShare || null
},
showSubtitles() {
return !this.isPodcast && this.store.getters['user/getUserSetting']('showSubtitles')
} }
}, },
methods: { methods: {
@@ -639,6 +699,9 @@ export default {
if (this.$refs.displayTitle) { if (this.$refs.displayTitle) {
this.displayTitleTruncated = this.$refs.displayTitle.scrollWidth > this.$refs.displayTitle.clientWidth this.displayTitleTruncated = this.$refs.displayTitle.scrollWidth > this.$refs.displayTitle.clientWidth
} }
if (this.$refs.displaySubtitle) {
this.displaySubtitleTruncated = this.$refs.displaySubtitle.scrollWidth > this.$refs.displaySubtitle.clientWidth
}
}) })
}, },
clickCard(e) { clickCard(e) {
@@ -840,6 +903,10 @@ export default {
this.store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode: this.recentEpisode }]) this.store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode: this.recentEpisode }])
this.store.commit('globals/setShowPlaylistsModal', true) this.store.commit('globals/setShowPlaylistsModal', true)
}, },
openShare() {
this.store.commit('setSelectedLibraryItem', this.libraryItem)
this.store.commit('globals/setShareModal', this.mediaItemShare)
},
deleteLibraryItem() { deleteLibraryItem() {
const payload = { const payload = {
message: this.$strings.MessageConfirmDeleteLibraryItem, message: this.$strings.MessageConfirmDeleteLibraryItem,
+44 -20
View File
@@ -1,24 +1,26 @@
<template> <template>
<div ref="card" :id="`collection-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard"> <div ref="card" :id="`collection-card-${index}`" :style="{ width: cardWidth + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" /> <div class="relative" :style="{ height: coverHeight + 'px' }">
<div class="w-full h-full bg-primary relative rounded overflow-hidden"> <div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<covers-collection-cover ref="cover" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <div class="w-full h-full bg-primary relative rounded overflow-hidden">
</div> <covers-collection-cover ref="cover" :book-items="books" :width="cardWidth" :height="coverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
<div class="absolute pointer-events-auto" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickEdit">
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
</div> </div>
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
<div class="absolute pointer-events-auto" :style="{ top: 0.5 + 'em', right: 0.5 + 'em' }" @click.stop.prevent="clickEdit">
<span class="material-symbols 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-symbols text-success" :style="{ top: 0.5 + 'em', left: 0.5 + 'em', fontSize: 1.5 + 'em' }">rss_feed</span>
</div> </div>
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span> <div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6e h-6e rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em ${0.5}em` }">
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }"> <p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div> </div>
</div> </div>
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center"> <div v-else class="relative z-30 left-0 right-0 mx-auto h-8e py-1e rounded-md text-center">
<p class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ title }}</p> <p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
</div> </div>
</div> </div>
</template> </template>
@@ -28,8 +30,10 @@ export default {
props: { props: {
index: Number, index: Number,
width: Number, width: Number,
height: Number, height: {
bookCoverAspectRatio: Number, type: Number,
default: 192
},
bookshelfView: { bookshelfView: {
type: Number, type: Number,
default: 0 default: 0
@@ -49,13 +53,33 @@ export default {
} }
}, },
computed: { computed: {
bookCoverAspectRatio() {
return this.store.getters['libraries/getBookCoverAspectRatio']
},
cardWidth() {
return this.width || this.coverHeight * 2
},
coverHeight() {
return this.height * this.sizeMultiplier
},
cardHeight() {
return this.coverHeight + this.bottomTextHeight
},
bottomTextHeight() {
if (!this.isAlternativeBookshelfView) return 0 // bottom text appears on top of the divider
const lineHeight = 1.5
const remSize = 16
const baseHeight = this.sizeMultiplier * lineHeight * remSize
const titleHeight = this.labelFontSize * baseHeight
const paddingHeight = 4 * 2 * this.sizeMultiplier // py-1
return titleHeight + paddingHeight
},
labelFontSize() { labelFontSize() {
if (this.width < 160) return 0.75 if (this.width < 160) return 0.75
return 0.875 return 0.9
}, },
sizeMultiplier() { sizeMultiplier() {
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2) return this.store.getters['user/getSizeMultiplier']
return this.width / 240
}, },
title() { title() {
return this.collection ? this.collection.name : '' return this.collection ? this.collection.name : ''
+31 -18
View File
@@ -1,21 +1,24 @@
<template> <template>
<div ref="card" :id="`playlist-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard"> <div ref="card" :id="`playlist-card-${index}`" :style="{ width: cardWidth + 'px', fontSize: sizeMultiplier + 'rem' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" /> <div class="relative" :style="{ height: coverHeight + 'px' }">
<div class="w-full h-full bg-primary relative rounded overflow-hidden"> <div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<covers-playlist-cover ref="cover" :items="items" :width="width" :height="height" /> <div class="w-full h-full bg-primary relative rounded overflow-hidden">
</div> <covers-playlist-cover ref="cover" :items="items" :width="cardWidth" :height="coverHeight" />
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none"> </div>
<div class="absolute pointer-events-auto" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickEdit"> <div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span> <div class="absolute pointer-events-auto" :style="{ top: 0.5 + 'em', right: 0.5 + 'em' }" @click.stop.prevent="clickEdit">
<span class="material-symbols text-white text-opacity-75 hover:text-opacity-100" :style="{ fontSize: 1.25 + 'em' }">edit</span>
</div>
</div> </div>
</div> </div>
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }"> <div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 -bottom-6e left-0 right-0 mx-auto h-6e rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p> <div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em ${0.5}em` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
</div> </div>
</div> </div>
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center"> <div v-else class="relative z-30 left-0 right-0 mx-auto h-8e py-1e rounded-md text-center">
<p class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ title }}</p> <p class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ title }}</p>
</div> </div>
</div> </div>
</template> </template>
@@ -25,8 +28,10 @@ export default {
props: { props: {
index: Number, index: Number,
width: Number, width: Number,
height: Number, height: {
bookCoverAspectRatio: Number, type: Number,
default: 192
},
bookshelfView: { bookshelfView: {
type: Number, type: Number,
default: 0 default: 0
@@ -45,13 +50,21 @@ export default {
} }
}, },
computed: { computed: {
bookCoverAspectRatio() {
return this.store.getters['libraries/getBookCoverAspectRatio']
},
cardWidth() {
return this.width || this.coverHeight
},
coverHeight() {
return this.height * this.sizeMultiplier
},
labelFontSize() { labelFontSize() {
if (this.width < 160) return 0.75 if (this.width < 160) return 0.75
return 0.875 return 0.9
}, },
sizeMultiplier() { sizeMultiplier() {
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6) return this.store.getters['user/getSizeMultiplier']
return this.width / 120
}, },
title() { title() {
return this.playlist ? this.playlist.name : '' return this.playlist ? this.playlist.name : ''
+48 -35
View File
@@ -1,28 +1,32 @@
<template> <template>
<div cy-id="card" ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard"> <div cy-id="card" ref="card" :id="`series-card-${index}`" :style="{ width: cardWidth + 'px' }" class="absolute rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" /> <div cy-id="covers-area" class="relative" :style="{ height: coverHeight + 'px' }">
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0"> <div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="cardWidth" :height="coverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
<div cy-id="seriesLengthMarker" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `0.1em 0.25em` }" style="background-color: #cd9d49dd">
<p :style="{ fontSize: 0.8 + 'em' }">{{ books.length }}</p>
</div>
<div cy-id="seriesProgressBar" v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1e shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
<div cy-id="hoveringDisplayTitle" v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: '1em' }">
<p :style="{ fontSize: 1.2 + 'em' }">{{ displayTitle }}</p>
</div>
<span cy-id="rssFeedMarker" v-if="!isHovering && rssFeed" class="absolute z-10 material-symbols text-success" :style="{ top: 0.5 + 'em', left: 0.5 + 'em', fontSize: 1.5 + 'em' }">rss_feed</span>
</div> </div>
<div cy-id="seriesLengthMarker" class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ books.length }}</div> <div cy-id="standardBottomText" v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6e h-6e rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0em 0.5em` }">
<div cy-id="seriesProgressBar" v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" /> <p cy-id="standardBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize + 'em' }">{{ displayTitle }}</p>
<div 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: `${sizeMultiplier}rem` }">
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
</div>
<span cy-id="rssFeedMarker" 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 cy-id="standardBottomText" 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 cy-id="standardBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
</div> </div>
</div> </div>
<div cy-id="detailBottomText" v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center"> <div cy-id="detailBottomText" v-else class="relative z-30 left-0 right-0 mx-auto py-1e rounded-md text-center">
<p cy-id="detailBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ displayTitle }}</p> <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 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p> <p cy-id="detailBottomSortLine" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displaySortLine }}</p>
</div> </div>
</div> </div>
</template> </template>
@@ -32,13 +36,14 @@ export default {
props: { props: {
index: Number, index: Number,
width: Number, width: Number,
height: Number, height: {
bookCoverAspectRatio: Number, type: Number,
default: 192
},
bookshelfView: { bookshelfView: {
type: Number, type: Number,
default: 0 default: 0
}, },
isCategorized: Boolean,
seriesMount: { seriesMount: {
type: Object, type: Object,
default: () => null default: () => null
@@ -56,29 +61,37 @@ export default {
} }
}, },
computed: { computed: {
bookCoverAspectRatio() {
return this.store.getters['libraries/getBookCoverAspectRatio']
},
cardWidth() {
return this.width || this.coverHeight * 2
},
coverHeight() {
return this.height * this.sizeMultiplier
},
dateFormat() { dateFormat() {
return this.store.state.serverSettings.dateFormat return this.store.state.serverSettings.dateFormat
}, },
labelFontSize() { labelFontSize() {
if (this.width < 160) return 0.75 if (this.width < 160) return 0.75
return 0.875 return 0.9
}, },
sizeMultiplier() { sizeMultiplier() {
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2) return this.store.getters['user/getSizeMultiplier']
return this.width / 240
}, },
seriesId() { seriesId() {
return this.series ? this.series.id : '' return this.series?.id || ''
}, },
title() { title() {
return this.series ? this.series.name : '' return this.series?.name || ''
}, },
nameIgnorePrefix() { nameIgnorePrefix() {
return this.series ? this.series.nameIgnorePrefix : '' return this.series?.nameIgnorePrefix || ''
}, },
displayTitle() { displayTitle() {
if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title || '\u00A0'
return this.title return this.title || '\u00A0'
}, },
displaySortLine() { displaySortLine() {
switch (this.orderBy) { switch (this.orderBy) {
@@ -97,13 +110,13 @@ export default {
} }
}, },
books() { books() {
return this.series ? this.series.books || [] : [] return this.series?.books || []
}, },
addedAt() { addedAt() {
return this.series ? this.series.addedAt : 0 return this.series?.addedAt || 0
}, },
totalDuration() { totalDuration() {
return this.series ? this.series.totalDuration : 0 return this.series?.totalDuration || 0
}, },
seriesBookProgress() { seriesBookProgress() {
return this.books return this.books
@@ -148,7 +161,7 @@ export default {
return this.bookshelfView == constants.BookshelfView.DETAIL return this.bookshelfView == constants.BookshelfView.DETAIL
}, },
rssFeed() { rssFeed() {
return this.series ? this.series.rssFeed : null return this.series?.rssFeed
} }
}, },
methods: { methods: {
+23 -19
View File
@@ -1,17 +1,19 @@
<template> <template>
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(name)}`"> <div>
<div cy-id="card" :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden"> <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(name)}`">
<div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-40"> <div cy-id="card" :style="{ width: cardWidth + 'px', height: cardHeight + 'px', fontSize: sizeMultiplier + 'rem' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
<span class="material-icons-outlined text-[10rem]">record_voice_over</span> <div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-40">
</div> <span class="material-symbols-outlined text-[10em]">record_voice_over</span>
</div>
<!-- Narrator name & num books overlay --> <!-- Narrator name & num books overlay -->
<div class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2"> <div class="absolute bottom-0 left-0 w-full py-1e bg-black bg-opacity-60 px-2e">
<p cy-id="name" class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p> <p cy-id="name" class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: 0.75 + 'em' }">{{ name }}</p>
<p cy-id="numBooks" class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p> <p cy-id="numBooks" class="text-center text-gray-200" :style="{ fontSize: 0.65 + 'em' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
</div>
</div> </div>
</div> </nuxt-link>
</nuxt-link> </div>
</template> </template>
<script> <script>
@@ -21,23 +23,22 @@ export default {
type: Object, type: Object,
default: () => {} default: () => {}
}, },
width: { width: Number,
type: Number,
default: 150
},
height: { height: {
type: Number, type: Number,
default: 100 default: 100
},
sizeMultiplier: {
type: Number,
default: 1
} }
}, },
data() { data() {
return {} return {}
}, },
computed: { computed: {
cardWidth() {
return this.cardHeight * 1.5
},
cardHeight() {
return this.height * this.sizeMultiplier
},
name() { name() {
return this.narrator?.name || '' return this.narrator?.name || ''
}, },
@@ -49,6 +50,9 @@ export default {
}, },
currentLibraryId() { currentLibraryId() {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
},
sizeMultiplier() {
return this.$store.getters['user/getSizeMultiplier']
} }
}, },
methods: {} methods: {}
@@ -1,10 +1,11 @@
<template> <template>
<div class="flex h-full px-1 overflow-hidden"> <div class="flex h-full px-1 overflow-hidden">
<div class="w-10 h-10 flex items-center justify-center"> <div class="w-10 h-10 flex items-center justify-center">
<span class="material-icons text-2xl text-gray-200">record_voice_over</span> <span class="material-symbols text-2xl text-gray-200">record_voice_over</span>
</div> </div>
<div class="flex-grow px-2 narratorSearchCardContent h-full"> <div class="flex-grow px-2 narratorSearchCardContent h-full">
<p class="truncate text-sm">{{ narrator }}</p> <p class="truncate text-sm">{{ narrator }}</p>
<p class="text-xs text-gray-400">{{ $getString('LabelXBooks', [numBooks]) }}</p>
</div> </div>
</div> </div>
</template> </template>
@@ -12,7 +13,8 @@
<script> <script>
export default { export default {
props: { props: {
narrator: String narrator: String,
numBooks: Number
}, },
data() { data() {
return {} return {}
@@ -26,7 +28,7 @@ export default {
<style scoped> <style scoped>
.narratorSearchCardContent { .narratorSearchCardContent {
width: calc(100% - 40px); width: calc(100% - 40px);
height: 40px; height: 44px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
+5 -3
View File
@@ -1,10 +1,11 @@
<template> <template>
<div class="flex h-full px-1 overflow-hidden"> <div class="flex h-full px-1 overflow-hidden">
<div class="w-10 h-10 flex items-center justify-center"> <div class="w-10 h-10 flex items-center justify-center">
<span class="material-icons text-2xl text-gray-200">local_offer</span> <span class="material-symbols text-2xl text-gray-200">local_offer</span>
</div> </div>
<div class="flex-grow px-2 tagSearchCardContent h-full"> <div class="flex-grow px-2 tagSearchCardContent h-full">
<p class="truncate text-sm">{{ tag }}</p> <p class="truncate text-sm">{{ tag }}</p>
<p class="text-xs text-gray-400">{{ $getString('LabelXItems', [numItems]) }}</p>
</div> </div>
</div> </div>
</template> </template>
@@ -12,7 +13,8 @@
<script> <script>
export default { export default {
props: { props: {
tag: String tag: String,
numItems: Number
}, },
data() { data() {
return {} return {}
@@ -26,7 +28,7 @@ export default {
<style> <style>
.tagSearchCardContent { .tagSearchCardContent {
width: calc(100% - 40px); width: calc(100% - 40px);
height: 40px; height: 44px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
+2 -2
View File
@@ -10,7 +10,7 @@
</svg> </svg>
</span> </span>
<div v-else class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-300" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected"> <div v-else class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-300" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
<span class="material-icons" style="font-size: 1.1rem">close</span> <span class="material-symbols" style="font-size: 1.1rem">close</span>
</div> </div>
</button> </button>
@@ -24,7 +24,7 @@
<!-- selected checkmark icon --> <!-- selected checkmark icon -->
<div v-if="item.value === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none"> <div v-if="item.value === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
<span class="material-icons text-base text-yellow-400">check</span> <span class="material-symbols text-base text-yellow-400">check</span>
</div> </div>
</li> </li>
</template> </template>
+22 -10
View File
@@ -5,8 +5,8 @@
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" /> <ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
</form> </form>
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear"> <div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span> <span v-if="!search" class="material-symbols" style="font-size: 1.2rem">search</span>
<span v-else class="material-icons" style="font-size: 1.2rem">close</span> <span v-else class="material-symbols" style="font-size: 1.2rem">close</span>
</div> </div>
</div> </div>
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full max-w-64 sm:max-w-80 sm:w-80 bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu"> <div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full max-w-64 sm:max-w-80 sm:w-80 bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu">
@@ -25,7 +25,7 @@
<template v-for="item in bookResults"> <template v-for="item in bookResults">
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption"> <li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
<nuxt-link :to="`/item/${item.libraryItem.id}`"> <nuxt-link :to="`/item/${item.libraryItem.id}`">
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" /> <cards-item-search-card :library-item="item.libraryItem" />
</nuxt-link> </nuxt-link>
</li> </li>
</template> </template>
@@ -34,7 +34,7 @@
<template v-for="item in podcastResults"> <template v-for="item in podcastResults">
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption"> <li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
<nuxt-link :to="`/item/${item.libraryItem.id}`"> <nuxt-link :to="`/item/${item.libraryItem.id}`">
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" /> <cards-item-search-card :library-item="item.libraryItem" />
</nuxt-link> </nuxt-link>
</li> </li>
</template> </template>
@@ -59,9 +59,18 @@
<p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelTags }}</p> <p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelTags }}</p>
<template v-for="item in tagResults"> <template v-for="item in tagResults">
<li :key="item.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption"> <li :key="`tag.${item.name}`" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(item.name)}`"> <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(item.name)}`">
<cards-tag-search-card :tag="item.name" /> <cards-tag-search-card :tag="item.name" :num-items="item.numItems" />
</nuxt-link>
</li>
</template>
<p v-if="genreResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelGenres }}</p>
<template v-for="item in genreResults">
<li :key="`genre.${item.name}`" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=genres.${$encode(item.name)}`">
<cards-genre-search-card :genre="item.name" :num-items="item.numItems" />
</nuxt-link> </nuxt-link>
</li> </li>
</template> </template>
@@ -70,7 +79,7 @@
<template v-for="narrator in narratorResults"> <template v-for="narrator in narratorResults">
<li :key="narrator.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption"> <li :key="narrator.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(narrator.name)}`"> <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(narrator.name)}`">
<cards-narrator-search-card :narrator="narrator.name" /> <cards-narrator-search-card :narrator="narrator.name" :num-books="narrator.numBooks" />
</nuxt-link> </nuxt-link>
</li> </li>
</template> </template>
@@ -95,6 +104,7 @@ export default {
authorResults: [], authorResults: [],
seriesResults: [], seriesResults: [],
tagResults: [], tagResults: [],
genreResults: [],
narratorResults: [], narratorResults: [],
searchTimeout: null, searchTimeout: null,
lastSearch: null lastSearch: null
@@ -105,7 +115,7 @@ export default {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
}, },
totalResults() { totalResults() {
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length + this.podcastResults.length + this.narratorResults.length return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length + this.genreResults.length + this.podcastResults.length + this.narratorResults.length
} }
}, },
methods: { methods: {
@@ -116,7 +126,7 @@ export default {
if (!this.search) return if (!this.search) return
var search = this.search var search = this.search
this.clearResults() this.clearResults()
this.$router.push(`/library/${this.currentLibraryId}/search?q=${search}`) this.$router.push(`/library/${this.currentLibraryId}/search?q=${encodeURIComponent(search)}`)
}, },
clearResults() { clearResults() {
this.search = null this.search = null
@@ -126,6 +136,7 @@ export default {
this.authorResults = [] this.authorResults = []
this.seriesResults = [] this.seriesResults = []
this.tagResults = [] this.tagResults = []
this.genreResults = []
this.narratorResults = [] this.narratorResults = []
this.showMenu = false this.showMenu = false
this.isFetching = false this.isFetching = false
@@ -155,7 +166,7 @@ export default {
} }
this.isFetching = true this.isFetching = true
const searchResults = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/search?q=${value}&limit=3`).catch((error) => { const searchResults = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/search?q=${encodeURIComponent(value)}&limit=3`).catch((error) => {
console.error('Search error', error) console.error('Search error', error)
return [] return []
}) })
@@ -168,6 +179,7 @@ export default {
this.authorResults = searchResults.authors || [] this.authorResults = searchResults.authors || []
this.seriesResults = searchResults.series || [] this.seriesResults = searchResults.series || []
this.tagResults = searchResults.tags || [] this.tagResults = searchResults.tags || []
this.genreResults = searchResults.genres || []
this.narratorResults = searchResults.narrators || [] this.narratorResults = searchResults.narrators || []
this.isFetching = false this.isFetching = false
@@ -10,7 +10,7 @@
</svg> </svg>
</span> </span>
<div v-else class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-200" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected"> <div v-else class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-200" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
<span class="material-icons" style="font-size: 1.1rem">close</span> <span class="material-symbols" style="font-size: 1.1rem">close</span>
</div> </div>
</button> </button>
@@ -22,11 +22,11 @@
<span class="font-normal ml-3 block truncate text-sm">{{ item.text }}</span> <span class="font-normal ml-3 block truncate text-sm">{{ item.text }}</span>
</div> </div>
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center"> <div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
<span class="material-icons text-2xl">arrow_right</span> <span class="material-symbols text-2xl">arrow_right</span>
</div> </div>
<!-- selected checkmark icon --> <!-- selected checkmark icon -->
<div v-if="item.value === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none"> <div v-if="item.value === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
<span class="material-icons text-base text-yellow-400">check</span> <span class="material-symbols text-base text-yellow-400">check</span>
</div> </div>
</li> </li>
</template> </template>
@@ -34,15 +34,15 @@
<ul v-show="sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label"> <ul v-show="sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-white/5" role="option" @click="sublist = null"> <li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-white/5" role="option" @click="sublist = null">
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center"> <div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
<span class="material-icons text-2xl">arrow_left</span> <span class="material-symbols text-2xl">arrow_left</span>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="font-normal block truncate">Back</span> <span class="font-normal block truncate">{{ $strings.ButtonBack }}</span>
</div> </div>
</li> </li>
<li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="option"> <li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="option">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<span class="font-normal block truncate py-2">No {{ sublist }}</span> <span class="font-normal block truncate py-2">{{ $getString('LabelLibraryFilterSublistEmpty', [selectedSublistText]) }}</span>
</div> </div>
</li> </li>
<template v-for="item in sublistItems"> <template v-for="item in sublistItems">
@@ -52,7 +52,7 @@
</div> </div>
<!-- selected checkmark icon --> <!-- selected checkmark icon -->
<div v-if="`${sublist}.${item.value}` === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none"> <div v-if="`${sublist}.${item.value}` === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
<span class="material-icons text-base text-yellow-400">check</span> <span class="material-symbols text-base text-yellow-400">check</span>
</div> </div>
</li> </li>
</template> </template>
@@ -89,6 +89,9 @@ export default {
this.$emit('input', val) this.$emit('input', val)
} }
}, },
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
libraryMediaType() { libraryMediaType() {
return this.$store.getters['libraries/getCurrentLibraryMediaType'] return this.$store.getters['libraries/getCurrentLibraryMediaType']
}, },
@@ -106,31 +109,37 @@ export default {
}, },
{ {
text: this.$strings.LabelGenre, text: this.$strings.LabelGenre,
textPlural: this.$strings.LabelGenres,
value: 'genres', value: 'genres',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelTag, text: this.$strings.LabelTag,
textPlural: this.$strings.LabelTags,
value: 'tags', value: 'tags',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelAuthor, text: this.$strings.LabelAuthor,
textPlural: this.$strings.LabelAuthors,
value: 'authors', value: 'authors',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelNarrator, text: this.$strings.LabelNarrator,
textPlural: this.$strings.LabelNarrators,
value: 'narrators', value: 'narrators',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelPublisher, text: this.$strings.LabelPublisher,
textPlural: this.$strings.LabelPublishers,
value: 'publishers', value: 'publishers',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelLanguage, text: this.$strings.LabelLanguage,
textPlural: this.$strings.LabelLanguages,
value: 'languages', value: 'languages',
sublist: true sublist: true
}, },
@@ -142,43 +151,50 @@ export default {
] ]
}, },
bookItems() { bookItems() {
return [ const items = [
{ {
text: this.$strings.LabelAll, text: this.$strings.LabelAll,
value: 'all' value: 'all'
}, },
{ {
text: this.$strings.LabelGenre, text: this.$strings.LabelGenre,
textPlural: this.$strings.LabelGenres,
value: 'genres', value: 'genres',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelTag, text: this.$strings.LabelTag,
textPlural: this.$strings.LabelTags,
value: 'tags', value: 'tags',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelSeries, text: this.$strings.LabelSeries,
textPlural: this.$strings.LabelSeries,
value: 'series', value: 'series',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelAuthor, text: this.$strings.LabelAuthor,
textPlural: this.$strings.LabelAuthors,
value: 'authors', value: 'authors',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelNarrator, text: this.$strings.LabelNarrator,
textPlural: this.$strings.LabelNarrators,
value: 'narrators', value: 'narrators',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelPublisher, text: this.$strings.LabelPublisher,
textPlural: this.$strings.LabelPublishers,
value: 'publishers', value: 'publishers',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelLanguage, text: this.$strings.LabelLanguage,
textPlural: this.$strings.LabelLanguages,
value: 'languages', value: 'languages',
sublist: true sublist: true
}, },
@@ -218,6 +234,14 @@ export default {
sublist: false sublist: false
} }
] ]
if (this.userIsAdminOrUp) {
items.push({
text: this.$strings.LabelShareOpen,
value: 'share-open',
sublist: false
})
}
return items
}, },
podcastItems() { podcastItems() {
return [ return [
@@ -227,16 +251,19 @@ export default {
}, },
{ {
text: this.$strings.LabelGenre, text: this.$strings.LabelGenre,
textPlural: this.$strings.LabelGenres,
value: 'genres', value: 'genres',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelTag, text: this.$strings.LabelTag,
textPlural: this.$strings.LabelTags,
value: 'tags', value: 'tags',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelLanguage, text: this.$strings.LabelLanguage,
textPlural: this.$strings.LabelLanguages,
value: 'languages', value: 'languages',
sublist: true sublist: true
}, },
@@ -255,11 +282,13 @@ export default {
}, },
{ {
text: this.$strings.LabelGenre, text: this.$strings.LabelGenre,
textPlural: this.$strings.LabelGenres,
value: 'genres', value: 'genres',
sublist: true sublist: true
}, },
{ {
text: this.$strings.LabelTag, text: this.$strings.LabelTag,
textPlural: this.$strings.LabelTags,
value: 'tags', value: 'tags',
sublist: true sublist: true
}, },
@@ -279,6 +308,13 @@ export default {
selectedItemSublist() { selectedItemSublist() {
return this.selected?.includes('.') ? this.selected.split('.')[0] : null return this.selected?.includes('.') ? this.selected.split('.')[0] : null
}, },
selectedSublistText() {
if (!this.sublist) {
return ''
}
const sublistItem = this.selectItems.find((i) => i.value === this.sublist)
return sublistItem?.textPlural || sublistItem?.text || ''
},
selectedText() { selectedText() {
if (!this.selected) return '' if (!this.selected) return ''
const parts = this.selected.split('.') const parts = this.selected.split('.')
@@ -3,7 +3,7 @@
<button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu"> <button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
<span class="flex items-center justify-between"> <span class="flex items-center justify-between">
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span> <span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
<span class="material-icons text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span> <span class="material-symbols text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
</span> </span>
</button> </button>
@@ -14,7 +14,7 @@
<span class="font-normal ml-3 block truncate">{{ item.text }}</span> <span class="font-normal ml-3 block truncate">{{ item.text }}</span>
</div> </div>
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4"> <span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
<span class="material-icons text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span> <span class="material-symbols text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
</span> </span>
</li> </li>
</template> </template>
@@ -88,6 +88,10 @@ export default {
{ {
text: this.$strings.LabelFileModified, text: this.$strings.LabelFileModified,
value: 'mtimeMs' value: 'mtimeMs'
},
{
text: this.$strings.LabelRandomly,
value: 'random'
} }
] ]
}, },
@@ -128,6 +132,10 @@ export default {
{ {
text: this.$strings.LabelFileModified, text: this.$strings.LabelFileModified,
value: 'mtimeMs' value: 'mtimeMs'
},
{
text: this.$strings.LabelRandomly,
value: 'random'
} }
] ]
}, },
+2 -2
View File
@@ -3,7 +3,7 @@
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu"> <button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
<span class="flex items-center justify-between"> <span class="flex items-center justify-between">
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span> <span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
<span class="material-icons text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span> <span class="material-symbols text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
</span> </span>
</button> </button>
@@ -14,7 +14,7 @@
<span class="font-normal ml-3 block truncate">{{ item.text }}</span> <span class="font-normal ml-3 block truncate">{{ item.text }}</span>
</div> </div>
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4"> <span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
<span class="material-icons text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span> <span class="material-symbols text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
</span> </span>
</li> </li>
</template> </template>
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="relative" v-click-outside="clickOutside" @mouseover="mouseover" @mouseleave="mouseleave"> <div class="relative" v-click-outside="clickOutside" @mouseover="mouseover" @mouseleave="mouseleave">
<button :aria-label="$strings.LabelVolume" class="text-gray-300 hover:text-white" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon"> <button :aria-label="$strings.LabelVolume" class="text-gray-300 hover:text-white" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon">
<span class="material-icons text-2xl sm:text-3xl">{{ volumeIcon }}</span> <span class="material-symbols text-2xl sm:text-3xl">{{ volumeIcon }}</span>
</button> </button>
<transition name="menux"> <transition name="menux">
<div v-show="isOpen" class="volumeMenu h-6 absolute bottom-2 w-28 px-2 bg-bg shadow-sm rounded-lg" style="left: -116px"> <div v-show="isOpen" class="volumeMenu h-6 absolute bottom-2 w-28 px-2 bg-bg shadow-sm rounded-lg" style="left: -116px">
+1 -1
View File
@@ -7,7 +7,7 @@
<img ref="cover" :src="cover" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0" :class="showCoverBg ? 'object-contain' : 'object-fill'" /> <img ref="cover" :src="cover" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0" :class="showCoverBg ? 'object-contain' : 'object-fill'" />
<a v-if="!imageFailed && showOpenNewTab && isHovering" :href="cover" @click.stop target="_blank" class="absolute bg-primary flex items-center justify-center shadow-sm rounded-full hover:scale-110 transform duration-100" :style="{ top: sizeMultiplier * 0.5 + 'rem', right: sizeMultiplier * 0.5 + 'rem', width: 2.5 * sizeMultiplier + 'rem', height: 2.5 * sizeMultiplier + 'rem' }"> <a v-if="!imageFailed && showOpenNewTab && isHovering" :href="cover" @click.stop target="_blank" class="absolute bg-primary flex items-center justify-center shadow-sm rounded-full hover:scale-110 transform duration-100" :style="{ top: sizeMultiplier * 0.5 + 'rem', right: sizeMultiplier * 0.5 + 'rem', width: 2.5 * sizeMultiplier + 'rem', height: 2.5 * sizeMultiplier + 'rem' }">
<span class="material-icons" :style="{ fontSize: sizeMultiplier * 1.75 + 'rem' }">open_in_new</span> <span class="material-symbols" :style="{ fontSize: sizeMultiplier * 1.75 + 'rem' }">open_in_new</span>
</a> </a>
</div> </div>
@@ -91,7 +91,7 @@
<ui-textarea-with-label :value="prettyFfprobeData" readonly :rows="30" class="text-xs" /> <ui-textarea-with-label :value="prettyFfprobeData" readonly :rows="30" class="text-xs" />
<button class="absolute top-4 right-4" :class="copiedToClipboard ? 'text-success' : 'text-white/50 hover:text-white/80'" @click.stop="copyFfprobeData"> <button class="absolute top-4 right-4" :class="copiedToClipboard ? 'text-success' : 'text-white/50 hover:text-white/80'" @click.stop="copyFfprobeData">
<span class="material-icons">{{ copiedToClipboard ? 'check' : 'content_copy' }}</span> <span class="material-symbols">{{ copiedToClipboard ? 'check' : 'content_copy' }}</span>
</button> </button>
</div> </div>
</div> </div>
@@ -19,7 +19,7 @@
<ui-tooltip :text="$strings.LabelUpdateCoverHelp"> <ui-tooltip :text="$strings.LabelUpdateCoverHelp">
<p class="pl-4"> <p class="pl-4">
{{ $strings.LabelUpdateCover }} {{ $strings.LabelUpdateCover }}
<span class="material-icons icon-text">info_outlined</span> <span class="material-symbols icon-text">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
@@ -28,7 +28,7 @@
<ui-tooltip :text="$strings.LabelUpdateDetailsHelp"> <ui-tooltip :text="$strings.LabelUpdateDetailsHelp">
<p class="pl-4"> <p class="pl-4">
{{ $strings.LabelUpdateDetails }} {{ $strings.LabelUpdateDetails }}
<span class="material-icons icon-text">info_outlined</span> <span class="material-symbols icon-text">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
+1 -1
View File
@@ -24,7 +24,7 @@
<div class="flex-grow px-2"> <div class="flex-grow px-2">
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" /> <ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
</div> </div>
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-icons text-2xl -mt-px">add</span></ui-btn> <ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-symbols text-2xl -mt-px">add</span></ui-btn>
</div> </div>
</form> </form>
</div> </div>
+2 -2
View File
@@ -1,8 +1,8 @@
<template> <template>
<modals-modal v-model="show" name="chapters" :width="600" :height="'unset'"> <modals-modal v-model="show" name="chapters" :width="600" :height="'unset'">
<div id="chapter-modal-wrapper" ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh"> <div id="chapter-modal-wrapper" ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<template v-for="chap in chapters"> <template v-for="chap in chapters">
<div :key="chap.id" :id="`chapter-row-${chap.id}`" class="flex items-center px-6 py-3 justify-start cursor-pointer hover:bg-bg relative" :class="chap.id === currentChapterId ? 'bg-yellow-400 bg-opacity-10' : chap.end / _playbackRate <= currentChapterStart ? 'bg-success bg-opacity-5' : 'bg-opacity-20'" @click="clickChapter(chap)"> <div :key="chap.id" :id="`chapter-row-${chap.id}`" class="flex items-center px-6 py-3 justify-start cursor-pointer relative" :class="chap.id === currentChapterId ? 'bg-yellow-400/20 hover:bg-yellow-400/10' : chap.end / _playbackRate <= currentChapterStart ? 'bg-success/10 hover:bg-success/5' : 'hover:bg-primary/10'" @click="clickChapter(chap)">
<p class="chapter-title truncate text-sm md:text-base"> <p class="chapter-title truncate text-sm md:text-base">
{{ chap.title }} {{ chap.title }}
</p> </p>
@@ -1,7 +1,7 @@
<template> <template>
<div ref="wrapper" class="hidden absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 rounded-lg items-center justify-center" style="z-index: 61" @click="clickClose"> <div ref="wrapper" class="hidden absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 rounded-lg items-center justify-center" style="z-index: 61" @click="clickClose">
<div class="absolute top-3 right-3 md:top-5 md:right-5 h-8 w-8 md:h-12 md:w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300"> <div class="absolute top-3 right-3 md:top-5 md:right-5 h-8 w-8 md:h-12 md:w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300">
<span class="material-icons text-2xl md:text-4xl">close</span> <span class="material-symbols text-2xl md:text-4xl">close</span>
</div> </div>
<div ref="content" class="text-white"> <div ref="content" class="text-white">
<form v-if="selectedSeries" @submit.prevent="submitSeriesForm"> <form v-if="selectedSeries" @submit.prevent="submitSeriesForm">
@@ -80,8 +80,8 @@
</div> </div>
</div> </div>
<div class="w-full md:w-1/3"> <div class="w-full md:w-1/3">
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mb-2 mt-6 md:mt-0">{{ $strings.LabelUser }}</p> <p v-if="!isMediaItemShareSession" class="font-semibold uppercase text-xs text-gray-400 tracking-wide mb-2 mt-6 md:mt-0">{{ $strings.LabelUser }}</p>
<p class="mb-1 text-xs">{{ _session.userId }}</p> <p v-if="!isMediaItemShareSession" class="mb-1 text-xs">{{ _session.userId }}</p>
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">{{ $strings.LabelMediaPlayer }}</p> <p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">{{ $strings.LabelMediaPlayer }}</p>
<p class="mb-1">{{ playMethodName }}</p> <p class="mb-1">{{ playMethodName }}</p>
@@ -99,8 +99,8 @@
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<ui-btn v-if="!isOpenSession" small color="error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn> <ui-btn v-if="!isOpenSession && !isMediaItemShareSession" small color="error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn>
<ui-btn v-else small color="error" @click.stop="closeSessionClick">Close Open Session</ui-btn> <ui-btn v-else-if="!isMediaItemShareSession" small color="error" @click.stop="closeSessionClick">Close Open Session</ui-btn>
</div> </div>
</div> </div>
</modals-modal> </modals-modal>
@@ -166,6 +166,9 @@ export default {
}, },
isOpenSession() { isOpenSession() {
return !!this._session.open return !!this._session.open
},
isMediaItemShareSession() {
return this._session.mediaPlayer === 'web-share'
} }
}, },
methods: { methods: {
+1 -1
View File
@@ -3,7 +3,7 @@
<div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" /> <div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" />
<button class="absolute top-4 right-4 landscape:top-4 landscape:right-4 md:portrait:top-5 md:portrait:right-5 lg:top-5 lg:right-5 inline-flex text-gray-200 hover:text-white" aria-label="Close modal" @click="clickClose"> <button class="absolute top-4 right-4 landscape:top-4 landscape:right-4 md:portrait:top-5 md:portrait:right-5 lg:top-5 lg:right-5 inline-flex text-gray-200 hover:text-white" aria-label="Close modal" @click="clickClose">
<span class="material-icons text-2xl landscape:text-2xl md:portrait:text-4xl lg:text-4xl">close</span> <span class="material-symbols text-2xl landscape:text-2xl md:portrait:text-4xl lg:text-4xl">close</span>
</button> </button>
<slot name="outer" /> <slot name="outer" />
<div ref="content" style="min-width: 380px; min-height: 200px; max-width: 100vw" class="relative text-white" aria-modal="true" :style="{ height: modalHeight, width: modalWidth, marginTop: contentMarginTop + 'px' }" @mousedown="mousedownModal" @mouseup="mouseupModal" v-click-outside="clickBg"> <div ref="content" style="min-width: 380px; min-height: 200px; max-width: 100vw" class="relative text-white" aria-modal="true" :style="{ height: modalHeight, width: modalWidth, marginTop: contentMarginTop + 'px' }" @mousedown="mousedownModal" @mouseup="mouseupModal" v-click-outside="clickBg">
@@ -0,0 +1,70 @@
<template>
<modals-modal v-model="show" name="player-settings" :width="500" :height="'unset'">
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden p-4" style="max-height: 80vh; min-height: 40vh">
<h3 class="text-xl font-semibold mb-8">{{ $strings.HeaderPlayerSettings }}</h3>
<div class="flex items-center mb-4">
<ui-toggle-switch v-model="useChapterTrack" @input="setUseChapterTrack" />
<div class="pl-4">
<span>{{ $strings.LabelUseChapterTrack }}</span>
</div>
</div>
<div class="flex items-center mb-4">
<ui-select-input v-model="jumpForwardAmount" :label="$strings.LabelJumpForwardAmount" menuMaxHeight="250px" :items="jumpValues" @input="setJumpForwardAmount" />
</div>
<div class="flex items-center">
<ui-select-input v-model="jumpBackwardAmount" :label="$strings.LabelJumpBackwardAmount" menuMaxHeight="250px" :items="jumpValues" @input="setJumpBackwardAmount" />
</div>
</div>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean
},
data() {
return {
useChapterTrack: false,
jumpValues: [
{ text: this.$getString('LabelTimeDurationXSeconds', ['10']), value: 10 },
{ text: this.$getString('LabelTimeDurationXSeconds', ['15']), value: 15 },
{ text: this.$getString('LabelTimeDurationXSeconds', ['30']), value: 30 },
{ text: this.$getString('LabelTimeDurationXSeconds', ['60']), value: 60 },
{ text: this.$getString('LabelTimeDurationXMinutes', ['2']), value: 120 },
{ text: this.$getString('LabelTimeDurationXMinutes', ['5']), value: 300 }
],
jumpForwardAmount: 10,
jumpBackwardAmount: 10
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}
},
methods: {
setUseChapterTrack() {
this.$store.dispatch('user/updateUserSettings', { useChapterTrack: this.useChapterTrack })
},
setJumpForwardAmount(val) {
this.jumpForwardAmount = val
this.$store.dispatch('user/updateUserSettings', { jumpForwardAmount: val })
},
setJumpBackwardAmount(val) {
this.jumpBackwardAmount = val
this.$store.dispatch('user/updateUserSettings', { jumpBackwardAmount: val })
}
},
mounted() {
this.useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack')
this.jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount')
this.jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount')
}
}
</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-symbols 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>
+77 -62
View File
@@ -6,34 +6,36 @@
</div> </div>
</template> </template>
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh"> <div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<div v-if="!timerSet" class="w-full"> <div class="w-full">
<template v-for="time in sleepTimes"> <template v-for="time in sleepTimes">
<div :key="time.text" class="flex items-center px-6 py-3 justify-center cursor-pointer hover:bg-bg relative" @click="setTime(time.seconds)"> <div :key="time.text" class="flex items-center px-6 py-3 justify-center cursor-pointer hover:bg-primary/25 relative" @click="setTime(time)">
<p class="text-xl text-center">{{ time.text }}</p> <p class="text-lg text-center">{{ time.text }}</p>
</div> </div>
</template> </template>
<form class="flex items-center justify-center px-6 py-3" @submit.prevent="submitCustomTime"> <form class="flex items-center justify-center px-6 py-3" @submit.prevent="submitCustomTime">
<ui-text-input v-model="customTime" type="number" step="any" min="0.1" placeholder="Time in minutes" class="w-48" /> <ui-text-input v-model="customTime" type="number" step="any" min="0.1" :placeholder="$strings.LabelTimeInMinutes" class="w-48" />
<ui-btn color="success" type="submit" :padding-x="0" class="h-9 w-12 flex items-center justify-center ml-1">Set</ui-btn> <ui-btn color="success" type="submit" :padding-x="0" class="h-9 w-12 flex items-center justify-center ml-1">Set</ui-btn>
</form> </form>
</div> </div>
<div v-else class="w-full p-4"> <div v-if="timerSet" class="w-full p-4">
<div class="mb-4 flex items-center justify-center"> <div class="mb-4 h-px w-full bg-white/10" />
<ui-btn :padding-x="2" small :disabled="remaining < 30 * 60" class="flex items-center mr-4" @click="decrement(30 * 60)">
<span class="material-icons text-lg">remove</span> <div v-if="timerType === $constants.SleepTimerTypes.COUNTDOWN" class="mb-4 flex items-center justify-center space-x-4">
<span class="pl-1 text-base font-mono">30m</span> <ui-btn :padding-x="2" small :disabled="remaining < 30 * 60" class="flex items-center h-9" @click="decrement(30 * 60)">
<span class="material-symbols text-lg">remove</span>
<span class="pl-1 text-sm">30m</span>
</ui-btn> </ui-btn>
<ui-icon-btn icon="remove" @click="decrement(60 * 5)" /> <ui-icon-btn icon="remove" class="min-w-9" @click="decrement(60 * 5)" />
<p class="mx-6 text-2xl font-mono">{{ $secondsToTimestamp(remaining) }}</p> <p class="text-2xl font-mono">{{ $secondsToTimestamp(remaining) }}</p>
<ui-icon-btn icon="add" @click="increment(60 * 5)" /> <ui-icon-btn icon="add" class="min-w-9" @click="increment(60 * 5)" />
<ui-btn :padding-x="2" small class="flex items-center ml-4" @click="increment(30 * 60)"> <ui-btn :padding-x="2" small class="flex items-center h-9" @click="increment(30 * 60)">
<span class="material-icons text-lg">add</span> <span class="material-symbols text-lg">add</span>
<span class="pl-1 text-base font-mono">30m</span> <span class="pl-1 text-sm">30m</span>
</ui-btn> </ui-btn>
</div> </div>
<ui-btn class="w-full" @click="$emit('cancel')">{{ $strings.ButtonCancel }}</ui-btn> <ui-btn class="w-full" @click="$emit('cancel')">{{ $strings.ButtonCancel }}</ui-btn>
@@ -47,52 +49,13 @@ export default {
props: { props: {
value: Boolean, value: Boolean,
timerSet: Boolean, timerSet: Boolean,
timerTime: Number, timerType: String,
remaining: Number remaining: Number,
hasChapters: Boolean
}, },
data() { data() {
return { return {
customTime: null, customTime: null
sleepTimes: [
{
seconds: 60 * 5,
text: '5 minutes'
},
{
seconds: 60 * 15,
text: '15 minutes'
},
{
seconds: 60 * 20,
text: '20 minutes'
},
{
seconds: 60 * 30,
text: '30 minutes'
},
{
seconds: 60 * 45,
text: '45 minutes'
},
{
seconds: 60 * 60,
text: '60 minutes'
},
{
seconds: 60 * 90,
text: '90 minutes'
},
{
seconds: 60 * 120,
text: '2 hours'
}
]
}
},
watch: {
show(newVal) {
if (newVal) {
}
} }
}, },
computed: { computed: {
@@ -103,6 +66,54 @@ export default {
set(val) { set(val) {
this.$emit('input', val) this.$emit('input', val)
} }
},
sleepTimes() {
const times = [
{
seconds: 60 * 5,
text: this.$getString('LabelTimeDurationXMinutes', ['5']),
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
},
{
seconds: 60 * 15,
text: this.$getString('LabelTimeDurationXMinutes', ['15']),
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
},
{
seconds: 60 * 20,
text: this.$getString('LabelTimeDurationXMinutes', ['20']),
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
},
{
seconds: 60 * 30,
text: this.$getString('LabelTimeDurationXMinutes', ['30']),
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
},
{
seconds: 60 * 45,
text: this.$getString('LabelTimeDurationXMinutes', ['45']),
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
},
{
seconds: 60 * 60,
text: this.$getString('LabelTimeDurationXMinutes', ['60']),
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
},
{
seconds: 60 * 90,
text: this.$getString('LabelTimeDurationXMinutes', ['90']),
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
},
{
seconds: 60 * 120,
text: this.$getString('LabelTimeDurationXHours', ['2']),
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
}
]
if (this.hasChapters) {
times.push({ seconds: -1, text: this.$strings.LabelEndOfChapter, timerType: this.$constants.SleepTimerTypes.CHAPTER })
}
return times
} }
}, },
methods: { methods: {
@@ -113,10 +124,14 @@ export default {
} }
const timeInSeconds = Math.round(Number(this.customTime) * 60) const timeInSeconds = Math.round(Number(this.customTime) * 60)
this.setTime(timeInSeconds) const time = {
seconds: timeInSeconds,
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
}
this.setTime(time)
}, },
setTime(seconds) { setTime(time) {
this.$emit('set', seconds) this.$emit('set', time)
}, },
increment(amount) { increment(amount) {
this.$emit('increment', amount) this.$emit('increment', amount)
@@ -12,7 +12,7 @@
</div> </div>
<div v-if="previewUpload" class="absolute top-0 left-0 w-full h-full z-10 bg-bg p-8"> <div v-if="previewUpload" class="absolute top-0 left-0 w-full h-full z-10 bg-bg p-8">
<p class="text-lg">Preview Cover</p> <p class="text-lg">Preview Cover</p>
<span class="absolute top-4 right-4 material-icons text-2xl cursor-pointer" @click="resetCoverPreview">close</span> <span class="absolute top-4 right-4 material-symbols text-2xl cursor-pointer" @click="resetCoverPreview">close</span>
<div class="flex justify-center py-4"> <div class="flex justify-center py-4">
<covers-preview-cover :src="previewUpload" :width="240" /> <covers-preview-cover :src="previewUpload" :width="240" />
</div> </div>
+15 -11
View File
@@ -9,9 +9,9 @@
<div class="flex"> <div class="flex">
<div class="w-40 p-2"> <div class="w-40 p-2">
<div class="w-full h-45 relative"> <div class="w-full h-45 relative">
<covers-author-image :author="author" /> <covers-author-image :author="authorCopy" />
<div v-if="userCanDelete && !processing && author.imagePath" class="absolute top-0 left-0 w-full h-full opacity-0 hover:opacity-100"> <div v-if="userCanDelete && !processing && author.imagePath" class="absolute top-0 left-0 w-full h-full opacity-0 hover:opacity-100">
<span class="absolute top-2 right-2 material-icons text-error transform hover:scale-125 transition-transform cursor-pointer text-lg" @click="removeCover">delete</span> <span class="absolute top-2 right-2 material-symbols text-error transform hover:scale-125 transition-transform cursor-pointer text-lg" @click="removeCover">delete</span>
</div> </div>
</div> </div>
</div> </div>
@@ -30,9 +30,6 @@
<ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" /> <ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" />
</div> </div>
</div> </div>
<!-- <div class="p-2">
<ui-text-input-with-label v-model="authorCopy.imagePath" :disabled="processing" :label="$strings.LabelPhotoPathURL" />
</div> -->
<div class="p-2"> <div class="p-2">
<ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" :label="$strings.LabelDescription" :rows="8" /> <ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" :label="$strings.LabelDescription" :rows="8" />
</div> </div>
@@ -106,9 +103,9 @@ export default {
methods: { methods: {
init() { init() {
this.imageUrl = '' this.imageUrl = ''
this.authorCopy.name = this.author.name this.authorCopy = {
this.authorCopy.asin = this.author.asin ...this.author
this.authorCopy.description = this.author.description }
}, },
removeClick() { removeClick() {
const payload = { const payload = {
@@ -171,7 +168,9 @@ export default {
.$delete(`/api/authors/${this.authorId}/image`) .$delete(`/api/authors/${this.authorId}/image`)
.then((data) => { .then((data) => {
this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess) this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess)
this.$store.commit('globals/showEditAuthorModal', data.author)
this.authorCopy.updatedAt = data.author.updatedAt
this.authorCopy.imagePath = data.author.imagePath
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
@@ -196,7 +195,9 @@ export default {
.then((data) => { .then((data) => {
this.imageUrl = '' this.imageUrl = ''
this.$toast.success('Author image updated') this.$toast.success('Author image updated')
this.$store.commit('globals/showEditAuthorModal', data.author)
this.authorCopy.updatedAt = data.author.updatedAt
this.authorCopy.imagePath = data.author.imagePath
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
@@ -231,8 +232,11 @@ export default {
} else if (response.updated) { } else if (response.updated) {
if (response.author.imagePath) { if (response.author.imagePath) {
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess) this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
this.$store.commit('globals/showEditAuthorModal', response.author)
} else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound) } else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
this.authorCopy = {
...response.author
}
} else { } else {
this.$toast.info('No updates were made for Author') this.$toast.info('No updates were made for Author')
} }
@@ -12,9 +12,9 @@
<div class="flex-grow pr-2"> <div class="flex-grow pr-2">
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" /> <ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
</div> </div>
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-icons text-2xl -mt-px">forward</span></ui-btn> <ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-symbols text-2xl -mt-px">forward</span></ui-btn>
<div class="pl-2 flex items-center"> <div class="pl-2 flex items-center">
<span class="material-icons text-3xl text-white text-opacity-70 hover:text-opacity-95 cursor-pointer" @click.stop.prevent="cancelEditing">close</span> <span class="material-symbols text-3xl text-white text-opacity-70 hover:text-opacity-95 cursor-pointer" @click.stop.prevent="cancelEditing">close</span>
</div> </div>
</div> </div>
</form> </form>
@@ -22,8 +22,8 @@
<p v-else class="pl-2 pr-2 truncate">{{ bookmark.title }}</p> <p v-else class="pl-2 pr-2 truncate">{{ bookmark.title }}</p>
</div> </div>
<div v-if="!isEditing" class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'"> <div v-if="!isEditing" class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
<span class="material-icons text-xl mr-2 text-gray-200 hover:text-yellow-400" @click.stop="editClick">edit</span> <span class="material-symbols text-xl mr-2 text-gray-200 hover:text-yellow-400" @click.stop="editClick">edit</span>
<span class="material-icons text-xl text-gray-200 hover:text-error cursor-pointer" @click.stop="deleteClick">delete</span> <span class="material-symbols text-xl text-gray-200 hover:text-error cursor-pointer" @click.stop="deleteClick">delete</span>
</div> </div>
</div> </div>
</template> </template>
@@ -6,8 +6,15 @@
</div> </div>
</template> </template>
<div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh"> <div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh">
<p class="text-xl font-bold pb-4">Changelog v{{ currentVersionNumber }}</p> <template v-for="release in releasesToShow">
<div class="custom-text" v-html="compiledMarkedown" /> <div :key="release.name">
<p class="text-xl font-bold pb-4">
Changelog <a :href="`https://github.com/advplyr/audiobookshelf/releases/tag/${release.name}`" target="_blank" class="hover:underline">{{ release.name }}</a> ({{ $formatDate(release.pubdate, dateFormat) }})
</p>
<div class="custom-text" v-html="getChangelog(release)" />
</div>
<div v-if="release !== releasesToShow[releasesToShow.length - 1]" class="border-b border-black-300 my-8" />
</template>
</div> </div>
</modals-modal> </modals-modal>
</template> </template>
@@ -18,17 +25,9 @@ import { marked } from '@/static/libs/marked/index.js'
export default { export default {
props: { props: {
value: Boolean, value: Boolean,
changelog: String, versionData: {
currentVersion: String type: Object,
}, default: () => {}
watch: {
show: {
immediate: true,
handler(newVal) {
if (newVal) {
this.init()
}
}
} }
}, },
computed: { computed: {
@@ -40,15 +39,17 @@ export default {
this.$emit('input', val) this.$emit('input', val)
} }
}, },
compiledMarkedown() { dateFormat() {
return marked.parse(this.changelog, { gfm: true, breaks: true }) return this.$store.state.serverSettings.dateFormat
}, },
currentVersionNumber() { releasesToShow() {
return this.currentVersion return this.versionData?.releasesToShow || []
} }
}, },
methods: { methods: {
init() {} getChangelog(release) {
return marked.parse(release.changelog || 'No Changelog Available', { gfm: true, breaks: true })
}
}, },
mounted() {} mounted() {}
} }
@@ -8,8 +8,8 @@
<nuxt-link :to="`/collection/${collection.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ collection.name }}</nuxt-link> <nuxt-link :to="`/collection/${collection.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ collection.name }}</nuxt-link>
</div> </div>
<div class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'"> <div class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
<ui-btn v-if="!isBookIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-icons text-2xl pt-px">add</span></ui-btn> <ui-btn v-if="!isBookIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-symbols text-2xl pt-px">add</span></ui-btn>
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-icons text-2xl pt-px">remove</span></ui-btn> <ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-symbols text-2xl pt-px">remove</span></ui-btn>
</div> </div>
</div> </div>
</template> </template>
@@ -28,7 +28,7 @@
<template v-else> <template v-else>
<div class="flex items-center mb-3"> <div class="flex items-center mb-3">
<div class="hover:bg-white hover:bg-opacity-10 cursor-pointer h-11 w-11 flex items-center justify-center rounded-full" @click="showImageUploader = false"> <div class="hover:bg-white hover:bg-opacity-10 cursor-pointer h-11 w-11 flex items-center justify-center rounded-full" @click="showImageUploader = false">
<span class="material-icons text-4xl">arrow_back</span> <span class="material-symbols text-4xl">arrow_back</span>
</div> </div>
<p class="ml-2 text-xl mb-1">Collection Cover Image</p> <p class="ml-2 text-xl mb-1">Collection Cover Image</p>
</div> </div>
+2 -2
View File
@@ -12,10 +12,10 @@
</div> </div>
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6"> <div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevBook" @mousedown.prevent>arrow_back_ios</div> <div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevBook" @mousedown.prevent>arrow_back_ios</div>
</div> </div>
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6"> <div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextBook" @mousedown.prevent>arrow_forward_ios</div> <div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextBook" @mousedown.prevent>arrow_forward_ios</div>
</div> </div>
<div class="w-full h-full max-h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative"> <div class="w-full h-full max-h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative">
+3 -3
View File
@@ -9,7 +9,7 @@
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" /> <div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
<div v-if="userCanDelete" class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-sm" @click="removeCover"> <div v-if="userCanDelete" class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-sm" @click="removeCover">
<ui-tooltip direction="top" :text="$strings.LabelRemoveCover"> <ui-tooltip direction="top" :text="$strings.LabelRemoveCover">
<span class="material-icons text-2xl">delete</span> <span class="material-symbols text-2xl">delete</span>
</ui-tooltip> </ui-tooltip>
</div> </div>
</div> </div>
@@ -19,7 +19,7 @@
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 md:min-w-32"> <div v-if="userCanUpload" class="w-10 md:w-40 pr-2 md:min-w-32">
<ui-file-input ref="fileInput" @change="fileUploadSelected"> <ui-file-input ref="fileInput" @change="fileUploadSelected">
<span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span> <span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span>
<span class="material-icons text-2xl inline-block md:!hidden">upload</span> <span class="material-symbols text-2xl inline-block md:!hidden">upload</span>
</ui-file-input> </ui-file-input>
</div> </div>
@@ -73,7 +73,7 @@
<div v-if="previewUpload" class="absolute top-0 left-0 w-full h-full z-10 bg-bg p-8"> <div v-if="previewUpload" class="absolute top-0 left-0 w-full h-full z-10 bg-bg p-8">
<p class="text-lg">{{ $strings.HeaderPreviewCover }}</p> <p class="text-lg">{{ $strings.HeaderPreviewCover }}</p>
<span class="absolute top-4 right-4 material-icons text-2xl cursor-pointer" @click="resetCoverPreview">close</span> <span class="absolute top-4 right-4 material-symbols text-2xl cursor-pointer" @click="resetCoverPreview">close</span>
<div class="flex justify-center py-4"> <div class="flex justify-center py-4">
<covers-preview-cover :src="previewUpload" :width="240" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-preview-cover :src="previewUpload" :width="240" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div> </div>
@@ -7,7 +7,7 @@
<div class="flex -mb-0.5"> <div class="flex -mb-0.5">
<p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': checkingNewEpisodes }">{{ $strings.LabelLimit }}</p> <p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': checkingNewEpisodes }">{{ $strings.LabelLimit }}</p>
<ui-tooltip direction="top" text="Max # of episodes to download. Use 0 for unlimited."> <ui-tooltip direction="top" text="Max # of episodes to download. Use 0 for unlimited.">
<span class="material-icons text-base">info_outlined</span> <span class="material-symbols text-base">info</span>
</ui-tooltip> </ui-tooltip>
</div> </div>
</ui-text-input-with-label> </ui-text-input-with-label>
+61 -20
View File
@@ -28,7 +28,7 @@
<div v-if="selectedMatchOrig" class="absolute top-0 left-0 w-full bg-bg h-full px-2 py-6 md:p-8 max-h-full overflow-y-auto overflow-x-hidden"> <div v-if="selectedMatchOrig" class="absolute top-0 left-0 w-full bg-bg h-full px-2 py-6 md:p-8 max-h-full overflow-y-auto overflow-x-hidden">
<div class="flex mb-4"> <div class="flex mb-4">
<div class="w-8 h-8 rounded-full hover:bg-white hover:bg-opacity-10 flex items-center justify-center cursor-pointer" @click="clearSelectedMatch"> <div class="w-8 h-8 rounded-full hover:bg-white hover:bg-opacity-10 flex items-center justify-center cursor-pointer" @click="clearSelectedMatch">
<span class="material-icons text-3xl">arrow_back</span> <span class="material-symbols text-3xl">arrow_back</span>
</div> </div>
<p class="text-xl pl-3">{{ $strings.HeaderUpdateDetails }}</p> <p class="text-xl pl-3">{{ $strings.HeaderUpdateDetails }}</p>
</div> </div>
@@ -59,49 +59,63 @@
<ui-checkbox v-model="selectedMatchUsage.title" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.title" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" :label="$strings.LabelTitle" /> <ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" :label="$strings.LabelTitle" />
<p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.title || '' }}</p> <p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('title', mediaMetadata.title)">{{ mediaMetadata.title || '' }}</a>
</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.subtitle" class="flex items-center py-2"> <div v-if="selectedMatchOrig.subtitle" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.subtitle" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.subtitle" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" :label="$strings.LabelSubtitle" /> <ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" :label="$strings.LabelSubtitle" />
<p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.subtitle || '' }}</p> <p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('subtitle', mediaMetadata.subtitle)">{{ mediaMetadata.subtitle }}</a>
</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.author" class="flex items-center py-2"> <div v-if="selectedMatchOrig.author" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.author" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.author" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" :label="$strings.LabelAuthor" /> <ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" :label="$strings.LabelAuthor" />
<p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.authorName || '' }}</p> <p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('author', mediaMetadata.authorName)">{{ mediaMetadata.authorName }}</a>
</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.narrator" class="flex items-center py-2"> <div v-if="selectedMatchOrig.narrator" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.narrator" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.narrator" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-multi-select v-model="selectedMatch.narrator" :items="narrators" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" /> <ui-multi-select v-model="selectedMatch.narrator" :items="narrators" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" />
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.narratorName || '' }}</p> <p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('narrator', mediaMetadata.narrators)">{{ mediaMetadata.narratorName }}</a>
</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.description" class="flex items-center py-2"> <div v-if="selectedMatchOrig.description" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.description" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.description" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" /> <ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" />
<p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</p> <p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</a>
</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.publisher" class="flex items-center py-2"> <div v-if="selectedMatchOrig.publisher" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.publisher" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.publisher" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" :label="$strings.LabelPublisher" /> <ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" :label="$strings.LabelPublisher" />
<p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.publisher || '' }}</p> <p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publisher', mediaMetadata.publisher)">{{ mediaMetadata.publisher }}</a>
</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.publishedYear" class="flex items-center py-2"> <div v-if="selectedMatchOrig.publishedYear" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.publishedYear" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.publishedYear" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" :label="$strings.LabelPublishYear" /> <ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" :label="$strings.LabelPublishYear" />
<p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.publishedYear || '' }}</p> <p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publishedYear', mediaMetadata.publishedYear)">{{ mediaMetadata.publishedYear }}</a>
</p>
</div> </div>
</div> </div>
@@ -109,42 +123,54 @@
<ui-checkbox v-model="selectedMatchUsage.series" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.series" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<widgets-series-input-widget v-model="selectedMatch.series" :disabled="!selectedMatchUsage.series" /> <widgets-series-input-widget v-model="selectedMatch.series" :disabled="!selectedMatchUsage.series" />
<p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.seriesName || '' }}</p> <p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('series', mediaMetadata.series)">{{ mediaMetadata.seriesName }}</a>
</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.genres && selectedMatchOrig.genres.length" class="flex items-center py-2"> <div v-if="selectedMatchOrig.genres?.length" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.genres" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.genres" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-multi-select v-model="selectedMatch.genres" :items="genres" :disabled="!selectedMatchUsage.genres" :label="$strings.LabelGenres" /> <ui-multi-select v-model="selectedMatch.genres" :items="genres" :disabled="!selectedMatchUsage.genres" :label="$strings.LabelGenres" />
<p v-if="mediaMetadata.genres" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.genres.join(', ') }}</p> <p v-if="mediaMetadata.genres?.length" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('genres', mediaMetadata.genres)">{{ mediaMetadata.genres.join(', ') }}</a>
</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.tags" class="flex items-center py-2"> <div v-if="selectedMatchOrig.tags" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.tags" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.tags" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-multi-select v-model="selectedMatch.tags" :items="tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" /> <ui-multi-select v-model="selectedMatch.tags" :items="tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" />
<p v-if="media.tags" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ media.tags.join(', ') }}</p> <p v-if="media.tags?.length" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('tags', media.tags)">{{ media.tags.join(', ') }}</a>
</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.language" class="flex items-center py-2"> <div v-if="selectedMatchOrig.language" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.language" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.language" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" :label="$strings.LabelLanguage" /> <ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" :label="$strings.LabelLanguage" />
<p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.language || '' }}</p> <p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('language', mediaMetadata.language)">{{ mediaMetadata.language }}</a>
</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.isbn" class="flex items-center py-2"> <div v-if="selectedMatchOrig.isbn" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.isbn" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.isbn" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" /> <ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" />
<p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.isbn || '' }}</p> <p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('isbn', mediaMetadata.isbn)">{{ mediaMetadata.isbn }}</a>
</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.asin" class="flex items-center py-2"> <div v-if="selectedMatchOrig.asin" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.asin" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.asin" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" /> <ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" />
<p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.asin || '' }}</p> <p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('asin', mediaMetadata.asin)">{{ mediaMetadata.asin }}</a>
</p>
</div> </div>
</div> </div>
@@ -152,28 +178,36 @@
<ui-checkbox v-model="selectedMatchUsage.itunesId" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.itunesId" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.itunesId" type="number" :disabled="!selectedMatchUsage.itunesId" label="iTunes ID" /> <ui-text-input-with-label v-model="selectedMatch.itunesId" type="number" :disabled="!selectedMatchUsage.itunesId" label="iTunes ID" />
<p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.itunesId || '' }}</p> <p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesId', mediaMetadata.itunesId)">{{ mediaMetadata.itunesId }}</a>
</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.feedUrl" class="flex items-center py-2"> <div v-if="selectedMatchOrig.feedUrl" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.feedUrl" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.feedUrl" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.feedUrl" :disabled="!selectedMatchUsage.feedUrl" label="RSS Feed URL" /> <ui-text-input-with-label v-model="selectedMatch.feedUrl" :disabled="!selectedMatchUsage.feedUrl" label="RSS Feed URL" />
<p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.feedUrl || '' }}</p> <p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('feedUrl', mediaMetadata.feedUrl)">{{ mediaMetadata.feedUrl }}</a>
</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.itunesPageUrl" class="flex items-center py-2"> <div v-if="selectedMatchOrig.itunesPageUrl" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.itunesPageUrl" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.itunesPageUrl" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.itunesPageUrl" :disabled="!selectedMatchUsage.itunesPageUrl" label="iTunes Page URL" /> <ui-text-input-with-label v-model="selectedMatch.itunesPageUrl" :disabled="!selectedMatchUsage.itunesPageUrl" label="iTunes Page URL" />
<p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.itunesPageUrl || '' }}</p> <p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesPageUrl', mediaMetadata.itunesPageUrl)">{{ mediaMetadata.itunesPageUrl }}</a>
</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.releaseDate" class="flex items-center py-2"> <div v-if="selectedMatchOrig.releaseDate" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.releaseDate" checkbox-bg="bg" @input="checkboxToggled" /> <ui-checkbox v-model="selectedMatchUsage.releaseDate" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" :label="$strings.LabelReleaseDate" /> <ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" :label="$strings.LabelReleaseDate" />
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.releaseDate || '' }}</p> <p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('releaseDate', mediaMetadata.releaseDate)">{{ mediaMetadata.releaseDate }}</a>
</p>
</div> </div>
</div> </div>
<div v-if="selectedMatchOrig.explicit != null" class="flex items-center pb-2" :class="{ 'pt-2': mediaMetadata.explicit == null }"> <div v-if="selectedMatchOrig.explicit != null" class="flex items-center pb-2" :class="{ 'pt-2': mediaMetadata.explicit == null }">
@@ -281,7 +315,7 @@ export default {
return this.$store.getters['libraries/getBookCoverAspectRatio'] return this.$store.getters['libraries/getBookCoverAspectRatio']
}, },
filterData() { filterData() {
return this.$store.state.libraries.filterData return this.$store.state.libraries.filterData || {}
}, },
providers() { providers() {
if (this.isPodcast) return this.$store.state.scanners.podcastProviders if (this.isPodcast) return this.$store.state.scanners.podcastProviders
@@ -321,6 +355,13 @@ export default {
} }
}, },
methods: { methods: {
setMatchFieldValue(field, value) {
if (Array.isArray(value)) {
this.selectedMatch[field] = [...value]
} else {
this.selectedMatch[field] = value
}
},
selectAllToggled(val) { selectAllToggled(val) {
for (const key in this.selectedMatchUsage) { for (const key in this.selectedMatchUsage) {
this.selectedMatchUsage[key] = val this.selectedMatchUsage[key] = val
@@ -15,7 +15,7 @@
<ui-tooltip text="Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes. <br>This will only delete 1 episode per new download."> <ui-tooltip text="Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes. <br>This will only delete 1 episode per new download.">
<p class="pl-4 text-base"> <p class="pl-4 text-base">
Max episodes to keep Max episodes to keep
<span class="material-icons icon-text">info_outlined</span> <span class="material-symbols icon-text">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
@@ -24,7 +24,7 @@
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded."> <ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded.">
<p class="pl-4 text-base"> <p class="pl-4 text-base">
Max new episodes to download per check Max new episodes to download per check
<span class="material-icons icon-text">info_outlined</span> <span class="material-symbols icon-text">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
+4 -13
View File
@@ -2,11 +2,8 @@
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6"> <div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6">
<p class="text-xl font-semibold mb-2">{{ $strings.HeaderAudiobookTools }}</p> <p class="text-xl font-semibold mb-2">{{ $strings.HeaderAudiobookTools }}</p>
<!-- alert for windows install -->
<widgets-alert v-if="isWindowsInstall" type="warning" class="my-8 text-base">Not supported for the Windows install yet</widgets-alert>
<!-- Merge to m4b --> <!-- Merge to m4b -->
<div v-if="showM4bDownload && !isWindowsInstall" class="w-full border border-black-200 p-4 my-8"> <div v-if="showM4bDownload" class="w-full border border-black-200 p-4 my-8">
<div class="flex flex-wrap items-center"> <div class="flex flex-wrap items-center">
<div> <div>
<p class="text-lg">{{ $strings.LabelToolsMakeM4b }}</p> <p class="text-lg">{{ $strings.LabelToolsMakeM4b }}</p>
@@ -16,14 +13,14 @@
<div> <div>
<ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=m4b`" class="flex items-center" <ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=m4b`" class="flex items-center"
>{{ $strings.ButtonOpenManager }} >{{ $strings.ButtonOpenManager }}
<span class="material-icons text-lg ml-2">launch</span> <span class="material-symbols text-lg ml-2">launch</span>
</ui-btn> </ui-btn>
</div> </div>
</div> </div>
</div> </div>
<!-- Embed Metadata --> <!-- Embed Metadata -->
<div v-if="mediaTracks.length && !isWindowsInstall" class="w-full border border-black-200 p-4 my-8"> <div v-if="mediaTracks.length" class="w-full border border-black-200 p-4 my-8">
<div class="flex items-center"> <div class="flex items-center">
<div> <div>
<p class="text-lg">{{ $strings.LabelToolsEmbedMetadata }}</p> <p class="text-lg">{{ $strings.LabelToolsEmbedMetadata }}</p>
@@ -33,7 +30,7 @@
<div> <div>
<ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=embed`" class="flex items-center" <ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=embed`" class="flex items-center"
>{{ $strings.ButtonOpenManager }} >{{ $strings.ButtonOpenManager }}
<span class="material-icons text-lg ml-2">launch</span> <span class="material-symbols text-lg ml-2">launch</span>
</ui-btn> </ui-btn>
<ui-btn v-if="!isMetadataEmbedQueued && !isEmbedTaskRunning" class="w-full mt-4" small @click.stop="quickEmbed">Quick Embed</ui-btn> <ui-btn v-if="!isMetadataEmbedQueued && !isEmbedTaskRunning" class="w-full mt-4" small @click.stop="quickEmbed">Quick Embed</ui-btn>
@@ -111,12 +108,6 @@ export default {
}, },
isEncodeTaskRunning() { isEncodeTaskRunning() {
return this.encodeTask && !this.encodeTask?.isFinished return this.encodeTask && !this.encodeTask?.isFinished
},
isWindowsInstall() {
return this.Source == 'windows'
},
Source() {
return this.$store.state.Source
} }
}, },
methods: { methods: {
@@ -19,12 +19,12 @@
<div class="folders-container overflow-y-auto w-full py-2 mb-2"> <div class="folders-container overflow-y-auto w-full py-2 mb-2">
<p class="px-1 text-sm font-semibold">{{ $strings.LabelFolders }}</p> <p class="px-1 text-sm font-semibold">{{ $strings.LabelFolders }}</p>
<div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2"> <div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2">
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span> <span class="material-symbols fill bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
<ui-editable-text ref="folderInput" v-model="folder.fullPath" :readonly="!!folder.id" type="text" class="w-full" @blur="existingFolderInputBlurred(folder)" /> <ui-editable-text ref="folderInput" v-model="folder.fullPath" :readonly="!!folder.id" type="text" class="w-full" @blur="existingFolderInputBlurred(folder)" />
<span v-show="folders.length > 1" class="material-icons text-2xl ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span> <span v-show="folders.length > 1" class="material-symbols text-2xl ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span>
</div> </div>
<div class="flex py-1 px-2 items-center w-full"> <div class="flex py-1 px-2 items-center w-full">
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span> <span class="material-symbols fill bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
<ui-editable-text ref="newFolderInput" v-model="newFolderPath" :placeholder="$strings.PlaceholderNewFolderPath" type="text" class="w-full" @blur="newFolderInputBlurred" /> <ui-editable-text ref="newFolderInput" v-model="newFolderPath" :placeholder="$strings.PlaceholderNewFolderPath" type="text" class="w-full" @blur="newFolderInputBlurred" />
</div> </div>
@@ -1,7 +1,7 @@
<template> <template>
<div class="w-full h-full bg-bg absolute top-0 left-0 px-4 py-4 z-10"> <div class="w-full h-full bg-bg absolute top-0 left-0 px-4 py-4 z-10">
<div class="flex items-center py-1 mb-2"> <div class="flex items-center py-1 mb-2">
<span class="material-icons text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span> <span class="material-symbols text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span>
<p class="px-4 text-xl">{{ $strings.HeaderChooseAFolder }}</p> <p class="px-4 text-xl">{{ $strings.HeaderChooseAFolder }}</p>
</div> </div>
<div v-if="rootDirs.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2"> <div v-if="rootDirs.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2">
@@ -10,18 +10,18 @@
<div v-if="rootDirs.length" class="relative flex bg-primary bg-opacity-50 p-4 folder-container"> <div v-if="rootDirs.length" class="relative flex bg-primary bg-opacity-50 p-4 folder-container">
<div class="w-1/2 border-r border-bg h-full overflow-y-auto"> <div class="w-1/2 border-r border-bg h-full overflow-y-auto">
<div v-if="level > 0" class="w-full p-1 cursor-pointer flex items-center hover:bg-white/10" @click="goBack"> <div v-if="level > 0" class="w-full p-1 cursor-pointer flex items-center hover:bg-white/10" @click="goBack">
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span> <span class="material-symbols fill bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
<p class="text-base font-mono px-2">..</p> <p class="text-base font-mono px-2">..</p>
</div> </div>
<div v-for="dir in _directories" :key="dir.path" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" :class="dir.className" @click="selectDir(dir)"> <div v-for="dir in _directories" :key="dir.path" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" :class="dir.className" @click="selectDir(dir)">
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span> <span class="material-symbols fill bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p> <p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
<span v-if="dir.path === selectedPath" class="material-icons" style="font-size: 1.1rem">arrow_right</span> <span v-if="dir.path === selectedPath" class="material-symbols" style="font-size: 1.1rem">arrow_right</span>
</div> </div>
</div> </div>
<div class="w-1/2 h-full overflow-y-auto"> <div class="w-1/2 h-full overflow-y-auto">
<div v-for="dir in _subdirs" :key="dir.path" :class="dir.className" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" @click="selectSubDir(dir)"> <div v-for="dir in _subdirs" :key="dir.path" :class="dir.className" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" @click="selectSubDir(dir)">
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span> <span class="material-symbols fill bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p> <p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
</div> </div>
</div> </div>
@@ -9,7 +9,7 @@
<p class="text-sm text-gray-300 pr-2">{{ $strings.LabelMetadataOrderOfPrecedenceDescription }}</p> <p class="text-sm text-gray-300 pr-2">{{ $strings.LabelMetadataOrderOfPrecedenceDescription }}</p>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex"> <ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex">
<a href="https://www.audiobookshelf.org/guides/book-scanner" target="_blank" class="inline-flex"> <a href="https://www.audiobookshelf.org/guides/book-scanner" target="_blank" class="inline-flex">
<span class="material-icons text-xl w-5">help_outline</span> <span class="material-symbols text-xl w-5">help_outline</span>
</a> </a>
</ui-tooltip> </ui-tooltip>
</div> </div>
@@ -17,7 +17,7 @@
<draggable v-model="metadataSourceMapped" v-bind="dragOptions" class="list-group" draggable=".item" handle=".drag-handle" tag="ul" @start="drag = true" @end="drag = false" @update="draggableUpdate"> <draggable v-model="metadataSourceMapped" v-bind="dragOptions" class="list-group" draggable=".item" handle=".drag-handle" tag="ul" @start="drag = true" @end="drag = false" @update="draggableUpdate">
<transition-group type="transition" :name="!drag ? 'flip-list' : null"> <transition-group type="transition" :name="!drag ? 'flip-list' : null">
<li v-for="(source, index) in metadataSourceMapped" :key="source.id" :class="source.include ? 'item' : 'opacity-50'" class="w-full px-2 flex items-center relative border border-white/10"> <li v-for="(source, index) in metadataSourceMapped" :key="source.id" :class="source.include ? 'item' : 'opacity-50'" class="w-full px-2 flex items-center relative border border-white/10">
<span class="material-icons drag-handle text-xl text-gray-400 hover:text-gray-50 mr-2 md:mr-4">reorder</span> <span class="material-symbols drag-handle text-xl text-gray-400 hover:text-gray-50 mr-2 md:mr-4">reorder</span>
<div class="text-center py-1 w-8 min-w-8"> <div class="text-center py-1 w-8 min-w-8">
{{ source.include ? getSourceIndex(source.id) : '' }} {{ source.include ? getSourceIndex(source.id) : '' }}
</div> </div>
@@ -5,7 +5,7 @@
<ui-tooltip :text="$strings.LabelSettingsSquareBookCoversHelp"> <ui-tooltip :text="$strings.LabelSettingsSquareBookCoversHelp">
<p class="pl-4 text-base"> <p class="pl-4 text-base">
{{ $strings.LabelSettingsSquareBookCovers }} {{ $strings.LabelSettingsSquareBookCovers }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-symbols icon-text text-sm">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
@@ -22,7 +22,7 @@
<ui-tooltip :text="$strings.LabelSettingsAudiobooksOnlyHelp"> <ui-tooltip :text="$strings.LabelSettingsAudiobooksOnlyHelp">
<p class="pl-4 text-base"> <p class="pl-4 text-base">
{{ $strings.LabelSettingsAudiobooksOnly }} {{ $strings.LabelSettingsAudiobooksOnly }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-symbols icon-text text-sm">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
@@ -44,7 +44,7 @@
<ui-tooltip :text="$strings.LabelSettingsHideSingleBookSeriesHelp"> <ui-tooltip :text="$strings.LabelSettingsHideSingleBookSeriesHelp">
<p class="pl-4 text-base"> <p class="pl-4 text-base">
{{ $strings.LabelSettingsHideSingleBookSeries }} {{ $strings.LabelSettingsHideSingleBookSeries }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-symbols icon-text text-sm">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
@@ -55,7 +55,7 @@
<ui-tooltip :text="$strings.LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp"> <ui-tooltip :text="$strings.LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp">
<p class="pl-4 text-base"> <p class="pl-4 text-base">
{{ $strings.LabelSettingsOnlyShowLaterBooksInContinueSeries }} {{ $strings.LabelSettingsOnlyShowLaterBooksInContinueSeries }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-symbols icon-text text-sm">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
@@ -66,7 +66,7 @@
<ui-tooltip :text="$strings.LabelSettingsEpubsAllowScriptedContentHelp"> <ui-tooltip :text="$strings.LabelSettingsEpubsAllowScriptedContentHelp">
<p class="pl-4 text-base"> <p class="pl-4 text-base">
{{ $strings.LabelSettingsEpubsAllowScriptedContent }} {{ $strings.LabelSettingsEpubsAllowScriptedContent }}
<span class="material-icons icon-text text-sm">info_outlined</span> <span class="material-symbols icon-text text-sm">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
@@ -10,10 +10,10 @@
<p v-if="isOpenInPlayer" class="text-sm text-right text-gray-400">{{ $strings.ButtonPlaying }}</p> <p v-if="isOpenInPlayer" class="text-sm text-right text-gray-400">{{ $strings.ButtonPlaying }}</p>
<div v-else-if="isHovering" class="flex items-center justify-end -mx-1"> <div v-else-if="isHovering" class="flex items-center justify-end -mx-1">
<button class="outline-none mx-1 flex items-center" @click.stop="playClick"> <button class="outline-none mx-1 flex items-center" @click.stop="playClick">
<span class="material-icons text-2xl text-success">play_arrow</span> <span class="material-symbols fill text-2xl text-success">play_arrow</span>
</button> </button>
<button class="outline-none mx-1 flex items-center" @click.stop="removeClick"> <button class="outline-none mx-1 flex items-center" @click.stop="removeClick">
<span class="material-icons text-2xl text-error">close</span> <span class="material-symbols text-2xl text-error">close</span>
</button> </button>
</div> </div>
<p v-else class="text-gray-400 text-sm text-right">{{ durationPretty }}</p> <p v-else class="text-gray-400 text-sm text-right">{{ durationPretty }}</p>
@@ -8,8 +8,8 @@
<nuxt-link :to="`/playlist/${playlist.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ playlist.name }}</nuxt-link> <nuxt-link :to="`/playlist/${playlist.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ playlist.name }}</nuxt-link>
</div> </div>
<div class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'"> <div class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
<ui-btn v-if="!isItemIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-icons text-2xl pt-px">add</span></ui-btn> <ui-btn v-if="!isItemIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-symbols text-2xl pt-px">add</span></ui-btn>
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-icons text-2xl pt-px">remove</span></ui-btn> <ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-symbols text-2xl pt-px">remove</span></ui-btn>
</div> </div>
</div> </div>
</template> </template>
@@ -12,10 +12,10 @@
</div> </div>
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6"> <div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevEpisode" @mousedown.prevent>arrow_back_ios</div> <div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevEpisode" @mousedown.prevent>arrow_back_ios</div>
</div> </div>
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6"> <div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextEpisode" @mousedown.prevent>arrow_forward_ios</div> <div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextEpisode" @mousedown.prevent>arrow_forward_ios</div>
</div> </div>
<div ref="wrapper" class="p-4 w-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh"> <div ref="wrapper" class="p-4 w-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
@@ -20,7 +20,7 @@
@click="toggleSelectEpisode(episode)" @click="toggleSelectEpisode(episode)"
> >
<div class="absolute top-0 left-0 h-full flex items-center p-2"> <div class="absolute top-0 left-0 h-full flex items-center p-2">
<span v-if="getIsEpisodeDownloaded(episode)" class="material-icons text-success text-xl">download_done</span> <span v-if="getIsEpisodeDownloaded(episode)" class="material-symbols text-success text-xl">download_done</span>
<ui-checkbox v-else v-model="selectedEpisodes[episode.cleanUrl]" small checkbox-bg="primary" border-color="gray-600" /> <ui-checkbox v-else v-model="selectedEpisodes[episode.cleanUrl]" small checkbox-bg="primary" border-color="gray-600" />
</div> </div>
<div class="px-8 py-2"> <div class="px-8 py-2">
@@ -16,11 +16,18 @@
</div> </div>
</div> </div>
<p class="text-lg font-semibold mb-2">{{ $strings.HeaderPodcastsToAdd }}</p> <p class="text-lg font-semibold mb-1">{{ $strings.HeaderPodcastsToAdd }}</p>
<p class="text-sm text-gray-300 mb-4">{{ $strings.MessageOpmlPreviewNote }}</p>
<div class="w-full overflow-y-auto" style="max-height: 50vh"> <div class="w-full overflow-y-auto" style="max-height: 50vh">
<template v-for="(feed, index) in feedMetadata"> <template v-for="(feed, index) in feeds">
<cards-podcast-feed-summary-card :key="index" :feed="feed" :library-folder-path="selectedFolderPath" class="my-1" /> <div :key="index" class="py-1 flex items-center">
<p class="text-lg font-semibold">{{ index + 1 }}.</p>
<div class="pl-2">
<p v-if="feed.title" class="text-sm font-semibold">{{ feed.title }}</p>
<p class="text-xs text-gray-400">{{ feed.feedUrl }}</p>
</div>
</div>
</template> </template>
</div> </div>
</div> </div>
@@ -45,9 +52,7 @@ export default {
return { return {
processing: false, processing: false,
selectedFolderId: null, selectedFolderId: null,
fullPath: null, autoDownloadEpisodes: false
autoDownloadEpisodes: false,
feedMetadata: []
} }
}, },
watch: { watch: {
@@ -96,73 +101,36 @@ export default {
} }
}, },
methods: { methods: {
toFeedMetadata(feed) {
const metadata = feed.metadata
return {
title: metadata.title,
author: metadata.author,
description: metadata.description,
releaseDate: '',
genres: [...metadata.categories],
feedUrl: metadata.feedUrl,
imageUrl: metadata.image,
itunesPageUrl: '',
itunesId: '',
itunesArtistId: '',
language: '',
numEpisodes: feed.numEpisodes
}
},
init() { init() {
this.feedMetadata = this.feeds.map(this.toFeedMetadata)
if (this.folderItems[0]) { if (this.folderItems[0]) {
this.selectedFolderId = this.folderItems[0].value this.selectedFolderId = this.folderItems[0].value
} }
}, },
async submit() { async submit() {
this.processing = true this.processing = true
const newFeedPayloads = this.feedMetadata.map((metadata) => {
return {
path: `${this.selectedFolderPath}/${this.$sanitizeFilename(metadata.title)}`,
folderId: this.selectedFolderId,
libraryId: this.currentLibrary.id,
media: {
metadata: {
...metadata
},
autoDownloadEpisodes: this.autoDownloadEpisodes
}
}
})
console.log('New feed payloads', newFeedPayloads)
for (const podcastPayload of newFeedPayloads) { const payload = {
await this.$axios feeds: this.feeds.map((f) => f.feedUrl),
.$post('/api/podcasts', podcastPayload) folderId: this.selectedFolderId,
.then(() => { libraryId: this.currentLibrary.id,
this.$toast.success(`${podcastPayload.media.metadata.title}: ${this.$strings.ToastPodcastCreateSuccess}`) autoDownloadEpisodes: this.autoDownloadEpisodes
})
.catch((error) => {
var errorMsg = error.response && error.response.data ? error.response.data : this.$strings.ToastPodcastCreateFailed
console.error('Failed to create podcast', podcastPayload, error)
this.$toast.error(`${podcastPayload.media.metadata.title}: ${errorMsg}`)
})
} }
this.processing = false this.$axios
this.show = false .$post('/api/podcasts/opml/create', payload)
.then(() => {
this.show = false
})
.catch((error) => {
const errorMsg = error.response?.data || this.$strings.ToastPodcastCreateFailed
console.error('Failed to create podcast', payload, error)
this.$toast.error(errorMsg)
})
.finally(() => {
this.processing = false
})
} }
}, },
mounted() {} mounted() {}
} }
</script> </script>
<style scoped>
#podcast-wrapper {
min-height: 400px;
max-height: 80vh;
}
#episodes-scroll {
max-height: calc(80vh - 200px);
}
</style>
@@ -132,7 +132,7 @@ export default {
this.searchedTitle = this.episodeTitle this.searchedTitle = this.episodeTitle
this.isProcessing = true this.isProcessing = true
this.$axios this.$axios
.$get(`/api/podcasts/${this.libraryItem.id}/search-episode?title=${this.$encodeUriPath(this.episodeTitle)}`) .$get(`/api/podcasts/${this.libraryItem.id}/search-episode?title=${encodeURIComponent(this.episodeTitle)}`)
.then((results) => { .then((results) => {
this.episodesFound = results.episodes.map((ep) => ep.episode) this.episodesFound = results.episodes.map((ep) => ep.episode)
console.log('Episodes found', this.episodesFound) console.log('Episodes found', this.episodesFound)
@@ -12,7 +12,7 @@
<div class="w-full relative"> <div class="w-full relative">
<ui-text-input v-model="currentFeed.feedUrl" readonly /> <ui-text-input v-model="currentFeed.feedUrl" readonly />
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(currentFeed.feedUrl)">content_copy</span> <span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(currentFeed.feedUrl)">content_copy</span>
</div> </div>
<div v-if="currentFeed.meta" class="mt-5"> <div v-if="currentFeed.meta" class="mt-5">
@@ -6,7 +6,7 @@
<div class="w-full relative"> <div class="w-full relative">
<ui-text-input v-model="feed.feedUrl" readonly /> <ui-text-input v-model="feed.feedUrl" readonly />
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feed.feedUrl)">content_copy</span> <span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feed.feedUrl)">content_copy</span>
</div> </div>
<div v-if="feed.meta" class="mt-5"> <div v-if="feed.meta" class="mt-5">
@@ -4,32 +4,32 @@
<template v-if="!loading"> <template v-if="!loading">
<ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 lg:mr-8"> <ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 lg:mr-8">
<button :aria-label="$strings.ButtonPreviousChapter" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter"> <button :aria-label="$strings.ButtonPreviousChapter" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
<span class="material-icons text-2xl sm:text-3xl">first_page</span> <span class="material-symbols text-2xl sm:text-3xl">first_page</span>
</button> </button>
</ui-tooltip> </ui-tooltip>
<ui-tooltip direction="top" :text="$strings.ButtonJumpBackward"> <ui-tooltip direction="top" :text="jumpBackwardText">
<button :aria-label="$strings.ButtonJumpBackward" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward"> <button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
<span class="material-icons text-2xl sm:text-3xl">replay_10</span> <span class="material-symbols text-2xl sm:text-3xl">replay</span>
</button> </button>
</ui-tooltip> </ui-tooltip>
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause"> <button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
<span class="material-icons text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span> <span class="material-symbols fill text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
</button> </button>
<ui-tooltip direction="top" :text="$strings.ButtonJumpForward"> <ui-tooltip direction="top" :text="jumpForwardText">
<button :aria-label="$strings.ButtonJumpForward" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward"> <button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
<span class="material-icons text-2xl sm:text-3xl">forward_10</span> <span class="material-symbols text-2xl sm:text-3xl">forward_media</span>
</button> </button>
</ui-tooltip> </ui-tooltip>
<ui-tooltip direction="top" :text="$strings.ButtonNextChapter" class="ml-4 lg:ml-8"> <ui-tooltip direction="top" :text="$strings.ButtonNextChapter" class="ml-4 lg:ml-8">
<button :aria-label="$strings.ButtonNextChapter" :disabled="!hasNextChapter" :class="hasNextChapter ? 'text-gray-300' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter"> <button :aria-label="$strings.ButtonNextChapter" :disabled="!hasNextChapter" :class="hasNextChapter ? 'text-gray-300' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter">
<span class="material-icons text-2xl sm:text-3xl">last_page</span> <span class="material-symbols text-2xl sm:text-3xl">last_page</span>
</button> </button>
</ui-tooltip> </ui-tooltip>
<controls-playback-speed-control v-model="playbackRateInput" @input="playbackRateUpdated" @change="playbackRateChanged" /> <controls-playback-speed-control v-model="playbackRateInput" @input="playbackRateUpdated" @change="playbackRateChanged" />
</template> </template>
<template v-else> <template v-else>
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin"> <div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
<span class="material-icons">autorenew</span> <span class="material-symbols text-2xl">autorenew</span>
</div> </div>
</template> </template>
<div class="flex-grow" /> <div class="flex-grow" />
@@ -56,6 +56,12 @@ export default {
set(val) { set(val) {
this.$emit('update:playbackRate', val) this.$emit('update:playbackRate', val)
} }
},
jumpForwardText() {
return this.getJumpText('jumpForwardAmount', this.$strings.ButtonJumpForward)
},
jumpBackwardText() {
return this.getJumpText('jumpBackwardAmount', this.$strings.ButtonJumpBackward)
} }
}, },
methods: { methods: {
@@ -83,6 +89,20 @@ export default {
this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => { this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => {
console.error('Failed to update settings', err) console.error('Failed to update settings', err)
}) })
},
getJumpText(setting, prefix) {
const amount = this.$store.getters['user/getUserSetting'](setting)
if (!amount) return prefix
let formattedTime = ''
if (amount <= 60) {
formattedTime = this.$getString('LabelTimeDurationXSeconds', [amount])
} else {
const minutes = Math.floor(amount / 60)
formattedTime = this.$getString('LabelTimeDurationXMinutes', [minutes])
}
return `${prefix} - ${formattedTime}`
} }
}, },
mounted() {} mounted() {}
+41 -32
View File
@@ -2,43 +2,43 @@
<div class="w-full -mt-6"> <div class="w-full -mt-6">
<div class="w-full relative mb-1"> <div class="w-full relative mb-1">
<div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full"> <div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full">
<!-- <span class="material-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> --> <!-- <span class="material-symbols text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
<ui-tooltip direction="top" :text="$strings.LabelVolume"> <ui-tooltip direction="top" :text="$strings.LabelVolume">
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden sm:block" /> <controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden sm:block" />
</ui-tooltip> </ui-tooltip>
<ui-tooltip direction="top" :text="$strings.LabelSleepTimer"> <ui-tooltip v-if="!hideSleepTimer" direction="top" :text="$strings.LabelSleepTimer">
<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')"> <button :aria-label="$strings.LabelSleepTimer" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
<span v-if="!sleepTimerSet" class="material-icons text-2xl">snooze</span> <span v-if="!sleepTimerSet" class="material-symbols text-2xl">snooze</span>
<div v-else class="flex items-center"> <div v-else class="flex items-center">
<span class="material-icons text-lg text-warning">snooze</span> <span class="material-symbols text-lg text-warning">snooze</span>
<p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p> <p class="text-sm sm:text-lg text-warning font-mono font-semibold text-center px-0.5 sm:pb-0.5 sm:min-w-8">{{ sleepTimerRemainingString }}</p>
</div> </div>
</button> </button>
</ui-tooltip> </ui-tooltip>
<ui-tooltip v-if="!isPodcast" direction="top" :text="$strings.LabelViewBookmarks"> <ui-tooltip v-if="!isPodcast && !hideBookmarks" direction="top" :text="$strings.LabelViewBookmarks">
<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')"> <button :aria-label="$strings.LabelViewBookmarks" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
<span class="material-icons text-2xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span> <span class="material-symbols text-2xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
</button> </button>
</ui-tooltip> </ui-tooltip>
<ui-tooltip v-if="chapters.length" direction="top" :text="$strings.LabelViewChapters"> <ui-tooltip v-if="chapters.length" direction="top" :text="$strings.LabelViewChapters">
<button :aria-label="$strings.LabelViewChapters" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters"> <button :aria-label="$strings.LabelViewChapters" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
<span class="material-icons text-2xl">format_list_bulleted</span> <span class="material-symbols text-2xl">format_list_bulleted</span>
</button> </button>
</ui-tooltip> </ui-tooltip>
<ui-tooltip v-if="playerQueueItems.length" direction="top" :text="$strings.LabelViewQueue"> <ui-tooltip v-if="playerQueueItems.length" direction="top" :text="$strings.LabelViewQueue">
<button :aria-label="$strings.LabelViewQueue" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')"> <button :aria-label="$strings.LabelViewQueue" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
<span class="material-icons text-2.5xl sm:text-3xl">playlist_play</span> <span class="material-symbols text-2.5xl sm:text-3xl">playlist_play</span>
</button> </button>
</ui-tooltip> </ui-tooltip>
<ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack"> <ui-tooltip direction="top" :text="$strings.LabelViewPlayerSettings">
<button :aria-label="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack" class="text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack"> <button :aria-label="$strings.LabelViewPlayerSettings" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerSettings')">
<span class="material-icons text-2xl sm:text-3xl transform transition-transform" :class="useChapterTrack ? 'rotate-180' : ''">timelapse</span> <span class="material-symbols text-2xl sm:text-2.5xl">settings_slow_motion</span>
</button> </button>
</ui-tooltip> </ui-tooltip>
</div> </div>
@@ -52,8 +52,8 @@
<p ref="currentTimestamp" class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">00:00:00</p> <p ref="currentTimestamp" class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">00:00:00</p>
<p class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto">&nbsp;/&nbsp;{{ progressPercent }}%</p> <p class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto">&nbsp;/&nbsp;{{ progressPercent }}%</p>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="text-xs sm:text-sm text-gray-300 pt-0.5"> <p class="text-xs sm:text-sm text-gray-300 pt-0.5 px-2 truncate">
{{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400">&nbsp;({{ currentChapterIndex + 1 }} of {{ chapters.length }})</span> {{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400">&nbsp;({{ $getString('LabelPlayerChapterNumberMarker', [currentChapterIndex + 1, chapters.length]) }})</span>
</p> </p>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p> <p class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
@@ -72,13 +72,17 @@ export default {
type: Array, type: Array,
default: () => [] default: () => []
}, },
currentChapter: Object,
bookmarks: { bookmarks: {
type: Array, type: Array,
default: () => [] default: () => []
}, },
sleepTimerSet: Boolean, sleepTimerSet: Boolean,
sleepTimerRemaining: Number, sleepTimerRemaining: Number,
isPodcast: Boolean sleepTimerType: String,
isPodcast: Boolean,
hideBookmarks: Boolean,
hideSleepTimer: Boolean
}, },
data() { data() {
return { return {
@@ -88,27 +92,34 @@ export default {
seekLoading: false, seekLoading: false,
showChaptersModal: false, showChaptersModal: false,
currentTime: 0, currentTime: 0,
duration: 0, duration: 0
useChapterTrack: false
} }
}, },
watch: { watch: {
playbackRate() { playbackRate() {
this.updateTimestamp() this.updateTimestamp()
},
useChapterTrack() {
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
this.updateTimestamp()
} }
}, },
computed: { computed: {
sleepTimerRemainingString() { sleepTimerRemainingString() {
var rounded = Math.round(this.sleepTimerRemaining) if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER) {
if (rounded < 90) { return 'EoC'
return `${rounded}s` } else {
var rounded = Math.round(this.sleepTimerRemaining)
if (rounded < 90) {
return `${rounded}s`
}
var minutesRounded = Math.round(rounded / 60)
if (minutesRounded <= 90) {
return `${minutesRounded}m`
}
var hoursRounded = Math.round(minutesRounded / 60)
return `${hoursRounded}h`
} }
var minutesRounded = Math.round(rounded / 60)
if (minutesRounded < 90) {
return `${minutesRounded}m`
}
var hoursRounded = Math.round(minutesRounded / 60)
return `${hoursRounded}h`
}, },
token() { token() {
return this.$store.getters['user/getToken'] return this.$store.getters['user/getToken']
@@ -133,9 +144,6 @@ export default {
if (!duration) return 0 if (!duration) return 0
return Math.round((100 * time) / duration) return Math.round((100 * time) / duration)
}, },
currentChapter() {
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
},
currentChapterName() { currentChapterName() {
return this.currentChapter ? this.currentChapter.title : '' return this.currentChapter ? this.currentChapter.title : ''
}, },
@@ -160,6 +168,10 @@ export default {
}, },
playerQueueItems() { playerQueueItems() {
return this.$store.state.playerQueueItems || [] return this.$store.state.playerQueueItems || []
},
useChapterTrack() {
const _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
return this.chapters.length ? _useChapterTrack : false
} }
}, },
methods: { methods: {
@@ -308,9 +320,6 @@ export default {
init() { init() {
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1 this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
const _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
this.useChapterTrack = this.chapters.length ? _useChapterTrack : false
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack) if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
this.setPlaybackRate(this.playbackRate) this.setPlaybackRate(this.playbackRate)
}, },
+11 -1
View File
@@ -3,7 +3,7 @@
<div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" /> <div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" />
<div ref="content" class="relative text-white" :style="{ height: modalHeight, width: modalWidth }" v-click-outside="clickedOutside"> <div ref="content" class="relative text-white" :style="{ height: modalHeight, width: modalWidth }" v-click-outside="clickedOutside">
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300"> <div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
<p class="text-lg mb-6 mt-2 px-1" v-html="message" /> <p id="confirm-prompt-message" class="text-lg mb-6 mt-2 px-1" v-html="message" />
<ui-checkbox v-if="checkboxLabel" v-model="checkboxValue" checkbox-bg="bg" :label="checkboxLabel" label-class="pl-2 text-base" class="mb-6 px-1" /> <ui-checkbox v-if="checkboxLabel" v-model="checkboxValue" checkbox-bg="bg" :label="checkboxLabel" label-class="pl-2 text-base" class="mb-6 px-1" />
@@ -132,3 +132,13 @@ export default {
} }
} }
</script> </script>
<style>
#confirm-prompt-message code {
font-size: 1rem;
border-radius: 6px;
background-color: rgb(82, 82, 82);
color: white;
padding: 2px 4px;
}
</style>
+6 -6
View File
@@ -15,13 +15,13 @@
</div> </div>
<div v-if="numPages" class="absolute top-0 left-4 sm:left-8 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowPageMenu"> <div v-if="numPages" class="absolute top-0 left-4 sm:left-8 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowPageMenu">
<span class="material-icons text-xl">menu</span> <span class="material-symbols text-xl">menu</span>
</div> </div>
<div v-if="comicMetadata" class="absolute top-0 left-16 sm:left-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowInfoMenu"> <div v-if="comicMetadata" class="absolute top-0 left-16 sm:left-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowInfoMenu">
<span class="material-icons text-xl">more</span> <span class="material-symbols text-xl">more</span>
</div> </div>
<a v-if="pages && numPages" :href="mainImg" :download="pages[page - 1]" class="absolute top-0 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" :class="comicMetadata ? 'left-28 sm:left-32' : 'left-16 sm:left-20'"> <a v-if="pages && numPages" :href="mainImg" :download="pages[page - 1]" class="absolute top-0 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" :class="comicMetadata ? 'left-28 sm:left-32' : 'left-16 sm:left-20'">
<span class="material-icons text-xl">download</span> <span class="material-symbols text-xl">download</span>
</a> </a>
<div v-if="numPages" class="absolute top-0 right-14 sm:right-16 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20"> <div v-if="numPages" class="absolute top-0 right-14 sm:right-16 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20">
@@ -35,12 +35,12 @@
<div class="w-full h-full relative"> <div class="w-full h-full relative">
<div v-show="canGoPrev" ref="prevButton" class="absolute top-0 left-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent> <div v-show="canGoPrev" ref="prevButton" class="absolute top-0 left-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent>
<div class="flex items-center justify-center h-full w-1/2"> <div class="flex items-center justify-center h-full w-1/2">
<span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span> <span v-show="loadedFirstPage" class="material-symbols text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span>
</div> </div>
</div> </div>
<div v-show="canGoNext" ref="nextButton" class="absolute top-0 right-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="next" @mousedown.prevent> <div v-show="canGoNext" ref="nextButton" class="absolute top-0 right-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="next" @mousedown.prevent>
<div class="flex items-center justify-center h-full w-1/2 ml-auto"> <div class="flex items-center justify-center h-full w-1/2 ml-auto">
<span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_forward_ios</span> <span v-show="loadedFirstPage" class="material-symbols text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_forward_ios</span>
</div> </div>
</div> </div>
<div ref="imageContainer" class="w-full h-full relative overflow-auto"> <div ref="imageContainer" class="w-full h-full relative overflow-auto">
@@ -179,7 +179,7 @@ export default {
ebookLocation: this.page, ebookLocation: this.page,
ebookProgress: Math.max(0, Math.min(1, (Number(this.page) - 1) / Number(this.numPages))) ebookProgress: Math.max(0, Math.min(1, (Number(this.page) - 1) / Number(this.numPages)))
} }
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => { this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload, { progress: false }).catch((error) => {
console.error('ComicReader.updateProgress failed:', error) console.error('ComicReader.updateProgress failed:', error)
}) })
}, },
+9 -5
View File
@@ -2,13 +2,13 @@
<div id="epub-reader" class="h-full w-full"> <div id="epub-reader" class="h-full w-full">
<div class="h-full flex items-center justify-center"> <div class="h-full flex items-center justify-center">
<button type="button" aria-label="Previous page" class="w-24 max-w-24 h-full hidden sm:flex items-center overflow-x-hidden justify-center opacity-50 hover:opacity-100"> <button type="button" aria-label="Previous page" class="w-24 max-w-24 h-full hidden sm:flex items-center overflow-x-hidden justify-center opacity-50 hover:opacity-100">
<span v-if="hasPrev" class="material-icons text-6xl" @mousedown.prevent @click="prev">chevron_left</span> <span v-if="hasPrev" class="material-symbols text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
</button> </button>
<div id="frame" class="w-full" style="height: 80%"> <div id="frame" class="w-full" style="height: 80%">
<div id="viewer"></div> <div id="viewer"></div>
</div> </div>
<button type="button" aria-label="Next page" class="w-24 max-w-24 h-full hidden sm:flex items-center justify-center overflow-x-hidden opacity-50 hover:opacity-100"> <button type="button" aria-label="Next page" class="w-24 max-w-24 h-full hidden sm:flex items-center justify-center overflow-x-hidden opacity-50 hover:opacity-100">
<span v-if="hasNext" class="material-icons text-6xl" @mousedown.prevent @click="next">chevron_right</span> <span v-if="hasNext" class="material-symbols text-6xl" @mousedown.prevent @click="next">chevron_right</span>
</button> </button>
</div> </div>
</div> </div>
@@ -46,7 +46,8 @@ export default {
font: 'serif', font: 'serif',
fontScale: 100, fontScale: 100,
lineSpacing: 115, lineSpacing: 115,
spread: 'auto' spread: 'auto',
textStroke: 0
} }
} }
}, },
@@ -109,11 +110,14 @@ export default {
const fontScale = this.ereaderSettings.fontScale / 100 const fontScale = this.ereaderSettings.fontScale / 100
const textStroke = this.ereaderSettings.textStroke / 100
return { return {
'*': { '*': {
color: `${fontColor}!important`, color: `${fontColor}!important`,
'background-color': `${backgroundColor}!important`, 'background-color': `${backgroundColor}!important`,
'line-height': lineSpacing * fontScale + 'rem!important' 'line-height': lineSpacing * fontScale + 'rem!important',
'-webkit-text-stroke': textStroke + 'px ' + fontColor + '!important'
}, },
a: { a: {
color: `${fontColor}!important` color: `${fontColor}!important`
@@ -195,7 +199,7 @@ export default {
*/ */
updateProgress(payload) { updateProgress(payload) {
if (!this.keepProgress) return if (!this.keepProgress) return
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => { this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload, { progress: false }).catch((error) => {
console.error('EpubReader.updateProgress failed:', error) console.error('EpubReader.updateProgress failed:', error)
}) })
}, },
+17 -16
View File
@@ -2,12 +2,12 @@
<div class="w-full h-full pt-20 relative"> <div class="w-full h-full pt-20 relative">
<div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent> <div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent>
<div class="flex items-center justify-center h-full w-1/2"> <div class="flex items-center justify-center h-full w-1/2">
<span class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span> <span class="material-symbols text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span>
</div> </div>
</div> </div>
<div v-show="canGoNext" class="absolute top-0 right-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="next" @mousedown.prevent> <div v-show="canGoNext" class="absolute top-0 right-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="next" @mousedown.prevent>
<div class="flex items-center justify-center h-full w-1/2 ml-auto"> <div class="flex items-center justify-center h-full w-1/2 ml-auto">
<span class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_forward_ios</span> <span class="material-symbols text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_forward_ios</span>
</div> </div>
</div> </div>
@@ -23,13 +23,10 @@
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<div :style="{ width: pdfWidth + 'px', height: pdfHeight + 'px' }" class="overflow-auto"> <div :style="{ width: pdfWidth + 'px', height: pdfHeight + 'px' }" class="overflow-auto">
<div v-if="loadedRatio > 0 && loadedRatio < 1" style="background-color: green; color: white; text-align: center" :style="{ width: loadedRatio * 100 + '%' }">{{ Math.floor(loadedRatio * 100) }}%</div> <div v-if="loadedRatio > 0 && loadedRatio < 1" style="background-color: green; color: white; text-align: center" :style="{ width: loadedRatio * 100 + '%' }">{{ Math.floor(loadedRatio * 100) }}%</div>
<pdf ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md" :src="pdfDocInitParams" :page="page" :rotate="rotate" @progress="progressEvt" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event" @loaded="loadedEvt"></pdf> <pdf v-if="pdfDocInitParams" ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md" :src="pdfDocInitParams" :page="page" :rotate="rotate" @progress="progressEvt" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event" @loaded="loadedEvt"></pdf>
</div> </div>
</div> </div>
</div> </div>
<!-- <div class="text-center py-2 text-lg">
<p>{{ page }} / {{ numPages }}</p>
</div> -->
</div> </div>
</template> </template>
@@ -57,7 +54,8 @@ export default {
rotate: 0, rotate: 0,
loadedRatio: 0, loadedRatio: 0,
page: 1, page: 1,
numPages: 0 numPages: 0,
pdfDocInitParams: null
} }
}, },
computed: { computed: {
@@ -108,14 +106,6 @@ export default {
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}` return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
} }
return `/api/items/${this.libraryItemId}/ebook` return `/api/items/${this.libraryItemId}/ebook`
},
pdfDocInitParams() {
return {
url: this.ebookUrl,
httpHeaders: {
Authorization: `Bearer ${this.userToken}`
}
}
} }
}, },
methods: { methods: {
@@ -136,7 +126,7 @@ export default {
ebookLocation: this.page, ebookLocation: this.page,
ebookProgress: Math.max(0, Math.min(1, (Number(this.page) - 1) / Number(this.numPages))) ebookProgress: Math.max(0, Math.min(1, (Number(this.page) - 1) / Number(this.numPages)))
} }
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => { this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload, { progress: false }).catch((error) => {
console.error('EpubReader.updateProgress failed:', error) console.error('EpubReader.updateProgress failed:', error)
}) })
}, },
@@ -149,6 +139,7 @@ export default {
this.loadedRatio = progress this.loadedRatio = progress
}, },
numPagesLoaded(e) { numPagesLoaded(e) {
if (!e) return
this.numPages = e this.numPages = e
}, },
prev() { prev() {
@@ -167,12 +158,22 @@ export default {
resize() { resize() {
this.windowWidth = window.innerWidth this.windowWidth = window.innerWidth
this.windowHeight = window.innerHeight this.windowHeight = window.innerHeight
},
init() {
this.pdfDocInitParams = {
url: this.ebookUrl,
httpHeaders: {
Authorization: `Bearer ${this.userToken}`
}
}
} }
}, },
mounted() { mounted() {
this.windowWidth = window.innerWidth this.windowWidth = window.innerWidth
this.windowHeight = window.innerHeight this.windowHeight = window.innerHeight
window.addEventListener('resize', this.resize) window.addEventListener('resize', this.resize)
this.init()
}, },
beforeDestroy() { beforeDestroy() {
window.removeEventListener('resize', this.resize) window.removeEventListener('resize', this.resize)
+19 -6
View File
@@ -2,10 +2,10 @@
<div v-if="show" id="reader" :data-theme="ereaderTheme" class="group absolute top-0 left-0 w-full z-60 data-[theme=dark]:bg-primary data-[theme=dark]:text-white data-[theme=light]:bg-white data-[theme=light]:text-black" :class="{ 'reader-player-open': !!streamLibraryItem }"> <div v-if="show" id="reader" :data-theme="ereaderTheme" class="group absolute top-0 left-0 w-full z-60 data-[theme=dark]:bg-primary data-[theme=dark]:text-white data-[theme=light]:bg-white data-[theme=light]:text-black" :class="{ 'reader-player-open': !!streamLibraryItem }">
<div class="absolute top-4 left-4 z-20 flex items-center"> <div class="absolute top-4 left-4 z-20 flex items-center">
<button v-if="isEpub" @click="toggleToC" type="button" aria-label="Table of contents menu" class="inline-flex opacity-80 hover:opacity-100"> <button v-if="isEpub" @click="toggleToC" type="button" aria-label="Table of contents menu" class="inline-flex opacity-80 hover:opacity-100">
<span class="material-icons text-2xl">menu</span> <span class="material-symbols text-2xl">menu</span>
</button> </button>
<button v-if="hasSettings" @click="openSettings" type="button" aria-label="Ereader settings" class="mx-4 inline-flex opacity-80 hover:opacity-100"> <button v-if="hasSettings" @click="openSettings" type="button" aria-label="Ereader settings" class="mx-4 inline-flex opacity-80 hover:opacity-100">
<span class="material-icons text-1.5xl">settings</span> <span class="material-symbols text-1.5xl">settings</span>
</button> </button>
</div> </div>
@@ -19,7 +19,7 @@
<div class="absolute top-4 right-4 z-20"> <div class="absolute top-4 right-4 z-20">
<button @click="close" type="button" aria-label="Close ereader" class="inline-flex opacity-80 hover:opacity-100"> <button @click="close" type="button" aria-label="Close ereader" class="inline-flex opacity-80 hover:opacity-100">
<span class="material-icons text-2xl">close</span> <span class="material-symbols text-2xl">close</span>
</button> </button>
</div> </div>
@@ -31,7 +31,7 @@
<div class="flex flex-col p-4 h-full"> <div class="flex flex-col p-4 h-full">
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<button @click.stop.prevent="toggleToC" type="button" aria-label="Close table of contents" class="inline-flex opacity-80 hover:opacity-100"> <button @click.stop.prevent="toggleToC" type="button" aria-label="Close table of contents" class="inline-flex opacity-80 hover:opacity-100">
<span class="material-icons text-2xl">arrow_back</span> <span class="material-symbols text-2xl">arrow_back</span>
</button> </button>
<p class="text-lg font-semibold ml-2">{{ $strings.HeaderTableOfContents }}</p> <p class="text-lg font-semibold ml-2">{{ $strings.HeaderTableOfContents }}</p>
@@ -98,6 +98,12 @@
</div> </div>
<ui-range-input v-model="ereaderSettings.lineSpacing" :min="100" :max="300" :step="5" @input="settingsUpdated" /> <ui-range-input v-model="ereaderSettings.lineSpacing" :min="100" :max="300" :step="5" @input="settingsUpdated" />
</div> </div>
<div class="flex items-center mb-4">
<div class="w-40">
<p class="text-lg">{{ $strings.LabelFontBoldness }}:</p>
</div>
<ui-range-input v-model="ereaderSettings.textStroke" :min="0" :max="300" :step="5" @input="settingsUpdated" />
</div>
<div class="flex items-center"> <div class="flex items-center">
<div class="w-40"> <div class="w-40">
<p class="text-lg">{{ $strings.LabelLayout }}:</p> <p class="text-lg">{{ $strings.LabelLayout }}:</p>
@@ -130,7 +136,9 @@ export default {
font: 'serif', font: 'serif',
fontScale: 100, fontScale: 100,
lineSpacing: 115, lineSpacing: 115,
spread: 'auto' fontBoldness: 100,
spread: 'auto',
textStroke: 0
} }
} }
}, },
@@ -378,7 +386,12 @@ export default {
try { try {
const settings = localStorage.getItem('ereaderSettings') const settings = localStorage.getItem('ereaderSettings')
if (settings) { if (settings) {
this.ereaderSettings = JSON.parse(settings) const _ereaderSettings = JSON.parse(settings)
for (const key in this.ereaderSettings) {
if (_ereaderSettings[key] !== undefined) {
this.ereaderSettings[key] = _ereaderSettings[key]
}
}
this.settingsUpdated() this.settingsUpdated()
} }
} catch (error) { } catch (error) {
+3 -3
View File
@@ -11,7 +11,7 @@
</div> </div>
<div class="flex p-2"> <div class="flex p-2">
<span class="material-icons text-5xl py-1">show_chart</span> <span class="material-symbols text-5xl py-1">show_chart</span>
<div class="px-1"> <div class="px-1">
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalTime) }}</p> <p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalTime) }}</p>
<p class="text-xs md:text-sm text-white text-opacity-80">{{ useOverallHours ? $strings.LabelStatsOverallHours : $strings.LabelStatsOverallDays }}</p> <p class="text-xs md:text-sm text-white text-opacity-80">{{ useOverallHours ? $strings.LabelStatsOverallHours : $strings.LabelStatsOverallDays }}</p>
@@ -29,7 +29,7 @@
</div> </div>
<div class="flex p-2"> <div class="flex p-2">
<span class="material-icons-outlined text-5xl pt-1">insert_drive_file</span> <span class="material-symbols-outlined text-5xl pt-1">insert_drive_file</span>
<div class="px-1"> <div class="px-1">
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalSizeNum) }}</p> <p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalSizeNum) }}</p>
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelSize }} ({{ totalSizeMod }})</p> <p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelSize }} ({{ totalSizeMod }})</p>
@@ -37,7 +37,7 @@
</div> </div>
<div class="flex p-2"> <div class="flex p-2">
<span class="material-icons-outlined text-5xl pt-1">audio_file</span> <span class="material-symbols-outlined text-5xl pt-1">audio_file</span>
<div class="px-1"> <div class="px-1">
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(numAudioTracks) }}</p> <p class="text-4.5xl leading-none font-bold">{{ $formatNumber(numAudioTracks) }}</p>
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAudioTracks }}</p> <p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAudioTracks }}</p>
+1 -1
View File
@@ -73,7 +73,7 @@ export default {
const addIcon = (icon, color, fontSize, x, y) => { const addIcon = (icon, color, fontSize, x, y) => {
ctx.fillStyle = color ctx.fillStyle = color
ctx.font = `${fontSize} Material Icons Outlined` ctx.font = `${fontSize} Material Symbols Outlined`
ctx.fillText(icon, x, y) ctx.fillText(icon, x, y)
} }
+11 -16
View File
@@ -2,15 +2,14 @@
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-1 sm:p-4 mb-4"> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-1 sm:p-4 mb-4">
<!-- hack to get icon fonts loaded on init --> <!-- hack to get icon fonts loaded on init -->
<div class="h-0 w-0 overflow-hidden opacity-0"> <div class="h-0 w-0 overflow-hidden opacity-0">
<span class="material-icons-outlined">close</span> <span class="material-symbols-outlined">close</span>
<span class="abs-icons icon-audiobookshelf" /> <span class="abs-icons icon-audiobookshelf" />
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<p class="hidden md:block text-xl font-semibold">{{ $getString('HeaderYearReview', [yearInReviewYear]) }}</p> <p class="hidden md:block text-xl font-semibold">{{ $getString('HeaderYearReview', [yearInReviewYear]) }}</p>
<div class="hidden md:block flex-grow" /> <div class="hidden md:block flex-grow" />
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide : <ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide : $strings.LabelYearReviewShow }}</ui-btn>
$strings.LabelYearReviewShow }}</ui-btn>
</div> </div>
<!-- your year in review --> <!-- your year in review -->
@@ -20,29 +19,26 @@
<div class="flex items-center justify-center mb-2 max-w-[800px] mx-auto"> <div class="flex items-center justify-center mb-2 max-w-[800px] mx-auto">
<!-- previous button --> <!-- previous button -->
<ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--"> <ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--">
<span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span> <span class="material-symbols text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span> <span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
</ui-btn> </ui-btn>
<!-- share button --> <!-- share button -->
<ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview">{{ <ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview">{{ $strings.ButtonShare }} </ui-btn>
$strings.ButtonShare }}
</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelPersonalYearReview', [yearInReviewVariant + 1]) }} <p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelPersonalYearReview', [yearInReviewVariant + 1]) }}</p>
</p>
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p> <p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
<!-- refresh button --> <!-- refresh button -->
<ui-btn small :disabled="processingYearInReview" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReview"> <ui-btn small :disabled="processingYearInReview" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReview">
<span class="hidden sm:inline-block">{{ $strings.ButtonRefresh }}</span> <span class="hidden sm:inline-block">{{ $strings.ButtonRefresh }}</span>
<span class="material-icons sm:!hidden text-lg py-px">refresh</span> <span class="material-symbols sm:!hidden text-lg py-px">refresh</span>
</ui-btn> </ui-btn>
<!-- next button --> <!-- next button -->
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++"> <ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span> <span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span> <span class="material-symbols-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn> </ui-btn>
</div> </div>
<stats-year-in-review ref="yearInReview" :variant="yearInReviewVariant" :year="yearInReviewYear" :processing.sync="processingYearInReview" /> <stats-year-in-review ref="yearInReview" :variant="yearInReviewVariant" :year="yearInReviewYear" :processing.sync="processingYearInReview" />
@@ -59,12 +55,11 @@
<div class="flex items-center justify-center mb-2"> <div class="flex items-center justify-center mb-2">
<!-- previous button --> <!-- previous button -->
<ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--"> <ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--">
<span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span> <span class="material-symbols text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span> <span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
</ui-btn> </ui-btn>
<!-- share button --> <!-- share button -->
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer">{{ $strings.ButtonShare }} <ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer">{{ $strings.ButtonShare }} </ui-btn>
</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelServerYearReview', [yearInReviewServerVariant + 1]) }}</p> <p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelServerYearReview', [yearInReviewServerVariant + 1]) }}</p>
@@ -74,12 +69,12 @@
<!-- refresh button --> <!-- refresh button -->
<ui-btn small :disabled="processingYearInReviewServer" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReviewServer"> <ui-btn small :disabled="processingYearInReviewServer" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReviewServer">
<span class="hidden sm:inline-block">{{ $strings.ButtonRefresh }}</span> <span class="hidden sm:inline-block">{{ $strings.ButtonRefresh }}</span>
<span class="material-icons sm:!hidden text-lg py-px">refresh</span> <span class="material-symbols sm:!hidden text-lg py-px">refresh</span>
</ui-btn> </ui-btn>
<!-- next button --> <!-- next button -->
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++"> <ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span> <span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span> <span class="material-symbols-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn> </ui-btn>
</div> </div>
</div> </div>
@@ -64,7 +64,7 @@ export default {
const addIcon = (icon, color, fontSize, x, y) => { const addIcon = (icon, color, fontSize, x, y) => {
ctx.fillStyle = color ctx.fillStyle = color
ctx.font = `${fontSize} Material Icons Outlined` ctx.font = `${fontSize} Material Symbols Outlined`
ctx.fillText(icon, x, y) ctx.fillText(icon, x, y)
} }
+4 -4
View File
@@ -23,12 +23,12 @@
<div class="w-full flex flex-row items-center justify-center"> <div class="w-full flex flex-row items-center justify-center">
<ui-btn v-if="backup.serverVersion && backup.key" small color="primary" @click="applyBackup(backup)">{{ $strings.ButtonRestore }}</ui-btn> <ui-btn v-if="backup.serverVersion && backup.key" small color="primary" @click="applyBackup(backup)">{{ $strings.ButtonRestore }}</ui-btn>
<ui-tooltip v-else text="This backup was created with an old version of audiobookshelf no longer supported" direction="bottom" class="mx-2 flex items-center"> <ui-tooltip v-else text="This backup was created with an old version of audiobookshelf no longer supported" direction="bottom" class="mx-2 flex items-center">
<span class="material-icons-outlined text-2xl text-error">error_outline</span> <span class="material-symbols-outlined text-2xl text-error">error_outline</span>
</ui-tooltip> </ui-tooltip>
<button aria-label="Download Backup" class="inline-flex material-icons text-xl mx-1 mt-1 text-white/70 hover:text-white/100" @click.stop="downloadBackup(backup)">download</button> <button aria-label="Download Backup" class="inline-flex material-symbols text-xl mx-1 mt-1 text-white/70 hover:text-white/100" @click.stop="downloadBackup(backup)">download</button>
<button aria-label="Delete Backup" class="inline-flex material-icons text-xl mx-1 text-white/70 hover:text-error" @click="deleteBackupClick(backup)">delete</button> <button aria-label="Delete Backup" class="inline-flex material-symbols text-xl mx-1 text-white/70 hover:text-error" @click="deleteBackupClick(backup)">delete</button>
</div> </div>
</td> </td>
</tr> </tr>
@@ -171,7 +171,7 @@ export default {
this.$axios this.$axios
.$get('/api/backups') .$get('/api/backups')
.then((data) => { .then((data) => {
this.$emit('loaded', data.backupLocation) this.$emit('loaded', data)
this.setBackups(data.backups || []) this.setBackups(data.backups || [])
}) })
.catch((error) => { .catch((error) => {
+4 -4
View File
@@ -6,7 +6,7 @@
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="userCanUpdate" small :to="`/audiobook/${libraryItemId}/chapters`" color="primary" class="mr-2" @click="clickEditChapters">{{ $strings.ButtonEditChapters }}</ui-btn> <ui-btn v-if="userCanUpdate" small :to="`/audiobook/${libraryItemId}/chapters`" color="primary" class="mr-2" @click="clickEditChapters">{{ $strings.ButtonEditChapters }}</ui-btn>
<div v-if="!keepOpen" class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="expanded ? 'transform rotate-180' : ''"> <div v-if="!keepOpen" class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="expanded ? 'transform rotate-180' : ''">
<span class="material-icons text-4xl">expand_more</span> <span class="material-symbols text-4xl">expand_more</span>
</div> </div>
</div> </div>
<transition name="slide"> <transition name="slide">
@@ -15,7 +15,7 @@
<th class="text-left w-16"><span class="px-4">Id</span></th> <th class="text-left w-16"><span class="px-4">Id</span></th>
<th class="text-left">{{ $strings.LabelTitle }}</th> <th class="text-left">{{ $strings.LabelTitle }}</th>
<th class="text-center">{{ $strings.LabelStart }}</th> <th class="text-center">{{ $strings.LabelStart }}</th>
<th class="text-center">{{ $strings.LabelEnd }}</th> <th class="text-center">{{ $strings.LabelDuration }}</th>
</tr> </tr>
<tr v-for="chapter in chapters" :key="chapter.id"> <tr v-for="chapter in chapters" :key="chapter.id">
<td class="text-left"> <td class="text-left">
@@ -27,8 +27,8 @@
<td class="font-mono text-center hover:underline cursor-pointer" @click.stop="goToTimestamp(chapter.start)"> <td class="font-mono text-center hover:underline cursor-pointer" @click.stop="goToTimestamp(chapter.start)">
{{ $secondsToTimestamp(chapter.start) }} {{ $secondsToTimestamp(chapter.start) }}
</td> </td>
<td class="font-mono text-center hover:underline cursor-pointer" @click.stop="goToTimestamp(chapter.start)"> <td class="font-mono text-center">
{{ $secondsToTimestamp(chapter.end) }} {{ $secondsToTimestamp(Math.max(0, chapter.end - chapter.start)) }}
</td> </td>
</tr> </tr>
</table> </table>
@@ -15,13 +15,13 @@
</td> </td>
<td class="py-0"> <td class="py-0">
<div class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="removeProvider(provider)"> <div class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="removeProvider(provider)">
<button type="button" :aria-label="$strings.ButtonDelete" class="material-icons text-base">delete</button> <button type="button" :aria-label="$strings.ButtonDelete" class="material-symbols text-base">delete</button>
</div> </div>
</td> </td>
</tr> </tr>
</table> </table>
<div v-else-if="!processing" class="text-center py-8"> <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>
<div v-if="processing" class="absolute inset-0 h-full flex items-center justify-center bg-black/40 rounded-md"> <div v-if="processing" class="absolute inset-0 h-full flex items-center justify-center bg-black/40 rounded-md">
+2 -2
View File
@@ -8,7 +8,7 @@
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="toggleFullPath">{{ $strings.ButtonFullPath }}</ui-btn> <ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="toggleFullPath">{{ $strings.ButtonFullPath }}</ui-btn>
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''"> <div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''">
<span class="material-icons text-4xl">expand_more</span> <span class="material-symbols text-4xl">expand_more</span>
</div> </div>
</div> </div>
<transition name="slide"> <transition name="slide">
@@ -18,7 +18,7 @@
<th class="text-left px-4">{{ $strings.LabelPath }}</th> <th class="text-left px-4">{{ $strings.LabelPath }}</th>
<th class="text-left w-24 min-w-24">{{ $strings.LabelSize }}</th> <th class="text-left w-24 min-w-24">{{ $strings.LabelSize }}</th>
<th class="text-left px-4 w-24"> <th class="text-left px-4 w-24">
{{ $strings.LabelRead }} <ui-tooltip :text="$strings.LabelReadEbookWithoutProgress" direction="top" class="inline-block"><span class="material-icons-outlined text-sm align-middle">info</span></ui-tooltip> {{ $strings.LabelRead }} <ui-tooltip :text="$strings.LabelReadEbookWithoutProgress" direction="top" class="inline-block"><span class="material-symbols-outlined text-sm align-middle">info</span></ui-tooltip>
</th> </th>
<th v-if="showMoreColumn" class="text-center w-16"></th> <th v-if="showMoreColumn" class="text-center w-16"></th>
</tr> </tr>
@@ -1,7 +1,7 @@
<template> <template>
<tr> <tr>
<td class="px-4"> <td class="px-4">
{{ showFullPath ? file.metadata.path : file.metadata.relPath }} <ui-tooltip :text="$strings.LabelPrimaryEbook" class="inline-block"><span v-if="isPrimary" class="material-icons-outlined text-success align-text-bottom">check_circle</span></ui-tooltip> {{ showFullPath ? file.metadata.path : file.metadata.relPath }} <ui-tooltip :text="$strings.LabelPrimaryEbook" class="inline-block"><span v-if="isPrimary" class="material-symbols-outlined text-success align-text-bottom">check_circle</span></ui-tooltip>
</td> </td>
<td> <td>
{{ $bytesPretty(file.metadata.size) }} {{ $bytesPretty(file.metadata.size) }}
@@ -8,7 +8,7 @@
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="toggleFullPath">{{ $strings.ButtonFullPath }}</ui-btn> <ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="toggleFullPath">{{ $strings.ButtonFullPath }}</ui-btn>
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''"> <div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''">
<span class="material-icons text-4xl">expand_more</span> <span class="material-symbols text-4xl">expand_more</span>
</div> </div>
</div> </div>
<transition name="slide"> <transition name="slide">
+1 -1
View File
@@ -11,7 +11,7 @@
<ui-btn small color="primary">{{ $strings.ButtonManageTracks }}</ui-btn> <ui-btn small color="primary">{{ $strings.ButtonManageTracks }}</ui-btn>
</nuxt-link> </nuxt-link>
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''"> <div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showTracks ? 'transform rotate-180' : ''">
<span class="material-icons text-4xl">expand_more</span> <span class="material-symbols text-4xl">expand_more</span>
</div> </div>
</div> </div>
<transition name="slide"> <transition name="slide">
@@ -5,7 +5,7 @@
<span class="bg-black-400 rounded-xl py-0.5 px-2 text-sm font-mono">{{ files.length }}</span> <span class="bg-black-400 rounded-xl py-0.5 px-2 text-sm font-mono">{{ files.length }}</span>
<div class="flex-grow" /> <div class="flex-grow" />
<div class="cursor-pointer h-9 w-9 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="expand ? 'transform rotate-180' : ''"> <div class="cursor-pointer h-9 w-9 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="expand ? 'transform rotate-180' : ''">
<span class="material-icons text-3xl">expand_more</span> <span class="material-symbols text-3xl">expand_more</span>
</div> </div>
</div> </div>
<transition name="slide"> <transition name="slide">
+2 -2
View File
@@ -42,10 +42,10 @@
<div class="w-full flex justify-left"> <div class="w-full flex justify-left">
<!-- Dont show edit for non-root users --> <!-- Dont show edit for non-root users -->
<div v-if="user.type !== 'root' || userIsRoot" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-opacity-100 cursor-pointer" @click.stop="editUser(user)"> <div v-if="user.type !== 'root' || userIsRoot" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-opacity-100 cursor-pointer" @click.stop="editUser(user)">
<button type="button" :aria-label="$getString('ButtonUserEdit', [user.username])" class="material-icons text-base">edit</button> <button type="button" :aria-label="$getString('ButtonUserEdit', [user.username])" class="material-symbols text-base">edit</button>
</div> </div>
<div v-show="user.type !== 'root'" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="deleteUserClick(user)"> <div v-show="user.type !== 'root'" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="deleteUserClick(user)">
<button type="button" :aria-label="$getString('ButtonUserDelete', [user.username])" class="material-icons text-base">delete</button> <button type="button" :aria-label="$getString('ButtonUserDelete', [user.username])" class="material-symbols text-base">delete</button>
</div> </div>
</div> </div>
</td> </td>
@@ -3,7 +3,7 @@
<div v-if="book" class="flex h-18 md:h-[5.5rem]"> <div v-if="book" class="flex h-18 md:h-[5.5rem]">
<div class="w-10 min-w-10 md:w-16 md:max-w-16 h-full"> <div class="w-10 min-w-10 md:w-16 md:max-w-16 h-full">
<div class="flex h-full items-center justify-center"> <div class="flex h-full items-center justify-center">
<span class="material-icons drag-handle text-lg md:text-xl">menu</span> <span class="material-symbols drag-handle text-lg md:text-xl">menu</span>
</div> </div>
</div> </div>
<div class="h-full flex items-center" :style="{ width: coverWidth + 'px', minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }"> <div class="h-full flex items-center" :style="{ width: coverWidth + 'px', minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }">
@@ -11,7 +11,7 @@
<covers-book-cover :library-item="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-book-cover :library-item="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div class="absolute top-0 left-0 flex items-center justify-center bg-black bg-opacity-50 h-full w-full z-10" v-show="isHovering && showPlayBtn"> <div class="absolute top-0 left-0 flex items-center justify-center bg-black bg-opacity-50 h-full w-full z-10" v-show="isHovering && showPlayBtn">
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick"> <div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
<span class="material-icons text-2xl">play_arrow</span> <span class="material-symbols fill text-2xl">play_arrow</span>
</div> </div>
</div> </div>
</div> </div>
@@ -16,14 +16,14 @@
<ui-context-menu-dropdown v-if="!isScanning && !isDeleting" :items="contextMenuItems" :icon-class="`text-1.5xl text-gray-${isHovering ? 50 : 400}`" class="!hidden md:!block" @action="contextMenuAction" /> <ui-context-menu-dropdown v-if="!isScanning && !isDeleting" :items="contextMenuItems" :icon-class="`text-1.5xl text-gray-${isHovering ? 50 : 400}`" class="!hidden md:!block" @action="contextMenuAction" />
<!-- Mobile context menu icon --> <!-- Mobile context menu icon -->
<span v-if="!isScanning && !isDeleting" class="!block md:!hidden material-icons text-xl text-gray-300 ml-3 cursor-pointer" @click.stop="showMenu">more_vert</span> <span v-if="!isScanning && !isDeleting" class="!block md:!hidden material-symbols text-xl text-gray-300 ml-3 cursor-pointer" @click.stop="showMenu">more_vert</span>
<div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin"> <div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin">
<svg viewBox="0 0 24 24" class="w-6 h-6"> <svg viewBox="0 0 24 24" class="w-6 h-6">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" /> <path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg> </svg>
</div> </div>
<span class="material-icons drag-handle text-xl text-gray-400 hover:text-gray-50 ml-2 md:ml-4">reorder</span> <span class="material-symbols drag-handle text-xl text-gray-400 hover:text-gray-50 ml-2 md:ml-4">reorder</span>
<!-- For mobile --> <!-- For mobile -->
<modals-dialog v-model="showMobileMenu" :title="menuTitle" :items="contextMenuItems" @action="contextMenuAction" /> <modals-dialog v-model="showMobileMenu" :title="menuTitle" :items="contextMenuItems" @action="contextMenuAction" />
@@ -3,14 +3,14 @@
<div v-if="item" class="flex h-16 md:h-20"> <div v-if="item" class="flex h-16 md:h-20">
<div class="w-10 min-w-10 md:w-16 md:max-w-16 h-full"> <div class="w-10 min-w-10 md:w-16 md:max-w-16 h-full">
<div class="flex h-full items-center justify-center"> <div class="flex h-full items-center justify-center">
<span class="material-icons drag-handle text-lg md:text-xl">menu</span> <span class="material-symbols drag-handle text-lg md:text-xl">menu</span>
</div> </div>
</div> </div>
<div class="h-full relative flex items-center" :style="{ width: coverWidth + 'px', minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }"> <div class="h-full relative flex items-center" :style="{ width: coverWidth + 'px', minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }">
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn"> <div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn">
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick"> <div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
<span class="material-icons text-2xl">play_arrow</span> <span class="material-symbols fill text-2xl">play_arrow</span>
</div> </div>
</div> </div>
</div> </div>

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