Compare commits

...

250 Commits

Author SHA1 Message Date
advplyr dd2d61f38e Version bump 2.1.2 2022-08-06 17:05:23 -05:00
advplyr ca2c2f2702 Fix:Episode match encode title query string 2022-08-06 16:51:25 -05:00
advplyr 1fc929ab33 Update:Min width on libraries dropdown 2022-08-06 16:41:26 -05:00
advplyr f5495d64a9 Update:Show resolution under covers in covers tab #885 2022-08-06 08:25:31 -05:00
advplyr d6afb17bf2 Fix:Library stats page overflowing under side nav #896 2022-08-06 08:10:12 -05:00
advplyr 2cba9d8f4a Fix:Finished items showing yellow bar on item page 2022-08-06 08:00:28 -05:00
advplyr e02169907d Add:Filter for RSS feeds open #893 2022-08-06 07:58:19 -05:00
advplyr 24a142e718 Add:RSS feed icon over library item covers when feed is open #893 2022-08-05 19:23:18 -05:00
advplyr 2cb4f972d7 Update:Collections page show total duration 2022-08-04 18:22:35 -05:00
advplyr 513d946faa Update:Updates to collections page for mobile screen sizes 2022-08-04 18:06:39 -05:00
advplyr 87d1f457ba Update:Editing series on book in edit modal sets focus to first available input #889 2022-08-03 18:38:08 -05:00
advplyr 8810f90226 Update:Cleanup edit modal match tab for mobile screens 2022-08-03 18:27:38 -05:00
advplyr 3d3571013f Add:Show audiobook duration and narrator in audible match results #886 2022-08-03 18:06:25 -05:00
advplyr 605a6d8b25 Update:Match tab searches for results when opening tab #882 2022-08-03 17:53:19 -05:00
advplyr 1bfa4b31f2 Fix:Audiobook RSS feed offset pubDate for each track to ensure correct order #888 2022-08-03 17:01:28 -05:00
advplyr 7a14b49aea Fix:Continue series shelf showing in reverse order 2022-08-02 16:41:52 -05:00
advplyr 95ac74d748 Fix:Overdrive chapter parser crash server on invalid meta data #880 2022-08-01 18:28:56 -05:00
advplyr fddf850a41 Add:Cron validation api endpoint 2022-08-01 18:06:22 -05:00
advplyr d93d4f3236 Update:Auto-download new podcast episode check max failed attempts to 24 2022-07-31 14:00:17 -05:00
advplyr 91f15d5a23 Update:Scanned in podcast episodes remove file ext from title 2022-07-31 13:52:34 -05:00
advplyr 516c5c3308 Add:Podcast episode match tab and find episode by title api route 2022-07-31 13:12:37 -05:00
advplyr f702c02859 Add click to play session at timestamp in users playback sessions table 2022-07-30 18:20:15 -05:00
advplyr ad88de0571 Version bump 2.1.1 2022-07-30 17:14:59 -05:00
advplyr b64a651b27 Update:Series page support sort ignore prefix #866 2022-07-30 17:07:54 -05:00
advplyr 06b8d1194c Fix:Library collapsed series to respect ignore prefixes setting #866 2022-07-30 16:18:26 -05:00
advplyr 377ae7ab19 Update /matchall api endpoint to GET 2022-07-30 15:52:13 -05:00
advplyr 53cf6edd6a Update:Show prompt before marking item as finished that has progress #805 2022-07-30 12:40:43 -05:00
advplyr 92bedeac15 Update:Click chapter times in chapters table to jump to timestamp 2022-07-30 12:25:15 -05:00
advplyr 3cf8b9dca9 Update start playback from bookmark time confirm to new confirm prompt 2022-07-30 11:53:48 -05:00
advplyr bcc2f847f9 Add:Click timestamp in listening sessions table to open playback at timestamp #798, add confirm prompt 2022-07-30 11:36:04 -05:00
advplyr f1421f351b Merge pull request #871 from arabshapt/feature/use-svg-instead-of-png
feature: use svg instead of png where possible for better quality
2022-07-30 08:45:20 -05:00
advplyr ed23feaf3f Fix typo Received Ping 2022-07-30 08:37:35 -05:00
arabshapt 668ebf8550 feature: use svg instead of png where possible for better quality 2022-07-30 13:58:20 +02:00
advplyr a8c7905f6d Add:Bookmarks icon btn on library item page and ability to open player at specified time #796 2022-07-29 19:06:52 -05:00
advplyr 45cd39ac0c Update:Remain on same tab in item edit modal when navigating next/prev #867 2022-07-29 18:19:19 -05:00
advplyr 21e1f62c65 Fix:Merging chapters from multiple files skipping chapters #857 2022-07-29 18:15:41 -05:00
advplyr 8416f2d6be Fix:Remove invalid playback sessions on server start #868 2022-07-29 17:13:46 -05:00
advplyr 3b4ac3a230 Fix:Long library names overflow #858 2022-07-26 18:54:32 -05:00
advplyr 6244909332 Update:Timestamp input support over 99 hours and focus input #657 2022-07-25 19:32:04 -05:00
advplyr 5db949e4a7 Update:Save after editing chapters redirects to previous page #827 2022-07-25 18:57:00 -05:00
advplyr c453d3e8c7 Update:Chapter editor to use timestamp input for chapter start time with toggle to show seconds #657 2022-07-25 18:40:11 -05:00
advplyr 9d7ffdfcd0 Update docker file healthcheck to use /healthcheck instead of /ping 2022-07-24 15:46:19 -05:00
advplyr 976427b0b3 Fix:Set correct mime type for m4b file static requests 2022-07-24 13:32:05 -05:00
advplyr 6cbfd8679b Update:Changing author name to match another authors name will merge the authors #487 2022-07-24 12:00:36 -05:00
advplyr 217bbb4a8e Merge pull request #842 from revilo951/master
Add restart and fix volumes
2022-07-20 17:49:43 -05:00
advplyr 9916a1e8f6 Fix:Watcher scanner to ignore non-media files that are not inside library item folders #834 2022-07-19 08:33:32 -05:00
advplyr 372101592c Version bump 2.1.0 2022-07-18 18:41:51 -05:00
advplyr 18123664ee Fix:RSS Feed cover, Update:Remove experimental scanner 2022-07-18 18:39:51 -05:00
advplyr 2e6e4f970c Remove comments from scanner 2022-07-18 18:17:50 -05:00
advplyr 1c9e56ce2e Fix:Libraries table to use draggable handle for items so they can be clicked and edited #839 2022-07-18 17:44:01 -05:00
advplyr 9e7b84f289 Update:JWT signing 2022-07-18 17:19:16 -05:00
revilo951 7b83ab8970 Add restart and fix volumes
Added `restart: unless-stopped`
Adjust volumes - probably shouldn't be scattering the volumes around the root dir?
https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-3
2022-07-18 10:12:02 +10:00
advplyr 86ee4dcff2 Update:Scanner adjustable number of parallel audio probes to use less CPU 2022-07-16 18:54:34 -05:00
advplyr 277a5fa37c Version bump 2.0.24 2022-07-14 19:51:33 -05:00
advplyr 51b87912f8 Update:Collapsed series shows series name instead of first book title and only sorts when sorting by title #629 2022-07-14 19:00:52 -05:00
advplyr 653019921e Add:Support for OGA file extension #804, Update:Mime type for m4b and m4a to audio/mp4 2022-07-14 18:32:00 -05:00
advplyr ccc291067d Fix:Truetype fonts format 2022-07-14 18:06:37 -05:00
advplyr af7e3a03f0 Fix:Audio player when using chapter track then playing an item without chapters 2022-07-13 19:38:34 -05:00
advplyr 7c40d26857 Fix:Sync local mobile app progress replacing local media progress id causing duplicate media progress in mobile 2022-07-13 19:18:49 -05:00
advplyr 6c507de501 Remove client marked dependency 2022-07-12 16:21:32 -05:00
advplyr 482a4340f5 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2022-07-12 16:19:54 -05:00
advplyr 21e704e12c Fix:Show toasts below appbar & toolbar #819 2022-07-12 16:11:23 -05:00
advplyr 2b91bff1af Fix:Ordering newly scanned in audio tracks properly #823 2022-07-12 15:02:08 -05:00
advplyr d11f9608b4 Remove old audio file scanner 2022-07-12 14:35:43 -05:00
advplyr 2b0b691b69 Merge pull request #821 from jmt-gh/whats_new_modal
Add ability to view current version's changelog from within ABS
2022-07-09 17:32:33 -05:00
advplyr 5dfd5c4971 Remove marked dependency 2022-07-09 17:29:30 -05:00
advplyr 201f1bff3e Add marked package in static libs 2022-07-09 17:27:30 -05:00
jmt-gh a22ebb257f add style comments 2022-07-08 20:45:09 -07:00
jmt-gh bf6e87d4bc update formatting 2022-07-08 20:34:32 -07:00
jmt-gh b823a93ae2 integrate modal to sidenavs 2022-07-08 20:29:18 -07:00
jmt-gh 05afd12682 add new changelog modal 2022-07-08 20:28:34 -07:00
jmt-gh 997e23150e extract current version changelog information 2022-07-08 20:26:30 -07:00
advplyr 3c5bf376b5 Remove archiver-utils dependency 2022-07-08 18:11:09 -05:00
advplyr bca2cfda13 Update:Remove log listener for root user & only set when on logger config page 2022-07-07 17:25:52 -05:00
advplyr 916b41d587 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2022-07-06 20:12:22 -05:00
advplyr ab08d83c04 Remove archiver dependency 2022-07-06 20:12:14 -05:00
advplyr 415e0a7b5a Remove dependency date-and-time 2022-07-06 19:18:27 -05:00
advplyr d301c12acd Remove dependency express-rate-limit 2022-07-06 19:14:47 -05:00
advplyr 7aa7e662b2 Remove dependency express-fileupload 2022-07-06 19:10:25 -05:00
advplyr 1dbfb5637a Remove bcryptjs dependency 2022-07-06 19:01:27 -05:00
advplyr 4e1aacb44f Remove command-line-args dependency 2022-07-06 18:56:13 -05:00
advplyr 954cf3e14e Remove jsonwebtoken dependency 2022-07-06 18:45:43 -05:00
advplyr b61ecefce4 Remove fluent-ffmpeg dependency 2022-07-06 17:38:19 -05:00
advplyr 8562b8d1b3 Remove node-ffprobe dependency 2022-07-06 17:07:08 -05:00
advplyr 06ec2159f5 Update bug.yaml 2022-07-06 07:18:03 -05:00
advplyr 68b565505e Update bug.yaml 2022-07-06 07:13:39 -05:00
advplyr 83ff2752dd Update bug.yaml 2022-07-06 07:10:27 -05:00
advplyr d0af1c3c9a Remove fs-extra dependency 2022-07-05 19:53:01 -05:00
advplyr 1ad46d4fb8 Add missing dependency license files 2022-07-05 19:33:43 -05:00
advplyr d3dd13eae5 Remove node-stream-zip dependency 2022-07-05 19:24:16 -05:00
advplyr f27982d887 Update:Default backup schedule to 1:30 to avoid conflict with new episode checks #761 2022-07-05 17:38:17 -05:00
advplyr 624a44f572 Fix:Quick match split multiple comma separated authors #808 2022-07-05 17:26:14 -05:00
advplyr e623bf7fde Remove rra dependency 2022-07-04 19:19:38 -05:00
advplyr 6fc70b8656 Remove LibGen provider and package 2022-07-04 19:14:52 -05:00
advplyr 354cefb9f4 Update:Update isFile flag on check scan data for library items 2022-07-03 15:04:41 -05:00
advplyr a78aa88dbc Fix:Audiobooks incorrectly flagged as single file root audiobooks #714 2022-07-03 14:41:07 -05:00
advplyr 9ac2453676 Merge pull request #802 from jmt-gh/getMediaProgress_fix
fix getMediaProgress always returning 404
2022-07-03 12:20:24 -05:00
advplyr bb70800b4e Merge pull request #801 from mcdinner/missing-cache-directory
Remove cachePathExists property (Issue #800)
2022-07-03 12:19:51 -05:00
jmt-gh 855272a558 fix getMediaProgress not returning properly 2022-07-03 10:15:40 -07:00
mcdinner ebb2c5f791 Remove cachePathExists property (Issue #800)
Remove cachePathsExist property to ensure missing cache directories are recreated when EnsureCachePaths() called.
2022-07-03 16:35:12 +02:00
advplyr 2e466bb164 Update tailwindcss 2022-07-02 20:02:43 -05:00
advplyr 95ebe0f087 Version bump 2.0.23 2022-07-02 19:15:23 -05:00
advplyr 0a6aa43b07 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2022-07-02 09:10:52 -05:00
advplyr 806a8cf659 Update:Library config page for mobile #753 and style updates 2022-07-02 09:10:47 -05:00
advplyr 1a32fbfeec Merge pull request #795 from jmt-gh/issue_794
Fix issue with unecessary empty search during match (Issue #794)
2022-06-30 15:48:07 -05:00
jmt-gh 67396c16dd formatting update 2022-06-29 19:25:59 -07:00
jmt-gh b0684b6f1b Await the responses from googlebooks and itunes 2022-06-29 19:19:58 -07:00
jmt-gh 661778c02c Await the response from audible for book search by ASIN 2022-06-29 19:18:00 -07:00
advplyr 5c4241aefe Merge pull request #792 from jmt-gh/issue_760
Fix truncation on your stats page (Issue #760)
2022-06-29 19:24:11 -05:00
jmt-gh 3f6bc90824 remove truncation from number column 2022-06-29 08:25:12 -07:00
advplyr 4ade6e04a8 Merge pull request #791 from alexmaras/master
fix: disable workbox to prevent failure with service worker
2022-06-29 08:23:07 -05:00
Alex Maras 49d0835236 fix: disable workbox to prevent failure with service worker 2022-06-29 21:11:03 +08:00
advplyr d90bd92bcc Fix:Item edit modal for mobile landscape #754 2022-06-28 18:29:11 -05:00
advplyr 41c016b8c7 Update:Match card show series and series sequence if available #762 2022-06-28 17:32:46 -05:00
advplyr 5b4d3f71f9 Update:Global library search strips periods, commas and other characters when matching #750 2022-06-26 15:46:16 -05:00
advplyr 256a9322ef Fix:Mobile toolbar for podcasts and add collections for books #693 2022-06-26 11:34:58 -05:00
advplyr 793f82e445 Update:Edit modal for mobile screen sizes and update tailwind 2022-06-26 11:15:19 -05:00
advplyr ab6da3914b Merge pull request #777 from alexmaras/fix/chapter-seek-bar
Fix/chapter seek bar
2022-06-25 11:03:23 -05:00
advplyr 0b53f0ebf3 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2022-06-25 11:01:06 -05:00
advplyr 76d668514e Fix:Mark as not finished when duration is not set in media progress #771 2022-06-25 11:01:01 -05:00
Alex Maras 3c347bef7d fix: better variable naming 2022-06-26 00:00:52 +08:00
Alex Maras e837e5f780 fix: reuse existing variable 2022-06-25 23:58:55 +08:00
Alex Maras 26348ccc74 Merge remote-tracking branch 'fork/master' into fix/chapter-seek-bar 2022-06-25 23:56:42 +08:00
Alex Maras 729a756e21 fix: use total time for chapter name resolution when in chapter track mode 2022-06-25 23:53:40 +08:00
advplyr 4dbddcf179 Merge pull request #776 from alexmaras/fix/chapter-seek-bar
fix: use chapter duration when seeking on track bar
2022-06-25 10:40:55 -05:00
Alex Maras f2fff34d4d fix: use chapter start as a base for the seek time if seeking within chapters 2022-06-25 23:37:18 +08:00
advplyr 59c5e2c1d9 Allow custom headers in requests 2022-06-25 10:36:37 -05:00
Alex Maras 067006f406 fix: use chapter duration when seeking on track bar 2022-06-25 23:24:40 +08:00
advplyr 93d82b973e Merge pull request #770 from jmt-gh/relative_chapter_times
Show chapter duration in the "now playing" chapter modal (Issue #767)
2022-06-23 17:51:56 -05:00
advplyr a9a3423b58 Update chapter modal for mobile 2022-06-23 17:50:25 -05:00
advplyr f4ee215ad8 Update chapters modal truncate long chapter titles and show chapter duration 2022-06-23 17:36:55 -05:00
jmt-gh 48431b1c35 add support for showing chapter duration in chapters modal 2022-06-22 18:58:57 -07:00
advplyr ce961f90ba Merge pull request #759 from jmt-gh/update_stats_uis
Update Library Stats and Your Stats UIs to match other Settings UIs
2022-06-22 17:50:05 -05:00
advplyr 916d2f6bb3 Merge pull request #758 from jmt-gh/update_settings
Update Settings page UI
2022-06-22 17:48:06 -05:00
advplyr 01e7098f00 Updates to setting formatting and copy 2022-06-22 17:47:21 -05:00
advplyr e02fbac4cd Merge pull request #757 from jmt-gh/update_users_table
Update UsersTable styling to match other tables
2022-06-22 17:30:46 -05:00
advplyr a8fce32e70 Merge pull request #749 from jmt-gh/add_reorder_icons_to_libraries
Add reorder icon to libraries table
2022-06-20 10:27:06 -05:00
jmt-gh) d0637c1e3d update library and your stat UIs to match 2022-06-19 19:25:44 -07:00
jmt-gh) f6702d299d update html formatting 2022-06-19 18:08:04 -07:00
jmt-gh) 033b7ece28 update text formatting 2022-06-19 17:46:38 -07:00
jmt-gh) 5f5dce6d53 initial Settings update 2022-06-19 17:31:52 -07:00
jmt-gh) 82c5c7518b remove unecessary css styling 2022-06-19 17:13:24 -07:00
jmt-gh) 7a60ffb3c4 update UsersTable styling 2022-06-19 17:10:15 -07:00
advplyr 2795f657b5 Merge pull request #755 from jmt-gh/update_sessions_table
Update Sessions page to have a matching "settings UI"
2022-06-19 18:28:35 -05:00
advplyr 9ef5b5830e Merge pull request #752 from jmt-gh/issue_702
Move matching toasts to top right (Issue #702)
2022-06-19 18:26:16 -05:00
advplyr 879adfa633 Remove last bottom-center for toast 2022-06-19 18:25:59 -05:00
advplyr b12a344776 Fix:Chromecast button on mobile screen sizes #756 2022-06-19 15:43:45 -05:00
jmt-gh 50b1098797 add back in empty state 2022-06-19 10:07:09 -07:00
jmt-gh fdfaa7eba4 unify on 'Listening Sessions' 2022-06-19 10:01:06 -07:00
jmt-gh 5525587513 update sessions table to match other settings tables UI 2022-06-19 09:54:22 -07:00
jmt-gh 1f20ed7640 Move matching toasts to top right 2022-06-19 09:31:51 -07:00
advplyr f741064843 Merge pull request #748 from jmt-gh/update_log_page
Update Log page to have a matching "settings UI"
2022-06-19 10:07:56 -05:00
advplyr d5138e4c0a Merge pull request #747 from jmt-gh/fix_remove_button_padding
Fix padding on "Remove All Library Items" button
2022-06-19 10:06:26 -05:00
advplyr 42a30c33db Merge pull request #746 from jmt-gh/update_placard_size
Fix placard sizes so "Continue Listening" fits
2022-06-19 10:06:04 -05:00
advplyr e5d978f8e8 Merge pull request #744 from jmt-gh/issue_741
Add toggle for switching between Chapter and Book Duration in player (issue #741)
2022-06-19 10:05:06 -05:00
advplyr ccc82520a9 Update chapter track progress bar, timestamps, hide chapter ticks. Update mobile responsiveness for player 2022-06-19 10:04:15 -05:00
jmt-gh) 22acf52a26 remove unecessary space 2022-06-19 02:06:15 -07:00
jmt-gh) 2ccd2786f4 add reorder icon to the library items 2022-06-19 02:03:05 -07:00
jmt-gh 0028136935 update height of content to optimize screen space 2022-06-19 01:53:23 -07:00
jmt-gh 0edc46b771 update log page to have a matching UI 2022-06-19 01:46:42 -07:00
jmt-gh 2261f3d1c3 fix right padding on remove all items button 2022-06-19 01:09:02 -07:00
jmt-gh 5c0e792782 fix placard size so continue listening fits 2022-06-19 01:03:56 -07:00
jmt-gh 644882e04f add support for swapping progress bar between current chapter duration and book duration 2022-06-18 23:55:34 -07:00
advplyr 67f51c6de9 Version bump 2.0.22 2022-06-18 18:43:19 -05:00
advplyr 0c8fd6ab0e Update:Uploader to treat audio files as separate audiobooks if uploading only audio files #670 2022-06-18 16:44:20 -05:00
advplyr 5452a57a14 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2022-06-18 13:11:20 -05:00
advplyr 19f020e7a6 Fix:Open playback session on init #743 2022-06-18 13:11:15 -05:00
advplyr 825641f2a9 Merge pull request #742 from mrdth/feature/fetch-author-image
Fetch author photo from external URL
2022-06-18 12:10:53 -05:00
advplyr 35ab4cb2fe Update photo url input to photo path/url to be consistent with item covers 2022-06-18 12:05:30 -05:00
advplyr fd13607d89 Update:Logs to use server local timezone instead of UTC #656 2022-06-18 11:34:28 -05:00
mrdth f79b4d44b9 Fetch author photo from external URL
Add a new text field 'Photo URL' on the author edit modal, if there
is no existing image for an author.

