Compare commits

...

463 Commits

Author SHA1 Message Date
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
FlyinPancake
6ef4944d89 Merge branch 'advplyr:master' into dewyer/add-custom-metadata-provider 2024-01-13 01:08:23 +01:00
FlyinPancake
3b531144cf implemented suggestions, extended CMPs with series 2024-01-12 21:45:03 +01:00
Torstein Eide
6ca684603c Fix typos 2024-01-12 14:35:30 +01:00
Torstein Eide
cf85d66b2f Add example for HAproxy 2024-01-12 14:26:32 +01:00
mozhu
81020ff34d 播客搜索地区配置增加默认参数 2024-01-05 15:50:20 +08:00
mozhu
fea78898a5 移动播客搜索地区配置到媒体库配置 2024-01-05 14:45:35 +08:00
mozhu
1be34564f2 数据绑定错误修改 2024-01-04 15:00:40 +08:00
mozhu
56eff7a236 增加播客搜索地区配置 2024-01-04 11:52:45 +08:00
Barnabas Ratki
12c6a1baa0 Fix log messages 2024-01-03 20:42:35 +01:00
Barnabas Ratki
5ea423072b Small fixes 2024-01-03 20:40:36 +01:00
Barnabas Ratki
08a41e37b4 Add specification 2024-01-03 20:27:42 +01:00
Barnabas Ratki
8027c4a06f Added support for custom metadata providers
WiP but already open to feedback
2024-01-03 20:25:34 +01:00
Jorge
679bdf36b1 Add CodeQL workflow 2023-07-03 09:15:04 +02:00
273 changed files with 24696 additions and 27921 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/HQgCbd6E75) for questions or if you are not sure about a bug."
value: 'Report issues with the mobile app [here](https://github.com/advplyr/audiobookshelf-app/issues/new/choose).'
- type: markdown
attributes:
value: "## Be as descriptive as you can. Include screenshots, error logs, browser, file types, everything you can think of that might be relevant."
value: 'Join the [discord server](https://discord.gg/HQgCbd6E75) for questions or if you are not sure about a bug.'
- type: textarea
id: what-happened
attributes:
label: Describe the issue
description: What happened & what did you expect to happen
label: What happened?
placeholder: Tell us what you see!
validations:
required: true
- type: textarea
id: what-was-expected
attributes:
label: What did you expect to happen?
placeholder: Tell us what you expected to see! Be as descriptive as you can and include screenshots if applicable.
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to reproduce the issue
value: "1. "
value: '1. '
validations:
required: true
- type: markdown
attributes:
value: '## Install Environment'
- type: input
id: version
attributes:
label: Audiobookshelf version
description: Do not put 'Latest version', please put the actual version here
placeholder: "e.g. v1.6.60"
placeholder: 'e.g. v1.6.60'
validations:
required: true
- type: dropdown
@@ -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,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

1
.gitignore vendored
View File

@@ -19,3 +19,4 @@
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

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

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,7 +7,7 @@ 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

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;

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

@@ -4,10 +4,10 @@
<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: {
@@ -97,6 +98,9 @@ export default {
},
streamLibraryItem() {
return this.$store.state.streamLibraryItem
},
isScanningLibrary() {
return !!this.$store.getters['tasks/getRunningLibraryScanTask'](this.currentLibraryId)
}
},
methods: {
@@ -273,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) {

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

@@ -98,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>
@@ -183,6 +186,30 @@ export default {
}
]
},
authorSortItems() {
return [
{
text: this.$strings.LabelAuthorFirstLast,
value: 'name'
},
{
text: this.$strings.LabelAuthorLastFirst,
value: 'lastFirst'
},
{
text: this.$strings.LabelNumberOfBooks,
value: 'numBooks'
},
{
text: this.$strings.LabelAddedAt,
value: 'addedAt'
},
{
text: this.$strings.LabelUpdatedAt,
value: 'updatedAt'
}
]
},
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
@@ -455,6 +482,9 @@ export default {
updateCollapseBookSeries() {
this.saveSettings()
},
updateAuthorSort() {
this.saveSettings()
},
saveSettings() {
this.$store.dispatch('user/updateUserSettings', this.settings)
},

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">
@@ -62,7 +62,8 @@ export default {
currScrollTop: 0,
resizeTimeout: null,
mountWindowWidth: 0,
lastItemIndexSelected: -1
lastItemIndexSelected: -1,
tempIsScanning: false
}
},
watch: {
@@ -208,6 +209,9 @@ export default {
},
streamLibraryItem() {
return this.$store.state.streamLibraryItem
},
isScanningLibrary() {
return !!this.$store.getters['tasks/getRunningLibraryScanTask'](this.currentLibraryId)
}
},
methods: {
@@ -727,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
})
}
},
@@ -775,4 +780,4 @@ export default {
background: var(--bookshelf-divider-bg);
box-shadow: 2px 14px 8px #111111aa;
}
</style>
</style>

View File

@@ -1,14 +1,17 @@
<template>
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 md:h-40 z-50 bg-primary px-2 md:px-4 pb-1 md:pb-4 pt-2">
<div v-if="streamLibraryItem" id="mediaPlayerContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 lg:h-40 z-50 bg-primary px-2 lg:px-4 pb-1 lg:pb-4 pt-2">
<div id="videoDock" />
<div class="absolute left-2 top-2 md:left-4 cursor-pointer">
<div class="absolute left-2 top-2 lg:left-4 cursor-pointer">
<covers-book-cover expand-on-click :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
</div>
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
<div class="flex items-start mb-6 lg:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
<div class="min-w-0">
<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">
@@ -18,7 +21,6 @@
<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 || {}
@@ -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,6 +1,7 @@
<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>
<slot name="header-items"></slot>

View File

