Compare commits

...

1113 Commits

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

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

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

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

I noticed that in a few rare cases the App wouldn't properly display
cover images. It turns out that due the file names containing a `#`, the
file path got corrupted, causing Audiobookshelf to return a 403.
2024-02-29 17:56:55 +01:00
advplyr
d2b006b909 Update:Windows binary manager to install ffmpeg/ffprobe 5.1 #1098 2024-02-28 16:16:44 -06:00
mfcar
f4a19e48ad Update login page 2024-02-28 19:21:11 +00:00
advplyr
38f12f4795 Fix:Podcast schedule max new episodes to download setting to 0 and fix input blurs #2680 2024-02-27 17:17:33 -06:00
advplyr
7a4f4b1586 Merge pull request #2676 from pmangro/2.8.0.a-PT-BT
[PT-BR] Updated strings
2024-02-27 15:59:09 -06:00
pmangro
20ec54e085 [PT-BR] Updated strings 2024-02-27 14:31:21 -03:00
Teekeks
655bebfec4 feat: Expanded filter to include "has no ebook" and "has no supplementary ebooks" options 2024-02-27 18:30:05 +01:00
advplyr
71e1abd263 Merge pull request #2673 from Machou/master
Update fr.json
2024-02-27 08:56:03 -06:00
Machou
72172dcb33 Update fr.json 2024-02-27 08:56:31 +01:00
advplyr
def2988e12 Update:Passport openid-client request timeout set to 10s (default was 3.5s) #2669 2024-02-26 17:20:11 -06:00
mikiher
b47793c365 Add cache control header for timestamped cover image requests 2024-02-26 14:00:25 +02:00
advplyr
3a99cc56b7 Update:Debian packager script to use xz compression instead of zstd 2024-02-25 12:56:04 -06:00
advplyr
24c35dede5 Merge pull request #2659 from mikiher/quick-match-dup-authors
Fix dup author addition logic
2024-02-25 08:12:05 -06:00
advplyr
8c4400dff1 Merge pull request #2657 from JBlond/master
Update de strings
2024-02-25 08:08:17 -06:00
advplyr
af8dffaa33 Merge pull request #2573 from mikiher/fix-match-update
Merge cover and media update in Match.vue into a single /media API call
2024-02-25 08:07:51 -06:00
advplyr
4a36a3c8e6 Merge branch 'master' into fix-match-update 2024-02-25 08:00:29 -06:00
mikiher
e6735e042e Fix dup author addition logic 2024-02-25 09:01:26 +02:00
JBlond
c799379a54 Update de strings 2024-02-24 20:23:51 +01:00
advplyr
d8b9f08e5a Merge pull request #2641 from Teekeks/year-review-translation
feat(i18n): made "Year in Review" UI elements translatable and added german translation for those
2024-02-23 17:01:43 -06:00
advplyr
608b25de45 Map en-us translations 2024-02-23 16:59:46 -06:00
advplyr
2db8869908 Merge branch 'master' into year-review-translation 2024-02-23 16:56:53 -06:00
advplyr
9500737bbe Merge pull request #2644 from RasmusKoit/estonian-translations
Adds estonian translation
2024-02-23 16:55:41 -06:00
advplyr
def2b6425b Update:Username and password inputs on login page trim whitespace #2628 2024-02-22 16:30:41 -06:00
mikiher
0f4b11494e Merge branch 'advplyr:master' into fix-match-update 2024-02-22 12:20:49 +02:00
rasmuskoit
46448ce1e9 Adds estonian translation 2024-02-22 09:46:09 +02:00
advplyr
fbe12b393f Merge pull request #2639 from pmangro/2.8.0.a-PT-BT
[PT-BR] Terminology adjustments and typo fixes
2024-02-21 18:29:29 -06:00
advplyr
ccf59b2c1a Merge pull request #2638 from DownloadableFox/master
Updated and fixed Spanish translation
2024-02-21 18:28:28 -06:00
Lena During
d7af3b7788 Merge branch 'master' into year-review-translation 2024-02-22 00:39:49 +01:00
Teekeks
682aca0b2a feat(i18n): made "Year in Review" UI elements translatable and added german translation for those 2024-02-22 00:36:43 +01:00
advplyr
3328ffe1b9 Merge pull request #2636 from megamegax/master
feat(i18n): add Hungarian translation
2024-02-21 16:07:36 -06:00
DownloadableFox
c07b7840e2 Updated and fixed Spanish translation 2024-02-20 21:14:55 -05:00
pmangro
9f848b2c64 Update pt-br.json - adjust terminology and typo 2024-02-20 17:28:37 -03:00
pmangro
3d66ec0761 PT-BR size adjustment 2024-02-20 11:42:48 -03:00
advplyr
f50920be69 Merge pull request #2635 from lonezel/lonezel-updated-german-translation
Update de.json
2024-02-20 08:37:48 -06:00
advplyr
d31add9d5a Merge pull request #2634 from pmangro/2.8.0-PT-BR
Updated pt-br string
2024-02-20 08:36:33 -06:00
Hunyady Mihály
a4dcb4f92e feat(i18n): add Hungarian translation 2024-02-20 12:25:43 +01:00
lonezel
2c589c1dbd Update de.json
Translated new strings to german - this is my first ever commit on Github, if I need to change something in my workflow let me know!
2024-02-20 10:19:48 +01:00
pmangro
60ea386c6d Updated pt-br string 2024-02-20 05:57:52 -03:00
advplyr
24be1a0ec5 Merge pull request #2629 from burghy86/patch-13
Update it.json
2024-02-19 08:56:39 -06:00
burghy86
e71a14756b Update it.json
new string update
2024-02-19 15:53:12 +01:00
advplyr
85fecbd1b9 Version bump v2.8.0 2024-02-18 16:43:16 -06:00
advplyr
335d39f317 Update guide link for custom metadata providers 2024-02-18 16:22:10 -06:00
advplyr
973a18d346 Update:Added button to user edit modal for unlinking user from openid #2587 2024-02-18 15:38:45 -06:00
advplyr
a43b93d796 Fix:Clear library filter data cache when library item is updated #2597 2024-02-18 14:58:46 -06:00
advplyr
acf75abdf1 Update:Match author use closest name match by levenshtein distance #2624 2024-02-18 13:06:51 -06:00
advplyr
58598bfcf2 Update:Clamp author description to 4 lines and add more button #2614 2024-02-18 11:32:24 -06:00
advplyr
7a570439db Update:Clamp item descriptions to 4 lines and show more button #2614 2024-02-18 11:24:36 -06:00
advplyr
6e769d1c20 Merge pull request #2554 from mikiher/ffmpeg-latest
Modify BinaryManager to download version 6.1
2024-02-17 17:42:24 -06:00
advplyr
d9e7f5d133 Update BinaryManager JSDocs, move validVersions to required binary objects 2024-02-17 17:40:33 -06:00
advplyr
a119b05d85 Merge branch 'master' into ffmpeg-latest 2024-02-17 17:05:51 -06:00
advplyr
7bf7b6bcf9 Merge pull request #2553 from Sapd/sso
OpenID: Implement Logout + Fix state + Fix URL Regex
2024-02-17 17:03:12 -06:00
advplyr
e47ea98cdd Fix:Disconnect from socket on logout, remove unnecessary logout function 2024-02-17 16:58:49 -06:00
advplyr
bf66e13377 Update jsdocs 2024-02-17 16:06:25 -06:00
advplyr
d7aba5629e Remove old login rate limiter 2024-02-17 15:29:06 -06:00
advplyr
a5c200ac79 Merge branch 'master' into sso 2024-02-17 14:15:41 -06:00
advplyr
fdc1fc1b2a Merge pull request #2491 from liaochuan/liaocl
Add Podcast Search Region
2024-02-17 13:32:23 -06:00
advplyr
42a4b762bd Fix translations sort order and pt-br translations 2024-02-17 13:30:30 -06:00
advplyr
180c328ed1 Update jsdocs for search podcasts 2024-02-17 13:24:49 -06:00
advplyr
2ec52a7a45 Merge branch 'master' into liaocl 2024-02-17 12:56:05 -06:00
advplyr
aacf37e32b Fix:Year in Review crashing when listening session has a null genre #2623 2024-02-16 16:16:55 -06:00
advplyr
52323b7eb5 Update:Podcast episode download show ffmpeg progress and print full debug log dump on error 2024-02-16 16:05:02 -06:00
advplyr
5b5613a762 Merge pull request #2617 from ipcintron/pwa
Update pwa icon to use iOS icon
2024-02-16 13:44:57 -06:00
ipcintron
de6df0c029 Forgot to modify nuxt.config.js 2024-02-16 12:29:52 -06:00
ipcintron
e180b3c171 Renamed the icon to make it clear it is being used for iOS 2024-02-16 12:27:10 -06:00
ipcintron
1364b79cbf Put the icon in the link array for iOS only 2024-02-16 10:10:09 -06:00
ipcintron
ef96f3102f Merge branch 'advplyr:master' into pwa 2024-02-16 09:51:10 -06:00
advplyr
06ce3b08f7 Merge pull request #2619 from ipcintron/theme
PWA (iOS) theme color fix
2024-02-16 09:31:11 -06:00
advplyr
a13217dddf Fix:Initial language code setting eventBus not yet defined 2024-02-16 09:12:47 -06:00
advplyr
ce528d4012 Merge pull request #2620 from pmangro/pmangro-patch-1
PT-BR Strings
2024-02-16 09:05:31 -06:00
pmangro
89207b6d2a Update i18n.js
Added PT-BR
2024-02-16 11:57:54 -03:00
pmangro
e9591caf81 Spelling 2024-02-16 11:56:31 -03:00
pmangro
24f1aae6b6 Update pt-br.json
Strings 541-766
2024-02-16 11:44:25 -03:00
ipcintron
04fbc9a22b change theme color 2024-02-16 02:15:27 -06:00
ipcintron
14e31d5690 update pwa icon 2024-02-16 01:32:04 -06:00
advplyr
a9e9808183 Fix:trim whitespace from asin for chapter lookup #2605 2024-02-15 17:05:48 -06:00
advplyr
af7cb2432b Update:Log uncaught exceptions to crash_logs.txt #706 & cleanup logger 2024-02-15 16:46:19 -06:00
pmangro
e0c1364916 Create pt-br.json
540 linhas iniciais
2024-02-15 19:08:05 -03:00
advplyr
04d16fc535 Fix:Audio player buttons to use button el and add aria-label translations #2599 2024-02-14 18:28:19 -06:00
advplyr
44135b3fed Rename StreamContainer to MediaPlayerContainer 2024-02-14 18:12:35 -06:00
advplyr
6111e8f0da Fix:Global search menu for mobile 2024-02-13 18:45:01 -06:00
advplyr
4e3e7b10ce Update:Custom metadata provider adapter sends mediaType as a query param 2024-02-12 17:12:49 -06:00
advplyr
ce7f81d676 Merge pull request #2486 from FlyinPancake/dewyer/add-custom-metadata-provider
[Feature] Add support for custom metadata providers through a REST API
2024-02-11 17:04:55 -06:00
advplyr
0cf2f8885e Add custom metadata provider controller, update model, move to item metadata utils 2024-02-11 16:48:16 -06:00
advplyr
ddf4b2646c Merge branch 'master' into dewyer/add-custom-metadata-provider 2024-02-11 09:10:29 -06:00
advplyr
fe1e0749a2 Update:Listening sessions table rows per page text wrapping 2024-02-08 19:12:59 -06:00
advplyr
2093468c92 Fix:Local playback sessions not persisting the last updatedAt value 2024-02-08 19:12:35 -06:00
mikiher
19af7454f2 Force Update LibraryItem model updatedAt refresh (fixes #2593) 2024-02-07 20:57:50 +02:00
KeyboardHammer
d24427aad8 fix property 2024-02-03 22:04:40 -06:00
KeyboardHammer
e2bb0cfb7c add sorting to author page 2024-02-03 21:48:35 -06:00
mikiher
2ebdb44826 Merge cover and media update in Match.vue into a single /media API call 2024-02-01 12:03:12 +02:00
advplyr
432e25565e Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-01-31 17:23:20 -06:00
advplyr
ebe511404a Remove updateMedia endpoint cover cache purge 2024-01-31 17:23:16 -06:00
advplyr
e0a79fb86c Merge pull request #2570 from Weldawadyathink/main
Return png from AudiobookCovers.com
2024-01-30 17:02:14 -06:00
Spenser Bushey
295ca3d9a2 Return png from AudiobookCovers.com
Changes AudiobookCovers.com provider to return the full size png file from the server. The original file url has the incorrect content-type header set, which caused issues downloading new cover images.
2024-01-30 09:15:50 -08:00
advplyr
dbad8bdb96 Merge pull request #2567 from ipcintron/lockscreen_cover
added raw cover on lockscreen for iOS
2024-01-29 15:21:44 -06:00
ipcintron
8c703859a0 added raw cover 2024-01-29 13:18:58 -06:00
advplyr
bedb260b00 Update:Epub ereader allow scripted content 2024-01-28 16:02:02 -06:00
advplyr
b49592301f Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-01-28 16:00:32 -06:00
advplyr
c6c67078b8 Update:PWA manifest icon to include PNG #2520 2024-01-28 16:00:26 -06:00
advplyr
9e45ad10f1 Merge pull request #2564 from Teekeks/patch-1
Update de.json
2024-01-28 10:10:13 -06:00
Lena During
24da859975 Update de.json 2024-01-28 16:06:07 +01:00
advplyr
0b6a8a9641 Update:Remove 64x64 app icon from PWA manifest so that only the SVG is available #2520 2024-01-27 10:50:44 -06:00
advplyr
e43c4f082e Fix:Rich text editor labels and add translations 2024-01-26 17:22:37 -06:00
advplyr
0b334cf957 Add:Authentication setting to show a custom message on login #2552 2024-01-26 17:08:23 -06:00
advplyr
ae387ab397 Merge pull request #2559 from bloodscript/master
German localization optimization
2024-01-26 16:28:02 -06:00
bloodscript
056e62dce8 added plural to metadata order hint 2024-01-26 23:15:27 +01:00
bloodscript
47999214bd corrected misspelling of adress 2024-01-26 23:13:11 +01:00
bloodscript
68473ee345 added missing metadata translation 2024-01-26 23:11:38 +01:00
bloodscript
455f27d443 mainly changed usage formular wording for you to the more commonly used, also corrected some misspellings 2024-01-26 23:09:54 +01:00
bloodscript
ba996c3b55 translation wasnt accurate verschluesselung means encryption 2024-01-26 20:12:37 +01:00
mikiher
d43a1109c8 Modify BinaryManager to download version 6.1 and remove old dowloaded versions 2024-01-25 17:51:06 +02:00
Denis Arnst
c3ba7daa16 Auth: Remove is_rest cookie 2024-01-25 16:05:41 +01:00
Denis Arnst
82048cd4f3 SSO: Also save openid_id_token longer 2024-01-25 15:13:56 +01:00
Denis Arnst
71b0a5cc81 SSO Settings: Fix Redirect URL Regex
Forgot to include subpaths
2024-01-25 11:49:10 +01:00
Denis Arnst
edb5ff1e33 SSO: Remove pick function 2024-01-25 11:44:20 +01:00
Denis Arnst
d4ed6348ee Auth: Store auth_method longer
Its not unrealistic that someone keeps being logged into the app for more than a year
if not stored longer logout process might not work anymore
2024-01-25 11:20:44 +01:00
Denis Arnst
f12ac685e8 /auth/openid: Restructure
- Distingush more explictly between mobile and web flow and simplify logic
- Allow state parameter to be passed in mobile flow
- Additional checks for correct parameters
- Remove unused id_token code
- Enforce S256 and don't allow plain PKCE
2024-01-25 11:13:34 +01:00
advplyr
b9ec4068ee Merge pull request #2510 from Torstein-Eide/patch-1
README, add HAproxy example
2024-01-24 16:55:32 -06:00
advplyr
02aabb8f97 Update readme.md 2024-01-24 16:55:21 -06:00
advplyr
dcec2154c0 Update readme.md 2024-01-24 16:55:17 -06:00
advplyr
bbc1d20396 Update readme.md 2024-01-24 16:55:12 -06:00
advplyr
e682213681 Update readme.md 2024-01-24 16:55:06 -06:00
advplyr
0153c0faae Update readme.md 2024-01-24 16:54:54 -06:00
Denis Arnst
87ebf4722b OpenID/SSO: Implement Logout functionality 2024-01-24 22:47:50 +01:00
advplyr
3906dca04e Update:RSS feeds only use chapter titles for episode titles if all audio tracks match chapter times #2543 2024-01-23 17:51:34 -06:00
advplyr
399ba314a3 Update github issue template to include Windows Tray App 2024-01-23 15:48:58 -06:00
advplyr
19e1803633 Remove unused import 2024-01-22 17:56:41 -06:00
advplyr
71048c7ff0 Remove support for Docker armv7 builds 2024-01-20 16:39:43 -06:00
advplyr
7f350279fa Update to node20
- updates many dependencies
- removes @nuxtjs/tailwindcss and postcss8
- pkg targets are using node18 until node20 targets are available
2024-01-19 17:54:41 -06:00
advplyr
90f4833c9e Version bump v2.7.2 2024-01-16 17:26:50 -06:00
advplyr
c0cb3a176f Update:Hide audiobook tools for windows install, remove debian folder picker alert 2024-01-16 17:19:45 -06:00
advplyr
7b0fa48e2e Update jsdocs for expanded library items 2024-01-16 16:31:16 -06:00
advplyr
b51853b3df Update:Use raw cover art for media session #2514 2024-01-15 08:34:12 -06:00
advplyr
f5545cd3f4 Add:Scanner extracts cover from comic files #1837 and ComicInfo.xml parser 2024-01-14 17:51:26 -06:00
advplyr
e76af3bfc2 Fix comic page menu dropdown highlight correct page 2024-01-13 16:41:13 -06:00
FlyinPancake
6ef4944d89 Merge branch 'advplyr:master' into dewyer/add-custom-metadata-provider 2024-01-13 01:08:23 +01:00
advplyr
850397e4c1 Add:Playlist button to podcast episodes on latest page #2455 2024-01-12 17:58:07 -06:00
FlyinPancake
3b531144cf implemented suggestions, extended CMPs with series 2024-01-12 21:45:03 +01:00
Torstein Eide
6ca684603c Fix typos 2024-01-12 14:35:30 +01:00
Torstein Eide
cf85d66b2f Add example for HAproxy 2024-01-12 14:26:32 +01:00
advplyr
e8fa029df7 Fix:Specific podcast rss feed cannot be fetched due to accept header #2446 2024-01-10 08:12:26 -06:00
advplyr
1a361c91f1 Merge pull request #2506 from FreedomBen/remove-dev-logs
Change `Logger.dev` calls to `Logger.debug`
2024-01-09 16:47:21 -06:00
Benjamin Porter
4a76059608 Change Logger.dev calls to Logger.debug
Logger.dev is kind of in a weird spot where it doesn't fit into the
standard log level.  It is called directly by some code and it only
checks whether a property is set (which comes from an env var) before
deciding to print out.

This standardizes on `debug` by changing the dev calls to debug. Also
removes the now unused code.
2024-01-09 15:24:23 -07:00
advplyr
da25eff5c1 Fix:Parse series sequence from OPF in cases where series_index is not directly underneath series meta #2505 2024-01-08 18:21:15 -06:00
advplyr
69e23ef9f2 Add:Epub metadata parser and cover extractor #1479 2024-01-07 17:51:07 -06:00
advplyr
48a08e9659 Merge pull request #2503 from Machou/master
Little Missed Update fr.json
2024-01-07 12:42:09 -06:00
Machou
4608f91ec6 Update fr.json 2024-01-07 02:41:16 +01:00
advplyr
e88c1fa329 Update:Show tooltip for library item card titles that are truncated #2451
- Refactored tooltip so that they dont overflow the window
2024-01-06 15:54:48 -06:00
advplyr
935e545caa Update readme for iOS beta full 2024-01-06 14:13:39 -06:00
advplyr
a426da534c Fix:Export OPML not escaping characters #2487 2024-01-05 14:45:25 -06:00
advplyr
eaf6bf29cc Fix:Improve performance for podcast rss feed episodes modal for large rss feeds 2024-01-05 14:39:25 -06:00
advplyr
a0eb6bd3dc Fix:Refresh podcast episode table when new episodes are downloaded 2024-01-05 14:38:29 -06:00
advplyr
fbe228a4f8 Merge pull request #2485 from Machou/patch-1
Update fr.json
2024-01-05 10:40:29 -06:00
advplyr
578a59063f Update discord invite link 2024-01-05 09:24:18 -06:00
mozhu
81020ff34d 播客搜索地区配置增加默认参数 2024-01-05 15:50:20 +08:00
mozhu
fea78898a5 移动播客搜索地区配置到媒体库配置 2024-01-05 14:45:35 +08:00
Machou
ffa7cc0d22 Update fr.json 2024-01-05 07:19:07 +01:00
advplyr
4f9969cd9b Merge pull request #2488 from FreedomBen/add-init-system-to-docker
Add tini as PID 1 handler in container image
2024-01-04 13:50:54 -06:00
mozhu
1be34564f2 数据绑定错误修改 2024-01-04 15:00:40 +08:00
mozhu
56eff7a236 增加播客搜索地区配置 2024-01-04 11:52:45 +08:00
advplyr
9f909b0d85 Update:Library folder browser to also work for debian and windows 2024-01-03 16:23:17 -06:00
Benjamin Porter
baa65b8155 Add tini as PID 1 handler in container image
This PR adds `tini` to the container image and uses it as PID 1 when
starting the container.  This ensures that proper PID 1 signal-handling
is implemented and passed to the underlying node.js process, thereby
ensuring that the ABS process has a chance to receive and handle signals
other than `SIGKILL`, such as the important `SIGINT`.

This is somewhat related to #2445 . Without this, the signal handled by
2445 won't be received when running in a container.

Some background:

In linux, PID 1 has special duties involving signal handling that are
different than other processes.  Node doesn't properly handle these
signals, which can lead to a number of problems ranging from annoying to
disruptive.  PID 1 also has reaping duties that can lead to resource
exhaustion if not properly handled.

For example, the container ignores `SIGINT` (Ctrl+C) as well as `docker stop`,
which can be annoying in development as you have to kill or wait for
the timeout to be reached.  In a production environment (such as Kubernetes)
this can lead to signal escalation and unnecessarily adds delays to
deployments and restarts as K8s has to wait for the timeout to be reached
before sending `SIGKILL`.

At best this is annoying and unnecessarily adds
delays.  At worst this can lead to file/data corruption as the process
doesn't get a chance to clean anything up when it is sent `SIGKILL`.
Without a proper PID 1 to forward signals, only SIGKILL can be used to
terminate the running process.
2024-01-03 13:55:43 -07:00
Barnabas Ratki
12c6a1baa0 Fix log messages 2024-01-03 20:42:35 +01:00
Barnabas Ratki
5ea423072b Small fixes 2024-01-03 20:40:36 +01:00
Barnabas Ratki
08a41e37b4 Add specification 2024-01-03 20:27:42 +01:00
Barnabas Ratki
8027c4a06f Added support for custom metadata providers
WiP but already open to feedback
2024-01-03 20:25:34 +01:00
Machou
a1e321b153 Update fr.json 2024-01-03 20:16:21 +01:00
advplyr
8c6a2ac5dd Merge pull request #2391 from mikiher/binary-manager
Add a binary manager that finds ffmpeg and ffprobe and installs them if not found
2024-01-02 14:25:56 -06:00
advplyr
b489bf9236 Restrict binary manager to Windows or development 2024-01-02 14:24:59 -06:00
advplyr
aa63aa6cf3 Merge branch 'master' into binary-manager 2024-01-02 14:16:27 -06:00
advplyr
9a2b93fb37 Version bump v2.7.1 2023-12-31 15:37:23 -06:00
advplyr
e8ea7efc98 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2023-12-31 15:36:37 -06:00
advplyr
81a76593da Fix:Merging chapters from multiple audio files with the same chapter titles #2461 2023-12-31 15:35:17 -06:00
advplyr
5336864f7d Merge pull request #2465 from thevoltagesource/getFileMtimeMs_Unhandled_Exception
Add try/catch to fileUtils.getFileMtimeMs
2023-12-31 15:34:43 -06:00
advplyr
d38058e1d2 Fix:Podcast episode time remaining shown on button showing 0 seconds after toggling mark as finished 2023-12-31 15:32:44 -06:00
advplyr
fececd4651 Fix:Playlists navigation button not showing on mobile screen #2469 2023-12-31 15:09:35 -06:00
advplyr
021adf3104 Update:Podcast episode table is lazy loaded #1549 2023-12-31 14:51:01 -06:00
advplyr
160c83df4a Update:podcastEpisodes table index added for createdAt column #2073 #2075 2023-12-30 16:14:14 -06:00
advplyr
456bb87a00 Update:Find one library item endpoint sequelize query split into two queries to improve performance #2073 #2075 2023-12-30 12:12:48 -06:00
advplyr
707451309c Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2023-12-29 17:05:40 -06:00
advplyr
269676e8a5 Update:CORS for /cover API endpoint for use in canvas in the mobile apps 2023-12-29 17:05:35 -06:00
Jacob Southard
e4effebc19 Add try/catch to fileutils.getFileMtimeMs 2023-12-29 10:04:59 -06:00
advplyr
fbbceddba8 Merge pull request #2454 from mikiher/socket-authority-close
Add SocketAuthority.close()
2023-12-28 16:32:40 -06:00
advplyr
9a634e0de5 Add JS docs for server stop 2023-12-28 16:32:21 -06:00
mikiher
21d0d43edc Add SocketAuthority.close() 2023-12-27 15:33:33 +02:00
mikiher
3051b963ef Merge branch 'advplyr:master' into binary-manager 2023-12-27 06:44:22 +02:00
advplyr
0d0bdce337 Fix:Fetch RSS feed request accept header #2446 2023-12-25 13:15:55 -06:00
advplyr
bdb5dc8c28 Merge pull request #2445 from mikiher/sigint-handler
Add a SIGINT handler for proper server shutdown
2023-12-25 12:51:22 -06:00
mikiher
209847d98a Add a SIGINT handler for proper server shutdown 2023-12-25 09:25:04 +02:00
advplyr
14f42e15d1 Fix:Book scanner update book series sequence if changed 2023-12-24 11:53:57 -06:00
advplyr
7402e4811d Merge pull request #2444 from jedrus2000/opf-multiple-series-support
Add: OPF file supports multiple series as sequence of : calibre:series and calibre:series_index; including tests
2023-12-24 11:42:06 -06:00
advplyr
6de0465b86 Update opf parser to ignore series with empty content and add tests 2023-12-24 11:41:27 -06:00
Andrzej Bargański
cd7c4baaaf Add: OPF file supports multiple series as sequence of : calibre:series and calibre:series_index; including tests 2023-12-24 00:43:42 +01:00
advplyr
a2db81bf7d Fix share button for year in review short card 2023-12-23 17:13:44 -06:00
advplyr
b376f89ce5 Version bump v2.7.0 2023-12-23 17:05:44 -06:00
advplyr
5633113f25 Update share buttons to not show an error on abort 2023-12-23 16:39:56 -06:00
advplyr
669415cfbf Merge pull request #2431 from pablojimenezmateo/comic-zoom
Add zoom controls to comic reader
2023-12-23 16:18:23 -06:00
advplyr
9f366863a9 Update comic reader buttons for mobile screens, add left scrollBy 2023-12-23 16:16:24 -06:00
advplyr
0d644fe0c9 Add:Year in review banner for user stats page #2373 2023-12-23 15:29:34 -06:00
advplyr
72fa6b8200 Fix:Show cover size widget when audio player is open #2443 2023-12-23 10:50:04 -06:00
advplyr
6d3f1d263a Merge pull request #2442 from JBlond/master
Follow up Translations for 76119445a3
2023-12-23 08:55:28 -06:00
JBlond
47bf9f7836 Follow up Translations for 76119445a3
* Update:Listening sessions table for multi-select, sorting and rows per page
- Updated get all sessions API endpoint to include sorting
- Added sessions API endpoint for batch deleting
2023-12-23 14:42:56 +01:00
advplyr
2738402aac Add:Year in review card for server stats #2373 2023-12-22 17:01:07 -06:00
advplyr
68d36522b1 Update:Listening sessions table UI for mobile 2023-12-21 14:36:51 -06:00
advplyr
24a587b944 Update:Remove playback sessions that are 3s or less on startup 2023-12-21 14:29:36 -06:00
advplyr
76119445a3 Update:Listening sessions table for multi-select, sorting and rows per page
- Updated get all sessions API endpoint to include sorting
- Added sessions API endpoint for batch deleting
2023-12-21 13:52:42 -06:00
advplyr
46ec59c74e Update:Year in review card prevent text overflow for narrator, author and genre #2373 2023-12-21 09:44:37 -06:00
advplyr
2b7122c744 Update:Year stats API endpoint & generate year in review image #2373 2023-12-20 17:18:21 -06:00
Pablo
52f0a5432b feat: enable zoom through the arrow buttons 2023-12-20 11:45:21 +01:00
advplyr
7391b4d0ec Add:User stats API for year stats 2023-12-19 17:19:33 -06:00
Pablo
aa7ee3e8ff fix: zoom buttons were showing when loading the image 2023-12-19 18:45:11 +01:00
Pablo
bef0f3709f feat: add basic zoom functionality to comic reader 2023-12-19 18:39:02 +01:00
advplyr
f33b011847 Merge pull request #2420 from treyg/synology-reverse-proxy-docs
docs: update synology reverse proxy to use the latest DSM settings
2023-12-17 16:42:43 -06:00
Trey Gordon
2d8d11d4da docs: update synology reverse proxy to use the latest DSM settings 2023-12-17 15:56:14 -05:00
advplyr
10b1784f6d Fix:Library search API endpoint /libraries/:id/search to check that query param q is a valid string 2023-12-17 12:23:55 -06:00
advplyr
f2f2ea161c Update:API endpoint /podcasts/feed validates rssFeed URL and uses SSRF req filter 2023-12-17 12:00:11 -06:00
advplyr
dc67a52000 Update:API endpoint /search/podcast throw 400 error if term query param is not supplied 2023-12-17 11:18:21 -06:00
advplyr
05820aa820 Update:API endpoints /podcasts/feed and /podcasts/opml restricted to admin users 2023-12-17 11:17:35 -06:00
advplyr
8966dbbcd1 Fix:Restrict podcast search page to admins 2023-12-17 11:06:03 -06:00
advplyr
cf32819c01 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2023-12-17 10:41:55 -06:00
advplyr
728496010c Update:/auth/openid/config API endpoint to require admin user and validate issuer URL 2023-12-17 10:41:39 -06:00
advplyr
0a08f47942 Merge pull request #2417 from springsunx/master
Update zh-cn.json
2023-12-15 12:14:15 -06:00
SunX
39ceb02500 Update zh-cn.json 2023-12-15 19:04:56 +08:00
advplyr
4336714248 Merge pull request #2415 from nichwall/docker_compose_update
Added comments to the Docker Compose file
2023-12-15 04:14:16 -06:00
nichwall
1d41904fc3 Added comments to the Docker Compose file 2023-12-14 21:04:37 -07:00
advplyr
fae383a045 Fix:RSS feeds for collections not updating #2414 2023-12-14 15:45:34 -06:00
mikiher
8f7a420cca Fix directory writable check (fs.access not working on Windows) 2023-12-14 09:47:18 +02:00
advplyr
9720ba3eed Merge pull request #2413 from JBlond/master
More german translations
2023-12-13 13:40:36 -06:00
JBlond
d3256d59d5 - Translate more strings
- Add missing least empty line
2023-12-13 20:12:25 +01:00
advplyr
fa5f7ab7a5 Merge pull request #2411 from Nab0y/master
Update Russian localization
2023-12-12 16:57:35 -06:00
Dmitry Naboychenko
6f26fd7238 Update Russian localization 2023-12-12 22:56:05 +03:00
advplyr
6abc0819d9 Merge pull request #2400 from mikiher/bookfinder-improvements
A few BookFinder improvements (including a fix for #2238)
2023-12-10 10:36:21 -06:00
advplyr
b580a23e7e BookFinder formatting update 2023-12-10 10:35:21 -06:00
advplyr
f659c3f11c Fix:Podcast RSS feed request header to include application/rss+xml #2401 2023-12-09 13:51:28 -06:00
mikiher
0282a0521b Sort audible match results by duration difference 2023-12-09 00:33:06 +02:00
advplyr
75637e4b94 Merge pull request #2397 from JBlond/master
Follow up for sso-redirecturi and #2305 and #2333
2023-12-08 15:42:28 -06:00
mikiher
b6c789dee6 Merge branch 'advplyr:master' into bookfinder-improvements 2023-12-08 14:07:25 +02:00
JBlond
8d3d636329 Follow up for sso-redirecturi and #2305 #2333
8f4c65ec8c / 7c9c278cc4 sso-redirecturi
2f6756eddf #2333
2e5822b7c8 #2305
2023-12-08 09:39:04 +01:00
advplyr
6f6395bad7 Only log update binary env path if it was updated 2023-12-07 17:32:06 -06:00
advplyr
b8c8d2a02e Merge pull request #2386 from Sapd/sso-redirecturi
SSO/OpenID: Use a mobile-redirect route (Fixes #2379 and #2381)
2023-12-07 17:12:36 -06:00
advplyr
98104a3c03 Map new translations to other files 2023-12-07 17:05:52 -06:00
advplyr
8f4c65ec8c Merge branch 'master' into sso-redirecturi 2023-12-07 17:04:59 -06:00
advplyr
341a0452da Update auth settings endpoint to return updated flag and show whether updates were made in client toast 2023-12-07 17:01:33 -06:00
mikiher
6afb8de3dd Remove ffbinaries local cache 2023-12-08 00:53:53 +02:00
mikiher
0e62ccc7aa Merge branch 'binary-manager' of https://github.com/mikiher/audiobookshelf into binary-manager 2023-12-07 23:51:33 +02:00
mikiher
09282a9a62 Remove all callbacks and refactor spaghetti code in downloadUrls 2023-12-07 23:49:46 +02:00
advplyr
18b3ab5610 Revert package-lock updates 2023-12-07 15:12:49 -06:00
mikiher
699a658df9 Remove debug printing from libs/ffbinaries 2023-12-07 08:50:45 +02:00
advplyr
b5e255a384 Update:Clean series sequence response from audible provider #2380
- Removes Book prefix
- Splits on spaces and takes first, removes trailing comma
2023-12-06 17:31:36 -06:00
mikiher
67ccd2c1fb Fix test after switching to libs/ffbinaries 2023-12-06 13:45:28 +02:00
mikiher
898b072e68 Merge branch 'advplyr:master' into binary-manager 2023-12-06 09:27:17 +02:00
advplyr
34156af403 Fix:Updating media progress not clearing cache #2392 2023-12-05 17:58:54 -06:00
advplyr
61a0126278 Remove ffbinaries dependency 2023-12-05 17:35:57 -06:00
advplyr
1ce1904c89 Add ffbinaries lib 2023-12-05 17:35:15 -06:00
advplyr
7c9c278cc4 Merge pull request #2293 from Dr-Blank/gujarati-translation
More Gujarati translations
2023-12-05 15:42:05 -06:00
advplyr
450507a812 Map english translations and merge with gu 2023-12-05 15:41:12 -06:00
mikiher
c074c835d4 Remove semicolons from test 2023-12-05 22:18:37 +02:00
mikiher
2e989fbe83 Add BinaryManager 2023-12-05 21:19:17 +02:00
mikiher
b1b325d00b Add ffbinaries dependency 2023-12-05 21:18:30 +02:00
Denis Arnst
cf00650c6d SSO/OpenID: Also fix possible race condition
- We need to define redirect_uri in the callback again, because the global params of passport can change between calls to the first route (ie. if multiple users log in at same time)
- Removed is_rest parameter as requirement for mobile flow (to maximise compatibility with possible oauth libraries)
- Also renamed some variables for clarity
2023-12-05 09:43:06 +01:00
Denis Arnst
e6ab28365f SSO/OpenID: Remove modifying redirect_uri in the callback
The redirect URI will be now correctly set to either /callback or /mobile-redirect in the /auth/openid route
2023-12-05 00:18:58 +01:00
Denis Arnst
80fd2a1a18 SSO/OpenID: Use a mobile-redirect route (Fixes #2379 and #2381)
- Implement /auth/openid/mobile-redirect this will redirect to an app-link like audiobookshelf://oauth
- An app must provide an `redirect_uri` parameter with the app-link in the authorization request to /auth/openid
- The user will have to whitelist possible URLs, or explicitly allow all
- Also modified MultiSelect to allow to hide the menu/popup
2023-12-04 22:36:34 +01:00
advplyr
84160b2f07 Fix:Server crash when user without a password attempts to login with a password #2378 2023-12-02 16:17:52 -06:00
advplyr
fbc2c2b481 Merge pull request #2333 from kieraneglin/ke/feature/upload-auto-fetch-data
Add ability to fetch book data on upload
2023-12-02 15:56:39 -06:00
Kieran Eglin
57a5005197 Addressed feedback changes 2023-12-01 21:42:54 -08:00
Kieran Eglin
9350c5513e Removed unneeded mixin 2023-12-01 15:19:50 -08:00
advplyr
f59516cc6e Fix:Hide change password form when password auth is disabled #2367 2023-12-01 17:10:33 -06:00
advplyr
88078ff813 Fix undefined series string when match has no series, minor ui updates 2023-12-01 16:44:04 -06:00
mikiher
281de48ed4 Fix "et al" cleanup 2023-11-30 21:49:24 +02:00
mikiher
3c6d6bf688 Merge branch 'advplyr:master' into bookfinder-improvements 2023-11-30 21:37:01 +02:00
mikiher
8ac0ce399f Remove "et al[.]" in author cleanup 2023-11-30 21:17:13 +02:00
mikiher
80458e24bd "[un]abridged" in title candidate generation 2023-11-30 21:15:25 +02:00
advplyr
6ab966ee2f Merge pull request #2365 from Sapd/sso-errorhandling
SSO/OpenID: Provide error messages to logs
2023-11-28 16:39:01 -06:00
advplyr
166477ae27 Fix:Narrators page 404 on reload #2359 2023-11-28 16:39:52 -06:00
advplyr
a719065b8d Auto formatting 2023-11-28 16:37:19 -06:00
Denis Arnst
36599a2984 SSO/OpenID: Rename probably misleading message 2023-11-28 21:16:39 +01:00
Kieran Eglin
d9c9289d65 Added error handling; Made querystring helper 2023-11-28 12:11:14 -08:00
Kieran Eglin
e5579b2c33 Improved UI; Added tooltips; Fixed unrelated layout issues 2023-11-28 11:45:44 -08:00
Denis Arnst
618028503b SSO/OpenID: Also Log token header 2023-11-28 20:07:49 +01:00
Kieran Eglin
2f6756eddf Merged parent 2023-11-28 10:21:41 -08:00
Denis Arnst
ad53894ea1 SSO/OpenID: Provide detailed error messages 2023-11-28 17:29:22 +01:00
advplyr
086954fb9c Version bump v2.6.0 2023-11-27 17:41:47 -06:00
advplyr
f243ad14e0 Add help link to oidc guide 2023-11-27 17:10:31 -06:00
advplyr
2e5822b7c8 Merge pull request #2305 from mikiher/nfo-metadata
Add NFO metadata source
2023-11-26 14:49:04 -06:00
advplyr
3d468339b3 Update parse nfo metadata test for description 2023-11-26 14:41:19 -06:00
advplyr
b4c14fc78d Parse NFO comma separated strings remove empty strings 2023-11-26 14:38:25 -06:00
advplyr
d9584174ff Parse NFO trim final parsed description 2023-11-26 14:33:35 -06:00
advplyr
36e00e8d6a Merge master 2023-11-26 13:54:06 -06:00
advplyr
5e69b54eb0 Reverse order of metadata precedence in UI, add translations 2023-11-26 13:45:43 -06:00
advplyr
5a8c60a8bc Merge pull request #2343 from mikiher/caching
Simple API Caching for /libraries* requests
2023-11-26 12:33:54 -06:00
mikiher
3ff41f2b43 Cache HTTP headers and status 2023-11-25 23:49:56 +02:00
advplyr
17cab0d3a8 Merge pull request #2351 from JBlond/master
de translation follow up
2023-11-25 12:18:08 -06:00
JBlond
0fac9e367d de translation follow up
for 2e06ae01a1
2023-11-25 19:10:26 +01:00
advplyr
bf0bcf8967 Merge pull request #2336 from JBlond/master
de language translation follow up
2023-11-25 11:31:59 -06:00
advplyr
2e06ae01a1 Merge pull request #2326 from lkiesow/hide-dev-logs
Allow enabling dev logs
2023-11-25 10:36:50 -06:00
mikiher
288a32cc1e Merge branch 'caching' of https://github.com/mikiher/audiobookshelf into caching 2023-11-25 08:14:54 +02:00
mikiher
26fc3a1966 Remove currently unused time measurement utils 2023-11-25 08:14:45 +02:00
advplyr
9d257ebecd Update:Home page shelf bulk items added socket event only adds new items to the recently added shelf instead of refreshing all shelves #2323 2023-11-24 15:36:42 -06:00
advplyr
1a046a9bcb Merge branch 'master' into caching 2023-11-24 14:38:27 -06:00
advplyr
7a9c869ac5 Ignore sequelize hooks when updating user lastSeen on socket authentication 2023-11-24 14:27:32 -06:00
advplyr
572fb0993c Rename ApiCacheManager to add .js file extension 2023-11-24 14:20:14 -06:00
advplyr
9beee3ed65 Fix:Change password api endpoint 2023-11-23 15:14:49 -06:00
mikiher
ab19e25586 Remove unnecessary timing measurements 2023-11-23 09:56:37 +02:00
mikiher
07d7d16418 Use a single router.get for API cache middleware 2023-11-23 09:55:55 +02:00
mikiher
5e1e748c71 Add ApiCacheManager unit test 2023-11-23 09:53:52 +02:00
advplyr
6651ad0d45 Update:Added translation strings for OIDC auth 2023-11-22 12:55:01 -06:00
advplyr
288beae874 Update:OIDC auth auto launch setting description to include manual override path 2023-11-22 12:38:11 -06:00
advplyr
32ce771911 Allow cors while in development 2023-11-22 12:37:18 -06:00
mikiher
d944ecaa21 Merge branch 'caching' of https://github.com/mikiher/audiobookshelf into caching 2023-11-22 19:10:29 +02:00
mikiher
5aeb6ade72 Merge branch 'caching' of https://github.com/mikiher/audiobookshelf into caching 2023-11-22 19:00:11 +02:00
mikiher
107b4b83c1 Add cache middleware to most /libraries get requests 2023-11-22 18:40:42 +02:00
JBlond
0d61e29ecf de language translation follow up for 27497451d9 2023-11-21 20:30:48 +01:00
mikiher
781d4f570f Add test for parseNfoMetadata 2023-11-21 09:12:37 +02:00
mikiher
a4d4f1bc2e Merge branch 'advplyr:master' into nfo-metadata 2023-11-21 09:09:12 +02:00
advplyr
048e27f03f Update:Openid auth endpoint sets the mobile flag on session to be used in the callback
Co-authored-by: Denis Arnst <git@sapd.eu>
2023-11-20 15:41:38 -06:00
Kieran Eglin
8c434703fb Added computed metadata check to UI dropdown 2023-11-20 09:18:50 -08:00
Kieran Eglin
3cc900ffbf Adds fetching book data on upload 2023-11-20 08:51:00 -08:00
Lars Kiesow
7b6aa3ba5a Allow enabling dev logs
This patch allows users to enable dev logs on production systems by
setting the `HIDE_DEV_LOGS` environment variable.

Before, you could only use this on a non-production environment. On
production, the logs would be disabled. This patch changes the behavior
and uses the `NODE_ENV` only as default. On production they are disabled
if `HIDE_DEV_LOGS` is undefined but can be enabled by setting
`HIDE_DEV_LOGS=0` on dev, they are enabled if undefined, but can be
disabled by setting `HIDE_DEV_LOGS=1`.
2023-11-19 21:00:54 +01:00
advplyr
aa933df525 Update oidc redirect_uri to check x-forwarded-proto header for proxies 2023-11-19 14:00:39 -06:00
advplyr
a0f137936d Merge pull request #2325 from lkiesow/milliseconds
Add milliseconds to logging
2023-11-19 13:41:10 -06:00
advplyr
dcbfc963c1 Update protocol for redirect_uri in openid strategy to work for reverse proxies 2023-11-19 13:38:09 -06:00
Lars Kiesow
91fa78d740 Add milliseconds to logging
This patch adds milliseconds to the time string used for logging. This
helps when debugging some timing issues and should have no real negative
side effect.
2023-11-19 20:36:04 +01:00
advplyr
89eb857c14 Fix initialize openid auth strategy 2023-11-19 12:57:17 -06:00
advplyr
e07d17c472 Merge pull request #1636 from lukeIam/auth_passportjs
Integrate passportjs for muti-strategy authentication and SSO
2023-11-19 11:46:52 -06:00
advplyr
4c2c320b9d Remove global CORS for api endpoints and setup temp CORS check for ebook endpoint 2023-11-19 11:32:48 -06:00
advplyr
56c574c928 Update package-lock 2023-11-19 08:29:58 -06:00
advplyr
d2aea86957 Merge pull request #2300 from mikiher/bookfinder-testing-mocha
Bookfinder.js unit testing with mocha
2023-11-18 13:55:18 -06:00
advplyr
80e061115f Add remove semicolons to .vscode settings, update BookFinder.test formatting 2023-11-18 13:41:08 -06:00
mikiher
4299627f5f Add lru-cache dependency 2023-11-17 08:54:16 +02:00
mikiher
6a722102c5 Use ApiCacheManager & timing middleware 2023-11-17 08:49:40 +02:00
mikiher
f22f3361d5 Add timing utils 2023-11-17 08:48:09 +02:00
mikiher
4dec8c265d Add ApiCacheManager 2023-11-17 08:47:40 +02:00
mikiher
d990e5b909 Add NFO metadata source 2023-11-12 13:30:23 +00:00
advplyr
fb48636510 Openid auth failures redirect to login page with error message.
Remove remaining google oauth server settings
2023-11-11 13:10:24 -06:00
advplyr
1ad6722e6d Remove google-oauth passport strategy 2023-11-11 11:29:59 -06:00
advplyr
557ef2ef79 Update /auth/openid endpoints for correct PKCE handling
- Provide error handling for /auth/openid
- Add session.mobile inside /auth/openid
- Proper PKCE handling for /auth/openid/callback
- redirect_uri handling for the token url in /auth/openid/callback

Co-authored-by: Denis Arnst <git@sapd.eu>
2023-11-11 10:52:05 -06:00
advplyr
cff2caa07a Update:Rename podcast search page to add #2301 2023-11-10 16:32:14 -06:00
advplyr
237fe84c54 Add new API endpoint for updating auth-settings and update passport auth strategies 2023-11-10 16:11:51 -06:00
advplyr
078cb0855f Merge branch 'master' into auth_passportjs 2023-11-10 07:26:07 -06:00
mikiher
ecba67da6d Add Istanbul coverage (nyc) 2023-11-10 10:02:02 +00:00
mikiher
ea05e1f559 Remove test/ from .gitigore (now contains unit tests) 2023-11-10 09:58:30 +00:00
advplyr
d3a55c8b1a Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2023-11-09 16:36:37 -06:00
advplyr
d6b17678ec Update:Persist soft/hard delete checkbox option #1689 2023-11-09 16:36:28 -06:00
advplyr
33e287a543 Update:Persist show full path option for tables #2285 2023-11-09 16:26:49 -06:00
advplyr
08f045a02b Merge pull request #2299 from burghy86/patch-11
Update it.json
2023-11-09 16:15:16 -06:00
mikiher
e8c14dbb58 Test BookFinder.js using mocha 2023-11-09 19:58:51 +00:00
burghy86
bf48eee705 Update it.json
arrange the additional lines.
how the hell did we get to over 700 lines in less than two months?
2023-11-09 15:46:25 +01:00
advplyr
8f4c75ff2b Update:Author card books translation string #2284 2023-11-08 16:28:05 -06:00
advplyr
ee75d672e6 Matching user by openid sub, email or username based on server settings. Auto register user. Persist sub on User records 2023-11-08 16:14:57 -06:00
advplyr
e140897313 Add match existing user by and auto register settings and UI 2023-11-08 14:45:29 -06:00
mikiher
d1671f0ddc Cleanup commented out tests 2023-11-08 16:37:12 +00:00
mikiher
2730486ba5 Add tests for AuthorCandidates and search() in BookFinder 2023-11-08 16:24:08 +00:00
mikiher
49e4515785 Add stripRedudantSpaces 2023-11-08 16:21:20 +00:00
mikiher
819c524f51 Pass audnexus to AuthorCandidates constructor directly 2023-11-08 16:19:24 +00:00
Dr-Blank
6d968f9044 Update gu.json 2023-11-06 18:16:03 -05:00
Dr-Blank
23fa9e8d7f Update gu.json 2023-11-06 18:15:18 -05:00
Dr-Blank
59a428d549 more gu translations 2023-11-06 18:10:57 -05:00
advplyr
70c213ad22 Merge pull request #2291 from brianjaustin/fix/collection-duration
Hide collection duration if 0
2023-11-06 16:21:25 -06:00
advplyr
aad6402fdb Update client/components/tables/collection/BookTableRow.vue 2023-11-06 16:18:35 -06:00
advplyr
5ce1cda2d0 Merge pull request #2283 from mikiher/watcher-single-file-update
Fix handling of single media file updates
2023-11-06 16:08:23 -06:00
mikiher
ba60fc7581 Add tests for TitleCanidates 2023-11-06 05:33:06 +00:00
Brian Austin
0344e8cf1b Hide collection duration if 0 2023-11-05 19:13:26 -05:00
advplyr
f840aa80f8 Add button to populate openid URLs using the issuer URL 2023-11-05 14:11:37 -06:00
advplyr
c17540e191 Add app and serverVersion properties to response from /status 2023-11-05 13:06:26 -06:00
advplyr
309ef807ab Update /auth/openid endpoint to work with PKCE from mobile
Co-authored-by: Denis Arnst <git@sapd.eu>
2023-11-05 13:05:16 -06:00
advplyr
61e05e92a8 Add Swedish language option 2023-11-05 13:05:16 -06:00
Gustav Almstrom
1e5d6a5d52 Added swedish translation of strings 2023-11-05 13:05:15 -06:00
advplyr
ff831678e8 Merge pull request #2288 from ScuttleSE/master
Added swedish translation of strings
2023-11-05 10:18:45 -06:00
advplyr
910be21e93 Add Swedish language option 2023-11-05 10:16:40 -06:00
mikiher
89055f8655 Remove unnecessary includesAuthorDiff from sorting 2023-11-05 16:14:26 +00:00
Gustav Almstrom
b9ccc28baa Added swedish translation of strings 2023-11-05 16:51:45 +01:00
mikiher
5a3d450482 Refactor diff declarations in title candidate sorting 2023-11-05 15:13:42 +00:00
mikiher
047e7a72f2 Make position an internal property of titleCandidates 2023-11-05 14:56:20 +00:00
mikiher
3a9d09ea63 Add jest to dev dependencies 2023-11-05 14:33:56 +00:00
mikiher
ee3d3808ef Refactor removing author from title candidate 2023-11-05 14:31:36 +00:00
mikiher
8f5a6b7c95 Move utility functions to module scope 2023-11-05 14:17:26 +00:00
advplyr
840811b464 Replace passport openidconnect plugin with openid-client, add JWKS and logout URL server settings, use email and email_verified instead of username 2023-11-04 15:36:43 -05:00
mikiher
567e1c46db Fix handling of single mefia file updates 2023-11-04 11:06:54 +00:00
advplyr
cfe0c2a986 Merge branch 'master' into auth_passportjs 2023-11-03 08:29:05 -05:00
advplyr
68546acf2a Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2023-11-03 07:08:04 -05:00
advplyr
5220361151 Fix:Podcast episode cron not adding/removing library items correctly #2277 2023-11-03 07:07:58 -05:00
advplyr
076e01dbfe Merge pull request #2276 from Plazec/patch-1
Update cs.json
2023-11-02 14:19:45 -05:00
advplyr
f15ed08b6a Merge branch 'master' into auth_passportjs 2023-11-02 13:55:16 -05:00
advplyr
828b96b2d9 Add server settings for changing openid button text and auto launching openid 2023-11-02 13:55:01 -05:00
Plazec
3100437651 Update cs.json
Corrected some translation errors and made the translation more consistent.
2023-11-02 19:44:32 +01:00
advplyr
20880a6bf6 Merge pull request #2274 from radekmuhlfeit2/patch-1
Create cs-CZ.json
2023-11-01 15:48:05 -05:00
advplyr
2eff69fe9f Add czech translation to dropdown 2023-11-01 15:42:54 -05:00
advplyr
5f035db0a9 Rename cs-CZ.json to cs.json 2023-11-01 15:29:58 -05:00
radekmuhlfeit2
e4a7e9d6b5 Create cs-CZ.json
Czech strings.
2023-11-01 20:21:45 +01:00
advplyr
ab14b561f5 Merge master 2023-11-01 08:58:48 -05:00
advplyr
5ce4734a70 Merge pull request #2272 from clement-dufour/master
Add support for the old Apple Podcasts iOS app
2023-11-01 07:43:48 -05:00
clement.dufour
1ae2089253 Update:Add cover file extension in RSS feeds 2023-11-01 12:11:24 +01:00
clement.dufour
3c21e9d413 Update:Simpler content URL in RSS feeds 2023-11-01 12:10:44 +01:00
advplyr
9616d99640 Fix:Crash when matching with author names ending in ??? by escaping regex strings #2265 2023-10-30 16:35:41 -05:00
advplyr
2ef11e5ad0 Version bump v2.5.0 2023-10-29 12:58:00 -05:00
advplyr
27497451d9 Add:Ereader device setting to set users that have access #1982 2023-10-29 11:28:34 -05:00
advplyr
94fd3841aa Update:Notification widget shows green dot indicating unseen completed tasks 2023-10-29 09:20:50 -05:00
advplyr
225dcdeafd Fix:RSS feed parser for episode metadata tags that have attributes #1996 2023-10-28 16:11:15 -05:00
advplyr
2c9f2e0d68 Fix podcast episode rss feed search showing all episodes are downloaded 2023-10-28 15:54:19 -05:00
advplyr
a9f74ace5a Merge pull request #2255 from MxMarx/added-search-epubs
Search epub text
2023-10-28 14:35:40 -05:00
advplyr
6dc5b58d8e Update TOC to not close when clicking on it 2023-10-28 14:32:11 -05:00
advplyr
88c794e710 Fix:Open RSS feed for series & collections respect prevent indexing option #2047 2023-10-28 13:45:06 -05:00
advplyr
61f2fb28e0 Add:Help icon buttons for libraries, rss feeds and users config pages, table add new buttons updated 2023-10-28 13:27:53 -05:00
advplyr
1df4dca4bb Merge pull request #2246 from MxMarx/expand-cover-images
show a modal with cover images when clicked
2023-10-27 16:55:53 -05:00
advplyr
6278bb8665 Move raw cover preview to a separate global component, fix item page cover overlay show on hover 2023-10-27 16:51:44 -05:00
MxMarx
4229cb7fb6 Added a method to unwrap the chapter list 2023-10-27 00:35:28 -07:00
MxMarx
5778200c8f Make epubs searchable 2023-10-27 00:14:46 -07:00
advplyr
5c1c511718 Merge pull request #2249 from mikiher/watcher-update-api
Add API to update a path on a watched library folder
2023-10-26 16:45:26 -05:00
advplyr
f9c4dd2457 Update watcher function calls, add js docs 2023-10-26 16:41:54 -05:00
advplyr
3bccd52196 Merge branch 'master' into watcher-update-api 2023-10-26 16:33:48 -05:00
advplyr
0c23da7b02 Add missing translations 2023-10-26 16:31:47 -05:00
advplyr
d577cae393 Merge pull request #2253 from MxMarx/add-epub-font-selector
Option to change the font family in epub viewer
2023-10-26 16:30:29 -05:00
MxMarx
24228b4424 Option to change the font family in epub viewer 2023-10-26 02:15:08 -07:00
advplyr
8dc4490169 Fix:Watcher waits for files to finish transferring before scanning #1362 #2248 2023-10-25 16:53:53 -05:00
advplyr
ef1cdf6ad2 Fix:Only show authors with books for users #2250 2023-10-24 17:04:54 -05:00
mikiher
e054b9a54c Add API to update a path on a watched library folder 2023-10-24 13:35:43 +00:00
MxMarx
32616aa441 show a modal with cover images when clicked 2023-10-23 20:37:51 -07:00
advplyr
0ee6336b02 Merge pull request #2245 from mikiher/watcher-fixes
Fix incorrect subpath checks in server/watcher.js
2023-10-23 17:28:44 -05:00
advplyr
9a477a9270 Add jsdocs 2023-10-23 17:28:59 -05:00
mikiher
976ae502bb Fix incorrect subpath checks 2023-10-23 21:48:34 +00:00
advplyr
c4c12836a4 Fix:Version in bottom left of siderail overlapping buttons #2195 2023-10-22 17:04:45 -05:00
advplyr
5a70c0d7be Fix:Authors page books hide radio button on hover 2023-10-22 16:40:12 -05:00
advplyr
60a80a2996 Update:Remove support for metadata.abs, added script to create metadata.json files if they dont exist 2023-10-22 15:53:05 -05:00
advplyr
ce88c6ccc3 Scanner metadata order of precedence description label, link to guide, add translations 2023-10-22 12:58:05 -05:00
advplyr
b42edfe7a7 Book duration shown on match page compares minutes #1803 2023-10-22 07:10:52 -05:00
advplyr
0cbcfbd273 Merge pull request #2233 from Hallo951/master
Update de.json
2023-10-21 17:04:04 -05:00
Hallo951
8ecec93e67 Update de.json 2023-10-21 22:33:31 +02:00
advplyr
49403771c9 Update:Quick match all for library to use task instead of toast, remove scan socket events 2023-10-21 13:53:00 -05:00
advplyr
50215dab9a Hide library modal tools tab for new libraries 2023-10-21 13:00:41 -05:00
advplyr
58b9a42c84 Add:Scan button on libraries table 2023-10-21 12:56:35 -05:00
advplyr
d7264f8c22 Update watcher scanner to show task notification 2023-10-21 12:25:45 -05:00
advplyr
bef6549805 Update:Replace library scan toast with task manager #1279 2023-10-20 17:46:18 -05:00
advplyr
6f65350269 Update:JSDocs for task manager 2023-10-20 16:39:32 -05:00
advplyr
5644a40a03 Update:Add missing tranlations #2217 2023-10-20 16:08:57 -05:00
advplyr
920ddf43d7 Remove unused old model functions 2023-10-19 17:20:12 -05:00
advplyr
4a5f534a65 Update:Description width of item page to match width of tables 2023-10-19 16:30:33 -05:00
advplyr
24031f12db Merge pull request #2229 from JBlond/master
Translate new string for DE language.
2023-10-19 16:17:09 -05:00
JBlond
22361d785d Translate new string for DE language. 2023-10-19 15:22:00 +02:00
advplyr
8c5ce6149f Fix:Aspect ratio of authors image on authors landing page #2227 2023-10-18 17:10:53 -05:00
advplyr
516b0b4464 Fix:Book scanner set item as missing if no media files are found #2226 2023-10-18 17:02:15 -05:00
advplyr
d22052c612 Update UI for library tools tab 2023-10-18 16:47:56 -05:00
advplyr
b4ce5342c0 Add:Tools tab on library modal, api endpoint to remove all metadata files from library item folders 2023-10-17 17:46:43 -05:00
advplyr
0d5792405f Fix:Podcast episodes store RSS feed guid so they can be matched if the RSS feed changes the episode URL #2207 2023-10-16 17:47:44 -05:00
advplyr
48a590df4a Fix:Authors page description shows line breaks #2218 2023-10-16 17:08:50 -05:00
advplyr
c264332994 Fix:Scanner detecting library item folder renames #1161 2023-10-15 12:55:22 -05:00
advplyr
cdd740015c Add:Danish translations 2023-10-15 08:23:22 -05:00
advplyr
07ad81969c Update:Scanner recognizes asin in book folder names #1852 2023-10-14 15:04:16 -05:00
advplyr
dcdd4bb20b Update:HLS router request validation, smooth out transcode reset logic 2023-10-14 12:50:48 -05:00
advplyr
c98fac30b6 Update:Validate image URI content-type before writing image file 2023-10-14 10:52:56 -05:00
advplyr
1f8372f5e5 Merge pull request #2215 from springsunx/master
Update zh-cn.json
2023-10-14 09:37:49 -05:00
SunX
616ecf77b0 Update zh-cn.json
Update zh-cn.json
2023-10-14 20:30:27 +08:00
advplyr
656c81a1fa Update:Remove image path input from author modal, add API endpoints for uploading and removing author image 2023-10-13 17:37:37 -05:00
advplyr
290a377ef9 Update:Remove local cover path input & replace with url from web input, include SSRF request filter 2023-10-13 16:33:47 -05:00
advplyr
05731c9f72 Remove unused css parser lib 2023-10-13 14:10:54 -05:00
advplyr
3108bc5ccc Fix:Server crash when removing last item from a playlist #2211 2023-10-13 13:33:15 -05:00
advplyr
e687a3403e Fix:Cleaning up orphan streams on server init #2209 2023-10-11 17:05:56 -05:00
advplyr
753ae3d7dc Fix:Server crash when downloading single file library items #2199 2023-10-10 17:51:52 -05:00
advplyr
c9a2fdcb29 Library scanner saves last scan info including metadata precedence. Remove force re-scan 2023-10-09 17:48:21 -05:00
advplyr
f84634e978 Fix OPF file scanner series sequence, book scanner check for mismatched audio file found lengths 2023-10-09 17:09:36 -05:00
advplyr
89821b91b0 Podcast scanner refactor/cleanup 2023-10-09 16:41:43 -05:00
advplyr
347b49f564 Update:Remove scanner settings, add library scanner settings tab, add order of precedence 2023-10-08 17:10:43 -05:00
advplyr
5ad9f507ba Merge pull request #2186 from mikiher/Fuzzy-Matching-Continued
Fuzzy matching continued
2023-10-08 10:39:11 -05:00
mikiher
f8f555b4b6 Remove some unused code in AuthorCandidates.add 2023-10-07 21:30:37 +00:00
advplyr
786df450e5 Merge branch 'master' into Fuzzy-Matching-Continued 2023-10-07 11:52:04 -05:00
advplyr
db9d5c9d43 Add:Support for pasting semicolon separated strings in multi select inputs #1198 2023-10-06 16:52:12 -05:00
advplyr
b447cf5c1c Fix:Handle non-ascii characters in global search by not lowercasing in query #2187 2023-10-05 17:00:40 -05:00
mikiher
f44b7ed1d0 [enhancement] If no valid authors, use clean author field 2023-10-05 18:41:18 +00:00
mikiher
b0b7a0a618 [enhancement] Reduce spurious matches in validateAuthor 2023-10-05 18:27:52 +00:00
mikiher
bf9f3895db [enhancement] Treat underscores as title part separators 2023-10-05 17:53:54 +00:00
mikiher
f3555a12ce [enhancement] Handle initials in author normalization 2023-10-05 14:50:16 +00:00
mikiher
b2acdadcea [enhancement] Added a couple title transformers 2023-10-05 14:29:40 +00:00
mikiher
9eff471afa [enhancement] AuthorCandidates, author validation 2023-10-05 12:05:30 +00:00
mikiher
8979586404 [enhancement] Improve candidate sorting 2023-10-05 10:28:55 +00:00
advplyr
bfe514b7d4 Add:Email inputs for users 2023-10-04 17:05:12 -05:00
mikiher
752bfffb11 [enhamcement] Only add title candidate before and after all transforms 2023-10-04 14:53:12 +00:00
mikiher
10f5bc8cbe [cleanup] Make original title/author check with more readable 2023-10-04 05:26:16 +00:00
advplyr
565ff36d4e Merge pull request #2175 from MarshDeer/master
Completed Spanish translation and corrected typo in an English string
2023-10-03 17:18:07 -05:00
advplyr
401bd91204 Add:Show current book duration on match page as compared with book listed #1803 2023-10-03 17:16:49 -05:00
mikiher
5d7c197c89 [fix] Add back toLowerCase to cleanAuthor/Title (required by other uses) 2023-10-03 19:43:37 +00:00
MarshDeer
8e97be8ef4 Spanish translation completed 2023-10-02 19:42:42 -03:00
MarshDeer
733ad52684 Typo correction 2023-10-02 19:42:25 -03:00
advplyr
5ccf0df308 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2023-10-02 17:09:19 -05:00
advplyr
a3a8937ba3 Fix:Crash when searching for cover without an author #2174 2023-10-02 17:09:12 -05:00
advplyr
2662e8f715 Merge branch 'master' into auth_passportjs 2023-10-02 16:21:47 -05:00
advplyr
28b2005068 Merge pull request #2171 from Alistair1231/master
make force transcode apply to all "ffmpeg error 1"
2023-10-02 08:35:23 -05:00
advplyr
7c9631c1b0 Update server/objects/Stream.js 2023-10-02 08:34:56 -05:00
Alistair1231
4352989242 update comment to include second issue that is adressed by change 2023-10-02 09:30:57 +02:00
Alistair Bahr
73bb73a04a make force transcode apply to all "ffmpeg error 1" 2023-10-02 09:25:34 +02:00
advplyr
20a1d40d99 Fix:Set date properly on local playback sessions #2168 2023-10-01 12:44:52 -05:00
advplyr
e10b178565 Fix:Crash on failed scanner find covers #2164 2023-10-01 09:03:01 -05:00
mikiher
46b0b3a6ef [cleanup] Refactor candidates logic to separate class 2023-10-01 08:42:47 +00:00
advplyr
f2aed08d51 Version bump v2.4.4 2023-09-30 16:04:06 -05:00
advplyr
c2c8cf919e Fix:Bad backup causing other backup files to not be displayed #1961 2023-09-30 16:01:10 -05:00
advplyr
9ebe23e91b Merge pull request #2162 from 92Kev/master
Update nl.json
2023-09-30 15:25:36 -05:00
advplyr
3d96749d38 Fix:Downloading podcasts with watcher causing duplicate episodes #2122 2023-09-30 15:12:37 -05:00
advplyr
1dc369180c Fix:Home page recent series shelf respect hide single book series library setting #2134 2023-09-30 14:32:40 -05:00
advplyr
8d3a326216 Fix:Newest episodes home page shelf #2119 2023-09-30 14:19:10 -05:00
mikiher
1d3ad38187 [cleanup] refactor OpenLib sort into getOpenLibResult 2023-09-30 18:08:03 +00:00
advplyr
1b22205f74 Update:Add libraryItems table index to improve performance #2073 2023-09-30 12:39:16 -05:00
92Kev
826fee4590 Update nl.json
Added Dutch translations where they were missing
2023-09-30 16:42:28 +02:00
advplyr
f0929729a3 Fix:Adding new podcast with auto download episodes not setting the schedule #2160 2023-09-29 14:52:04 -05:00
advplyr
98ed2e01cc Fix:Scanner overwriting metadata when metadata file is not stored with items #2155 2023-09-28 17:23:52 -05:00
advplyr
ed82a5aa19 Update:Library folder path editable in library edit modal until submit #2150 2023-09-27 17:50:32 -05:00
advplyr
d7b2476473 Merge pull request #2146 from JBlond/master
Update German translation strings.
2023-09-26 17:18:18 -05:00
JBlond
ee162f468a Adjust Message according to https://de.wikipedia.org/wiki/Durchkopplung#Fremdsprachliche_Begriffe
as suggested by https://github.com/vanto
2023-09-26 09:51:34 +02:00
advplyr
0d5a30b214 Update JWT auth extractors, add state in openid redirect, add back cors for api router 2023-09-25 17:05:58 -05:00
JBlond
cb6678fa71 Update German translation strings. 2023-09-25 11:16:57 +02:00
advplyr
10011d3886 Add:Remove option for authors & show authors with 0 books on authors page #2124 2023-09-24 17:06:32 -05:00
advplyr
0367d9ec2a Fix:OPF files creating empty tags and genres #2142 2023-09-24 16:15:42 -05:00
advplyr
26f520ca4a Fix:Listening sessions page showing filtered user for open listening sessions #2136 2023-09-24 15:59:29 -05:00
advplyr
e282142d3f Add authentication page in config, add /auth-settings GET endpoint, remove authOpenIDCallbackURL server setting 2023-09-24 15:36:35 -05:00
advplyr
7ba10db7d4 Update login button openid and google urls 2023-09-24 12:39:38 -05:00
advplyr
f6de373388 Update /status endpoint to return available auth methods, fix socket auth, update openid to use username instead of email 2023-09-24 12:36:36 -05:00
advplyr
8683fc9fe4 Fix:Show series name when collapsing sub-series #2140 2023-09-23 14:38:30 -05:00
advplyr
fd0920c808 Fix:Updating RSS feeds with new episodes #2139 2023-09-23 14:27:13 -05:00
advplyr
9922294507 Fix setting tokenSecret on init 2023-09-23 13:42:28 -05:00
advplyr
f42ab45e1b Update passwordless root user check to user user.type instead of user.id 2023-09-23 13:30:28 -05:00
lukeIam
7a131880e5 show/hide of login buttons 2023-09-23 17:02:27 +01:00
advplyr
a446fc0f20 Merge pull request #2138 from husjon/add-norwegian_translation
Added Norwegian translation
2023-09-23 09:37:55 -05:00
Jon Erling Hustadnes
202c26acf5 translation progress 2023-09-23 16:31:23 +02:00
Jon Erling Hustadnes
f0b2acb4c7 added 'no' to languageCodeMap i18n.js 2023-09-23 16:29:54 +02:00
advplyr
102c90c4e8 Merge pull request #2133 from mfcar/mf/backup
Add more information to the backup page
2023-09-22 16:56:12 -05:00
advplyr
7c484d8e96 Remove commented out backup location 2023-09-22 16:53:59 -05:00
advplyr
e9f0f7d1bc Add LabelBackupLocation translation strings 2023-09-22 16:51:35 -05:00
advplyr
f37ab53eff Update get all backups api endpoint to return backupLocation, display location above backup settings 2023-09-22 16:49:01 -05:00
advplyr
97b0b98605 Merge pull request #2102 from selfhost-alt/sqlite-query-logging
Add ability to enable DEV logs of Sqlite queries
2023-09-22 16:17:32 -05:00
advplyr
1ab34fa77f Update server/Database.js 2023-09-22 16:14:12 -05:00
advplyr
b64ecc7c6f Update server/Database.js 2023-09-22 16:14:00 -05:00
advplyr
a11fc214e9 Merge pull request #2099 from mikiher/Fuzzy-Matching
Fuzzy Matching V1
2023-09-22 16:07:17 -05:00
advplyr
61c48602e8 Add jsdocs to BookFinder search functions 2023-09-22 16:03:41 -05:00
Jon Erling Hustadnes
452d59dcf6 copy of en-us.json to no.json 2023-09-22 12:31:38 +02:00
advplyr
5e976c08af Update cover API endpoint to only load necessary data from DB #2073 2023-09-21 16:57:48 -05:00
advplyr
f1cce76e2c Merge pull request #2121 from Hallo951/master
Update de.json
2023-09-20 17:32:03 -05:00
advplyr
872fba1103 Merge pull request #2129 from mikiher/URI-encoding-2
Use encodeURIComponent for text inputs in Match.vue
2023-09-20 16:49:18 -05:00
mfcar
944f5950ca File missing 2023-09-20 22:36:30 +01:00
mfcar
bfa87a2131 Add a way to see the backup location 2023-09-20 22:33:58 +01:00
lukeIam
0e75c80627 prepare show/hide of login buttons 2023-09-20 19:45:32 +01:00
lukeIam
2c25f64652 Add /auth_methods route 2023-09-20 19:16:08 +01:00
lukeIam
45cf00bd04 fix openid + jwt auth 2023-09-20 19:06:16 +01:00
lukeIam
f6113e85c7 cookie lifetime 2023-09-20 18:48:57 +01:00
lukeIam
2c90bba774 small refactorings 2023-09-20 18:37:55 +01:00
lukeIam
51b0750a3f Merge remote-tracking branch 'origin/master' into auth_passportjs 2023-09-20 17:34:29 +01:00
mikiher
6eab985b1e Use encodeURIComponent for text inputs 2023-09-20 11:25:21 +00:00
mikiher
81a9b8d158 Merge branch 'advplyr:master' into Fuzzy-Matching 2023-09-20 13:12:18 +03:00
mfcar
9519f6418d Now, whenever someone requests a backup file, it will automatically suggest a default file name for the downloaded file. 2023-09-19 22:37:57 +01:00
advplyr
9967a5dc66 Fix:Set ebookFormat on scans #2126 2023-09-19 15:42:38 -05:00
Hallo951
9382055bf2 Update de.json 2023-09-19 09:39:46 +02:00
advplyr
604f52762b Merge pull request #2120 from itzexor/x-accel-encode
[server] x-accel: encode all paths to URIs
2023-09-18 17:51:53 -05:00
advplyr
ae88a4d20a Fix:Matching a library with no items not removing library scan #2118 2023-09-18 17:38:45 -05:00
advplyr
b5a27226cc Fix:Misleading log on cover manager 2023-09-18 16:45:30 -05:00
advplyr
2c71324381 Fix:Book re-scan properly checking if existing coverPath exists #2110 2023-09-18 16:43:43 -05:00
James Ross
207ba7ec8e x-accel: encode all paths to URIs
updates util function  encodeUriPath to use node:url with a file:// path
prefix, and updates all instances x-accel redirection to use this helper
util instead of sending unencoded paths into the header.
2023-09-18 13:08:19 -07:00
advplyr
e56b8edc0a Version bump 2.4.3 2023-09-17 16:13:19 -05:00
advplyr
8ab0a0a14d Update personalized shelves logs to dev logs 2023-09-17 16:09:21 -05:00
advplyr
4e01722ba6 Merge pull request #2103 from selfhost-alt/faster-scan-for-empty-series
Scan for empty book series more efficiently
2023-09-17 15:54:33 -05:00
advplyr
87eaacea22 Fix empty podcast and empty book queries when cleaning db on init 2023-09-17 15:53:25 -05:00
advplyr
3ad4f05449 Merge branch 'master' into faster-scan-for-empty-series 2023-09-17 15:47:06 -05:00
advplyr
817be40959 Merge pull request #2101 from selfhost-alt/fix-parse-full-name-typo
Fix typo in fixParsedNameCase
2023-09-17 15:43:25 -05:00
advplyr
d18592eaeb Fix:Duplicate series and authors being added on matches and scans #2106 2023-09-17 15:29:39 -05:00
advplyr
0aae672e19 Fix:Scanner purge cover cache when extracting from audio file 2023-09-17 14:53:25 -05:00
lukeIam
0a6cd89090 Allow rest mode login (?isRest=true) 2023-09-17 18:42:42 +01:00
advplyr
cfd9a01da7 Fix:Server crash when removing item from playlist #2115 2023-09-17 12:40:13 -05:00
lukeIam
942aa93f57 Fix: local login not possible 2023-09-16 19:45:04 +00:00
lukeIam
763c0f4a3d add missing await 2023-09-16 18:51:29 +00:00
lukeIam
7af3033f8d Fix: ci error - no token sercret 2023-09-16 18:42:48 +00:00
lukeIam
91d8451ab3 Remove log messages 2023-09-16 18:22:23 +00:00
lukeIam
6aaf3f0f02 Fix bug with undefined property 2023-09-16 18:22:11 +00:00
lukeIam
226a774ab9 Merge remote-tracking branch 'origin/master' into auth_passportjs 2023-09-16 18:02:51 +00:00
Selfhost Alt
19cf3bfb9f Fix query to actually return empty series 2023-09-15 13:32:21 -07:00
mikiher
67bbe21513 Make quick-match more conservative 2023-09-15 09:24:19 +00:00
Selfhost Alt
b668c6e37a Remove stray quote 2023-09-14 23:04:47 -07:00
Selfhost Alt
71762ef837 Newline before printing query 2023-09-14 23:01:40 -07:00
Selfhost Alt
b1524d245e Add ability to enable DEV logs of Sqlite queries 2023-09-14 22:52:43 -07:00
Selfhost Alt
8b39b01269 Scan for empty book series more efficiently 2023-09-14 22:35:33 -07:00
Selfhost Alt
f7849d2956 Fix typo in fixParsedNameCase 2023-09-14 22:12:22 -07:00
mikiher
ac746f199b Fuzzy Matching V1 2023-09-14 21:32:20 +00:00
lukeIam
af4c35069b Use a short-time cookie to remember where to callback to 2023-09-14 18:49:19 +01:00
advplyr
fea28351f9 Version bump 2.4.2 2023-09-13 16:48:28 -05:00
advplyr
bb124d3274 Merge pull request #2089 from Lionfox2/patch-2
Update de.json
2023-09-13 16:46:39 -05:00
advplyr
6cd1b82ada Merge pull request #2083 from Machou/patch-1
Update fr.json
2023-09-13 16:46:23 -05:00
advplyr
c701617fbb Merge pull request #2079 from Nab0y/master
Update Russian localization
2023-09-13 16:46:02 -05:00
lukeIam
405c954b65 Updated + first rough implementation 2023-09-13 16:35:39 +00:00
Lionfox2
5d84c426fe Update de.json
The more suitable translation for "discover" is "Entdecken", same wording as in other media apps like Spotify
2023-09-12 23:30:58 +02:00
advplyr
083ba2fe19 Fix:Podcast download queue page available on refresh #2088 2023-09-12 15:35:14 -05:00
advplyr
1024bc5a75 Fix:Podcast library stat for total size #2072 2023-09-12 13:43:28 -05:00
advplyr
9553c19b33 Fix:Authors dropdown to use filter data instead of API endpoint #2077 2023-09-12 12:33:41 -05:00
Machou
2cbc9a07cb Update fr.json 2023-09-12 18:11:38 +02:00
advplyr
ab97a9d613 Fix:Crash when updating book author or series that includes an apostrophe #2070 2023-09-12 10:41:39 -05:00
advplyr
f1a7fd0d50 Fix:Podcast library include number of incomplete episodes in home page shelf api request #2081 2023-09-11 17:51:39 -05:00
Dmitry Naboychenko
e9d7efbc5c Update russian localization 2023-09-11 21:35:36 +03:00
lukeIam
f0f03efe17 Merge remote-tracking branch 'origin/master' into auth_passportjs 2023-09-10 13:11:35 +00:00
advplyr
6e5d334874 Version bump 2.4.1 2023-09-09 15:47:17 -05:00
advplyr
6822628994 Fix:Missing narrators library filter 2023-09-09 15:46:33 -05:00
advplyr
98d9fd8c32 Fix:Get all items api endpoint support providing no limit #2067 2023-09-09 15:01:58 -05:00
advplyr
e2cca60853 Fix:Crash on podcast library page sort by title #2069 2023-09-09 14:56:36 -05:00
advplyr
e80b313a7b Fix:Server crash when quick match with find covers setting enabled #2068 2023-09-09 08:57:59 -05:00
advplyr
b09b95ef24 Fix:Browse folders when adding new library folder crashing server #2065 2023-09-09 07:47:17 -05:00
advplyr
aec45d04f7 Version bump 2.4.0 2023-09-08 17:26:21 -05:00
advplyr
87d037cb0a Fix clean database method and version bump 2.3.5 2023-09-08 17:20:39 -05:00
advplyr
f6baf06164 Version bump 2.3.4 2023-09-08 16:20:03 -05:00
advplyr
7e75845851 Ignore podcast directory from watcher for additional 5s after downloading new episode 2023-09-08 15:12:39 -05:00
advplyr
2a11932822 Scanner ignore .part files #2063 2023-09-08 14:50:59 -05:00
advplyr
80fee92037 Update "disable watcher" server setting to display as "enable watcher" #2055 2023-09-08 14:28:21 -05:00
advplyr
d0c02a801a Update open rss feed prevent indexing - dont include block tags when not preventing indexing 2023-09-08 14:03:12 -05:00
advplyr
9e13c64408 Handle sorting when collapsing by series and filtering by series on library page 2023-09-08 13:42:19 -05:00
advplyr
826963bf00 Add api route for changing sorting prefixes, update default sorting prefixes to include a 2023-09-08 12:32:30 -05:00
advplyr
39b6ede1e9 Add support for hide from continue listening 2023-09-08 11:20:22 -05:00
advplyr
066d853156 Add support for hide from continue listening on new home page shelves route 2023-09-07 17:49:35 -05:00
advplyr
efae529fac Add cover finder to new book scanner 2023-09-06 17:48:50 -05:00
advplyr
934c0b9093 Fix watcher scanner detecting existing items 2023-09-06 15:43:59 -05:00
advplyr
f02992dd4d Remove the setting of file permissions #2057 2023-09-06 07:12:11 -05:00
advplyr
10011bd6a3 Add startup function to remove invalid records from DB 2023-09-05 17:58:13 -05:00
advplyr
a44ee913c4 Fix crash on get recent series home page shelf endpoint 2023-09-05 16:10:46 -05:00
advplyr
adccccbd7a Remove index creation from migration file 2023-09-05 15:36:19 -05:00
advplyr
05b1b2be36 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2023-09-05 15:07:49 -05:00
advplyr
7cc35a2cbe Remove indexes for columns that didnt exist in 2.3.3 2023-09-05 15:07:41 -05:00
advplyr
8d479b6e34 Merge pull request #2054 from JBlond/master
Follow-up update for German strings
2023-09-05 08:23:04 -05:00
JBlond
74d300f048 Follow up update for German string to 469167df66.
Plus addtional string translation
2023-09-05 11:36:41 +02:00
advplyr
1dd1fe8994 Update match all books to load items from DB, remove library items loading to memory on init 2023-09-04 16:33:55 -05:00
advplyr
03115e5e53 Replace old items filter/sort api endpoint with new, handle collapse sub-series 2023-09-04 15:26:07 -05:00
advplyr
b1c07834be Remove force re-scan and old scanner logic 2023-09-04 13:59:37 -05:00
advplyr
b9da3fa30e Add new podcast scanner and remove old scanner 2023-09-04 11:50:55 -05:00
advplyr
42ff3d8314 Add new library item scanner 2023-09-03 17:51:58 -05:00
advplyr
e63aab95d8 Update new library scanner to handle metadata file changes 2023-09-03 15:14:58 -05:00
advplyr
9123dcb365 Remove series search api endpoint, update authors and series to load from db 2023-09-03 10:49:02 -05:00
advplyr
7567e91878 Update get library item api endpoint to remove unnecessary authors include query param 2023-09-03 10:04:14 -05:00
advplyr
1b1bdea3c8 Remove authors search api endpoint 2023-09-03 09:54:23 -05:00
advplyr
2df95c1712 Updates for new book scanner 2023-09-02 17:49:28 -05:00
advplyr
4ad1cd2968 Fix:Batch API endpoints crash on reset library filter data 2023-09-02 10:46:47 -05:00
advplyr
0ecfdab463 Update new library scanner for scanning in new books 2023-09-01 18:01:17 -05:00
advplyr
75276f5a44 Fix:Server crash when updating cover to a directory #2007 2023-08-30 18:05:52 -05:00
advplyr
4585d2816b Fix:Comic reader not detecting file sort order when number is more than 5-digit #2036 2023-08-29 15:47:34 -05:00
advplyr
f8f94f2a6d Update new library scanner to check for cover images and ebooks 2023-08-28 17:50:21 -05:00
advplyr
2c8448d147 Updates to new library scanner and adding jsdoc types 2023-08-27 17:19:57 -05:00
advplyr
ea1d051cfb Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2023-08-26 16:33:34 -05:00
advplyr
a38e43213d Fix:Server crash when deleting library item #2031 2023-08-26 16:33:27 -05:00
advplyr
6cac8fcd6e Merge pull request #2030 from bluecmd/debian-tar-owner
Fix system file ownership for Debian package
2023-08-25 16:19:26 -05:00
Christian Svensson
8e65c78869 Fix system file ownership for Debian package
Extract ffmpeg and tone but ignore the ownership information from the tar archive. This will ensure that those files are owned by root, not the UIDs/GIDs the tar files happen to contain.
2023-08-25 18:14:17 +02:00
advplyr
a3899b68e1 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2023-08-24 17:55:35 -05:00
advplyr
1187f91063 Update jsdoc defs for models 2023-08-24 17:55:29 -05:00
advplyr
7c288a5ff9 Merge pull request #2027 from realmain/docker-compose
Added podcasts volume to docker compose
2023-08-24 15:22:24 -05:00
advplyr
e0dae44c7d Update:Show published year on library page when sorting books by published year #2017 2023-08-23 18:01:58 -05:00
realmain
754498958d Added podcasts volume 2023-08-23 15:55:07 -07:00
advplyr
ec15978e26 Merge pull request #2026 from shawnphoffman/shawn/rss-feeds
Add config page for all RSS feeds
2023-08-22 16:42:31 -05:00
advplyr
469167df66 Update get all feeds route to be admin-only, map translation strings 2023-08-22 16:37:22 -05:00
advplyr
e7c43a3f32 Merge pull request #2009 from JBlond/master
Update German strings
2023-08-22 16:08:47 -05:00
Shawn Hoffman
24989e73ae Merge branch 'master' into shawn/rss-feeds 2023-08-22 10:30:16 -07:00
Shawn Hoffman
13427b9f70 Add RSS feeds config page 2023-08-22 10:11:10 -07:00
Mario
adafefecd4 Merge branch 'advplyr:master' into master 2023-08-22 11:39:13 +02:00
advplyr
6f96b069b5 Fix search query 2023-08-21 16:33:16 -05:00
advplyr
6c1b4e3a36 Update db model references 2023-08-20 13:34:03 -05:00
advplyr
21343ffbd1 Update numIssues on filter data, fix watcher scanning in new items 2023-08-20 13:16:53 -05:00
advplyr
4f94deefa0 Fix remove items with issues API route & remove old endpoints 2023-08-19 17:12:24 -05:00
advplyr
332078e6c1 Update library stats API route to load from db 2023-08-19 16:53:33 -05:00
advplyr
ff0d6326d3 Update OPML api route to load podcasts from db 2023-08-19 15:19:27 -05:00
advplyr
8d451217a3 Update recent-episodes API route to load from db 2023-08-19 14:49:06 -05:00
advplyr
f21d69339f Update search query to use user permissions 2023-08-19 14:11:34 -05:00
advplyr
c77cead9ae Update search endpoints to search db directly 2023-08-19 13:59:22 -05:00
advplyr
b334d40998 Update library routes to middlewareNew 2023-08-18 17:12:15 -05:00
advplyr
4e4a976050 Update get library series api endpoint to load from db 2023-08-18 17:08:34 -05:00
advplyr
9d7d4c6902 Update filterData for authors/series when added/removed 2023-08-18 14:40:36 -05:00
advplyr
7222171c5b Update checking empty series to load from Db 2023-08-17 17:58:57 -05:00
advplyr
361732a463 Update get User API endpoint to load media progress from db 2023-08-17 17:26:12 -05:00
advplyr
1ebe8a6f4c Update scanner to load library items from db 2023-08-16 18:08:00 -05:00
advplyr
a98942a361 Add jsdoc types to remaining models 2023-08-16 16:38:48 -05:00
advplyr
0bc89cd40f Fix collapse series and sort by title without ignore prefix 2023-08-16 15:24:56 -05:00
advplyr
2ae86ab5bb Fix Library undefined sequelize 2023-08-16 14:49:06 -05:00
advplyr
c707bcf0f6 Add jsdoc types for models 2023-08-15 18:03:43 -05:00
JBlond
10040ba9fa Update German strings as follow up of 09eefae808 2023-08-15 15:38:27 +02:00
advplyr
7afda1295b Update Author model to define types 2023-08-14 18:22:38 -05:00
advplyr
6d6e8613cf Update library API endpoints to load library items from db 2023-08-13 17:45:53 -05:00
advplyr
3651fffbee Update library filter data to load from db and cache, update rss feed routes to load library items from db 2023-08-13 15:10:26 -05:00
advplyr
8d03b23f46 Update MiscController api routes to load library items from db 2023-08-13 13:10:34 -05:00
advplyr
fc44c801f2 Update playlist API endpoints to load library items from DB 2023-08-13 11:22:38 -05:00
advplyr
6056c14926 Update podcast controller to load library items from db 2023-08-12 17:29:08 -05:00
advplyr
f465193b9c Update User.toJSONForPublic to remove mostRecent key and session key only includes the PlaybackSession 2023-08-12 16:11:58 -05:00
advplyr
09c9c28028 Remove test API endpoint for albums 2023-08-12 15:54:59 -05:00
advplyr
f1130eb63a Update MeController api endpoints to load library items from DB 2023-08-12 15:52:09 -05:00
advplyr
db80cec168 Update collection API routes to load libraryItems from DB 2023-08-12 15:01:27 -05:00
lukeIam
dd9a3858d7 Merge remote-tracking branch 'origin/master' into auth_passportjs 2023-08-12 16:44:44 +02:00
advplyr
38029d1202 Update library collections api endpoint to use libraryItems from db 2023-08-11 17:49:06 -05:00
advplyr
aac2879652 Fix library query sort by title, add indexes for books and libraryItems 2023-08-10 17:46:27 -05:00
advplyr
8c9fc3ddb5 Fix discover home page shelf query, add indexes for libraryItems and mediaProgresses table 2023-08-09 17:48:31 -05:00
advplyr
33e04d0cbb Update home page queries merging listen/read shelves, limit recent shelves to 60 days out 2023-08-07 17:59:04 -05:00
advplyr
fbb5fd41fb Merge pull request #1994 from NiclasHaderer/fix-backup-server-crash
Fix: server crash when uploading invalid backup file
2023-08-07 17:17:57 -05:00
advplyr
43a5296dd7 Update server/managers/BackupManager.js 2023-08-07 17:14:47 -05:00
advplyr
345ff1aa66 Update author API endpoints to load library items from DB 2023-08-06 15:06:45 -05:00
advplyr
56e3449db6 Remove media progress for podcast episodes when episode is removed 2023-08-06 14:18:51 -05:00
advplyr
1372c24535 Update queries to account for user permissions 2023-08-06 13:36:58 -05:00
Niclas Haderer
409c5f7b75 fix: the server does not crash any more when an invalid backup file is uploaded 2023-08-06 10:05:53 +02:00
advplyr
83d0db0607 Fix getting default library id for user 2023-08-05 16:37:26 -05:00
advplyr
91b6c4412d Add remaining personalized shelf queries for podcasts 2023-08-05 15:28:16 -05:00
advplyr
09eefae808 Add remaining personalized shelf queries, update book libraries home page to use new API endpoint 2023-08-05 14:01:16 -05:00
advplyr
80b3bfea51 Add recent series home page shelf query 2023-08-04 18:07:55 -05:00
advplyr
516298b5b2 Update continue series shelf to include rss feed 2023-08-04 17:26:43 -05:00
advplyr
8edab98163 Update continue series shelf queries 2023-08-04 17:24:06 -05:00
advplyr
58da095bcf Update query for continue series shelf 2023-08-03 18:14:25 -05:00
advplyr
b9633691f4 Add new personalized home page shelves API endpoint 2023-08-02 18:29:28 -05:00
advplyr
7ec1d8ee5f Fix socket authority check for valid client 2023-08-01 16:34:01 -05:00
advplyr
83a1374e79 Merge pull request #1985 from tomazed/translation-fr
fr translation update
2023-08-01 16:23:28 -05:00
Tomazed
5ef00bac92 fr translation update 2023-08-01 11:51:15 +02:00
advplyr
95c4b3862b Include library item podcast queries 2023-07-31 17:59:51 -05:00
advplyr
eeaf012cdc Update new library item API endpoint to handle collapse series 2023-07-30 17:51:44 -05:00
advplyr
11120a3765 Update version specific db migration check 2023-07-30 11:09:49 -05:00
advplyr
4d0acb30ba Update bookSeries & bookAuthors table to include createdAt timestamp 2023-07-29 17:25:11 -05:00
advplyr
4dbe8d29d9 Update db migration for duration, size, lastFirst, and ignore prefix columns 2023-07-28 18:03:31 -05:00
advplyr
0ca4ff4fca Update readme 2023-07-27 18:17:29 -05:00
advplyr
8be1651c6b Fix:Sync local media progress when library item not found #1971 2023-07-26 18:08:55 -05:00
advplyr
af2db86d1a Merge pull request #1965 from petras-sukys/l10n/lt
Added Lithuanian translation
2023-07-25 15:03:07 -05:00
Petras Šukys
57c834f88d Added EOL at EOF 2023-07-25 22:20:13 +03:00
Petras Šukys
65fdebde20 Added language entry for Lithuanian (Lietuvių) 2023-07-25 22:16:13 +03:00
Petras Šukys
b58e42ebf3 Lithuanian translation, part 5 2023-07-25 22:11:12 +03:00
Petras Šukys
b2d45f598b Merge branch 'master' into l10n/lt 2023-07-25 08:03:19 +03:00
Petras Šukys
09c4e690c6 Lithuanian translation, part 4 2023-07-25 07:57:51 +03:00
Petras Šukys
67ba481dca Lithuanian translation, part 3 2023-07-24 22:49:44 +03:00
advplyr
710a62c2af Update:Load playlists only when needed & remove podcast episode from playlist when deleted 2023-07-23 09:42:57 -05:00
advplyr
5a9eed0a5a Update:Only load collections when needed 2023-07-22 16:18:55 -05:00
advplyr
354e16e462 Update:Only load Users when needed 2023-07-22 15:32:20 -05:00
advplyr
1d974375a0 Update:Only load libraries from db when needed 2023-07-22 14:25:20 -05:00
advplyr
1c40af3eef Update:Sequelize transactionType to IMMEDIATE to fix SQLITE_BUSY #1910 2023-07-22 11:30:29 -05:00
advplyr
daa8c4cd67 Update:Remove sort index from podcast episodes 2023-07-22 09:24:46 -05:00
advplyr
d5da4441cd Fix:Set podcast episode audio file index to 1 on scans 2023-07-22 09:05:43 -05:00
advplyr
80aea0c82d Fix:Save metadata files when updating library items #1952 2023-07-22 07:50:47 -05:00
Petras Šukys
14836eeb0d Lithuanian translation, part 2 2023-07-22 12:00:29 +03:00
Petras Šukys
85e9883d3e Lithuanian translation, part 1 2023-07-22 11:53:37 +03:00
Petras Šukys
80ca73e491 Copied en-us language file to lt 2023-07-22 10:52:06 +03:00
advplyr
22323f606d Fix:RSS feed covers #1948 2023-07-21 16:59:00 -05:00
advplyr
01b65eb678 Fix:Initialize Feed entityUpdatedAt to prevent updated RSS feed every request 2023-07-21 16:18:58 -05:00
advplyr
d1d94c37a7 Update:Remove --max-old-space-size from Dockerfile 2023-07-20 17:00:08 -05:00
advplyr
838a24c8a5 Update:Remove healthcheck from Dockerfile 2023-07-20 16:59:46 -05:00
advplyr
3f380b0839 Fix:Parsing authors from meta tags removes duplicates #1932 2023-07-20 16:55:49 -05:00
advplyr
7fdf1a1d7f Merge pull request #1946 from rasmuslos/master
Fix byte conversion
2023-07-20 15:55:16 -05:00
advplyr
c2793fe29b Fix:Crash when author is set without a name #1934 2023-07-19 17:13:57 -05:00
Rasmus Krämer
38596d017f Fix byte conversion 2023-07-19 23:59:00 +02:00
advplyr
24b9ac6a68 Version bump 2.3.3 2023-07-19 16:32:41 -05:00
advplyr
9a5ed64fae Update database loading library items incrementally to reduce mem usage 2023-07-19 15:36:18 -05:00
advplyr
c2af96e7cd Fix:New user setting tags array #1933 2023-07-18 17:43:33 -05:00
advplyr
104cadb0b3 Version bump 2.3.2 2023-07-17 17:49:12 -05:00
advplyr
6814adffcc Update:Only load feeds when needed 2023-07-17 16:48:46 -05:00
advplyr
20c11e381e Update docker file heap size 2023-07-17 14:41:21 -05:00
advplyr
b5952f16eb Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2023-07-17 13:58:25 -05:00
advplyr
5b6878e5de Fix:Crash on local playback sessions #1912 2023-07-17 13:58:19 -05:00
advplyr
89a25bcf39 Merge pull request #1917 from burghy86/patch-10
Update it.json
2023-07-17 08:09:28 -05:00
advplyr
d0cd512be8 Fix:Crash when updating sequence on series #1919 2023-07-17 08:09:08 -05:00
advplyr
3543dea0fb Merge pull request #1923 from JBlond/master
Update German language file
2023-07-17 08:09:07 -05:00
advplyr
1949e25ccb Merge pull request #1925 from Machou/patch-1
Update fr.json
2023-07-17 08:08:43 -05:00
advplyr
b715ef3bfc Increase heap size to 4gb in Dockerfile 2023-07-17 07:48:23 -05:00
Machou
954050df81 Update fr.json 2023-07-17 14:28:56 +02:00
JBlond
e4aa7f10fa Update German language file 2023-07-17 10:02:54 +02:00
advplyr
2afd0e2acd Update dbMigration for old main library ids 2023-07-16 16:39:59 -05:00
advplyr
0829237166 Fix:Libraries out of order #1911 2023-07-16 15:43:46 -05:00
advplyr
541975f038 Version bump 2.3.1 2023-07-16 15:34:35 -05:00
advplyr
01bf58ab97 Fix createAuthor 2023-07-16 15:29:43 -05:00
advplyr
d99b2c25e8 Fixes for db migration & local playback sessions 2023-07-16 15:05:51 -05:00
burghy86
a31df5ff81 Update it.json
traslate new string and fix error
2023-07-16 21:50:15 +02:00
advplyr
63e5cf2e60 Fix:Accessing series page for some users #787 2023-07-16 08:39:08 -05:00
advplyr
7beca048e7 Version bump v2.3.0 2023-07-15 15:29:25 -05:00
advplyr
ec998dc1ac Update:Podcast library item covers show number of episodes incomplete #782 2023-07-15 14:45:08 -05:00
advplyr
ddc54c8811 Update:Downloading library item shows log on the server with username #1461 2023-07-15 13:39:12 -05:00
advplyr
72e306935f Update:Support and as separator between multiple authors #1790 2023-07-15 13:28:31 -05:00
advplyr
96a7c7f4d1 Fix:Embedded chapters with invalid IDs, update chapter ids to always be the index #1783 2023-07-15 12:46:51 -05:00
advplyr
9c65d655b8 Fix:Realtime update cover on cover tab in item edit modal 2023-07-15 12:37:33 -05:00
advplyr
b108f2241b Add:Library filter for publishers & link to publisher filter on book page #1813 2023-07-15 12:22:13 -05:00
advplyr
9439acf300 Merge pull request #1906 from warnwar/master
stop opf importer from adding duplicate info
2023-07-15 11:44:41 -05:00
advplyr
d181e66d83 Update server/utils/parsers/parseOpfMetadata.js 2023-07-15 11:41:44 -05:00
advplyr
a87c3f2c77 Update server/utils/parsers/parseOpfMetadata.js 2023-07-15 11:41:40 -05:00
advplyr
2834f6077e Update server/utils/parsers/parseOpfMetadata.js 2023-07-15 11:41:35 -05:00
advplyr
918013ccb3 Add:Option on podcast page to mark all episodes as finished/unfinished #1862 2023-07-15 11:27:06 -05:00
advplyr
4c4672c6c1 Update:Item page UI for details that take up multiple lines 2023-07-15 11:00:07 -05:00
advplyr
b3991574c7 Merge pull request #1907 from advplyr/sqlite_2
Migration to use sqlite3
2023-07-14 15:11:23 -05:00
WarWar
47b9ee557e stop opf importer from adding duplicate info 2023-07-14 05:15:29 +00:00
Jorge
679bdf36b1 Add CodeQL workflow 2023-07-03 09:15:04 +02:00
lukeIam
95e6fef3d1 Merge remote-tracking branch 'origin/master' into auth_passportjs 2023-05-27 10:56:05 +02:00
advplyr
4359ca28df Fix XAccel issue 2023-04-29 16:05:05 -05:00
advplyr
8b685436de Merge 2023-04-29 15:49:04 -05:00
advplyr
8d0064763c Merge branch 'master' into auth_passportjs 2023-04-16 10:08:17 -05:00
advplyr
7010a13648 Fixes for passport local and allow empty password 2023-04-16 10:08:13 -05:00
lukeIam
812395b21b Merge remote-tracking branch 'origin/master' into auth_passportjs 2023-04-14 20:27:43 +02:00
lukeIam
62b0940766 Added passport-openidconnect implementation 2023-04-14 20:26:29 +02:00
lukeIam
08676a675a Fix: small problem with this context in Auth.js 2023-03-24 18:31:58 +01:00
lukeIam
be53b31712 Merge remote-tracking branch 'origin/master' into auth_passportjs 2023-03-24 18:23:08 +01:00
lukeIam
e1ddb95250 Inital passportjs integration 2023-03-24 18:21:25 +01:00
415 changed files with 55884 additions and 35925 deletions

View File

@@ -1,5 +1,5 @@
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
ARG VARIANT=16
ARG VARIANT=20
FROM mcr.microsoft.com/devcontainers/javascript-node:0-${VARIANT} as base
# Setup the node environment

View File

@@ -5,5 +5,6 @@ module.exports.config = {
ConfigPath: Path.resolve('config'),
MetadataPath: Path.resolve('metadata'),
FFmpegPath: '/usr/bin/ffmpeg',
FFProbePath: '/usr/bin/ffprobe'
FFProbePath: '/usr/bin/ffprobe',
SkipBinariesCheck: true
}

View File

@@ -8,7 +8,7 @@
// Append -bullseye or -buster to pin to an OS version.
// Use -bullseye variants on local arm64/Apple Silicon.
"args": {
"VARIANT": "16"
"VARIANT": "20"
}
},
"mounts": [

8
.editorconfig Normal file
View File

@@ -0,0 +1,8 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true

View File

@@ -1,40 +1,50 @@
name: 🐞 Bug Report
description: File a bug/issue
title: "[Bug]: "
labels: ["bug", "triage"]
description: File a bug/issue and help us improve Audiobookshelf
title: '[Bug]: '
labels: ['bug', 'triage']
body:
- type: markdown
attributes:
value: "### Please first search for your issue and check the [docs](https://audiobookshelf.org/docs)."
value: 'Thank you for filing a bug report! 🐛'
- type: markdown
attributes:
value: "### Mobile app issues report [here](https://github.com/advplyr/audiobookshelf-app/issues/new/choose)."
value: 'Please first search for your issue and check the [docs](https://audiobookshelf.org/docs).'
- type: markdown
attributes:
value: "### Join the [discord server](https://discord.gg/pJsjuNCKRq) for questions or if you are not sure about a bug."
value: 'Report issues with the mobile app [here](https://github.com/advplyr/audiobookshelf-app/issues/new/choose).'
- type: markdown
attributes:
value: "## Be as descriptive as you can. Include screenshots, error logs, browser, file types, everything you can think of that might be relevant."
value: 'Join the [discord server](https://discord.gg/HQgCbd6E75) for questions or if you are not sure about a bug.'
- type: textarea
id: what-happened
attributes:
label: Describe the issue
description: What happened & what did you expect to happen
label: What happened?
placeholder: Tell us what you see!
validations:
required: true
- type: textarea
id: what-was-expected
attributes:
label: What did you expect to happen?
placeholder: Tell us what you expected to see! Be as descriptive as you can and include screenshots if applicable.
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to reproduce the issue
value: "1. "
value: '1. '
validations:
required: true
- type: markdown
attributes:
value: '## Install Environment'
- type: input
id: version
attributes:
label: Audiobookshelf version
description: Do not put 'Latest version', please put the actual version here
placeholder: "e.g. v1.6.60"
placeholder: 'e.g. v1.6.60'
validations:
required: true
- type: dropdown
@@ -44,7 +54,45 @@ body:
options:
- Docker
- Debian/PPA
- Windows Tray App
- Built from source
- Other
- Other (list in "Additional Notes" box)
validations:
required: true
required: true
- type: dropdown
id: server-os
attributes:
label: What OS is your Audiobookshelf server hosted from?
options:
- Windows
- macOS
- Linux
- Other (list in "Additional Notes" box)
validations:
required: true
- type: dropdown
id: desktop-browsers
attributes:
label: If the issue is being seen in the UI, what browsers are you seeing the problem on?
options:
- Chrome
- Firefox
- Safari
- Edge
- Firefox for Android
- Chrome for Android
- Safari on iOS
- Other (list in "Additional Notes" box)
- type: textarea
id: logs
attributes:
label: Logs
description: Please include any relevant logs here. This field is automatically formatted into code, so you do not need to include any backticks.
placeholder: Paste logs here
render: shell
- type: textarea
id: additional-notes
attributes:
label: Additional Notes
description: Anything else you want to add?
placeholder: 'e.g. I have tried X, Y, and Z.'

View File

@@ -1,7 +1,7 @@
blank_issues_enabled: false
contact_links:
- name: Discord
url: https://discord.gg/pJsjuNCKRq
url: https://discord.gg/HQgCbd6E75
about: Ask questions, get help troubleshooting, and join the Abs community here.
- name: Matrix
url: https://matrix.to/#/#audiobookshelf:matrix.org

View File

@@ -1,17 +1,63 @@
name: 🚀 Feature Request
description: Request a feature/enhancement
title: "[Enhancement]: "
labels: ["enhancement"]
title: '[Enhancement]: '
labels: ['enhancement']
body:
- type: markdown
attributes:
value: "### Please first search in both issues & discussions for your enhancement."
value: '#### *Mobile app features should be [requested here](https://github.com/advplyr/audiobookshelf-app/issues/new/choose)*.'
- type: markdown
attributes:
value: "### Mobile app features should be requested [here](https://github.com/advplyr/audiobookshelf-app/issues/new/choose)."
value: '## Web/Server Feature Request Description'
- type: markdown
attributes:
value: 'Please first search in both issues & discussions for your enhancement.'
- type: dropdown
id: enhancment-type
attributes:
label: Type of Enhancement
options:
- Server Backend
- Web Interface/Frontend
- Documentation
- type: textarea
id: describe
attributes:
label: Describe the feature/enhancement
label: Describe the Feature/Enhancement
description: Please help us understand what you want.
placeholder: What is your vision?
validations:
required: true
- type: textarea
id: the-why
attributes:
label: Why would this be helpful?
description: Please help us understand why this would enhance your experience.
placeholder: Explain the "why" or "use case".
validations:
required: true
- type: textarea
id: image
attributes:
label: Future Implementation (Screenshot)
description: Please help us visualize by including a doodle or screenshot.
placeholder: How could this look?
validations:
required: true
- type: markdown
attributes:
value: '## Web/Server Current Implementation'
- type: input
id: version
attributes:
label: Audiobookshelf Server Version
description: Do not put 'Latest version', please put your current version number here
placeholder: 'e.g. v1.6.60'
validations:
required: true
- type: textarea
id: current-image
attributes:
label: Current Implementation (Screenshot)
description: What page were you looking at when you thought of this enhancement?
placeholder: If an image is not applicable, please explain why.

65
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,65 @@
name: "CodeQL"
on:
push:
branches: [ 'master' ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ 'master' ]
schedule:
- cron: '16 5 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

View File

@@ -71,7 +71,7 @@ jobs:
with:
tags: ${{ github.event.inputs.tags || steps.meta.outputs.tags }}
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
platforms: linux/amd64,linux/arm64
push: true
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max

30
.github/workflows/i18n-integration.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Verify all i18n files are alphabetized
on:
pull_request:
paths:
- client/strings/** # Should only check if any strings changed
push:
paths:
- client/strings/** # Should only check if any strings changed
jobs:
update_translations:
runs-on: ubuntu-latest
steps:
# Check out the repository
- name: Checkout repository
uses: actions/checkout@v4
# Set up node to run the javascript
- name: Set up node
uses: actions/setup-node@v4
with:
node-version: "20"
# The only argument is the `directory`, which is where the i18n files are
# stored.
- name: Run Update JSON Files action
uses: audiobookshelf/audiobookshelf-i18n-updater@v1.2.0
with:
directory: "client/strings/" # Adjust the directory path as needed

View File

@@ -16,7 +16,7 @@ jobs:
- name: setup nade
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20
- name: install pkg
run: npm install -g pkg

View File

@@ -0,0 +1,17 @@
name: Dispatch an abs-windows event
on:
release:
types: [published]
workflow_dispatch:
jobs:
abs-windows-dispatch:
runs-on: ubuntu-latest
steps:
- name: Send a remote repository dispatch event
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.ABS_WINDOWS_PAT }}
repository: mikiher/audiobookshelf-windows
event-type: build-windows

37
.github/workflows/unit-tests.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Run Unit Tests
on:
workflow_dispatch:
inputs:
ref:
description: 'Branch/Tag/SHA to test'
required: true
pull_request:
push:
jobs:
run-unit-tests:
name: Run Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout (push/pull request)
uses: actions/checkout@v4
if: github.event_name != 'workflow_dispatch'
- name: Checkout (workflow_dispatch)
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
if: github.event_name == 'workflow_dispatch'
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test

7
.gitignore vendored
View File

@@ -7,11 +7,16 @@
/podcasts/
/media/
/metadata/
test/
/client/.nuxt/
/client/dist/
/dist/
/deploy/
/coverage/
/.nyc_output/
/ffmpeg*
/ffprobe*
sw.*
.DS_STORE
.idea/*
tailwind.compiled.css

17
.prettierrc Normal file
View File

@@ -0,0 +1,17 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 400,
"proseWrap": "never",
"trailingComma": "none",
"overrides": [
{
"files": ["*.html"],
"options": {
"singleQuote": false,
"wrapAttributes": false,
"sortAttributes": false
}
}
]
}

7
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"recommendations": [
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode",
"octref.vetur"
]
}

View File

@@ -16,5 +16,12 @@
},
"editor.formatOnSave": true,
"editor.detectIndentation": true,
"editor.tabSize": 2
"editor.tabSize": 2,
"javascript.format.semicolons": "remove",
"[javascript][json][jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "octref.vetur"
}
}

View File

@@ -1,5 +1,5 @@
### STAGE 0: Build client ###
FROM node:16-alpine AS build
FROM node:20-alpine AS build
WORKDIR /client
COPY /client /client
RUN npm ci && npm cache clean --force
@@ -7,9 +7,10 @@ RUN npm run generate
### STAGE 1: Build server ###
FROM sandreas/tone:v0.1.5 AS tone
FROM node:16-alpine
FROM node:20-alpine
ENV NODE_ENV=production
RUN apk update && \
apk add --no-cache --update \
curl \
@@ -17,7 +18,8 @@ RUN apk update && \
ffmpeg \
make \
python3 \
g++
g++ \
tini
COPY --from=tone /usr/local/bin/tone /usr/local/bin/
COPY --from=build /client/dist /client/dist
@@ -29,9 +31,6 @@ RUN npm ci --only=production
RUN apk del make python3 g++
EXPOSE 80
HEALTHCHECK \
--interval=30s \
--timeout=3s \
--start-period=10s \
CMD curl -f http://127.0.0.1/healthcheck || exit 1
ENTRYPOINT ["tini", "--"]
CMD ["node", "index.js"]

View File

@@ -60,13 +60,13 @@ install_ffmpeg() {
fi
$WGET
tar xvf ffmpeg-git-amd64-static.tar.xz --strip-components=1
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
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"

View File

@@ -48,11 +48,10 @@ Description: $DESCRIPTION"
echo "$controlfile" > dist/debian/DEBIAN/control;
# Package debian
pkg -t node16-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf .
pkg -t node18-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf .
fakeroot dpkg-deb --build dist/debian
fakeroot dpkg-deb -Zxz --build dist/debian
mv dist/debian.deb "dist/$OUTPUT_FILE"
chmod +x "dist/$OUTPUT_FILE"
echo "Finished! Filename: $OUTPUT_FILE"

View File

@@ -30,8 +30,7 @@
}
.bookshelf-row {
/* Sidebar width + scrollbar width */
width: calc(100vw - 88px);
width: calc(100vw - (100vw - 100%));
}
@media (max-width: 768px) {
@@ -217,36 +216,6 @@ Bookshelf Label
filter: blur(20px);
}
.episode-subtitle {
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
line-height: 16px;
/* fallback */
max-height: 32px;
/* fallback */
-webkit-line-clamp: 2;
/* number of lines to show */
-webkit-box-orient: vertical;
}
.episode-subtitle-long {
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
line-height: 16px;
/* fallback */
max-height: 72px;
/* fallback */
-webkit-line-clamp: 6;
/* number of lines to show */
-webkit-box-orient: vertical;
}
/* Padding for toastification toasts in the top right to not cover appbar/toolbar */
.app-bar-and-toolbar .Vue-Toastification__container.top-right {
padding-top: 104px;
@@ -258,4 +227,24 @@ Bookshelf Label
.no-bars .Vue-Toastification__container.top-right {
padding-top: 8px;
}
.abs-btn::before {
content: '';
position: absolute;
border-radius: 6px;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0);
transition: all 0.1s ease-in-out;
}
.abs-btn:hover:not(:disabled)::before {
background-color: rgba(255, 255, 255, 0.1);
}
.abs-btn:disabled::before {
background-color: rgba(0, 0, 0, 0.2);
}

View File

@@ -24,6 +24,7 @@
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
vertical-align: top;
}
.material-icons:not([class*="text-"]) {

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -186,7 +186,7 @@ export default {
methods: {
requestBatchQuickEmbed() {
const payload = {
message: 'Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?',
message: this.$strings.MessageConfirmQuickEmbed,
callback: (confirmed) => {
if (confirmed) {
this.$axios
@@ -219,7 +219,7 @@ export default {
},
async batchRescan() {
const payload = {
message: `Are you sure you want to re-scan ${this.selectedMediaItems.length} items?`,
message: this.$getString('MessageConfirmReScanLibraryItems', [this.selectedMediaItems.length]),
callback: (confirmed) => {
if (confirmed) {
this.$axios
@@ -316,13 +316,15 @@ export default {
},
batchDeleteClick() {
const payload = {
message: `This will delete ${this.numMediaItemsSelected} library items from the database and your file system. Are you sure?`,
checkboxLabel: 'Delete from file system. Uncheck to only remove from database.',
message: this.$getString('MessageConfirmDeleteLibraryItems', [this.numMediaItemsSelected]),
checkboxLabel: this.$strings.LabelDeleteFromFileSystemCheckbox,
yesButtonText: this.$strings.ButtonDelete,
yesButtonColor: 'error',
checkboxDefaultValue: true,
checkboxDefaultValue: !Number(localStorage.getItem('softDeleteDefault') || 0),
callback: (confirmed, hardDelete) => {
if (confirmed) {
localStorage.setItem('softDeleteDefault', hardDelete ? 0 : 1)
this.$store.commit('setProcessingBatch', true)
this.$axios

View File

@@ -1,13 +1,13 @@
<template>
<div id="bookshelf" ref="wrapper" class="w-full max-w-full h-full overflow-y-scroll relative">
<!-- Cover size widget -->
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-50" />
<widgets-cover-size-widget class="fixed right-4 z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" />
<div v-if="loaded && !shelves.length && !search" class="w-full flex flex-col items-center justify-center py-12">
<p class="text-center text-2xl mb-4 py-4">{{ libraryName }} Library is empty!</p>
<p class="text-center text-2xl mb-4 py-4">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p>
<div v-if="userIsAdminOrUp" class="flex">
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn>
<ui-btn to="/config" color="primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn>
<ui-btn color="success" class="w-52" :loading="isScanningLibrary || tempIsScanning" @click="scan">{{ $strings.ButtonScanLibrary }}</ui-btn>
</div>
</div>
<div v-else-if="loaded && !shelves.length && search" class="w-full h-40 flex items-center justify-center">
@@ -58,7 +58,8 @@ export default {
scannerParseSubtitle: false,
wrapperClientWidth: 0,
shelves: [],
lastItemIndexSelected: -1
lastItemIndexSelected: -1,
tempIsScanning: false
}
},
computed: {
@@ -68,6 +69,9 @@ export default {
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
currentLibraryMediaType() {
return this.$store.getters['libraries/getCurrentLibraryMediaType']
},
libraryName() {
return this.$store.getters['libraries/getCurrentLibraryName']
},
@@ -91,6 +95,12 @@ export default {
},
selectedMediaItems() {
return this.$store.state.globals.selectedMediaItems || []
},
streamLibraryItem() {
return this.$store.state.streamLibraryItem
},
isScanningLibrary() {
return !!this.$store.getters['tasks/getRunningLibraryScanTask'](this.currentLibraryId)
}
},
methods: {
@@ -168,7 +178,7 @@ export default {
},
async fetchCategories() {
const categories = await this.$axios
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed`)
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete`)
.then((data) => {
return data
})
@@ -267,14 +277,15 @@ export default {
this.shelves = shelves
},
scan() {
this.tempIsScanning = true
this.$store
.dispatch('libraries/requestLibraryScan', { libraryId: this.$store.state.libraries.currentLibraryId })
.then(() => {
this.$toast.success('Library scan started')
})
.catch((error) => {
console.error('Failed to start scan', error)
this.$toast.error('Failed to start scan')
this.$toast.error(this.$strings.ToastLibraryScanFailedToStart)
})
.finally(() => {
this.tempIsScanning = false
})
},
userUpdated(user) {
@@ -335,9 +346,15 @@ export default {
libraryItemsAdded(libraryItems) {
console.log('libraryItems added', libraryItems)
const isThisLibrary = !libraryItems.some((li) => li.libraryId !== this.currentLibraryId)
if (!this.search && isThisLibrary) {
this.fetchCategories()
const recentlyAddedShelf = this.shelves.find((shelf) => shelf.id === 'recently-added')
if (!recentlyAddedShelf) return
// Add new library item to the recently added shelf
for (const libraryItem of libraryItems) {
if (libraryItem.libraryId === this.currentLibraryId && !recentlyAddedShelf.entities.some((ent) => ent.id === libraryItem.id)) {
// Add to front of array
recentlyAddedShelf.entities.unshift(libraryItem)
}
}
},
libraryItemsUpdated(items) {
@@ -346,8 +363,6 @@ export default {
})
},
episodeAdded(episodeWithLibraryItem) {
console.log('Podcast episode added', episodeWithLibraryItem)
const isThisLibrary = episodeWithLibraryItem.libraryItem?.libraryId === this.currentLibraryId
if (!this.search && isThisLibrary) {
this.fetchCategories()

View File

@@ -4,7 +4,7 @@
<div class="w-full h-full pt-6">
<div v-if="shelf.type === 'book' || shelf.type === 'podcast'" class="flex items-center">
<template v-for="(entity, index) in shelf.entities">
<cards-lazy-book-card :key="entity.id" :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}-${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" />
</template>
</div>
<div v-if="shelf.type === 'episode'" class="flex items-center">

View File

@@ -22,6 +22,10 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
</svg>
</nuxt-link>
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="flex-grow h-full flex justify-center items-center" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p v-if="isPlaylistsPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonPlaylists }}</p>
<span v-else class="material-icons-outlined text-lg">queue_music</span>
</nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
<span v-else class="material-icons-outlined text-lg">collections_bookmark</span>
@@ -36,7 +40,7 @@
</svg>
</nuxt-link>
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p class="text-sm">{{ $strings.ButtonSearch }}</p>
<p class="text-sm">{{ $strings.ButtonAdd }}</p>
</nuxt-link>
</div>
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-40 flex items-center justify-end md:justify-start px-2 md:px-8">
@@ -94,6 +98,9 @@
<template v-else-if="page === 'authors'">
<div class="flex-grow" />
<ui-btn v-if="userCanUpdate && authors && authors.length && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
<!-- author sort select -->
<controls-sort-select v-if="authors && authors.length" v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" />
</template>
</div>
</div>
@@ -179,6 +186,30 @@ export default {
}
]
},
authorSortItems() {
return [
{
text: this.$strings.LabelAuthorFirstLast,
value: 'name'
},
{
text: this.$strings.LabelAuthorLastFirst,
value: 'lastFirst'
},
{
text: this.$strings.LabelNumberOfBooks,
value: 'numBooks'
},
{
text: this.$strings.LabelAddedAt,
value: 'addedAt'
},
{
text: this.$strings.LabelUpdatedAt,
value: 'updatedAt'
}
]
},
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
@@ -293,6 +324,9 @@ export default {
}
return items
},
showPlaylists() {
return this.$store.state.libraries.numUserPlaylists > 0
}
},
methods: {
@@ -448,6 +482,9 @@ export default {
updateCollapseBookSeries() {
this.saveSettings()
},
updateAuthorSort() {
this.saveSettings()
},
saveSettings() {
this.$store.dispatch('user/updateUserSettings', this.settings)
},

View File

@@ -14,10 +14,10 @@
</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="flex justify-between">
<p class="underline font-mono text-sm" @click="clickChangelog">v{{ $config.version }}</p>
<div class="flex items-center justify-between">
<button type="button" class="underline font-mono text-sm" @click="clickChangelog">v{{ $config.version }}</button>
<p class="font-mono text-xs text-gray-300 italic">{{ Source }}</p>
<p class="text-xs text-gray-300 italic">{{ Source }}</p>
</div>
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ latestVersion }}</a>
</div>
@@ -99,6 +99,16 @@ export default {
id: 'config-item-metadata-utils',
title: this.$strings.HeaderItemMetadataUtils,
path: '/config/item-metadata-utils'
},
{
id: 'config-rss-feeds',
title: this.$strings.HeaderRSSFeeds,
path: '/config/rss-feeds'
},
{
id: 'config-authentication',
title: this.$strings.HeaderAuthentication,
path: '/config/authentication'
}
]

View File

@@ -10,7 +10,7 @@
<p class="text-center text-2xl mb-4 py-4">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p>
<div v-if="userIsAdminOrUp" class="flex">
<ui-btn to="/config" color="primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn>
<ui-btn color="success" class="w-52" @click="scan">{{ $strings.ButtonScanLibrary }}</ui-btn>
<ui-btn color="success" class="w-52" :loading="isScanningLibrary || tempIsScanning" @click="scan">{{ $strings.ButtonScanLibrary }}</ui-btn>
</div>
</div>
<div v-else-if="!totalShelves && initialized" class="w-full py-16">
@@ -21,7 +21,7 @@
</div>
</div>
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-50" />
<widgets-cover-size-widget class="fixed right-4 z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" />
</div>
</template>
@@ -62,7 +62,8 @@ export default {
currScrollTop: 0,
resizeTimeout: null,
mountWindowWidth: 0,
lastItemIndexSelected: -1
lastItemIndexSelected: -1,
tempIsScanning: false
}
},
watch: {
@@ -205,6 +206,12 @@ export default {
sizeMultiplier() {
const baseSize = this.isCoverSquareAspectRatio ? 192 : 120
return this.entityWidth / baseSize
},
streamLibraryItem() {
return this.$store.state.streamLibraryItem
},
isScanningLibrary() {
return !!this.$store.getters['tasks/getRunningLibraryScanTask'](this.currentLibraryId)
}
},
methods: {
@@ -313,12 +320,12 @@ export default {
this.currentSFQueryString = this.buildSearchParams()
}
const entityPath = this.entityName === 'series-books' ? 'items' : this.entityName
let entityPath = this.entityName === 'series-books' ? 'items' : this.entityName
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&include=rssfeed`
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&include=rssfeed,numEpisodesIncomplete`
const payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
console.error('failed to fetch books', error)
console.error('failed to fetch items', error)
return null
})
@@ -623,6 +630,11 @@ export default {
return entitiesPerShelfBefore < this.entitiesPerShelf // Books per shelf has changed
},
async init(bookshelf) {
if (this.entityName === 'series') {
this.booksPerFetch = 50
} else {
this.booksPerFetch = 100
}
this.checkUpdateSearchParams()
this.initSizeData(bookshelf)
@@ -719,14 +731,15 @@ export default {
}
},
scan() {
this.tempIsScanning = true
this.$store
.dispatch('libraries/requestLibraryScan', { libraryId: this.currentLibraryId })
.then(() => {
this.$toast.success('Library scan started')
})
.catch((error) => {
console.error('Failed to start scan', error)
this.$toast.error('Failed to start scan')
this.$toast.error(this.$strings.ToastLibraryScanFailedToStart)
})
.finally(() => {
this.tempIsScanning = false
})
}
},
@@ -767,4 +780,4 @@ export default {
background: var(--bookshelf-divider-bg);
box-shadow: 2px 14px 8px #111111aa;
}
</style>
</style>

View File

@@ -1,24 +1,26 @@
<template>
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 md:h-40 z-50 bg-primary px-2 md:px-4 pb-1 md:pb-4 pt-2">
<div v-if="streamLibraryItem" id="mediaPlayerContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 lg:h-40 z-50 bg-primary px-2 lg:px-4 pb-1 lg:pb-4 pt-2">
<div id="videoDock" />
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-2 top-2 md:left-4 cursor-pointer">
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
</nuxt-link>
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
<div class="absolute left-2 top-2 lg:left-4 cursor-pointer">
<covers-book-cover expand-on-click :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
</div>
<div class="flex items-start mb-6 lg:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
<div class="min-w-0">
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
{{ title }}
</nuxt-link>
<div class="flex items-center">
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
{{ title }}
</nuxt-link>
<widgets-explicit-indicator v-if="isExplicit" />
</div>
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center">
<span class="material-icons text-sm">person</span>
<div class="flex items-center">
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
<div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div>
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base">
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}?library=${libraryId}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
</div>
<div v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</div>
<widgets-explicit-indicator :explicit="isExplicit"></widgets-explicit-indicator>
</div>
</div>
@@ -29,7 +31,7 @@
</div>
<div class="flex-grow" />
<ui-tooltip direction="top" :text="$strings.LabelClosePlayer">
<span class="material-icons sm:px-2 py-1 md:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</span>
<button :aria-label="$strings.LabelClosePlayer" class="material-icons sm:px-2 py-1 lg:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</button>
</ui-tooltip>
</div>
<player-ui
@@ -82,13 +84,11 @@ export default {
sleepTimer: null,
displayTitle: null,
currentPlaybackRate: 1,
syncFailedToast: null
syncFailedToast: null,
coverAspectRatio: 1
}
},
computed: {
coverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
isSquareCover() {
return this.coverAspectRatio === 1
},
@@ -138,7 +138,7 @@ export default {
return this.streamLibraryItem?.mediaType === 'music'
},
isExplicit() {
return this.mediaMetadata.explicit || false
return !!this.mediaMetadata.explicit
},
mediaMetadata() {
return this.media.metadata || {}
@@ -349,7 +349,7 @@ export default {
}
if ('mediaSession' in navigator) {
var coverImageSrc = this.$store.getters['globals/getLibraryItemCoverSrc'](this.streamLibraryItem, '/Logo.png')
var coverImageSrc = this.$store.getters['globals/getLibraryItemCoverSrc'](this.streamLibraryItem, '/Logo.png', true)
const artwork = [
{
src: coverImageSrc
@@ -380,7 +380,7 @@ export default {
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === data.stream) {
if (!data.numSegments) return
var chunks = data.chunks
console.log(`[StreamContainer] Stream Progress ${data.percent}`)
console.log(`[MediaPlayerContainer] Stream Progress ${data.percent}`)
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
} else {
@@ -397,17 +397,17 @@ export default {
this.playerHandler.prepareOpenSession(session, this.currentPlaybackRate)
},
streamOpen(session) {
console.log(`[StreamContainer] Stream session open`, session)
console.log(`[MediaPlayerContainer] Stream session open`, session)
},
streamClosed(streamId) {
// Stream was closed from the server
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) {
console.warn('[StreamContainer] Closing stream due to request from server')
console.warn('[MediaPlayerContainer] Closing stream due to request from server')
this.playerHandler.closePlayer()
}
},
streamReady() {
console.log(`[StreamContainer] Stream Ready`)
console.log(`[MediaPlayerContainer] Stream Ready`)
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.setStreamReady()
} else {
@@ -417,7 +417,7 @@ export default {
streamError(streamId) {
// Stream had critical error from the server
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) {
console.warn('[StreamContainer] Closing stream due to stream error from server')
console.warn('[MediaPlayerContainer] Closing stream due to stream error from server')
this.playerHandler.closePlayer()
}
},
@@ -457,6 +457,9 @@ export default {
episodeId,
queueItems: payload.queueItems || []
})
// Set cover aspect ratio for this item's library since the library may change
this.coverAspectRatio = this.$store.getters['libraries/getBookCoverAspectRatio']
this.$nextTick(() => {
if (this.$refs.audioPlayer) this.$refs.audioPlayer.checkUpdateChapterTrack()
})
@@ -496,7 +499,7 @@ export default {
</script>
<style>
#streamContainer {
#mediaPlayerContainer {
box-shadow: 0px -6px 8px #1111113f;
}
</style>

View File

@@ -1,11 +1,10 @@
<template>
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-2 sm:p-4 mb-8">
<div class="flex items-center mb-2">
<slot name="header-prefix"></slot>
<h1 class="text-xl">{{ headerText }}</h1>
<div v-if="showAddButton" class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clicked">
<button type="button" class="material-icons" :aria-label="$strings.ButtonAdd + ': ' + headerText" style="font-size: 1.4rem">add</button>
</div>
<slot name="header-items"></slot>
</div>
<p v-if="description" id="settings-description" class="mb-6 text-gray-200" v-html="description" />
@@ -19,14 +18,9 @@ export default {
props: {
headerText: String,
description: String,
note: String,
showAddButton: Boolean
note: String
},
methods: {
clicked() {
this.$emit('clicked')
}
}
methods: {}
}
</script>

View File

@@ -3,117 +3,119 @@
<!-- ugly little workaround to cover up the shadow overlapping the bookshelf toolbar -->
<div v-if="isShowingBookshelfToolbar" class="absolute top-0 -right-4 w-4 bg-bg h-10 pointer-events-none" />
<nuxt-link :to="`/library/${currentLibraryId}`" 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="homePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<div id="siderail-buttons-container" :class="{ 'player-open': streamLibraryItem }" class="w-full overflow-y-auto overflow-x-hidden">
<nuxt-link :to="`/library/${currentLibraryId}`" 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="homePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonHome }}</p>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonHome }}</p>
<div v-show="homePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<div v-show="homePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-icons text-2xl">format_list_bulleted</span>
<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>
<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>
<div v-show="isPodcastLatestPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<div v-show="isPodcastLatestPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" 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="showLibrary ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" 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="showLibrary ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLibrary }}</p>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLibrary }}</p>
<div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" 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="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
</svg>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" 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="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
</svg>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSeries }}</p>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSeries }}</p>
<div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-icons-outlined text-2xl">collections_bookmark</span>
<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>
<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>
<div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-icons text-2.5xl">queue_music</span>
<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>
<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>
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" 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="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<svg class="w-6 h-6" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z"
/>
</svg>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" 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="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<svg class="w-6 h-6" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z"
/>
</svg>
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAuthors }}</p>
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAuthors }}</p>
<div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/narrators`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isNarratorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-icons text-2xl">record_voice_over</span>
<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>
<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" />
</nuxt-link>
<div v-show="isNarratorsPage" 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'">
<span class="abs-icons icon-podcast text-xl"></span>
<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>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSearch }}</p>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAdd }}</p>
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link v-if="isMusicLibrary" :to="`/library/${currentLibraryId}/bookshelf/albums`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isMusicAlbumsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-icons-outlined text-xl">album</span>
<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>
<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>
<div v-show="isMusicAlbumsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<div v-show="isMusicAlbumsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-icons text-2xl">file_download</span>
<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>
<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" />
</nuxt-link>
<div v-show="isPodcastDownloadQueuePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</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'">
<span class="material-icons text-2xl">warning</span>
<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>
<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>
<div v-show="showingIssues" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
<div class="absolute top-1 right-1 w-4 h-4 rounded-full bg-white bg-opacity-30 flex items-center justify-center">
<p class="text-xs font-mono pb-0.5">{{ numIssues }}</p>
</div>
</nuxt-link>
<div v-show="showingIssues" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
<div class="absolute top-1 right-1 w-4 h-4 rounded-full bg-white bg-opacity-30 flex items-center justify-center">
<p class="text-xs font-mono pb-0.5">{{ numIssues }}</p>
</div>
</nuxt-link>
</div>
<div class="w-full h-12 px-1 py-2 border-t border-black border-opacity-20 absolute left-0" :style="{ bottom: streamLibraryItem ? '240px' : '65px' }">
<div class="w-full h-12 px-1 py-2 border-t border-black/20 bg-bg absolute left-0" :style="{ bottom: streamLibraryItem ? '224px' : '65px' }">
<p class="underline font-mono text-xs text-center text-gray-300 leading-3 mb-1" @click="clickChangelog">v{{ $config.version }}</p>
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xxs text-center block leading-3">Update</a>
<p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p>
@@ -235,3 +237,12 @@ export default {
mounted() {}
}
</script>
<style>
#siderail-buttons-container {
max-height: calc(100vh - 64px - 48px);
}
#siderail-buttons-container.player-open {
max-height: calc(100vh - 64px - 48px - 160px);
}
</style>

View File

@@ -1,35 +1,35 @@
<template>
<nuxt-link :to="`/author/${author.id}?library=${currentLibraryId}`">
<div @mouseover="mouseover" @mouseleave="mouseleave">
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
<nuxt-link :to="`/author/${author?.id}`">
<div cy-id="card" :style="{ width: width + 'px'}" @mouseover="mouseover" @mouseleave="mouseleave">
<div cy-id="imageArea" :style="{ height: height + 'px' }" class=" bg-primary box-shadow-book rounded-md relative overflow-hidden">
<!-- Image or placeholder -->
<covers-author-image :author="author" />
<covers-author-image :author="author"/>
<!-- Author name & num books overlay -->
<div v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2">
<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">
<p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} {{ $strings.LabelBooks }}</p>
</div>
<!-- Search icon btn -->
<div v-show="!searching && isHovering && userCanUpdate" class="absolute top-0 left-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="searchAuthor">
<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">
<ui-tooltip :text="$strings.ButtonQuickMatch" direction="bottom">
<span class="material-icons text-lg">search</span>
</ui-tooltip>
</div>
<div v-show="isHovering && !searching && userCanUpdate" class="absolute top-0 right-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="$emit('edit', author)">
<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)">
<ui-tooltip :text="$strings.LabelEdit" direction="bottom">
<span class="material-icons text-lg">edit</span>
</ui-tooltip>
</div>
<!-- Loading spinner -->
<div v-show="searching" class="absolute top-0 left-0 z-10 w-full h-full bg-black bg-opacity-50 flex items-center justify-center">
<div cy-id="spinner" v-show="searching" class="absolute top-0 left-0 z-10 w-full h-full bg-black bg-opacity-50 flex items-center justify-center">
<widgets-loading-spinner size="" />
</div>
</div>
<div v-show="nameBelow" class="w-full py-1 px-2">
<p class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
<div cy-id="nameBelow" v-show="nameBelow" class="w-full py-1 px-2">
<p class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
</div>
</div>
</nuxt-link>

View File

@@ -13,10 +13,10 @@
<div class="flex-grow" />
<p class="text-sm md:text-base">{{ book.publishedYear }}</p>
</div>
<p v-if="book.author" class="text-gray-300 text-xs md:text-sm">by {{ book.author }}</p>
<p v-if="book.narrator" class="text-gray-400 text-xs">Narrated by {{ book.narrator }}</p>
<p v-if="book.duration" class="text-gray-400 text-xs">Runtime: {{ $elapsedPrettyExtended(book.duration * 60) }}</p>
<div v-if="book.series && book.series.length" class="flex py-1 -mx-1">
<p v-if="book.author" class="text-gray-300 text-xs md:text-sm">{{ $getString('LabelByAuthor', [book.author]) }}</p>
<p v-if="book.narrator" class="text-gray-400 text-xs">{{ $strings.LabelNarrators }}: {{ book.narrator }}</p>
<p v-if="book.duration" class="text-gray-400 text-xs">{{ $strings.LabelDuration }}: {{ $elapsedPrettyExtended(bookDuration, false) }} {{ bookDurationComparison }}</p>
<div v-if="book.series?.length" class="flex py-1 -mx-1">
<div v-for="(series, index) in book.series" :key="index" class="bg-white bg-opacity-10 rounded-full px-1 py-0.5 mx-1">
<p class="leading-3 text-xs text-gray-400">
{{ series.series }}<span v-if="series.sequence">&nbsp;#{{ series.sequence }}</span>
@@ -29,11 +29,9 @@
</div>
<div v-else class="px-4 flex-grow">
<h1>
<div class="flex items-center">
{{ book.title }}<widgets-explicit-indicator :explicit="book.explicit" />
</div>
<div class="flex items-center">{{ book.title }}<widgets-explicit-indicator v-if="book.explicit" /></div>
</h1>
<p class="text-base text-gray-300 whitespace-nowrap truncate">by {{ book.author }}</p>
<p class="text-base text-gray-300 whitespace-nowrap truncate">{{ $getString('LabelByAuthor', [book.author]) }}</p>
<p v-if="book.genres" class="text-xs text-gray-400 leading-5">{{ book.genres.join(', ') }}</p>
<p class="text-xs text-gray-400 leading-5">{{ book.trackCount }} Episodes</p>
</div>
@@ -56,7 +54,8 @@ export default {
default: () => {}
},
isPodcast: Boolean,
bookCoverAspectRatio: Number
bookCoverAspectRatio: Number,
currentBookDuration: Number
},
data() {
return {
@@ -65,12 +64,27 @@ export default {
},
computed: {
bookCovers() {
return this.book.covers ? this.book.covers || [] : []
return this.book.covers || []
},
bookDuration() {
return (this.book.duration || 0) * 60
},
bookDurationComparison() {
if (!this.book.duration || !this.currentBookDuration) return ''
const currentBookDurationMinutes = Math.floor(this.currentBookDuration / 60)
let differenceInMinutes = currentBookDurationMinutes - this.book.duration
if (differenceInMinutes < 0) {
differenceInMinutes = Math.abs(differenceInMinutes)
return this.$getString('LabelDurationComparisonLonger', [this.$elapsedPrettyExtended(differenceInMinutes * 60, false, false)])
} else if (differenceInMinutes > 0) {
return this.$getString('LabelDurationComparisonShorter', [this.$elapsedPrettyExtended(differenceInMinutes * 60, false, false)])
}
return this.$strings.LabelDurationComparisonExactMatch
}
},
methods: {
selectMatch() {
var book = { ...this.book }
const book = { ...this.book }
book.cover = this.selectedCover
this.$emit('select', book)
},

View File

@@ -7,7 +7,7 @@
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-gray-300" v-html="matchHtml" />
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p>
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">{{ $getString('LabelByAuthor', [authorName]) }}</p>
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin' || matchKey === 'episode' || matchKey === 'narrators'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
@@ -69,7 +69,7 @@ export default {
if (this.matchKey === 'episode') return `<p class="truncate">${this.$strings.LabelEpisode}: ${html}</p>`
if (this.matchKey === 'tags') return `<p class="truncate">${this.$strings.LabelTags}: ${html}</p>`
if (this.matchKey === 'subtitle') return `<p class="truncate">${html}</p>`
if (this.matchKey === 'authors') return `by ${html}`
if (this.matchKey === 'authors') this.$getString('LabelByAuthor', [html])
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
if (this.matchKey === 'series') return `<p class="truncate">${this.$strings.LabelSeries}: ${html}</p>`
@@ -90,4 +90,4 @@ export default {
flex-direction: column;
justify-content: center;
}
</style>
</style>

View File

@@ -1,10 +1,8 @@
<template>
<div class="flex items-center px-1 overflow-hidden">
<div class="w-8 flex items-center justify-center">
<!-- <div class="text-lg"> -->
<span v-if="isFinished" :class="taskIconStatus" class="material-icons text-base">{{ actionIcon }}</span>
<widgets-loading-spinner v-else />
<!-- </div> -->
</div>
<div class="flex-grow px-2 taskRunningCardContent">
<p class="truncate text-sm">{{ title }}</p>
@@ -12,7 +10,9 @@
<p class="truncate text-xs text-gray-300">{{ description }}</p>
<p v-if="isFailed && failedMessage" class="text-xs truncate text-red-500">{{ failedMessage }}</p>
<p v-else-if="!isFinished && cancelingScan" class="text-xs truncate">Canceling...</p>
</div>
<ui-btn v-if="userIsAdminOrUp && !isFinished && isLibraryScan && !cancelingScan" color="primary" :padding-y="1" :padding-x="1" class="text-xs w-16 max-w-16 truncate mr-1" @click.stop="cancelScan">{{ this.$strings.ButtonCancel }}</ui-btn>
</div>
</template>
@@ -25,9 +25,14 @@ export default {
}
},
data() {
return {}
return {
cancelingScan: false
}
},
computed: {
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
title() {
return this.task.title || 'No Title'
},
@@ -76,9 +81,22 @@ export default {
}
return ''
},
isLibraryScan() {
return this.action === 'library-scan' || this.action === 'library-match-all'
}
},
methods: {
cancelScan() {
const libraryId = this.task?.data?.libraryId
if (!libraryId) {
console.error('No library id in library-scan task', this.task)
return
}
this.cancelingScan = true
this.$root.socket.emit('cancel_scan', libraryId)
}
},
methods: {},
mounted() {}
}
</script>

View File

@@ -15,24 +15,37 @@
<div class="flex my-2 -mx-2">
<div class="w-1/2 px-2">
<ui-text-input-with-label v-model="itemData.title" :disabled="processing" :label="$strings.LabelTitle" @input="titleUpdated" />
<ui-text-input-with-label v-model.trim="itemData.title" :disabled="processing" :label="$strings.LabelTitle" @input="titleUpdated" />
</div>
<div class="w-1/2 px-2">
<ui-text-input-with-label v-if="!isPodcast" v-model="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
<div v-if="!isPodcast" class="flex items-end">
<ui-text-input-with-label v-model.trim="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
<ui-tooltip :text="$strings.LabelUploaderItemFetchMetadataHelp">
<div class="ml-2 mb-1 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-primary cursor-pointer" @click="fetchMetadata">
<span class="text-base text-white text-opacity-80 font-mono material-icons">sync</span>
</div>
</ui-tooltip>
</div>
<div v-else class="w-full">
<p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p>
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" />
<p class="px-1 text-sm font-semibold">
{{ $strings.LabelDirectory }}
<em class="font-normal text-xs pl-2">(auto)</em>
</p>
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs" />
</div>
</div>
</div>
<div v-if="!isPodcast" class="flex my-2 -mx-2">
<div class="w-1/2 px-2">
<ui-text-input-with-label v-model="itemData.series" :disabled="processing" :label="$strings.LabelSeries" note="(optional)" />
<ui-text-input-with-label v-model.trim="itemData.series" :disabled="processing" :label="$strings.LabelSeries" note="(optional)" inputClass="h-10" />
</div>
<div class="w-1/2 px-2">
<div class="w-full">
<p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p>
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" />
<label class="px-1 text-sm font-semibold">
{{ $strings.LabelDirectory }}
<em class="font-normal text-xs pl-2">(auto)</em>
</label>
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs h-10" />
</div>
</div>
</div>
@@ -42,14 +55,14 @@
<tables-uploaded-files-table v-if="item.ignoredFiles.length" :title="$strings.HeaderIgnoredFiles" :files="item.ignoredFiles" />
</template>
<widgets-alert v-if="uploadSuccess" type="success">
<p class="text-base">{{ $strings.MessageUploaderItemSuccess }}</p>
<p class="text-base">"{{ itemData.title }}" {{ $strings.MessageUploaderItemSuccess }}</p>
</widgets-alert>
<widgets-alert v-if="uploadFailed" type="error">
<p class="text-base">{{ $strings.MessageUploaderItemFailed }}</p>
<p class="text-base">"{{ itemData.title }}" {{ $strings.MessageUploaderItemFailed }}</p>
</widgets-alert>
<div v-if="isUploading" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20">
<ui-loading-indicator :text="$strings.MessageUploading" />
<div v-if="isNonInteractable" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20">
<ui-loading-indicator :text="nonInteractionLabel" />
</div>
</div>
</template>
@@ -64,7 +77,8 @@ export default {
default: () => {}
},
mediaType: String,
processing: Boolean
processing: Boolean,
provider: String
},
data() {
return {
@@ -76,7 +90,8 @@ export default {
error: '',
isUploading: false,
uploadFailed: false,
uploadSuccess: false
uploadSuccess: false,
isFetchingMetadata: false
}
},
computed: {
@@ -87,12 +102,19 @@ export default {
if (!this.itemData.title) return ''
if (this.isPodcast) return this.itemData.title
if (this.itemData.series && this.itemData.author) {
return Path.join(this.itemData.author, this.itemData.series, this.itemData.title)
} else if (this.itemData.author) {
return Path.join(this.itemData.author, this.itemData.title)
} else {
return this.itemData.title
const outputPathParts = [this.itemData.author, this.itemData.series, this.itemData.title]
const cleanedOutputPathParts = outputPathParts.filter(Boolean).map((part) => this.$sanitizeFilename(part))
return Path.join(...cleanedOutputPathParts)
},
isNonInteractable() {
return this.isUploading || this.isFetchingMetadata
},
nonInteractionLabel() {
if (this.isUploading) {
return this.$strings.MessageUploading
} else if (this.isFetchingMetadata) {
return this.$strings.LabelFetchingMetadata
}
}
},
@@ -105,9 +127,42 @@ export default {
titleUpdated() {
this.error = ''
},
async fetchMetadata() {
if (!this.itemData.title.trim().length) {
return
}
this.isFetchingMetadata = true
this.error = ''
try {
const searchQueryString = new URLSearchParams({
title: this.itemData.title,
author: this.itemData.author,
provider: this.provider
})
const [bestCandidate, ..._rest] = await this.$axios.$get(`/api/search/books?${searchQueryString}`)
if (bestCandidate) {
this.itemData = {
...this.itemData,
title: bestCandidate.title,
author: bestCandidate.author,
series: (bestCandidate.series || [])[0]?.series
}
} else {
this.error = this.$strings.ErrorUploadFetchMetadataNoResults
}
} catch (e) {
console.error('Failed', e)
this.error = this.$strings.ErrorUploadFetchMetadataAPI
} finally {
this.isFetchingMetadata = false
}
},
getData() {
if (!this.itemData.title) {
this.error = 'Must have a title'
this.error = this.$strings.ErrorUploadLacksTitle
return null
}
this.error = ''
@@ -128,4 +183,4 @@ export default {
}
}
}
</script>
</script>

View File

@@ -1,124 +1,126 @@
<template>
<div ref="card" :id="`book-card-${index}`" :style="{ minWidth: width + 'px', maxWidth: width + 'px', height: height + 'px' }" class="rounded-sm z-10 bg-primary cursor-pointer box-shadow-book" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<div ref="card" :id="`book-card-${index}`" :style="{ minWidth: 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">
<!-- When cover image does not fill -->
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
<div 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>
<!-- Alternative bookshelf title/author/sort -->
<div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
<div cy-id="detailBottom" v-if="isAlternativeBookshelfView || isAuthorBookshelfView" dir="auto" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
<div :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
<div class="flex items-center">
<span class="truncate">{{ displayTitle }}</span>
<widgets-explicit-indicator :explicit="isExplicit" />
</div>
<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>
<widgets-explicit-indicator cy-id="explicitIndicator" v-if="isExplicit" />
</ui-tooltip>
</div>
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || '&nbsp;' }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
<p cy-id="line2" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || '&nbsp;' }}</p>
<p cy-id="line3" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
</div>
<div v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }" style="background-color: #78350f">
<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 :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequenceList }}</p>
</div>
<div v-else-if="booksInSeries" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }" style="background-color: #cd9d49dd">
<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 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' }">
<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 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 }" />
<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 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 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 class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">
{{ titleCleaned }}
</p>
<p cy-id="placeholderTitleText" class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p>
</div>
</div>
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
<p class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
<div 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 collapsed series in library and podcasts (unless showing podcast episode) -->
<div v-if="!booksInSeries && (!isPodcast || episodeProgress)" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
<!-- Finished progress bar for collapsed series -->
<div v-else-if="booksInSeries && seriesIsFinished" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b bg-success" :style="{ width: width * userProgressPercent + 'px' }"></div>
<!-- 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 v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen) && !processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded hidden md:block" :class="overlayWrapperClasslist">
<div v-show="showPlayButton" class="h-full flex items-center justify-center pointer-events-none">
<div 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 v-show="showReadButton" class="h-full flex items-center justify-center pointer-events-none">
<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 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">
<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>
<div 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">
<!-- 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 ref="moreIcon" v-show="!isSelectionMode && moreMenuItems.length" class="hidden md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-150" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
<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 v-if="ebookFormat" class="absolute" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }">
<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 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">
<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 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 class="text-gray-200 text-center" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">{{ series }}</p>
<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 v-if="showError" :text="errorText" class="absolute bottom-4 left-0 z-10">
<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 v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" :style="{ padding: 0.375 * sizeMultiplier + 'rem' }">
<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 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` }">
<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 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` }">
<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 v-else-if="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' }">
<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>
</template>
@@ -158,6 +160,7 @@ export default {
imageReady: false,
selected: false,
isSelectionMode: false,
displayTitleTruncated: false,
showCoverBg: false
}
},
@@ -213,8 +216,11 @@ export default {
// Only included when filtering by series or collapse series or Continue Series shelf on home page
return this.mediaMetadata.series
},
seriesName() {
return this.series?.name || null
},
seriesSequence() {
return this.series ? this.series.sequence : null
return this.series?.sequence || null
},
libraryId() {
return this._libraryItem.libraryId
@@ -227,9 +233,11 @@ export default {
return this.media.numTracks || 0 // toJSONMinified
},
numEpisodes() {
if (!this.isPodcast) return 0
return this.media.numEpisodes || 0
},
numEpisodesIncomplete() {
return this._libraryItem.numEpisodesIncomplete || 0
},
processingBatch() {
return this.store.state.processingBatch
},
@@ -311,6 +319,7 @@ export default {
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
if (this.orderBy === 'media.metadata.publishedYear' && this.mediaMetadata.publishedYear) return 'Published ' + this.mediaMetadata.publishedYear
return null
},
episodeProgress() {
@@ -330,11 +339,22 @@ export default {
if (!this.userProgress || this.userProgress.progress) return false
return this.userProgress.ebookProgress > 0
},
seriesProgressPercent() {
if (!this.libraryItemIdsInSeries.length) return 0
let progressPercent = 0
const useEBookProgress = this.useEBookProgress
this.libraryItemIdsInSeries.forEach((lid) => {
const progress = this.store.getters['user/getUserMediaProgress'](lid)
if (progress) progressPercent += progress.isFinished ? 1 : useEBookProgress ? progress.ebookProgress || 0 : progress.progress || 0
})
return progressPercent / this.libraryItemIdsInSeries.length
},
userProgressPercent() {
if (this.useEBookProgress) return Math.max(Math.min(1, this.userProgress.ebookProgress), 0)
return this.userProgress ? Math.max(Math.min(1, this.userProgress.progress), 0) || 0 : 0
let progressPercent = this.itemIsFinished ? 1 : this.booksInSeries ? this.seriesProgressPercent : this.useEBookProgress ? this.userProgress?.ebookProgress || 0 : this.userProgress?.progress || 0
return Math.max(Math.min(1, progressPercent), 0)
},
itemIsFinished() {
if (this.booksInSeries) return this.seriesIsFinished
return this.userProgress ? !!this.userProgress.isFinished : false
},
seriesIsFinished() {
@@ -345,7 +365,7 @@ export default {
},
showError() {
if (this.recentEpisode) return false // Dont show podcast error on episode card
return this.numInvalidAudioFiles || this.numMissingParts || this.isMissing || this.isInvalid
return this.isMissing || this.isInvalid
},
libraryItemIdStreaming() {
return this.store.getters['getLibraryItemIdStreaming']
@@ -375,29 +395,13 @@ export default {
isInvalid() {
return this._libraryItem.isInvalid
},
numMissingParts() {
if (this.isPodcast) return 0
return this.media.numMissingParts
},
numInvalidAudioFiles() {
if (this.isPodcast) return 0
return this.media.numInvalidAudioFiles
},
errorText() {
if (this.isMissing) return 'Item directory is missing!'
else if (this.isInvalid) {
if (this.isPodcast) return 'Podcast has no episodes'
return 'Item has no audio tracks & ebook'
}
let txt = ''
if (this.numMissingParts) {
txt += `${this.numMissingParts} missing parts.`
}
if (this.numInvalidAudioFiles) {
if (txt) txt += ' '
txt += `${this.numInvalidAudioFiles} invalid audio files.`
}
return txt || 'Unknown Error'
return 'Unknown Error'
},
overlayWrapperClasslist() {
const classes = []
@@ -630,6 +634,12 @@ export default {
}
this.libraryItem = libraryItem
this.$nextTick(() => {
if (this.$refs.displayTitle) {
this.displayTitleTruncated = this.$refs.displayTitle.scrollWidth > this.$refs.displayTitle.clientWidth
}
})
},
clickCard(e) {
if (this.processing) return
@@ -832,13 +842,15 @@ export default {
},
deleteLibraryItem() {
const payload = {
message: 'This will delete the library item from the database and your file system. Are you sure?',
checkboxLabel: 'Delete from file system. Uncheck to only remove from database.',
message: this.$strings.MessageConfirmDeleteLibraryItem,
checkboxLabel: this.$strings.LabelDeleteFromFileSystemCheckbox,
yesButtonText: this.$strings.ButtonDelete,
yesButtonColor: 'error',
checkboxDefaultValue: true,
checkboxDefaultValue: !Number(localStorage.getItem('softDeleteDefault') || 0),
callback: (confirmed, hardDelete) => {
if (confirmed) {
localStorage.setItem('softDeleteDefault', hardDelete ? 0 : 1)
this.processing = true
const axios = this.$axios || this.$nuxt.$axios
axios

View File

@@ -1,28 +1,28 @@
<template>
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<div cy-id="card" ref="card" :id="`series-card-${index}`" :style="{ width: 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 class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ books.length }}</div>
<div cy-id="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 v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
<div 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 + '%' }" />
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
<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 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>
<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 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 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 class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
<p cy-id="standardBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
</div>
</div>
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
<p class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
<div cy-id="detailBottomText" v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
<p cy-id="detailBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
<p cy-id="detailBottomSortLine" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
</div>
</div>
</template>
@@ -119,9 +119,13 @@ export default {
return this.seriesBookProgress.some((p) => !p.isFinished && p.progress > 0)
},
seriesPercentInProgress() {
let totalFinishedAndInProgress = this.seriesBooksFinished.length
if (this.hasSeriesBookInProgress) totalFinishedAndInProgress += 1
return Math.min(1, Math.max(0, totalFinishedAndInProgress / this.books.length))
if (!this.books.length) return 0
let progressPercent = 0
this.seriesBookProgress.forEach((progress) => {
progressPercent += progress.isFinished ? 1 : progress.progress || 0
})
progressPercent /= this.books.length
return Math.min(1, Math.max(0, progressPercent))
},
isSeriesFinished() {
return this.books.length === this.seriesBooksFinished.length

View File

@@ -1,14 +1,14 @@
<template>
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(narrator.name)}`">
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(name)}`">
<div cy-id="card" :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
<div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-40">
<span class="material-icons-outlined text-[10rem]">record_voice_over</span>
</div>
<!-- Narrator name & num books overlay -->
<div class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2">
<p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
<p cy-id="name" class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
<p cy-id="numBooks" class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
</div>
</div>
</nuxt-link>
@@ -21,8 +21,14 @@ export default {
type: Object,
default: () => {}
},
width: Number,
height: Number,
width: {
type: Number,
default: 150
},
height: {
type: Number,
default: 100
},
sizeMultiplier: {
type: Number,
default: 1
@@ -36,7 +42,7 @@ export default {
return this.narrator?.name || ''
},
numBooks() {
return this.narrator?.books?.length || 0
return this.narrator?.numBooks || this.narrator?.books?.length || 0
},
userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate']

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div v-if="narrators?.length" class="flex py-0.5 mt-4">
<div class="w-32">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNarrators }}</span>
</div>
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
@@ -12,7 +12,7 @@
</div>
</div>
<div v-if="publishedYear" class="flex py-0.5">
<div class="w-32">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublishYear }}</span>
</div>
<div>
@@ -20,15 +20,15 @@
</div>
</div>
<div v-if="publisher" class="flex py-0.5">
<div class="w-32">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublisher }}</span>
</div>
<div>
{{ publisher }}
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=publishers.${$encode(publisher)}`" class="hover:underline">{{ publisher }}</nuxt-link>
</div>
</div>
<div v-if="musicAlbum" class="flex py-0.5">
<div class="w-32">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Album</span>
</div>
<div>
@@ -36,7 +36,7 @@
</div>
</div>
<div v-if="musicAlbumArtist" class="flex py-0.5">
<div class="w-32">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Album Artist</span>
</div>
<div>
@@ -44,7 +44,7 @@
</div>
</div>
<div v-if="musicTrackPretty" class="flex py-0.5">
<div class="w-32">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Track</span>
</div>
<div>
@@ -52,7 +52,7 @@
</div>
</div>
<div v-if="musicDiscPretty" class="flex py-0.5">
<div class="w-32">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Disc</span>
</div>
<div>
@@ -60,7 +60,7 @@
</div>
</div>
<div v-if="podcastType" class="flex py-0.5">
<div class="w-32">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span>
</div>
<div class="capitalize">
@@ -68,7 +68,7 @@
</div>
</div>
<div class="flex py-0.5" v-if="genres.length">
<div class="w-32">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
</div>
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
@@ -79,7 +79,7 @@
</div>
</div>
<div class="flex py-0.5" v-if="tags.length">
<div class="w-32">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelTags }}</span>
</div>
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
@@ -89,8 +89,16 @@
</template>
</div>
</div>
<div v-if="language" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelLanguage }}</span>
</div>
<div>
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=languages.${$encode(language)}`" class="hover:underline">{{ language }}</nuxt-link>
</div>
</div>
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
<div class="w-32">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
</div>
<div>
@@ -98,7 +106,7 @@
</div>
</div>
<div class="flex py-0.5">
<div class="w-32">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelSize }}</span>
</div>
<div>
@@ -182,6 +190,9 @@ export default {
narrators() {
return this.mediaMetadata.narrators || []
},
language() {
return this.mediaMetadata.language || null
},
durationPretty() {
if (this.isPodcast) return this.$elapsedPrettyExtended(this.totalPodcastDuration)

View File

@@ -1,13 +1,15 @@
<template>
<div class="sm:w-80 w-full relative">
<form @submit.prevent="submitSearch">
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
</form>
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
<span v-else class="material-icons" style="font-size: 1.2rem">close</span>
<div class="">
<div class="w-full relative sm:w-80">
<form @submit.prevent="submitSearch">
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
</form>
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
<span v-else class="material-icons" style="font-size: 1.2rem">close</span>
</div>
</div>
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-40 sm:w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu">
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full max-w-64 sm:max-w-80 sm:w-80 bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu">
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<li v-if="isTyping" class="py-2 px-2">
<p>{{ $strings.MessageThinking }}</p>
@@ -103,7 +105,7 @@ export default {
return this.$store.state.libraries.currentLibraryId
},
totalResults() {
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length + this.podcastResults.length
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length + this.podcastResults.length + this.narratorResults.length
}
},
methods: {

View File

@@ -124,6 +124,11 @@ export default {
value: 'narrators',
sublist: true
},
{
text: this.$strings.LabelPublisher,
value: 'publishers',
sublist: true
},
{
text: this.$strings.LabelLanguage,
value: 'languages',
@@ -167,6 +172,11 @@ export default {
value: 'narrators',
sublist: true
},
{
text: this.$strings.LabelPublisher,
value: 'publishers',
sublist: true
},
{
text: this.$strings.LabelLanguage,
value: 'languages',
@@ -225,6 +235,11 @@ export default {
value: 'tags',
sublist: true
},
{
text: this.$strings.LabelLanguage,
value: 'languages',
sublist: true
},
{
text: this.$strings.ButtonIssues,
value: 'issues',
@@ -313,6 +328,9 @@ export default {
languages() {
return this.filterData.languages || []
},
publishers() {
return this.filterData.publishers || []
},
progress() {
return [
{
@@ -335,6 +353,10 @@ export default {
},
tracks() {
return [
{
id: 'none',
name: this.$strings.LabelTracksNone
},
{
id: 'single',
name: this.$strings.LabelTracksSingleTrack
@@ -351,9 +373,17 @@ export default {
id: 'ebook',
name: this.$strings.LabelHasEbook
},
{
id: 'no-ebook',
name: this.$strings.LabelMissingEbook
},
{
id: 'supplementary',
name: this.$strings.LabelHasSupplementaryEbook
},
{
id: 'no-supplementary',
name: this.$strings.LabelMissingSupplementaryEbook
}
]
},

View File

@@ -1,8 +1,8 @@
<template>
<div class="relative" v-click-outside="clickOutside" @mouseover="mouseover" @mouseleave="mouseleave">
<div class="cursor-pointer text-gray-300 hover:text-white" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon">
<button :aria-label="$strings.LabelVolume" class="text-gray-300 hover:text-white" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon">
<span class="material-icons text-2xl sm:text-3xl">{{ volumeIcon }}</span>
</div>
</button>
<transition name="menux">
<div v-show="isOpen" class="volumeMenu h-6 absolute bottom-2 w-28 px-2 bg-bg shadow-sm rounded-lg" style="left: -116px">
<div ref="volumeTrack" class="h-1 w-full bg-gray-500 my-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack">
@@ -38,8 +38,8 @@ export default {
},
set(val) {
try {
localStorage.setItem("volume", val);
} catch(error) {
localStorage.setItem('volume', val)
} catch (error) {
console.error('Failed to store volume', err)
}
this.$emit('input', val)
@@ -146,7 +146,7 @@ export default {
if (this.value === 0) {
this.isMute = true
}
const storageVolume = localStorage.getItem("volume")
const storageVolume = localStorage.getItem('volume')
if (storageVolume) {
this.volume = parseFloat(storageVolume)
}

View File

@@ -84,4 +84,4 @@ export default {
},
mounted() {}
}
</script>
</script>

View File

@@ -5,7 +5,8 @@
<div class="absolute cover-bg" ref="coverBg" />
</div>
<img v-if="libraryItem" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-fill'" />
<img v-if="libraryItem" ref="cover" :src="fullCoverUrl" loading="lazy" draggable="false" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-fill'" @click="clickCover" />
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
<p class="text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
<div class="absolute top-2 right-2">
@@ -43,6 +44,7 @@ export default {
type: Number,
default: 120
},
expandOnClick: Boolean,
bookCoverAspectRatio: Number
},
data() {
@@ -99,9 +101,14 @@ export default {
},
fullCoverUrl() {
if (!this.libraryItem) return null
var store = this.$store || this.$nuxt.$store
const store = this.$store || this.$nuxt.$store
return store.getters['globals/getLibraryItemCoverSrc'](this.libraryItem, this.placeholderUrl)
},
rawCoverUrl() {
if (!this.libraryItem) return null
const store = this.$store || this.$nuxt.$store
return store.getters['globals/getLibraryItemCoverSrc'](this.libraryItem, this.placeholderUrl, true)
},
cover() {
return this.media.coverPath || this.placeholderUrl
},
@@ -124,14 +131,16 @@ export default {
authorBottom() {
return 0.75 * this.sizeMultiplier
},
userToken() {
return this.$store.getters['user/getToken']
},
resolution() {
return `${this.naturalWidth}x${this.naturalHeight}px`
}
},
methods: {
clickCover() {
if (this.expandOnClick && this.libraryItem) {
this.$store.commit('globals/setRawCoverPreviewModal', this.rawCoverUrl)
}
},
setCoverBg() {
if (this.$refs.coverBg) {
this.$refs.coverBg.style.backgroundImage = `url("${this.fullCoverUrl}")`

View File

@@ -13,8 +13,8 @@
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
<img src="/Logo.png" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
<p class="text-center text-error" :style="{ fontSize: sizeMultiplier + 'rem' }">Invalid Cover</p>
<img v-if="width > 100" src="/Logo.png" class="mb-2" :style="{ height: 40 * sizeMultiplier + 'px' }" />
<p class="text-center text-error" :style="{ fontSize: invalidCoverFontSize + 'rem' }">Invalid Cover</p>
</div>
</div>
@@ -58,11 +58,14 @@ export default {
sizeMultiplier() {
return this.width / 120
},
invalidCoverFontSize() {
return Math.max(this.sizeMultiplier * 0.8, 0.5)
},
placeholderCoverPadding() {
return 0.8 * this.sizeMultiplier
},
resolution() {
return `${this.naturalWidth}x${this.naturalHeight}px`
return `${this.naturalWidth}×${this.naturalHeight}px`
},
placeholderUrl() {
const config = this.$config || this.$nuxt.$config

View File

@@ -6,21 +6,25 @@
</div>
</template>
<form @submit.prevent="submitForm">
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="min-height: 400px; max-height: 80vh">
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="min-height: 400px; max-height: 80vh">
<div class="w-full p-8">
<div class="flex py-2">
<div class="w-1/2 px-2">
<ui-text-input-with-label v-model="newUser.username" :label="$strings.LabelUsername" />
<ui-text-input-with-label v-model.trim="newUser.username" :label="$strings.LabelUsername" />
</div>
<div class="w-1/2 px-2">
<ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? $strings.LabelPassword : $strings.LabelChangePassword" type="password" />
<ui-text-input-with-label v-else v-model.trim="newUser.email" :label="$strings.LabelEmail" />
</div>
</div>
<div v-show="!isEditingRoot" class="flex py-2">
<div class="px-2 w-52">
<ui-dropdown v-model="newUser.type" :label="$strings.LabelAccountType" :disabled="isEditingRoot" :items="accountTypes" @input="userTypeUpdated" />
<div class="w-1/2 px-2">
<ui-text-input-with-label v-model.trim="newUser.email" :label="$strings.LabelEmail" />
</div>
<div class="flex-grow" />
<div class="px-2 w-52">
<ui-dropdown v-model="newUser.type" :label="$strings.LabelAccountType" :disabled="isEditingRoot" :items="accountTypes" small @input="userTypeUpdated" />
</div>
<div class="flex items-center pt-4 px-2">
<p class="px-3 font-semibold" id="user-enabled-toggle" :class="isEditingRoot ? 'text-gray-300' : ''">{{ $strings.LabelEnable }}</p>
<ui-toggle-switch labeledBy="user-enabled-toggle" v-model="newUser.isActive" :disabled="isEditingRoot" />
@@ -103,12 +107,12 @@
<ui-toggle-switch labeledBy="selected-tags-not-accessible--permissions-toggle" v-model="newUser.permissions.selectedTagsNotAccessible" />
</div>
</div>
</div>
</div>
<div class="flex pt-4 px-2">
<ui-btn v-if="isEditingRoot" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn>
<ui-btn v-if="hasOpenIDLink" small :loading="unlinkingFromOpenID" color="primary" type="button" class="mr-2" @click.stop="unlinkOpenID">Unlink OpenID</ui-btn>
<ui-btn v-if="isEditingRoot" small class="flex items-center" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn>
<div class="flex-grow" />
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
</div>
@@ -133,7 +137,8 @@ export default {
newUser: {},
isNew: true,
tags: [],
loadingTags: false
loadingTags: false,
unlinkingFromOpenID: false
}
},
watch: {
@@ -177,7 +182,7 @@ export default {
return this.isNew ? this.$strings.HeaderNewAccount : this.$strings.HeaderUpdateAccount
},
isEditingRoot() {
return this.account && this.account.type === 'root'
return this.account?.type === 'root'
},
libraries() {
return this.$store.state.libraries.libraries
@@ -195,6 +200,9 @@ export default {
},
tagsSelectionText() {
return this.newUser.permissions.selectedTagsNotAccessible ? this.$strings.LabelTagsNotAccessibleToUser : this.$strings.LabelTagsAccessibleToUser
},
hasOpenIDLink() {
return !!this.account?.hasOpenIDLink
}
},
methods: {
@@ -202,6 +210,31 @@ export default {
// Force close when navigating - used in UsersTable
if (this.$refs.modal) this.$refs.modal.setHide()
},
unlinkOpenID() {
const payload = {
message: 'Are you sure you want to unlink this user from OpenID?',
callback: (confirmed) => {
if (confirmed) {
this.unlinkingFromOpenID = true
this.$axios
.$patch(`/api/users/${this.account.id}/openid-unlink`)
.then(() => {
this.$toast.success('User unlinked from OpenID')
this.show = false
})
.catch((error) => {
console.error('Failed to unlink user from OpenID', error)
this.$toast.error('Failed to unlink user from OpenID')
})
.finally(() => {
this.unlinkingFromOpenID = false
})
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
accessAllTagsToggled(val) {
if (val) {
if (this.newUser.itemTagsSelected?.length) {
@@ -258,7 +291,6 @@ export default {
if (account.type === 'root' && !account.isActive) return
this.processing = true
console.log('Calling update', account)
this.$axios
.$patch(`/api/users/${this.account.id}`, account)
.then((data) => {
@@ -327,9 +359,11 @@ export default {
init() {
this.fetchAllTags()
this.isNew = !this.account
if (this.account) {
this.newUser = {
username: this.account.username,
email: this.account.email,
password: this.account.password,
type: this.account.type,
isActive: this.account.isActive,
@@ -338,9 +372,9 @@ export default {
itemTagsSelected: [...(this.account.itemTagsSelected || [])]
}
} else {
this.fetchAllTags()
this.newUser = {
username: null,
email: null,
password: null,
type: 'user',
isActive: true,
@@ -353,7 +387,8 @@ export default {
accessAllTags: true,
selectedTagsNotAccessible: false
},
librariesAccessible: []
librariesAccessible: [],
itemTagsSelected: []
}
}
}

View File

@@ -0,0 +1,105 @@
<template>
<modals-modal ref="modal" v-model="show" name="custom-metadata-provider" :width="600" :height="'unset'" :processing="processing">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
<p class="text-3xl text-white truncate">Add custom metadata provider</p>
</div>
</template>
<form @submit.prevent="submitForm">
<div class="px-4 w-full flex items-center text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="min-height: 400px; max-height: 80vh">
<div class="w-full p-8">
<div class="flex mb-2">
<div class="w-3/4 p-1">
<ui-text-input-with-label v-model="newName" :label="$strings.LabelName" />
</div>
<div class="w-1/4 p-1">
<ui-text-input-with-label value="Book" readonly :label="$strings.LabelMediaType" />
</div>
</div>
<div class="w-full mb-2 p-1">
<ui-text-input-with-label v-model="newUrl" label="URL" />
</div>
<div class="w-full mb-2 p-1">
<ui-text-input-with-label v-model="newAuthHeaderValue" :label="'Authorization Header Value'" type="password" />
</div>
<div class="flex px-1 pt-4">
<div class="flex-grow" />
<ui-btn color="success" type="submit">{{ $strings.ButtonAdd }}</ui-btn>
</div>
</div>
</div>
</form>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean
},
data() {
return {
processing: false,
newName: '',
newUrl: '',
newAuthHeaderValue: ''
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.init()
}
}
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}
},
methods: {
submitForm() {
if (!this.newName || !this.newUrl) {
this.$toast.error('Must add name and url')
return
}
this.processing = true
this.$axios
.$post('/api/custom-metadata-providers', {
name: this.newName,
url: this.newUrl,
mediaType: 'book', // Currently only supporting book mediaType
authHeaderValue: this.newAuthHeaderValue
})
.then((data) => {
this.$emit('added', data.provider)
this.$toast.success('New provider added')
this.show = false
})
.catch((error) => {
const errorMsg = error.response?.data || 'Unknown error'
console.error('Failed to add provider', error)
this.$toast.error('Failed to add provider: ' + errorMsg)
})
.finally(() => {
this.processing = false
})
},
init() {
this.processing = false
this.newName = ''
this.newUrl = ''
this.newAuthHeaderValue = ''
}
},
mounted() {}
}
</script>

View File

@@ -34,11 +34,6 @@ export default {
data() {
return {}
},
watch: {
value(newVal) {
this.$nextTick(this.scrollToChapter)
}
},
computed: {
show: {
get() {
@@ -53,7 +48,7 @@ export default {
return this.playbackRate
},
currentChapterId() {
return this.currentChapter ? this.currentChapter.id : null
return this.currentChapter?.id || null
},
currentChapterStart() {
return (this.currentChapter?.start || 0) / this._playbackRate
@@ -74,6 +69,11 @@ export default {
}
}
}
},
updated() {
if (this.value) {
this.$nextTick(this.scrollToChapter)
}
}
}
</script>

View File

@@ -8,7 +8,7 @@
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden p-6" style="max-height: 80vh">
<div class="flex items-center">
<p class="text-base text-gray-200">{{ _session.displayTitle }}</p>
<p v-if="_session.displayAuthor" class="text-xs text-gray-400 px-4">by {{ _session.displayAuthor }}</p>
<p v-if="_session.displayAuthor" class="text-xs text-gray-400 px-4">{{ $getString('LabelByAuthor', [_session.displayAuthor]) }}</p>
</div>
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
@@ -88,10 +88,11 @@
<p class="mb-1">{{ _session.mediaPlayer }}</p>
<p v-if="hasDeviceInfo" class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">{{ $strings.LabelDevice }}</p>
<p v-if="clientDisplayName" class="mb-1">{{ clientDisplayName }}</p>
<p v-if="deviceInfo.ipAddress" class="mb-1">{{ deviceInfo.ipAddress }}</p>
<p v-if="osDisplayName" class="mb-1">{{ osDisplayName }}</p>
<p v-if="deviceInfo.browserName" class="mb-1">{{ deviceInfo.browserName }}</p>
<p v-if="clientDisplayName" class="mb-1">{{ clientDisplayName }}</p>
<p v-if="deviceDisplayName" class="mb-1">{{ deviceDisplayName }}</p>
<p v-if="deviceInfo.sdkVersion" class="mb-1">SDK {{ $strings.LabelVersion }}: {{ deviceInfo.sdkVersion }}</p>
<p v-if="deviceInfo.deviceType" class="mb-1">{{ $strings.LabelType }}: {{ deviceInfo.deviceType }}</p>
</div>
@@ -141,10 +142,14 @@ export default {
if (!this.deviceInfo.osName) return null
return `${this.deviceInfo.osName} ${this.deviceInfo.osVersion}`
},
clientDisplayName() {
deviceDisplayName() {
if (!this.deviceInfo.manufacturer || !this.deviceInfo.model) return null
return `${this.deviceInfo.manufacturer} ${this.deviceInfo.model}`
},
clientDisplayName() {
if (!this.deviceInfo.clientName) return null
return `${this.deviceInfo.clientName} ${this.deviceInfo.clientVersion || ''}`
},
playMethodName() {
const playMethod = this._session.playMethod
if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play'

View File

@@ -0,0 +1,30 @@
<template>
<modals-modal v-model="show" name="cover" :width="'90%'" :height="'90%'" :contentMarginTop="0">
<div class="w-full h-full" @click="show = false">
<img loading="lazy" :src="rawCoverUrl" class="w-full h-full z-10 object-scale-down" />
</div>
</modals-modal>
</template>
<script>
export default {
data() {
return {}
},
computed: {
show: {
get() {
return this.$store.state.globals.showRawCoverPreviewModal
},
set(val) {
this.$store.commit('globals/setShowRawCoverPreviewModal', val)
}
},
rawCoverUrl() {
return this.$store.state.globals.selectedRawCoverUrl
}
},
methods: {},
mounted() {}
}
</script>

View File

@@ -5,18 +5,23 @@
<p class="text-3xl text-white truncate">{{ title }}</p>
</div>
</template>
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
<form v-if="author" @submit.prevent="submitForm">
<div class="flex">
<div class="w-40 p-2">
<div class="w-full h-45 relative">
<covers-author-image :author="author" />
<div v-show="!processing && author.imagePath" class="absolute top-0 left-0 w-full h-full opacity-0 hover:opacity-100">
<span class="absolute top-2 right-2 material-icons text-error transform hover:scale-125 transition-transform cursor-pointer text-lg" @click="removeCover">delete</span>
</div>
<div v-if="author" class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
<div class="flex">
<div class="w-40 p-2">
<div class="w-full h-45 relative">
<covers-author-image :author="authorCopy" />
<div v-if="userCanDelete && !processing && author.imagePath" class="absolute top-0 left-0 w-full h-full opacity-0 hover:opacity-100">
<span class="absolute top-2 right-2 material-icons text-error transform hover:scale-125 transition-transform cursor-pointer text-lg" @click="removeCover">delete</span>
</div>
</div>
<div class="flex-grow">
</div>
<div class="flex-grow">
<form @submit.prevent="submitUploadCover" class="flex flex-grow mb-2 p-2">
<ui-text-input v-model="imageUrl" :placeholder="$strings.LabelImageURLFromTheWeb" class="h-9 w-full" />
<ui-btn color="success" type="submit" :padding-x="4" :disabled="!imageUrl" class="ml-2 sm:ml-3 w-24 h-9">{{ $strings.ButtonSubmit }}</ui-btn>
</form>
<form v-if="author" @submit.prevent="submitForm">
<div class="flex">
<div class="w-3/4 p-2">
<ui-text-input-with-label v-model="authorCopy.name" :disabled="processing" :label="$strings.LabelName" />
@@ -25,21 +30,20 @@
<ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" />
</div>
</div>
<div class="p-2">
<ui-text-input-with-label v-model="authorCopy.imagePath" :disabled="processing" :label="$strings.LabelPhotoPathURL" />
</div>
<div class="p-2">
<ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" :label="$strings.LabelDescription" :rows="8" />
</div>
<div class="flex pt-2 px-2">
<ui-btn type="button" @click="searchAuthor">{{ $strings.ButtonQuickMatch }}</ui-btn>
<ui-btn v-if="userCanDelete" small color="error" type="button" @click.stop="removeClick">{{ $strings.ButtonRemove }}</ui-btn>
<div class="flex-grow" />
<ui-btn type="button" class="mx-2" @click="searchAuthor">{{ $strings.ButtonQuickMatch }}</ui-btn>
<ui-btn type="submit">{{ $strings.ButtonSave }}</ui-btn>
</div>
</div>
</form>
</div>
</form>
</div>
</div>
</modals-modal>
</template>
@@ -51,9 +55,9 @@ export default {
authorCopy: {
name: '',
asin: '',
description: '',
imagePath: ''
description: ''
},
imageUrl: '',
processing: false
}
},
@@ -91,17 +95,45 @@ export default {
},
libraryProvider() {
return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
},
userCanDelete() {
return this.$store.getters['user/getUserCanDelete']
}
},
methods: {
init() {
this.authorCopy.name = this.author.name
this.authorCopy.asin = this.author.asin
this.authorCopy.description = this.author.description
this.authorCopy.imagePath = this.author.imagePath
this.imageUrl = ''
this.authorCopy = {
...this.author
}
},
removeClick() {
const payload = {
message: this.$getString('MessageConfirmRemoveAuthor', [this.author.name]),
callback: (confirmed) => {
if (confirmed) {
this.processing = true
this.$axios
.$delete(`/api/authors/${this.authorId}`)
.then(() => {
this.$toast.success('Author removed')
this.show = false
})
.catch((error) => {
console.error('Failed to remove author', error)
this.$toast.error('Failed to remove author')
})
.finally(() => {
this.processing = false
})
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
async submitForm() {
var keysToCheck = ['name', 'asin', 'description', 'imagePath']
var keysToCheck = ['name', 'asin', 'description']
var updatePayload = {}
keysToCheck.forEach((key) => {
if (this.authorCopy[key] !== this.author[key]) {
@@ -130,21 +162,50 @@ export default {
}
this.processing = false
},
async removeCover() {
var updatePayload = {
imagePath: null
}
removeCover() {
this.processing = true
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
console.error('Failed', error)
this.$toast.error(this.$strings.ToastAuthorImageRemoveFailed)
return null
})
if (result && result.updated) {
this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess)
this.$store.commit('globals/showEditAuthorModal', result.author)
this.$axios
.$delete(`/api/authors/${this.authorId}/image`)
.then((data) => {
this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess)
this.authorCopy.updatedAt = data.author.updatedAt
this.authorCopy.imagePath = data.author.imagePath
})
.catch((error) => {
console.error('Failed', error)
this.$toast.error(this.$strings.ToastAuthorImageRemoveFailed)
})
.finally(() => {
this.processing = false
})
},
submitUploadCover() {
if (!this.imageUrl?.startsWith('http:') && !this.imageUrl?.startsWith('https:')) {
this.$toast.error('Invalid image url')
return
}
this.processing = false
this.processing = true
const updatePayload = {
url: this.imageUrl
}
this.$axios
.$post(`/api/authors/${this.authorId}/image`, updatePayload)
.then((data) => {
this.imageUrl = ''
this.$toast.success('Author image updated')
this.authorCopy.updatedAt = data.author.updatedAt
this.authorCopy.imagePath = data.author.imagePath
})
.catch((error) => {
console.error('Failed', error)
this.$toast.error(error.response.data || 'Failed to remove author image')
})
.finally(() => {
this.processing = false
})
},
async searchAuthor() {
if (!this.authorCopy.name && !this.authorCopy.asin) {
@@ -171,8 +232,11 @@ export default {
} else if (response.updated) {
if (response.author.imagePath) {
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
this.$store.commit('globals/showEditAuthorModal', response.author)
} else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
this.authorCopy = {
...response.author
}
} else {
this.$toast.info('No updates were made for Author')
}
@@ -182,4 +246,4 @@ export default {
mounted() {},
beforeDestroy() {}
}
</script>
</script>

View File

@@ -122,7 +122,7 @@ export default {
})
.catch((error) => {
console.error('Failed to get collections', error)
this.$toast.error('Failed to load collections')
this.$toast.error(this.$strings.ToastFailedToLoadData)
})
.finally(() => {
this.processing = false

View File

@@ -8,7 +8,7 @@
<form @submit.prevent="submitForm">
<div class="w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300">
<div class="w-full px-3 py-5 md:p-12">
<div class="flex items-center -mx-1 mb-2">
<div class="flex items-center -mx-1 mb-4">
<div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="ereaderNameInput" v-model="newDevice.name" :disabled="processing" :label="$strings.LabelName" />
</div>
@@ -16,6 +16,14 @@
<ui-text-input-with-label ref="ereaderEmailInput" v-model="newDevice.email" :disabled="processing" :label="$strings.LabelEmail" />
</div>
</div>
<div class="flex items-center -mx-1 mb-4">
<div class="w-full md:w-1/2 px-1">
<ui-dropdown v-model="newDevice.availabilityOption" :label="$strings.LabelDeviceIsAvailableTo" :items="userAvailabilityOptions" @input="availabilityOptionChanged" />
</div>
<div class="w-full md:w-1/2 px-1">
<ui-multi-select-dropdown v-if="newDevice.availabilityOption === 'specificUsers'" v-model="newDevice.users" :label="$strings.HeaderUsers" :items="userOptions" />
</div>
</div>
<div class="flex items-center pt-4">
<div class="flex-grow" />
@@ -38,14 +46,21 @@ export default {
ereaderDevice: {
type: Object,
default: () => null
}
},
users: {
type: Array,
default: () => []
},
loadUsers: Function
},
data() {
return {
processing: false,
newDevice: {
name: '',
email: ''
email: '',
availabilityOption: 'adminAndUp',
users: []
}
}
},
@@ -68,10 +83,43 @@ export default {
}
},
title() {
return this.ereaderDevice ? 'Create Device' : 'Update Device'
return !this.ereaderDevice ? 'Create Device' : 'Update Device'
},
userAvailabilityOptions() {
return [
{
text: this.$strings.LabelAdminUsersOnly,
value: 'adminOrUp'
},
{
text: this.$strings.LabelAllUsersExcludingGuests,
value: 'userOrUp'
},
{
text: this.$strings.LabelAllUsersIncludingGuests,
value: 'guestOrUp'
},
{
text: this.$strings.LabelSelectUsers,
value: 'specificUsers'
}
]
},
userOptions() {
return this.users.map((u) => ({ text: u.username, value: u.id }))
}
},
methods: {
availabilityOptionChanged(option) {
if (option === 'specificUsers' && !this.users.length) {
this.callLoadUsers()
}
},
async callLoadUsers() {
this.processing = true
await this.loadUsers()
this.processing = false
},
submitForm() {
this.$refs.ereaderNameInput.blur()
this.$refs.ereaderEmailInput.blur()
@@ -81,19 +129,27 @@ export default {
return
}
if (this.newDevice.availabilityOption === 'specificUsers' && !this.newDevice.users.length) {
this.$toast.error('Must select at least one user')
return
}
if (this.newDevice.availabilityOption !== 'specificUsers') {
this.newDevice.users = []
}
this.newDevice.name = this.newDevice.name.trim()
this.newDevice.email = this.newDevice.email.trim()
if (!this.ereaderDevice) {
if (this.existingDevices.some((d) => d.name === this.newDevice.name)) {
this.$toast.error('EReader device with that name already exists')
this.$toast.error('Ereader device with that name already exists')
return
}
this.submitCreate()
} else {
if (this.ereaderDevice.name !== this.newDevice.name && this.existingDevices.some((d) => d.name === this.newDevice.name)) {
this.$toast.error('EReader device with that name already exists')
this.$toast.error('Ereader device with that name already exists')
return
}
@@ -160,9 +216,13 @@ export default {
if (this.ereaderDevice) {
this.newDevice.name = this.ereaderDevice.name
this.newDevice.email = this.ereaderDevice.email
this.newDevice.availabilityOption = this.ereaderDevice.availabilityOption || 'adminOrUp'
this.newDevice.users = this.ereaderDevice.users || []
} else {
this.newDevice.name = ''
this.newDevice.email = ''
this.newDevice.availabilityOption = 'adminOrUp'
this.newDevice.users = []
}
}
},

View File

@@ -1,10 +1,10 @@
<template>
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
<div class="w-full mb-4">
<tables-chapters-table v-if="chapters.length" :library-item="libraryItem" keep-open />
<tables-chapters-table v-if="chapters.length" :library-item="libraryItem" keep-open @close="closeModal" />
<div v-if="!chapters.length" class="py-4 text-center">
<p class="mb-8 text-xl">{{ $strings.MessageNoChapters }}</p>
<ui-btn v-if="userCanUpdate" :to="`/audiobook/${libraryItem.id}/chapters`">{{ $strings.ButtonAddChapters }}</ui-btn>
<ui-btn v-if="userCanUpdate" :to="`/audiobook/${libraryItem.id}/chapters`" @click="clickAddChapters">{{ $strings.ButtonAddChapters }}</ui-btn>
</div>
</div>
</div>
@@ -23,7 +23,7 @@ export default {
},
computed: {
media() {
return this.libraryItem ? this.libraryItem.media || {} : {}
return this.libraryItem?.media || {}
},
chapters() {
return this.media.chapters || []
@@ -32,6 +32,15 @@ export default {
return this.$store.getters['user/getUserCanUpdate']
}
},
methods: {}
methods: {
closeModal() {
this.$emit('close')
},
clickAddChapters() {
if (this.$route.name === 'audiobook-id-chapters' && this.$route.params?.id === this.libraryItem?.id) {
this.closeModal()
}
}
}
}
</script>
</script>

View File

@@ -1,30 +1,31 @@
<template>
<div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
<div class="flex flex-wrap">
<div class="relative">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div class="flex flex-col sm:flex-row mb-4">
<div class="relative self-center">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, libraryItemUpdatedAt, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<!-- book cover overlay -->
<div v-if="media.coverPath" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
<div 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">
<span class="material-icons text-2xl">delete</span>
</ui-tooltip>
</div>
</div>
</div>
<div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-2 md:mt-0">
<div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-6 md:mt-0">
<div class="flex items-center">
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 pt-4 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">
<span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span>
<span class="material-icons text-2xl inline-block md:!hidden">upload</span>
</ui-file-input>
</div>
<form @submit.prevent="submitForm" class="flex flex-grow">
<ui-text-input-with-label v-model="imageUrl" :label="$strings.LabelCoverImageURL" />
<ui-btn color="success" type="submit" :padding-x="4" class="mt-5 ml-2 sm:ml-3 w-24">{{ $strings.ButtonSave }}</ui-btn>
<ui-text-input v-model="imageUrl" :placeholder="$strings.LabelImageURLFromTheWeb" class="h-9 w-full" />
<ui-btn color="success" type="submit" :padding-x="4" :disabled="!imageUrl" class="ml-2 sm:ml-3 w-24 h-9">{{ $strings.ButtonSubmit }}</ui-btn>
</form>
</div>
@@ -48,23 +49,23 @@
</div>
</div>
<form @submit.prevent="submitSearchForm">
<div class="flex items-center justify-start -mx-1 h-20">
<div class="w-48 px-1">
<div class="flex flex-wrap sm:flex-nowrap items-center justify-start -mx-1">
<div class="w-48 flex-grow p-1">
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small />
</div>
<div class="w-72 px-1">
<div class="w-72 flex-grow p-1">
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" />
</div>
<div v-show="provider != 'itunes' && provider != 'audiobookcovers'" class="w-72 px-1">
<div v-show="provider != 'itunes' && provider != 'audiobookcovers'" class="w-72 flex-grow p-1">
<ui-text-input-with-label v-model="searchAuthor" :label="$strings.LabelAuthor" />
</div>
<ui-btn class="mt-5 ml-1" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
<ui-btn class="mt-5 ml-1 md:min-w-24" :padding-x="4" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
</div>
</form>
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-80 overflow-y-scroll mt-2 max-w-full">
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center sm:max-h-80 sm:overflow-y-scroll mt-2 max-w-full">
<p v-if="!coversFound.length">{{ $strings.MessageNoCoversFound }}</p>
<template v-for="cover in coversFound">
<div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="updateCover(cover)">
<div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === coverPath ? 'border-yellow-300' : ''" @click="updateCover(cover)">
<covers-preview-cover :src="cover" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
</template>
@@ -139,16 +140,19 @@ export default {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
libraryItemId() {
return this.libraryItem ? this.libraryItem.id : null
return this.libraryItem?.id || null
},
libraryItemUpdatedAt() {
return this.libraryItem?.updatedAt || null
},
mediaType() {
return this.libraryItem ? this.libraryItem.mediaType : null
return this.libraryItem?.mediaType || null
},
isPodcast() {
return this.mediaType == 'podcast'
},
media() {
return this.libraryItem ? this.libraryItem.media || {} : {}
return this.libraryItem?.media || {}
},
coverPath() {
return this.media.coverPath
@@ -157,11 +161,14 @@ export default {
return this.media.metadata || {}
},
libraryFiles() {
return this.libraryItem ? this.libraryItem.libraryFiles || [] : []
return this.libraryItem?.libraryFiles || []
},
userCanUpload() {
return this.$store.getters['user/getUserCanUpload']
},
userCanDelete() {
return this.$store.getters['user/getUserCanDelete']
},
userToken() {
return this.$store.getters['user/getToken']
},
@@ -219,72 +226,53 @@ export default {
this.coversFound = []
this.hasSearched = false
}
this.imageUrl = this.media.coverPath || ''
this.imageUrl = ''
this.searchTitle = this.mediaMetadata.title || ''
this.searchAuthor = this.mediaMetadata.authorName || ''
if (this.isPodcast) this.provider = 'itunes'
else this.provider = localStorage.getItem('book-cover-provider') || localStorage.getItem('book-provider') || 'google'
},
removeCover() {
if (!this.media.coverPath) {
this.imageUrl = ''
if (!this.coverPath) {
return
}
this.updateCover('')
this.isProcessing = true
this.$axios
.$delete(`/api/items/${this.libraryItemId}/cover`)
.then(() => {})
.catch((error) => {
console.error('Failed to remove cover', error)
if (error.response?.data) {
this.$toast.error(error.response.data)
}
})
.finally(() => {
this.isProcessing = false
})
},
submitForm() {
this.updateCover(this.imageUrl)
},
async updateCover(cover) {
if (cover === this.coverPath) {
console.warn('Cover has not changed..', cover)
if (!cover.startsWith('http:') && !cover.startsWith('https:')) {
this.$toast.error('Invalid URL')
return
}
this.isProcessing = true
var success = false
if (!cover) {
// Remove cover
success = await this.$axios
.$delete(`/api/items/${this.libraryItemId}/cover`)
.then(() => true)
.catch((error) => {
console.error('Failed to remove cover', error)
if (error.response && error.response.data) {
this.$toast.error(error.response.data)
}
return false
})
} else if (cover.startsWith('http:') || cover.startsWith('https:')) {
// Download cover from url and use
success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover }).catch((error) => {
console.error('Failed to download cover from url', error)
if (error.response && error.response.data) {
this.$toast.error(error.response.data)
}
return false
this.$axios
.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover })
.then(() => {
this.imageUrl = ''
this.$toast.success('Update Successful')
})
} else {
// Update local cover url
const updatePayload = {
cover
}
success = await this.$axios.$patch(`/api/items/${this.libraryItemId}/cover`, updatePayload).catch((error) => {
console.error('Failed to update', error)
if (error.response && error.response.data) {
this.$toast.error(error.response.data)
}
return false
.catch((error) => {
console.error('Failed to update cover', error)
this.$toast.error(error.response?.data || 'Failed to update cover')
})
.finally(() => {
this.isProcessing = false
})
}
if (success) {
this.$toast.success('Update Successful')
// this.$emit('close')
} else {
this.imageUrl = this.media.coverPath || ''
}
this.isProcessing = false
},
getSearchQuery() {
var searchQuery = `provider=${this.provider}&title=${this.searchTitle}`
@@ -317,7 +305,19 @@ export default {
this.hasSearched = true
},
setCover(coverFile) {
this.updateCover(coverFile.metadata.path)
this.isProcessing = true
this.$axios
.$patch(`/api/items/${this.libraryItemId}/cover`, { cover: coverFile.metadata.path })
.then(() => {
this.$toast.success('Update Successful')
})
.catch((error) => {
console.error('Failed to set local cover', error)
this.$toast.error(error.response?.data || 'Failed to set cover')
})
.finally(() => {
this.isProcessing = false
})
}
}
}

View File

@@ -11,8 +11,8 @@
<ui-btn v-if="userIsAdminOrUp" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">{{ $strings.ButtonQuickMatch }}</ui-btn>
</ui-tooltip>
<ui-tooltip :disabled="!!libraryScan" text="Rescan library item including metadata" direction="bottom" class="mr-2 md:mr-4">
<ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">{{ $strings.ButtonReScan }}</ui-btn>
<ui-tooltip :disabled="isLibraryScanning" text="Rescan library item including metadata" direction="bottom" class="mr-2 md:mr-4">
<ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="isLibraryScanning" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">{{ $strings.ButtonReScan }}</ui-btn>
</ui-tooltip>
<div class="flex-grow" />
@@ -80,9 +80,9 @@ export default {
libraryProvider() {
return this.$store.getters['libraries/getLibraryProvider'](this.libraryId) || 'google'
},
libraryScan() {
isLibraryScanning() {
if (!this.libraryId) return null
return this.$store.getters['scanners/getLibraryScan'](this.libraryId)
return !!this.$store.getters['tasks/getRunningLibraryScanTask'](this.libraryId)
}
},
methods: {

View File

@@ -20,20 +20,16 @@
<div v-if="!episodes.length" class="flex my-4 text-center justify-center text-xl">{{ $strings.MessageNoEpisodes }}</div>
<table v-else class="text-sm tracksTable">
<tr>
<th class="text-left">Sort #</th>
<th class="text-left whitespace-nowrap">{{ $strings.LabelEpisode }}</th>
<th class="text-left">{{ $strings.EpisodeTitle }}</th>
<th class="text-center w-28">{{ $strings.EpisodeDuration }}</th>
<th class="text-center w-28">{{ $strings.EpisodeSize }}</th>
<th class="text-center w-20 min-w-20">{{ $strings.LabelEpisode }}</th>
<th class="text-left">{{ $strings.LabelEpisodeTitle }}</th>
<th class="text-center w-28">{{ $strings.LabelEpisodeDuration }}</th>
<th class="text-center w-28">{{ $strings.LabelEpisodeSize }}</th>
</tr>
<tr v-for="episode in episodes" :key="episode.id">
<td class="text-left">
<p class="px-4">{{ episode.index }}</p>
<td class="text-center w-20 min-w-20">
<p>{{ episode.episode }}</p>
</td>
<td class="text-left">
<p class="px-4">{{ episode.episode }}</p>
</td>
<td>
<td dir="auto">
{{ episode.title }}
</td>
<td class="font-mono text-center">

View File

@@ -14,8 +14,7 @@ export default {
},
data() {
return {
tracks: [],
showFullPath: false
tracks: []
}
},
watch: {

View File

@@ -22,7 +22,7 @@
</div>
<div v-show="!processing" class="w-full max-h-full overflow-y-auto overflow-x-hidden matchListWrapper mt-4">
<template v-for="(res, index) in searchResults">
<cards-book-match-card :key="index" :book="res" :is-podcast="isPodcast" :book-cover-aspect-ratio="bookCoverAspectRatio" @select="selectMatch" />
<cards-book-match-card :key="index" :book="res" :current-book-duration="currentBookDuration" :is-podcast="isPodcast" :book-cover-aspect-ratio="bookCoverAspectRatio" @select="selectMatch" />
</template>
</div>
<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">
@@ -32,7 +32,7 @@
</div>
<p class="text-xl pl-3">{{ $strings.HeaderUpdateDetails }}</p>
</div>
<ui-checkbox v-model="selectAll" checkbox-bg="bg" @input="selectAllToggled" />
<ui-checkbox v-model="selectAll" :label="$strings.LabelSelectAll" checkbox-bg="bg" @input="selectAllToggled" />
<form @submit.prevent="submitMatchUpdate">
<div v-if="selectedMatchOrig.cover" class="flex flex-wrap md:flex-nowrap items-center justify-center">
<div class="flex flex-grow items-center py-2">
@@ -42,15 +42,15 @@
<div class="flex py-2">
<div>
<p class="text-center text-gray-200">New</p>
<p class="text-center text-gray-200">{{ $strings.LabelNew }}</p>
<a :href="selectedMatch.cover" target="_blank" class="bg-primary">
<covers-preview-cover :src="selectedMatch.cover" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</a>
</div>
<div v-if="media.coverPath">
<p class="text-center text-gray-200">Current</p>
<a :href="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" target="_blank" class="bg-primary">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div v-if="media.coverPath" class="ml-0.5">
<p class="text-center text-gray-200">{{ $strings.LabelCurrent }}</p>
<a :href="$store.getters['globals/getLibraryItemCoverSrc'](libraryItem, null, true)" target="_blank" class="bg-primary">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrc'](libraryItem, null, true)" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</a>
</div>
</div>
@@ -79,7 +79,7 @@
<div v-if="selectedMatchOrig.narrator" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.narrator" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.narrator" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" />
<ui-multi-select v-model="selectedMatch.narrator" :items="narrators" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" />
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.narratorName || '' }}</p>
</div>
</div>
@@ -122,7 +122,7 @@
<div v-if="selectedMatchOrig.tags" class="flex items-center py-2">
<ui-checkbox v-model="selectedMatchUsage.tags" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" />
<ui-multi-select v-model="selectedMatch.tags" :items="tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" />
<p v-if="media.tags" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ media.tags.join(', ') }}</p>
</div>
</div>
@@ -180,14 +180,14 @@
<ui-checkbox v-model="selectedMatchUsage.explicit" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4" :class="{ 'pt-4': mediaMetadata.explicit != null }">
<ui-checkbox v-model="selectedMatch.explicit" :label="$strings.LabelExplicit" :disabled="!selectedMatchUsage.explicit" :checkbox-bg="!selectedMatchUsage.explicit ? 'bg' : 'primary'" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
<p v-if="mediaMetadata.explicit != null" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.explicit ? 'Explicit (checked)' : 'Not Explicit (unchecked)' }}</p>
<p v-if="mediaMetadata.explicit != null" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.explicit ? $strings.LabelExplicitChecked : $strings.LabelExplicitUnchecked }}</p>
</div>
</div>
<div v-if="selectedMatchOrig.abridged != null" class="flex items-center pb-2" :class="{ 'pt-2': mediaMetadata.abridged == null }">
<ui-checkbox v-model="selectedMatchUsage.abridged" checkbox-bg="bg" @input="checkboxToggled" />
<div class="flex-grow ml-4" :class="{ 'pt-4': mediaMetadata.abridged != null }">
<ui-checkbox v-model="selectedMatch.abridged" :label="$strings.LabelAbridged" :disabled="!selectedMatchUsage.abridged" :checkbox-bg="!selectedMatchUsage.abridged ? 'bg' : 'primary'" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
<p v-if="mediaMetadata.abridged != null" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.abridged ? 'Abridged (checked)' : 'Unabridged (unchecked)' }}</p>
<p v-if="mediaMetadata.abridged != null" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.abridged ? $strings.LabelAbridgedChecked : $strings.LabelAbridgedUnchecked }}</p>
</div>
</div>
@@ -280,6 +280,9 @@ export default {
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
filterData() {
return this.$store.state.libraries.filterData
},
providers() {
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
return this.$store.state.scanners.providers
@@ -290,22 +293,31 @@ export default {
return this.$strings.LabelSearchTitle
},
media() {
return this.libraryItem ? this.libraryItem.media || {} : {}
return this.libraryItem?.media || {}
},
mediaMetadata() {
return this.media.metadata || {}
},
currentBookDuration() {
if (this.isPodcast) return 0
return this.media.duration || 0
},
mediaType() {
return this.libraryItem ? this.libraryItem.mediaType : null
return this.libraryItem?.mediaType || null
},
isPodcast() {
return this.mediaType == 'podcast'
},
narrators() {
return this.filterData.narrators || []
},
genres() {
const filterData = this.$store.state.libraries.filterData || {}
const currentGenres = filterData.genres || []
const currentGenres = this.filterData.genres || []
const selectedMatchGenres = this.selectedMatch.genres || []
return [...new Set([...currentGenres ,...selectedMatchGenres])]
return [...new Set([...currentGenres, ...selectedMatchGenres])]
},
tags() {
return this.filterData.tags || []
}
},
methods: {
@@ -324,10 +336,22 @@ export default {
console.error('PersistProvider', error)
}
},
getDefaultBookProvider() {
let provider = localStorage.getItem('book-provider')
if (!provider) return 'google'
// Validate book provider
if (!this.$store.getters['scanners/checkBookProviderExists'](provider)) {
console.error('Stored book provider does not exist', provider)
localStorage.removeItem('book-provider')
return 'google'
}
return provider
},
getSearchQuery() {
if (this.isPodcast) return `term=${this.searchTitle}`
var searchQuery = `provider=${this.provider}&fallbackTitleOnly=1&title=${this.searchTitle}`
if (this.searchAuthor) searchQuery += `&author=${this.searchAuthor}`
if (this.isPodcast) return `term=${encodeURIComponent(this.searchTitle)}`
var searchQuery = `provider=${this.provider}&fallbackTitleOnly=1&title=${encodeURIComponent(this.searchTitle)}`
if (this.searchAuthor) searchQuery += `&author=${encodeURIComponent(this.searchAuthor)}`
if (this.libraryItemId) searchQuery += `&id=${this.libraryItemId}`
return searchQuery
},
submitSearch() {
@@ -429,7 +453,9 @@ export default {
this.searchTitle = this.libraryItem.media.metadata.title
this.searchAuthor = this.libraryItem.media.metadata.authorName || ''
if (this.isPodcast) this.provider = 'itunes'
else this.provider = localStorage.getItem('book-provider') || 'google'
else {
this.provider = this.getDefaultBookProvider()
}
// Prefer using ASIN if set and using audible provider
if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) {
@@ -461,6 +487,12 @@ export default {
// match.genres = match.genres.join(',')
match.genres = match.genres.split(',').map((g) => g.trim())
}
if (match.tags && !Array.isArray(match.tags)) {
match.tags = match.tags.split(',').map((g) => g.trim())
}
if (match.narrator && !Array.isArray(match.narrator)) {
match.narrator = match.narrator.split(',').map((g) => g.trim())
}
}
console.log('Select Match', match)
@@ -490,7 +522,10 @@ export default {
} else if (key === 'author' && !this.isPodcast) {
var authors = this.selectedMatch[key]
if (!Array.isArray(authors)) {
authors = authors.split(',').map((au) => au.trim())
authors = authors
.split(',')
.map((au) => au.trim())
.filter((au) => !!au)
}
var authorPayload = []
authors.forEach((authorName) =>
@@ -501,11 +536,11 @@ export default {
)
updatePayload.metadata.authors = authorPayload
} else if (key === 'narrator') {
updatePayload.metadata.narrators = this.selectedMatch[key].split(',').map((v) => v.trim())
updatePayload.metadata.narrators = this.selectedMatch[key]
} else if (key === 'genres') {
updatePayload.metadata.genres = [...this.selectedMatch[key]]
} else if (key === 'tags') {
updatePayload.tags = this.selectedMatch[key].split(',').map((v) => v.trim())
updatePayload.tags = this.selectedMatch[key]
} else if (key === 'itunesId') {
updatePayload.metadata.itunesId = Number(this.selectedMatch[key])
} else {
@@ -528,24 +563,11 @@ export default {
// Persist in local storage
localStorage.setItem('selectedMatchUsage', JSON.stringify(this.selectedMatchUsage))
if (updatePayload.metadata.cover) {
const coverPayload = {
url: updatePayload.metadata.cover
}
const success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, coverPayload).catch((error) => {
console.error('Failed to update', error)
return false
})
if (success) {
this.$toast.success(this.$strings.ToastItemCoverUpdateSuccess)
} else {
this.$toast.error(this.$strings.ToastItemCoverUpdateFailed)
}
console.log('Updated cover')
delete updatePayload.metadata.cover
}
if (Object.keys(updatePayload).length) {
if (updatePayload.metadata.cover) {
updatePayload.url = updatePayload.metadata.cover
delete updatePayload.metadata.cover
}
const mediaUpdatePayload = updatePayload
const updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, mediaUpdatePayload).catch((error) => {
console.error('Failed to update', error)
@@ -580,6 +602,7 @@ export default {
.matchListWrapper {
height: calc(100% - 124px);
}
@media (min-width: 768px) {
.matchListWrapper {
height: calc(100% - 80px);

View File

@@ -20,7 +20,7 @@
</ui-tooltip>
</div>
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
<ui-text-input ref="maxEpisodesInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" />
<ui-text-input ref="maxEpisodesToDownloadInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" />
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded.">
<p class="pl-4 text-base">
Max new episodes to download per check
@@ -129,9 +129,12 @@ export default {
return
}
}
if (this.$refs.maxEpisodesInput && this.$refs.maxEpisodesInput.isFocused) {
if (this.$refs.maxEpisodesInput?.isFocused) {
this.$refs.maxEpisodesInput.blur()
return
}
if (this.$refs.maxEpisodesToDownloadInput?.isFocused) {
this.$refs.maxEpisodesToDownloadInput.blur()
}
const updatePayload = {
@@ -140,9 +143,11 @@ export default {
if (this.enableAutoDownloadEpisodes) {
updatePayload.autoDownloadSchedule = this.cronExpression
}
this.newMaxEpisodesToKeep = Number(this.newMaxEpisodesToKeep)
if (this.newMaxEpisodesToKeep !== this.maxEpisodesToKeep) {
updatePayload.maxEpisodesToKeep = this.newMaxEpisodesToKeep
}
this.newMaxNewEpisodesToDownload = Number(this.newMaxNewEpisodesToDownload)
if (this.newMaxNewEpisodesToDownload !== this.maxNewEpisodesToDownload) {
updatePayload.maxNewEpisodesToDownload = this.newMaxNewEpisodesToDownload
}

View File

@@ -2,8 +2,11 @@
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6">
<p class="text-xl font-semibold mb-2">{{ $strings.HeaderAudiobookTools }}</p>
<!-- alert for windows install -->
<widgets-alert v-if="isWindowsInstall" type="warning" class="my-8 text-base">Not supported for the Windows install yet</widgets-alert>
<!-- Merge to m4b -->
<div v-if="showM4bDownload" class="w-full border border-black-200 p-4 my-8">
<div v-if="showM4bDownload && !isWindowsInstall" class="w-full border border-black-200 p-4 my-8">
<div class="flex flex-wrap items-center">
<div>
<p class="text-lg">{{ $strings.LabelToolsMakeM4b }}</p>
@@ -19,22 +22,8 @@
</div>
</div>
<!-- Split to mp3 -->
<!-- <div v-if="showMp3Split" class="w-full border border-black-200 p-4 my-8">
<div class="flex items-center">
<div>
<p class="text-lg">{{ $strings.LabelToolsSplitM4b }}</p>
<p class="max-w-sm text-sm pt-2 text-gray-300">{{ $strings.LabelToolsSplitM4bDescription }}</p>
</div>
<div class="flex-grow" />
<div>
<ui-btn :disabled="true">{{ $strings.MessageNotYetImplemented }}</ui-btn>
</div>
</div>
</div> -->
<!-- Embed Metadata -->
<div v-if="mediaTracks.length" class="w-full border border-black-200 p-4 my-8">
<div v-if="mediaTracks.length && !isWindowsInstall" class="w-full border border-black-200 p-4 my-8">
<div class="flex items-center">
<div>
<p class="text-lg">{{ $strings.LabelToolsEmbedMetadata }}</p>
@@ -122,6 +111,12 @@ export default {
},
isEncodeTaskRunning() {
return this.encodeTask && !this.encodeTask?.isFinished
},
isWindowsInstall() {
return this.Source == 'windows'
},
Source() {
return this.$store.state.Source
}
},
methods: {

View File

@@ -20,7 +20,7 @@
<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">
<span class="material-icons 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 type="text" class="w-full" />
<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>
</div>
<div class="flex py-1 px-2 items-center w-full">
@@ -31,7 +31,7 @@
<ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn>
</div>
</div>
<modals-libraries-folder-chooser v-else :paths="folderPaths" @back="showDirectoryPicker = false" @select="selectFolder" />
<modals-libraries-lazy-folder-chooser v-else :paths="folderPaths" @back="showDirectoryPicker = false" @select="selectFolder" />
</div>
</template>
@@ -67,10 +67,6 @@ export default {
value: 'podcast',
text: this.$strings.LabelPodcasts
}
// {
// value: 'music',
// text: 'Music'
// }
]
},
folderPaths() {
@@ -110,6 +106,11 @@ export default {
formUpdated() {
this.$emit('update', this.getLibraryData())
},
existingFolderInputBlurred(folder) {
if (!folder.fullPath) {
this.removeFolder(folder)
}
},
newFolderInputBlurred() {
if (this.newFolderPath) {
this.folders.push({ fullPath: this.newFolderPath })
@@ -149,6 +150,7 @@ export default {
this.folders = this.library ? this.library.folders.map((p) => ({ ...p })) : []
this.icon = this.library ? this.library.icon : 'default'
this.mediaType = this.library ? this.library.mediaType : 'book'
this.showDirectoryPicker = false
}
},

View File

@@ -1,5 +1,5 @@
<template>
<modals-modal v-model="show" name="edit-library" :width="700" :height="'unset'" :processing="processing">
<modals-modal v-model="show" name="edit-library" :width="800" :height="'unset'" :processing="processing">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
<p class="text-xl md:text-3xl text-white truncate">{{ title }}</p>
@@ -12,9 +12,9 @@
</div>
<div class="px-2 md:px-4 w-full text-sm pt-2 md:pt-6 pb-20 rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
<component v-if="libraryCopy && show" ref="tabComponent" :is="tabName" :is-new="!library" :library="libraryCopy" :processing.sync="processing" @update="updateLibrary" @close="show = false" />
<component v-if="libraryCopy && show" ref="tabComponent" :is="tabName" :is-new="!library" :library="libraryCopy" :library-id="libraryId" :processing.sync="processing" @update="updateLibrary" @close="show = false" />
<div class="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-white border-opacity-10">
<div v-show="selectedTab !== 'tools'" class="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-white border-opacity-10">
<div class="flex justify-end">
<ui-btn @click="submit">{{ buttonText }}</ui-btn>
</div>
@@ -54,6 +54,12 @@ export default {
buttonText() {
return this.library ? this.$strings.ButtonSave : this.$strings.ButtonCreate
},
mediaType() {
return this.libraryCopy?.mediaType
},
libraryId() {
return this.library?.id
},
tabs() {
return [
{
@@ -66,12 +72,26 @@ export default {
title: this.$strings.HeaderSettings,
component: 'modals-libraries-library-settings'
},
{
id: 'scanner',
title: this.$strings.HeaderSettingsScanner,
component: 'modals-libraries-library-scanner-settings'
},
{
id: 'schedule',
title: this.$strings.HeaderSchedule,
component: 'modals-libraries-schedule-scan'
},
{
id: 'tools',
title: this.$strings.HeaderTools,
component: 'modals-libraries-library-tools'
}
]
].filter((tab) => {
// Do not show tools tab for new libraries
if (tab.id === 'tools' && !this.library) return false
return tab.id !== 'scanner' || this.mediaType === 'book'
})
},
tabName() {
var _tab = this.tabs.find((t) => t.id === this.selectedTab)
@@ -105,7 +125,10 @@ export default {
disableWatcher: false,
skipMatchingMediaWithAsin: false,
skipMatchingMediaWithIsbn: false,
autoScanCronExpression: null
autoScanCronExpression: null,
hideSingleBookSeries: false,
onlyShowLaterBooksInContinueSeries: false,
metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
}
}
},
@@ -120,7 +143,7 @@ export default {
for (const key in this.libraryCopy) {
if (library[key] !== undefined) {
if (key === 'folders') {
this.libraryCopy.folders = library.folders.map((f) => ({ ...f }))
this.libraryCopy.folders = library.folders.map((f) => ({ ...f })).filter((f) => !!f.fullPath?.trim())
} else if (key === 'settings') {
for (const settingKey in library.settings) {
this.libraryCopy.settings[settingKey] = library.settings[settingKey]

View File

@@ -4,35 +4,37 @@
<span class="material-icons text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span>
<p class="px-4 text-xl">{{ $strings.HeaderChooseAFolder }}</p>
</div>
<div v-if="allFolders.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2">
<p class="font-mono truncate">{{ selectedPath || '\\' }}</p>
<div v-if="rootDirs.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2">
<p class="font-mono truncate">{{ selectedPath || '/' }}</p>
</div>
<div v-if="allFolders.length" class="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 v-if="level > 0" class="w-full p-1 cursor-pointer flex items-center" @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>
<p class="text-base font-mono px-2">..</p>
</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" :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>
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
<span v-if="dir.dirs && dir.dirs.length && dir.path === selectedPath" class="material-icons" style="font-size: 1.1rem">arrow_right</span>
<span v-if="dir.path === selectedPath" class="material-icons" style="font-size: 1.1rem">arrow_right</span>
</div>
</div>
<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" @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>
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
</div>
</div>
<div v-if="loadingDirs" class="absolute inset-0 w-full h-full flex items-center justify-center bg-black/10">
<ui-loading-indicator />
</div>
</div>
<div v-else-if="loadingFolders" class="py-12 text-center">
<div v-else-if="initialLoad" class="py-12 text-center">
<p>{{ $strings.MessageLoadingFolders }}</p>
</div>
<div v-else class="py-12 text-center max-w-sm mx-auto">
<p class="text-lg mb-2">{{ $strings.MessageNoFoldersAvailable }}</p>
<p class="text-gray-300 mb-2">{{ $strings.NoteFolderPicker }}</p>
<p v-if="isDebian" class="text-red-400">{{ $strings.NoteFolderPickerDebian }}</p>
</div>
<div class="w-full py-2">
@@ -51,11 +53,12 @@ export default {
},
data() {
return {
loadingFolders: false,
allFolders: [],
initialLoad: false,
loadingDirs: false,
isPosix: true,
rootDirs: [],
directories: [],
selectedPath: '',
selectedFullPath: '',
subdirs: [],
level: 0,
currentDir: null,
@@ -89,68 +92,91 @@ export default {
...d
}
})
},
isDebian() {
return this.Source == 'debian'
},
Source() {
return this.$store.state.Source
}
},
methods: {
goBack() {
var splitPaths = this.selectedPath.split('\\').slice(1)
var prev = splitPaths.slice(0, -1).join('\\')
async goBack() {
let selPath = this.selectedPath.replace(/^\//, '')
var splitPaths = selPath.split('/')
var currDirs = this.allFolders
for (let i = 0; i < splitPaths.length; i++) {
var _dir = currDirs.find((dir) => dir.dirname === splitPaths[i])
if (_dir && _dir.path.slice(1) === prev) {
this.directories = currDirs
this.selectDir(_dir)
return
} else if (_dir) {
currDirs = _dir.dirs
}
let previousPath = ''
let lookupPath = ''
if (splitPaths.length > 2) {
lookupPath = splitPaths.slice(0, -2).join('/')
}
previousPath = splitPaths.slice(0, -1).join('/')
if (!this.isPosix) {
// For windows drives add a trailing slash. e.g. C:/
if (!this.isPosix && lookupPath.endsWith(':')) {
lookupPath += '/'
}
if (!this.isPosix && previousPath.endsWith(':')) {
previousPath += '/'
}
} else {
// Add leading slash
if (previousPath) previousPath = '/' + previousPath
if (lookupPath) lookupPath = '/' + lookupPath
}
this.level--
this.subdirs = this.directories
this.selectedPath = previousPath
this.directories = await this.fetchDirs(lookupPath, this.level)
},
selectDir(dir) {
async selectDir(dir) {
if (dir.isUsed) return
this.selectedPath = dir.path
this.selectedFullPath = dir.fullPath
this.level = dir.level
this.subdirs = dir.dirs
this.subdirs = await this.fetchDirs(dir.path, dir.level + 1)
},
selectSubDir(dir) {
async selectSubDir(dir) {
if (dir.isUsed) return
this.selectedPath = dir.path
this.selectedFullPath = dir.fullPath
this.level = dir.level
this.directories = this.subdirs
this.subdirs = dir.dirs
this.subdirs = await this.fetchDirs(dir.path, dir.level + 1)
},
selectFolder() {
if (!this.selectedPath) {
console.error('No Selected path')
return
}
if (this.paths.find((p) => p.startsWith(this.selectedFullPath))) {
if (this.paths.find((p) => p.startsWith(this.selectedPath))) {
this.$toast.error(`Oops, you cannot add a parent directory of a folder already added`)
return
}
this.$emit('select', this.selectedFullPath)
this.$emit('select', this.selectedPath)
this.selectedPath = ''
this.selectedFullPath = ''
},
fetchDirs(path, level) {
this.loadingDirs = true
return this.$axios
.$get(`/api/filesystem?path=${path}&level=${level}`)
.then((data) => {
console.log('Fetched directories', data.directories)
this.isPosix = !!data.posix
return data.directories
})
.catch((error) => {
console.error('Failed to get filesystem paths', error)
this.$toast.error('Failed to get filesystem paths')
return []
})
.finally(() => {
this.loadingDirs = false
})
},
async init() {
this.loadingFolders = true
this.allFolders = await this.$store.dispatch('libraries/loadFolders')
this.loadingFolders = false
this.initialLoad = true
this.rootDirs = await this.fetchDirs('', 0)
this.initialLoad = false
this.directories = this.allFolders
this.directories = this.rootDirs
this.subdirs = []
this.selectedPath = ''
this.selectedFullPath = ''
}
},
mounted() {

View File

@@ -0,0 +1,160 @@
<template>
<div class="w-full h-full px-1 md:px-4 py-1 mb-4">
<div class="flex items-center justify-between mb-2">
<h2 class="text-base md:text-lg text-gray-200">{{ $strings.HeaderMetadataOrderOfPrecedence }}</h2>
<ui-btn small @click="resetToDefault">{{ $strings.ButtonResetToDefault }}</ui-btn>
</div>
<div class="flex items-center justify-between md:justify-start mb-4">
<p class="text-sm text-gray-300 pr-2">{{ $strings.LabelMetadataOrderOfPrecedenceDescription }}</p>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" 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>
</a>
</ui-tooltip>
</div>
<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">
<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>
<div class="text-center py-1 w-8 min-w-8">
{{ source.include ? getSourceIndex(source.id) : '' }}
</div>
<div class="flex-grow inline-flex justify-between px-4 py-3">
{{ source.name }} <span v-if="source.include && (index === firstActiveSourceIndex || index === lastActiveSourceIndex)" class="px-2 italic font-semibold text-xs text-gray-400">{{ index === firstActiveSourceIndex ? $strings.LabelHighestPriority : $strings.LabelLowestPriority }}</span>
</div>
<div class="px-2 opacity-100">
<ui-toggle-switch v-model="source.include" :off-color="'error'" @input="includeToggled(source)" />
</div>
</li>
</transition-group>
</draggable>
</div>
</template>
<script>
import draggable from 'vuedraggable'
export default {
components: {
draggable
},
props: {
library: {
type: Object,
default: () => null
},
processing: Boolean
},
data() {
return {
drag: false,
dragOptions: {
animation: 200,
group: 'description',
ghostClass: 'ghost'
},
metadataSourceData: {
folderStructure: {
id: 'folderStructure',
name: 'Folder structure',
include: true
},
audioMetatags: {
id: 'audioMetatags',
name: 'Audio file meta tags OR ebook metadata',
include: true
},
nfoFile: {
id: 'nfoFile',
name: 'NFO file',
include: true
},
txtFiles: {
id: 'txtFiles',
name: 'desc.txt & reader.txt files',
include: true
},
opfFile: {
id: 'opfFile',
name: 'OPF file',
include: true
},
absMetadata: {
id: 'absMetadata',
name: 'Audiobookshelf metadata file',
include: true
}
},
metadataSourceMapped: []
}
},
computed: {
librarySettings() {
return this.library.settings || {}
},
mediaType() {
return this.library.mediaType
},
isBookLibrary() {
return this.mediaType === 'book'
},
firstActiveSourceIndex() {
return this.metadataSourceMapped.findIndex((source) => source.include)
},
lastActiveSourceIndex() {
return this.metadataSourceMapped.findLastIndex((source) => source.include)
}
},
methods: {
getSourceIndex(source) {
const activeSources = (this.librarySettings.metadataPrecedence || []).map((s) => s).reverse()
return activeSources.findIndex((s) => s === source) + 1
},
resetToDefault() {
this.metadataSourceMapped = []
for (const key in this.metadataSourceData) {
this.metadataSourceMapped.push({ ...this.metadataSourceData[key] })
}
this.metadataSourceMapped.reverse()
this.$emit('update', this.getLibraryData())
},
getLibraryData() {
const metadataSourceIds = this.metadataSourceMapped.map((source) => (source.include ? source.id : null)).filter((s) => s)
metadataSourceIds.reverse()
return {
settings: {
metadataPrecedence: metadataSourceIds
}
}
},
includeToggled(source) {
this.updated()
},
draggableUpdate() {
this.updated()
},
updated() {
this.$emit('update', this.getLibraryData())
},
init() {
const metadataPrecedence = this.librarySettings.metadataPrecedence || []
this.metadataSourceMapped = metadataPrecedence.map((source) => this.metadataSourceData[source]).filter((s) => s)
for (const sourceKey in this.metadataSourceData) {
if (!metadataPrecedence.includes(sourceKey)) {
const unusedSourceData = { ...this.metadataSourceData[sourceKey], include: false }
this.metadataSourceMapped.unshift(unusedSourceData)
}
}
this.metadataSourceMapped.reverse()
}
},
mounted() {
this.init()
}
}
</script>

View File

@@ -11,9 +11,9 @@
</div>
<div class="py-3">
<div class="flex items-center">
<ui-toggle-switch v-if="!globalWatcherDisabled" v-model="disableWatcher" @input="formUpdated" />
<ui-toggle-switch v-if="!globalWatcherDisabled" v-model="enableWatcher" @input="formUpdated" />
<ui-toggle-switch v-else disabled :value="false" />
<p class="pl-4 text-base">{{ $strings.LabelSettingsDisableWatcherForLibrary }}</p>
<p class="pl-4 text-base">{{ $strings.LabelSettingsEnableWatcherForLibrary }}</p>
</div>
<p v-if="globalWatcherDisabled" class="text-xs text-warning">*{{ $strings.MessageWatcherIsDisabledGlobally }}</p>
</div>
@@ -49,6 +49,31 @@
</ui-tooltip>
</div>
</div>
<div v-if="isBookLibrary" class="py-3">
<div class="flex items-center">
<ui-toggle-switch v-model="onlyShowLaterBooksInContinueSeries" @input="formUpdated" />
<ui-tooltip :text="$strings.LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp">
<p class="pl-4 text-base">
{{ $strings.LabelSettingsOnlyShowLaterBooksInContinueSeries }}
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div>
</div>
<div v-if="isBookLibrary" class="py-3">
<div class="flex items-center">
<ui-toggle-switch v-model="epubsAllowScriptedContent" @input="formUpdated" />
<ui-tooltip :text="$strings.LabelSettingsEpubsAllowScriptedContentHelp">
<p class="pl-4 text-base">
{{ $strings.LabelSettingsEpubsAllowScriptedContent }}
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div>
</div>
<div v-if="isPodcastLibrary" class="py-3">
<ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-72" menu-max-height="200px" @input="formUpdated" />
</div>
</div>
</template>
@@ -65,11 +90,14 @@ export default {
return {
provider: null,
useSquareBookCovers: false,
disableWatcher: false,
enableWatcher: false,
skipMatchingMediaWithAsin: false,
skipMatchingMediaWithIsbn: false,
audiobooksOnly: false,
hideSingleBookSeries: false
epubsAllowScriptedContent: false,
hideSingleBookSeries: false,
onlyShowLaterBooksInContinueSeries: false,
podcastSearchRegion: 'us'
}
},
computed: {
@@ -85,6 +113,9 @@ export default {
isBookLibrary() {
return this.mediaType === 'book'
},
isPodcastLibrary() {
return this.mediaType === 'podcast'
},
providers() {
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
return this.$store.state.scanners.providers
@@ -95,11 +126,14 @@ export default {
return {
settings: {
coverAspectRatio: this.useSquareBookCovers ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD,
disableWatcher: !!this.disableWatcher,
disableWatcher: !this.enableWatcher,
skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin,
skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn,
audiobooksOnly: !!this.audiobooksOnly,
hideSingleBookSeries: !!this.hideSingleBookSeries
epubsAllowScriptedContent: !!this.epubsAllowScriptedContent,
hideSingleBookSeries: !!this.hideSingleBookSeries,
onlyShowLaterBooksInContinueSeries: !!this.onlyShowLaterBooksInContinueSeries,
podcastSearchRegion: this.podcastSearchRegion
}
}
},
@@ -108,15 +142,18 @@ export default {
},
init() {
this.useSquareBookCovers = this.librarySettings.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE
this.disableWatcher = !!this.librarySettings.disableWatcher
this.enableWatcher = !this.librarySettings.disableWatcher
this.skipMatchingMediaWithAsin = !!this.librarySettings.skipMatchingMediaWithAsin
this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn
this.audiobooksOnly = !!this.librarySettings.audiobooksOnly
this.epubsAllowScriptedContent = !!this.librarySettings.epubsAllowScriptedContent
this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries
this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries
this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us'
}
},
mounted() {
this.init()
}
}
</script>
</script>

View File

@@ -0,0 +1,81 @@
<template>
<div class="w-full h-full px-1 md:px-2 py-1 mb-4">
<div class="w-full border border-black-200 p-4 my-8">
<div class="flex flex-wrap items-center">
<div>
<p class="text-lg">Remove metadata files in library item folders</p>
<p class="max-w-sm text-sm pt-2 text-gray-300">Remove all metadata.json or metadata.abs files in your {{ mediaType }} folders</p>
</div>
<div class="flex-grow" />
<div>
<ui-btn class="mb-4 block" @click.stop="removeAllMetadataClick('json')">Remove all metadata.json</ui-btn>
<ui-btn @click.stop="removeAllMetadataClick('abs')">Remove all metadata.abs</ui-btn>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
library: {
type: Object,
default: () => null
},
libraryId: String,
processing: Boolean
},
data() {
return {}
},
computed: {
librarySettings() {
return this.library.settings || {}
},
mediaType() {
return this.library.mediaType
},
isBookLibrary() {
return this.mediaType === 'book'
}
},
methods: {
removeAllMetadataClick(ext) {
const payload = {
message: `Are you sure you want to remove all metadata.${ext} files in your library item folders?`,
persistent: true,
callback: (confirmed) => {
if (confirmed) {
this.removeAllMetadataInLibrary(ext)
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
removeAllMetadataInLibrary(ext) {
this.$emit('update:processing', true)
this.$axios
.$post(`/api/libraries/${this.libraryId}/remove-metadata?ext=${ext}`)
.then((data) => {
if (!data.found) {
this.$toast.info(`No metadata.${ext} files were found in library`)
} else if (!data.removed) {
this.$toast.success(`No metadata.${ext} files removed`)
} else {
this.$toast.success(`Successfully removed ${data.removed} metadata.${ext} files`)
}
})
.catch((error) => {
console.error('Failed to remove metadata files', error)
this.$toast.error('Failed to remove metadata files')
})
.finally(() => {
this.$emit('update:processing', false)
})
}
},
mounted() {}
}
</script>

View File

@@ -115,7 +115,7 @@ export default {
})
.catch((error) => {
console.error('Failed to get playlists', error)
this.$toast.error('Failed to load user playlists')
this.$toast.error(this.$strings.ToastFailedToLoadData)
})
.finally(() => {
this.processing = false

View File

@@ -16,11 +16,11 @@
v-for="(episode, index) in episodesList"
:key="index"
class="relative"
:class="itemEpisodeMap[episode.cleanUrl] ? 'bg-primary bg-opacity-40' : selectedEpisodes[episode.cleanUrl] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'"
:class="getIsEpisodeDownloaded(episode) ? 'bg-primary bg-opacity-40' : selectedEpisodes[episode.cleanUrl] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'"
@click="toggleSelectEpisode(episode)"
>
<div class="absolute top-0 left-0 h-full flex items-center p-2">
<span v-if="itemEpisodeMap[episode.cleanUrl]" class="material-icons text-success text-xl">download_done</span>
<span v-if="getIsEpisodeDownloaded(episode)" class="material-icons text-success text-xl">download_done</span>
<ui-checkbox v-else v-model="selectedEpisodes[episode.cleanUrl]" small checkbox-bg="primary" border-color="gray-600" />
</div>
<div class="px-8 py-2">
@@ -33,7 +33,7 @@
<div class="break-words">{{ episode.title }}</div>
<widgets-podcast-type-indicator :type="episode.episodeType" />
</div>
<p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p>
<p v-if="episode.subtitle" class="mb-1 text-sm text-gray-300 line-clamp-2">{{ episode.subtitle }}</p>
<p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
</div>
</div>
@@ -68,7 +68,9 @@ export default {
selectAll: false,
search: null,
searchTimeout: null,
searchText: null
searchText: null,
downloadedEpisodeGuidMap: {},
downloadedEpisodeUrlMap: {}
}
},
watch: {
@@ -93,7 +95,7 @@ export default {
return this.libraryItem.media.metadata.title || 'Unknown'
},
allDownloaded() {
return !this.episodesCleaned.some((episode) => !this.itemEpisodeMap[episode.cleanUrl])
return !this.episodesCleaned.some((episode) => !this.getIsEpisodeDownloaded(episode))
},
episodesSelected() {
return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key])
@@ -104,18 +106,7 @@ export default {
return this.$getString('LabelDownloadNEpisodes', [this.episodesSelected.length])
},
itemEpisodes() {
if (!this.libraryItem) return []
return this.libraryItem.media.episodes || []
},
itemEpisodeMap() {
const map = {}
this.itemEpisodes.forEach((item) => {
if (item.enclosure) {
const cleanUrl = this.getCleanEpisodeUrl(item.enclosure.url)
map[cleanUrl] = true
}
})
return map
return this.libraryItem?.media.episodes || []
},
episodesList() {
return this.episodesCleaned.filter((episode) => {
@@ -127,12 +118,25 @@ export default {
if (this.episodesList.length === this.episodesCleaned.length) {
return this.$strings.LabelSelectAllEpisodes
}
const episodesNotDownloaded = this.episodesList.filter((ep) => !this.itemEpisodeMap[ep.cleanUrl]).length
const episodesNotDownloaded = this.episodesList.filter((ep) => !this.getIsEpisodeDownloaded(ep)).length
return this.$getString('LabelSelectEpisodesShowing', [episodesNotDownloaded])
}
},
methods: {
getIsEpisodeDownloaded(episode) {
if (episode.guid && !!this.downloadedEpisodeGuidMap[episode.guid]) {
return true
}
if (this.downloadedEpisodeUrlMap[episode.cleanUrl]) {
return true
}
return false
},
/**
* UPDATE: As of v2.4.5 guid is used for matching existing downloaded episodes if it is found on the RSS feed.
* Fallback to checking the clean url
* @see https://github.com/advplyr/audiobookshelf/issues/2207
*
* RSS feed episode url is used for matching with existing downloaded episodes.
* Some RSS feeds include timestamps in the episode url (e.g. patreon) that can change on requests.
* These need to be removed in order to detect the same episode each time the feed is pulled.
@@ -169,13 +173,13 @@ export default {
},
toggleSelectAll(val) {
for (const episode of this.episodesList) {
if (this.itemEpisodeMap[episode.cleanUrl]) this.selectedEpisodes[episode.cleanUrl] = false
if (this.getIsEpisodeDownloaded(episode)) this.selectedEpisodes[episode.cleanUrl] = false
else this.$set(this.selectedEpisodes, episode.cleanUrl, val)
}
},
checkSetIsSelectedAll() {
for (const episode of this.episodesList) {
if (!this.itemEpisodeMap[episode.cleanUrl] && !this.selectedEpisodes[episode.cleanUrl]) {
if (!this.getIsEpisodeDownloaded(episode) && !this.selectedEpisodes[episode.cleanUrl]) {
this.selectAll = false
return
}
@@ -183,7 +187,7 @@ export default {
this.selectAll = true
},
toggleSelectEpisode(episode) {
if (this.itemEpisodeMap[episode.cleanUrl]) return
if (this.getIsEpisodeDownloaded(episode)) return
this.$set(this.selectedEpisodes, episode.cleanUrl, !this.selectedEpisodes[episode.cleanUrl])
this.checkSetIsSelectedAll()
},
@@ -219,6 +223,14 @@ export default {
})
},
init() {
this.downloadedEpisodeGuidMap = {}
this.downloadedEpisodeUrlMap = {}
this.itemEpisodes.forEach((episode) => {
if (episode.guid) this.downloadedEpisodeGuidMap[episode.guid] = episode.id
if (episode.enclosure?.url) this.downloadedEpisodeUrlMap[this.getCleanEpisodeUrl(episode.enclosure.url)] = episode.id
})
this.episodesCleaned = this.episodes
.filter((ep) => ep.enclosure?.url)
.map((_ep) => {

View File

@@ -15,8 +15,8 @@
<p class="text-xs text-gray-300">{{ podcastAuthor }}</p>
</div>
</div>
<p class="text-lg font-semibold mb-6">{{ title }}</p>
<div v-if="description" class="default-style" v-html="description" />
<p dir="auto" class="text-lg font-semibold mb-6">{{ title }}</p>
<div v-if="description" dir="auto" class="default-style" v-html="description" />
<p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p>
</div>
</modals-modal>

View File

@@ -19,7 +19,7 @@
<div class="w-full p-1">
<ui-textarea-with-label v-model="newEpisode.subtitle" :label="$strings.LabelSubtitle" :rows="3" />
</div>
<div class="w-full p-1 default-style">
<div class="w-full p-1">
<ui-rich-text-editor :label="$strings.LabelDescription" v-model="newEpisode.description" />
</div>
</div>

View File

@@ -18,7 +18,7 @@
<div v-for="(episode, index) in episodesFound" :key="index" class="w-full py-4 border-b border-white border-opacity-5 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer px-2" @click.stop="selectEpisode(episode)">
<p v-if="episode.episode" class="font-semibold text-gray-200">#{{ episode.episode }}</p>
<p class="break-words mb-1">{{ episode.title }}</p>
<p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p>
<p v-if="episode.subtitle" class="mb-1 text-sm text-gray-300 line-clamp-2">{{ episode.subtitle }}</p>
<p class="text-xs text-gray-400">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
</div>
</template>

View File

@@ -132,6 +132,8 @@ export default {
return
}
this.processing = true
const payload = {
serverAddress: window.origin,
slug: this.newFeedSlug,
@@ -151,6 +153,9 @@ export default {
const errorMsg = error.response ? error.response.data : null
this.$toast.error(errorMsg || 'Failed to open RSS Feed')
})
.finally(() => {
this.processing = false
})
},
copyToClipboard(str) {
this.$copyToClipboard(str, this)

View File

@@ -0,0 +1,124 @@
<template>
<modals-modal v-model="show" name="rss-feed-view-modal" :processing="processing" :width="700" :height="'unset'">
<div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
<div v-if="feed" class="w-full">
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedGeneral }}</p>
<div class="w-full relative">
<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>
</div>
<div v-if="feed.meta" class="mt-5">
<div class="flex py-0.5">
<div class="w-48">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedPreventIndexing }}</span>
</div>
<div>{{ feed.meta.preventIndexing ? 'Yes' : 'No' }}</div>
</div>
<div v-if="feed.meta.ownerName" class="flex py-0.5">
<div class="w-48">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerName }}</span>
</div>
<div>{{ feed.meta.ownerName }}</div>
</div>
<div v-if="feed.meta.ownerEmail" class="flex py-0.5">
<div class="w-48">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelRSSFeedCustomOwnerEmail }}</span>
</div>
<div>{{ feed.meta.ownerEmail }}</div>
</div>
</div>
<!-- -->
<div class="episodesTable mt-2">
<div class="bg-primary bg-opacity-40 h-12 header">
{{ $strings.LabelEpisodeTitle }}
</div>
<div class="scroller">
<div v-for="episode in feed.episodes" :key="episode.id" class="h-8 text-xs truncate">
{{ episode.title }}
</div>
</div>
</div>
</div>
</div>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean,
feed: {
type: Object,
default: () => {}
}
},
data() {
return {
processing: false
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
_feed() {
return this.feed || {}
}
},
methods: {
copyToClipboard(str) {
this.$copyToClipboard(str, this)
}
},
mounted() {}
}
</script>
<style scoped>
.episodesTable {
width: 100%;
max-width: 100%;
border: 1px solid #474747;
display: flex;
flex-direction: column;
}
.episodesTable div.header {
background-color: #272727;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
padding: 4px 8px;
}
.episodesTable .scroller {
display: flex;
flex-direction: column;
max-height: 250px;
overflow-x: hidden;
overflow-y: scroll;
}
.episodesTable .scroller div {
background-color: #373838;
padding: 4px 8px;
display: flex;
align-items: center;
justify-content: flex-start;
height: 32px;
flex: 0 0 32px;
}
.episodesTable .scroller div:nth-child(even) {
background-color: #2f2f2f;
}
</style>

View File

@@ -1,22 +1,30 @@
<template>
<div class="flex pt-4 pb-2 md:pt-0 md:pb-2">
<div class="flex items-center pt-4 pb-2 lg:pt-0 lg:pb-2">
<div class="flex-grow" />
<template v-if="!loading">
<div class="cursor-pointer flex items-center justify-center text-gray-300 mr-4 md:mr-8" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
<span class="material-icons text-2xl sm:text-3xl">first_page</span>
</div>
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
<span class="material-icons text-2xl sm:text-3xl">replay_10</span>
</div>
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
<ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 lg:mr-8">
<button :aria-label="$strings.ButtonPreviousChapter" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
<span class="material-icons text-2xl sm:text-3xl">first_page</span>
</button>
</ui-tooltip>
<ui-tooltip direction="top" :text="$strings.ButtonJumpBackward">
<button :aria-label="$strings.ButtonJumpBackward" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
<span class="material-icons text-2xl sm:text-3xl">replay_10</span>
</button>
</ui-tooltip>
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
<span class="material-icons text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
</div>
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
<span class="material-icons text-2xl sm:text-3xl">forward_10</span>
</div>
<div class="flex items-center justify-center ml-4 md:ml-8" :class="hasNextChapter ? 'text-gray-300 cursor-pointer' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter">
<span class="material-icons text-2xl sm:text-3xl">last_page</span>
</div>
</button>
<ui-tooltip direction="top" :text="$strings.ButtonJumpForward">
<button :aria-label="$strings.ButtonJumpForward" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
<span class="material-icons text-2xl sm:text-3xl">forward_10</span>
</button>
</ui-tooltip>
<ui-tooltip direction="top" :text="$strings.ButtonNextChapter" class="ml-4 lg:ml-8">
<button :aria-label="$strings.ButtonNextChapter" :disabled="!hasNextChapter" :class="hasNextChapter ? 'text-gray-300' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter">
<span class="material-icons text-2xl sm:text-3xl">last_page</span>
</button>
</ui-tooltip>
<controls-playback-speed-control v-model="playbackRateInput" @input="playbackRateUpdated" @change="playbackRateChanged" />
</template>
<template v-else>

View File

@@ -57,7 +57,6 @@ export default {
},
watch: {
duration: {
immediate: true,
handler() {
this.setChapterTicks()
}
@@ -205,10 +204,14 @@ export default {
},
windowResize() {
this.setTrackWidth()
this.setChapterTicks()
this.updatePlayedTrackWidth()
this.updateBufferTrack()
}
},
mounted() {
this.setTrackWidth()
this.setChapterTicks()
window.addEventListener('resize', this.windowResize)
},
beforeDestroy() {

View File

@@ -1,45 +1,45 @@
<template>
<div class="w-full -mt-6">
<div class="w-full relative mb-1">
<div class="absolute -top-10 md:top-0 right-0 lg:right-2 flex items-center h-full">
<div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full">
<!-- <span class="material-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
<ui-tooltip direction="top" :text="$strings.LabelVolume">
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden md:block" />
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden sm:block" />
</ui-tooltip>
<ui-tooltip direction="top" :text="$strings.LabelSleepTimer">
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
<button :aria-label="$strings.LabelSleepTimer" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
<span v-if="!sleepTimerSet" class="material-icons text-2xl">snooze</span>
<div v-else class="flex items-center">
<span class="material-icons text-lg text-warning">snooze</span>
<p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p>
</div>
</div>
</button>
</ui-tooltip>
<ui-tooltip v-if="!isPodcast" direction="top" :text="$strings.LabelViewBookmarks">
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
<button :aria-label="$strings.LabelViewBookmarks" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
<span class="material-icons text-2xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
</div>
</button>
</ui-tooltip>
<ui-tooltip v-if="chapters.length" direction="top" :text="$strings.LabelViewChapters">
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
<button :aria-label="$strings.LabelViewChapters" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
<span class="material-icons text-2xl">format_list_bulleted</span>
</div>
</button>
</ui-tooltip>
<ui-tooltip v-if="playerQueueItems.length" direction="top" :text="$strings.LabelViewQueue">
<button class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
<button :aria-label="$strings.LabelViewQueue" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
<span class="material-icons text-2.5xl sm:text-3xl">playlist_play</span>
</button>
</ui-tooltip>
<ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack">
<div class="cursor-pointer text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack">
<button :aria-label="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack" class="text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack">
<span class="material-icons text-2xl sm:text-3xl transform transition-transform" :class="useChapterTrack ? 'rotate-180' : ''">timelapse</span>
</div>
</button>
</ui-tooltip>
</div>

View File

@@ -1,7 +1,7 @@
<template>
<div class="w-full h-full">
<div v-show="showPageMenu" v-click-outside="clickOutside" class="pagemenu absolute top-9 left-8 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400" :style="{ width: pageMenuWidth + 'px' }">
<div v-for="(file, index) in cleanedPageNames" :key="file" class="w-full cursor-pointer hover:bg-black-200 px-2 py-1" :class="page === index ? 'bg-black-200' : ''" @click="setPage(index + 1)">
<div v-for="(file, index) in cleanedPageNames" :key="file" class="w-full cursor-pointer hover:bg-black-200 px-2 py-1" :class="page === index + 1 ? 'bg-black-200' : ''" @click="setPage(index + 1)">
<p class="text-sm truncate">{{ file }}</p>
</div>
</div>
@@ -14,34 +14,40 @@
</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-32' : 'left-20'">
<span class="material-icons text-xl">download</span>
</a>
<div v-if="comicMetadata" class="absolute top-0 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>
</div>
<div v-if="numPages" class="absolute top-0 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>
</div>
<div v-if="numPages" class="absolute top-0 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="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>
</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'">
<span class="material-icons text-xl">download</span>
</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">
<p class="font-mono">{{ page }} / {{ numPages }}</p>
</div>
<div v-if="mainImg" class="absolute top-0 right-36 sm:right-40 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">
<ui-icon-btn icon="zoom_out" :size="8" :disabled="!canScaleDown" borderless class="mr-px" @click="zoomOut" />
<ui-icon-btn icon="zoom_in" :size="8" :disabled="!canScaleUp" borderless class="ml-px" @click="zoomIn" />
</div>
<div class="overflow-hidden w-full h-full relative">
<div v-show="canGoPrev" 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="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 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>
</div>
</div>
<div v-show="canGoNext" 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">
<span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_forward_ios</span>
</div>
</div>
<div class="h-full flex justify-center">
<img v-if="mainImg" :src="mainImg" class="object-contain h-full m-auto" />
<div ref="imageContainer" class="w-full h-full relative overflow-auto">
<div class="h-full flex" :class="scale > 100 ? '' : 'justify-center'">
<img v-if="mainImg" :style="{ minWidth: scale + '%', width: scale + '%' }" :src="mainImg" class="object-contain m-auto" />
</div>
</div>
<div v-show="loading" class="w-full h-full absolute top-0 left-0 flex items-center justify-center z-10">
<ui-loading-indicator />
</div>
@@ -54,6 +60,10 @@ import Path from 'path'
import { Archive } from 'libarchive.js/main.js'
import { CompressedFile } from 'libarchive.js/src/compressed-file'
// This is % with respect to the screen width
const MAX_SCALE = 400
const MIN_SCALE = 10
Archive.init({
workerUrl: '/libarchive/worker-bundle.js'
})
@@ -81,7 +91,8 @@ export default {
showInfoMenu: false,
loadTimeout: null,
loadedFirstPage: false,
comicMetadata: null
comicMetadata: null,
scale: 80
}
},
watch: {
@@ -136,6 +147,12 @@ export default {
return p
}) || []
)
},
canScaleUp() {
return this.scale < MAX_SCALE
},
canScaleDown() {
return this.scale > MIN_SCALE
}
},
methods: {
@@ -303,8 +320,8 @@ export default {
},
parseImageFilename(filename) {
var basename = Path.basename(filename, Path.extname(filename))
var numbersinpath = basename.match(/\d{1,5}/g)
if (!numbersinpath || !numbersinpath.length) {
var numbersinpath = basename.match(/\d+/g)
if (!numbersinpath?.length) {
return {
index: -1,
filename
@@ -317,7 +334,7 @@ export default {
}
},
parseFilenames(filenames) {
const acceptableImages = ['.jpeg', '.jpg', '.png']
const acceptableImages = ['.jpeg', '.jpg', '.png', '.webp']
var imageFiles = filenames.filter((f) => {
return acceptableImages.includes((Path.extname(f) || '').toLowerCase())
})
@@ -331,10 +348,37 @@ export default {
orderedImages = orderedImages.concat(noNumImages.map((i) => i.filename))
this.pages = orderedImages
},
zoomIn() {
this.scale += 10
},
zoomOut() {
this.scale -= 10
},
scroll(event) {
const imageContainer = this.$refs.imageContainer
imageContainer.scrollBy({
top: event.deltaY,
left: event.deltaX,
behavior: 'auto'
})
}
},
mounted() {},
beforeDestroy() {}
mounted() {
const prevButton = this.$refs.prevButton
const nextButton = this.$refs.nextButton
prevButton.addEventListener('wheel', this.scroll, { passive: false })
nextButton.addEventListener('wheel', this.scroll, { passive: false })
},
beforeDestroy() {
const prevButton = this.$refs.prevButton
const nextButton = this.$refs.nextButton
prevButton.removeEventListener('wheel', this.scroll, { passive: false })
nextButton.removeEventListener('wheel', this.scroll, { passive: false })
}
}
</script>

View File

@@ -40,8 +40,10 @@ export default {
book: null,
/** @type {ePub.Rendition} */
rendition: null,
chapters: [],
ereaderSettings: {
theme: 'dark',
font: 'serif',
fontScale: 100,
lineSpacing: 115,
spread: 'auto'
@@ -61,16 +63,15 @@ export default {
libraryItemId() {
return this.libraryItem?.id
},
allowScriptedContent() {
return this.$store.getters['libraries/getLibraryEpubsAllowScriptedContent']
},
hasPrev() {
return !this.rendition?.location?.atStart
},
hasNext() {
return !this.rendition?.location?.atEnd
},
/** @returns {Array<ePub.NavItem>} */
chapters() {
return this.book?.navigation?.toc || []
},
userMediaProgress() {
if (!this.libraryItemId) return
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
@@ -130,17 +131,55 @@ export default {
const fontScale = settings.fontScale || 100
this.rendition.themes.fontSize(`${fontScale}%`)
this.rendition.themes.font(settings.font)
this.rendition.spread(settings.spread || 'auto')
},
prev() {
if (!this.rendition?.manager) return
return this.rendition?.prev()
},
next() {
if (!this.rendition?.manager) return
return this.rendition?.next()
},
goToChapter(href) {
if (!this.rendition?.manager) return
return this.rendition?.display(href)
},
/** @returns {object} Returns the chapter that the `position` in the book is in */
findChapterFromPosition(chapters, position) {
let foundChapter
for (let i = 0; i < chapters.length; i++) {
if (position >= chapters[i].start && (!chapters[i + 1] || position < chapters[i + 1].start)) {
foundChapter = chapters[i]
if (chapters[i].subitems && chapters[i].subitems.length > 0) {
return this.findChapterFromPosition(chapters[i].subitems, position, foundChapter)
}
break
}
}
return foundChapter
},
/** @returns {Array} Returns an array of chapters that only includes chapters with query results */
async searchBook(query) {
const chapters = structuredClone(await this.chapters)
const searchResults = await Promise.all(this.book.spine.spineItems.map((item) => item.load(this.book.load.bind(this.book)).then(item.find.bind(item, query)).finally(item.unload.bind(item))))
const mergedResults = [].concat(...searchResults)
mergedResults.forEach((chapter) => {
chapter.start = this.book.locations.percentageFromCfi(chapter.cfi)
const foundChapter = this.findChapterFromPosition(chapters, chapter.start)
if (foundChapter) foundChapter.searchResults.push(chapter)
})
let filteredResults = chapters.filter(function f(o) {
if (o.searchResults.length) return true
if (o.subitems.length) {
return (o.subitems = o.subitems.filter(f)).length
}
})
return filteredResults
},
keyUp(e) {
const rtl = this.book.package.metadata.direction === 'rtl'
if ((e.keyCode || e.which) == 37) {
@@ -280,6 +319,7 @@ export default {
reader.rendition = reader.book.renderTo('viewer', {
width: this.readerWidth,
height: this.readerHeight * 0.8,
allowScriptedContent: this.allowScriptedContent,
spread: 'auto',
snap: true,
manager: 'continuous',
@@ -314,8 +354,77 @@ export default {
this.checkSaveLocations(reader.book.locations.save())
})
}
this.getChapters()
})
},
getChapters() {
// Load the list of chapters in the book. See https://github.com/futurepress/epub.js/issues/759
const toc = this.book?.navigation?.toc || []
const tocTree = []
const resolveURL = (url, relativeTo) => {
// see https://github.com/futurepress/epub.js/issues/1084
// HACK-ish: abuse the URL API a little to resolve the path
// the base needs to be a valid URL, or it will throw a TypeError,
// so we just set a random base URI and remove it later
const base = 'https://example.invalid/'
return new URL(url, base + relativeTo).href.replace(base, '')
}
const basePath = this.book.packaging.navPath || this.book.packaging.ncxPath
const createTree = async (toc, parent) => {
const promises = toc.map(async (tocItem, i) => {
const href = resolveURL(tocItem.href, basePath)
const id = href.split('#')[1]
const item = this.book.spine.get(href)
await item.load(this.book.load.bind(this.book))
const el = id ? item.document.getElementById(id) : item.document.body
const cfi = item.cfiFromElement(el)
parent[i] = {
title: tocItem.label.trim(),
subitems: [],
href,
cfi,
start: this.book.locations.percentageFromCfi(cfi),
end: null, // set by flattenChapters()
id: null, // set by flattenChapters()
searchResults: []
}
if (tocItem.subitems) {
await createTree(tocItem.subitems, parent[i].subitems)
}
})
await Promise.all(promises)
}
return createTree(toc, tocTree).then(() => {
this.chapters = tocTree
})
},
flattenChapters(chapters) {
// Convert the nested epub chapters into something that looks like audiobook chapters for player-ui
const unwrap = (chapters) => {
return chapters.reduce((acc, chapter) => {
return chapter.subitems ? [...acc, chapter, ...unwrap(chapter.subitems)] : [...acc, chapter]
}, [])
}
let flattenedChapters = unwrap(chapters)
flattenedChapters = flattenedChapters.sort((a, b) => a.start - b.start)
for (let i = 0; i < flattenedChapters.length; i++) {
flattenedChapters[i].id = i
if (i < flattenedChapters.length - 1) {
flattenedChapters[i].end = flattenedChapters[i + 1].start
} else {
flattenedChapters[i].end = 1
}
}
return flattenedChapters
},
resize() {
this.windowWidth = window.innerWidth
this.windowHeight = window.innerHeight

View File

@@ -26,9 +26,9 @@
<component v-if="componentName" ref="readerComponent" :is="componentName" :library-item="selectedLibraryItem" :player-open="!!streamLibraryItem" :keep-progress="keepProgress" :file-id="ebookFileId" @touchstart="touchstart" @touchend="touchend" @hook:mounted="readerMounted" />
<!-- TOC side nav -->
<div v-if="tocOpen" class="w-full h-full fixed inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div>
<div v-if="isEpub" class="w-96 h-full max-h-full absolute top-0 left-0 shadow-xl transition-transform z-30 group-data-[theme=dark]:bg-primary group-data-[theme=dark]:text-white group-data-[theme=light]:bg-white group-data-[theme=light]:text-black" :class="tocOpen ? 'translate-x-0' : '-translate-x-96'" @click.stop.prevent="toggleToC">
<div class="p-4 h-full">
<div v-if="tocOpen" class="w-full h-full overflow-y-scroll absolute inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div>
<div v-if="isEpub" class="w-96 h-full max-h-full absolute top-0 left-0 shadow-xl transition-transform z-30 group-data-[theme=dark]:bg-primary group-data-[theme=dark]:text-white group-data-[theme=light]:bg-white group-data-[theme=light]:text-black" :class="tocOpen ? 'translate-x-0' : '-translate-x-96'" @click.stop.prevent>
<div class="flex flex-col p-4 h-full">
<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">
<span class="material-icons text-2xl">arrow_back</span>
@@ -36,13 +36,28 @@
<p class="text-lg font-semibold ml-2">{{ $strings.HeaderTableOfContents }}</p>
</div>
<div class="tocContent">
<form @submit.prevent="searchBook" @click.stop.prevent>
<ui-text-input clearable ref="input" @clear="searchBook" v-model="searchQuery" :placeholder="$strings.PlaceholderSearch" class="h-8 w-full text-sm flex mb-2" />
</form>
<div class="overflow-y-auto">
<div v-if="isSearching && !this.searchResults.length" class="w-full h-40 justify-center">
<p class="text-center text-xl py-4">{{ $strings.MessageNoResults }}</p>
</div>
<ul>
<li v-for="chapter in chapters" :key="chapter.id" class="py-1">
<a :href="chapter.href" class="opacity-80 hover:opacity-100" @click.prevent="$refs.readerComponent.goToChapter(chapter.href)">{{ chapter.label }}</a>
<li v-for="chapter in isSearching ? this.searchResults : chapters" :key="chapter.id" class="py-1">
<a :href="chapter.href" class="opacity-80 hover:opacity-100" @click.prevent="goToChapter(chapter.href)">{{ chapter.title }}</a>
<div v-for="searchResults in chapter.searchResults" :key="searchResults.cfi" class="text-sm py-1 pl-4">
<a :href="searchResults.cfi" class="opacity-50 hover:opacity-100" @click.prevent="goToChapter(searchResults.cfi)">{{ searchResults.excerpt }}</a>
</div>
<ul v-if="chapter.subitems.length">
<li v-for="subchapter in chapter.subitems" :key="subchapter.id" class="py-1 pl-4">
<a :href="subchapter.href" class="opacity-80 hover:opacity-100" @click.prevent="$refs.readerComponent.goToChapter(subchapter.href)">{{ subchapter.label }}</a>
<a :href="subchapter.href" class="opacity-80 hover:opacity-100" @click.prevent="goToChapter(subchapter.href)">{{ subchapter.title }}</a>
<div v-for="subChapterSearchResults in subchapter.searchResults" :key="subChapterSearchResults.cfi" class="text-sm py-1 pl-4">
<a :href="subChapterSearchResults.cfi" class="opacity-50 hover:opacity-100" @click.prevent="goToChapter(subChapterSearchResults.cfi)">{{ subChapterSearchResults.excerpt }}</a>
</div>
</li>
</ul>
</li>
@@ -63,7 +78,13 @@
<div class="w-40">
<p class="text-lg">{{ $strings.LabelTheme }}:</p>
</div>
<ui-toggle-btns v-model="ereaderSettings.theme" :items="themeItems" @input="settingsUpdated" />
<ui-toggle-btns v-model="ereaderSettings.theme" :items="themeItems.theme" @input="settingsUpdated" />
</div>
<div class="flex items-center mb-4">
<div class="w-40">
<p class="text-lg">{{ $strings.LabelFontFamily }}:</p>
</div>
<ui-toggle-btns v-model="ereaderSettings.font" :items="themeItems.font" @input="settingsUpdated" />
</div>
<div class="flex items-center mb-4">
<div class="w-40">
@@ -99,10 +120,14 @@ export default {
touchstartTime: 0,
touchIdentifier: null,
chapters: [],
isSearching: false,
searchResults: [],
searchQuery: '',
tocOpen: false,
showSettings: false,
ereaderSettings: {
theme: 'dark',
font: 'serif',
fontScale: 100,
lineSpacing: 115,
spread: 'auto'
@@ -142,16 +167,28 @@ export default {
]
},
themeItems() {
return [
{
text: this.$strings.LabelThemeDark,
value: 'dark'
},
{
text: this.$strings.LabelThemeLight,
value: 'light'
}
]
return {
theme: [
{
text: this.$strings.LabelThemeDark,
value: 'dark'
},
{
text: this.$strings.LabelThemeLight,
value: 'light'
}
],
font: [
{
text: 'Sans',
value: 'sans-serif'
},
{
text: 'Serif',
value: 'serif'
}
]
}
},
componentName() {
if (this.ebookType === 'epub') return 'readers-epub-reader'
@@ -235,6 +272,10 @@ export default {
}
},
methods: {
goToChapter(uri) {
this.toggleToC()
this.$refs.readerComponent.goToChapter(uri)
},
readerMounted() {
if (this.isEpub) {
this.loadEreaderSettings()
@@ -262,6 +303,15 @@ export default {
this.close()
}
},
async searchBook() {
if (this.searchQuery.length > 1) {
this.searchResults = await this.$refs.readerComponent.searchBook(this.searchQuery)
this.isSearching = true
} else {
this.isSearching = false
this.searchResults = []
}
},
next() {
if (this.$refs.readerComponent?.next) this.$refs.readerComponent.next()
},
@@ -340,6 +390,8 @@ export default {
},
close() {
this.unregisterListeners()
this.isSearching = false
this.searchQuery = ''
this.show = false
}
},
@@ -353,10 +405,6 @@ export default {
</script>
<style>
.tocContent {
height: calc(100% - 36px);
overflow-y: auto;
}
#reader {
height: 100%;
}

View File

@@ -1,45 +1,45 @@
<template>
<div class="flex flex-wrap justify-center mt-6">
<div class="flex px-2">
<svg class="h-14 w-14 md:h-18 md:w-18" viewBox="0 0 24 24">
<div class="flex p-2">
<svg class="h-14 w-14" viewBox="0 0 24 24">
<path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" />
</svg>
<div class="px-2">
<p class="text-4xl md:text-5xl font-bold">{{ totalItems }}</p>
<div class="px-1">
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalItems) }}</p>
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsInLibrary }}</p>
</div>
</div>
<div class="flex px-4">
<span class="material-icons text-7xl">show_chart</span>
<div class="flex p-2">
<span class="material-icons text-5xl py-1">show_chart</span>
<div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalTime }}</p>
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalTime) }}</p>
<p class="text-xs md:text-sm text-white text-opacity-80">{{ useOverallHours ? $strings.LabelStatsOverallHours : $strings.LabelStatsOverallDays }}</p>
</div>
</div>
<div class="flex px-4">
<svg class="h-14 w-14 md:h-18 md:w-18" viewBox="0 0 24 24">
<div v-if="isBookLibrary" class="flex p-2">
<svg class="h-14 w-14" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" />
</svg>
<div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalAuthors }}</p>
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalAuthors) }}</p>
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAuthors }}</p>
</div>
</div>
<div class="flex px-4">
<span class="material-icons-outlined text-6xl pt-1">insert_drive_file</span>
<div class="flex p-2">
<span class="material-icons-outlined text-5xl pt-1">insert_drive_file</span>
<div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalSizeNum }}</p>
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalSizeNum) }}</p>
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelSize }} ({{ totalSizeMod }})</p>
</div>
</div>
<div class="flex px-4">
<span class="material-icons-outlined text-6xl pt-1">audio_file</span>
<div class="flex p-2">
<span class="material-icons-outlined text-5xl pt-1">audio_file</span>
<div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ numAudioTracks }}</p>
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(numAudioTracks) }}</p>
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAudioTracks }}</p>
</div>
</div>
@@ -58,26 +58,32 @@ export default {
return {}
},
computed: {
currentLibraryMediaType() {
return this.$store.getters['libraries/getCurrentLibraryMediaType']
},
isBookLibrary() {
return this.currentLibraryMediaType === 'book'
},
user() {
return this.$store.state.user.user
},
totalItems() {
return this.libraryStats ? this.libraryStats.totalItems : 0
return this.libraryStats?.totalItems || 0
},
totalAuthors() {
return this.libraryStats ? this.libraryStats.totalAuthors : 0
return this.libraryStats?.totalAuthors || 0
},
numAudioTracks() {
return this.libraryStats ? this.libraryStats.numAudioTracks : 0
return this.libraryStats?.numAudioTracks || 0
},
totalDuration() {
return this.libraryStats ? this.libraryStats.totalDuration : 0
return this.libraryStats?.totalDuration || 0
},
totalHours() {
return Math.round(this.totalDuration / (60 * 60))
},
totalSizePretty() {
var totalSize = this.libraryStats ? this.libraryStats.totalSize : 0
var totalSize = this.libraryStats?.totalSize || 0
return this.$bytesPretty(totalSize, 1)
},
totalSizeNum() {

View File

@@ -0,0 +1,285 @@
<template>
<div>
<div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
<widgets-loading-spinner />
</div>
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" />
</div>
</template>
<script>
export default {
props: {
variant: {
type: Number,
default: 0
},
year: Number,
processing: Boolean
},
data() {
return {
canvas: null,
dataUrl: null,
yearStats: null
}
},
watch: {
variant() {
this.init()
}
},
methods: {
async initCanvas() {
if (!this.yearStats) return
const canvas = document.createElement('canvas')
canvas.width = 800
canvas.height = 800
const ctx = canvas.getContext('2d')
const createRoundedRect = (x, y, w, h) => {
const grd1 = ctx.createLinearGradient(x, y, x + w, y + h)
grd1.addColorStop(0, '#44444455')
grd1.addColorStop(1, '#ffffff11')
ctx.fillStyle = grd1
ctx.strokeStyle = '#C0C0C088'
ctx.beginPath()
ctx.roundRect(x, y, w, h, [20])
ctx.fill()
ctx.stroke()
}
const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y, maxWidth = 0) => {
ctx.fillStyle = color
ctx.font = `${fontWeight} ${fontSize} Source Sans Pro`
ctx.letterSpacing = letterSpacing
// If maxWidth is specified then continue to remove chars until under maxWidth and add ellipsis
if (maxWidth) {
let txtWidth = ctx.measureText(text).width
while (txtWidth > maxWidth) {
console.warn(`Text "${text}" is greater than max width ${maxWidth} (width:${txtWidth})`)
if (text.endsWith('...')) text = text.slice(0, -4) // Repeated checks remove 1 char at a time
else text = text.slice(0, -3) // First check remove last 3 chars
text += '...'
txtWidth = ctx.measureText(text).width
console.log(`Checking text "${text}" (width:${txtWidth})`)
}
}
ctx.fillText(text, x, y)
}
const addIcon = (icon, color, fontSize, x, y) => {
ctx.fillStyle = color
ctx.font = `${fontSize} Material Icons Outlined`
ctx.fillText(icon, x, y)
}
// Bg color
ctx.fillStyle = '#232323'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Cover image tiles
const bookCovers = this.yearStats.finishedBooksWithCovers
bookCovers.push(...this.yearStats.booksWithCovers)
let finishedBookCoverImgs = {}
if (bookCovers.length) {
let index = 0
ctx.globalAlpha = 0.25
ctx.save()
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate((-Math.PI / 180) * 25)
ctx.translate(-canvas.width / 2, -canvas.height / 2)
ctx.translate(-130, -120)
for (let x = 0; x < 5; x++) {
for (let y = 0; y < 5; y++) {
const coverIndex = index % bookCovers.length
let libraryItemId = bookCovers[coverIndex]
index++
await new Promise((resolve) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.addEventListener('load', () => {
let sw = img.width
if (img.width > img.height) {
sw = img.height
}
let sx = -(sw - img.width) / 2
let sy = -(sw - img.height) / 2
ctx.drawImage(img, sx, sy, sw, sw, 215 * x, 215 * y, 215, 215)
resolve()
if (this.yearStats.finishedBooksWithCovers.includes(libraryItemId) && !finishedBookCoverImgs[libraryItemId]) {
finishedBookCoverImgs[libraryItemId] = {
img,
sx,
sy,
sw
}
}
})
img.addEventListener('error', () => {
resolve()
})
img.src = this.$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId)
})
}
}
ctx.restore()
}
ctx.globalAlpha = 1
ctx.textBaseline = 'middle'
// Create gradient
const grd1 = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
grd1.addColorStop(0, '#000000aa')
grd1.addColorStop(1, '#cd9d49aa')
ctx.fillStyle = grd1
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Top Abs icon
let tanColor = '#ffdb70'
ctx.fillStyle = tanColor
ctx.font = '42px absicons'
ctx.fillText('\ue900', 15, 36)
// Top text
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
// Top left box
createRoundedRect(50, 100, 340, 160)
addText(this.yearStats.numBooksFinished, '64px', 'bold', 'white', '0px', 160, 165)
addText('books finished', '28px', 'normal', tanColor, '0px', 160, 210)
const readIconPath = new Path2D()
readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 2, d: 2, e: 100, f: 160 })
ctx.fillStyle = '#ffffff'
ctx.fill(readIconPath)
// Box top right
createRoundedRect(410, 100, 340, 160)
addText(this.$elapsedPrettyExtended(this.yearStats.totalListeningTime, true, false), '40px', 'bold', 'white', '0px', 500, 165)
addText('spent listening', '28px', 'normal', tanColor, '0px', 500, 205)
addIcon('watch_later', 'white', '52px', 440, 180)
// Box bottom left
createRoundedRect(50, 280, 340, 160)
addText(this.yearStats.totalListeningSessions, '64px', 'bold', 'white', '0px', 160, 345)
addText('sessions', '28px', 'normal', tanColor, '1px', 160, 390)
addIcon('headphones', 'white', '52px', 95, 360)
// Box bottom right
createRoundedRect(410, 280, 340, 160)
addText(this.yearStats.numBooksListened, '64px', 'bold', 'white', '0px', 500, 345)
addText('books listened to', '28px', 'normal', tanColor, '0px', 500, 390)
addIcon('local_library', 'white', '52px', 440, 360)
if (!this.variant) {
// Text stats
const topNarrator = this.yearStats.mostListenedNarrator
if (topNarrator) {
addText('TOP NARRATOR', '24px', 'normal', tanColor, '1px', 70, 520)
addText(topNarrator.name, '36px', 'bolder', 'white', '0px', 70, 564, 330)
addText(this.$elapsedPrettyExtended(topNarrator.time, true, false), '24px', 'lighter', 'white', '1px', 70, 599)
}
const topGenre = this.yearStats.topGenres[0]
if (topGenre) {
addText('TOP GENRE', '24px', 'normal', tanColor, '1px', 430, 520)
addText(topGenre.genre, '36px', 'bolder', 'white', '0px', 430, 564, 330)
addText(this.$elapsedPrettyExtended(topGenre.time, true, false), '24px', 'lighter', 'white', '1px', 430, 599)
}
const topAuthor = this.yearStats.topAuthors[0]
if (topAuthor) {
addText('TOP AUTHOR', '24px', 'normal', tanColor, '1px', 70, 670)
addText(topAuthor.name, '36px', 'bolder', 'white', '0px', 70, 714, 330)
addText(this.$elapsedPrettyExtended(topAuthor.time, true, false), '24px', 'lighter', 'white', '1px', 70, 749)
}
if (this.yearStats.mostListenedMonth?.time) {
const jsdate = new Date(this.year, this.yearStats.mostListenedMonth.month, 1)
const monthName = this.$formatJsDate(jsdate, 'LLLL')
addText('TOP MONTH', '24px', 'normal', tanColor, '1px', 430, 670)
addText(monthName, '36px', 'bolder', 'white', '0px', 430, 714, 330)
addText(this.$elapsedPrettyExtended(this.yearStats.mostListenedMonth.time, true, false), '24px', 'lighter', 'white', '1px', 430, 749)
}
} else if (this.variant === 1) {
// Bottom images
finishedBookCoverImgs = Object.values(finishedBookCoverImgs)
if (finishedBookCoverImgs.length > 0) {
ctx.textAlign = 'center'
addText('Some books finished this year...', '28px', 'normal', tanColor, '0px', canvas.width / 2, 530)
for (let i = 0; i < Math.min(5, finishedBookCoverImgs.length); i++) {
let imgToAdd = finishedBookCoverImgs[i]
ctx.drawImage(imgToAdd.img, imgToAdd.sx, imgToAdd.sy, imgToAdd.sw, imgToAdd.sw, 40 + 145 * i, 570, 140, 140)
}
}
} else if (this.variant === 2) {
// Text stats
if (this.yearStats.topAuthors.length) {
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 524)
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 584 + i * 60, 330)
}
}
if (this.yearStats.topGenres.length) {
addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 524)
for (let i = 0; i < this.yearStats.topGenres.length; i++) {
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 584 + i * 60, 330)
}
}
}
this.canvas = canvas
this.dataUrl = canvas.toDataURL('png')
},
refresh() {
this.init()
},
share() {
this.canvas.toBlob((blob) => {
const file = new File([blob], 'yearinreview.png', { type: blob.type })
const shareData = {
files: [file]
}
if (navigator.canShare(shareData)) {
navigator
.share(shareData)
.then(() => {
console.log('Share success')
})
.catch((error) => {
console.error('Failed to share', error)
if (error.name !== 'AbortError') {
this.$toast.error('Failed to share: ' + error.message)
}
})
} else {
this.$toast.error('Cannot share natively on this device')
}
})
},
async init() {
this.$emit('update:processing', true)
this.yearStats = await this.$axios.$get(`/api/me/stats/year/${this.year}`).catch((err) => {
console.error('Failed to load stats for year', err)
this.$toast.error(this.$strings.ToastFailedToLoadData)
return null
})
await this.initCanvas()
this.$emit('update:processing', false)
}
},
mounted() {
this.init()
}
}
</script>

View File

@@ -0,0 +1,146 @@
<template>
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-1 sm:p-4 mb-4">
<!-- hack to get icon fonts loaded on init -->
<div class="h-0 w-0 overflow-hidden opacity-0">
<span class="material-icons-outlined">close</span>
<span class="abs-icons icon-audiobookshelf" />
</div>
<div class="flex items-center">
<p class="hidden md:block text-xl font-semibold">{{ $getString('HeaderYearReview', [yearInReviewYear]) }}</p>
<div class="hidden md:block flex-grow" />
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide :
$strings.LabelYearReviewShow }}</ui-btn>
</div>
<!-- your year in review -->
<div v-if="showYearInReview">
<div class="w-full h-px bg-slate-200/10 my-4" />
<div class="flex items-center justify-center mb-2 max-w-[800px] mx-auto">
<!-- previous button -->
<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="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
</ui-btn>
<!-- 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">{{
$strings.ButtonShare }}
</ui-btn>
<div class="flex-grow" />
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelPersonalYearReview', [yearInReviewVariant + 1]) }}
</p>
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p>
<div class="flex-grow" />
<!-- refresh button -->
<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="material-icons sm:!hidden text-lg py-px">refresh</span>
</ui-btn>
<!-- next button -->
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn>
</div>
<stats-year-in-review ref="yearInReview" :variant="yearInReviewVariant" :year="yearInReviewYear" :processing.sync="processingYearInReview" />
<!-- your year in review short -->
<div class="w-full max-w-[800px] mx-auto my-4">
<!-- share button -->
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex sm:hidden items-center font-semibold mb-1" @click="shareYearInReviewShort">{{ $strings.ButtonShare }}</ui-btn>
<stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" />
</div>
<!-- your server in review -->
<div v-if="isAdminOrUp" class="w-full max-w-[800px] mx-auto mb-2 mt-4 border-t pt-4 border-white/10">
<div class="flex items-center justify-center mb-2">
<!-- previous button -->
<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="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
</ui-btn>
<!-- 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>
<div class="flex-grow" />
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelServerYearReview', [yearInReviewServerVariant + 1]) }}</p>
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p>
<div class="flex-grow" />
<!-- refresh button -->
<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="material-icons sm:!hidden text-lg py-px">refresh</span>
</ui-btn>
<!-- next button -->
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn>
</div>
</div>
<stats-year-in-review-server v-if="isAdminOrUp" ref="yearInReviewServer" :year="yearInReviewYear" :variant="yearInReviewServerVariant" :processing.sync="processingYearInReviewServer" />
</div>
</div>
</template>
<script>
export default {
data() {
return {
showYearInReview: false,
yearInReviewYear: 0,
yearInReviewVariant: 0,
yearInReviewServerVariant: 0,
processingYearInReview: false,
processingYearInReviewShort: false,
processingYearInReviewServer: false,
showShareButton: false
}
},
computed: {
isAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
}
},
methods: {
shareYearInReviewServer() {
this.$refs.yearInReviewServer.share()
},
shareYearInReview() {
this.$refs.yearInReview.share()
},
shareYearInReviewShort() {
this.$refs.yearInReviewShort.share()
},
refreshYearInReviewServer() {
this.$refs.yearInReviewServer.refresh()
},
refreshYearInReview() {
this.$refs.yearInReview.refresh()
this.$refs.yearInReviewShort.refresh()
},
clickShowYearInReview() {
this.showYearInReview = !this.showYearInReview
}
},
beforeMount() {
this.yearInReviewYear = new Date().getFullYear()
// When not December show previous year
if (new Date().getMonth() < 11) {
this.yearInReviewYear--
}
},
mounted() {
if (typeof navigator.share !== 'undefined' && navigator.share) {
this.showShareButton = true
} else {
console.warn('Navigator.share not supported')
}
}
}
</script>

View File

@@ -0,0 +1,264 @@
<template>
<div>
<div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
<widgets-loading-spinner />
</div>
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" />
</div>
</template>
<script>
export default {
props: {
variant: {
type: Number,
default: 0
},
processing: Boolean,
year: Number
},
data() {
return {
canvas: null,
dataUrl: null,
yearStats: null
}
},
watch: {
variant() {
this.init()
}
},
methods: {
async initCanvas() {
if (!this.yearStats) return
const canvas = document.createElement('canvas')
canvas.width = 800
canvas.height = 800
const ctx = canvas.getContext('2d')
const createRoundedRect = (x, y, w, h) => {
const grd1 = ctx.createLinearGradient(x, y, x + w, y + h)
grd1.addColorStop(0, '#44444455')
grd1.addColorStop(1, '#ffffff11')
ctx.fillStyle = grd1
ctx.strokeStyle = '#C0C0C088'
ctx.beginPath()
ctx.roundRect(x, y, w, h, [20])
ctx.fill()
ctx.stroke()
}
const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y, maxWidth = 0) => {
ctx.fillStyle = color
ctx.font = `${fontWeight} ${fontSize} Source Sans Pro`
ctx.letterSpacing = letterSpacing
// If maxWidth is specified then continue to remove chars until under maxWidth and add ellipsis
if (maxWidth) {
let txtWidth = ctx.measureText(text).width
while (txtWidth > maxWidth) {
console.warn(`Text "${text}" is greater than max width ${maxWidth} (width:${txtWidth})`)
if (text.endsWith('...')) text = text.slice(0, -4) // Repeated checks remove 1 char at a time
else text = text.slice(0, -3) // First check remove last 3 chars
text += '...'
txtWidth = ctx.measureText(text).width
console.log(`Checking text "${text}" (width:${txtWidth})`)
}
}
ctx.fillText(text, x, y)
}
// Bg color
ctx.fillStyle = '#232323'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Cover image tiles
let imgsToAdd = {}
if (this.yearStats.booksAddedWithCovers.length) {
let index = 0
ctx.globalAlpha = 0.25
ctx.save()
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate((-Math.PI / 180) * 25)
ctx.translate(-canvas.width / 2, -canvas.height / 2)
ctx.translate(-130, -120)
for (let x = 0; x < 5; x++) {
for (let y = 0; y < 5; y++) {
const coverIndex = index % this.yearStats.booksAddedWithCovers.length
let libraryItemId = this.yearStats.booksAddedWithCovers[coverIndex]
index++
await new Promise((resolve) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.addEventListener('load', () => {
let sw = img.width
if (img.width > img.height) {
sw = img.height
}
let sx = -(sw - img.width) / 2
let sy = -(sw - img.height) / 2
ctx.drawImage(img, sx, sy, sw, sw, 215 * x, 215 * y, 215, 215)
if (!imgsToAdd[libraryItemId]) {
imgsToAdd[libraryItemId] = {
img,
sx,
sy,
sw
}
}
resolve()
})
img.addEventListener('error', () => {
resolve()
})
img.src = this.$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId)
})
}
}
ctx.restore()
}
ctx.globalAlpha = 1
ctx.textBaseline = 'middle'
// Create gradient
const grd1 = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
grd1.addColorStop(0, '#000000aa')
grd1.addColorStop(1, '#cd9d49aa')
ctx.fillStyle = grd1
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Top Abs icon
let tanColor = '#ffdb70'
ctx.fillStyle = tanColor
ctx.font = '42px absicons'
ctx.fillText('\ue900', 15, 36)
// Top text
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
// Top left box
createRoundedRect(40, 100, 230, 100)
ctx.textAlign = 'center'
addText(this.yearStats.numBooksAdded, '48px', 'bold', 'white', '0px', 155, 140)
addText('books added', '18px', 'normal', tanColor, '0px', 155, 170)
// Box top right
createRoundedRect(285, 100, 230, 100)
addText(this.yearStats.numAuthorsAdded, '48px', 'bold', 'white', '0px', 400, 140)
addText('authors added', '18px', 'normal', tanColor, '0px', 400, 170)
// Box bottom left
createRoundedRect(530, 100, 230, 100)
addText(this.yearStats.numListeningSessions, '48px', 'bold', 'white', '0px', 645, 140)
addText('sessions', '18px', 'normal', tanColor, '1px', 645, 170)
// Text stats
if (this.yearStats.totalBooksAddedSize) {
addText('Your book collection grew to...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 260)
addText(this.$bytesPretty(this.yearStats.totalBooksSize), '36px', 'bolder', 'white', '0px', canvas.width / 2, 300)
addText('+' + this.$bytesPretty(this.yearStats.totalBooksAddedSize), '20px', 'lighter', 'white', '0px', canvas.width / 2, 330)
}
if (this.yearStats.totalBooksAddedDuration) {
addText('With a total duration of...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 400)
addText(this.$elapsedPrettyExtended(this.yearStats.totalBooksDuration, true, false), '36px', 'bolder', 'white', '0px', canvas.width / 2, 440)
addText('+' + this.$elapsedPrettyExtended(this.yearStats.totalBooksAddedDuration, true, false), '20px', 'lighter', 'white', '0px', canvas.width / 2, 470)
}
if (!this.variant) {
// Bottom images
imgsToAdd = Object.values(imgsToAdd)
if (imgsToAdd.length > 0) {
addText('Some additions include...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 540)
for (let i = 0; i < Math.min(5, imgsToAdd.length); i++) {
let imgToAdd = imgsToAdd[i]
ctx.drawImage(imgToAdd.img, imgToAdd.sx, imgToAdd.sy, imgToAdd.sw, imgToAdd.sw, 40 + 145 * i, 580, 140, 140)
}
}
} else if (this.variant === 1) {
// Text stats
ctx.textAlign = 'left'
if (this.yearStats.topAuthors.length) {
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549)
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
}
}
if (this.yearStats.topNarrators.length) {
addText('TOP NARRATORS', '24px', 'normal', tanColor, '1px', 430, 549)
for (let i = 0; i < this.yearStats.topNarrators.length; i++) {
addText(this.yearStats.topNarrators[i].name, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
}
}
} else if (this.variant === 2) {
// Text stats
ctx.textAlign = 'left'
if (this.yearStats.topAuthors.length) {
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549)
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
}
}
if (this.yearStats.topGenres.length) {
addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 549)
for (let i = 0; i < this.yearStats.topGenres.length; i++) {
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
}
}
}
this.canvas = canvas
this.dataUrl = canvas.toDataURL('png')
},
share() {
this.canvas.toBlob((blob) => {
const file = new File([blob], 'yearinreviewserver.png', { type: blob.type })
const shareData = {
files: [file]
}
if (navigator.canShare(shareData)) {
navigator
.share(shareData)
.then(() => {
console.log('Share success')
})
.catch((error) => {
console.error('Failed to share', error)
if (error.name !== 'AbortError') {
this.$toast.error('Failed to share: ' + error.message)
}
})
} else {
this.$toast.error('Cannot share natively on this device')
}
})
},
refresh() {
this.init()
},
async init() {
this.$emit('update:processing', true)
this.yearStats = await this.$axios.$get(`/api/stats/year/${this.year}`).catch((err) => {
console.error('Failed to load stats for year', err)
this.$toast.error(this.$strings.ToastFailedToLoadData)
return null
})
await this.initCanvas()
this.$emit('update:processing', false)
}
},
mounted() {
this.init()
}
}
</script>

View File

@@ -0,0 +1,194 @@
<template>
<div>
<div v-if="processing" class="max-w-[600px] h-32 sm:h-[200px] flex items-center justify-center">
<widgets-loading-spinner />
</div>
<img v-else-if="dataUrl" :src="dataUrl" />
</div>
</template>
<script>
export default {
props: {
processing: Boolean,
year: Number
},
data() {
return {
canvas: null,
dataUrl: null,
yearStats: null
}
},
methods: {
async initCanvas() {
if (!this.yearStats) return
const canvas = document.createElement('canvas')
canvas.width = 600
canvas.height = 200
const ctx = canvas.getContext('2d')
const createRoundedRect = (x, y, w, h) => {
const grd1 = ctx.createLinearGradient(x, y, x + w, y + h)
grd1.addColorStop(0, '#44444455')
grd1.addColorStop(1, '#ffffff11')
ctx.fillStyle = grd1
ctx.strokeStyle = '#C0C0C088'
ctx.beginPath()
ctx.roundRect(x, y, w, h, [20])
ctx.fill()
ctx.stroke()
}
const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y, maxWidth = 0) => {
ctx.fillStyle = color
ctx.font = `${fontWeight} ${fontSize} Source Sans Pro`
ctx.letterSpacing = letterSpacing
// If maxWidth is specified then continue to remove chars until under maxWidth and add ellipsis
if (maxWidth) {
let txtWidth = ctx.measureText(text).width
while (txtWidth > maxWidth) {
console.warn(`Text "${text}" is greater than max width ${maxWidth} (width:${txtWidth})`)
if (text.endsWith('...')) text = text.slice(0, -4) // Repeated checks remove 1 char at a time
else text = text.slice(0, -3) // First check remove last 3 chars
text += '...'
txtWidth = ctx.measureText(text).width
console.log(`Checking text "${text}" (width:${txtWidth})`)
}
}
ctx.fillText(text, x, y)
}
const addIcon = (icon, color, fontSize, x, y) => {
ctx.fillStyle = color
ctx.font = `${fontSize} Material Icons Outlined`
ctx.fillText(icon, x, y)
}
// Bg color
ctx.fillStyle = '#232323'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Cover image tiles
const bookCovers = this.yearStats.finishedBooksWithCovers
bookCovers.push(...this.yearStats.booksWithCovers)
if (bookCovers.length) {
let index = 0
ctx.globalAlpha = 0.25
ctx.save()
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate((-Math.PI / 180) * 25)
ctx.translate(-canvas.width / 2, -canvas.height / 2)
ctx.translate(-10, -90)
for (let x = 0; x < 4; x++) {
for (let y = 0; y < 3; y++) {
const coverIndex = index % bookCovers.length
let libraryItemId = bookCovers[coverIndex]
index++
await new Promise((resolve) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.addEventListener('load', () => {
let sw = img.width
if (img.width > img.height) {
sw = img.height
}
let sx = -(sw - img.width) / 2
let sy = -(sw - img.height) / 2
ctx.drawImage(img, sx, sy, sw, sw, 155 * x, 155 * y, 155, 155)
resolve()
})
img.addEventListener('error', () => {
resolve()
})
img.src = this.$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId)
})
}
}
ctx.restore()
}
ctx.globalAlpha = 1
ctx.textBaseline = 'middle'
// Create gradient
const grd1 = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
grd1.addColorStop(0, '#000000aa')
grd1.addColorStop(1, '#cd9d49aa')
ctx.fillStyle = grd1
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Top Abs icon
let tanColor = '#ffdb70'
ctx.fillStyle = tanColor
ctx.font = '42px absicons'
ctx.fillText('\ue900', 15, 36)
// Top text
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
// Top left box
createRoundedRect(15, 75, 280, 110)
addText(this.yearStats.numBooksFinished, '48px', 'bold', 'white', '0px', 105, 120)
addText('books finished', '20px', 'normal', tanColor, '0px', 105, 155)
const readIconPath = new Path2D()
readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 1.5, d: 1.5, e: 55, f: 115 })
ctx.fillStyle = '#ffffff'
ctx.fill(readIconPath)
createRoundedRect(305, 75, 280, 110)
addText(this.yearStats.numBooksListened, '48px', 'bold', 'white', '0px', 400, 120)
addText('books listened to', '20px', 'normal', tanColor, '0px', 400, 155)
addIcon('local_library', 'white', '42px', 345, 130)
this.canvas = canvas
this.dataUrl = canvas.toDataURL('png')
},
share() {
this.canvas.toBlob((blob) => {
const file = new File([blob], 'yearinreviewshort.png', { type: blob.type })
const shareData = {
files: [file]
}
if (navigator.canShare(shareData)) {
navigator
.share(shareData)
.then(() => {
console.log('Share success')
})
.catch((error) => {
console.error('Failed to share', error)
if (error.name !== 'AbortError') {
this.$toast.error('Failed to share: ' + error.message)
}
})
} else {
this.$toast.error('Cannot share natively on this device')
}
})
},
refresh() {
this.init()
},
async init() {
this.$emit('update:processing', true)
this.yearStats = await this.$axios.$get(`/api/me/stats/year/${this.year}`).catch((err) => {
console.error('Failed to load stats for year', err)
this.$toast.error(this.$strings.ToastFailedToLoadData)
return null
})
await this.initCanvas()
this.$emit('update:processing', false)
}
},
mounted() {
this.init()
}
}
</script>

View File

@@ -94,11 +94,11 @@ export default {
this.$axios
.$delete(`/api/items/${this.libraryItemId}/file/${this.track.audioFile.ino}`)
.then(() => {
this.$toast.success('File deleted')
this.$toast.success(this.$strings.ToastDeleteFileSuccess)
})
.catch((error) => {
console.error('Failed to delete file', error)
this.$toast.error('Failed to delete file')
this.$toast.error(this.$strings.ToastDeleteFileFailed)
})
}
},
@@ -112,4 +112,4 @@ export default {
},
mounted() {}
}
</script>
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div class="text-center mt-4">
<div class="text-center mt-4 relative">
<div class="flex py-4">
<ui-file-input ref="fileInput" class="mr-2" accept=".audiobookshelf" @change="backupUploaded">{{ $strings.ButtonUploadBackup }}</ui-file-input>
<div class="flex-grow" />
@@ -54,6 +54,10 @@
</div>
</div>
</prompt-dialog>
<div v-if="isApplyingBackup" class="absolute inset-0 w-full h-full flex items-center justify-center bg-black/20 rounded-md">
<ui-loading-indicator />
</div>
</div>
</template>
@@ -64,6 +68,7 @@ export default {
showConfirmApply: false,
selectedBackup: null,
isBackingUp: false,
isApplyingBackup: false,
processing: false,
backups: []
}
@@ -85,19 +90,21 @@ export default {
},
confirm() {
this.showConfirmApply = false
this.isApplyingBackup = true
this.$axios
.$get(`/api/backups/${this.selectedBackup.id}/apply`)
.then(() => {
this.isBackingUp = false
location.replace('/config/backups?backup=1')
})
.catch((error) => {
this.isBackingUp = false
console.error('Failed to apply backup', error)
const errorMsg = error.response.data || this.$strings.ToastBackupRestoreFailed
this.$toast.error(errorMsg)
})
.finally(() => {
this.isApplyingBackup = false
})
},
deleteBackupClick(backup) {
if (confirm(this.$getString('MessageConfirmDeleteBackup', [this.$formatDatetime(backup.createdAt, this.dateFormat, this.timeFormat)]))) {
@@ -164,11 +171,12 @@ export default {
this.$axios
.$get('/api/backups')
.then((data) => {
this.$emit('loaded', data.backupLocation)
this.setBackups(data.backups || [])
})
.catch((error) => {
console.error('Failed to load backups', error)
this.$toast.error('Failed to load backups')
this.$toast.error(this.$strings.ToastFailedToLoadData)
})
.finally(() => {
this.processing = false
@@ -179,7 +187,6 @@ export default {
this.loadBackups()
if (this.$route.query.backup) {
this.$toast.success('Backup applied successfully')
this.$router.replace('/config')
}
}
}

View File

@@ -4,7 +4,7 @@
<p class="pr-4">{{ $strings.HeaderChapters }}</p>
<span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ chapters.length }}</span>
<div class="flex-grow" />
<ui-btn v-if="userCanUpdate" small :to="`/audiobook/${libraryItemId}/chapters`" color="primary" class="mr-2">{{ $strings.ButtonEditChapters }}</ui-btn>
<ui-btn v-if="userCanUpdate" small :to="`/audiobook/${libraryItemId}/chapters`" color="primary" class="mr-2" @click="clickEditChapters">{{ $strings.ButtonEditChapters }}</ui-btn>
<div v-if="!keepOpen" class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="expanded ? 'transform rotate-180' : ''">
<span class="material-icons text-4xl">expand_more</span>
</div>
@@ -21,7 +21,7 @@
<td class="text-left">
<p class="px-4">{{ chapter.id }}</p>
</td>
<td>
<td dir="auto">
{{ chapter.title }}
</td>
<td class="font-mono text-center hover:underline cursor-pointer" @click.stop="goToTimestamp(chapter.start)">
@@ -107,8 +107,14 @@ export default {
}
this.$store.commit('globals/setConfirmPrompt', payload)
}
},
clickEditChapters() {
// Used for Chapters tab in modal
if (this.$route.name === 'audiobook-id-chapters' && this.$route.params?.id === this.libraryItem?.id) {
this.$emit('close')
}
}
},
mounted() {}
}
</script>
</script>

View File

@@ -0,0 +1,127 @@
<template>
<div class="min-h-40">
<table v-if="providers.length" id="providers">
<tr>
<th>{{ $strings.LabelName }}</th>
<th>URL</th>
<th>Authorization Header Value</th>
<th class="w-12"></th>
</tr>
<tr v-for="provider in providers" :key="provider.id">
<td class="text-sm">{{ provider.name }}</td>
<td class="text-sm">{{ provider.url }}</td>
<td class="text-sm">
<span v-if="provider.authHeaderValue" class="custom-provider-api-key">{{ provider.authHeaderValue }}</span>
</td>
<td class="py-0">
<div class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="removeProvider(provider)">
<button type="button" :aria-label="$strings.ButtonDelete" class="material-icons text-base">delete</button>
</div>
</td>
</tr>
</table>
<div v-else-if="!processing" class="text-center py-8">
<p class="text-lg">No custom metadata providers</p>
</div>
<div v-if="processing" class="absolute inset-0 h-full flex items-center justify-center bg-black/40 rounded-md">
<ui-loading-indicator />
</div>
</div>
</template>
<script>
export default {
props: {
providers: {
type: Array,
default: () => []
},
processing: Boolean
},
data() {
return {}
},
methods: {
removeProvider(provider) {
const payload = {
message: `Are you sure you want remove custom metadata provider "${provider.name}"?`,
callback: (confirmed) => {
if (confirmed) {
this.$emit('update:processing', true)
this.$axios
.$delete(`/api/custom-metadata-providers/${provider.id}`)
.then(() => {
this.$toast.success('Provider removed')
this.$emit('removed', provider.id)
})
.catch((error) => {
console.error('Failed to remove provider', error)
this.$toast.error('Failed to remove provider')
})
.finally(() => {
this.$emit('update:processing', false)
})
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
}
}
}
</script>
<style>
#providers {
table-layout: fixed;
border-collapse: collapse;
border: 1px solid #474747;
width: 100%;
}
#providers td,
#providers th {
/* border: 1px solid #2e2e2e; */
padding: 8px 8px;
text-align: left;
}
#providers td.py-0 {
padding: 0px 8px;
}
#providers tr:nth-child(even) {
background-color: #373838;
}
#providers tr:nth-child(odd) {
background-color: #2f2f2f;
}
#providers tr:hover {
background-color: #444;
}
#providers th {
font-size: 0.8rem;
font-weight: 600;
padding-top: 5px;
padding-bottom: 5px;
background-color: #272727;
}
.custom-provider-api-key {
padding: 1px;
background-color: #272727;
border-radius: 4px;
color: transparent;
transition: color, background-color 0.5s ease;
}
.custom-provider-api-key:hover {
background-color: transparent;
color: white;
}
</style>

View File

@@ -6,7 +6,7 @@
<span class="text-sm font-mono">{{ ebookFiles.length }}</span>
</div>
<div class="flex-grow" />
<ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">{{ $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' : ''">
<span class="material-icons text-4xl">expand_more</span>
</div>
@@ -75,6 +75,10 @@ export default {
}
},
methods: {
toggleFullPath() {
this.showFullPath = !this.showFullPath
localStorage.setItem('showFullPath', this.showFullPath ? 1 : 0)
},
readEbook(fileIno) {
this.$store.commit('showEReader', { libraryItem: this.libraryItem, keepProgress: false, fileId: fileIno })
},
@@ -82,6 +86,10 @@ export default {
this.showFiles = !this.showFiles
}
},
mounted() {}
mounted() {
if (this.userIsAdmin) {
this.showFullPath = !!Number(localStorage.getItem('showFullPath') || 0)
}
}
}
</script>

View File

@@ -115,11 +115,11 @@ export default {
this.$axios
.$delete(`/api/items/${this.libraryItemId}/file/${this.file.ino}`)
.then(() => {
this.$toast.success('File deleted')
this.$toast.success(this.$strings.ToastDeleteFileSuccess)
})
.catch((error) => {
console.error('Failed to delete file', error)
this.$toast.error('Failed to delete file')
this.$toast.error(this.$strings.ToastDeleteFileFailed)
})
.finally(() => {
this.processing = false
@@ -136,4 +136,4 @@ export default {
},
mounted() {}
}
</script>
</script>

View File

@@ -6,13 +6,13 @@
<span class="text-sm font-mono">{{ files.length }}</span>
</div>
<div class="flex-grow" />
<ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">{{ $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' : ''">
<span class="material-icons text-4xl">expand_more</span>
</div>
</div>
<transition name="slide">
<div class="w-full" v-show="showFiles">
<div class="w-full" v-if="showFiles">
<table class="text-sm tracksTable">
<tr>
<th class="text-left px-4">{{ $strings.LabelPath }}</th>
@@ -70,7 +70,7 @@ export default {
},
audioFiles() {
if (this.libraryItem.mediaType === 'podcast') {
return this.libraryItem.media?.episodes.map((ep) => ep.audioFile) || []
return this.libraryItem.media?.episodes.map((ep) => ep.audioFile).filter((af) => af) || []
}
return this.libraryItem.media?.audioFiles || []
},
@@ -84,6 +84,10 @@ export default {
}
},
methods: {
toggleFullPath() {
this.showFullPath = !this.showFullPath
localStorage.setItem('showFullPath', this.showFullPath ? 1 : 0)
},
clickBar() {
this.showFiles = !this.showFiles
},
@@ -93,6 +97,9 @@ export default {
}
},
mounted() {
if (this.userIsAdmin) {
this.showFullPath = !!Number(localStorage.getItem('showFullPath') || 0)
}
this.showFiles = this.expanded
}
}

View File

@@ -89,11 +89,11 @@ export default {
this.$axios
.$delete(`/api/items/${this.libraryItemId}/file/${this.file.ino}`)
.then(() => {
this.$toast.success('File deleted')
this.$toast.success(this.$strings.ToastDeleteFileSuccess)
})
.catch((error) => {
console.error('Failed to delete file', error)
this.$toast.error('Failed to delete file')
this.$toast.error(this.$strings.ToastDeleteFileFailed)
})
}
},
@@ -107,4 +107,4 @@ export default {
},
mounted() {}
}
</script>
</script>

View File

@@ -6,7 +6,7 @@
<span class="text-sm font-mono">{{ tracks.length }}</span>
</div>
<div class="flex-grow" />
<ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="showFullPath = !showFullPath">{{ $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>
<nuxt-link v-if="userCanUpdate && !isFile" :to="`/audiobook/${libraryItemId}/edit`" class="mr-2 md:mr-4" @mousedown.prevent>
<ui-btn small color="primary">{{ $strings.ButtonManageTracks }}</ui-btn>
</nuxt-link>
@@ -74,6 +74,10 @@ export default {
}
},
methods: {
toggleFullPath() {
this.showFullPath = !this.showFullPath
localStorage.setItem('showFullPath', this.showFullPath ? 1 : 0)
},
clickBar() {
this.showTracks = !this.showTracks
},
@@ -82,6 +86,10 @@ export default {
this.showAudioFileDataModal = true
}
},
mounted() {}
mounted() {
if (this.userIsAdmin) {
this.showFullPath = !!Number(localStorage.getItem('showFullPath') || 0)
}
}
}
</script>

View File

@@ -10,7 +10,7 @@
<th class="w-32 hidden sm:table-cell">{{ $strings.LabelCreatedAt }}</th>
<th class="w-32"></th>
</tr>
<tr v-for="user in users" :key="user.id" class="cursor-pointer" :class="user.isActive ? '' : 'bg-error bg-opacity-20'" @click="$router.push(`/config/users/${user.id}`)">
<tr v-for="user in users" :key="user.id" class="cursor-pointer" :class="user.isActive ? '' : '!bg-error/10'" @click="$router.push(`/config/users/${user.id}`)">
<td>
<div class="flex items-center">
<widgets-online-indicator :value="!!usersOnline[user.id]" />
@@ -52,8 +52,6 @@
</tr>
</table>
</div>
<modals-account-modal ref="accountModal" v-model="showAccountModal" :account="selectedAccount" />
</div>
</template>
@@ -62,8 +60,6 @@ export default {
data() {
return {
users: [],
selectedAccount: null,
showAccountModal: false,
isDeletingUser: false
}
},
@@ -114,13 +110,8 @@ export default {
})
}
},
clickAddUser() {
this.selectedAccount = null
this.showAccountModal = true
},
editUser(user) {
this.selectedAccount = user
this.showAccountModal = true
this.$emit('edit', user)
},
loadUsers() {
this.$axios
@@ -129,7 +120,6 @@ export default {
this.users = res.users.sort((a, b) => {
return a.createdAt - b.createdAt
})
console.log('Loaded users', this.users)
})
.catch((error) => {
console.error('Failed', error)

View File

@@ -26,11 +26,11 @@
</div>
<div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300">
<template v-for="(author, index) in bookAuthors">
<nuxt-link :key="author.id" :to="`/author/${author.id}?library=${book.libraryId}`" class="truncate hover:underline">{{ author.name }}</nuxt-link
<nuxt-link :key="author.id" :to="`/author/${author.id}`" class="truncate hover:underline">{{ author.name }}</nuxt-link
><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">,&nbsp;</span>
</template>
</div>
<p class="text-xs md:text-sm text-gray-400">{{ bookDuration }}</p>
<p v-if="media.duration" class="text-xs md:text-sm text-gray-400">{{ bookDuration }}</p>
</div>
</div>
</div>

View File

@@ -11,10 +11,6 @@
<ui-btn @click="clickAddLibrary">{{ $strings.ButtonAddYourFirstLibrary }}</ui-btn>
</div>
<p v-if="libraries.length" class="text-xs mt-4 text-gray-200">
*<strong>{{ $strings.ButtonForceReScan }}</strong> {{ $strings.MessageForceReScanDescription }}
</p>
<p v-if="libraries.length && libraries.some((li) => li.mediaType === 'book')" class="text-xs mt-4 text-gray-200">
**<strong>{{ $strings.ButtonMatchBooks }}</strong> {{ $strings.MessageMatchBooksDescription }}
</p>
@@ -46,13 +42,10 @@ export default {
return this.$store.getters['libraries/getCurrentLibrary']
},
currentLibraryId() {
return this.currentLibrary ? this.currentLibrary.id : null
return this.currentLibrary?.id || null
},
libraries() {
return this.$store.getters['libraries/getSortedLibraries']()
},
libraryScans() {
return this.$store.state.scanners.libraryScans
}
},
methods: {

View File

@@ -1,7 +1,7 @@
<template>
<div class="w-full pl-2 pr-4 md:px-4 h-12 border border-white border-opacity-10 flex items-center relative -mt-px" :class="selected ? 'bg-primary bg-opacity-50' : 'hover:bg-primary hover:bg-opacity-25'" @mouseover="mouseover = true" @mouseleave="mouseover = false">
<div v-show="selected" class="absolute top-0 left-0 h-full w-0.5 bg-warning z-10" />
<ui-library-icon v-if="!libraryScan" :icon="library.icon" :size="6" font-size="lg md:text-xl" class="text-white" :class="isHovering ? 'text-opacity-90' : 'text-opacity-50'" />
<ui-library-icon v-if="!isScanning" :icon="library.icon" :size="6" font-size="lg md:text-xl" class="text-white" :class="isHovering ? 'text-opacity-90' : 'text-opacity-50'" />
<svg v-else viewBox="0 0 24 24" class="h-6 w-6 text-white text-opacity-50 animate-spin">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
@@ -9,11 +9,14 @@
<div class="flex-grow" />
<!-- Scan button only shown on desktop -->
<ui-btn v-if="!isScanning && !isDeleting" color="bg" class="hidden md:block mx-2 text-xs" :padding-y="1" :padding-x="3" @click.stop="scanBtnClick">{{ this.$strings.ButtonScan }}</ui-btn>
<!-- Desktop context menu icon -->
<ui-context-menu-dropdown v-if="!libraryScan && !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 -->
<span v-if="!libraryScan && !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-icons 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">
<svg viewBox="0 0 24 24" class="w-6 h-6">
@@ -48,8 +51,8 @@ export default {
isHovering() {
return this.mouseover && !this.dragging
},
libraryScan() {
return this.$store.getters['scanners/getLibraryScan'](this.library.id)
isScanning() {
return !!this.$store.getters['tasks/getRunningLibraryScanTask'](this.library.id)
},
mediaType() {
return this.library.mediaType
@@ -71,11 +74,6 @@ export default {
text: this.$strings.ButtonScan,
action: 'scan',
value: 'scan'
},
{
text: this.$strings.ButtonForceReScan,
action: 'force-scan',
value: 'force-scan'
}
]
if (this.isBookLibrary) {
@@ -94,14 +92,17 @@ export default {
}
},
methods: {
scanBtnClick() {
this.scan()
},
contextMenuAction({ action }) {
this.showMobileMenu = false
if (action === 'edit') {
this.editClick()
} else if (action === 'scan') {
this.scan()
} else if (action === 'force-scan') {
this.forceScan()
} else if (action === 'force-rescan') {
this.scan(true)
} else if (action === 'match-books') {
this.matchAll()
} else if (action === 'delete') {
@@ -126,37 +127,17 @@ export default {
editClick() {
this.$emit('edit', this.library)
},
scan() {
scan(force = false) {
this.$store
.dispatch('libraries/requestLibraryScan', { libraryId: this.library.id })
.dispatch('libraries/requestLibraryScan', { libraryId: this.library.id, force })
.then(() => {
this.$toast.success(this.$strings.ToastLibraryScanStarted)
// this.$toast.success(this.$strings.ToastLibraryScanStarted)
})
.catch((error) => {
console.error('Failed to start scan', error)
this.$toast.error(this.$strings.ToastLibraryScanFailedToStart)
})
},
forceScan() {
const payload = {
message: this.$strings.MessageConfirmForceReScan,
callback: (confirmed) => {
if (confirmed) {
this.$store
.dispatch('libraries/requestLibraryScan', { libraryId: this.library.id, force: 1 })
.then(() => {
this.$toast.success(this.$strings.ToastLibraryScanStarted)
})
.catch((error) => {
console.error('Failed to start scan', error)
this.$toast.error(this.$strings.ToastLibraryScanFailedToStart)
})
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
deleteClick() {
const payload = {
message: this.$getString('MessageConfirmDeleteLibrary', [this.library.name]),

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