When submitted, the image is saved from the URL provided
2022-06-18 17:04:38 +01:00
advplyr 91e30a6e84 Merge pull request #736 from cassieesposito/dateFormat
Date format
2022-06-18 10:34:32 -05:00
advplyr 8ab0f164a6 Update server settings ui and add display settings section 2022-06-18 10:32:12 -05:00
advplyr 578bb03404 Merge pull request #730 from jmt-gh/issue_719
Properly support multiple narrators when Matching (#719)
2022-06-18 09:32:46 -05:00
advplyr 06582b5371 Merge pull request #739 from jmt-gh/mobile_responsive_search
Show search bar on mobile responsive views (fix #715)
2022-06-18 09:30:17 -05:00
Cassie Esposito 7f6baf35b7 Merge branch 'advplyr:master' into dateFormat 2022-06-17 20:58:05 -07:00
advplyr 6227d0baa1 Update 2022-06-17 19:09:03 -05:00
advplyr e334b585be Libraries dropdown updates for mobile 2022-06-17 16:50:46 -05:00
advplyr c83b3f19f7 Merge pull request #735 from samosbor/master
Update BookFinder.js: Retry Search with Clean Title and Author if No Books Found
2022-06-17 15:46:57 -05:00
Sam d31ec055f9 check if cleantitle and cleanauthor are different 2022-06-17 10:31:31 -07:00
jmt-gh 38c259a45e initial commit for responsive search bar on mobile 2022-06-17 07:48:42 -07:00
Cassie Esposito b2ee24de98 Applied dateFormat setting to alternative bookshelf view 2022-06-17 01:35:24 -07:00
Cassie Esposito 9ba0e52bb7 Added an option to change date format 2022-06-17 01:26:10 -07:00
Sam edc712e6f6 Merge branch 'master' of https://github.com/samosbor/audiobookshelf 2022-06-17 01:14:19 -07:00
Sam 485888b2d9 list includes syntax 2022-06-16 22:37:36 -07:00
advplyr c2e90d4d83 Merge pull request #734 from jmt-gh/add_local_docker_build_commands
Add npm commands to build local docker containers
2022-06-16 17:09:48 -05:00
advplyr f5d89b8f52 Merge pull request #733 from jmt-gh/multiline_pills
Update pills so that they handle multi-line properly
2022-06-16 17:08:59 -05:00
advplyr 378b40790a Merge branch 'master' into multiline_pills 2022-06-16 17:08:47 -05:00
advplyr be3d38392d Merge pull request #732 from jmt-gh/update_pills
Update details "pills" so they don't overlap when they flow to new lines
2022-06-16 17:07:21 -05:00
Sam Osborne 27fef50983 retry search with clean title author 2022-06-16 13:48:37 -07:00
jmt-gh 167df85c1e add npm commands to build local docker containers 2022-06-15 22:23:36 -07:00
jmt-gh 009e16c9a4 add break-all for multiline support for long entries 2022-06-15 22:12:56 -07:00
jmt-gh b40cc767b2 add margin on y so pills don't touch when they overflow 2022-06-15 20:49:27 -07:00
jmt-gh 4f5f2d32be Properly support multiple narrators when Matching
This commit adds proper support for multiple narrators when matching. We
were accidentally missing a split in the same way we handle it for
genres and tags
2022-06-15 20:23:56 -07:00
advplyr 66be3e0281 Update:Clear global search input when clicking dropdown item #723 2022-06-15 12:35:50 -05:00
advplyr 987f188f00 Merge pull request #720 from jmt-gh/fix_formatting
Fix indents from 4 spaces to 2 in parseOverdriveMediaMarkers.js
2022-06-14 13:28:11 -05:00
jmt-gh daca2bdf2a fix spacing from 4 to 2 2022-06-13 22:23:02 -07:00
advplyr 8894f52439 Merge pull request #716 from jmt-gh/abs_overdrive
Add support for leveraging chapter data directly from Overdrive mp3s during scanning
2022-06-13 18:25:44 -05:00
jmt-gh 863f81e55a remove logger 2022-06-12 20:43:20 -07:00
jmt-gh d03d3735e5 Add chapter end time support 2022-06-12 20:28:09 -07:00
advplyr 3bb2df6e12 Fix:Abs metadata parser to allow second equals sign in value #634 2022-06-12 17:47:14 -05:00
advplyr 80c9efc618 Update:Make m4b timeout to 30 mins 2022-06-12 15:59:36 -05:00
jmt-gh 3279901ab0 remove clientside changes 2022-06-12 12:24:29 -07:00
jmt-gh d43d351721 remove loggers 2022-06-12 02:03:26 -07:00
jmt-gh 8210eba439 clean up loggers 2022-06-12 01:57:00 -07:00
jmt-gh cbd7294b0b add getter to libraryscan.js for overdrivemediamarker 2022-06-12 01:54:58 -07:00
jmt-gh 6064e8af87 fix using OMMs with regular scan option 2022-06-12 01:46:50 -07:00
jmt-gh 8754f0c25f Cleaning up server code
Doing some literal cleaning
2022-06-12 01:38:09 -07:00
jmt-gh f31700f668 update comments 2022-06-12 00:54:34 -07:00
jmt-gh 9877b139f6 add new parser for overdrive media markers 2022-06-12 00:53:56 -07:00
jmt-gh 5643c846ee Fix bug for certain scan types
Needed to look in to scanOptions to access the properties I wanted.
It's..... unclear to me if this needs to be done for those other ones as
well. I think so?
2022-06-12 00:47:54 -07:00
jmt-gh 5a071babe9 added library/ to .gitignore 2022-06-12 00:22:04 -07:00
jmt-gh 3d85d0bce6 add settings page setting 2022-06-12 00:21:37 -07:00
jmt-gh 68afc2c718 Add support for various scan types
This commit adds support for the various scan types, and ensures that we
only run Overdrive parsing on files that can actually support it
2022-06-11 23:56:36 -07:00
jmt-gh b3d9323f66 Initial commit for server side approach
This is the first commit for bringing this over to the server side.

It works! Right now it fails if the autoscanner or or the manual
individual book scanner try to do it's thing. I'll need to update those
2022-06-11 23:17:22 -07:00
jmt-gh effc63755b remove unused console.log 2022-06-11 11:07:49 -07:00
jmt-gh b90934a72a remove unused modal 2022-06-11 11:06:19 -07:00
jmt-gh e01748eb2f Full support for generating/applying chapter data
This commit adds the rest of the support for actually being able to use
Overdrive MediaMarkers.

Shoutout to benonymity's project OverdriveChapterize, where I was able
to port over the logic to actually do the timestamp conversions

https://github.com/benonymity/OverdriveChapterizer/blob/main/chapters.py

I still need to do a lot of cleanup of the actual code, and finish the UI.
2022-06-11 10:57:17 -07:00
jmt-gh 430fbf5e46 Parse out mediamarkers from all files
This commit updates the logic in
generateChaptersFromOverdriveMediaMarkers to create a single array of
objects that holds all of the clean MediaMarker data. it still needs to
be conveted to the NewChapters format.

I should also rename this function to "cleanChaptersFromOMM", but I"ll
do that later
2022-06-11 09:34:22 -07:00
jmt-gh 27e6b9ce0d DRAFT client support for overdrive media markers
Initial client side support. Still a good amount to do. Specifically
around actually parsing out all of the media markers, and generating a
single chapter object that can be applied
2022-06-11 02:01:37 -07:00
jmt-gh fc614b9833 Add support for overdrive media marker file tag
This commit adds serverside support for grabbing the
overdrive_media_marker file tag that exists on mp3 files from overdrive
2022-06-11 02:00:07 -07:00
advplyr a97c102369 Version bump 2.0.21 2022-06-09 18:24:03 -05:00
advplyr 745a491f90 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2022-06-09 17:56:22 -05:00
advplyr b2880ab0a9 Fix:Match tab #708 2022-06-09 17:56:16 -05:00
advplyr f916454c55 Merge pull request #707 from jmt-gh/fix_library_scanning
Fix library scanning failing
2022-06-09 04:54:08 -05:00
jmt-gh 701b8ea12e Fix bug with library scanning introduced in #697
Looks like #697 missed a reference update that caused scanning libraries
to fail. This fixes that
2022-06-08 19:15:35 -07:00
advplyr 2079942ccd Merge pull request #697 from jvanbruegge/patch-1
Use `show` and `episode_id` tags for audiobook series
2022-06-08 16:10:28 -05:00
advplyr 140b718592 Merge pull request #699 from jmt-gh/698_metadata_downloads_not_created
Update some instances of mkdir to ensureDir (#698)
2022-06-08 16:05:19 -05:00
Jan van Brügge 2de8c72131 Allow show and episode_id tags for audiobook series
FFmpeg only supports a very limited number of tags for m4b files (see https://wiki.multimedia.cx/index.php?title=FFmpeg_Metadata) by default. `series` and `series-part` are only possible by enabling custom tags with `-movflags use_metadata_tags`. To work around that, `show` and `episode_id` are added as second option.
2022-06-08 11:11:17 +02:00
advplyr 089d4b5cee Update:Remove fast-sort dependency 2022-06-07 20:22:23 -05:00
advplyr e06a015d6e Update:Remove proper-lockfile dependency 2022-06-07 20:15:00 -05:00
advplyr b7e546f2f5 Update:Remove node-cron dependency 2022-06-07 20:04:51 -05:00
advplyr 26ef275ab4 Update:Remove image-type dependency 2022-06-07 19:53:05 -05:00
advplyr 416db7c981 Update:Remove read-chunk dependency 2022-06-07 19:44:38 -05:00
advplyr 78079b2e60 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2022-06-07 19:25:28 -05:00
advplyr 03bffb725a Update:Remove rss feed dependencies add node-xml lib 2022-06-07 19:25:14 -05:00
advplyr 46fc89e247 Update:Cache feed xml 2022-06-07 18:37:37 -05:00
advplyr fbbceaa642 Add:Persist RSS feeds in db #696, Update:RSS feed data model 2022-06-07 18:29:43 -05:00
jmt-gh f5aae25cc8 remove random character 2022-06-06 18:52:08 -07:00
jmt-gh 8d03943acb remove extra debug log 2022-06-06 18:51:49 -07:00
jmt-gh 853513b926 update approach for ensuring download directory always exists 2022-06-06 18:51:08 -07:00
jmt-gh c606a41314 update Cache folder creation to leverage ensureDirs 2022-06-06 08:18:15 -07:00
jmt-gh 35f29ca22b Use ensureDir instead of mkdir to fix 698
This commit updates the mkdir for creating the download location to
ensureDir, which is an alias for mkdirs and mkdirp, meaning they will
create the entire path of the directory if it does not exist.

https://github.com/jprichardson/node-fs-extra/blob/master/docs/ensureDir.md
2022-06-06 08:12:58 -07:00
advplyr ac00f3ebe7 Merge pull request #692 from cassieesposito/getBookDataFromDir-refactor
Fixed bugs that caused getSequence to run twice and broke year recognition
2022-06-05 18:02:39 -05:00
Cassie Esposito 6846de98f8 Fixed bugs that caused getSequence to run twice and broke year recognition 2022-06-05 15:52:18 -07:00
advplyr 881baa818d Fix:Progress filter 2022-06-05 15:26:27 -05:00
advplyr b671145e73 Merge pull request #682 from jmt-gh/update_cover_on_merge
Support embedding cover art metadata in the embed metadata tool
2022-06-05 13:04:12 -05:00
jmt-gh 8809c7b900 Handle null and delete cover cases
This commit adds in supporting if a cover path is null. If this is the
case, we completely remove the video stream from the file, as the user
either:

a) uploaded a file with no video stream (so removing it is a no-op)
b) removed the cover in ABS, so we should respect that on merge
2022-06-05 10:36:42 -07:00
jmt-gh 9b6fa8fe8c Merge branch 'advplyr:master' into update_cover_on_merge 2022-06-04 19:00:41 -07:00
jmt-gh d6c02ebb2c Support embedding cover art metadata
Added support for chapter metadata in #678, but completely missed that
coverart wasn't getting embedded in the embed metadata tool. This commit
adds that in
2022-06-04 18:56:55 -07:00
444 changed files with 42974 additions and 4061 deletions
+7
View File
@@ -9,6 +9,12 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: "### Mobile app issues report [here](https://github.com/advplyr/audiobookshelf-app/issues/new/choose)." value: "### Mobile app issues report [here](https://github.com/advplyr/audiobookshelf-app/issues/new/choose)."
- type: markdown
attributes:
value: "### Join the [discord server](https://discord.gg/pJsjuNCKRq) for questions or if you are not sure about a bug."
- 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."
- type: textarea - type: textarea
id: what-happened id: what-happened
attributes: attributes:
@@ -27,6 +33,7 @@ body:
id: version id: version
attributes: attributes:
label: Audiobookshelf version 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: validations:
required: true required: true
+2 -1
View File
@@ -11,6 +11,7 @@ test/
/client/.nuxt/ /client/.nuxt/
/client/dist/ /client/dist/
/dist/ /dist/
library/
sw.* sw.*
.DS_STORE .DS_STORE
+1 -1
View File
@@ -25,5 +25,5 @@ HEALTHCHECK \
--interval=30s \ --interval=30s \
--timeout=3s \ --timeout=3s \
--start-period=10s \ --start-period=10s \
CMD curl -f http://127.0.0.1/ping || exit 1 CMD curl -f http://127.0.0.1/healthcheck || exit 1
CMD ["npm", "start"] CMD ["npm", "start"]
+14
View File
@@ -225,4 +225,18 @@ Bookshelf Label
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
/* number of lines to show */ /* number of lines to show */
-webkit-box-orient: vertical; -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;
}
.app-bar .Vue-Toastification__container.top-right {
padding-top: 64px;
}
.no-bars .Vue-Toastification__container.top-right {
padding-top: 8px;
} }
+10 -3
View File
@@ -1,34 +1,40 @@
.flip-list-move { .flip-list-move {
transition: transform 0.5s; transition: transform 0.5s;
} }
.no-move { .no-move {
transition: transform 0s; transition: transform 0s;
} }
.ghost { .ghost {
opacity: 0.5; opacity: 0.5;
background-color: rgba(255, 255, 255, 0.25); background-color: rgba(255, 255, 255, 0.25);
} }
.list-group { .list-group {
min-height: 30px; min-height: 30px;
} }
#librariesTable .item {
cursor: n-resize;
}
.drag-handle { .drag-handle {
cursor: n-resize; cursor: n-resize;
} }
.list-group-item:not(.exclude) { .list-group-item:not(.exclude) {
cursor: n-resize; cursor: n-resize;
} }
.list-group-item.exclude { .list-group-item.exclude {
cursor: not-allowed; cursor: not-allowed;
} }
.list-group-item:not(.ghost):not(.exclude):hover { .list-group-item:not(.ghost):not(.exclude):hover {
background-color: rgba(0, 0, 0, 0.1); background-color: rgba(0, 0, 0, 0.1);
} }
.list-group-item:nth-child(even):not(.ghost):not(.exclude) { .list-group-item:nth-child(even):not(.ghost):not(.exclude) {
background-color: rgba(0, 0, 0, 0.25); background-color: rgba(0, 0, 0, 0.25);
} }
.list-group-item:nth-child(even):not(.ghost):not(.exclude):hover { .list-group-item:nth-child(even):not(.ghost):not(.exclude):hover {
background-color: rgba(0, 0, 0, 0.1); background-color: rgba(0, 0, 0, 0.1);
} }
@@ -36,6 +42,7 @@
.list-group-item.exclude:not(.ghost) { .list-group-item.exclude:not(.ghost) {
background-color: rgba(255, 0, 0, 0.25); background-color: rgba(255, 0, 0, 0.25);
} }
.list-group-item.exclude:not(.ghost):hover { .list-group-item.exclude:not(.ghost):hover {
background-color: rgba(223, 0, 0, 0.25); background-color: rgba(223, 0, 0, 0.25);
} }
+26 -26
View File
@@ -74,7 +74,7 @@
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
} }
@@ -84,7 +84,7 @@
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
@@ -94,7 +94,7 @@
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype');
unicode-range: U+1F00-1FFF; unicode-range: U+1F00-1FFF;
} }
@@ -104,7 +104,7 @@
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype');
unicode-range: U+0370-03FF; unicode-range: U+0370-03FF;
} }
@@ -114,7 +114,7 @@
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
} }
@@ -124,7 +124,7 @@
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
} }
@@ -134,7 +134,7 @@
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }
@@ -154,7 +154,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
@@ -164,7 +164,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype');
unicode-range: U+1F00-1FFF; unicode-range: U+1F00-1FFF;
} }
@@ -174,7 +174,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype');
unicode-range: U+0370-03FF; unicode-range: U+0370-03FF;
} }
@@ -184,7 +184,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
} }
@@ -194,7 +194,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
} }
@@ -204,7 +204,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }
@@ -214,7 +214,7 @@
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
} }
@@ -224,7 +224,7 @@
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
@@ -234,7 +234,7 @@
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype');
unicode-range: U+1F00-1FFF; unicode-range: U+1F00-1FFF;
} }
@@ -244,7 +244,7 @@
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype');
unicode-range: U+0370-03FF; unicode-range: U+0370-03FF;
} }
@@ -254,7 +254,7 @@
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
} }
@@ -264,7 +264,7 @@
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
} }
@@ -274,7 +274,7 @@
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf'); src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }
@@ -284,7 +284,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf'); src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
} }
@@ -294,7 +294,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf'); src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
} }
@@ -304,7 +304,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf'); src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype');
unicode-range: U+1F00-1FFF; unicode-range: U+1F00-1FFF;
} }
@@ -314,7 +314,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf'); src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype');
unicode-range: U+0370-03FF; unicode-range: U+0370-03FF;
} }
@@ -324,7 +324,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf'); src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
} }
@@ -334,6 +334,6 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf'); src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }
+5 -5
View File
@@ -3,16 +3,16 @@
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-50"> <div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-50">
<div class="flex h-full items-center"> <div class="flex h-full items-center">
<nuxt-link to="/"> <nuxt-link to="/">
<img src="/icon48.png" class="w-10 h-10 md:w-12 md:h-12 mr-4" /> <img src="/icon.svg" class="w-10 min-w-10 h-10 mr-2 sm:w-12 sm:min-w-12 sm:h-12 sm:mr-4" />
</nuxt-link> </nuxt-link>
<nuxt-link to="/"> <nuxt-link to="/">
<h1 class="text-2xl font-book mr-6 hidden lg:block hover:underline">audiobookshelf</h1> <h1 class="text-2xl font-book mr-6 hidden lg:block hover:underline">audiobookshelf</h1>
</nuxt-link> </nuxt-link>
<ui-libraries-dropdown /> <ui-libraries-dropdown class="mr-2" />
<controls-global-search v-if="currentLibrary" class="hidden md:block" /> <controls-global-search v-if="currentLibrary" class="" />
<div class="flex-grow" /> <div class="flex-grow" />
<span v-if="showExperimentalFeatures" class="material-icons text-4xl text-warning pr-0 sm:pr-2 md:pr-4">logo_dev</span> <span v-if="showExperimentalFeatures" class="material-icons text-4xl text-warning pr-0 sm:pr-2 md:pr-4">logo_dev</span>
@@ -20,11 +20,11 @@
<ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center"> <ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
<span class="material-icons-outlined text-warning text-opacity-50"> cast </span> <span class="material-icons-outlined text-warning text-opacity-50"> cast </span>
</ui-tooltip> </ui-tooltip>
<div v-if="isChromecastInitialized" class="w-6 h-6 mr-2 cursor-pointer"> <div v-if="isChromecastInitialized" class="w-6 min-w-6 h-6 ml-2 mr-1 sm:mx-2 cursor-pointer">
<google-cast-launcher></google-cast-launcher> <google-cast-launcher></google-cast-launcher>
</div> </div>
<nuxt-link v-if="currentLibrary" to="/config/stats" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1"> <nuxt-link v-if="currentLibrary" to="/config/stats" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
<span class="material-icons" aria-label="User Stats" role="button">equalizer</span> <span class="material-icons" aria-label="User Stats" role="button">equalizer</span>
</nuxt-link> </nuxt-link>
+1 -1
View File
@@ -25,7 +25,7 @@
</div> </div>
</div> </div>
<div class="absolute text-center categoryPlacard font-book transform z-30 bottom-0.5 left-4 md:left-8 w-36 rounded-md" style="height: 22px"> <div class="absolute text-center categoryPlacard font-book transform z-30 bottom-px left-4 md:left-8 w-44 rounded-md" style="height: 22px">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border"> <div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border">
<p class="transform text-sm">{{ shelf.label }}</p> <p class="transform text-sm">{{ shelf.label }}</p>
</div> </div>
+22 -4
View File
@@ -2,13 +2,19 @@
<div class="w-full h-20 md:h-10 relative"> <div class="w-full h-20 md:h-10 relative">
<div class="flex md:hidden h-10 items-center"> <div class="flex md:hidden h-10 items-center">
<nuxt-link :to="`/library/${currentLibraryId}`" class="flex-grow h-full flex justify-center items-center" :class="homePage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> <nuxt-link :to="`/library/${currentLibraryId}`" class="flex-grow h-full flex justify-center items-center" :class="homePage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p>Home</p> <p class="text-sm">Home</p>
</nuxt-link> </nuxt-link>
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="flex-grow h-full flex justify-center items-center" :class="showLibrary ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> <nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="flex-grow h-full flex justify-center items-center" :class="showLibrary ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p>Library</p> <p class="text-sm">Library</p>
</nuxt-link> </nuxt-link>
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf/series`" class="flex-grow h-full flex justify-center items-center" :class="paramId === 'series' ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> <nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="flex-grow h-full flex justify-center items-center" :class="paramId === 'series' ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p>Series</p> <p class="text-sm">Series</p>
</nuxt-link>
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p class="text-sm">Collections</p>
</nuxt-link>
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p class="text-sm">Search</p>
</nuxt-link> </nuxt-link>
</div> </div>
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-30 flex items-center justify-end md:justify-start px-2 md:px-8"> <div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-30 flex items-center justify-end md:justify-start px-2 md:px-8">
@@ -98,6 +104,9 @@ export default {
} }
}, },
computed: { computed: {
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
userCanDelete() { userCanDelete() {
return this.$store.getters['user/getUserCanDelete'] return this.$store.getters['user/getUserCanDelete']
}, },
@@ -129,6 +138,12 @@ export default {
currentLibraryId() { currentLibraryId() {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
}, },
currentLibraryMediaType() {
return this.$store.getters['libraries/getCurrentLibraryMediaType']
},
isPodcastLibrary() {
return this.currentLibraryMediaType === 'podcast'
},
homePage() { homePage() {
return this.$route.name === 'library-library' return this.$route.name === 'library-library'
}, },
@@ -156,6 +171,9 @@ export default {
}, },
isIssuesFilter() { isIssuesFilter() {
return this.filterBy === 'issues' && this.$route.query.filter === 'issues' return this.filterBy === 'issues' && this.$route.query.filter === 'issues'
},
isPodcastSearchPage() {
return this.$route.name === 'library-library-podcast-search'
} }
}, },
methods: { methods: {
+16 -6
View File
@@ -4,19 +4,21 @@
<span class="material-icons text-2xl">arrow_back</span> <span class="material-icons text-2xl">arrow_back</span>
</div> </div>
<nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-4 h-12 border-b border-opacity-0 flex items-center cursor-pointer relative" :class="routeName === route.id ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'"> <nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-4 h-12 border-b border-primary border-opacity-30 flex items-center cursor-pointer relative" :class="routeName === route.id ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
<p>{{ route.title }}</p> <p>{{ route.title }}</p>
<div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> <div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> </nuxt-link>
<div class="w-full h-12 px-4 border-t border-black border-opacity-20 absolute left-0 flex flex-col justify-center" :style="{ bottom: streamLibraryItem && isMobileLandscape ? '300px' : '65px' }"> <div class="w-full h-12 px-4 border-t border-black border-opacity-20 absolute left-0 flex flex-col justify-center" :style="{ bottom: streamLibraryItem && isMobileLandscape ? '300px' : '65px' }">
<div class="flex justify-between"> <div class="flex justify-between">
<p class="font-mono text-sm">v{{ $config.version }}</p> <p class="underline font-mono text-sm" @click="clickChangelog">v{{ $config.version }}</p>
<p class="font-mono text-xs text-gray-300 italic">{{ Source }}</p> <p class="font-mono text-xs text-gray-300 italic">{{ Source }}</p>
</div> </div>
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ latestVersion }}</a> <a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ latestVersion }}</a>
</div> </div>
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version"/>
</div> </div>
</template> </template>
@@ -26,7 +28,9 @@ export default {
isOpen: Boolean isOpen: Boolean
}, },
data() { data() {
return {} return {
showChangelogModal: false
}
}, },
computed: { computed: {
Source() { Source() {
@@ -66,7 +70,7 @@ export default {
}, },
{ {
id: 'config-sessions', id: 'config-sessions',
title: 'Sessions', title: 'Listening Sessions',
path: '/config/sessions' path: '/config/sessions'
}, },
{ {
@@ -76,7 +80,7 @@ export default {
}, },
{ {
id: 'config-log', id: 'config-log',
title: 'Log', title: 'Logs',
path: '/config/log' path: '/config/log'
} }
] ]
@@ -129,18 +133,24 @@ export default {
githubTagUrl() { githubTagUrl() {
return this.versionData.githubTagUrl return this.versionData.githubTagUrl
}, },
currentVersionChangelog() {
return this.versionData.currentVersionChangelog || 'No Changelog Available'
},
streamLibraryItem() { streamLibraryItem() {
return this.$store.state.streamLibraryItem return this.$store.state.streamLibraryItem
} }
}, },
methods: { methods: {
clickChangelog(){
this.showChangelogModal = true
},
clickOutside() { clickOutside() {
if (!this.isOpen) return if (!this.isOpen) return
this.closeDrawer() this.closeDrawer()
}, },
closeDrawer() { closeDrawer() {
this.$emit('update:isOpen', false) this.$emit('update:isOpen', false)
} },
} }
} }
</script> </script>
+1
View File
@@ -88,6 +88,7 @@ export default {
if (this.page === 'collections') return "You haven't made any collections yet" if (this.page === 'collections') return "You haven't made any collections yet"
if (this.hasFilter) { if (this.hasFilter) {
if (this.filterName === 'Issues') return 'No Issues' if (this.filterName === 'Issues') return 'No Issues'
else if (this.filterName === 'Feed-open') return 'No RSS feeds are open'
return `No Results for filter "${this.filterName}: ${this.filterValue}"` return `No Results for filter "${this.filterName}: ${this.filterValue}"`
} }
return 'No results' return 'No results'
+14 -6
View File
@@ -75,17 +75,21 @@
</nuxt-link> </nuxt-link>
<div class="w-full h-12 px-1 py-2 border-t border-black border-opacity-20 absolute left-0" :style="{ bottom: streamLibraryItem ? '240px' : '65px' }"> <div class="w-full h-12 px-1 py-2 border-t border-black border-opacity-20 absolute left-0" :style="{ bottom: streamLibraryItem ? '240px' : '65px' }">
<p class="font-mono text-xs text-center text-gray-300 leading-3 mb-1">v{{ $config.version }}</p> <p class="underline font-mono text-xs text-center text-gray-300 leading-3 mb-1" @click="clickChangelog">v{{ $config.version }}</p>
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xxs text-center block leading-3">Update</a> <a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xxs text-center block leading-3">Update</a>
<p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p> <p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p>
</div> </div>
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version"/>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return {} return {
showChangelogModal: false
}
}, },
computed: { computed: {
Source() { Source() {
@@ -150,17 +154,21 @@ export default {
hasUpdate() { hasUpdate() {
return !!this.versionData.hasUpdate return !!this.versionData.hasUpdate
}, },
latestVersion() {
return this.versionData.latestVersion
},
githubTagUrl() { githubTagUrl() {
return this.versionData.githubTagUrl return this.versionData.githubTagUrl
}, },
currentVersionChangelog() {
return this.versionData.currentVersionChangelog || 'No Changelog Available'
},
streamLibraryItem() { streamLibraryItem() {
return this.$store.state.streamLibraryItem return this.$store.state.streamLibraryItem
} }
}, },
methods: {}, methods: {
clickChangelog(){
this.showChangelogModal = true
}
},
mounted() {} mounted() {}
} }
</script> </script>
+20 -10
View File
@@ -1,30 +1,30 @@
<template> <template>
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-40 bg-primary px-4 pb-1 md:pb-4 pt-2"> <div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-40 bg-primary px-4 pb-1 md:pb-4 pt-2">
<div id="videoDock" /> <div id="videoDock" />
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-4 cursor-pointer" :style="{ top: bookCoverPosTop + 'px' }"> <nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-1 sm:left-4 cursor-pointer" :style="{ top: bookCoverPosTop + 'px' }">
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</nuxt-link> </nuxt-link>
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : 'pl-24'"> <div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : 'pl-20 sm:pl-24'">
<div> <div>
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-base sm:text-lg"> <nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg">
{{ title }} {{ title }}
</nuxt-link> </nuxt-link>
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center"> <div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center">
<span class="material-icons text-sm">person</span> <span class="material-icons text-sm">person</span>
<p v-if="podcastAuthor">{{ podcastAuthor }}</p> <p v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</p>
<p v-else-if="authors.length" class="pl-1.5 text-sm sm:text-base"> <p v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base">
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link> <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
</p> </p>
<p v-else class="text-sm sm:text-base cursor-pointer pl-2">Unknown</p> <p v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">Unknown</p>
</div> </div>
<div class="text-gray-400 flex items-center"> <div class="text-gray-400 flex items-center">
<span class="material-icons text-xs">schedule</span> <span class="material-icons text-xs">schedule</span>
<p class="font-mono text-sm pl-2 pb-px">{{ totalDurationPretty }}</p> <p class="font-mono text-xs sm:text-sm pl-1 sm:pl-1.5 pb-px">{{ totalDurationPretty }}</p>
</div> </div>
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
<span class="material-icons px-2 py-1 md:p-4 cursor-pointer" @click="closePlayer">close</span> <span class="material-icons sm:px-2 py-1 md:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</span>
</div> </div>
<player-ui <player-ui
ref="audioPlayer" ref="audioPlayer"
@@ -364,7 +364,11 @@ export default {
var episodeId = payload.episodeId || null var episodeId = payload.episodeId || null
if (this.playerHandler.libraryItemId == libraryItemId && this.playerHandler.episodeId == episodeId) { if (this.playerHandler.libraryItemId == libraryItemId && this.playerHandler.episodeId == episodeId) {
this.playerHandler.play() if (payload.startTime !== null && !isNaN(payload.startTime)) {
this.seek(payload.startTime)
} else {
this.playerHandler.play()
}
return return
} }
@@ -377,7 +381,11 @@ export default {
libraryItem, libraryItem,
episodeId episodeId
}) })
this.playerHandler.load(libraryItem, episodeId, true, this.initialPlaybackRate) this.$nextTick(() => {
if (this.$refs.audioPlayer) this.$refs.audioPlayer.checkUpdateChapterTrack()
})
this.playerHandler.load(libraryItem, episodeId, true, this.initialPlaybackRate, payload.startTime)
}, },
pauseItem() { pauseItem() {
this.playerHandler.pause() this.playerHandler.pause()
@@ -389,11 +397,13 @@ export default {
}, },
mounted() { mounted() {
this.$eventBus.$on('cast-session-active', this.castSessionActive) this.$eventBus.$on('cast-session-active', this.castSessionActive)
this.$eventBus.$on('playback-seek', this.seek)
this.$eventBus.$on('play-item', this.playLibraryItem) this.$eventBus.$on('play-item', this.playLibraryItem)
this.$eventBus.$on('pause-item', this.pauseItem) this.$eventBus.$on('pause-item', this.pauseItem)
}, },
beforeDestroy() { beforeDestroy() {
this.$eventBus.$off('cast-session-active', this.castSessionActive) this.$eventBus.$off('cast-session-active', this.castSessionActive)
this.$eventBus.$off('playback-seek', this.seek)
this.$eventBus.$off('play-item', this.playLibraryItem) this.$eventBus.$off('play-item', this.playLibraryItem)
this.$eventBus.$off('pause-item', this.pauseItem) this.$eventBus.$off('pause-item', this.pauseItem)
} }
+23 -12
View File
@@ -1,16 +1,27 @@
<template> <template>
<div class="w-full border-b border-gray-700 pb-2"> <div v-if="book" class="w-full border-b border-gray-700 pb-2">
<div class="flex py-1 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer" @click="selectMatch"> <div class="flex py-1 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer" @click="selectMatch">
<div class="h-24 bg-primary" :style="{ minWidth: 96 / bookCoverAspectRatio + 'px' }"> <div class="min-w-12 max-w-12 md:min-w-20 md:max-w-20">
<img v-if="selectedCover" :src="selectedCover" class="h-full w-full object-contain" /> <div class="w-full bg-primary">
</div> <img v-if="selectedCover" :src="selectedCover" class="h-full w-full object-contain" />
<div v-if="!isPodcast" class="px-4 flex-grow"> </div>
<div class="flex items-center"> </div>
<h1>{{ book.title }}</h1> <div v-if="!isPodcast" class="px-2 md:px-4 flex-grow">
<div class="flex-grow" /> <div class="flex items-center">
<p>{{ book.publishedYear }}</p> <h1 class="text-sm md:text-base">{{ book.title }}</h1>
<div class="flex-grow" />
<p class="text-sm md:text-base">{{ book.publishedYear }}</p>
</div>
<p class="text-gray-300 text-xs md:text-sm">by {{ book.author }}</p>
<p v-if="book.narrator" class="text-gray-400 text-xs">Narrated by {{ book.narrator }}</p>
<p v-if="book.duration" class="text-gray-400 text-xs">Runtime: {{ $elapsedPrettyExtended(book.duration) }}</p>
<div v-if="book.series && book.series.length" class="flex py-1 -mx-1">
<div v-for="(series, index) in book.series" :key="index" class="bg-white bg-opacity-10 rounded-full px-1 py-0.5 mx-1">
<p class="leading-3 text-xs text-gray-400">
{{ series.series }}<span v-if="series.volumeNumber">&nbsp;#{{ series.volumeNumber }}</span>
</p>
</div>
</div> </div>
<p class="text-gray-400">{{ book.author }}</p>
<div class="w-full max-h-12 overflow-hidden"> <div class="w-full max-h-12 overflow-hidden">
<p class="text-gray-500 text-xs">{{ book.description }}</p> <p class="text-gray-500 text-xs">{{ book.description }}</p>
</div> </div>
@@ -18,7 +29,7 @@
<div v-else class="px-4 flex-grow"> <div v-else class="px-4 flex-grow">
<h1>{{ book.title }}</h1> <h1>{{ book.title }}</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">by {{ book.author }}</p>
<p class="text-xs text-gray-400 leading-5">{{ book.genres.join(', ') }}</p> <p v-if="book.genres" class="text-xs text-gray-400 leading-5">{{ book.genres.join(', ') }}</p>
<p class="text-xs text-gray-400 leading-5">{{ book.trackCount }} Episodes</p> <p class="text-xs text-gray-400 leading-5">{{ book.trackCount }} Episodes</p>
</div> </div>
</div> </div>
@@ -56,7 +67,7 @@ export default {
selectMatch() { selectMatch() {
var book = { ...this.book } var book = { ...this.book }
book.cover = this.selectedCover book.cover = this.selectedCover
this.$emit('select', this.book) this.$emit('select', book)
}, },
clickCover(cover) { clickCover(cover) {
this.selectedCover = cover this.selectedCover = cover
+3 -14
View File
@@ -62,21 +62,10 @@ export default {
matchHtml() { matchHtml() {
if (!this.matchText || !this.search) return '' if (!this.matchText || !this.search) return ''
if (this.matchKey === 'subtitle') return '' if (this.matchKey === 'subtitle') return ''
var matchSplit = this.matchText.toLowerCase().split(this.search.toLowerCase().trim())
if (matchSplit.length < 2) return ''
var html = '' // This used to highlight the part of the search found
var totalLenSoFar = 0 // but with removing commas periods etc this is no longer plausible
for (let i = 0; i < matchSplit.length - 1; i++) { const html = this.matchText
var indexOf = matchSplit[i].length
var firstPart = this.matchText.substr(totalLenSoFar, indexOf)
var actualWasThere = this.matchText.substr(totalLenSoFar + indexOf, this.search.length)
totalLenSoFar += indexOf + this.search.length
html += `${firstPart}<strong class="text-warning">${actualWasThere}</strong>`
}
var lastPart = this.matchText.substr(totalLenSoFar)
html += lastPart
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>` if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
if (this.matchKey === 'authors') return `by ${html}` if (this.matchKey === 'authors') return `by ${html}`
+36 -8
View File
@@ -78,6 +78,10 @@
</div> </div>
</ui-tooltip> </ui-tooltip>
<div v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" :style="{ padding: 0.375 * sizeMultiplier + 'rem' }">
<span class="material-icons" :style="{ fontSize: sizeMultiplier * 1.5 + 'rem' }">rss_feed</span>
</div>
<!-- Series sequence --> <!-- 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 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> <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequence }}</p>
@@ -144,6 +148,9 @@ export default {
} }
}, },
computed: { computed: {
dateFormat() {
return this.store.state.serverSettings.dateFormat
},
showExperimentalFeatures() { showExperimentalFeatures() {
return this.store.state.showExperimentalFeatures return this.store.state.showExperimentalFeatures
}, },
@@ -245,13 +252,15 @@ export default {
return this.mediaMetadata.authorNameLF return this.mediaMetadata.authorNameLF
}, },
displayTitle() { displayTitle() {
if (this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix) { if (this.recentEpisode) return this.recentEpisode.title
return this.mediaMetadata.titleIgnorePrefix const ignorePrefix = this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix
} if (this.collapsedSeries) return ignorePrefix ? this.collapsedSeries.nameIgnorePrefix : this.collapsedSeries.name
return this.title return ignorePrefix ? this.mediaMetadata.titleIgnorePrefix : this.title
}, },
displayLineTwo() { displayLineTwo() {
if (this.recentEpisode) return this.title
if (this.isPodcast) return this.author if (this.isPodcast) return this.author
if (this.collapsedSeries) return ''
if (this.isAuthorBookshelfView) { if (this.isAuthorBookshelfView) {
return this.mediaMetadata.publishedYear || '' return this.mediaMetadata.publishedYear || ''
} }
@@ -259,9 +268,10 @@ export default {
return this.author return this.author
}, },
displaySortLine() { displaySortLine() {
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs) if (this.collapsedSeries) return null
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs) if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt) if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt, this.dateFormat)
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false) if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size) if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes` if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
@@ -438,6 +448,10 @@ export default {
if (!this.isAlternativeBookshelfView && !this.isAuthorBookshelfView) return 0 if (!this.isAlternativeBookshelfView && !this.isAuthorBookshelfView) return 0
else if (!this.displaySortLine) return 3 * this.sizeMultiplier else if (!this.displaySortLine) return 3 * this.sizeMultiplier
return 4.25 * this.sizeMultiplier return 4.25 * this.sizeMultiplier
},
rssFeed() {
if (this.booksInSeries) return null
return this.store.getters['feeds/getFeedForItem'](this.libraryItemId)
} }
}, },
methods: { methods: {
@@ -494,7 +508,21 @@ export default {
} }
this.$emit('edit', this.libraryItem) this.$emit('edit', this.libraryItem)
}, },
toggleFinished() { toggleFinished(confirmed = false) {
if (!this.itemIsFinished && this.userProgressPercent > 0 && !confirmed) {
const payload = {
message: `Are you sure you want to mark "${this.displayTitle}" as finished?`,
callback: (confirmed) => {
if (confirmed) {
this.toggleFinished(true)
}
},
type: 'yesNo'
}
this.store.commit('globals/setConfirmPrompt', payload)
return
}
var updatePayload = { var updatePayload = {
isFinished: !this.itemIsFinished isFinished: !this.itemIsFinished
} }
+13 -5
View File
@@ -2,7 +2,7 @@
<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 ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" /> <div class="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"> <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="title" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" :group-to="seriesBooksRoute" /> <covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" :group-to="seriesBooksRoute" />
</div> </div>
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ books.length }}</div> <div 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>
@@ -10,16 +10,16 @@
<div v-if="isSeriesFinished" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b bg-success w-full" /> <div v-if="isSeriesFinished" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b bg-success w-full" />
<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 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 class="font-book" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ title }}</p> <p class="font-book" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
</div> </div>
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }"> <div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }"> <div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p> <p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
</div> </div>
</div> </div>
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center"> <div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p> <p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
</div> </div>
</div> </div>
</template> </template>
@@ -39,7 +39,8 @@ export default {
seriesMount: { seriesMount: {
type: Object, type: Object,
default: () => null default: () => null
} },
sortingIgnorePrefix: Boolean
}, },
data() { data() {
return { return {
@@ -65,6 +66,13 @@ export default {
title() { title() {
return this.series ? this.series.name : '' return this.series ? this.series.name : ''
}, },
nameIgnorePrefix() {
return this.series ? this.series.nameIgnorePrefix : ''
},
displayTitle() {
if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title
return this.title
},
books() { books() {
return this.series ? this.series.books || [] : [] return this.series ? this.series.books || [] : []
}, },
@@ -116,6 +116,11 @@ export default {
text: 'Issues', text: 'Issues',
value: 'issues', value: 'issues',
sublist: false sublist: false
},
{
text: 'RSS Feed Open',
value: 'feed-open',
sublist: false
} }
], ],
podcastItems: [ podcastItems: [
+10 -7
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="w-80 ml-6 relative"> <div class="sm:w-80 w-full relative">
<form @submit.prevent="submitSearch"> <form @submit.prevent="submitSearch">
<ui-text-input ref="input" v-model="search" placeholder="Search.." @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" /> <ui-text-input ref="input" v-model="search" placeholder="Search.." @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
</form> </form>
@@ -7,7 +7,7 @@
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span> <span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
<span v-else class="material-icons" style="font-size: 1.2rem">close</span> <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-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-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">
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label"> <ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<li v-if="isTyping" class="py-2 px-2"> <li v-if="isTyping" class="py-2 px-2">
<p>Thinking...</p> <p>Thinking...</p>
@@ -21,7 +21,7 @@
<template v-else> <template v-else>
<p v-if="bookResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Books</p> <p v-if="bookResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Books</p>
<template v-for="item in bookResults"> <template v-for="item in bookResults">
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option"> <li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
<nuxt-link :to="`/item/${item.libraryItem.id}`"> <nuxt-link :to="`/item/${item.libraryItem.id}`">
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" /> <cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
</nuxt-link> </nuxt-link>
@@ -30,7 +30,7 @@
<p v-if="podcastResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Podcasts</p> <p v-if="podcastResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Podcasts</p>
<template v-for="item in podcastResults"> <template v-for="item in podcastResults">
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option"> <li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
<nuxt-link :to="`/item/${item.libraryItem.id}`"> <nuxt-link :to="`/item/${item.libraryItem.id}`">
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" /> <cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
</nuxt-link> </nuxt-link>
@@ -39,7 +39,7 @@
<p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Authors</p> <p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Authors</p>
<template v-for="item in authorResults"> <template v-for="item in authorResults">
<li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option"> <li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(item.id)}`"> <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(item.id)}`">
<cards-author-search-card :author="item" /> <cards-author-search-card :author="item" />
</nuxt-link> </nuxt-link>
@@ -48,7 +48,7 @@
<p v-if="seriesResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Series</p> <p v-if="seriesResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Series</p>
<template v-for="item in seriesResults"> <template v-for="item in seriesResults">
<li :key="item.series.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option"> <li :key="item.series.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
<nuxt-link :to="`/library/${currentLibraryId}/series/${item.series.id}`"> <nuxt-link :to="`/library/${currentLibraryId}/series/${item.series.id}`">
<cards-series-search-card :series="item.series" :book-items="item.books" /> <cards-series-search-card :series="item.series" :book-items="item.books" />
</nuxt-link> </nuxt-link>
@@ -57,7 +57,7 @@
<p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Tags</p> <p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Tags</p>
<template v-for="item in tagResults"> <template v-for="item in tagResults">
<li :key="item.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option"> <li :key="item.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(item.name)}`"> <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(item.name)}`">
<cards-tag-search-card :tag="item.name" /> <cards-tag-search-card :tag="item.name" />
</nuxt-link> </nuxt-link>
@@ -97,6 +97,9 @@ export default {
} }
}, },
methods: { methods: {
clickOption() {
this.clearResults()
},
submitSearch() { submitSearch() {
if (!this.search) return if (!this.search) return
var search = this.search var search = this.search
@@ -1,10 +1,10 @@
<template> <template>
<div class="relative ml-8" v-click-outside="clickOutside"> <div ref="wrapper" class="relative ml-4 sm:ml-8" v-click-outside="clickOutside">
<div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="setShowMenu(true)"> <div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="setShowMenu(true)">
<span class="font-mono uppercase text-gray-200">{{ playbackRate.toFixed(1) }}<span class="text-lg"></span></span> <span class="font-mono uppercase text-gray-200 text-sm sm:text-base">{{ playbackRate.toFixed(1) }}<span class="text-base sm:text-lg"></span></span>
</div> </div>
<div v-show="showMenu" class="absolute -top-20 left-0 z-20 bg-bg border-black-200 border shadow-xl rounded-lg" style="left: -92px"> <div v-show="showMenu" class="absolute -top-20 z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }">
<div class="absolute -bottom-2 left-0 right-0 w-full flex justify-center"> <div class="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }">
<div class="arrow-down" /> <div class="arrow-down" />
</div> </div>
<div class="flex items-center h-9 relative overflow-hidden rounded-lg" style="width: 220px"> <div class="flex items-center h-9 relative overflow-hidden rounded-lg" style="width: 220px">
@@ -19,7 +19,7 @@
<div class="w-full py-1 px-4"> <div class="w-full py-1 px-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" /> <ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" />
<p class="px-2 text-3xl">{{ playbackRate }}<span class="text-2xl"></span></p> <p class="px-2 text-2xl sm:text-3xl">{{ playbackRate }}<span class="text-2xl"></span></p>
<ui-icon-btn :disabled="!canIncrement" icon="add" @click="increment" /> <ui-icon-btn :disabled="!canIncrement" icon="add" @click="increment" />
</div> </div>
</div> </div>
@@ -40,7 +40,9 @@ export default {
showMenu: false, showMenu: false,
currentPlaybackRate: 0, currentPlaybackRate: 0,
MIN_SPEED: 0.5, MIN_SPEED: 0.5,
MAX_SPEED: 3 MAX_SPEED: 3,
menuLeft: -92,
arrowLeft: 0
} }
}, },
computed: { computed: {
@@ -80,8 +82,22 @@ export default {
var newPlaybackRate = this.playbackRate - 0.1 var newPlaybackRate = this.playbackRate - 0.1
this.playbackRate = Number(newPlaybackRate.toFixed(1)) this.playbackRate = Number(newPlaybackRate.toFixed(1))
}, },
updateMenuPositions() {
if (!this.$refs.wrapper) return
const boundingBox = this.$refs.wrapper.getBoundingClientRect()
if (boundingBox.left + 110 > window.innerWidth - 10) {
this.menuLeft = window.innerWidth - 230 - boundingBox.left
this.arrowLeft = Math.abs(this.menuLeft) - 92
} else {
this.menuLeft = -92
this.arrowLeft = 0
}
},
setShowMenu(val) { setShowMenu(val) {
if (val) { if (val) {
this.updateMenuPositions()
this.currentPlaybackRate = this.playbackRate this.currentPlaybackRate = this.playbackRate
} else if (this.currentPlaybackRate !== this.playbackRate) { } else if (this.currentPlaybackRate !== this.playbackRate) {
this.$emit('change', this.playbackRate) this.$emit('change', this.playbackRate)
+2 -2
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="relative" v-click-outside="clickOutside" @mouseover="mouseover" @mouseleave="mouseleave"> <div class="relative" v-click-outside="clickOutside" @mouseover="mouseover" @mouseleave="mouseleave">
<div class="cursor-pointer" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon"> <div class="cursor-pointer text-gray-300 hover:text-white" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon">
<span class="material-icons text-3xl">{{ volumeIcon }}</span> <span class="material-icons text-2xl sm:text-3xl">{{ volumeIcon }}</span>
</div> </div>
<transition name="menux"> <transition name="menux">
<div v-show="isOpen" class="volumeMenu h-6 absolute bottom-2 w-28 px-2 bg-bg shadow-sm rounded-lg" style="left: -116px"> <div v-show="isOpen" class="volumeMenu h-6 absolute bottom-2 w-28 px-2 bg-bg shadow-sm rounded-lg" style="left: -116px">
+3
View File
@@ -125,6 +125,9 @@ export default {
}, },
userToken() { userToken() {
return this.$store.getters['user/getToken'] return this.$store.getters['user/getToken']
},
resolution() {
return `${this.naturalWidth}x${this.naturalHeight}px`
} }
}, },
methods: { methods: {
+7 -2
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="relative rounded-sm overflow-hidden" :style="{ height: width * bookCoverAspectRatio + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }" @mouseover="isHovering = true" @mouseleave="isHovering = false"> <div class="relative rounded-sm" :style="{ height: width * bookCoverAspectRatio + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }" @mouseover="isHovering = true" @mouseleave="isHovering = false">
<div class="w-full h-full relative"> <div class="w-full h-full relative overflow-hidden">
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary"> <div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
<div class="absolute cover-bg" ref="coverBg" /> <div class="absolute cover-bg" ref="coverBg" />
</div> </div>
@@ -17,6 +17,8 @@
<p class="text-center font-book text-error" :style="{ fontSize: sizeMultiplier + 'rem' }">Invalid Cover</p> <p class="text-center font-book text-error" :style="{ fontSize: sizeMultiplier + 'rem' }">Invalid Cover</p>
</div> </div>
</div> </div>
<p v-if="!imageFailed" class="absolute -bottom-5 left-0 right-0 mx-auto text-xs text-gray-300 text-center">{{ resolution }}</p>
</div> </div>
</template> </template>
@@ -54,6 +56,9 @@ export default {
}, },
placeholderCoverPadding() { placeholderCoverPadding() {
return 0.8 * this.sizeMultiplier return 0.8 * this.sizeMultiplier
},
resolution() {
return `${this.naturalWidth}x${this.naturalHeight}px`
} }
}, },
methods: { methods: {
+9 -2
View File
@@ -146,7 +146,6 @@ export default {
watch: { watch: {
show: { show: {
handler(newVal) { handler(newVal) {
console.log('accoutn modal show change', newVal)
if (newVal) { if (newVal) {
this.init() this.init()
} }
@@ -162,6 +161,9 @@ export default {
this.$emit('input', val) this.$emit('input', val)
} }
}, },
user() {
return this.$store.state.user.user
},
title() { title() {
return this.isNew ? 'Add New Account' : `Update ${(this.account || {}).username}` return this.isNew ? 'Add New Account' : `Update ${(this.account || {}).username}`
}, },
@@ -250,6 +252,12 @@ export default {
this.$toast.error(`Failed to update account: ${data.error}`) this.$toast.error(`Failed to update account: ${data.error}`)
} else { } else {
console.log('Account updated', data.user) console.log('Account updated', data.user)
if (data.user.id === this.user.id && data.user.token !== this.user.token) {
console.log('Current user token was updated')
this.$store.commit('user/setUserToken', data.user.token)
}
this.$toast.success('Account updated') this.$toast.success('Account updated')
this.show = false this.show = false
} }
@@ -305,7 +313,6 @@ export default {
this.isNew = !this.account this.isNew = !this.account
if (this.account) { if (this.account) {
console.log(this.account)
this.newUser = { this.newUser = {
username: this.account.username, username: this.account.username,
password: this.account.password, password: this.account.password,
+10 -4
View File
@@ -1,6 +1,11 @@
<template> <template>
<modals-modal v-model="show" name="bookmarks" :width="500" :height="'unset'"> <modals-modal v-model="show" name="bookmarks" :width="500" :height="'unset'">
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh"> <template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
<p class="font-book text-3xl text-white truncate">Your Bookmarks</p>
</div>
</template>
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<div v-if="show" class="w-full h-full"> <div v-if="show" class="w-full h-full">
<template v-for="bookmark in bookmarks"> <template v-for="bookmark in bookmarks">
<modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" @click="clickBookmark" @update="submitUpdateBookmark" @delete="deleteBookmark" /> <modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" @click="clickBookmark" @update="submitUpdateBookmark" @delete="deleteBookmark" />
@@ -8,8 +13,8 @@
<div v-if="!bookmarks.length" class="flex h-32 items-center justify-center"> <div v-if="!bookmarks.length" class="flex h-32 items-center justify-center">
<p class="text-xl">No Bookmarks</p> <p class="text-xl">No Bookmarks</p>
</div> </div>
<div class="w-full h-px bg-white bg-opacity-10" /> <div v-if="!hideCreate" class="w-full h-px bg-white bg-opacity-10" />
<form @submit.prevent="submitCreateBookmark"> <form v-if="!hideCreate" @submit.prevent="submitCreateBookmark">
<div v-show="canCreateBookmark" class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80"> <div v-show="canCreateBookmark" class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">
<div class="w-16 max-w-16 text-center"> <div class="w-16 max-w-16 text-center">
<p class="text-sm font-mono text-gray-400"> <p class="text-sm font-mono text-gray-400">
@@ -39,7 +44,8 @@ export default {
type: Number, type: Number,
default: 0 default: 0
}, },
libraryItemId: String libraryItemId: String,
hideCreate: Boolean
}, },
data() { data() {
return { return {
+19 -5
View File
@@ -1,11 +1,14 @@
<template> <template>
<modals-modal v-model="show" name="chapters" :width="500" :height="'unset'"> <modals-modal v-model="show" name="chapters" :width="600" :height="'unset'">
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh"> <div id="chapter-modal-wrapper" ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<template v-for="chap in chapters"> <template v-for="chap in chapters">
<div :key="chap.id" :id="`chapter-row-${chap.id}`" class="flex items-center px-6 py-3 justify-start cursor-pointer hover:bg-bg relative" :class="chap.id === currentChapterId ? 'bg-yellow-400 bg-opacity-10' : chap.end <= currentChapterStart ? 'bg-success bg-opacity-5' : 'bg-opacity-20'" @click="clickChapter(chap)"> <div :key="chap.id" :id="`chapter-row-${chap.id}`" class="flex items-center px-6 py-3 justify-start cursor-pointer hover:bg-bg relative" :class="chap.id === currentChapterId ? 'bg-yellow-400 bg-opacity-10' : chap.end <= currentChapterStart ? 'bg-success bg-opacity-5' : 'bg-opacity-20'" @click="clickChapter(chap)">
{{ chap.title }} <p class="chapter-title truncate text-sm md:text-base">
{{ chap.title }}
</p>
<span class="font-mono text-xxs sm:text-xs text-gray-400 pl-2 whitespace-nowrap">{{ $elapsedPrettyExtended(chap.end - chap.start) }}</span>
<span class="flex-grow" /> <span class="flex-grow" />
<span class="font-mono text-sm text-gray-300">{{ $secondsToTimestamp(chap.start) }}</span> <span class="font-mono text-xs sm:text-sm text-gray-300">{{ $secondsToTimestamp(chap.start) }}</span>
<div v-show="chap.id === currentChapterId" class="w-0.5 h-full absolute top-0 left-0 bg-yellow-400" /> <div v-show="chap.id === currentChapterId" class="w-0.5 h-full absolute top-0 left-0 bg-yellow-400" />
</div> </div>
@@ -70,4 +73,15 @@ export default {
} }
} }
} }
</script> </script>
<style>
#chapter-modal-wrapper .chapter-title {
max-width: calc(100% - 120px);
}
@media (min-width: 640px) {
#chapter-modal-wrapper .chapter-title {
max-width: calc(100% - 150px);
}
}
</style>
+56
View File
@@ -0,0 +1,56 @@
<template>
<modals-modal v-model="show" :width="300" height="100%">
<template #outer>
<div v-if="title" class="absolute top-7 left-4 z-40" style="max-width: 80%">
<p class="text-white text-lg truncate">{{ title }}</p>
</div>
</template>
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in items">
<li :key="item.value" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" :class="selected === item.value ? 'bg-success bg-opacity-10' : ''" role="option" @click="clickedOption(item.value)">
<div class="relative flex items-center px-3">
<p class="font-normal block truncate text-base text-white text-opacity-80">{{ item.text }}</p>
</div>
</li>
</template>
</ul>
</div>
</div>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean,
title: String,
items: {
type: Array,
default: () => []
},
selected: String // optional
},
data() {
return {}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}
},
methods: {
clickedOption(action) {
this.$emit('action', action)
}
},
mounted() {}
}
</script>
@@ -1,17 +1,17 @@
<template> <template>
<div ref="wrapper" class="hidden absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 rounded-lg items-center justify-center" style="z-index: 51" @click="clickClose"> <div ref="wrapper" class="hidden absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 rounded-lg items-center justify-center" style="z-index: 51" @click="clickClose">
<div class="absolute top-5 right-5 h-12 w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300"> <div class="absolute top-3 right-3 md:top-5 md:right-5 h-8 w-8 md:h-12 md:w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300">
<span class="material-icons text-4xl">close</span> <span class="material-icons text-2xl md:text-4xl">close</span>
</div> </div>
<div ref="content" class="text-white"> <div ref="content" class="text-white">
<form v-if="selectedSeries" @submit.prevent="submitSeriesForm"> <form v-if="selectedSeries" @submit.prevent="submitSeriesForm">
<div class="bg-bg rounded-lg p-8" @click.stop> <div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop>
<div class="flex"> <div class="flex">
<div class="flex-grow p-1 min-w-80"> <div class="flex-grow p-1 min-w-48 sm:min-w-64 md:min-w-80">
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!selectedSeries.id.startsWith('new')" label="Series Name" /> <ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!isNewSeries" label="Series Name" />
</div> </div>
<div class="w-40 p-1"> <div class="w-24 sm:w-28 md:w-40 p-1">
<ui-text-input-with-label v-model="selectedSeries.sequence" label="Sequence" /> <ui-text-input-with-label ref="sequenceInput" v-model="selectedSeries.sequence" label="Sequence" />
</div> </div>
</div> </div>
<div class="flex justify-end mt-2 p-1"> <div class="flex justify-end mt-2 p-1">
@@ -59,9 +59,26 @@ export default {
set(val) { set(val) {
this.$emit('input', val) this.$emit('input', val)
} }
},
isNewSeries() {
if (!this.selectedSeries || !this.selectedSeries.id) return false
return this.selectedSeries.id.startsWith('new')
} }
}, },
methods: { methods: {
setInputFocus() {
if (this.isNewSeries) {
// Focus on series input if new series
if (this.$refs.newSeriesSelect) {
this.$refs.newSeriesSelect.setFocus()
}
} else {
// Focus on sequence input if existing series
if (this.$refs.sequenceInput) {
this.$refs.sequenceInput.setFocus()
}
}
},
submitSeriesForm() { submitSeriesForm() {
if (this.$refs.newSeriesSelect) { if (this.$refs.newSeriesSelect) {
this.$refs.newSeriesSelect.blur() this.$refs.newSeriesSelect.blur()
@@ -89,15 +106,15 @@ export default {
setTimeout(() => { setTimeout(() => {
this.content.style.transform = 'scale(1)' this.content.style.transform = 'scale(1)'
}, 10) }, 10)
document.documentElement.classList.add('modal-open')
this.$store.commit('setInnerModalOpen', true) this.$store.commit('setInnerModalOpen', true)
this.$eventBus.$on('modal-hotkey', this.hotkey) this.$eventBus.$on('modal-hotkey', this.hotkey)
this.setInputFocus()
}, },
setHide() { setHide() {
if (this.content) this.content.style.transform = 'scale(0)' if (this.content) this.content.style.transform = 'scale(0)'
if (this.el) this.el.remove() if (this.el) this.el.remove()
document.documentElement.classList.remove('modal-open')
this.$store.commit('setInnerModalOpen', false) this.$store.commit('setInnerModalOpen', false)
this.$eventBus.$off('modal-hotkey', this.hotkey) this.$eventBus.$off('modal-hotkey', this.hotkey)
@@ -5,7 +5,7 @@
<p class="font-book text-3xl text-white truncate">Session {{ _session.id }}</p> <p class="font-book text-3xl text-white truncate">Session {{ _session.id }}</p>
</div> </div>
</template> </template>
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden p-6" style="max-height: 80vh"> <div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden p-6" style="max-height: 80vh">
<div class="flex items-center"> <div class="flex items-center">
<p class="text-base text-gray-200">{{ _session.displayTitle }}</p> <p class="text-base text-gray-200">{{ _session.displayTitle }}</p>
<p v-if="_session.displayAuthor" class="text-xs text-gray-400 px-4">by {{ _session.displayAuthor }}</p> <p v-if="_session.displayAuthor" class="text-xs text-gray-400 px-4">by {{ _session.displayAuthor }}</p>
+15 -6
View File
@@ -2,11 +2,11 @@
<div ref="wrapper" class="modal modal-bg w-full h-full fixed top-0 left-0 bg-primary items-center justify-center opacity-0 hidden" :class="`z-${zIndex} bg-opacity-${bgOpacity}`"> <div ref="wrapper" class="modal modal-bg w-full h-full fixed top-0 left-0 bg-primary items-center justify-center opacity-0 hidden" :class="`z-${zIndex} bg-opacity-${bgOpacity}`">
<div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" /> <div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" />
<div class="absolute top-5 right-5 h-12 w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" @click="clickClose"> <div class="absolute top-3 right-3 landscape:top-2 landscape:right-2 md:portrait:top-5 md:portrait:right-5 lg:top-5 lg:right-5 h-8 w-8 landscape:h-8 landscape:w-8 md:portrait:h-12 md:portrait:w-12 lg:w-12 lg:h-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" @click="clickClose">
<span class="material-icons text-4xl">close</span> <span class="material-icons text-2xl landscape:text-2xl md:portrait:text-4xl lg:text-4xl">close</span>
</div> </div>
<slot name="outer" /> <slot name="outer" />
<div ref="content" style="min-width: 400px; min-height: 200px" class="relative text-white" :style="{ height: modalHeight, width: modalWidth, marginTop: contentMarginTop + 'px' }" @mousedown="mousedownModal" @mouseup="mouseupModal" v-click-outside="clickBg"> <div ref="content" style="min-width: 380px; min-height: 200px; max-width: 100vw" class="relative text-white" :style="{ height: modalHeight, width: modalWidth, marginTop: contentMarginTop + 'px' }" @mousedown="mousedownModal" @mouseup="mouseupModal" v-click-outside="clickBg">
<slot /> <slot />
<div v-if="processing" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-black bg-opacity-60 rounded-lg flex items-center justify-center"> <div v-if="processing" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-black bg-opacity-60 rounded-lg flex items-center justify-center">
<ui-loading-indicator /> <ui-loading-indicator />
@@ -50,7 +50,8 @@ export default {
return { return {
el: null, el: null,
content: null, content: null,
preventClickoutside: false preventClickoutside: false,
isShowingPrompt: false
} }
}, },
watch: { watch: {
@@ -93,7 +94,7 @@ export default {
this.show = false this.show = false
}, },
clickBg(ev) { clickBg(ev) {
if (!this.show) return if (!this.show || this.isShowingPrompt) return
if (this.preventClickoutside) { if (this.preventClickoutside) {
this.preventClickoutside = false this.preventClickoutside = false
return return
@@ -147,8 +148,16 @@ export default {
} else { } else {
console.warn('Invalid modal init', this.name) console.warn('Invalid modal init', this.name)
} }
},
showingPrompt(isShowing) {
this.isShowingPrompt = isShowing
} }
}, },
mounted() {} mounted() {
this.$eventBus.$on('showing-prompt', this.showingPrompt)
},
beforeDestroy() {
this.$eventBus.$off('showing-prompt', this.showingPrompt)
}
} }
</script> </script>
+11 -10
View File
@@ -25,6 +25,9 @@
<ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" /> <ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" />
</div> </div>
</div> </div>
<div class="p-2">
<ui-text-input-with-label v-model="authorCopy.imagePath" :disabled="processing" label="Photo Path/URL" />
</div>
<div class="p-2"> <div class="p-2">
<ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" label="Description" :rows="8" /> <ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" label="Description" :rows="8" />
</div> </div>
@@ -43,19 +46,13 @@
<script> <script>
export default { export default {
// props: {
// value: Boolean,
// author: {
// type: Object,
// default: () => {}
// }
// },
data() { data() {
return { return {
authorCopy: { authorCopy: {
name: '', name: '',
asin: '', asin: '',
description: '' description: '',
imagePath: ''
}, },
processing: false processing: false
} }
@@ -95,9 +92,10 @@ export default {
this.authorCopy.name = this.author.name this.authorCopy.name = this.author.name
this.authorCopy.asin = this.author.asin this.authorCopy.asin = this.author.asin
this.authorCopy.description = this.author.description this.authorCopy.description = this.author.description
this.authorCopy.imagePath = this.author.imagePath
}, },
async submitForm() { async submitForm() {
var keysToCheck = ['name', 'asin', 'description'] var keysToCheck = ['name', 'asin', 'description', 'imagePath']
var updatePayload = {} var updatePayload = {}
keysToCheck.forEach((key) => { keysToCheck.forEach((key) => {
if (this.authorCopy[key] !== this.author[key]) { if (this.authorCopy[key] !== this.author[key]) {
@@ -118,6 +116,9 @@ export default {
if (result.updated) { if (result.updated) {
this.$toast.success('Author updated') this.$toast.success('Author updated')
this.show = false this.show = false
} else if (result.merged) {
this.$toast.success('Author merged')
this.show = false
} else this.$toast.info('No updates were needed') } else this.$toast.info('No updates were needed')
} }
this.processing = false this.processing = false
@@ -167,4 +168,4 @@ export default {
mounted() {}, mounted() {},
beforeDestroy() {} beforeDestroy() {}
} }
</script> </script>
@@ -1,6 +1,5 @@
<template> <template>
<div class="flex items-center px-4 py-4 justify-start relative hover:bg-bg" :class="wrapperClass" @click="click" @mouseover="mouseover" @mouseleave="mouseleave"> <div class="flex items-center px-4 py-4 justify-start relative bg-primary hover:bg-opacity-25" :class="wrapperClass" @click.stop="click" @mouseover="mouseover" @mouseleave="mouseleave">
<!-- <span class="material-icons" :class="highlight ? 'text-success' : 'text-white text-opacity-80'">{{ highlight ? 'bookmark' : 'bookmark_border' }}</span> -->
<div class="w-16 max-w-16 text-center"> <div class="w-16 max-w-16 text-center">
<p class="text-sm font-mono text-gray-400"> <p class="text-sm font-mono text-gray-400">
{{ this.$secondsToTimestamp(bookmark.time) }} {{ this.$secondsToTimestamp(bookmark.time) }}
@@ -0,0 +1,73 @@
<template>
<modals-modal v-model="show" name="changelog" :width="800" :height="'unset'">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
<p class="font-book text-3xl text-white truncate">Changelog</p>
</div>
</template>
<div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh">
<p class="text-xl font-bold pb-4">Changelog v{{ currentVersionNumber }}</p>
<div class="custom-text" v-html="compiledMarkedown" />
</div>
</modals-modal>
</template>
<script>
import { marked } from '@/static/libs/marked/index.js'
export default {
props: {
value: Boolean,
changelog: String,
currentVersion: String
},
watch: {
show: {
immediate: true,
handler(newVal) {
if (newVal) {
this.init()
}
}
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
compiledMarkedown() {
return marked.parse(this.changelog, { gfm: true, breaks: true })
},
currentVersionNumber() {
return this.currentVersion
}
},
methods: {
init() {}
},
mounted() {}
}
</script>
<style scoped>
/*
1. we need to manually define styles to apply to the parsed markdown elements,
since we don't have access to the actual elements in this component
2. v-deep allows these to take effect on the content passed in to the v-html in the div above
*/
.custom-text ::v-deep > h2 {
@apply text-lg font-bold;
}
.custom-text ::v-deep > h3 {
@apply text-lg font-bold;
}
.custom-text ::v-deep > ul {
@apply list-disc list-inside pb-4;
}
</style>
+22 -9
View File
@@ -1,13 +1,13 @@
<template> <template>
<modals-modal v-model="show" name="edit-book" :width="800" :height="height" :processing="processing" :content-margin-top="75"> <modals-modal v-model="show" name="edit-book" :width="800" :height="height" :processing="processing" :content-margin-top="marginTop">
<template #outer> <template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none"> <div class="absolute top-0 left-0 p-4 landscape:px-4 landscape:py-2 md:portrait:p-5 lg:p-5 w-2/3 overflow-hidden pointer-events-none">
<p class="font-book text-3xl text-white truncate pointer-events-none">{{ title }}</p> <p class="font-book text-xl md:portrait:text-3xl md:landscape:text-lg lg:text-3xl text-white truncate pointer-events-none">{{ title }}</p>
</div> </div>
</template> </template>
<div class="absolute -top-10 left-0 z-10 w-full flex"> <div class="absolute -top-10 left-0 z-10 w-full flex">
<template v-for="tab in availableTabs"> <template v-for="tab in availableTabs">
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div> <div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
</template> </template>
</div> </div>
@@ -30,6 +30,8 @@ export default {
return { return {
processing: false, processing: false,
libraryItem: null, libraryItem: null,
availableHeight: 0,
marginTop: 0,
tabs: [ tabs: [
{ {
id: 'details', id: 'details',
@@ -133,8 +135,7 @@ export default {
}) })
}, },
height() { height() {
var maxHeightAllowed = window.innerHeight - 150 return Math.min(this.availableHeight, 650)
return Math.min(maxHeightAllowed, 650)
}, },
tabName() { tabName() {
var _tab = this.tabs.find((t) => t.id === this.selectedTab) var _tab = this.tabs.find((t) => t.id === this.selectedTab)
@@ -189,7 +190,6 @@ export default {
if (prevBook) { if (prevBook) {
this.unregisterListeners() this.unregisterListeners()
this.libraryItem = prevBook this.libraryItem = prevBook
this.selectedTab = 'details'
this.$store.commit('setSelectedLibraryItem', prevBook) this.$store.commit('setSelectedLibraryItem', prevBook)
this.$nextTick(this.registerListeners) this.$nextTick(this.registerListeners)
} else { } else {
@@ -209,7 +209,6 @@ export default {
if (nextBook) { if (nextBook) {
this.unregisterListeners() this.unregisterListeners()
this.libraryItem = nextBook this.libraryItem = nextBook
this.selectedTab = 'details'
this.$store.commit('setSelectedLibraryItem', nextBook) this.$store.commit('setSelectedLibraryItem', nextBook)
this.$nextTick(this.registerListeners) this.$nextTick(this.registerListeners)
} else { } else {
@@ -246,15 +245,29 @@ export default {
} }
}, },
registerListeners() { registerListeners() {
window.addEventListener('orientationchange', this.orientationChange)
this.$eventBus.$on('modal-hotkey', this.hotkey) this.$eventBus.$on('modal-hotkey', this.hotkey)
this.$eventBus.$on(`${this.selectedLibraryItemId}_updated`, this.libraryItemUpdated) this.$eventBus.$on(`${this.selectedLibraryItemId}_updated`, this.libraryItemUpdated)
}, },
unregisterListeners() { unregisterListeners() {
window.removeEventListener('orientationchange', this.orientationChange)
this.$eventBus.$off('modal-hotkey', this.hotkey) this.$eventBus.$off('modal-hotkey', this.hotkey)
this.$eventBus.$off(`${this.selectedLibraryItemId}_updated`, this.libraryItemUpdated) this.$eventBus.$off(`${this.selectedLibraryItemId}_updated`, this.libraryItemUpdated)
},
orientationChange() {
setTimeout(this.setHeight, 50)
},
setHeight() {
const smAndBelow = window.innerWidth < 1024 && window.innerWidth > window.innerHeight
this.marginTop = smAndBelow ? 90 : 75
const heightModifier = smAndBelow ? 95 : 150
this.availableHeight = window.innerHeight - heightModifier
} }
}, },
mounted() {}, mounted() {
this.setHeight()
},
beforeDestroy() { beforeDestroy() {
this.unregisterListeners() this.unregisterListeners()
} }
+9 -9
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6 relative"> <div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
<div class="flex"> <div class="flex flex-wrap">
<div class="relative"> <div class="relative">
<covers-book-cover :library-item="libraryItem" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-book-cover :library-item="libraryItem" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<!-- book cover overlay --> <!-- book cover overlay -->
@@ -11,14 +11,14 @@
</div> </div>
</div> </div>
</div> </div>
<div class="flex-grow pl-6 pr-2"> <div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-2 md:mt-0">
<div class="flex items-center"> <div class="flex items-center">
<div v-if="userCanUpload" class="w-40 pr-2 pt-4" style="min-width: 160px"> <div v-if="userCanUpload" class="w-10 md:w-40 pr-2 pt-4 md:min-w-32">
<ui-file-input ref="fileInput" @change="fileUploadSelected">Upload Cover</ui-file-input> <ui-file-input ref="fileInput" @change="fileUploadSelected"><span class="hidden md:inline-block">Upload Cover</span><span class="material-icons inline-block md:!hidden">upload</span></ui-file-input>
</div> </div>
<form @submit.prevent="submitForm" class="flex flex-grow"> <form @submit.prevent="submitForm" class="flex flex-grow">
<ui-text-input-with-label v-model="imageUrl" label="Cover Image URL" /> <ui-text-input-with-label v-model="imageUrl" label="Cover Image URL" />
<ui-btn color="success" type="submit" :padding-x="4" class="mt-5 ml-3 w-24">Update</ui-btn> <ui-btn color="success" type="submit" :padding-x="4" class="mt-5 ml-2 sm:ml-3 w-24">Update</ui-btn>
</form> </form>
</div> </div>
@@ -31,9 +31,9 @@
<div v-if="showLocalCovers" class="flex items-center justify-center"> <div v-if="showLocalCovers" class="flex items-center justify-center">
<template v-for="cover in localCovers"> <template v-for="cover in localCovers">
<div :key="cover.path" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(cover)"> <div :key="cover.path" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(cover)">
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }"> <div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
<img :src="`${cover.localPath}?token=${userToken}`" class="h-full w-full object-contain" /> <covers-preview-cover :src="`${cover.localPath}?token=${userToken}`" :width="96 / bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div> </div>
</div> </div>
</template> </template>
@@ -58,7 +58,7 @@
<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 max-h-80 overflow-y-scroll mt-2 max-w-full">
<p v-if="!coversFound.length">No Covers Found</p> <p v-if="!coversFound.length">No Covers Found</p>
<template v-for="cover in coversFound"> <template v-for="cover in coversFound">
<div :key="cover" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="updateCover(cover)"> <div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="updateCover(cover)">
<covers-preview-cover :src="cover" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-preview-cover :src="cover" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div> </div>
</template> </template>
@@ -5,23 +5,24 @@
<widgets-podcast-details-edit v-else ref="itemDetailsEdit" :library-item="libraryItem" @submit="saveAndClose" /> <widgets-podcast-details-edit v-else ref="itemDetailsEdit" :library-item="libraryItem" @submit="saveAndClose" />
</div> </div>
<div class="absolute bottom-0 left-0 w-full py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'border-t border-white border-opacity-5'"> <div class="absolute bottom-0 left-0 w-full py-2 md:py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'border-t border-white border-opacity-5'">
<div class="flex items-center px-4"> <div class="flex items-center px-4">
<ui-btn v-if="userCanDelete" color="error" type="button" class="h-8" :padding-x="3" small @click.stop.prevent="removeItem">Remove</ui-btn> <ui-btn v-if="userCanDelete" color="error" type="button" class="h-8 hidden md:block" :padding-x="3" small @click.stop.prevent="removeItem">Remove</ui-btn>
<ui-icon-btn bg-color="error" icon="delete" class="md:hidden" :size="7" icon-font-size="1rem" @click.stop.prevent="removeItem" />
<div class="flex-grow" /> <div class="flex-grow" />
<ui-tooltip v-if="mediaType == 'book'" :disabled="!!quickMatching" :text="`(Root User Only) Populate empty book details & cover with first book result from '${libraryProvider}'. Does not overwrite details.`" direction="bottom" class="mr-4"> <ui-tooltip v-if="mediaType == 'book'" :disabled="!!quickMatching" :text="`(Root User Only) Populate empty book details & cover with first book result from '${libraryProvider}'. Does not overwrite details.`" direction="bottom" class="mr-2 md:mr-4">
<ui-btn v-if="userIsAdminOrUp" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">Quick Match</ui-btn> <ui-btn v-if="userIsAdminOrUp" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">Quick Match</ui-btn>
</ui-tooltip> </ui-tooltip>
<ui-tooltip :disabled="!!libraryScan" text="(Root User Only) Rescan audiobook including metadata" direction="bottom" class="mr-4"> <ui-tooltip :disabled="!!libraryScan" text="(Root User Only) Rescan audiobook including metadata" direction="bottom" class="mr-2 md:mr-4">
<ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn> <ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn>
</ui-tooltip> </ui-tooltip>
<ui-btn @click="save" class="mx-2">Save</ui-btn> <ui-btn @click="save" class="mx-2 hidden md:block">Save</ui-btn>
<ui-btn @click="saveAndClose">Save & Close</ui-btn> <ui-btn @click="saveAndClose">Save<span class="hidden md:inline-block">&nbsp;& Close</span></ui-btn>
</div> </div>
</div> </div>
</div> </div>
@@ -2,13 +2,13 @@
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6"> <div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6">
<!-- Merge to m4b --> <!-- Merge to m4b -->
<div v-if="showM4bDownload" class="w-full border border-black-200 p-4 my-8"> <div v-if="showM4bDownload" class="w-full border border-black-200 p-4 my-8">
<div class="flex items-center"> <div class="flex flex-wrap items-center">
<div> <div>
<p class="text-lg">Make M4B Audiobook File <span class="text-error">*</span></p> <p class="text-lg">Make M4B Audiobook File <span class="text-error">*</span></p>
<p class="max-w-sm text-sm pt-2 text-gray-300">Generate a .M4B audiobook file with embedded metadata, cover image, and chapters. <br /><span class="text-warning">*</span> Does not delete existing audio files.</p> <p class="max-w-sm text-sm pt-2 text-gray-300">Generate a .M4B audiobook file with embedded metadata, cover image, and chapters. <br /><span class="text-warning">*</span> Does not delete existing audio files.</p>
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
<div> <div class="mt-2 md:mt-0">
<p v-if="abmergeStatus === $constants.DownloadStatus.FAILED" class="text-error mb-2">Download Failed</p> <p v-if="abmergeStatus === $constants.DownloadStatus.FAILED" class="text-error mb-2">Download Failed</p>
<p v-if="abmergeStatus === $constants.DownloadStatus.READY" class="text-success mb-2">Download Ready!</p> <p v-if="abmergeStatus === $constants.DownloadStatus.READY" class="text-success mb-2">Download Ready!</p>
<p v-if="abmergeStatus === $constants.DownloadStatus.EXPIRED" class="text-error mb-2">Download Expired</p> <p v-if="abmergeStatus === $constants.DownloadStatus.EXPIRED" class="text-error mb-2">Download Expired</p>
@@ -69,7 +69,7 @@
<p v-if="showM4bDownload" class="text-left text-base mb-4 py-4"> <p v-if="showM4bDownload" class="text-left text-base mb-4 py-4">
<span class="text-error">* <strong>Experimental</strong></span <span class="text-error">* <strong>Experimental</strong></span
>&nbsp;-&nbsp;M4b merge can take several minutes and will be stored in <span class="bg-primary bg-opacity-75 font-mono p-1 text-base">/metadata/downloads</span>. After the download is ready, it will remain available for 60 minutes, then be deleted. Download will timeout after 20 minutes. >&nbsp;-&nbsp;M4b merge can take several minutes and will be stored in <span class="bg-primary bg-opacity-75 font-mono p-1 text-base">/metadata/downloads</span>. After the download is ready, it will remain available for 60 minutes, then be deleted. Download will timeout after 30 minutes.
</p> </p>
<!-- <p v-if="isSingleM4b" class="text-lg text-center my-8">Audiobook is already a single m4b!</p> --> <!-- <p v-if="isSingleM4b" class="text-lg text-center my-8">Audiobook is already a single m4b!</p> -->
+44 -26
View File
@@ -1,14 +1,14 @@
<template> <template>
<div id="match-wrapper" class="w-full h-full overflow-hidden px-4 py-6 relative"> <div id="match-wrapper" class="w-full h-full overflow-hidden px-2 md:px-4 py-4 md:py-6 relative">
<form @submit.prevent="submitSearch"> <form @submit.prevent="submitSearch">
<div class="flex items-center justify-start -mx-1 h-20"> <div class="flex flex-wrap md:flex-nowrap items-center justify-start -mx-1">
<div class="w-40 px-1"> <div class="w-36 px-1">
<ui-dropdown v-model="provider" :items="providers" label="Provider" small /> <ui-dropdown v-model="provider" :items="providers" label="Provider" small />
</div> </div>
<div class="w-72 px-1"> <div class="flex-grow md:w-72 px-1">
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" placeholder="Search" /> <ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" placeholder="Search" />
</div> </div>
<div v-show="provider != 'itunes'" class="w-72 px-1"> <div v-show="provider != 'itunes'" class="w-60 md:w-72 px-1">
<ui-text-input-with-label v-model="searchAuthor" label="Author" /> <ui-text-input-with-label v-model="searchAuthor" label="Author" />
</div> </div>
<ui-btn class="mt-5 ml-1" type="submit">Search</ui-btn> <ui-btn class="mt-5 ml-1" type="submit">Search</ui-btn>
@@ -20,7 +20,7 @@
<div v-show="!processing && !searchResults.length && hasSearched" class="flex h-full items-center justify-center"> <div v-show="!processing && !searchResults.length && hasSearched" class="flex h-full items-center justify-center">
<p>No Results</p> <p>No Results</p>
</div> </div>
<div v-show="!processing" class="w-full max-h-full overflow-y-auto overflow-x-hidden matchListWrapper"> <div v-show="!processing" class="w-full max-h-full overflow-y-auto overflow-x-hidden matchListWrapper mt-4">
<template v-for="(res, index) in searchResults"> <template v-for="(res, index) in searchResults">
<cards-book-match-card :key="index" :book="res" :is-podcast="isPodcast" :book-cover-aspect-ratio="bookCoverAspectRatio" @select="selectMatch" /> <cards-book-match-card :key="index" :book="res" :is-podcast="isPodcast" :book-cover-aspect-ratio="bookCoverAspectRatio" @select="selectMatch" />
</template> </template>
@@ -299,7 +299,7 @@ export default {
this.isProcessing = true this.isProcessing = true
this.lastSearch = searchQuery this.lastSearch = searchQuery
var searchEntity = this.isPodcast ? 'podcast' : 'books' var searchEntity = this.isPodcast ? 'podcast' : 'books'
var results = await this.$axios.$get(`/api/search/${searchEntity}?${searchQuery}`).catch((error) => { var results = await this.$axios.$get(`/api/search/${searchEntity}?${searchQuery}`, { timeout: 10000 }).catch((error) => {
console.error('Failed', error) console.error('Failed', error)
return [] return []
}) })
@@ -363,23 +363,33 @@ export default {
this.searchAuthor = this.libraryItem.media.metadata.authorName || '' this.searchAuthor = this.libraryItem.media.metadata.authorName || ''
if (this.isPodcast) this.provider = 'itunes' if (this.isPodcast) this.provider = 'itunes'
else this.provider = localStorage.getItem('book-provider') || 'google' else this.provider = localStorage.getItem('book-provider') || 'google'
if (this.searchTitle) {
this.submitSearch()
}
}, },
selectMatch(match) { selectMatch(match) {
if (match && match.series) { if (match) {
if (!match.series.length) { if (match.series) {
delete match.series if (!match.series.length) {
} else { delete match.series
match.series = match.series.map((se) => { } else {
return { match.series = match.series.map((se) => {
id: `new-${Math.floor(Math.random() * 10000)}`, return {
displayName: se.volumeNumber ? `${se.series} #${se.volumeNumber}` : se.series, id: `new-${Math.floor(Math.random() * 10000)}`,
name: se.series, displayName: se.volumeNumber ? `${se.series} #${se.volumeNumber}` : se.series,
sequence: se.volumeNumber || '' name: se.series,
} sequence: se.volumeNumber || ''
}) }
})
}
}
if (match.genres && Array.isArray(match.genres)) {
match.genres = match.genres.join(',')
} }
} }
console.log('Select Match', match)
this.selectedMatch = match this.selectedMatch = match
}, },
buildMatchUpdatePayload() { buildMatchUpdatePayload() {
@@ -409,11 +419,12 @@ export default {
updatePayload.metadata.series = seriesPayload updatePayload.metadata.series = seriesPayload
} else if (key === 'author' && !this.isPodcast) { } else if (key === 'author' && !this.isPodcast) {
if (!Array.isArray(this.selectedMatch[key])) { var authors = this.selectedMatch[key]
this.selectedMatch[key] = this.selectedMatch[key].split(',').map((au) => au.trim()) if (!Array.isArray(authors)) {
authors = authors.split(',').map((au) => au.trim())
} }
var authorPayload = [] var authorPayload = []
this.selectedMatch[key].forEach((authorName) => authors.forEach((authorName) =>
authorPayload.push({ authorPayload.push({
id: `new-${Math.floor(Math.random() * 10000)}`, id: `new-${Math.floor(Math.random() * 10000)}`,
name: authorName name: authorName
@@ -421,11 +432,11 @@ export default {
) )
updatePayload.metadata.authors = authorPayload updatePayload.metadata.authors = authorPayload
} else if (key === 'narrator') { } else if (key === 'narrator') {
updatePayload.metadata.narrators = [this.selectedMatch[key]] updatePayload.metadata.narrators = this.selectedMatch[key].split(',').map((v) => v.trim())
} else if (key === 'genres') { } else if (key === 'genres') {
updatePayload.metadata.genres = this.selectedMatch[key].split(',') updatePayload.metadata.genres = this.selectedMatch[key].split(',').map((v) => v.trim())
} else if (key === 'tags') { } else if (key === 'tags') {
updatePayload.tags = this.selectedMatch[key].split(',') updatePayload.tags = this.selectedMatch[key].split(',').map((v) => v.trim())
} else if (key === 'itunesId') { } else if (key === 'itunesId') {
updatePayload.metadata.itunesId = Number(this.selectedMatch[key]) updatePayload.metadata.itunesId = Number(this.selectedMatch[key])
} else { } else {
@@ -441,6 +452,8 @@ export default {
if (!Object.keys(updatePayload).length) { if (!Object.keys(updatePayload).length) {
return return
} }
console.log('Match payload', updatePayload)
this.isProcessing = true this.isProcessing = true
if (updatePayload.metadata.cover) { if (updatePayload.metadata.cover) {
@@ -488,6 +501,11 @@ export default {
<style> <style>
.matchListWrapper { .matchListWrapper {
height: calc(100% - 80px); height: calc(100% - 124px);
}
@media (min-width: 768px) {
.matchListWrapper {
height: calc(100% - 80px);
}
} }
</style> </style>
@@ -5,33 +5,14 @@
<p class="font-book text-3xl text-white truncate">{{ title }}</p> <p class="font-book text-3xl text-white truncate">{{ title }}</p>
</div> </div>
</template> </template>
<div ref="wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh"> <div class="absolute -top-10 left-0 z-10 w-full flex">
<div class="flex flex-wrap"> <template v-for="tab in tabs">
<div class="w-1/5 p-1"> <div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
<ui-text-input-with-label v-model="newEpisode.season" label="Season" /> </template>
</div> </div>
<div class="w-1/5 p-1">
<ui-text-input-with-label v-model="newEpisode.episode" label="Episode" /> <div ref="wrapper" class="p-4 w-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
</div> <component v-if="libraryItem && show" :is="tabComponentName" :library-item="libraryItem" :episode="episode" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
<div class="w-1/5 p-1">
<ui-text-input-with-label v-model="newEpisode.episodeType" label="Episode Type" />
</div>
<div class="w-2/5 p-1">
<ui-text-input-with-label v-model="pubDateInput" @input="updatePubDate" type="datetime-local" label="Pub Date" />
</div>
<div class="w-full p-1">
<ui-text-input-with-label v-model="newEpisode.title" label="Title" />
</div>
<div class="w-full p-1">
<ui-textarea-with-label v-model="newEpisode.subtitle" label="Subtitle" :rows="3" />
</div>
<div class="w-full p-1 default-style">
<ui-rich-text-editor v-if="show" label="Description" v-model="newEpisode.description" />
</div>
</div>
<div class="flex justify-end pt-4">
<ui-btn @click="submit">Submit</ui-btn>
</div>
</div> </div>
</modals-modal> </modals-modal>
</template> </template>
@@ -41,25 +22,19 @@ export default {
data() { data() {
return { return {
processing: false, processing: false,
newEpisode: { selectedTab: 'details',
season: null, tabs: [
episode: null, {
episodeType: null, id: 'details',
title: null, title: 'Details',
subtitle: null, component: 'modals-podcast-tabs-episode-details'
description: null, },
pubDate: null, {
publishedAt: null id: 'match',
}, title: 'Match',
pubDateInput: null component: 'modals-podcast-tabs-episode-match'
} }
}, ]
watch: {
episode: {
immediate: true,
handler(newVal) {
if (newVal) this.init()
}
} }
}, },
computed: { computed: {
@@ -77,67 +52,29 @@ export default {
episode() { episode() {
return this.$store.state.globals.selectedEpisode return this.$store.state.globals.selectedEpisode
}, },
episodeId() {
return this.episode ? this.episode.id : null
},
title() { title() {
if (!this.libraryItem) return '' if (!this.libraryItem) return ''
return this.libraryItem.media.metadata.title || 'Unknown' return this.libraryItem.media.metadata.title || 'Unknown'
},
tabComponentName() {
var _tab = this.tabs.find((t) => t.id === this.selectedTab)
return _tab ? _tab.component : ''
} }
}, },
methods: { methods: {
updatePubDate(val) { selectTab(tab) {
if (val) { this.selectedTab = tab
this.newEpisode.pubDate = this.$formatJsDate(new Date(val), 'E, d MMM yyyy HH:mm:ssxx')
this.newEpisode.publishedAt = new Date(val).valueOf()
} else {
this.newEpisode.pubDate = null
this.newEpisode.publishedAt = null
}
},
init() {
this.newEpisode.season = this.episode.season || ''
this.newEpisode.episode = this.episode.episode || ''
this.newEpisode.episodeType = this.episode.episodeType || ''
this.newEpisode.title = this.episode.title || ''
this.newEpisode.subtitle = this.episode.subtitle || ''
this.newEpisode.description = this.episode.description || ''
this.newEpisode.pubDate = this.episode.pubDate || ''
this.newEpisode.publishedAt = this.episode.publishedAt
this.pubDateInput = this.episode.pubDate ? this.$formatJsDate(new Date(this.episode.pubDate), "yyyy-MM-dd'T'HH:mm") : null
},
getUpdatePayload() {
var updatePayload = {}
for (const key in this.newEpisode) {
if (this.newEpisode[key] != this.episode[key]) {
updatePayload[key] = this.newEpisode[key]
}
}
return updatePayload
},
submit() {
const payload = this.getUpdatePayload()
if (!Object.keys(payload).length) {
return this.$toast.info('No updates were made')
}
this.processing = true
this.$axios
.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, payload)
.then(() => {
this.processing = false
this.$toast.success('Podcast episode updated')
this.show = false
})
.catch((error) => {
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed update episode'
console.error('Failed update episode', error)
this.processing = false
this.$toast.error(errorMsg)
})
} }
}, },
mounted() {} mounted() {}
} }
</script> </script>
<style scoped>
.tab {
height: 40px;
}
.tab.tab-selected {
height: 41px;
}
</style>
@@ -0,0 +1,136 @@
<template>
<div>
<div class="flex flex-wrap">
<div class="w-1/5 p-1">
<ui-text-input-with-label v-model="newEpisode.season" label="Season" />
</div>
<div class="w-1/5 p-1">
<ui-text-input-with-label v-model="newEpisode.episode" label="Episode" />
</div>
<div class="w-1/5 p-1">
<ui-text-input-with-label v-model="newEpisode.episodeType" label="Episode Type" />
</div>
<div class="w-2/5 p-1">
<ui-text-input-with-label v-model="pubDateInput" @input="updatePubDate" type="datetime-local" label="Pub Date" />
</div>
<div class="w-full p-1">
<ui-text-input-with-label v-model="newEpisode.title" label="Title" />
</div>
<div class="w-full p-1">
<ui-textarea-with-label v-model="newEpisode.subtitle" label="Subtitle" :rows="3" />
</div>
<div class="w-full p-1 default-style">
<ui-rich-text-editor label="Description" v-model="newEpisode.description" />
</div>
</div>
<div class="flex justify-end pt-4">
<ui-btn @click="submit">Submit</ui-btn>
</div>
</div>
</template>
<script>
export default {
props: {
processing: Boolean,
libraryItem: {
type: Object,
default: () => {}
},
episode: {
type: Object,
default: () => {}
}
},
data() {
return {
newEpisode: {
season: null,
episode: null,
episodeType: null,
title: null,
subtitle: null,
description: null,
pubDate: null,
publishedAt: null
},
pubDateInput: null
}
},
watch: {
episode: {
immediate: true,
handler(newVal) {
if (newVal) this.init()
}
}
},
computed: {
isProcessing: {
get() {
return this.processing
},
set(val) {
this.$emit('update:processing', val)
}
},
episodeId() {
return this.episode ? this.episode.id : null
}
},
methods: {
updatePubDate(val) {
if (val) {
this.newEpisode.pubDate = this.$formatJsDate(new Date(val), 'E, d MMM yyyy HH:mm:ssxx')
this.newEpisode.publishedAt = new Date(val).valueOf()
} else {
this.newEpisode.pubDate = null
this.newEpisode.publishedAt = null
}
},
init() {
this.newEpisode.season = this.episode.season || ''
this.newEpisode.episode = this.episode.episode || ''
this.newEpisode.episodeType = this.episode.episodeType || ''
this.newEpisode.title = this.episode.title || ''
this.newEpisode.subtitle = this.episode.subtitle || ''
this.newEpisode.description = this.episode.description || ''
this.newEpisode.pubDate = this.episode.pubDate || ''
this.newEpisode.publishedAt = this.episode.publishedAt
this.pubDateInput = this.episode.pubDate ? this.$formatJsDate(new Date(this.episode.pubDate), "yyyy-MM-dd'T'HH:mm") : null
},
getUpdatePayload() {
var updatePayload = {}
for (const key in this.newEpisode) {
if (this.newEpisode[key] != this.episode[key]) {
updatePayload[key] = this.newEpisode[key]
}
}
return updatePayload
},
submit() {
const payload = this.getUpdatePayload()
if (!Object.keys(payload).length) {
return this.$toast.info('No updates were made')
}
this.isProcessing = true
this.$axios
.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, payload)
.then(() => {
this.isProcessing = false
this.$toast.success('Podcast episode updated')
this.$emit('close')
})
.catch((error) => {
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to update episode'
console.error('Failed update episode', error)
this.isProcessing = false
this.$toast.error(errorMsg)
})
}
},
mounted() {}
}
</script>
@@ -0,0 +1,156 @@
<template>
<div style="min-height: 200px">
<template v-if="!podcastFeedUrl">
<div class="py-8">
<widgets-alert type="error">Podcast has no RSS feed url to use for matching</widgets-alert>
</div>
</template>
<template v-else>
<form @submit.prevent="submitForm">
<div class="flex mb-2">
<ui-text-input-with-label v-model="episodeTitle" :disabled="isProcessing" label="Episode Title" class="pr-1" />
<ui-btn class="mt-5 ml-1" :loading="isProcessing" type="submit">Search</ui-btn>
</div>
</form>
<div v-if="!isProcessing && searchedTitle && !episodesFound.length" class="w-full py-8">
<p class="text-center text-lg">No episode matches found</p>
</div>
<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 class="text-xs text-gray-400">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
</div>
</template>
</div>
</template>
<script>
export default {
props: {
processing: Boolean,
libraryItem: {
type: Object,
default: () => {}
},
episode: {
type: Object,
default: () => {}
}
},
data() {
return {
episodeTitle: '',
searchedTitle: '',
episodesFound: []
}
},
watch: {
episode: {
immediate: true,
handler(newVal) {
if (newVal) this.init()
}
}
},
computed: {
isProcessing: {
get() {
return this.processing
},
set(val) {
this.$emit('update:processing', val)
}
},
episodeId() {
return this.episode ? this.episode.id : null
},
media() {
return this.libraryItem ? this.libraryItem.media || {} : {}
},
mediaMetadata() {
return this.media.metadata || {}
},
podcastFeedUrl() {
return this.mediaMetadata.feedUrl
}
},
methods: {
getUpdatePayload(episodeData) {
var updatePayload = {}
for (const key in episodeData) {
if (key === 'enclosure') {
if (!this.episode.enclosure || JSON.stringify(this.episode.enclosure) !== JSON.stringify(episodeData.enclosure)) {
updatePayload[key] = {
...episodeData.enclosure
}
}
} else if (episodeData[key] != this.episode[key]) {
updatePayload[key] = episodeData[key]
}
}
return updatePayload
},
selectEpisode(episode) {
const episodeData = {
title: episode.title || '',
subtitle: episode.subtitle || '',
description: episode.description || '',
enclosure: episode.enclosure || null,
episode: episode.episode || '',
episodeType: episode.episodeType || '',
season: episode.season || '',
pubDate: episode.pubDate || '',
publishedAt: episode.publishedAt
}
const updatePayload = this.getUpdatePayload(episodeData)
if (!Object.keys(updatePayload).length) {
return this.$toast.info('No updates are necessary')
}
console.log('Episode update payload', updatePayload)
this.isProcessing = true
this.$axios
.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatePayload)
.then(() => {
this.isProcessing = false
this.$toast.success('Podcast episode updated')
this.$emit('selectTab', 'details')
})
.catch((error) => {
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to update episode'
console.error('Failed update episode', error)
this.isProcessing = false
this.$toast.error(errorMsg)
})
},
submitForm() {
if (!this.episodeTitle || !this.episodeTitle.length) {
this.$toast.error('Must enter an episode title')
return
}
this.searchedTitle = this.episodeTitle
this.isProcessing = true
this.$axios
.$get(`/api/podcasts/${this.libraryItem.id}/search-episode?title=${this.$encodeUriPath(this.episodeTitle)}`)
.then((results) => {
this.episodesFound = results.episodes.map((ep) => ep.episode)
console.log('Episodes found', this.episodesFound)
this.isProcessing = false
})
.catch((error) => {
console.error('Failed to search for episode', error)
var errMsg = error.response ? error.response.data || '' : ''
this.$toast.error(errMsg || 'Failed to search for episode')
this.isProcessing = false
})
},
init() {
this.searchedTitle = null
this.episodesFound = []
this.episodeTitle = this.episode ? this.episode.title || '' : ''
}
},
mounted() {}
}
</script>
@@ -27,7 +27,6 @@
<p v-if="hasEpisodesWithoutPubDate" class="w-full pt-2 text-warning text-xs">Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.</p> <p v-if="hasEpisodesWithoutPubDate" class="w-full pt-2 text-warning text-xs">Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.</p>
</div> </div>
<div v-show="userIsAdminOrUp" class="flex items-center pt-6"> <div v-show="userIsAdminOrUp" class="flex items-center pt-6">
<p class="text-xs text-gray-300">Note: RSS feed URLs are not authenticated</p>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-if="currentFeedUrl" color="error" small @click="closeFeed">Close RSS Feed</ui-btn> <ui-btn v-if="currentFeedUrl" color="error" small @click="closeFeed">Close RSS Feed</ui-btn>
<ui-btn v-else color="success" small @click="openFeed">Open RSS Feed</ui-btn> <ui-btn v-else color="success" small @click="openFeed">Open RSS Feed</ui-btn>
@@ -3,21 +3,21 @@
<div class="flex-grow" /> <div class="flex-grow" />
<template v-if="!loading"> <template v-if="!loading">
<div class="cursor-pointer flex items-center justify-center text-gray-300 mr-4 md:mr-8" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter"> <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-3xl">first_page</span> <span class="material-icons text-2xl sm:text-3xl">first_page</span>
</div> </div>
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward"> <div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
<span class="material-icons text-3xl">replay_10</span> <span class="material-icons text-2xl sm:text-3xl">replay_10</span>
</div> </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"> <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">
<span class="material-icons">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span> <span class="material-icons">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
</div> </div>
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward"> <div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
<span class="material-icons text-3xl">forward_10</span> <span class="material-icons text-2xl sm:text-3xl">forward_10</span>
</div> </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"> <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-3xl">last_page</span> <span class="material-icons text-2xl sm:text-3xl">last_page</span>
</div> </div>
<controls-playback-speed-control v-model="playbackRate" @input="playbackRateUpdated" @change="playbackRateChanged" /> <controls-playback-speed-control v-model="playbackRateInput" @input="playbackRateUpdated" @change="playbackRateChanged" />
</template> </template>
<template v-else> <template v-else>
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin"> <div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
@@ -40,7 +40,16 @@ export default {
data() { data() {
return {} return {}
}, },
computed: {}, computed: {
playbackRateInput: {
get() {
return this.playbackRate
},
set(val) {
this.$emit('update:playbackRate', val)
}
}
},
methods: { methods: {
playPause() { playPause() {
this.$emit('playPause') this.$emit('playPause')
+39 -11
View File
@@ -8,7 +8,7 @@
<div ref="trackCursor" class="h-full w-0.5 bg-gray-50 absolute top-0 left-0 opacity-0 pointer-events-none" /> <div ref="trackCursor" class="h-full w-0.5 bg-gray-50 absolute top-0 left-0 opacity-0 pointer-events-none" />
<div v-if="loading" class="h-full w-1/4 absolute left-0 top-0 loadingTrack pointer-events-none bg-white bg-opacity-25" /> <div v-if="loading" class="h-full w-1/4 absolute left-0 top-0 loadingTrack pointer-events-none bg-white bg-opacity-25" />
</div> </div>
<div ref="track" class="w-full h-2 relative overflow-hidden"> <div class="w-full h-2 relative overflow-hidden" :class="useChapterTrack ? 'opacity-0' : ''">
<template v-for="(tick, index) in chapterTicks"> <template v-for="(tick, index) in chapterTicks">
<div :key="index" :style="{ left: tick.left + 'px' }" class="absolute top-0 w-px bg-white bg-opacity-30 h-1 pointer-events-none" /> <div :key="index" :style="{ left: tick.left + 'px' }" class="absolute top-0 w-px bg-white bg-opacity-30 h-1 pointer-events-none" />
</template> </template>
@@ -34,6 +34,10 @@ export default {
chapters: { chapters: {
type: Array, type: Array,
default: () => [] default: () => []
},
currentChapter: {
type: Object,
default: () => {}
} }
}, },
data() { data() {
@@ -46,7 +50,8 @@ export default {
trackOffsetLeft: 16, // Track is 16px from edge trackOffsetLeft: 16, // Track is 16px from edge
playedTrackWidth: 0, playedTrackWidth: 0,
readyTrackWidth: 0, readyTrackWidth: 0,
bufferTrackWidth: 0 bufferTrackWidth: 0,
useChapterTrack: false
} }
}, },
watch: { watch: {
@@ -57,14 +62,30 @@ export default {
} }
} }
}, },
computed: {}, computed: {
currentChapterDuration() {
if (!this.currentChapter) return 0
return this.currentChapter.end - this.currentChapter.start
},
currentChapterStart() {
if (!this.currentChapter) return 0
return this.currentChapter.start
}
},
methods: { methods: {
setUseChapterTrack(useChapterTrack) {
this.useChapterTrack = useChapterTrack
this.updateBufferTrack()
this.updatePlayedTrackWidth()
},
clickTrack(e) { clickTrack(e) {
if (this.loading) return if (this.loading) return
var offsetX = e.offsetX var offsetX = e.offsetX
var perc = offsetX / this.trackWidth var perc = offsetX / this.trackWidth
var time = perc * this.duration const baseTime = this.useChapterTrack ? this.currentChapterStart : 0;
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration;
const time = baseTime + (perc * duration);
if (isNaN(time) || time === null) { if (isNaN(time) || time === null) {
console.error('Invalid time', perc, time) console.error('Invalid time', perc, time)
return return
@@ -76,7 +97,10 @@ export default {
this.updateBufferTrack() this.updateBufferTrack()
}, },
updateBufferTrack() { updateBufferTrack() {
var bufferlen = (this.bufferTime / this.duration) * this.trackWidth const time = this.useChapterTrack ? Math.max(0, this.bufferTime - this.currentChapterStart) : this.bufferTime
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration
var bufferlen = (time / duration) * this.trackWidth
bufferlen = Math.round(bufferlen) bufferlen = Math.round(bufferlen)
if (this.bufferTrackWidth === bufferlen || !this.$refs.bufferTrack) return if (this.bufferTrackWidth === bufferlen || !this.$refs.bufferTrack) return
if (this.$refs.bufferTrack) this.$refs.bufferTrack.style.width = bufferlen + 'px' if (this.$refs.bufferTrack) this.$refs.bufferTrack.style.width = bufferlen + 'px'
@@ -97,8 +121,10 @@ export default {
this.updatePlayedTrackWidth() this.updatePlayedTrackWidth()
}, },
updatePlayedTrackWidth() { updatePlayedTrackWidth() {
var perc = this.currentTime / this.duration const time = this.useChapterTrack ? Math.max(0, this.currentTime - this.currentChapterStart) : this.currentTime
var ptWidth = Math.round(perc * this.trackWidth) const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration
var ptWidth = Math.round((time / duration) * this.trackWidth)
if (this.playedTrackWidth === ptWidth) { if (this.playedTrackWidth === ptWidth) {
return return
} }
@@ -116,9 +142,11 @@ export default {
}, },
mousemoveTrack(e) { mousemoveTrack(e) {
var offsetX = e.offsetX var offsetX = e.offsetX
var time = (offsetX / this.trackWidth) * this.duration
console.log('Mousemove track', this.trackWidth, this.duration) const baseTime = this.useChapterTrack ? this.currentChapterStart : 0;
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration;
const progressTime = (offsetX / this.trackWidth) * duration;
const totalTime = baseTime + progressTime;
if (this.$refs.hoverTimestamp) { if (this.$refs.hoverTimestamp) {
var width = this.$refs.hoverTimestamp.clientWidth var width = this.$refs.hoverTimestamp.clientWidth
@@ -139,9 +167,9 @@ export default {
this.$refs.hoverTimestampArrow.style.left = posLeft + 'px' this.$refs.hoverTimestampArrow.style.left = posLeft + 'px'
} }
if (this.$refs.hoverTimestampText) { if (this.$refs.hoverTimestampText) {
var hoverText = this.$secondsToTimestamp(time) var hoverText = this.$secondsToTimestamp(progressTime)
var chapter = this.chapters.find((chapter) => chapter.start <= time && time < chapter.end) var chapter = this.chapters.find((chapter) => chapter.start <= totalTime && totalTime < chapter.end)
if (chapter && chapter.title) { if (chapter && chapter.title) {
hoverText += ` - ${chapter.title}` hoverText += ` - ${chapter.title}`
} }
+64 -18
View File
@@ -1,40 +1,48 @@
<template> <template>
<div class="w-full -mt-6"> <div class="w-full -mt-6">
<div class="w-full relative mb-1"> <div class="w-full relative mb-1">
<div class="absolute -top-10 md:top-0 right-0 md:right-2 flex items-center h-full"> <div class="absolute -top-10 md:top-0 right-0 lg:right-2 flex items-center h-full">
<!-- <span class="material-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> --> <!-- <span class="material-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
<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 md:block" />
<div class="cursor-pointer text-gray-300 mx-1 md:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')"> <div class="cursor-pointer 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" style="font-size: 1.7rem">snooze</span> <span v-if="!sleepTimerSet" class="material-icons text-2xl sm:text-2.5xl">snooze</span>
<div v-else class="flex items-center"> <div v-else class="flex items-center">
<span class="material-icons text-lg text-warning">snooze</span> <span class="material-icons text-lg text-warning">snooze</span>
<p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p> <p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p>
</div> </div>
</div> </div>
<div v-if="!isPodcast" class="cursor-pointer text-gray-300 mx-1 md:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')"> <div v-if="!isPodcast" class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
<span class="material-icons" style="font-size: 1.7rem">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span> <span class="material-icons text-2xl sm:text-2.5xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
</div> </div>
<div v-if="chapters.length" class="cursor-pointer text-gray-300 mx-1 md:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters"> <div v-if="chapters.length" class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
<span class="material-icons text-3xl">format_list_bulleted</span> <span class="material-icons text-2xl sm:text-3xl">format_list_bulleted</span>
</div> </div>
<ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? 'Use full track' : 'Use chapter track'">
<div class="cursor-pointer 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>
</ui-tooltip>
</div> </div>
<player-playback-controls :loading="loading" :seek-loading="seekLoading" :playback-rate="playbackRate" :paused="paused" :has-next-chapter="hasNextChapter" @prevChapter="prevChapter" @nextChapter="nextChapter" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setPlaybackRate="setPlaybackRate" @playPause="playPause" /> <player-playback-controls :loading="loading" :seek-loading="seekLoading" :playback-rate.sync="playbackRate" :paused="paused" :has-next-chapter="hasNextChapter" @prevChapter="prevChapter" @nextChapter="nextChapter" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setPlaybackRate="setPlaybackRate" @playPause="playPause" />
</div> </div>
<player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" @seek="seek" /> <player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" :current-chapter="currentChapter" @seek="seek" />
<div class="flex"> <div class="flex">
<p ref="currentTimestamp" class="font-mono text-sm text-gray-100 pointer-events-auto">00:00:00</p> <p ref="currentTimestamp" class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">00:00:00</p>
<p class="font-mono text-sm text-gray-100 pointer-events-auto">&nbsp;/&nbsp;{{ progressPercent }}%</p> <p class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto">&nbsp;/&nbsp;{{ progressPercent }}%</p>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="text-sm text-gray-300 pt-0.5">{{ currentChapterName }}</p> <p class="text-xs sm:text-sm text-gray-300 pt-0.5">
{{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400">&nbsp;({{ currentChapterIndex + 1 }} of {{ chapters.length }})</span>
</p>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="font-mono text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p> <p class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
</div> </div>
<modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" /> <modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" />
@@ -66,7 +74,8 @@ export default {
seekLoading: false, seekLoading: false,
showChaptersModal: false, showChaptersModal: false,
currentTime: 0, currentTime: 0,
duration: 0 duration: 0,
useChapterTrack: false
} }
}, },
computed: { computed: {
@@ -86,6 +95,10 @@ export default {
return this.$store.getters['user/getToken'] return this.$store.getters['user/getToken']
}, },
timeRemaining() { timeRemaining() {
if (this.useChapterTrack && this.currentChapter) {
var currChapTime = this.currentTime - this.currentChapter.start
return (this.currentChapterDuration - currChapTime) / this.playbackRate
}
return (this.duration - this.currentTime) / this.playbackRate return (this.duration - this.currentTime) / this.playbackRate
}, },
timeRemainingPretty() { timeRemainingPretty() {
@@ -95,8 +108,11 @@ export default {
return '-' + this.$secondsToTimestamp(this.timeRemaining) return '-' + this.$secondsToTimestamp(this.timeRemaining)
}, },
progressPercent() { progressPercent() {
if (!this.duration) return 0 const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration
return Math.round((100 * this.currentTime) / this.duration) const time = this.useChapterTrack ? Math.max(this.currentTime - this.currentChapterStart) : this.currentTime
if (!duration) return 0
return Math.round((100 * time) / duration)
}, },
currentChapter() { currentChapter() {
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end) return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
@@ -104,6 +120,14 @@ export default {
currentChapterName() { currentChapterName() {
return this.currentChapter ? this.currentChapter.title : '' return this.currentChapter ? this.currentChapter.title : ''
}, },
currentChapterDuration() {
if (!this.currentChapter) return 0
return this.currentChapter.end - this.currentChapter.start
},
currentChapterStart() {
if (!this.currentChapter) return 0
return this.currentChapter.start
},
isFullscreen() { isFullscreen() {
return this.$store.state.playerIsFullscreen return this.$store.state.playerIsFullscreen
}, },
@@ -192,6 +216,23 @@ export default {
this.seek(chapter.start) this.seek(chapter.start)
this.showChaptersModal = false this.showChaptersModal = false
}, },
setUseChapterTrack() {
var useChapterTrack = !this.useChapterTrack
this.useChapterTrack = useChapterTrack
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(useChapterTrack)
this.$store.dispatch('user/updateUserSettings', { useChapterTrack }).catch((err) => {
console.error('Failed to update settings', err)
})
this.updateTimestamp()
},
checkUpdateChapterTrack() {
// Changing media in player may not have chapters
if (!this.chapters.length && this.useChapterTrack) {
this.useChapterTrack = false
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
}
},
seek(time) { seek(time) {
this.$emit('seek', time) this.$emit('seek', time)
}, },
@@ -239,10 +280,10 @@ export default {
console.error('No timestamp el') console.error('No timestamp el')
return return
} }
var currTimeClean = this.$secondsToTimestamp(this.currentTime) const time = this.useChapterTrack ? Math.max(0, this.currentTime - this.currentChapterStart) : this.currentTime
var currTimeClean = this.$secondsToTimestamp(time)
ts.innerText = currTimeClean ts.innerText = currTimeClean
}, },
setBufferTime(bufferTime) { setBufferTime(bufferTime) {
if (this.$refs.trackbar) this.$refs.trackbar.setBufferTime(bufferTime) if (this.$refs.trackbar) this.$refs.trackbar.setBufferTime(bufferTime)
}, },
@@ -252,6 +293,11 @@ export default {
}, },
init() { init() {
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1 this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
var _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
this.useChapterTrack = this.chapters.length ? _useChapterTrack : false
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
this.$emit('setPlaybackRate', this.playbackRate) this.$emit('setPlaybackRate', this.playbackRate)
}, },
settingsUpdated(settings) { settingsUpdated(settings) {
+117
View File
@@ -0,0 +1,117 @@
<template>
<div ref="wrapper" class="modal modal-bg w-full h-full fixed top-0 left-0 bg-primary bg-opacity-75 flex items-center justify-center z-60 opacity-0">
<div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" />
<div ref="content" class="relative text-white" :style="{ height: modalHeight, width: modalWidth }" v-click-outside="clickedOutside">
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
<p class="text-base mb-8 mt-2 px-1">{{ message }}</p>
<div class="flex px-1 items-center">
<ui-btn v-if="isYesNo" color="primary" @click="nevermind">Cancel</ui-btn>
<div class="flex-grow" />
<ui-btn v-if="isYesNo" color="success" @click="confirm">Yes</ui-btn>
<ui-btn v-else color="primary" @click="confirm">Ok</ui-btn>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {},
data() {
return {
el: null,
content: null
}
},
watch: {
show(newVal) {
if (newVal) {
this.setShow()
} else {
this.setHide()
}
}
},
computed: {
show: {
get() {
return this.$store.state.globals.showConfirmPrompt
},
set(val) {
this.$store.commit('globals/setShowConfirmPrompt', val)
}
},
confirmPromptOptions() {
return this.$store.state.globals.confirmPromptOptions || {}
},
message() {
return this.confirmPromptOptions.message || ''
},
callback() {
return this.confirmPromptOptions.callback
},
type() {
return this.confirmPromptOptions.type || 'ok'
},
persistent() {
return !!this.confirmPromptOptions.persistent
},
isYesNo() {
return this.type === 'yesNo'
},
modalHeight() {
return 'unset'
},
modalWidth() {
return '500px'
}
},
methods: {
clickedOutside(evt) {
if (!this.show) return
if (evt) {
evt.stopPropagation()
evt.preventDefault()
}
if (this.persistent) return
if (this.callback) this.callback(false)
this.show = false
},
nevermind() {
if (this.callback) this.callback(false)
this.show = false
},
confirm() {
if (this.callback) this.callback(true)
this.show = false
},
setShow() {
this.$eventBus.$emit('showing-prompt', true)
document.body.appendChild(this.el)
setTimeout(() => {
this.content.style.transform = 'scale(1)'
}, 10)
},
setHide() {
this.$eventBus.$emit('showing-prompt', false)
this.content.style.transform = 'scale(0)'
this.el.remove()
}
},
mounted() {
this.el = this.$refs.wrapper
this.content = this.$refs.content
this.content.style.transform = 'scale(0)'
this.content.style.transition = 'transform 0.25s cubic-bezier(0.16, 1, 0.3, 1)'
this.el.style.opacity = 1
this.el.remove()
},
beforeDestroy() {
if (this.show) {
this.$eventBus.$emit('showing-prompt', false)
}
}
}
</script>
-2
View File
@@ -65,12 +65,10 @@ export default {
setTimeout(() => { setTimeout(() => {
this.content.style.transform = 'scale(1)' this.content.style.transform = 'scale(1)'
}, 10) }, 10)
document.documentElement.classList.add('modal-open')
}, },
setHide() { setHide() {
this.content.style.transform = 'scale(0)' this.content.style.transform = 'scale(0)'
this.el.remove() this.el.remove()
document.documentElement.classList.remove('modal-open')
} }
}, },
mounted() { mounted() {
+1 -1
View File
@@ -2,7 +2,7 @@
<div id="heatmap" class="w-full"> <div id="heatmap" class="w-full">
<div class="mx-auto" :style="{ height: innerHeight + 160 + 'px', width: innerWidth + 52 + 'px' }" style="background-color: rgba(13, 17, 23, 0)"> <div class="mx-auto" :style="{ height: innerHeight + 160 + 'px', width: innerWidth + 52 + 'px' }" style="background-color: rgba(13, 17, 23, 0)">
<p class="mb-2 px-1 text-sm text-gray-200">{{ Object.values(daysListening).length }} listening sessions in the last year</p> <p class="mb-2 px-1 text-sm text-gray-200">{{ Object.values(daysListening).length }} listening sessions in the last year</p>
<div class="border border-opacity-25 rounded py-2 w-full" style="background-color: #232323" :style="{ height: innerHeight + 80 + 'px' }"> <div class="border border-white border-opacity-25 rounded py-2 w-full" style="background-color: #232323" :style="{ height: innerHeight + 80 + 'px' }">
<div :style="{ width: innerWidth + 'px', height: innerHeight + 'px' }" class="ml-10 mt-5 absolute" @mouseover="mouseover" @mouseout="mouseout"> <div :style="{ width: innerWidth + 'px', height: innerHeight + 'px' }" class="ml-10 mt-5 absolute" @mouseover="mouseover" @mouseout="mouseout">
<div v-for="dayLabel in dayLabels" :key="dayLabel.label" :style="dayLabel.style" class="absolute top-0 left-0 text-gray-300">{{ dayLabel.label }}</div> <div v-for="dayLabel in dayLabels" :key="dayLabel.label" :style="dayLabel.style" class="absolute top-0 left-0 text-gray-300">{{ dayLabel.label }}</div>
+29 -2
View File
@@ -24,10 +24,10 @@
<td class="font-book"> <td class="font-book">
{{ chapter.title }} {{ chapter.title }}
</td> </td>
<td class="font-mono text-center"> <td class="font-mono text-center hover:underline cursor-pointer" @click.stop="goToTimestamp(chapter.start)">
{{ $secondsToTimestamp(chapter.start) }} {{ $secondsToTimestamp(chapter.start) }}
</td> </td>
<td class="font-mono text-center"> <td class="font-mono text-center hover:underline cursor-pointer" @click.stop="goToTimestamp(chapter.start)">
{{ $secondsToTimestamp(chapter.end) }} {{ $secondsToTimestamp(chapter.end) }}
</td> </td>
</tr> </tr>
@@ -57,6 +57,9 @@ export default {
media() { media() {
return this.libraryItem ? this.libraryItem.media || {} : {} return this.libraryItem ? this.libraryItem.media || {} : {}
}, },
metadata() {
return this.media.metadata || {}
},
chapters() { chapters() {
return this.media.chapters || [] return this.media.chapters || []
}, },
@@ -67,6 +70,30 @@ export default {
methods: { methods: {
clickBar() { clickBar() {
this.expanded = !this.expanded this.expanded = !this.expanded
},
goToTimestamp(time) {
if (this.$store.getters['getIsMediaStreaming'](this.libraryItemId)) {
this.$eventBus.$emit('play-item', {
libraryItemId: this.libraryItemId,
episodeId: null,
startTime: time
})
} else {
const payload = {
message: `Start playback for "${this.metadata.title}" at ${this.$secondsToTimestamp(time)}?`,
callback: (confirmed) => {
if (confirmed) {
this.$eventBus.$emit('play-item', {
libraryItemId: this.libraryItemId,
episodeId: null,
startTime: time
})
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
}
} }
}, },
mounted() {} mounted() {}
@@ -1,12 +1,13 @@
<template> <template>
<div class="w-full bg-primary bg-opacity-40"> <div class="w-full bg-primary bg-opacity-40">
<div class="w-full h-14 flex items-center px-4 bg-primary"> <div class="w-full h-14 flex items-center px-4 md:px-6 py-2 bg-primary">
<p>Collection List</p> <p class="pr-4">Collection List</p>
<div class="w-6 h-6 bg-white bg-opacity-10 flex items-center justify-center rounded-full ml-2">
<p class="font-mono text-sm">{{ books.length }}</p> <div class="w-6 h-6 md:w-7 md:h-7 bg-white bg-opacity-10 rounded-full flex items-center justify-center">
<span class="text-xs md:text-sm font-mono leading-none">{{ books.length }}</span>
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
<!-- <p v-if="totalDuration">{{ totalDurationPretty }}</p> --> <p v-if="totalDuration" class="text-sm text-gray-200">{{ totalDurationPretty }}</p>
</div> </div>
<draggable v-model="booksCopy" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate"> <draggable v-model="booksCopy" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate">
<transition-group type="transition" :name="!drag ? 'collection-book' : null"> <transition-group type="transition" :name="!drag ? 'collection-book' : null">
@@ -56,6 +57,16 @@ export default {
}, },
bookCoverAspectRatio() { bookCoverAspectRatio() {
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6 return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
},
totalDuration() {
var _total = 0
this.books.forEach((book) => {
_total += book.media.duration
})
return _total
},
totalDurationPretty() {
return this.$elapsedPrettyExtended(this.totalDuration)
} }
}, },
methods: { methods: {
+6 -2
View File
@@ -192,7 +192,11 @@ export default {
} }
#accounts tr:nth-child(even) { #accounts tr:nth-child(even) {
background-color: #3a3a3a; background-color: #373838;
}
#accounts tr:nth-child(odd) {
background-color: #2f2f2f;
} }
#accounts tr:hover { #accounts tr:hover {
@@ -204,6 +208,6 @@ export default {
font-weight: 600; font-weight: 600;
padding-top: 5px; padding-top: 5px;
padding-bottom: 5px; padding-bottom: 5px;
background-color: #333; background-color: #272727
} }
</style> </style>
@@ -1,12 +1,12 @@
<template> <template>
<div class="w-full px-2 py-2 overflow-hidden relative" @mouseover="mouseover" @mouseleave="mouseleave" :class="isHovering ? 'bg-white bg-opacity-5' : ''"> <div class="w-full px-1 md:px-2 py-2 overflow-hidden relative" @mouseover="mouseover" @mouseleave="mouseleave" :class="isHovering ? 'bg-white bg-opacity-5' : ''">
<div v-if="book" class="flex h-20"> <div v-if="book" class="flex h-16 md:h-20">
<div class="w-16 max-w-16 h-full"> <div class="w-10 min-w-10 md:w-16 md:max-w-16 h-full">
<div class="flex h-full items-center justify-center"> <div class="flex h-full items-center justify-center">
<span class="material-icons drag-handle text-xl">menu</span> <span class="material-icons drag-handle text-lg md:text-xl">menu</span>
</div> </div>
</div> </div>
<div class="h-full relative" :style="{ width: coverWidth + 'px' }"> <div class="h-full relative" :style="{ width: coverWidth + 'px', minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }">
<covers-book-cover :library-item="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-book-cover :library-item="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn"> <div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn">
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick"> <div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
@@ -107,9 +107,12 @@ export default {
userIsFinished() { userIsFinished() {
return this.itemProgress ? !!this.itemProgress.isFinished : false return this.itemProgress ? !!this.itemProgress.isFinished : false
}, },
coverSize() {
return this.$store.state.globals.isMobile ? 40 : 50
},
coverWidth() { coverWidth() {
if (this.bookCoverAspectRatio === 1) return 50 * 1.6 if (this.bookCoverAspectRatio === 1) return this.coverSize * 1.6
return 50 return this.coverSize
} }
}, },
methods: { methods: {
@@ -6,10 +6,10 @@
<span class="material-icons" style="font-size: 1.4rem">add</span> <span class="material-icons" style="font-size: 1.4rem">add</span>
</div> </div>
</div> </div>
<draggable v-if="libraryCopies.length" :list="libraryCopies" v-bind="dragOptions" class="list-group" draggable=".item" tag="div" @start="startDrag" @end="endDrag"> <draggable v-if="libraryCopies.length" :list="libraryCopies" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="startDrag" @end="endDrag">
<template v-for="library in libraryCopies"> <template v-for="library in libraryCopies">
<div :key="library.id" class="item"> <div :key="library.id" class="item">
<tables-library-item :library="library" :selected="currentLibraryId === library.id" :show-edit="true" :dragging="drag" @edit="editLibrary" @click="setLibrary" /> <tables-library-item :library="library" :selected="currentLibraryId === library.id" :dragging="drag" @edit="editLibrary" @click="setLibrary" />
</div> </div>
</template> </template>
</draggable> </draggable>
@@ -7,18 +7,26 @@
</svg> </svg>
<p class="text-xl font-book pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p> <p class="text-xl font-book pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn v-show="isHovering && !libraryScan" small color="success" @click.stop="scan">Scan</ui-btn> <ui-btn v-show="isHovering && !libraryScan" class="hidden md:block" small color="success" @click.stop="scan">Scan</ui-btn>
<ui-btn v-show="isHovering && !libraryScan" small color="bg" class="ml-2" @click.stop="forceScan">Force Re-Scan</ui-btn> <ui-btn v-show="isHovering && !libraryScan" small color="bg" class="ml-2 hidden md:block" @click.stop="forceScan">Force Re-Scan</ui-btn>
<ui-btn v-show="isHovering && !libraryScan && isBookLibrary" small color="bg" class="ml-2" @click.stop="matchAll">Match Books</ui-btn> <ui-btn v-show="isHovering && !libraryScan && isBookLibrary" small color="bg" class="ml-2 hidden md:block" @click.stop="matchAll">Match Books</ui-btn>
<span v-show="isHovering && !libraryScan && showEdit" class="material-icons text-xl text-gray-300 hover:text-gray-50 ml-4 cursor-pointer" @click.stop="editClick">edit</span> <span v-if="isHovering && !libraryScan" class="!hidden md:!block material-icons text-xl text-gray-300 hover:text-gray-50 ml-4 cursor-pointer" @click.stop="editClick">edit</span>
<span v-show="!libraryScan && isHovering && showEdit && !isDeleting" class="material-icons text-xl text-gray-300 ml-3" :class="isMain ? 'text-opacity-5 cursor-not-allowed' : 'hover:text-gray-50 cursor-pointer'" @click.stop="deleteClick">delete</span> <span v-if="!libraryScan && isHovering && !isDeleting" class="!hidden md:!block material-icons text-xl text-gray-300 ml-3 hover:text-gray-50 cursor-pointer" @click.stop="deleteClick">delete</span>
<!-- For mobile -->
<span v-if="!libraryScan" class="!block md:!hidden material-icons text-xl text-gray-300 ml-4 cursor-pointer" @click.stop="editClick">edit</span>
<span v-if="!libraryScan && !isDeleting" class="!block md:!hidden material-icons text-2xl text-gray-300 ml-3 cursor-pointer" @click.stop="showMenu">more_vert</span>
<div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin"> <div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin">
<svg viewBox="0 0 24 24" class="w-6 h-6"> <svg viewBox="0 0 24 24" class="w-6 h-6">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" /> <path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg> </svg>
</div> </div>
<span class="material-icons drag-handle text-xl text-gray-400 hover:text-gray-50 ml-4">reorder</span>
<!-- For mobile -->
<modals-dialog v-model="showMobileMenu" :title="menuTitle" :items="mobileMenuItems" @action="mobileMenuAction" />
</div> </div>
</template> </template>
@@ -30,22 +38,19 @@ export default {
default: () => {} default: () => {}
}, },
selected: Boolean, selected: Boolean,
showEdit: Boolean,
dragging: Boolean dragging: Boolean
}, },
data() { data() {
return { return {
mouseover: false, mouseover: false,
isDeleting: false isDeleting: false,
showMobileMenu: false
} }
}, },
computed: { computed: {
isHovering() { isHovering() {
return this.mouseover && !this.dragging return this.mouseover && !this.dragging
}, },
isMain() {
return this.library.id === 'main'
},
libraryScan() { libraryScan() {
return this.$store.getters['scanners/getLibraryScan'](this.library.id) return this.$store.getters['scanners/getLibraryScan'](this.library.id)
}, },
@@ -54,12 +59,53 @@ export default {
}, },
isBookLibrary() { isBookLibrary() {
return this.mediaType === 'book' return this.mediaType === 'book'
},
menuTitle() {
return this.library.name
},
mobileMenuItems() {
const items = [
{
text: 'Scan',
value: 'scan'
},
{
text: 'Force Re-Scan',
value: 'force-scan'
}
]
if (this.isBookLibrary) {
items.push({
text: 'Match Books',
value: 'match-books'
})
}
items.push({
text: 'Delete',
value: 'delete'
})
return items
} }
}, },
methods: { methods: {
mobileMenuAction(action) {
this.showMobileMenu = false
if (action === 'scan') {
this.scan()
} else if (action === 'force-scan') {
this.forceScan()
} else if (action === 'match-books') {
this.matchAll()
} else if (action === 'delete') {
this.deleteClick()
}
},
showMenu() {
this.showMobileMenu = true
},
matchAll() { matchAll() {
this.$axios this.$axios
.$post(`/api/libraries/${this.library.id}/matchall`) .$get(`/api/libraries/${this.library.id}/matchall`)
.then(() => { .then(() => {
console.log('Starting scan for matches') console.log('Starting scan for matches')
}) })
@@ -97,7 +143,6 @@ export default {
} }
}, },
deleteClick() { deleteClick() {
if (this.isMain) return
if (confirm(`Are you sure you want to permanently delete library "${this.library.name}"?`)) { if (confirm(`Are you sure you want to permanently delete library "${this.library.name}"?`)) {
this.isDeleting = true this.isDeleting = true
this.$axios this.$axios
@@ -78,7 +78,7 @@ export default {
return this.$secondsToTimestamp(this.episode.duration) return this.$secondsToTimestamp(this.episode.duration)
}, },
isStreaming() { isStreaming() {
return this.$store.getters['getIsEpisodeStreaming'](this.libraryItemId, this.episode.id) return this.$store.getters['getIsMediaStreaming'](this.libraryItemId, this.episode.id)
}, },
streamIsPlaying() { streamIsPlaying() {
return this.$store.state.streamIsPlaying && this.isStreaming return this.$store.state.streamIsPlaying && this.isStreaming
@@ -124,7 +124,21 @@ export default {
}) })
} }
}, },
toggleFinished() { toggleFinished(confirmed = false) {
if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) {
const payload = {
message: `Are you sure you want to mark "${this.title}" as finished?`,
callback: (confirmed) => {
if (confirmed) {
this.toggleFinished(true)
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
return
}
var updatePayload = { var updatePayload = {
isFinished: !this.userIsFinished isFinished: !this.userIsFinished
} }
+2 -1
View File
@@ -1,7 +1,8 @@
<template> <template>
<div> <div>
<input ref="fileInput" type="file" :accept="accept" class="hidden" @change="inputChanged" /> <input ref="fileInput" type="file" :accept="accept" class="hidden" @change="inputChanged" />
<ui-btn @click="clickUpload" color="primary" type="text"><slot /></ui-btn> <ui-btn @click="clickUpload" color="primary" class="hidden md:block" type="text"><slot /></ui-btn>
<ui-icon-btn @click="clickUpload" icon="upload" class="block md:hidden" />
</div> </div>
</template> </template>
+3
View File
@@ -89,6 +89,9 @@ export default {
// this.currentSearch = this.textInput // this.currentSearch = this.textInput
}, 100) }, 100)
}, },
setFocus() {
if (this.$refs.input && this.editable) this.$refs.input.focus()
},
inputFocus() { inputFocus() {
this.isFocused = true this.isFocused = true
}, },
+9 -9
View File
@@ -1,18 +1,18 @@
<template> <template>
<div v-if="currentLibrary" class="relative w-36 h-8" v-click-outside="clickOutsideObj"> <div v-if="currentLibrary" class="relative h-8 max-w-52 min-w-32" v-click-outside="clickOutsideObj">
<button type="button" :disabled="disabled" class="relative h-full w-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu"> <button type="button" :disabled="disabled" class="w-10 sm:w-full relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm focus:outline-none cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
<span class="flex items-center"> <div class="flex items-center justify-center sm:justify-start">
<widgets-library-icon :icon="currentLibraryIcon" class="mr-2" /> <widgets-library-icon :icon="currentLibraryIcon" class="sm:mr-1.5" />
<span class="block truncate text-sm">{{ currentLibrary.name }}</span> <span class="hidden sm:block truncate">{{ currentLibrary.name }}</span>
</span> </div>
</button> </button>
<transition name="menu"> <transition name="menu">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox"> <ul v-show="showMenu" class="absolute z-10 -mt-px min-w-48 w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
<template v-for="library in librariesFiltered"> <template v-for="library in librariesFiltered">
<li :key="library.id" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="selectLibrary(library)"> <li :key="library.id" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="selectLibrary(library)">
<div class="flex items-center px-3"> <div class="flex items-center px-2">
<widgets-library-icon :icon="library.icon" class="mr-2" /> <widgets-library-icon :icon="library.icon" class="mr-1.5 text-gray-400" />
<span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span> <span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span>
</div> </div>
</li> </li>
+1 -1
View File
@@ -4,7 +4,7 @@
<div ref="wrapper" class="relative"> <div ref="wrapper" class="relative">
<form @submit.prevent="submitForm"> <form @submit.prevent="submitForm">
<div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-1" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent> <div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-1" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
<div v-for="item in selected" :key="item" class="rounded-full px-2 py-1 mx-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center relative"> <div v-for="item in selected" :key="item" class="rounded-full px-2 py-1 mx-0.5 my-0.5 text-xs bg-bg flex flex-nowrap break-all items-center relative">
<div class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer"> <div class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer">
<span v-if="showEdit" class="material-icons text-white hover:text-warning" style="font-size: 1.1rem" @click.stop="editItem(item)">edit</span> <span v-if="showEdit" class="material-icons text-white hover:text-warning" style="font-size: 1.1rem" @click.stop="editItem(item)">edit</span>
<span class="material-icons text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item)">close</span> <span class="material-icons text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item)">close</span>
+3
View File
@@ -80,6 +80,9 @@ export default {
blur() { blur() {
if (this.$refs.input) this.$refs.input.blur() if (this.$refs.input) this.$refs.input.blur()
}, },
setFocus() {
if (this.$refs.input) this.$refs.input.focus()
},
mouseover() { mouseover() {
this.isHovering = true this.isHovering = true
}, },
@@ -34,6 +34,11 @@ export default {
} }
}, },
methods: { methods: {
setFocus() {
if (this.$refs.input && this.$refs.input.setFocus) {
this.$refs.input.setFocus()
}
},
blur() { blur() {
if (this.$refs.input && this.$refs.input.blur) { if (this.$refs.input && this.$refs.input.blur) {
this.$refs.input.blur() this.$refs.input.blur()
+212
View File
@@ -0,0 +1,212 @@
<template>
<div 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">
<div v-if="digit == ':'" :key="index" class="px-px" @click.stop="clickMedian(index)">:</div>
<div v-else :key="index" class="px-px" :class="{ 'digit-focused': focusedDigit == digit }" @click.stop="focusDigit(digit)">{{ digits[digit] }}</div>
</template>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
value: [String, Number],
showThreeDigitHour: Boolean
},
data() {
return {
clickOutsideObj: {
handler: this.clickOutside,
events: ['mousedown'],
isActive: true
},
digitDisplay: ['hour1', 'hour0', ':', 'minute1', 'minute0', ':', 'second1', 'second0'],
focusedDigit: null,
digits: {
hour2: 0,
hour1: 0,
hour0: 0,
minute1: 0,
minute0: 0,
second1: 0,
second0: 0
},
isOver99Hours: false
}
},
watch: {
value: {
immediate: true,
handler() {
this.initDigits()
}
}
},
computed: {},
methods: {
initDigits() {
var totalSeconds = !this.value || isNaN(this.value) ? 0 : Number(this.value)
totalSeconds = Math.round(totalSeconds)
var minutes = Math.floor(totalSeconds / 60)
var seconds = totalSeconds - minutes * 60
var hours = Math.floor(minutes / 60)
minutes -= hours * 60
this.digits.second1 = seconds <= 9 ? 0 : Number(String(seconds)[0])
this.digits.second0 = seconds <= 9 ? seconds : Number(String(seconds)[1])
this.digits.minute1 = minutes <= 9 ? 0 : Number(String(minutes)[0])
this.digits.minute0 = minutes <= 9 ? minutes : Number(String(minutes)[1])
if (hours > 99) {
this.digits.hour2 = Number(String(hours)[0])
this.digits.hour1 = Number(String(hours)[1])
this.digits.hour0 = Number(String(hours)[2])
this.isOver99Hours = true
} else {
this.digits.hour1 = hours <= 9 ? 0 : Number(String(hours)[0])
this.digits.hour0 = hours <= 9 ? hours : Number(String(hours)[1])
this.isOver99Hours = this.showThreeDigitHour
}
if (this.isOver99Hours) {
this.digitDisplay = ['hour2', 'hour1', 'hour0', ':', 'minute1', 'minute0', ':', 'second1', 'second0']
} else {
this.digitDisplay = ['hour1', 'hour0', ':', 'minute1', 'minute0', ':', 'second1', 'second0']
}
},
updateSeconds() {
var seconds = this.digits.second0 + this.digits.second1 * 10
seconds += this.digits.minute0 * 60 + this.digits.minute1 * 600
seconds += this.digits.hour0 * 3600 + this.digits.hour1 * 36000
if (this.isOver99Hours) seconds += this.digits.hour2 * 360000
if (Number(this.value) !== seconds) {
this.$emit('input', seconds)
this.$emit('change', seconds)
}
},
clickMedian(index) {
// Click colon select digit to right
if (index >= 5) {
this.focusedDigit = 'second1'
} else {
this.focusedDigit = 'minute1'
}
},
clickOutside() {
this.removeFocus()
},
removeFocus() {
this.focusedDigit = null
this.removeListeners()
},
focusDigit(digit) {
if (this.focusedDigit == null || isNaN(this.focusedDigit)) this.initListeners()
this.focusedDigit = digit
},
clickInput() {
if (this.focusedDigit) return
this.focusDigit('second0')
},
shiftFocusLeft() {
if (!this.focusedDigit) return
if (this.focusedDigit.endsWith('2')) return
const isDigit1 = this.focusedDigit.endsWith('1')
if (!isDigit1) {
const digit1Key = this.focusedDigit.replace('0', '1')
this.focusedDigit = digit1Key
} else if (this.focusedDigit.startsWith('second')) {
this.focusedDigit = 'minute0'
} else if (this.focusedDigit.startsWith('minute')) {
this.focusedDigit = 'hour0'
} else if (this.isOver99Hours && this.focusedDigit.startsWith('hour')) {
this.focusedDigit = 'hour2'
}
},
shiftFocusRight() {
if (!this.focusedDigit) return
if (this.focusedDigit.endsWith('2')) {
// Must be hour2
this.focusedDigit = 'hour1'
return
}
const isDigit1 = this.focusedDigit.endsWith('1')
if (isDigit1) {
const digit0Key = this.focusedDigit.replace('1', '0')
this.focusedDigit = digit0Key
} else if (this.focusedDigit.startsWith('hour')) {
this.focusedDigit = 'minute1'
} else if (this.focusedDigit.startsWith('minute')) {
this.focusedDigit = 'second1'
}
},
increaseFocused() {
if (!this.focusedDigit) return
const isDigit1 = this.focusedDigit.endsWith('1')
const digit = Number(this.digits[this.focusedDigit])
if (isDigit1 && !this.focusedDigit.startsWith('hour')) this.digits[this.focusedDigit] = (digit + 1) % 6
else this.digits[this.focusedDigit] = (digit + 1) % 10
this.updateSeconds()
},
decreaseFocused() {
if (!this.focusedDigit) return
const isDigit1 = this.focusedDigit.endsWith('1')
const digit = Number(this.digits[this.focusedDigit])
if (isDigit1 && !this.focusedDigit.startsWith('hour')) this.digits[this.focusedDigit] = digit - 1 < 0 ? 5 : digit - 1
else this.digits[this.focusedDigit] = digit - 1 < 0 ? 9 : digit - 1
this.updateSeconds()
},
keydown(evt) {
if (!this.focusedDigit || !evt.key) return
if (evt.key === 'ArrowLeft') {
return this.shiftFocusLeft()
} else if (evt.key === 'ArrowRight') {
return this.shiftFocusRight()
} else if (evt.key === 'ArrowUp') {
return this.increaseFocused()
} else if (evt.key === 'ArrowDown') {
return this.decreaseFocused()
} else if (evt.key === 'Enter' || evt.key === 'Escape') {
return this.removeFocus()
}
if (isNaN(evt.key)) return
var digit = Number(evt.key)
const isDigit1 = this.focusedDigit.endsWith('1')
if (isDigit1 && !this.focusedDigit.startsWith('hour') && digit >= 6) {
digit = 5
}
this.digits[this.focusedDigit] = digit
this.updateSeconds()
this.shiftFocusRight()
},
initListeners() {
window.addEventListener('keydown', this.keydown)
},
removeListeners() {
window.removeEventListener('keydown', this.keydown)
}
},
mounted() {},
beforeDestroy() {
this.removeListeners()
}
}
</script>
<style scoped>
.digit-focused {
background-color: #555;
}
</style>
+1 -1
View File
@@ -53,7 +53,7 @@ export default {
var tooltip = document.createElement('div') var tooltip = document.createElement('div')
this.tooltipId = String(Math.floor(Math.random() * 10000)) this.tooltipId = String(Math.floor(Math.random() * 10000))
tooltip.id = this.tooltipId tooltip.id = this.tooltipId
tooltip.className = 'tooltip-wrapper absolute px-2 py-1 text-white pointer-events-none text-xs rounded shadow-lg max-w-xs' tooltip.className = 'tooltip-wrapper absolute px-2 py-1 text-white pointer-events-none text-xs rounded shadow-lg max-w-xs text-center hidden sm:block'
tooltip.style.zIndex = 100 tooltip.style.zIndex = 100
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)' tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
tooltip.innerHTML = this.text tooltip.innerHTML = this.text
+18 -18
View File
@@ -1,21 +1,21 @@
<template> <template>
<div class="w-full h-full relative"> <div class="w-full h-full relative">
<form class="w-full h-full px-4 py-6" @submit.prevent="submitForm"> <form class="w-full h-full px-2 md:px-4 py-6" @submit.prevent="submitForm">
<div class="flex -mx-1"> <div class="flex flex-wrap -mx-1">
<div class="w-1/2 px-1"> <div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="titleInput" v-model="details.title" label="Title" /> <ui-text-input-with-label ref="titleInput" v-model="details.title" label="Title" />
</div> </div>
<div class="flex-grow px-1"> <div class="flex-grow px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" label="Subtitle" /> <ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" label="Subtitle" />
</div> </div>
</div> </div>
<div class="flex mt-2 -mx-1"> <div class="flex flex-wrap mt-2 -mx-1">
<div class="w-3/4 px-1"> <div class="w-full md:w-3/4 px-1">
<!-- Authors filter only contains authors in this library, use query input to query all authors --> <!-- Authors filter only contains authors in this library, use query input to query all authors -->
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" label="Authors" endpoint="authors/search" /> <ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" label="Authors" endpoint="authors/search" />
</div> </div>
<div class="flex-grow px-1"> <div class="flex-grow px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" label="Publish Year" /> <ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" label="Publish Year" />
</div> </div>
</div> </div>
@@ -28,35 +28,35 @@
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" label="Description" class="mt-2" /> <ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" label="Description" class="mt-2" />
<div class="flex mt-2 -mx-1"> <div class="flex flex-wrap mt-2 -mx-1">
<div class="w-1/2 px-1"> <div class="w-full md:w-1/2 px-1">
<ui-multi-select ref="genresSelect" v-model="details.genres" label="Genres" :items="genres" /> <ui-multi-select ref="genresSelect" v-model="details.genres" label="Genres" :items="genres" />
</div> </div>
<div class="flex-grow px-1"> <div class="flex-grow px-1 mt-2 md:mt-0">
<ui-multi-select ref="tagsSelect" v-model="newTags" label="Tags" :items="tags" /> <ui-multi-select ref="tagsSelect" v-model="newTags" label="Tags" :items="tags" />
</div> </div>
</div> </div>
<div class="flex mt-2 -mx-1"> <div class="flex flex-wrap mt-2 -mx-1">
<div class="w-1/2 px-1"> <div class="w-full md:w-1/2 px-1">
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" label="Narrators" :items="narrators" /> <ui-multi-select ref="narratorsSelect" v-model="details.narrators" label="Narrators" :items="narrators" />
</div> </div>
<div class="w-1/4 px-1"> <div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" /> <ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" />
</div> </div>
<div class="w-1/4 px-1"> <div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" /> <ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" />
</div> </div>
</div> </div>
<div class="flex mt-2 -mx-1"> <div class="flex flex-wrap mt-2 -mx-1">
<div class="w-1/2 px-1"> <div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" label="Publisher" /> <ui-text-input-with-label ref="publisherInput" v-model="details.publisher" label="Publisher" />
</div> </div>
<div class="w-1/4 px-1"> <div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="languageInput" v-model="details.language" label="Language" /> <ui-text-input-with-label ref="languageInput" v-model="details.language" label="Language" />
</div> </div>
<div class="flex-grow px-1 pt-6"> <div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
<div class="flex justify-center"> <div class="flex justify-center">
<ui-checkbox v-model="details.explicit" label="Explicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" /> <ui-checkbox v-model="details.explicit" label="Explicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
</div> </div>
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<div :class="`h-${size} w-${size}`"> <div :class="`h-${size} w-${size} min-w-${size}`">
<component :is="iconComponentName" /> <component :is="iconComponentName" />
</div> </div>
</template> </template>
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="absolute w-36 bg-bg rounded-md border border-black-200 shadow-lg z-50" v-click-outside="clickOutsideObj" style="top: 0; left: 0"> <div class="absolute w-36 bg-bg rounded-md border border-black-200 shadow-lg z-50" v-click-outside="clickOutsideObj" style="top: 0; left: 0">
<template v-for="(item, index) in items"> <template v-for="(item, index) in items">
<div :key="index" class="flex h-7 items-center px-2 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click="clickAction(item.func)"> <div :key="index" class="flex h-7 items-center px-2 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.func)">
<p>{{ item.text }}</p> <p>{{ item.text }}</p>
</div> </div>
</template> </template>
+6 -1
View File
@@ -5,5 +5,10 @@
</template> </template>
<script> <script>
export default {} export default {
mounted() {
document.body.classList.remove('app-bar', 'app-bar-and-toolbar')
document.body.classList.add('no-bars')
}
}
</script> </script>
+33 -18
View File
@@ -15,6 +15,7 @@
<modals-podcast-edit-episode /> <modals-podcast-edit-episode />
<modals-podcast-view-episode /> <modals-podcast-view-episode />
<modals-authors-edit-modal /> <modals-authors-edit-modal />
<prompt-confirm />
<readers-reader /> <readers-reader />
</div> </div>
</template> </template>
@@ -40,6 +41,7 @@ export default {
if (this.$store.state.selectedLibraryItems) { if (this.$store.state.selectedLibraryItems) {
this.$store.commit('setSelectedLibraryItems', []) this.$store.commit('setSelectedLibraryItems', [])
} }
this.updateBodyClass()
} }
}, },
computed: { computed: {
@@ -53,11 +55,23 @@ export default {
if (!this.$route.name) return false if (!this.$route.name) return false
return !this.$route.name.startsWith('config') && this.$store.state.libraries.currentLibraryId return !this.$route.name.startsWith('config') && this.$store.state.libraries.currentLibraryId
}, },
isShowingToolbar() {
return this.isShowingSideRail && this.$route.name !== 'upload' && this.$route.name !== 'account'
},
appContentMarginLeft() { appContentMarginLeft() {
return this.isShowingSideRail ? 80 : 0 return this.isShowingSideRail ? 80 : 0
} }
}, },
methods: { methods: {
updateBodyClass() {
if (this.isShowingToolbar) {
document.body.classList.remove('no-bars', 'app-bar')
document.body.classList.add('app-bar-and-toolbar')
} else {
document.body.classList.remove('no-bars', 'app-bar-and-toolbar')
document.body.classList.add('app-bar')
}
},
updateSocketConnectionToast(content, type, timeout) { updateSocketConnectionToast(content, type, timeout) {
if (this.socketConnectionToastId !== null && this.socketConnectionToastId !== undefined) { if (this.socketConnectionToastId !== null && this.socketConnectionToastId !== undefined) {
this.$toast.update(this.socketConnectionToastId, { content: content, options: { timeout: timeout, type: type, closeButton: false, position: 'bottom-center', onClose: () => null, closeOnClick: timeout !== null } }, false) this.$toast.update(this.socketConnectionToastId, { content: content, options: { timeout: timeout, type: type, closeButton: false, position: 'bottom-center', onClose: () => null, closeOnClick: timeout !== null } }, false)
@@ -110,13 +124,7 @@ export default {
} }
console.log('Init Payload', payload) console.log('Init Payload', payload)
if (payload.session) { if (payload.session) {
if (this.$refs.streamContainer) { this.$refs.streamContainer.sessionOpen(payload.session)
this.$refs.streamContainer.sessionOpen(payload.session)
} else {
console.warn('Stream Container not mounted')
}
}
if (payload.serverSettings) {
} }
// Start scans currently running // Start scans currently running
@@ -203,6 +211,12 @@ export default {
libraryItemUpdated(libraryItem) { libraryItemUpdated(libraryItem) {
if (this.$store.state.selectedLibraryItem && this.$store.state.selectedLibraryItem.id === libraryItem.id) { if (this.$store.state.selectedLibraryItem && this.$store.state.selectedLibraryItem.id === libraryItem.id) {
this.$store.commit('setSelectedLibraryItem', libraryItem) this.$store.commit('setSelectedLibraryItem', libraryItem)
if (this.$store.state.globals.selectedEpisode && libraryItem.mediaType === 'podcast') {
const episode = libraryItem.media.episodes.find((ep) => ep.id === this.$store.state.globals.selectedEpisode.id)
if (episode) {
this.$store.commit('globals/setSelectedEpisode', episode)
}
}
} }
this.$eventBus.$emit(`${libraryItem.id}_updated`, libraryItem) this.$eventBus.$emit(`${libraryItem.id}_updated`, libraryItem)
this.$store.commit('libraries/updateFilterDataWithItem', libraryItem) this.$store.commit('libraries/updateFilterDataWithItem', libraryItem)
@@ -243,9 +257,9 @@ export default {
var existingScan = this.$store.getters['scanners/getLibraryScan'](data.id) var existingScan = this.$store.getters['scanners/getLibraryScan'](data.id)
if (existingScan && !isNaN(existingScan.toastId)) { if (existingScan && !isNaN(existingScan.toastId)) {
this.$toast.update(existingScan.toastId, { content: message, options: { timeout: 5000, type: 'success', closeButton: false, position: 'bottom-center', onClose: () => null } }, true) this.$toast.update(existingScan.toastId, { content: message, options: { timeout: 5000, type: 'success', closeButton: false, onClose: () => null } }, true)
} else { } else {
this.$toast.success(message, { timeout: 5000, position: 'bottom-center' }) this.$toast.success(message, { timeout: 5000 })
} }
this.$store.commit('scanners/remove', data) this.$store.commit('scanners/remove', data)
@@ -254,7 +268,7 @@ export default {
this.$root.socket.emit('cancel_scan', id) this.$root.socket.emit('cancel_scan', id)
}, },
scanStart(data) { scanStart(data) {
data.toastId = this.$toast(`${data.type === 'match' ? 'Matching' : 'Scanning'} "${data.name}"...`, { timeout: false, type: 'info', draggable: false, closeOnClick: false, closeButton: CloseButton, closeButtonClassName: 'cancel-scan-btn', showCloseButtonOnHover: false, position: 'bottom-center', onClose: () => this.onScanToastCancel(data.id) }) data.toastId = this.$toast(`${data.type === 'match' ? 'Matching' : 'Scanning'} "${data.name}"...`, { timeout: false, type: 'info', draggable: false, closeOnClick: false, closeButton: CloseButton, closeButtonClassName: 'cancel-scan-btn', showCloseButtonOnHover: false, onClose: () => this.onScanToastCancel(data.id) })
this.$store.commit('scanners/addUpdate', data) this.$store.commit('scanners/addUpdate', data)
}, },
scanProgress(data) { scanProgress(data) {
@@ -263,7 +277,7 @@ export default {
data.toastId = existingScan.toastId data.toastId = existingScan.toastId
this.$toast.update(existingScan.toastId, { content: `Scanning "${existingScan.name}"... ${data.progress.progress || 0}%`, options: { timeout: false } }, true) this.$toast.update(existingScan.toastId, { content: `Scanning "${existingScan.name}"... ${data.progress.progress || 0}%`, options: { timeout: false } }, true)
} else { } else {
data.toastId = this.$toast(`Scanning "${data.name}"...`, { timeout: false, type: 'info', draggable: false, closeOnClick: false, closeButton: CloseButton, closeButtonClassName: 'cancel-scan-btn', showCloseButtonOnHover: false, position: 'bottom-center', onClose: () => this.onScanToastCancel(data.id) }) data.toastId = this.$toast(`Scanning "${data.name}"...`, { timeout: false, type: 'info', draggable: false, closeOnClick: false, closeButton: CloseButton, closeButtonClassName: 'cancel-scan-btn', showCloseButtonOnHover: false, onClose: () => this.onScanToastCancel(data.id) })
} }
this.$store.commit('scanners/addUpdate', data) this.$store.commit('scanners/addUpdate', data)
@@ -347,11 +361,11 @@ export default {
download.status = this.$constants.DownloadStatus.EXPIRED download.status = this.$constants.DownloadStatus.EXPIRED
this.$store.commit('downloads/addUpdateDownload', download) this.$store.commit('downloads/addUpdateDownload', download)
}, },
showErrorToast(message) { rssFeedOpen(data) {
this.$toast.error(message) this.$store.commit('feeds/addFeed', data)
}, },
showSuccessToast(message) { rssFeedClosed(data) {
this.$toast.success(message) this.$store.commit('feeds/removeFeed', data)
}, },
backupApplied() { backupApplied() {
// Force refresh // Force refresh
@@ -423,9 +437,9 @@ export default {
this.socket.on('abmerge_killed', this.abmergeKilled) this.socket.on('abmerge_killed', this.abmergeKilled)
this.socket.on('abmerge_expired', this.abmergeExpired) this.socket.on('abmerge_expired', this.abmergeExpired)
// Toast Listeners // Feed Listeners
this.socket.on('show_error_toast', this.showErrorToast) this.socket.on('rss_feed_open', this.rssFeedOpen)
this.socket.on('show_success_toast', this.showSuccessToast) this.socket.on('rss_feed_closed', this.rssFeedClosed)
this.socket.on('backup_applied', this.backupApplied) this.socket.on('backup_applied', this.backupApplied)
}, },
@@ -527,6 +541,7 @@ export default {
this.initializeSocket() this.initializeSocket()
}, },
mounted() { mounted() {
this.updateBodyClass()
this.resize() this.resize()
window.addEventListener('resize', this.resize) window.addEventListener('resize', this.resize)
window.addEventListener('keydown', this.keyDown) window.addEventListener('keydown', this.keyDown)
+2 -2
View File
@@ -52,13 +52,13 @@ export default {
width: this.entityWidth, width: this.entityWidth,
height: this.entityHeight, height: this.entityHeight,
bookCoverAspectRatio: this.bookCoverAspectRatio, bookCoverAspectRatio: this.bookCoverAspectRatio,
bookshelfView: this.bookshelfView bookshelfView: this.bookshelfView,
sortingIgnorePrefix: !!this.sortingIgnorePrefix
} }
if (this.entityName === 'books') { if (this.entityName === 'books') {
props.filterBy = this.filterBy props.filterBy = this.filterBy
props.orderBy = this.orderBy props.orderBy = this.orderBy
props.sortingIgnorePrefix = !!this.sortingIgnorePrefix
} }
var _this = this var _this = this
+41 -11
View File
@@ -28,11 +28,11 @@ export default {
var validOtherFiles = [] var validOtherFiles = []
var ignoredFiles = [] var ignoredFiles = []
files.forEach((file) => { files.forEach((file) => {
var filetype = this.checkFileType(file.name) // var filetype = this.checkFileType(file.name)
if (!filetype) ignoredFiles.push(file) if (!file.filetype) ignoredFiles.push(file)
else { else {
file.filetype = filetype // file.filetype = filetype
if (filetype === 'audio' || (filetype === 'ebook' && mediaType === 'book')) validItemFiles.push(file) if (file.filetype === 'audio' || (file.filetype === 'ebook' && mediaType === 'book')) validItemFiles.push(file)
else validOtherFiles.push(file) else validOtherFiles.push(file)
} }
}) })
@@ -82,12 +82,18 @@ export default {
items: itemResults, items: itemResults,
ignoredFiles: ignoredFilesInRoot ignoredFiles: ignoredFilesInRoot
} }
} else { } else if (filetree.some((f) => f.filetype !== 'audio') || mediaType !== 'book') {
// Single Book drop // Single Book drop
return { return {
items: this.itemFromTreeItems(filetree, mediaType), items: this.itemFromTreeItems(filetree, mediaType),
ignoredFiles: [] ignoredFiles: []
} }
} else {
// Only audio files dropped so treat each one as an audiobook
return {
items: filetree.map((audioFile) => ({ itemFiles: [audioFile], otherFiles: [], ignoredFiles: [] })),
ignoredFiles: []
}
} }
}, },
getFilesDropped(dataTransferItems) { getFilesDropped(dataTransferItems) {
@@ -95,11 +101,12 @@ export default {
path: '/', path: '/',
items: [] items: []
} }
function traverseFileTreePromise(item, currtreemap) { function traverseFileTreePromise(item, currtreemap, checkFileType) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (item.isFile) { if (item.isFile) {
item.file((file) => { item.file((file) => {
file.filepath = currtreemap.path + file.name //save full path file.filepath = currtreemap.path + file.name //save full path
file.filetype = checkFileType(file.name)
currtreemap.items.push(file) currtreemap.items.push(file)
resolve(file) resolve(file)
}) })
@@ -119,7 +126,7 @@ export default {
dirReader.readEntries((entries) => { dirReader.readEntries((entries) => {
if (entries.length > 0) { if (entries.length > 0) {
for (let entr of entries) { for (let entr of entries) {
entriesPromises.push(traverseFileTreePromise(entr, newtreemap)) entriesPromises.push(traverseFileTreePromise(entr, newtreemap, checkFileType))
} }
readEntries() readEntries()
} else { } else {
@@ -135,7 +142,7 @@ export default {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let entriesPromises = [] let entriesPromises = []
for (let it of dataTransferItems) { for (let it of dataTransferItems) {
var filetree = traverseFileTreePromise(it.webkitGetAsEntry(), treemap) var filetree = traverseFileTreePromise(it.webkitGetAsEntry(), treemap, this.checkFileType)
entriesPromises.push(filetree) entriesPromises.push(filetree)
} }
Promise.all(entriesPromises).then(() => { Promise.all(entriesPromises).then(() => {
@@ -152,7 +159,9 @@ export default {
...book ...book
} }
var firstBookFile = book.itemFiles[0] var firstBookFile = book.itemFiles[0]
if (!firstBookFile.filepath) return audiobook // No path if (!firstBookFile.filepath) {
return audiobook // No path
}
var firstBookPath = Path.dirname(firstBookFile.filepath) var firstBookPath = Path.dirname(firstBookFile.filepath)
@@ -165,6 +174,9 @@ export default {
if (dirs.length) { if (dirs.length) {
audiobook.author = dirs.pop() audiobook.author = dirs.pop()
} }
} else {
// Use file basename as title
audiobook.title = Path.basename(firstBookFile.name, Path.extname(firstBookFile.name))
} }
return audiobook return audiobook
}, },
@@ -178,7 +190,12 @@ export default {
if (!firstAudioFile.filepath) return podcast // No path if (!firstAudioFile.filepath) return podcast // No path
var firstPath = Path.dirname(firstAudioFile.filepath) var firstPath = Path.dirname(firstAudioFile.filepath)
var dirs = firstPath.split('/').filter(d => !!d && d !== '.') var dirs = firstPath.split('/').filter(d => !!d && d !== '.')
podcast.title = dirs.length > 1 ? dirs[1] : dirs[0] if (dirs.length) {
podcast.title = dirs.length > 1 ? dirs[1] : dirs[0]
} else {
podcast.title = Path.basename(firstAudioFile.name, Path.extname(firstAudioFile.name))
}
return podcast return podcast
}, },
cleanItem(item, mediaType, index) { cleanItem(item, mediaType, index) {
@@ -188,6 +205,7 @@ export default {
async getItemsFromDataTransferItems(dataTransferItems, mediaType) { async getItemsFromDataTransferItems(dataTransferItems, mediaType) {
var files = await this.getFilesDropped(dataTransferItems) var files = await this.getFilesDropped(dataTransferItems)
if (!files || !files.length) return { error: 'No files found ' } if (!files || !files.length) return { error: 'No files found ' }
var itemData = this.fileTreeToItems(files, mediaType) var itemData = this.fileTreeToItems(files, mediaType)
if (!itemData.items.length && !itemData.ignoredFiles.length) { if (!itemData.items.length && !itemData.ignoredFiles.length) {
return { error: 'Invalid file drop' } return { error: 'Invalid file drop' }
@@ -218,9 +236,12 @@ export default {
else { else {
file.filetype = filetype file.filetype = filetype
if (file.webkitRelativePath) file.filepath = file.webkitRelativePath if (file.webkitRelativePath) file.filepath = file.webkitRelativePath
else file.filepath = file.name
if (filetype === 'audio' || (filetype === 'ebook' && mediaType === 'book')) { if (filetype === 'audio' || (filetype === 'ebook' && mediaType === 'book')) {
var dir = file.filepath ? Path.dirname(file.filepath) : '' var dir = file.filepath ? Path.dirname(file.filepath) : ''
if (dir === '.') dir = ''
if (!itemMap[dir]) { if (!itemMap[dir]) {
itemMap[dir] = { itemMap[dir] = {
path: dir, path: dir,
@@ -246,8 +267,17 @@ export default {
} }
}) })
var items = []
var index = 1 var index = 1
var items = Object.values(itemMap).map(i => this.cleanItem(i, mediaType, index++)) // If book media type and all files are audio files then treat each one as an audiobook
if (itemMap[''] && !otherFiles.length && mediaType === 'book' && !itemMap[''].itemFiles.some(f => f.filetype !== 'audio')) {
items = itemMap[''].itemFiles.map((audioFile) => {
return this.cleanItem({ itemFiles: [audioFile], otherFiles: [], ignoredFiles: [] }, mediaType, index++)
})
} else {
items = Object.values(itemMap).map(i => this.cleanItem(i, mediaType, index++))
}
return { return {
items, items,
ignoredFiles: ignoredFiles ignoredFiles: ignoredFiles
+25 -6
View File
@@ -58,7 +58,8 @@ module.exports = {
buildModules: [ buildModules: [
// https://go.nuxtjs.dev/tailwindcss // https://go.nuxtjs.dev/tailwindcss
'@nuxtjs/tailwindcss', '@nuxtjs/tailwindcss',
'@nuxtjs/pwa' '@nuxtjs/pwa',
'@nuxt/postcss8'
], ],
// Modules: https://go.nuxtjs.dev/config-modules // Modules: https://go.nuxtjs.dev/config-modules
@@ -107,23 +108,33 @@ module.exports = {
background_color: '#373838', background_color: '#373838',
icons: [ icons: [
{ {
src: '/icon64.png', src: '/icon.svg',
sizes: "64x64" sizes: "64x64"
}, },
{ {
src: '/icon192.png', src: '/icon.svg',
sizes: "192x192" sizes: "192x192"
}, },
{ {
src: '/Logo.png', src: '/icon.svg',
sizes: "512x512" sizes: "512x512"
} }
] ]
},
workbox: {
enabled: false,
} }
}, },
// Build Configuration: https://go.nuxtjs.dev/config-build // Build Configuration: https://go.nuxtjs.dev/config-build
build: {}, build: {
postcss: {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
},
watchers: { watchers: {
webpack: { webpack: {
aggregateTimeout: 300, aggregateTimeout: 300,
@@ -133,5 +144,13 @@ module.exports = {
server: { server: {
port: process.env.NODE_ENV === 'production' ? 80 : 3000, port: process.env.NODE_ENV === 'production' ? 80 : 3000,
host: '0.0.0.0' host: '0.0.0.0'
} },
/**
* Temporary workaround for @nuxt-community/tailwindcss-module.
*
* Reported: 2022-05-23
* See: [Issue tracker](https://github.com/nuxt-community/tailwindcss-module/issues/480)
*/
devServerHandlers: [],
} }
+566 -397
View File
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.0.20", "version": "2.1.2",
"description": "Self-hosted audiobook and podcast client", "description": "Self-hosted audiobook and podcast client",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -29,8 +29,11 @@
"vuedraggable": "^2.24.3" "vuedraggable": "^2.24.3"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/postcss8": "^1.1.3",
"@nuxtjs/pwa": "^3.3.5", "@nuxtjs/pwa": "^3.3.5",
"@nuxtjs/tailwindcss": "^4.2.1", "@nuxtjs/tailwindcss": "^4.2.1",
"postcss": "^8.3.6" "autoprefixer": "^10.4.7",
"postcss": "^8.3.6",
"tailwindcss": "^3.1.4"
} }
} }
+16 -6
View File
@@ -17,6 +17,7 @@
<div class="flex items-center"> <div class="flex items-center">
<p class="text-lg mb-4 font-semibold">Audiobook Chapters</p> <p class="text-lg mb-4 font-semibold">Audiobook Chapters</p>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-checkbox v-model="showSecondInputs" checkbox-bg="primary" small label-class="text-sm text-gray-200 pl-1" label="Show seconds" class="mx-2" />
<ui-btn color="primary" small class="mx-2" @click="showFindChaptersModal = true">Lookup</ui-btn> <ui-btn color="primary" small class="mx-2" @click="showFindChaptersModal = true">Lookup</ui-btn>
<ui-btn color="success" small @click="saveChapters">Save</ui-btn> <ui-btn color="success" small @click="saveChapters">Save</ui-btn>
<div class="w-40" /> <div class="w-40" />
@@ -32,7 +33,8 @@
<div :key="chapter.id" class="flex py-1"> <div :key="chapter.id" class="flex py-1">
<div class="w-12">#{{ chapter.id + 1 }}</div> <div class="w-12">#{{ chapter.id + 1 }}</div>
<div class="w-32 px-1"> <div class="w-32 px-1">
<ui-text-input v-model="chapter.start" type="number" class="text-xs" @change="checkChapters" /> <ui-text-input v-if="showSecondInputs" v-model="chapter.start" type="number" class="text-xs" @change="checkChapters" />
<ui-time-picker v-else class="text-xs" v-model="chapter.start" :show-three-digit-hour="mediaDuration >= 360000" @change="checkChapters" />
</div> </div>
<div class="flex-grow px-1"> <div class="flex-grow px-1">
<ui-text-input v-model="chapter.title" class="text-xs" /> <ui-text-input v-model="chapter.title" class="text-xs" />
@@ -136,7 +138,7 @@
<script> <script>
export default { export default {
async asyncData({ store, params, app, redirect, route }) { async asyncData({ store, params, app, redirect, from }) {
if (!store.getters['user/getUserCanUpdate']) { if (!store.getters['user/getUserCanUpdate']) {
return redirect('/?error=unauthorized') return redirect('/?error=unauthorized')
} }
@@ -152,8 +154,12 @@ export default {
console.error('Invalid media type') console.error('Invalid media type')
return redirect('/') return redirect('/')
} }
var previousRoute = from ? from.fullPath : null
if (from && from.path === '/login') previousRoute = null
return { return {
libraryItem libraryItem,
previousRoute
} }
}, },
data() { data() {
@@ -168,7 +174,8 @@ export default {
asinInput: null, asinInput: null,
findingChapters: false, findingChapters: false,
showFindChaptersModal: false, showFindChaptersModal: false,
chapterData: null chapterData: null,
showSecondInputs: false
} }
}, },
computed: { computed: {
@@ -339,7 +346,6 @@ export default {
this.saving = true this.saving = true
console.log('udpated chapters', this.newChapters)
const payload = { const payload = {
chapters: this.newChapters chapters: this.newChapters
} }
@@ -349,7 +355,11 @@ export default {
this.saving = false this.saving = false
if (data.updated) { if (data.updated) {
this.$toast.success('Chapters updated') this.$toast.success('Chapters updated')
this.$router.push(`/item/${this.libraryItem.id}`) if (this.previousRoute) {
this.$router.push(this.previousRoute)
} else {
this.$router.push(`/item/${this.libraryItem.id}`)
}
} else { } else {
this.$toast.info('No changes needed updating') this.$toast.info('No changes needed updating')
} }
+2 -2
View File
@@ -8,8 +8,8 @@
</div> </div>
</div> </div>
<div class="flex-grow px-2 py-6 md:py-0 md:px-10"> <div class="flex-grow px-2 py-6 md:py-0 md:px-10">
<div class="flex sm:items-end flex-col sm:flex-row"> <div class="flex items-end flex-row flex-wrap md:flex-nowrap">
<h1 class="text-2xl md:text-3xl font-sans"> <h1 class="text-2xl md:text-3xl font-sans w-full md:w-fit mb-4 md:mb-0">
{{ collectionName }} {{ collectionName }}
</h1> </h1>
<div class="flex-grow" /> <div class="flex-grow" />
+8
View File
@@ -71,6 +71,11 @@ export default {
.configContent.page-library-stats { .configContent.page-library-stats {
width: 1200px; width: 1200px;
} }
@media (max-width: 1550px) {
.configContent.page-library-stats {
margin-left: 176px;
}
}
@media (max-width: 1240px) { @media (max-width: 1240px) {
.configContent { .configContent {
margin-left: 176px; margin-left: 176px;
@@ -82,5 +87,8 @@ export default {
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
} }
.configContent.page-library-stats {
margin-left: 0px;
}
} }
</style> </style>
+22 -2
View File
@@ -14,6 +14,12 @@
</ui-tooltip> </ui-tooltip>
</div> </div>
<!-- <div class="flex items-center py-2">
<ui-text-input v-model="cronExpression" :disabled="updatingServerSettings" class="w-32" @change="changedCronExpression" />
<p class="pl-4 text-lg">Cron expression</p>
</div> -->
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-text-input type="number" v-model="backupsToKeep" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateBackupsSettings" /> <ui-text-input type="number" v-model="backupsToKeep" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateBackupsSettings" />
@@ -41,6 +47,7 @@ export default {
dailyBackups: true, dailyBackups: true,
backupsToKeep: 2, backupsToKeep: 2,
maxBackupSize: 1, maxBackupSize: 1,
// cronExpression: '',
newServerSettings: {} newServerSettings: {}
} }
}, },
@@ -54,7 +61,7 @@ export default {
}, },
computed: { computed: {
dailyBackupsTooltip() { dailyBackupsTooltip() {
return 'Runs at 1am every day (your server time). Saved in /metadata/backups.' return 'Runs at 1:30am every day (your server time). Saved in /metadata/backups.'
}, },
maxBackupSizeTooltip() { maxBackupSizeTooltip() {
return 'As a safeguard against misconfiguration, backups will fail if they exceed the configured size.' return 'As a safeguard against misconfiguration, backups will fail if they exceed the configured size.'
@@ -64,6 +71,18 @@ export default {
} }
}, },
methods: { methods: {
// changedCronExpression() {
// this.$axios
// .$post('/api/validate-cron', { expression: this.cronExpression })
// .then(() => {
// console.log('Cron is valid')
// })
// .catch((error) => {
// console.error('Cron validation failed', error)
// const msg = (error.response ? error.response.data : null) || 'Unknown cron validation error'
// this.$toast.error(msg)
// })
// },
updateBackupsSettings() { updateBackupsSettings() {
if (isNaN(this.maxBackupSize) || this.maxBackupSize <= 0) { if (isNaN(this.maxBackupSize) || this.maxBackupSize <= 0) {
this.$toast.error('Invalid maximum backup size') this.$toast.error('Invalid maximum backup size')
@@ -74,7 +93,7 @@ export default {
return return
} }
var updatePayload = { var updatePayload = {
backupSchedule: this.dailyBackups ? '0 1 * * *' : false, backupSchedule: this.dailyBackups ? '30 1 * * *' : false,
backupsToKeep: Number(this.backupsToKeep), backupsToKeep: Number(this.backupsToKeep),
maxBackupSize: Number(this.maxBackupSize) maxBackupSize: Number(this.maxBackupSize)
} }
@@ -99,6 +118,7 @@ export default {
this.backupsToKeep = this.newServerSettings.backupsToKeep || 2 this.backupsToKeep = this.newServerSettings.backupsToKeep || 2
this.dailyBackups = !!this.newServerSettings.backupSchedule this.dailyBackups = !!this.newServerSettings.backupSchedule
this.maxBackupSize = this.newServerSettings.maxBackupSize || 1 this.maxBackupSize = this.newServerSettings.maxBackupSize || 1
// this.cronExpression = '30 1 * * *'
} }
}, },
mounted() { mounted() {
+220 -153
View File
@@ -1,150 +1,210 @@
<template> <template>
<div> <div>
<!-- <div class="h-0.5 bg-primary bg-opacity-50 w-full" /> --> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-2">
<div class="mb-2">
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
<div class="flex items-center mb-2">
<h1 class="text-xl">Settings</h1> <h1 class="text-xl">Settings</h1>
</div> </div>
<div class="flex items-center py-2"> <div class="lg:flex">
<ui-toggle-switch v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" /> <div class="flex-1">
<ui-tooltip :text="tooltips.storeCoverWithItem"> <div class="pt-4">
<p class="pl-4 text-lg"> <h2 class="font-semibold">General</h2>
Store covers with item </div>
<span class="material-icons icon-text">info_outlined</span> <div class="flex items-end py-2">
</p> <ui-toggle-switch v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
</ui-tooltip> <ui-tooltip :text="tooltips.storeCoverWithItem">
</div> <p class="pl-4">
Store covers with item
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.storeMetadataWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithItem', val)" /> <ui-toggle-switch v-model="newServerSettings.storeMetadataWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithItem', val)" />
<ui-tooltip :text="tooltips.storeMetadataWithItem"> <ui-tooltip :text="tooltips.storeMetadataWithItem">
<p class="pl-4 text-lg"> <p class="pl-4">
Store metadata with item Store metadata with item
<span class="material-icons icon-text">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="useSquareBookCovers" :disabled="updatingServerSettings" @input="updateBookCoverAspectRatio" /> <ui-toggle-switch v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
<ui-tooltip :text="tooltips.coverAspectRatio"> <ui-tooltip :text="tooltips.sortingIgnorePrefix">
<p class="pl-4 text-lg"> <p class="pl-4">
Use square book covers Ignore prefixes when sorting
<span class="material-icons icon-text">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div v-if="newServerSettings.sortingIgnorePrefix" class="w-72 ml-14 mb-2">
<ui-multi-select v-model="newServerSettings.sortingPrefixes" small :items="newServerSettings.sortingPrefixes" label="Prefixes to Ignore (case insensitive)" @input="updateSortingPrefixes" :disabled="updatingServerSettings" />
</div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="useAlternativeBookshelfView" :disabled="updatingServerSettings" @input="updateAlternativeBookshelfView" /> <ui-toggle-switch v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
<ui-tooltip :text="tooltips.bookshelfView"> <p class="pl-4">Chromecast support</p>
<p class="pl-4 text-lg"> </div>
Use alternative bookshelf view
<span class="material-icons icon-text">info_outlined</span>
</p>
</ui-tooltip>
</div>
<div class="flex items-center py-2"> <div class="pt-4">
<ui-toggle-switch v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" /> <h2 class="font-semibold">Display</h2>
<ui-tooltip :text="tooltips.sortingIgnorePrefix"> </div>
<p class="pl-4 text-lg">
Ignore prefixes when sorting title and series
<span class="material-icons icon-text">info_outlined</span>
</p>
</ui-tooltip>
</div>
<div v-if="newServerSettings.sortingIgnorePrefix" class="w-72 ml-14 mb-2">
<ui-multi-select v-model="newServerSettings.sortingPrefixes" small :items="newServerSettings.sortingPrefixes" label="Prefixes to Ignore (case insensitive)" @input="updateSortingPrefixes" :disabled="updatingServerSettings" />
</div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" /> <ui-toggle-switch v-model="useSquareBookCovers" :disabled="updatingServerSettings" @input="updateBookCoverAspectRatio" />
<p class="pl-4 text-lg">Enable Chromecast</p> <ui-tooltip :text="tooltips.coverAspectRatio">
</div> <p class="pl-4">
Square book covers
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div>
<div class="flex items-center mb-2 mt-8"> <div class="flex items-center py-2">
<h1 class="text-xl">Scanner Settings</h1> <ui-toggle-switch v-model="useAlternativeBookshelfView" :disabled="updatingServerSettings" @input="updateAlternativeBookshelfView" />
</div> <ui-tooltip :text="tooltips.bookshelfView">
<p class="pl-4">
Alternative bookshelf view
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" /> <p class="pr-4">Date Format</p>
<ui-tooltip :text="tooltips.scannerParseSubtitle"> <ui-dropdown v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-40" @input="(val) => updateSettingsKey('dateFormat', val)" />
<p class="pl-4 text-lg"> </div>
Scanner parse subtitles </div>
<span class="material-icons icon-text">info_outlined</span>
</p>
</ui-tooltip>
</div>
<div class="flex items-center py-2"> <div class="flex-1">
<ui-toggle-switch v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" /> <div class="pt-4">
<ui-tooltip :text="tooltips.scannerFindCovers"> <h2 class="font-semibold">Scanner</h2>
<p class="pl-4 text-lg"> </div>
Scanner find covers
<span class="material-icons icon-text">info_outlined</span>
</p>
</ui-tooltip>
<div class="flex-grow" />
</div>
<div v-if="newServerSettings.scannerFindCovers" class="w-44 ml-14 mb-2">
<ui-dropdown v-model="newServerSettings.scannerCoverProvider" small :items="providers" label="Cover Provider" @input="updateScannerCoverProvider" :disabled="updatingServerSettings" />
</div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerPreferAudioMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferAudioMetadata', val)" /> <ui-toggle-switch v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" />
<ui-tooltip :text="tooltips.scannerPreferAudioMetadata"> <ui-tooltip :text="tooltips.scannerParseSubtitle">
<p class="pl-4 text-lg"> <p class="pl-4">
Scanner prefer audio metadata Parse subtitles
<span class="material-icons icon-text">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerPreferOpfMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOpfMetadata', val)" /> <ui-toggle-switch v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" />
<ui-tooltip :text="tooltips.scannerPreferOpfMetadata"> <ui-tooltip :text="tooltips.scannerFindCovers">
<p class="pl-4 text-lg"> <p class="pl-4">
Scanner prefer OPF metadata Find covers
<span class="material-icons icon-text">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> <div class="flex-grow" />
</div>
<div v-if="newServerSettings.scannerFindCovers" class="w-44 ml-14 mb-2">
<ui-dropdown v-model="newServerSettings.scannerCoverProvider" small :items="providers" label="Cover Provider" @input="updateScannerCoverProvider" :disabled="updatingServerSettings" />
</div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" /> <ui-toggle-switch v-model="newServerSettings.scannerPreferOverdriveMediaMarker" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOverdriveMediaMarker', val)" />
<ui-tooltip :text="tooltips.scannerPreferMatchedMetadata"> <ui-tooltip :text="tooltips.scannerPreferOverdriveMediaMarker">
<p class="pl-4 text-lg"> <p class="pl-4">
Scanner prefer matched metadata Use Overdrive Media Markers for chapters
<span class="material-icons icon-text">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerDisableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', val)" /> <ui-toggle-switch v-model="newServerSettings.scannerPreferAudioMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferAudioMetadata', val)" />
<ui-tooltip :text="tooltips.scannerDisableWatcher"> <ui-tooltip :text="tooltips.scannerPreferAudioMetadata">
<p class="pl-4 text-lg"> <p class="pl-4">
Disable Watcher Prefer audio metadata
<span class="material-icons icon-text">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div class="flex items-center mb-2 mt-8"> <div class="flex items-center py-2">
<h1 class="text-xl">Experimental Feature Settings</h1> <ui-toggle-switch v-model="newServerSettings.scannerPreferOpfMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOpfMetadata', val)" />
</div> <ui-tooltip :text="tooltips.scannerPreferOpfMetadata">
<p class="pl-4">
Prefer OPF metadata
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.enableEReader" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('enableEReader', val)" /> <ui-toggle-switch v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
<ui-tooltip :text="tooltips.enableEReader"> <ui-tooltip :text="tooltips.scannerPreferMatchedMetadata">
<p class="pl-4 text-lg"> <p class="pl-4">
Enable e-reader for all users Prefer matched metadata
<span class="material-icons icon-text">info_outlined</span> <span class="material-icons icon-text text-sm">info_outlined</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerDisableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', val)" />
<ui-tooltip :text="tooltips.scannerDisableWatcher">
<p class="pl-4">
Disable Watcher
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div>
<!-- <div class="flex items-center py-2">
<ui-text-input type="number" v-model="newServerSettings.scannerMaxThreads" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateScannerMaxThreads" />
<ui-tooltip :text="tooltips.scannerMaxThreads">
<p class="pl-4">
Max # of threads to use
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div> -->
<div class="pt-4">
<h2 class="font-semibold">Experimental Features</h2>
</div>
<div class="flex items-center py-2">
<div class="flex items-center">
<ui-toggle-switch v-model="showExperimentalFeatures" />
<ui-tooltip :text="tooltips.experimentalFeatures">
<p class="pl-4">
Experimental Features
<a href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank">
<span class="material-icons icon-text text-sm">info_outlined</span>
</a>
</p>
</ui-tooltip>
</div>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.enableEReader" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('enableEReader', val)" />
<ui-tooltip :text="tooltips.enableEReader">
<p class="pl-4">
Enable e-reader for all users
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div>
<!-- <div class="flex items-center py-2">
<ui-toggle-switch v-model="newServerSettings.scannerUseSingleThreadedProber" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerUseSingleThreadedProber', val)" />
<ui-tooltip :text="tooltips.scannerUseSingleThreadedProber">
<p class="pl-4">
Scanner use old single threaded audio prober
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div> -->
</div>
</div> </div>
</div> </div>
@@ -152,7 +212,7 @@
<div class="flex items-center py-4"> <div class="flex items-center py-4">
<ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isPurgingCache" @click="purgeCache">Purge Cache</ui-btn> <ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isPurgingCache" @click="purgeCache">Purge Cache</ui-btn>
<ui-btn color="bg" small :padding-x="4" class="hidden lg:block" :loading="isResettingLibraryItems" @click="resetLibraryItems">Remove All Library Items</ui-btn> <ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isResettingLibraryItems" @click="resetLibraryItems">Remove All Library Items</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="pr-2 text-sm font-book text-yellow-400"> <p class="pr-2 text-sm font-book text-yellow-400">
Report bugs, request features, and contribute on Report bugs, request features, and contribute on
@@ -188,30 +248,12 @@
<div class="h-0.5 bg-primary bg-opacity-30 w-full" /> <div class="h-0.5 bg-primary bg-opacity-30 w-full" />
<div class="py-12 mb-4 opacity-60 hover:opacity-100">
<div class="flex items-center">
<div>
<div class="flex items-center">
<ui-toggle-switch v-model="showExperimentalFeatures" />
<ui-tooltip :text="tooltips.experimentalFeatures">
<p class="pl-4 text-lg">
Experimental Features
<a href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank">
<span class="material-icons icon-text">info_outlined</span>
</a>
</p>
</ui-tooltip>
</div>
</div>
</div>
</div>
<prompt-dialog v-model="showConfirmPurgeCache" :width="675"> <prompt-dialog v-model="showConfirmPurgeCache" :width="675">
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300"> <div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
<p class="text-error text-lg font-semibold">Important Notice!</p> <p class="text-error font-semibold">Important Notice!</p>
<p class="text-lg my-2 text-center">Purge cache will delete the entire directory at <span class="font-mono">/metadata/cache</span>.</p> <p class="my-2 text-center">Purge cache will delete the entire directory at <span class="font-mono">/metadata/cache</span>.</p>
<p class="text-lg text-center mb-8">Are you sure you want to remove the cache directory?</p> <p class="text-center mb-8">Are you sure you want to remove the cache directory?</p>
<div class="flex px-1 items-center"> <div class="flex px-1 items-center">
<ui-btn color="primary" @click="showConfirmPurgeCache = false">Nevermind</ui-btn> <ui-btn color="primary" @click="showConfirmPurgeCache = false">Nevermind</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
@@ -245,7 +287,10 @@ export default {
storeCoverWithItem: 'By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named "cover" will be kept', storeCoverWithItem: 'By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named "cover" will be kept',
storeMetadataWithItem: 'By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension', storeMetadataWithItem: 'By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension',
coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers', coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers',
enableEReader: 'E-reader is still a work in progress, but use this setting to open it up to all your users (or use the "Experimental Features" toggle below just for you)' enableEReader: 'E-reader is still a work in progress, but use this setting to open it up to all your users (or use the "Experimental Features" toggle just for use by you)',
scannerPreferOverdriveMediaMarker: 'MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically',
scannerUseSingleThreadedProber: 'The old scanner used a single thread. Leaving it in to use as a comparison for now.',
scannerMaxThreads: 'Number of concurrent media files to scan at a time. Value of 1 will be a slower scan but less CPU usage. <br><br>Value of 0 defaults to # of CPU cores for this server times 2 (i.e. 4-core CPU will be 8)'
}, },
showConfirmPurgeCache: false showConfirmPurgeCache: false
} }
@@ -271,11 +316,31 @@ export default {
set(val) { set(val) {
this.$store.commit('setExperimentalFeatures', val) this.$store.commit('setExperimentalFeatures', val)
} }
},
dateFormats() {
return this.$store.state.globals.dateFormats
} }
}, },
methods: { methods: {
updateEnableChromecast(val) { updateScannerMaxThreads(val) {
this.updateServerSettings({ enableChromecast: val }) if (!val || isNaN(val)) {
this.$toast.error('Invalid max threads must be a number')
this.newServerSettings.scannerMaxThreads = 0
return
}
if (Number(val) < 0) {
this.$toast.error('Max threads must be >= 0')
this.newServerSettings.scannerMaxThreads = 0
return
}
if (Math.round(Number(val)) !== Number(val)) {
this.$toast.error('Max threads must be an integer')
this.newServerSettings.scannerMaxThreads = 0
return
}
this.updateServerSettings({
scannerMaxThreads: Number(val)
})
}, },
updateSortingPrefixes(val) { updateSortingPrefixes(val) {
if (!val || !val.length) { if (!val || !val.length) {
@@ -314,10 +379,12 @@ export default {
.then((success) => { .then((success) => {
console.log('Updated Server Settings', success) console.log('Updated Server Settings', success)
this.updatingServerSettings = false this.updatingServerSettings = false
this.$toast.success('Server settings updated')
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update server settings', error) console.error('Failed to update server settings', error)
this.updatingServerSettings = false this.updatingServerSettings = false
this.$toast.error('Failed to update server settings')
}) })
}, },
initServerSettings() { initServerSettings() {
+3 -4
View File
@@ -1,10 +1,9 @@
<template> <template>
<div> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
<p class="text-xl">Stats for library {{ currentLibraryName }}</p> <h1 class="text-xl">Stats for library {{ currentLibraryName }}</h1>
<stats-preview-icons v-if="totalItems" :library-stats="libraryStats" /> <stats-preview-icons v-if="totalItems" :library-stats="libraryStats" />
<div class="flex md:flex-row flex-wrap justify-between flex-col mt-12"> <div class="flex lg:flex-row flex-wrap justify-between flex-col mt-8">
<div class="w-80 my-6 mx-auto"> <div class="w-80 my-6 mx-auto">
<h1 class="text-2xl mb-4 font-book">Top 5 Genres</h1> <h1 class="text-2xl mb-4 font-book">Top 5 Genres</h1>
<p v-if="!top5Genres.length">No Genres</p> <p v-if="!top5Genres.length">No Genres</p>
+12 -14
View File
@@ -1,24 +1,21 @@
<template> <template>
<div id="page-wrapper" class="page overflow-y-auto" :class="streamLibraryItem ? 'streaming' : ''"> <div class="w-full h-full">
<div class="w-full max-w-4xl mx-auto"> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
<div class="mb-4 flex flex-col sm:flex-row items-start sm:items-end"> <div class="flex items-center mb-2">
<p class="text-2xl mr-4 mb-2 sm:mb-0">Logger</p> <h1 class="text-xl">Logs</h1>
</div>
<div class="flex justify-between mb-2 place-items-end">
<ui-text-input ref="input" v-model="search" placeholder="Search filter.." @input="inputUpdate" clearable class="w-full sm:w-40 h-8 text-sm sm:mb-0" />
<ui-text-input ref="input" v-model="search" placeholder="Search filter.." @input="inputUpdate" clearable class="w-full sm:w-40 h-8 text-sm mb-2 sm:mb-0" /> <ui-dropdown v-model="newServerSettings.logLevel" label="Server Log Level" :items="logLevelItems" @input="logLevelUpdated" class="w-full sm:w-44" />
<div class="flex-grow" />
<div class="w-full sm:w-44">
<ui-dropdown v-model="newServerSettings.logLevel" label="Server Log Level" :items="logLevelItems" @input="logLevelUpdated" />
</div>
</div> </div>
<div class="relative"> <div class="relative">
<div ref="container" class="relative w-full h-full bg-primary border-bg overflow-x-hidden overflow-y-auto text-red shadow-inner rounded-md" style="max-height: 550px; min-height: 550px"> <div ref="container" class="relative w-full h-full bg-primary border-bg overflow-x-hidden overflow-y-auto text-red shadow-inner rounded-md" style="max-height: 800px; min-height: 550px">
<template v-for="(log, index) in logs"> <template v-for="(log, index) in logs">
<div :key="index" class="flex flex-nowrap px-2 py-1 items-start text-sm bg-opacity-10" :class="`bg-${logColors[log.level]}`"> <div :key="index" class="flex flex-nowrap px-2 py-1 items-start text-sm bg-opacity-10" :class="`bg-${logColors[log.level]}`">
<p class="text-gray-400 w-40 font-mono">{{ log.timestamp.split('.')[0].split('T').join(' ') }}</p> <p class="text-gray-400 w-36 font-mono text-xs">{{ log.timestamp }}</p>
<p class="font-semibold w-12 text-right" :class="`text-${logColors[log.level]}`">{{ log.levelName }}</p> <p class="font-semibold w-12 text-right text-sm" :class="`text-${logColors[log.level]}`">{{ log.levelName }}</p>
<p class="px-4 logmessage">{{ log.message }}</p> <p class="px-4 logmessage">{{ log.message }}</p>
</div> </div>
</template> </template>
@@ -164,6 +161,7 @@ export default {
}, },
beforeDestroy() { beforeDestroy() {
if (!this.$root.socket) return if (!this.$root.socket) return
this.$root.socket.emit('remove_log_listener')
this.$root.socket.off('daily_logs', this.dailyLogsLoaded) this.$root.socket.off('daily_logs', this.dailyLogsLoaded)
this.$root.socket.off('log', this.logEvtReceived) this.$root.socket.off('log', this.logEvtReceived)
} }
+92 -56
View File
@@ -1,61 +1,61 @@
<template> <template>
<div class="w-full h-full"> <div class="w-full h-full">
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-0 sm:p-4 mb-8"> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
<div class="py-2"> <div class="flex items-center mb-2">
<div class="flex items-center mb-1"> <h1 class="text-xl">Listening Sessions</h1>
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Sessions</h1>
<div class="flex-grow" />
<ui-dropdown v-model="selectedUser" :items="userItems" label="Filter by User" small class="max-w-48" @input="updateUserFilter" />
</div>
<div v-if="listeningSessions.length" class="block max-w-full">
<table class="userSessionsTable">
<tr class="bg-primary bg-opacity-40">
<th class="w-48 min-w-48 text-left">Item</th>
<th class="w-20 min-w-20 text-left hidden md:table-cell">User</th>
<th class="w-32 min-w-32 text-left hidden md:table-cell">Play Method</th>
<th class="w-32 min-w-32 text-left hidden sm:table-cell">Device Info</th>
<th class="w-32 min-w-32">Listened</th>
<th class="w-16 min-w-16">Last Time</th>
<th class="flex-grow hidden sm:table-cell">Last Update</th>
</tr>
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
<td class="py-1 max-w-48">
<p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p>
<p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p>
</td>
<td class="hidden md:table-cell">
<p v-if="filteredUserUsername" class="text-xs">{{ filteredUserUsername }}</p>
<p v-else class="text-xs">{{ session.user ? session.user.username : 'N/A' }}</p>
</td>
<td class="hidden md:table-cell">
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
</td>
<td class="hidden sm:table-cell">
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
</td>
<td class="text-center">
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
</td>
<td class="text-center">
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
</td>
<td class="text-center hidden sm:table-cell">
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')">
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
</ui-tooltip>
</td>
</tr>
</table>
<div class="flex items-center justify-end py-1">
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
<p class="text-sm mx-1">Page {{ currentPage + 1 }} of {{ numPages }}</p>
<ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
</div>
</div>
<p v-else class="text-white text-opacity-50">No sessions yet...</p>
</div> </div>
<div class="flex justify-end mb-2">
<ui-dropdown v-model="selectedUser" :items="userItems" label="Filter by User" small class="max-w-48" @input="updateUserFilter" />
</div>
<div v-if="listeningSessions.length" class="block max-w-full">
<table class="userSessionsTable">
<tr class="bg-primary bg-opacity-40">
<th class="w-48 min-w-48 text-left">Item</th>
<th class="w-20 min-w-20 text-left hidden md:table-cell">User</th>
<th class="w-32 min-w-32 text-left hidden md:table-cell">Play Method</th>
<th class="w-32 min-w-32 text-left hidden sm:table-cell">Device Info</th>
<th class="w-32 min-w-32">Listened</th>
<th class="w-16 min-w-16">Last Time</th>
<th class="flex-grow hidden sm:table-cell">Last Update</th>
</tr>
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
<td class="py-1 max-w-48">
<p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p>
<p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p>
</td>
<td class="hidden md:table-cell">
<p v-if="filteredUserUsername" class="text-xs">{{ filteredUserUsername }}</p>
<p v-else class="text-xs">{{ session.user ? session.user.username : 'N/A' }}</p>
</td>
<td class="hidden md:table-cell">
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
</td>
<td class="hidden sm:table-cell">
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
</td>
<td class="text-center">
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
</td>
<td class="text-center hover:underline" @click.stop="clickCurrentTime(session)">
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
</td>
<td class="text-center hidden sm:table-cell">
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')">
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
</ui-tooltip>
</td>
</tr>
</table>
<div class="flex items-center justify-end my-2">
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
<p class="text-sm mx-1">Page {{ currentPage + 1 }} of {{ numPages }}</p>
<ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
</div>
</div>
<p v-else class="text-white text-opacity-50">No sessions yet...</p>
</div> </div>
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" /> <modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" />
@@ -89,7 +89,8 @@ export default {
total: 0, total: 0,
currentPage: 0, currentPage: 0,
userFilter: null, userFilter: null,
selectedUser: '' selectedUser: '',
processingGoToTimestamp: false
} }
}, },
computed: { computed: {
@@ -110,6 +111,41 @@ export default {
} }
}, },
methods: { methods: {
async clickCurrentTime(session) {
if (this.processingGoToTimestamp) return
this.processingGoToTimestamp = true
const libraryItem = await this.$axios.$get(`/api/items/${session.libraryItemId}`).catch((error) => {
console.error('Failed to get library item', error)
return null
})
if (!libraryItem) {
this.$toast.error('Failed to get library item')
this.processingGoToTimestamp = false
return
}
if (session.episodeId && !libraryItem.media.episodes.find((ep) => ep.id === session.episodeId)) {
this.$toast.error('Failed to get podcast episode')
this.processingGoToTimestamp = false
return
}
const payload = {
message: `Start playback for "${session.displayTitle}" at ${this.$secondsToTimestamp(session.currentTime)}?`,
callback: (confirmed) => {
if (confirmed) {
this.$eventBus.$emit('play-item', {
libraryItemId: libraryItem.id,
episodeId: session.episodeId || null,
startTime: session.currentTime
})
}
this.processingGoToTimestamp = false
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
updateUserFilter() { updateUserFilter() {
this.loadSessions(0) this.loadSessions(0)
}, },
+7 -2
View File
@@ -1,5 +1,7 @@
<template> <template>
<div> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
<h1 class="text-xl">Stats for {{ username }}</h1>
<div class="flex justify-center"> <div class="flex justify-center">
<div class="flex p-2"> <div class="flex p-2">
<svg class="hidden sm:block h-14 w-14 lg:h-18 lg:w-18" viewBox="0 0 24 24"> <svg class="hidden sm:block h-14 w-14 lg:h-18 lg:w-18" viewBox="0 0 24 24">
@@ -46,7 +48,7 @@
<template v-for="(item, index) in mostRecentListeningSessions"> <template v-for="(item, index) in mostRecentListeningSessions">
<div :key="item.id" class="w-full py-0.5"> <div :key="item.id" class="w-full py-0.5">
<div class="flex items-center mb-1"> <div class="flex items-center mb-1">
<p class="text-sm font-book text-white text-opacity-70 w-6 truncate">{{ index + 1 }}.&nbsp;</p> <p class="text-sm font-book text-white text-opacity-70 w-8">{{ index + 1 }}.&nbsp;</p>
<div class="w-56"> <div class="w-56">
<p class="text-sm font-book text-white text-opacity-80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p> <p class="text-sm font-book text-white text-opacity-80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.updatedAt) }}</p> <p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
@@ -84,6 +86,9 @@ export default {
user() { user() {
return this.$store.state.user.user return this.$store.state.user.user
}, },
username() {
return this.user.username
},
currentLibraryId() { currentLibraryId() {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
}, },
+15 -11
View File
@@ -13,11 +13,12 @@
<widgets-online-indicator :value="!!userOnline" /> <widgets-online-indicator :value="!!userOnline" />
<h1 class="text-xl pl-2">{{ username }}</h1> <h1 class="text-xl pl-2">{{ username }}</h1>
</div> </div>
<div class="cursor-pointer text-gray-400 hover:text-white" @click="copyToClipboard(userToken)"> <div v-if="userToken" class="flex text-xs mt-4">
<p v-if="userToken" class="py-2 text-xs"> <ui-text-input-with-label label="API Token" :value="userToken" readonly />
<strong class="text-white">API Token: </strong><br /><span class="text-white">{{ userToken }}</span
><span class="material-icons pl-2 text-base">content_copy</span> <div class="px-1 mt-8 cursor-pointer" @click="copyToClipboard(userToken)">
</p> <span class="material-icons pl-2 text-base">content_copy</span>
</div>
</div> </div>
<div class="w-full h-px bg-white bg-opacity-10 my-2" /> <div class="w-full h-px bg-white bg-opacity-10 my-2" />
<div class="py-2"> <div class="py-2">
@@ -138,12 +139,15 @@ export default {
this.$copyToClipboard(str, this) this.$copyToClipboard(str, this)
}, },
async init() { async init() {
this.listeningSessions = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions?page=0&itemsPerPage=10`).then((data) => { this.listeningSessions = await this.$axios
return data.sessions || [] .$get(`/api/users/${this.user.id}/listening-sessions?page=0&itemsPerPage=10`)
}).catch((err) => { .then((data) => {
console.error('Failed to load listening sesions', err) return data.sessions || []
return [] })
}) .catch((err) => {
console.error('Failed to load listening sesions', err)
return []
})
this.listeningStats = await this.$axios.$get(`/api/users/${this.user.id}/listening-stats`).catch((err) => { this.listeningStats = await this.$axios.$get(`/api/users/${this.user.id}/listening-stats`).catch((err) => {
console.error('Failed to load listening sesions', err) console.error('Failed to load listening sesions', err)
return [] return []
+38 -2
View File
@@ -42,7 +42,7 @@
<td class="text-center"> <td class="text-center">
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p> <p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
</td> </td>
<td class="text-center"> <td class="text-center hover:underline" @click.stop="clickCurrentTime(session)">
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p> <p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
</td> </td>
<td class="text-center hidden sm:table-cell"> <td class="text-center hidden sm:table-cell">
@@ -85,7 +85,8 @@ export default {
listeningSessions: [], listeningSessions: [],
numPages: 0, numPages: 0,
total: 0, total: 0,
currentPage: 0 currentPage: 0,
processingGoToTimestamp: false
} }
}, },
computed: { computed: {
@@ -97,6 +98,41 @@ export default {
} }
}, },
methods: { methods: {
async clickCurrentTime(session) {
if (this.processingGoToTimestamp) return
this.processingGoToTimestamp = true
const libraryItem = await this.$axios.$get(`/api/items/${session.libraryItemId}`).catch((error) => {
console.error('Failed to get library item', error)
return null
})
if (!libraryItem) {
this.$toast.error('Failed to get library item')
this.processingGoToTimestamp = false
return
}
if (session.episodeId && !libraryItem.media.episodes.find((ep) => ep.id === session.episodeId)) {
this.$toast.error('Failed to get podcast episode')
this.processingGoToTimestamp = false
return
}
const payload = {
message: `Start playback for "${session.displayTitle}" at ${this.$secondsToTimestamp(session.currentTime)}?`,
callback: (confirmed) => {
if (confirmed) {
this.$eventBus.$emit('play-item', {
libraryItemId: libraryItem.id,
episodeId: session.episodeId || null,
startTime: session.currentTime
})
}
this.processingGoToTimestamp = false
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
prevPage() { prevPage() {
this.loadSessions(this.currentPage - 1) this.loadSessions(this.currentPage - 1)
}, },
+69 -16
View File
@@ -7,11 +7,11 @@
<covers-book-cover :library-item="libraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-book-cover :library-item="libraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<!-- Item Progress Bar --> <!-- Item Progress Bar -->
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div> <div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div>
<!-- Item Cover Overlay --> <!-- Item Cover Overlay -->
<div class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-30 opacity-0 hover:opacity-100 transition-opacity" @mousedown.prevent @mouseup.prevent> <div class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-30 opacity-0 hover:opacity-100 transition-opacity" @mousedown.prevent @mouseup.prevent>
<div v-show="showPlayButton && !streaming" class="h-full flex items-center justify-center pointer-events-none"> <div v-show="showPlayButton && !isStreaming" 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 cursor-pointer" @click.stop.prevent="startStream"> <div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto cursor-pointer" @click.stop.prevent="startStream">
<span class="material-icons text-4xl">play_circle_filled</span> <span class="material-icons text-4xl">play_circle_filled</span>
</div> </div>
@@ -118,9 +118,9 @@
<!-- Progress --> <!-- Progress -->
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''"> <div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''">
<p v-if="progressPercent < 1" class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p> <p v-if="progressPercent < 1" class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
<p v-else class="text-xs">Finished {{ $formatDate(userProgressFinishedAt, 'MM/dd/yyyy') }}</p> <p v-else class="text-xs">Finished {{ $formatDate(userProgressFinishedAt, dateFormat) }}</p>
<p v-if="progressPercent < 1" class="text-gray-200 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p> <p v-if="progressPercent < 1" class="text-gray-200 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
<p class="text-gray-400 text-xs pt-1">Started {{ $formatDate(userProgressStartedAt, 'MM/dd/yyyy') }}</p> <p class="text-gray-400 text-xs pt-1">Started {{ $formatDate(userProgressStartedAt, dateFormat) }}</p>
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick"> <div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
<span class="material-icons text-sm">close</span> <span class="material-icons text-sm">close</span>
@@ -129,12 +129,12 @@
<!-- Icon buttons --> <!-- Icon buttons -->
<div class="flex items-center justify-center md:justify-start pt-4"> <div class="flex items-center justify-center md:justify-start pt-4">
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="startStream"> <ui-btn v-if="showPlayButton" :disabled="isStreaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="startStream">
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span> <span v-show="!isStreaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
{{ streaming ? 'Playing' : 'Play' }} {{ isStreaming ? 'Playing' : 'Play' }}
</ui-btn> </ui-btn>
<ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2"> <ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">error</span> <span v-show="!isStreaming" class="material-icons -ml-2 pr-1 text-white">error</span>
{{ isMissing ? 'Missing' : 'Incomplete' }} {{ isMissing ? 'Missing' : 'Incomplete' }}
</ui-btn> </ui-btn>
@@ -160,7 +160,11 @@
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" /> <ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
</ui-tooltip> </ui-tooltip>
<!-- Experimental RSS feed open --> <ui-tooltip v-if="bookmarks.length" text="Your Bookmarks" direction="top">
<ui-icon-btn :icon="bookmarks.length ? 'bookmarks' : 'bookmark_border'" class="mx-0.5" @click="clickBookmarksBtn" />
</ui-tooltip>
<!-- RSS feed -->
<ui-tooltip v-if="showRssFeedBtn" text="Open RSS Feed" direction="top"> <ui-tooltip v-if="showRssFeedBtn" text="Open RSS Feed" direction="top">
<ui-icon-btn icon="rss_feed" class="mx-0.5" :bg-color="rssFeedUrl ? 'success' : 'primary'" outlined @click="clickRSSFeed" /> <ui-icon-btn icon="rss_feed" class="mx-0.5" :bg-color="rssFeedUrl ? 'success' : 'primary'" outlined @click="clickRSSFeed" />
</ui-tooltip> </ui-tooltip>
@@ -189,6 +193,7 @@
<modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" /> <modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" />
<modals-rssfeed-view-modal v-model="showRssFeedModal" :library-item="libraryItem" :feed-url="rssFeedUrl" /> <modals-rssfeed-view-modal v-model="showRssFeedModal" :library-item="libraryItem" :feed-url="rssFeedUrl" />
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :library-item-id="libraryItemId" hide-create @select="selectBookmark" />
</div> </div>
</template> </template>
@@ -222,10 +227,14 @@ export default {
podcastFeedEpisodes: [], podcastFeedEpisodes: [],
episodesDownloading: [], episodesDownloading: [],
episodeDownloadsQueued: [], episodeDownloadsQueued: [],
showRssFeedModal: false showRssFeedModal: false,
showBookmarksModal: false
} }
}, },
computed: { computed: {
dateFormat() {
return this.$store.state.serverSettings.dateFormat
},
showExperimentalFeatures() { showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures return this.$store.state.showExperimentalFeatures
}, },
@@ -293,6 +302,10 @@ export default {
chapters() { chapters() {
return this.media.chapters || [] return this.media.chapters || []
}, },
bookmarks() {
if (this.isPodcast) return []
return this.$store.getters['user/getUserBookmarksForItem'](this.libraryItemId)
},
tracks() { tracks() {
return this.media.tracks || [] return this.media.tracks || []
}, },
@@ -386,7 +399,7 @@ export default {
streamLibraryItem() { streamLibraryItem() {
return this.$store.state.streamLibraryItem return this.$store.state.streamLibraryItem
}, },
streaming() { isStreaming() {
return this.streamLibraryItem && this.streamLibraryItem.id === this.libraryItemId return this.streamLibraryItem && this.streamLibraryItem.id === this.libraryItemId
}, },
userCanUpdate() { userCanUpdate() {
@@ -406,6 +419,31 @@ export default {
} }
}, },
methods: { methods: {
clickBookmarksBtn() {
this.showBookmarksModal = true
},
selectBookmark(bookmark) {
if (!bookmark) return
if (this.isStreaming) {
this.$eventBus.$emit('playback-seek', bookmark.time)
} else if (this.streamLibraryItem) {
this.showBookmarksModal = false
console.log('Already streaming library item so ask about it')
const payload = {
message: `Start playback for "${this.title}" at ${this.$secondsToTimestamp(bookmark.time)}?`,
callback: (confirmed) => {
if (confirmed) {
this.startStream(bookmark.time)
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
} else {
this.startStream(bookmark.time)
}
this.showBookmarksModal = false
},
clearDownloadQueue() { clearDownloadQueue() {
if (confirm('Are you sure you want to clear episode download queue?')) { if (confirm('Are you sure you want to clear episode download queue?')) {
this.$axios this.$axios
@@ -450,7 +488,21 @@ export default {
openEbook() { openEbook() {
this.$store.commit('showEReader', this.libraryItem) this.$store.commit('showEReader', this.libraryItem)
}, },
toggleFinished() { toggleFinished(confirmed = false) {
if (!this.userIsFinished && this.progressPercent > 0 && !confirmed) {
const payload = {
message: `Are you sure you want to mark "${this.title}" as finished?`,
callback: (confirmed) => {
if (confirmed) {
this.toggleFinished(true)
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
return
}
var updatePayload = { var updatePayload = {
isFinished: !this.userIsFinished isFinished: !this.userIsFinished
} }
@@ -467,7 +519,7 @@ export default {
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`) this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
}) })
}, },
startStream() { startStream(startTime = null) {
var episodeId = null var episodeId = null
if (this.isPodcast) { if (this.isPodcast) {
var episode = this.podcastEpisodes.find((ep) => { var episode = this.podcastEpisodes.find((ep) => {
@@ -480,7 +532,8 @@ export default {
this.$eventBus.$emit('play-item', { this.$eventBus.$emit('play-item', {
libraryItemId: this.libraryItem.id, libraryItemId: this.libraryItem.id,
episodeId episodeId,
startTime
}) })
}, },
editClick() { editClick() {
@@ -534,13 +587,13 @@ export default {
} }
}, },
rssFeedOpen(data) { rssFeedOpen(data) {
if (data.libraryItemId === this.libraryItemId) { if (data.entityId === this.libraryItemId) {
console.log('RSS Feed Opened', data) console.log('RSS Feed Opened', data)
this.rssFeedUrl = data.feedUrl this.rssFeedUrl = data.feedUrl
} }
}, },
rssFeedClosed(data) { rssFeedClosed(data) {
if (data.libraryItemId === this.libraryItemId) { if (data.entityId === this.libraryItemId) {
console.log('RSS Feed Closed', data) console.log('RSS Feed Closed', data)
this.rssFeedUrl = null this.rssFeedUrl = null
} }
@@ -2,25 +2,26 @@
<div class="page" :class="streamLibraryItem ? 'streaming' : ''"> <div class="page" :class="streamLibraryItem ? 'streaming' : ''">
<app-book-shelf-toolbar page="podcast-search" /> <app-book-shelf-toolbar page="podcast-search" />
<div class="w-full h-full overflow-y-auto p-12 relative"> <div class="w-full h-full overflow-y-auto px-2 py-6 sm:px-4 md:p-12 relative">
<div class="w-full max-w-4xl mx-auto flex"> <div class="w-full max-w-4xl mx-auto flex">
<form @submit.prevent="submit" class="flex flex-grow"> <form @submit.prevent="submit" class="flex flex-grow">
<ui-text-input v-model="searchInput" :disabled="processing" placeholder="Enter search term or RSS feed URL" class="flex-grow mr-2" /> <ui-text-input v-model="searchInput" :disabled="processing" placeholder="Enter search term or RSS feed URL" class="flex-grow mr-2 text-sm md:text-base" />
<ui-btn type="submit" :disabled="processing">Submit</ui-btn> <ui-btn type="submit" :disabled="processing" class="hidden md:block">Submit</ui-btn>
<ui-btn type="submit" :disabled="processing" class="block md:hidden" small>Submit</ui-btn>
</form> </form>
<ui-file-input ref="fileInput" :accept="'.opml, .txt'" class="mx-2" @change="opmlFileUpload"> Upload OPML File </ui-file-input> <ui-file-input ref="fileInput" :accept="'.opml, .txt'" class="ml-2" @change="opmlFileUpload"> Upload OPML File </ui-file-input>
</div> </div>
<div class="w-full max-w-3xl mx-auto py-4"> <div class="w-full max-w-3xl mx-auto py-4">
<p v-if="termSearched && !results.length && !processing" class="text-center text-xl">No podcasts found</p> <p v-if="termSearched && !results.length && !processing" class="text-center text-xl">No podcasts found</p>
<template v-for="podcast in results"> <template v-for="podcast in results">
<div :key="podcast.id" class="flex p-1 hover:bg-primary hover:bg-opacity-25 cursor-pointer" @click="selectPodcast(podcast)"> <div :key="podcast.id" class="flex p-1 hover:bg-primary hover:bg-opacity-25 cursor-pointer" @click="selectPodcast(podcast)">
<div class="w-24 min-w-24 h-24 bg-primary"> <div class="w-20 min-w-20 h-20 md:w-24 md:min-w-24 md:h-24 bg-primary">
<img v-if="podcast.cover" :src="podcast.cover" class="h-full w-full" /> <img v-if="podcast.cover" :src="podcast.cover" class="h-full w-full" />
</div> </div>
<div class="flex-grow pl-4 max-w-2xl"> <div class="flex-grow pl-4 max-w-2xl">
<a :href="podcast.pageUrl" class="text-lg text-gray-200 hover:underline" target="_blank" @click.stop>{{ podcast.title }}</a> <a :href="podcast.pageUrl" class="text-base md:text-lg text-gray-200 hover:underline" target="_blank" @click.stop>{{ podcast.title }}</a>
<p class="text-base text-gray-300 whitespace-nowrap truncate">by {{ podcast.artistName }}</p> <p class="text-sm md:text-base text-gray-300 whitespace-nowrap truncate">by {{ podcast.artistName }}</p>
<p class="text-xs text-gray-400 leading-5">{{ podcast.genres.join(', ') }}</p> <p class="text-xs text-gray-400 leading-5">{{ podcast.genres.join(', ') }}</p>
<p class="text-xs text-gray-400 leading-5">{{ podcast.trackCount }} Episodes</p> <p class="text-xs text-gray-400 leading-5">{{ podcast.trackCount }} Episodes</p>
</div> </div>
+2 -1
View File
@@ -124,9 +124,10 @@ export default {
location.reload() location.reload()
}, },
setUser({ user, userDefaultLibraryId, serverSettings, Source }) { setUser({ user, userDefaultLibraryId, serverSettings, Source, feeds }) {
this.$store.commit('setServerSettings', serverSettings) this.$store.commit('setServerSettings', serverSettings)
this.$store.commit('setSource', Source) this.$store.commit('setSource', Source)
this.$store.commit('feeds/setFeeds', feeds)
if (serverSettings.chromecastEnabled) { if (serverSettings.chromecastEnabled) {
console.log('Chromecast enabled import script') console.log('Chromecast enabled import script')
+3 -1
View File
@@ -29,7 +29,9 @@
</div> </div>
</div> </div>
<div class="pt-8 text-center"> <div class="pt-8 text-center">
<p class="text-xs text-white text-opacity-50 font-mono"><strong>Supported File Types: </strong>{{ inputAccept.join(', ') }}</p> <p class="text-xs text-white text-opacity-50 font-mono mb-4"><strong>Supported File Types: </strong>{{ inputAccept.join(', ') }}</p>
<p class="text-sm text-white text-opacity-70">Folders with media files will be treated as separate library items. <span v-if="selectedLibraryMediaType === 'book'">If uploading only audio files then each audio file will be treated as a separate audiobook.</span></p>
</div> </div>
</div> </div>
<!-- Item list header --> <!-- Item list header -->
+14 -8
View File
@@ -18,6 +18,7 @@ export default class PlayerHandler {
this.isHlsTranscode = false this.isHlsTranscode = false
this.isVideo = false this.isVideo = false
this.currentSessionId = null this.currentSessionId = null
this.startTimeOverride = undefined // Used for starting playback at a specific time (i.e. clicking bookmark from library item page)
this.startTime = 0 this.startTime = 0
this.failedProgressSyncs = 0 this.failedProgressSyncs = 0
@@ -51,12 +52,13 @@ export default class PlayerHandler {
return this.libraryItem.media.episodes.find(ep => ep.id === this.episodeId) return this.libraryItem.media.episodes.find(ep => ep.id === this.episodeId)
} }
load(libraryItem, episodeId, playWhenReady, playbackRate) { load(libraryItem, episodeId, playWhenReady, playbackRate, startTimeOverride = undefined) {
this.libraryItem = libraryItem this.libraryItem = libraryItem
this.episodeId = episodeId this.episodeId = episodeId
this.playWhenReady = playWhenReady this.playWhenReady = playWhenReady
this.initialPlaybackRate = playbackRate this.initialPlaybackRate = playbackRate
this.isVideo = libraryItem.mediaType === 'video' this.isVideo = libraryItem.mediaType === 'video'
this.startTimeOverride = (startTimeOverride == null || isNaN(startTimeOverride)) ? undefined : Number(startTimeOverride)
if (!this.player) this.switchPlayer(playWhenReady) if (!this.player) this.switchPlayer(playWhenReady)
else this.prepare() else this.prepare()
@@ -142,11 +144,13 @@ export default class PlayerHandler {
} else { } else {
this.stopPlayInterval() this.stopPlayInterval()
} }
if (this.playerState === 'LOADED' || this.playerState === 'PLAYING') { if (this.player) {
this.ctx.setDuration(this.getDuration()) if (this.playerState === 'LOADED' || this.playerState === 'PLAYING') {
} this.ctx.setDuration(this.getDuration())
if (this.playerState !== 'LOADING') { }
this.ctx.setCurrentTime(this.player.getCurrentTime()) if (this.playerState !== 'LOADING') {
this.ctx.setCurrentTime(this.player.getCurrentTime())
}
} }
this.ctx.setPlaying(this.playerState === 'PLAYING') this.ctx.setPlaying(this.playerState === 'PLAYING')
@@ -177,18 +181,20 @@ export default class PlayerHandler {
} }
prepareOpenSession(session, playbackRate) { // Session opened on init socket prepareOpenSession(session, playbackRate) { // Session opened on init socket
if (!this.player) this.switchPlayer() // Must set player first for open sessions
this.libraryItem = session.libraryItem this.libraryItem = session.libraryItem
this.isVideo = session.libraryItem.mediaType === 'video' this.isVideo = session.libraryItem.mediaType === 'video'
this.playWhenReady = false this.playWhenReady = false
this.initialPlaybackRate = playbackRate this.initialPlaybackRate = playbackRate
this.startTimeOverride = undefined
if (!this.player) this.switchPlayer()
this.prepareSession(session) this.prepareSession(session)
} }
prepareSession(session) { prepareSession(session) {
this.failedProgressSyncs = 0 this.failedProgressSyncs = 0
this.startTime = session.currentTime this.startTime = this.startTimeOverride !== undefined ? this.startTimeOverride : session.currentTime
this.currentSessionId = session.id this.currentSessionId = session.id
this.displayTitle = session.displayTitle this.displayTitle = session.displayTitle
this.displayAuthor = session.displayAuthor this.displayAuthor = session.displayAuthor
+1 -1
View File
@@ -23,7 +23,7 @@ export default (ctx) => {
var castContext = cast.framework.CastContext.getInstance() var castContext = cast.framework.CastContext.getInstance()
castContext.setOptions({ castContext.setOptions({
receiverApplicationId: process.env.chromecastReceiver, receiverApplicationId: process.env.chromecastReceiver,
autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED autoJoinPolicy: chrome.cast ? chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED : null
}); });
castContext.addEventListener( castContext.addEventListener(
+1 -1
View File
@@ -1,6 +1,6 @@
const SupportedFileTypes = { const SupportedFileTypes = {
image: ['png', 'jpg', 'jpeg', 'webp'], image: ['png', 'jpg', 'jpeg', 'webp'],
audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'mp4', 'aac', 'wma', 'aiff', 'wav'], audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav'],
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'], ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
info: ['nfo'], info: ['nfo'],
text: ['txt'], text: ['txt'],
+1 -2
View File
@@ -169,7 +169,7 @@ Vue.prototype.$sanitizeSlug = (str) => {
str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i)) str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i))
} }
str = str.replace('.', '-') // replace a dot by a dash str = str.replace('.', '-') // replace a dot by a dash
.replace(/[^a-z0-9 -_]/g, '') // remove invalid chars .replace(/[^a-z0-9 -_]/g, '') // remove invalid chars
.replace(/\s+/g, '-') // collapse whitespace and replace by a dash .replace(/\s+/g, '-') // collapse whitespace and replace by a dash
.replace(/-+/g, '-') // collapse dashes .replace(/-+/g, '-') // collapse dashes
@@ -204,7 +204,6 @@ Vue.prototype.$copyToClipboard = (str, ctx) => {
}) })
} }
function xmlToJson(xml) { function xmlToJson(xml) {
const json = {}; const json = {};
for (const res of xml.matchAll(/(?:<(\w*)(?:\s[^>]*)*>)((?:(?!<\1).)*)(?:<\/\1>)|<(\w*)(?:\s*)*\/>/gm)) { for (const res of xml.matchAll(/(?:<(\w*)(?:\s[^>]*)*>)((?:(?!<\1).)*)(?:<\/\1>)|<(\w*)(?:\s*)*\/>/gm)) {
+7 -1
View File
@@ -47,8 +47,13 @@ export async function checkForUpdate() {
largestVer = verObj largestVer = verObj
} }
} }
if (verObj.version == currVerObj.version) {
currVerObj.changelog = release.body
}
}) })
} }
}) })
if (!largestVer) { if (!largestVer) {
console.error('No valid version tags to compare with') console.error('No valid version tags to compare with')
@@ -59,6 +64,7 @@ export async function checkForUpdate() {
hasUpdate: largestVer.total > currVerObj.total, hasUpdate: largestVer.total > currVerObj.total,
latestVersion: largestVer.version, latestVersion: largestVer.version,
githubTagUrl: `https://github.com/advplyr/audiobookshelf/releases/tag/v${largestVer.version}`, githubTagUrl: `https://github.com/advplyr/audiobookshelf/releases/tag/v${largestVer.version}`,
currentVersion: currVerObj.version currentVersion: currVerObj.version,
currentVersionChangelog: currVerObj.changelog
} }
} }
+41
View File
@@ -0,0 +1,41 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1235.7 1235.4" style="enable-background:new 0 0 6702.7 1277.4;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:url(#SVGID_1_);}
.st2{fill:#C9C9C9;}
.st3{font-family:'GentiumBookBasic';}
.st4{font-size:800px;}
.st5{fill:#474747;}
</style>
<title>bgAsset 6</title>
<g id="Layer_2_1_">
<g id="Layer_2-2">
<g id="Layer_4">
<g id="Layer_5">
<circle class="st0" cx="618.6" cy="618.6" r="618.6"/>
</g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="617.37" y1="1257.3" x2="617.37" y2="61.4399" gradientTransform="matrix(1 0 0 -1 0 1278)">
<stop offset="0.32" style="stop-color:#CD9D49"/>
<stop offset="0.99" style="stop-color:#875D27"/>
</linearGradient>
<circle class="st1" cx="617.4" cy="618.6" r="597.9"/>
</g>
<path class="st0" d="M1005.6,574.1c-4.8-4-12.4-10-22.6-17v-79.2c0-201.9-163.7-365.6-365.6-365.6l0,0
c-201.9,0-365.6,163.7-365.6,365.6v79.2c-10.2,7-17.7,13-22.6,17c-4.1,3.4-6.5,8.5-6.5,13.9v94.9c0,5.4,2.4,10.5,6.5,14
c11.3,9.4,37.2,29.1,77.5,49.3v9.2c0,24.9,16,45,35.8,45l0,0c19.8,0,35.8-20.2,35.8-45V527.8c0-24.9-16-45-35.8-45l0,0
c-19,0-34.5,18.5-35.8,41.9h-0.1v-46.9c0-171.6,139.1-310.7,310.7-310.7l0,0C789,167.2,928,306.3,928,477.9v46.9H928
c-1.3-23.4-16.8-41.9-35.8-41.9l0,0c-19.8,0-35.8,20.2-35.8,45v227.6c0,24.9,16,45,35.8,45l0,0c19.8,0,35.8-20.2,35.8-45v-9.2
c40.3-20.2,66.2-39.9,77.5-49.3c4.2-3.5,6.5-8.6,6.5-14V588C1012.1,582.6,1009.7,577.5,1005.6,574.1z"/>
<path class="st0" d="M489.9,969.7c23.9,0,43.3-19.4,43.3-43.3V441.6c0-23.9-19.4-43.3-43.3-43.3h-44.7
c-23.9,0-43.3,19.4-43.3,43.3v484.8c0,23.9,19.4,43.3,43.3,43.3L489.9,969.7z M418.2,514.6h98.7v10.3h-98.7V514.6z"/>
<path class="st0" d="M639.7,969.7c23.9,0,43.3-19.4,43.3-43.3V441.6c0-23.9-19.4-43.3-43.3-43.3H595c-23.9,0-43.3,19.4-43.3,43.3
v484.8c0,23.9,19.4,43.3,43.3,43.3H639.7z M568,514.6h98.7v10.3H568V514.6z"/>
<path class="st0" d="M789.6,969.7c23.9,0,43.3-19.4,43.3-43.3V441.6c0-23.9-19.4-43.3-43.3-43.3h-44.7
c-23.9,0-43.3,19.4-43.3,43.3v484.8c0,23.9,19.4,43.3,43.3,43.3L789.6,969.7z M717.9,514.6h98.7v10.3h-98.7V514.6z"/>
<path class="st0" d="M327.1,984.7h580.5c18,0,32.6,14.6,32.6,32.6v0c0,18-14.6,32.6-32.6,32.6H327.1c-18,0-32.6-14.6-32.6-32.6v0
C294.5,999.3,309.1,984.7,327.1,984.7z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+44
View File
@@ -0,0 +1,44 @@
# License information
## Contribution License Agreement
If you contribute code to this project, you are implicitly allowing your code
to be distributed under the MIT license. You are also implicitly verifying that
all code is your original work. `</legalese>`
## Marked
Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/)
Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
## Markdown
Copyright © 2004, John Gruber
http://daringfireball.net/
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name “Markdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
This software is provided by the copyright holders and contributors “as is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.
File diff suppressed because one or more lines are too long
+28
View File
@@ -0,0 +1,28 @@
export const state = () => ({
feeds: []
})
export const getters = {
getFeedForItem: state => id => {
return state.feeds.find(feed => feed.id === id)
}
}
export const actions = {
}
export const mutations = {
addFeed(state, feed) {
var index = state.feeds.findIndex(f => f.id === feed.id)
if (index >= 0) state.feeds.splice(index, 1, feed)
else state.feeds.push(feed)
},
removeFeed(state, feed) {
state.feeds = state.feeds.filter(f => f.id !== feed.id)
},
setFeeds(state, feeds) {
state.feeds = feeds || []
}
}
+24 -2
View File
@@ -1,4 +1,3 @@
export const state = () => ({ export const state = () => ({
isMobile: false, isMobile: false,
isMobileLandscape: false, isMobileLandscape: false,
@@ -7,12 +6,28 @@ export const state = () => ({
showEditCollectionModal: false, showEditCollectionModal: false,
showEditPodcastEpisode: false, showEditPodcastEpisode: false,
showViewPodcastEpisodeModal: false, showViewPodcastEpisodeModal: false,
showConfirmPrompt: false,
confirmPromptOptions: null,
showEditAuthorModal: false, showEditAuthorModal: false,
selectedEpisode: null, selectedEpisode: null,
selectedCollection: null, selectedCollection: null,
selectedAuthor: null, selectedAuthor: null,
isCasting: false, // Actively casting isCasting: false, // Actively casting
isChromecastInitialized: false // Script loaded isChromecastInitialized: false, // Script loaded
dateFormats: [
{
text: 'MM/DD/YYYY',
value: 'MM/dd/yyyy'
},
{
text: 'DD/MM/YYYY',
value: 'dd/MM/yyyy'
},
{
text: 'YYYY-MM-DD',
value: 'yyyy-MM-dd'
}
]
}) })
export const getters = { export const getters = {
@@ -56,6 +71,13 @@ export const mutations = {
setShowViewPodcastEpisodeModal(state, val) { setShowViewPodcastEpisodeModal(state, val) {
state.showViewPodcastEpisodeModal = val state.showViewPodcastEpisodeModal = val
}, },
setShowConfirmPrompt(state, val) {
state.showConfirmPrompt = val
},
setConfirmPrompt(state, options) {
state.confirmPromptOptions = options
state.showConfirmPrompt = true
},
setEditCollection(state, collection) { setEditCollection(state, collection) {
state.selectedCollection = collection state.selectedCollection = collection
state.showEditCollectionModal = true state.showEditCollectionModal = true
+2 -1
View File
@@ -41,8 +41,9 @@ export const getters = {
getLibraryItemIdStreaming: state => { getLibraryItemIdStreaming: state => {
return state.streamLibraryItem ? state.streamLibraryItem.id : null return state.streamLibraryItem ? state.streamLibraryItem.id : null
}, },
getIsEpisodeStreaming: state => (libraryItemId, episodeId) => { getIsMediaStreaming: state => (libraryItemId, episodeId) => {
if (!state.streamLibraryItem) return null if (!state.streamLibraryItem) return null
if (!episodeId) return state.streamLibraryItem.id == libraryItemId
return state.streamLibraryItem.id == libraryItemId && state.streamEpisodeId == episodeId return state.streamLibraryItem.id == libraryItemId && state.streamEpisodeId == episodeId
} }
} }
+4
View File
@@ -136,6 +136,10 @@ export const mutations = {
localStorage.removeItem('token') localStorage.removeItem('token')
} }
}, },
setUserToken(state, token) {
state.user.token = token
localStorage.setItem('token', user.token)
},
updateMediaProgress(state, { id, data }) { updateMediaProgress(state, { id, data }) {
if (!state.user) return if (!state.user) return
if (!data) { if (!data) {

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