@@ -1,35 +1,35 @@
<template>
<nuxt-link :to="`/author/${author.id}`">
<div @mouseover="mouseover" @mouseleave="mouseleave">
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
<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 }} {{ $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,9 +13,9 @@
<div class="flex-grow" />
<p class="text-sm md:text-base">{{ book.publishedYear }}</p>
</div>
<p v-if="book.author" class="text-gray-300 text-xs md:text-sm">by {{ book.author }}</p>
<p v-if="book.narrator" class="text-gray-400 text-xs">Narrated by {{ book.narrator }}</p>
<p v-if="book.duration" class="text-gray-400 text-xs">Runtime: {{ $elapsedPrettyExtended(bookDuration, false) }} {{ bookDurationComparison }}</p>
<p v-if="book.author" class="text-gray-300 text-xs md:text-sm">{{ $getString('LabelByAuthor', [book.author]) }}</p>
<p v-if="book.narrator" class="text-gray-400 text-xs">{{ $strings.LabelNarrators }}: {{ book.narrator }}</p>
<p v-if="book.duration" class="text-gray-400 text-xs">{{ $strings.LabelDuration }}: {{ $elapsedPrettyExtended(bookDuration, false) }} {{ bookDurationComparison }}</p>
<div v-if="book.series?.length" class="flex py-1 -mx-1">
<div v-for="(series, index) in book.series" :key="index" class="bg-white bg-opacity-10 rounded-full px-1 py-0.5 mx-1">
<p class="leading-3 text-xs text-gray-400">
@@ -29,9 +29,9 @@
</div>
<div v-else class="px-4 flex-grow">
<h1>
<div class="flex items-center">{{ book.title }}<widgets-explicit-indicator :explicit="book.explicit" /></div>
<div class="flex items-center">{{ book.title }}<widgets-explicit-indicator v-if="book.explicit" /></div>
</h1>
<p class="text-base text-gray-300 whitespace-nowrap truncate">by {{ book.author }}</p>
<p class="text-base text-gray-300 whitespace-nowrap truncate">{{ $getString('LabelByAuthor', [book.author]) }}</p>
<p v-if="book.genres" class="text-xs text-gray-400 leading-5">{{ book.genres.join(', ') }}</p>
<p class="text-xs text-gray-400 leading-5">{{ book.trackCount }} Episodes</p>
</div>
@@ -75,11 +75,11 @@ export default {
let differenceInMinutes = currentBookDurationMinutes - this.book.duration
if (differenceInMinutes < 0) {
differenceInMinutes = Math.abs(differenceInMinutes)
return `(${this.$elapsedPrettyExtended(differenceInMinutes * 60, false, false)} shorter)`
return this.$getString('LabelDurationComparisonLonger', [this.$elapsedPrettyExtended(differenceInMinutes * 60, false, false)])
} else if (differenceInMinutes > 0) {
return `(${this.$elapsedPrettyExtended(differenceInMinutes * 60, false, false)} longer)`
return this.$getString('LabelDurationComparisonShorter', [this.$elapsedPrettyExtended(differenceInMinutes * 60, false, false)])
}
return '(exact match)'
return this.$strings.LabelDurationComparisonExactMatch
}
},
methods: {

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

@@ -21,15 +21,16 @@
<div v-if="!isPodcast" class="flex items-end">
<ui-text-input-with-label v-model.trim="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
<ui-tooltip :text="$strings.LabelUploaderItemFetchMetadataHelp">
<div
class="ml-2 mb-1 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-primary cursor-pointer"
@click="fetchMetadata">
<div class="ml-2 mb-1 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-primary cursor-pointer" @click="fetchMetadata">
<span class="text-base text-white text-opacity-80 font-mono material-icons">sync</span>
</div>
</ui-tooltip>
</div>
<div v-else class="w-full">
<p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p>
<p class="px-1 text-sm font-semibold">
{{ $strings.LabelDirectory }}
<em class="font-normal text-xs pl-2">(auto)</em>
</p>
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs" />
</div>
</div>
@@ -40,7 +41,10 @@
</div>
<div class="w-1/2 px-2">
<div class="w-full">
<label class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></label>
<label class="px-1 text-sm font-semibold">
{{ $strings.LabelDirectory }}
<em class="font-normal text-xs pl-2">(auto)</em>
</label>
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs h-10" />
</div>
</div>
@@ -51,10 +55,10 @@
<tables-uploaded-files-table v-if="item.ignoredFiles.length" :title="$strings.HeaderIgnoredFiles" :files="item.ignoredFiles" />
</template>
<widgets-alert v-if="uploadSuccess" type="success">
<p class="text-base">{{ $strings.MessageUploaderItemSuccess }}</p>
<p class="text-base">"{{ itemData.title }}" {{ $strings.MessageUploaderItemSuccess }}</p>
</widgets-alert>
<widgets-alert v-if="uploadFailed" type="error">
<p class="text-base">{{ $strings.MessageUploaderItemFailed }}</p>
<p class="text-base">"{{ itemData.title }}" {{ $strings.MessageUploaderItemFailed }}</p>
</widgets-alert>
<div v-if="isNonInteractable" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20">
@@ -70,7 +74,7 @@ export default {
props: {
item: {
type: Object,
default: () => { }
default: () => {}
},
mediaType: String,
processing: Boolean,
@@ -99,7 +103,7 @@ export default {
if (this.isPodcast) return this.itemData.title
const outputPathParts = [this.itemData.author, this.itemData.series, this.itemData.title]
const cleanedOutputPathParts = outputPathParts.filter(Boolean).map(part => this.$sanitizeFilename(part))
const cleanedOutputPathParts = outputPathParts.filter(Boolean).map((part) => this.$sanitizeFilename(part))
return Path.join(...cleanedOutputPathParts)
},

View File

@@ -1,128 +1,124 @@
<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' }">
<ui-tooltip :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
<p ref="displayTitle" class="truncate">{{ displayTitle }}</p>
<widgets-explicit-indicator :explicit="isExplicit" />
<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>
<!-- Radio button -->
<div v-if="!isAuthorBookshelfView" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="selectBtnClick">
<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' }">
<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="!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' }">
<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 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' }">
<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>
@@ -343,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() {
@@ -358,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']
@@ -388,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 = []

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

View File

@@ -89,6 +89,14 @@
</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-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
@@ -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>

View File

@@ -235,6 +235,11 @@ export default {
value: 'tags',
sublist: true
},
{
text: this.$strings.LabelLanguage,
value: 'languages',
sublist: true
},
{
text: this.$strings.ButtonIssues,
value: 'issues',
@@ -368,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

@@ -101,9 +101,14 @@ export default {
},
fullCoverUrl() {
if (!this.libraryItem) return null
var store = this.$store || this.$nuxt.$store
const store = this.$store || this.$nuxt.$store
return store.getters['globals/getLibraryItemCoverSrc'](this.libraryItem, this.placeholderUrl)
},
rawCoverUrl() {
if (!this.libraryItem) return null
const store = this.$store || this.$nuxt.$store
return store.getters['globals/getLibraryItemCoverSrc'](this.libraryItem, this.placeholderUrl, true)
},
cover() {
return this.media.coverPath || this.placeholderUrl
},
@@ -126,9 +131,6 @@ export default {
authorBottom() {
return 0.75 * this.sizeMultiplier
},
userToken() {
return this.$store.getters['user/getToken']
},
resolution() {
return `${this.naturalWidth}x${this.naturalHeight}px`
}
@@ -136,7 +138,7 @@ export default {
methods: {
clickCover() {
if (this.expandOnClick && this.libraryItem) {
this.$store.commit('globals/setRawCoverPreviewModal', this.libraryItem.id)
this.$store.commit('globals/setRawCoverPreviewModal', this.rawCoverUrl)
}
},
setCoverBg() {

View File

@@ -65,7 +65,7 @@ export default {
return 0.8 * this.sizeMultiplier
},
resolution() {
return `${this.naturalWidth}x${this.naturalHeight}px`
return `${this.naturalWidth}×${this.naturalHeight}px`
},
placeholderUrl() {
const config = this.$config || this.$nuxt.$config

View File

@@ -10,21 +10,21 @@
<div class="w-full p-8">
<div class="flex py-2">
<div class="w-1/2 px-2">
<ui-text-input-with-label v-model="newUser.username" :label="$strings.LabelUsername" />
<ui-text-input-with-label v-model.trim="newUser.username" :label="$strings.LabelUsername" />
</div>
<div class="w-1/2 px-2">
<ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? $strings.LabelPassword : $strings.LabelChangePassword" type="password" />
<ui-text-input-with-label v-else v-model="newUser.email" :label="$strings.LabelEmail" />
<ui-text-input-with-label v-else v-model.trim="newUser.email" :label="$strings.LabelEmail" />
</div>
</div>
<div v-show="!isEditingRoot" class="flex py-2">
<div class="w-1/2 px-2">
<ui-text-input-with-label v-model="newUser.email" :label="$strings.LabelEmail" />
<ui-text-input-with-label v-model.trim="newUser.email" :label="$strings.LabelEmail" />
</div>
<div class="px-2 w-52">
<ui-dropdown v-model="newUser.type" :label="$strings.LabelAccountType" :disabled="isEditingRoot" :items="accountTypes" small @input="userTypeUpdated" />
</div>
<!-- <div class="flex-grow" /> -->
<div class="flex items-center pt-4 px-2">
<p class="px-3 font-semibold" id="user-enabled-toggle" :class="isEditingRoot ? 'text-gray-300' : ''">{{ $strings.LabelEnable }}</p>
<ui-toggle-switch labeledBy="user-enabled-toggle" v-model="newUser.isActive" :disabled="isEditingRoot" />
@@ -111,7 +111,8 @@
</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>
@@ -136,7 +137,8 @@ export default {
newUser: {},
isNew: true,
tags: [],
loadingTags: false
loadingTags: false,
unlinkingFromOpenID: false
}
},
watch: {
@@ -180,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
@@ -198,6 +200,9 @@ export default {
},
tagsSelectionText() {
return this.newUser.permissions.selectedTagsNotAccessible ? this.$strings.LabelTagsNotAccessibleToUser : this.$strings.LabelTagsAccessibleToUser
},
hasOpenIDLink() {
return !!this.account?.hasOpenIDLink
}
},
methods: {
@@ -205,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) {

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

@@ -20,14 +20,11 @@ export default {
this.$store.commit('globals/setShowRawCoverPreviewModal', val)
}
},
selectedLibraryItemId() {
return this.$store.state.globals.selectedLibraryItemId
},
rawCoverUrl() {
return this.$store.getters['globals/getLibraryItemCoverSrcById'](this.selectedLibraryItemId, null, true)
return this.$store.state.globals.selectedRawCoverUrl
}
},
methods: {},
mounted() {}
}
</script>
</script>

View File

@@ -242,4 +242,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

@@ -46,7 +46,12 @@ export default {
ereaderDevice: {
type: Object,
default: () => null
}
},
users: {
type: Array,
default: () => []
},
loadUsers: Function
},
data() {
return {
@@ -56,8 +61,7 @@ export default {
email: '',
availabilityOption: 'adminAndUp',
users: []
},
users: []
}
}
},
watch: {
@@ -108,25 +112,13 @@ export default {
methods: {
availabilityOptionChanged(option) {
if (option === 'specificUsers' && !this.users.length) {
this.loadUsers()
this.callLoadUsers()
}
},
async loadUsers() {
async callLoadUsers() {
this.processing = true
this.users = await this.$axios
.$get('/api/users')
.then((res) => {
return res.users.sort((a, b) => {
return a.createdAt - b.createdAt
})
})
.catch((error) => {
console.error('Failed', error)
return []
})
.finally(() => {
this.processing = false
})
await this.loadUsers()
this.processing = false
},
submitForm() {
this.$refs.ereaderNameInput.blur()
@@ -226,10 +218,6 @@ export default {
this.newDevice.email = this.ereaderDevice.email
this.newDevice.availabilityOption = this.ereaderDevice.availabilityOption || 'adminOrUp'
this.newDevice.users = this.ereaderDevice.users || []
if (this.newDevice.availabilityOption === 'specificUsers' && !this.users.length) {
this.loadUsers()
}
} else {
this.newDevice.name = ''
this.newDevice.email = ''

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,7 +1,7 @@
<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 mb-4">
<div class="relative">
<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 -->
@@ -14,7 +14,7 @@
</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 md:min-w-32">
<ui-file-input ref="fileInput" @change="fileUploadSelected">
@@ -49,20 +49,20 @@
</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 === coverPath ? 'border-yellow-300' : ''" @click="updateCover(cover)">

View File

@@ -29,7 +29,7 @@
<td class="text-center w-20 min-w-20">
<p>{{ episode.episode }}</p>
</td>
<td>
<td dir="auto">
{{ episode.title }}
</td>
<td class="font-mono text-center">

View File

@@ -32,7 +32,7 @@
</div>
<p class="text-xl pl-3">{{ $strings.HeaderUpdateDetails }}</p>
</div>
<ui-checkbox v-model="selectAll" checkbox-bg="bg" @input="selectAllToggled" />
<ui-checkbox v-model="selectAll" :label="$strings.LabelSelectAll" checkbox-bg="bg" @input="selectAllToggled" />
<form @submit.prevent="submitMatchUpdate">
<div v-if="selectedMatchOrig.cover" class="flex flex-wrap md:flex-nowrap items-center justify-center">
<div class="flex flex-grow items-center py-2">
@@ -42,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
@@ -305,11 +308,16 @@ export default {
isPodcast() {
return this.mediaType == 'podcast'
},
narrators() {
return this.filterData.narrators || []
},
genres() {
const filterData = this.$store.state.libraries.filterData || {}
const currentGenres = filterData.genres || []
const currentGenres = this.filterData.genres || []
const selectedMatchGenres = this.selectedMatch.genres || []
return [...new Set([...currentGenres, ...selectedMatchGenres])]
},
tags() {
return this.filterData.tags || []
}
},
methods: {
@@ -328,6 +336,17 @@ 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=${encodeURIComponent(this.searchTitle)}`
var searchQuery = `provider=${this.provider}&fallbackTitleOnly=1&title=${encodeURIComponent(this.searchTitle)}`
@@ -434,7 +453,9 @@ export default {
this.searchTitle = this.libraryItem.media.metadata.title
this.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) {
@@ -466,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)
@@ -495,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) =>
@@ -506,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 {
@@ -533,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)

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

@@ -127,6 +127,7 @@ export default {
skipMatchingMediaWithIsbn: false,
autoScanCronExpression: null,
hideSingleBookSeries: false,
onlyShowLaterBooksInContinueSeries: false,
metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
}
}

View File

@@ -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>
@@ -69,7 +94,10 @@ export default {
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
@@ -99,7 +130,10 @@ export default {
skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin,
skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn,
audiobooksOnly: !!this.audiobooksOnly,
hideSingleBookSeries: !!this.hideSingleBookSeries
epubsAllowScriptedContent: !!this.epubsAllowScriptedContent,
hideSingleBookSeries: !!this.hideSingleBookSeries,
onlyShowLaterBooksInContinueSeries: !!this.onlyShowLaterBooksInContinueSeries,
podcastSearchRegion: this.podcastSearchRegion
}
}
},
@@ -112,11 +146,14 @@ export default {
this.skipMatchingMediaWithAsin = !!this.librarySettings.skipMatchingMediaWithAsin
this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn
this.audiobooksOnly = !!this.librarySettings.audiobooksOnly
this.epubsAllowScriptedContent = !!this.librarySettings.epubsAllowScriptedContent
this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries
this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries
this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us'
}
},
mounted() {
this.init()
}
}
</script>
</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

@@ -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>

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

@@ -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

@@ -334,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())
})

View File

@@ -63,6 +63,9 @@ export default {
libraryItemId() {
return this.libraryItem?.id
},
allowScriptedContent() {
return this.$store.getters['libraries/getLibraryEpubsAllowScriptedContent']
},
hasPrev() {
return !this.rendition?.location?.atStart
},
@@ -316,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',

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 v-if="isBookLibrary" 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>

View File

@@ -271,7 +271,7 @@ export default {
this.$emit('update:processing', true)
this.yearStats = await this.$axios.$get(`/api/me/stats/year/${this.year}`).catch((err) => {
console.error('Failed to load stats for year', err)
this.$toast.error('Failed to load year stats')
this.$toast.error(this.$strings.ToastFailedToLoadData)
return null
})
await this.initCanvas()
@@ -282,4 +282,4 @@ export default {
this.init()
}
}
</script>
</script>

View File

@@ -7,9 +7,10 @@
</div>
<div class="flex items-center">
<p class="hidden md:block text-xl font-semibold">{{ yearInReviewYear }} Year in Review</p>
<p class="hidden md:block text-xl font-semibold">{{ $getString('HeaderYearReview', [yearInReviewYear]) }}</p>
<div class="hidden md:block flex-grow" />
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? 'Hide Year in Review' : 'See Year in Review' }}</ui-btn>
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide :
$strings.LabelYearReviewShow }}</ui-btn>
</div>
<!-- your year in review -->
@@ -20,24 +21,27 @@
<!-- 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">Previous</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"> Share </ui-btn>
<ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview">{{
$strings.ButtonShare }}
</ui-btn>
<div class="flex-grow" />
<p class="hidden sm:block text-lg font-semibold">Your Year in Review ({{ yearInReviewVariant + 1 }})</p>
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelPersonalYearReview', [yearInReviewVariant + 1]) }}
</p>
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p>
<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">Refresh</span>
<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">Next</span>
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn>
</div>
@@ -46,7 +50,7 @@
<!-- 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"> Share </ui-btn>
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex sm:hidden items-center font-semibold mb-1" @click="shareYearInReviewShort">{{ $strings.ButtonShare }}</ui-btn>
<stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" />
</div>
@@ -56,24 +60,25 @@
<!-- 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">Previous</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"> Share </ui-btn>
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer">{{ $strings.ButtonShare }}
</ui-btn>
<div class="flex-grow" />
<p class="hidden sm:block text-lg font-semibold">Server Year in Review ({{ yearInReviewServerVariant + 1 }})</p>
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelServerYearReview', [yearInReviewServerVariant + 1]) }}</p>
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p>
<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">Refresh</span>
<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">Next</span>
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn>
</div>

View File

@@ -250,7 +250,7 @@ export default {
this.$emit('update:processing', true)
this.yearStats = await this.$axios.$get(`/api/stats/year/${this.year}`).catch((err) => {
console.error('Failed to load stats for year', err)
this.$toast.error('Failed to load year stats')
this.$toast.error(this.$strings.ToastFailedToLoadData)
return null
})
await this.initCanvas()
@@ -261,4 +261,4 @@ export default {
this.init()
}
}
</script>
</script>

View File

@@ -180,7 +180,7 @@ export default {
this.$emit('update:processing', true)
this.yearStats = await this.$axios.$get(`/api/me/stats/year/${this.year}`).catch((err) => {
console.error('Failed to load stats for year', err)
this.$toast.error('Failed to load year stats')
this.$toast.error(this.$strings.ToastFailedToLoadData)
return null
})
await this.initCanvas()
@@ -191,4 +191,4 @@ export default {
this.init()
}
}
</script>
</script>

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)]))) {
@@ -169,7 +176,7 @@ export default {
})
.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
@@ -180,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

@@ -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

@@ -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

@@ -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]" />

View File

@@ -38,7 +38,7 @@
<div v-if="userCanUpdate" class="mx-1" :class="isHovering ? '' : 'ml-6'">
<ui-icon-btn icon="edit" borderless @click="clickEdit" />
</div>
<div v-if="userCanDelete" class="mx-1">
<div class="mx-1" :class="isHovering ? '' : 'ml-6'">
<ui-icon-btn icon="close" borderless @click="removeClick" />
</div>
</div>
@@ -75,8 +75,7 @@ export default {
},
computed: {
translateDistance() {
if (!this.userCanUpdate && !this.userCanDelete) return 'translate-x-0'
else if (!this.userCanUpdate || !this.userCanDelete) return '-translate-x-12'
if (!this.userCanUpdate) return '-translate-x-12'
return '-translate-x-24'
},
libraryItem() {
@@ -233,4 +232,4 @@ export default {
},
mounted() {}
}
</script>
</script>

View File

@@ -20,7 +20,7 @@
<td class="px-4">
<div class="flex items-center">
<nuxt-link :to="`/item/${downloadQueued.libraryItemId}`" class="text-sm text-gray-200 hover:underline">{{ downloadQueued.podcastTitle }}</nuxt-link>
<widgets-explicit-indicator :explicit="downloadQueued.podcastExplicit" />
<widgets-explicit-indicator v-if="downloadQueued.podcastExplicit" />
</div>
</td>
<td>
@@ -30,7 +30,7 @@
<widgets-podcast-type-indicator :type="downloadQueued.episodeType" />
</div>
</td>
<td class="px-4">
<td dir="auto" class="px-4">
{{ downloadQueued.episodeDisplayTitle }}
</td>
<td class="text-xs">

View File

@@ -2,13 +2,13 @@
<div :id="`lazy-episode-${index}`" class="w-full h-full cursor-pointer" @mouseover="mouseover" @mouseleave="mouseleave">
<div class="flex" @click="clickedEpisode">
<div class="flex-grow">
<div class="flex items-center">
<div dir="auto" class="flex items-center">
<span class="text-sm font-semibold">{{ episodeTitle }}</span>
<widgets-podcast-type-indicator :type="episodeType" />
</div>
<div class="h-10 flex items-center mt-1.5 mb-0.5">
<p class="text-sm text-gray-200 episode-subtitle" v-html="episodeSubtitle"></p>
<div class="h-10 flex items-center mt-1.5 mb-0.5 overflow-hidden">
<p class="text-sm text-gray-200 line-clamp-2" v-html="episodeSubtitle"></p>
</div>
<div class="h-8 flex items-center">
<div class="w-full inline-flex justify-between max-w-xl">

View File

@@ -1,5 +1,5 @@
<template>
<nuxt-link v-if="to" :to="to" class="abs-btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList">
<nuxt-link v-if="to" :to="to" class="abs-btn outline-none rounded-md shadow-md relative border border-gray-600 text-center" :disabled="disabled || loading" :class="classList" @click.native="click">
<slot />
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">

View File

@@ -13,7 +13,7 @@
</button>
<transition name="menu">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="listbox">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="listbox" :style="{ maxHeight: menuMaxHeight }">
<template v-for="item in itemsToShow">
<li :key="item.value" class="text-gray-100 relative py-2 cursor-pointer hover:bg-black-400" :id="'listbox-option-' + item.value" role="option" tabindex="0" @keyup.enter="clickedOption(item.value)" @click="clickedOption(item.value)">
<div class="flex items-center">
@@ -41,7 +41,11 @@ export default {
default: () => []
},
disabled: Boolean,
small: Boolean
small: Boolean,
menuMaxHeight: {
type: String,
default: '224px'
}
},
data() {
return {

View File

@@ -1,7 +1,7 @@
<template>
<div>
<input ref="fileInput" type="file" :accept="accept" class="hidden" @change="inputChanged" />
<ui-btn @click="clickUpload" color="primary" class="hidden md:block" type="text"><slot /></ui-btn>
<ui-btn @click="clickUpload" color="primary" class="hidden md:block w-full" type="text"><slot /></ui-btn>
<ui-icon-btn @click="clickUpload" icon="upload" class="block md:hidden" />
</div>
</template>

View File

@@ -83,15 +83,21 @@ export default {
},
async updateLibrary(library) {
var currLibraryId = this.currentLibraryId
if (currLibraryId === library.id) {
return
}
this.disabled = true
await this.$store.dispatch('libraries/fetch', library.id)
if (this.$route.name.startsWith('config')) {
// No need to refresh
} else if (this.$route.name.startsWith('library')) {
var newRoute = this.$route.path.replace(currLibraryId, library.id)
} else if (this.$route.name.startsWith('library') && this.$route.name !== 'library-library-series-id') {
const newRoute = this.$route.path.replace(currLibraryId, library.id)
this.$router.push(newRoute)
} else if (this.$route.name === 'library-library-series-id' && library.mediaType === 'book') {
// For series item page redirect to root series page
this.$router.push(`/library/${library.id}/bookshelf/series`)
} else {
this.$router.push(`/library/${library.id}`)
}
@@ -107,4 +113,4 @@ export default {
.librariesDropdownMenu {
max-height: calc(100vh - 75px);
}
</style>
</style>

View File

@@ -11,13 +11,13 @@
</div>
{{ item }}
</div>
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" style="min-width: 40px; width: 40px" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
</div>
</form>
<ul ref="menu" v-show="showMenu" class="absolute z-60 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in itemsToShow">
<li :key="item" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
<li :key="item" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="itemsToShow[selectedMenuItemIndex] === item ? 'text-yellow-300' : ''" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
<div class="flex items-center">
<span class="font-normal ml-3 block truncate">{{ item }}</span>
</div>
@@ -54,7 +54,7 @@ export default {
menuDisabled: {
type: Boolean,
default: false
},
}
},
data() {
return {
@@ -62,7 +62,9 @@ export default {
currentSearch: null,
typingTimeout: null,
isFocused: false,
menu: null
menu: null,
filteredItems: null,
selectedMenuItemIndex: null
}
},
watch: {
@@ -91,24 +93,63 @@ export default {
return classes.join(' ')
},
itemsToShow() {
if (!this.currentSearch || !this.textInput) {
if (!this.currentSearch || !this.textInput || !this.filteredItems) {
return this.items
}
return this.items.filter((i) => {
var iValue = String(i).toLowerCase()
return iValue.includes(this.currentSearch.toLowerCase())
})
return this.filteredItems
}
},
methods: {
editItem(item) {
this.$emit('edit', item)
},
keydownInput() {
search() {
if (!this.textInput) {
this.filteredItems = null
return
}
this.currentSearch = this.textInput
const results = this.items.filter((i) => {
var iValue = String(i).toLowerCase()
return iValue.includes(this.currentSearch.toLowerCase())
})
this.filteredItems = results || []
},
keydownInput(event) {
let items = this.itemsToShow
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault()
if (!items.length) return
if (event.key === 'ArrowDown') {
if (this.selectedMenuItemIndex === null) {
this.selectedMenuItemIndex = 0
} else {
this.selectedMenuItemIndex = Math.min(this.selectedMenuItemIndex + 1, items.length - 1)
}
} else if (event.key === 'ArrowUp') {
if (this.selectedMenuItemIndex === null) {
this.selectedMenuItemIndex = items.length - 1
} else {
this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0)
}
}
this.recalcScroll()
return
} else if (event.key === 'Enter') {
if (this.selectedMenuItemIndex !== null) {
this.clickedOption(event, items[this.selectedMenuItemIndex])
} else {
this.submitForm()
}
return
}
this.selectedMenuItemIndex = null
clearTimeout(this.typingTimeout)
this.typingTimeout = setTimeout(() => {
this.currentSearch = this.textInput
this.search()
}, 100)
this.setInputWidth()
},
@@ -120,6 +161,24 @@ export default {
this.recalcMenuPos()
}, 50)
},
recalcScroll() {
if (!this.menu) return
var menuItems = this.menu.querySelectorAll('li')
if (!menuItems.length) return
var selectedItem = menuItems[this.selectedMenuItemIndex]
if (!selectedItem) return
var menuHeight = this.menu.offsetHeight
var itemHeight = selectedItem.offsetHeight
var itemTop = selectedItem.offsetTop
var itemBottom = itemTop + itemHeight
if (itemBottom > this.menu.scrollTop + menuHeight) {
let menuPaddingBottom = parseFloat(window.getComputedStyle(this.menu).paddingBottom)
this.menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom
} else if (itemTop < this.menu.scrollTop) {
let menuPaddingTop = parseFloat(window.getComputedStyle(this.menu).paddingTop)
this.menu.scrollTop = itemTop - menuPaddingTop
}
},
recalcMenuPos() {
if (!this.menu || !this.$refs.inputWrapper) return
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
@@ -208,7 +267,10 @@ export default {
e.stopPropagation()
e.preventDefault()
}
if (this.$refs.input) this.$refs.input.focus()
if (this.$refs.input) {
this.$refs.input.style.width = '24px'
this.$refs.input.focus()
}
var newSelected = null
if (this.selected.includes(itemValue)) {
@@ -219,6 +281,7 @@ export default {
}
this.textInput = null
this.currentSearch = null
this.selectedMenuItemIndex = null
this.$emit('input', newSelected)
this.$nextTick(() => {
this.recalcMenuPos()
@@ -239,12 +302,21 @@ export default {
this.recalcMenuPos()
})
},
resetInput() {
this.textInput = null
this.currentSearch = null
this.selectedMenuItemIndex = null
this.$nextTick(() => {
this.blur()
})
},
insertNewItem(item) {
this.selected.push(item)
this.$emit('input', this.selected)
this.$emit('newItem', item)
this.textInput = null
this.currentSearch = null
this.selectedMenuItemIndex = null
this.$nextTick(() => {
this.blur()
})
@@ -252,15 +324,19 @@ export default {
submitForm() {
if (!this.textInput) return
var cleaned = this.textInput.trim()
var matchesItem = this.items.find((i) => {
return i === cleaned
})
if (matchesItem) {
this.clickedOption(null, matchesItem)
const cleaned = this.textInput.trim()
if (!cleaned) {
this.resetInput()
} else {
this.insertNewItem(this.textInput)
const matchesItem = this.items.find((i) => i === cleaned)
if (matchesItem) {
this.clickedOption(null, matchesItem)
} else {
this.insertNewItem(cleaned)
}
}
if (this.$refs.input) this.$refs.input.style.width = '24px'
},
scroll() {
this.recalcMenuPos()
@@ -287,4 +363,4 @@ input:read-only {
color: #aaa;
background-color: #444;
}
</style>
</style>

View File

@@ -14,13 +14,13 @@
<div v-if="showEdit && !disabled" class="rounded-full cursor-pointer w-6 h-6 mx-0.5 bg-bg flex items-center justify-center">
<span class="material-icons text-white hover:text-success pt-px pr-px" style="font-size: 1.1rem" @click.stop="addItem">add</span>
</div>
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" style="min-width: 40px; width: 40px" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
</div>
</form>
<ul ref="menu" v-show="showMenu" class="absolute z-60 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in itemsToShow">
<li :key="item.id" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
<li :key="item.id" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="itemsToShow[selectedMenuItemIndex] === item ? 'text-yellow-300' : ''" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
<div class="flex items-center">
<span class="font-normal ml-3 block truncate">{{ item.name }}</span>
</div>
@@ -63,7 +63,8 @@ export default {
typingTimeout: null,
isFocused: false,
menu: null,
items: []
items: [],
selectedMenuItemIndex: null
}
},
watch: {
@@ -122,7 +123,35 @@ export default {
this.items = results || []
},
keydownInput() {
keydownInput(event) {
let items = this.itemsToShow
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault()
if (!items.length) return
if (event.key === 'ArrowDown') {
if (this.selectedMenuItemIndex === null) {
this.selectedMenuItemIndex = 0
} else {
this.selectedMenuItemIndex = Math.min(this.selectedMenuItemIndex + 1, items.length - 1)
}
} else if (event.key === 'ArrowUp') {
if (this.selectedMenuItemIndex === null) {
this.selectedMenuItemIndex = items.length - 1
} else {
this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0)
}
}
this.recalcScroll()
return
} else if (event.key === 'Enter') {
if (this.selectedMenuItemIndex !== null) {
this.clickedOption(event, items[this.selectedMenuItemIndex])
} else {
this.submitForm()
}
return
}
this.selectedMenuItemIndex = null
clearTimeout(this.typingTimeout)
this.typingTimeout = setTimeout(() => {
this.search()
@@ -137,6 +166,24 @@ export default {
this.recalcMenuPos()
}, 50)
},
recalcScroll() {
if (!this.menu) return
var menuItems = this.menu.querySelectorAll('li')
if (!menuItems.length) return
var selectedItem = menuItems[this.selectedMenuItemIndex]
if (!selectedItem) return
var menuHeight = this.menu.offsetHeight
var itemHeight = selectedItem.offsetHeight
var itemTop = selectedItem.offsetTop
var itemBottom = itemTop + itemHeight
if (itemBottom > this.menu.scrollTop + menuHeight) {
let menuPaddingBottom = parseFloat(window.getComputedStyle(this.menu).paddingBottom)
this.menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom
} else if (itemTop < this.menu.scrollTop) {
let menuPaddingTop = parseFloat(window.getComputedStyle(this.menu).paddingTop)
this.menu.scrollTop = itemTop - menuPaddingTop
}
},
recalcMenuPos() {
if (!this.menu || !this.$refs.inputWrapper) return
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
@@ -228,7 +275,10 @@ export default {
e.stopPropagation()
e.preventDefault()
}
if (this.$refs.input) this.$refs.input.focus()
if (this.$refs.input) {
this.$refs.input.style.width = '24px'
this.$refs.input.focus()
}
let newSelected = null
if (this.getIsSelected(item.id)) {
@@ -244,6 +294,7 @@ export default {
}
this.textInput = null
this.currentSearch = null
this.selectedMenuItemIndex = null
this.$emit('input', newSelected)
this.$nextTick(() => {
@@ -271,6 +322,7 @@ export default {
this.$emit('newItem', item)
this.textInput = null
this.currentSearch = null
this.selectedMenuItemIndex = null
this.$nextTick(() => {
this.blur()
})
@@ -291,6 +343,7 @@ export default {
name: this.textInput
})
}
if (this.$refs.input) this.$refs.input.style.width = '24px'
},
scroll() {
this.recalcMenuPos()

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div class="default-style">
<p v-if="label" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
{{ label }}
</p>
@@ -29,31 +29,31 @@ export default {
config() {
return {
toolbar: {
getDefaultHTML: () => ` <div class="trix-button-row">
getDefaultHTML: () => `<div class="trix-button-row">
<span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" title="#{lang.bold}" tabindex="-1">#{lang.bold}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" title="#{lang.italic}" tabindex="-1">#{lang.italic}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" title="#{lang.strike}" tabindex="-1">#{lang.strike}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="#{lang.link}" tabindex="-1">#{lang.link}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" title="${this.$strings.LabelFontBold}" tabindex="-1">${this.$strings.LabelFontBold}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" title="${this.$strings.LabelFontItalic}" tabindex="-1">${this.$strings.LabelFontItalic}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" title="${this.$strings.LabelFontStrikethrough}" tabindex="-1">${this.$strings.LabelFontStrikethrough}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="${this.$strings.LabelTextEditorLink}" tabindex="-1">${this.$strings.LabelTextEditorLink}</button>
</span>
<span class="trix-button-group trix-button-group--block-tools" data-trix-button-group="block-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" title="#{lang.bullets}" tabindex="-1">#{lang.bullets}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" title="#{lang.numbers}" tabindex="-1">#{lang.numbers}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" title="${this.$strings.LabelTextEditorBulletedList}" tabindex="-1">${this.$strings.LabelTextEditorBulletedList}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" title="${this.$strings.LabelTextEditorNumberedList}" tabindex="-1">${this.$strings.LabelTextEditorNumberedList}</button>
</span>
<span class="trix-button-group-spacer"></span>
<span class="trix-button-group trix-button-group--history-tools" data-trix-button-group="history-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" title="#{lang.undo}" tabindex="-1">#{lang.undo}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" title="#{lang.redo}" tabindex="-1">#{lang.redo}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" title="${this.$strings.LabelUndo}" tabindex="-1">${this.$strings.LabelUndo}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" title="${this.$strings.LabelRedo}" tabindex="-1">${this.$strings.LabelRedo}</button>
</span>
</div>
<div class="trix-dialogs" data-trix-dialogs>
<div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
<div class="trix-dialog__link-fields">
<input type="url" name="href" class="trix-input trix-input--dialog" placeholder="#{lang.urlPlaceholder}" aria-label="#{lang.url}" required data-trix-input>
<input type="url" name="href" class="trix-input trix-input--dialog" placeholder="" aria-label="URL" required data-trix-input>
<div class="trix-button-group">
<input type="button" class="trix-button trix-button--dialog" value="#{lang.link}" data-trix-method="setAttribute">
<input type="button" class="trix-button trix-button--dialog" value="#{lang.unlink}" data-trix-method="removeAttribute">
<input type="button" class="trix-button trix-button--dialog" value="${this.$strings.LabelTextEditorLink}" data-trix-method="setAttribute">
<input type="button" class="trix-button trix-button--dialog" value="${this.$strings.LabelTextEditorUnlink}" data-trix-method="removeAttribute">
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
<template>
<div ref="wrapper" class="relative">
<input :id="inputId" ref="input" v-model="inputValue" :type="actualType" :step="step" :min="min" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
<input :id="inputId" :name="inputName" ref="input" v-model="inputValue" :type="actualType" :step="step" :min="min" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" dir="auto" class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
<div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
<span class="material-icons text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
</div>
@@ -33,6 +33,7 @@ export default {
textCenter: Boolean,
clearable: Boolean,
inputId: String,
inputName: String,
step: [String, Number],
min: [String, Number]
},
@@ -117,4 +118,4 @@ input:read-only {
input::-webkit-calendar-picker-indicator {
filter: invert(1);
}
</style>
</style>

View File

@@ -5,7 +5,7 @@
>{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em></label
>
</slot>
<ui-text-input :placeholder="label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" :class="inputClass" @blur="inputBlurred" />
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" :class="inputClass" @blur="inputBlurred" />
</div>
</template>
@@ -14,6 +14,7 @@ export default {
props: {
value: [String, Number],
label: String,
placeholder: String,
note: String,
type: {
type: String,

View File

@@ -1,5 +1,5 @@
<template>
<textarea ref="input" v-model="inputValue" :rows="rows" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="py-2 px-3 rounded bg-primary text-gray-200 focus:border-gray-500 focus:outline-none" :class="transparent ? '' : 'border border-gray-600'" @change="change" />
<textarea ref="input" v-model="inputValue" :rows="rows" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" dir="auto" class="py-2 px-3 rounded bg-primary text-gray-200 focus:border-gray-500 focus:outline-none" :class="transparent ? '' : 'border border-gray-600'" @change="change" />
</template>
<script>

View File

@@ -1,5 +1,5 @@
<template>
<div class="relative">
<div tabindex="0" @focus="focusDigit('second0')" class="relative">
<div class="rounded text-gray-200 border w-full px-3 py-2" :class="focusedDigit ? 'bg-primary bg-opacity-50 border-gray-300' : 'bg-primary border-gray-600'" @click="clickInput" v-click-outside="clickOutsideObj">
<div class="flex items-center">
<template v-for="(digit, index) in digitDisplay">
@@ -174,7 +174,7 @@ export default {
return this.increaseFocused()
} else if (evt.key === 'ArrowDown') {
return this.decreaseFocused()
} else if (evt.key === 'Enter' || evt.key === 'Escape') {
} else if (evt.key === 'Enter' || evt.key === 'Escape' || evt.key === 'Tab') {
return this.removeFocus()
}
@@ -209,4 +209,4 @@ export default {
.digit-focused {
background-color: #555;
}
</style>
</style>

View File

@@ -1,84 +0,0 @@
<template>
<div class="w-full">
<div v-if="missingParts.length" class="bg-error border-red-800 shadow-md p-4">
<p class="text-sm mb-2">
{{ $strings.LabelMissingParts }} <span class="text-sm">({{ missingParts.length }})</span>
</p>
<p class="text-sm font-mono">{{ missingPartChunks.join(', ') }}</p>
</div>
<div v-if="invalidParts.length" class="bg-error border-red-800 shadow-md p-4">
<p class="text-sm mb-2">
{{ $strings.LabelInvalidParts }} <span class="text-sm">({{ invalidParts.length }})</span>
</p>
<div>
<p v-for="part in invalidParts" :key="part.filename" class="text-sm font-mono">{{ part.filename }}: {{ part.error }}</p>
</div>
</div>
<tables-tracks-table :title="$strings.LabelStatsAudioTracks" :tracks="tracksWithAudioFile" :is-file="isFile" :library-item-id="libraryItemId" class="mt-6" />
</div>
</template>
<script>
export default {
props: {
libraryItemId: String,
media: {
type: Object,
default: () => {}
},
isFile: Boolean
},
data() {
return {}
},
computed: {
tracksWithAudioFile() {
return this.media.tracks.map((track) => {
track.audioFile = this.media.audioFiles.find((af) => af.metadata.path === track.metadata.path)
return track
})
},
missingPartChunks() {
if (this.missingParts === 1) return this.missingParts[0]
var chunks = []
var currentIndex = this.missingParts[0]
var currentChunk = [this.missingParts[0]]
for (let i = 1; i < this.missingParts.length; i++) {
var partIndex = this.missingParts[i]
if (currentIndex === partIndex - 1) {
currentChunk.push(partIndex)
currentIndex = partIndex
} else {
// console.log('Chunk ended', currentChunk.join(', '), currentIndex, partIndex)
if (currentChunk.length === 0) {
console.error('How is current chunk 0?', currentChunk.join(', '))
}
chunks.push(currentChunk)
currentChunk = [partIndex]
currentIndex = partIndex
}
}
if (currentChunk.length) {
chunks.push(currentChunk)
}
chunks = chunks.map((chunk) => {
if (chunk.length === 1) return chunk[0]
else return `${chunk[0]}-${chunk[chunk.length - 1]}`
})
return chunks
},
missingParts() {
return this.media.missingParts || []
},
invalidParts() {
return this.media.invalidParts || []
}
},
methods: {},
mounted() {}
}
</script>

View File

@@ -10,10 +10,10 @@
<span class="material-icons text-2xl">chevron_right</span>
</button>
</div>
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex" :style="{ height: height + 'px' }">
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex space-x-4" :style="{ height: height + 'px' }">
<template v-for="(item, index) in items">
<cards-author-card :key="item.id" :ref="`slider-item-${item.id}`" :index="index" :author="item" :height="cardHeight" :width="cardWidth" class="relative mx-2" @edit="editAuthor" @hook:updated="setScrollVars" />
<cards-author-card :key="item.id" :ref="`slider-item-${item.id}`" :index="index" :author="item" :height="cardHeight" :width="cardWidth" class="relative" @edit="editAuthor" @hook:updated="setScrollVars" />
</template>
</div>
</div>

View File

@@ -10,8 +10,8 @@
<span class="material-icons text-2xl">chevron_right</span>
</button>
</div>
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex" :style="{ height: height + 'px' }">
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex space-x-4" :style="{ height: height + 'px' }">
<template v-for="(item, index) in items">
<cards-lazy-book-card
:key="item.recentEpisode.id"
@@ -23,7 +23,7 @@
:book-cover-aspect-ratio="bookCoverAspectRatio"
:bookshelf-view="bookshelfView"
:continue-listening-shelf="continueListeningShelf"
class="relative mx-2"
class="relative"
@edit="editEpisode"
@editPodcast="editPodcast"
@select="selectItem"

View File

@@ -1,5 +1,5 @@
<template>
<ui-tooltip v-if="explicit" :text="$strings.LabelExplicit" direction="top">
<ui-tooltip :text="$strings.LabelExplicit" direction="top">
<svg xmlns="http://www.w3.org/2000/svg" width="12px" height="12px" viewBox="0 0 512 512" class="ml-1">
<path
fill="white"
@@ -40,9 +40,7 @@
<script>
export default {
props: {
explicit: Boolean
},
props: {},
data() {
return {}
},

View File

@@ -10,10 +10,24 @@
<span class="material-icons text-2xl">chevron_right</span>
</button>
</div>
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex" :style="{ height: height + 'px' }">
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex space-x-4" :style="{ height: height + 'px' }">
<template v-for="(item, index) in items">
<cards-lazy-book-card :key="item.id + '-' + shelfId" :ref="`slider-item-${item.id}`" :index="index" :book-mount="item" :height="cardHeight" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" :bookshelf-view="bookshelfView" :continue-listening-shelf="continueListeningShelf" class="relative mx-2" @edit="editItem" @select="selectItem" @hook:updated="setScrollVars" />
<cards-lazy-book-card
:key="item.id + '-' + shelfId + '-' + index"
:ref="`slider-item-${item.id}`"
:index="index"
:book-mount="item"
:height="cardHeight"
:width="cardWidth"
:book-cover-aspect-ratio="bookCoverAspectRatio"
:bookshelf-view="bookshelfView"
:continue-listening-shelf="continueListeningShelf"
class="relative"
@edit="editItem"
@select="selectItem"
@hook:updated="setScrollVars"
/>
</template>
</div>
</div>

View File

@@ -1,5 +1,5 @@
<template>
<div ref="wrapper" class="absolute bg-bg rounded-md py-1 border border-black-200 shadow-lg z-50" v-click-outside="clickOutsideObj" :style="{ width: menuWidth + 'px' }" style="top: 0; left: 0">
<div ref="wrapper" class="absolute bg-bg rounded-md py-1 border border-black-200 shadow-lg z-50" v-click-outside="clickOutsideObj" :style="{ width: menuWidth + 'px' }">
<template v-for="(item, index) in items">
<template v-if="item.subitems">
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-default" :class="{ 'bg-white/5': mouseoverItemIndex == index }" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop>
@@ -94,4 +94,4 @@ export default {
},
beforeDestroy() {}
}
</script>
</script>

View File

@@ -10,10 +10,10 @@
<span class="material-icons text-2xl">chevron_right</span>
</button>
</div>
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex" :style="{ height: height + 'px' }">
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex space-x-4" :style="{ height: height + 'px' }">
<template v-for="item in items">
<cards-narrator-card :key="item.name" :ref="`slider-item-${item.name}`" :narrator="item" :height="cardHeight" :width="cardWidth" class="relative mx-2" @hook:updated="setScrollVars" />
<cards-narrator-card :key="item.name" :ref="`slider-item-${item.name}`" :narrator="item" :height="cardHeight" :width="cardWidth" class="relative" @hook:updated="setScrollVars" />
</template>
</div>
</div>

View File

@@ -10,10 +10,10 @@
<span class="material-icons text-2xl">chevron_right</span>
</button>
</div>
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex" :style="{ height: height + 'px' }">
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex space-x-4" :style="{ height: height + 'px' }">
<template v-for="(item, index) in items">
<cards-lazy-series-card :key="item.id" :ref="`slider-item-${item.id}`" :index="index" :series-mount="item" :height="cardHeight" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" :bookshelf-view="$constants.BookshelfView.DETAIL" class="relative mx-2" @hook:updated="setScrollVars" />
<cards-lazy-series-card :key="item.id" :ref="`slider-item-${item.id}`" :index="index" :series-mount="item" :height="cardHeight" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" :bookshelf-view="$constants.BookshelfView.DETAIL" class="relative" @hook:updated="setScrollVars" />
</template>
</div>
</div>

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