v0.7.6 - Personal Table of Contents + Rating Overhaul (#2170)
* Report Media Issues (#1964) * Started working on a report problems implementation. * Started code * Added logging to book and archive service. * Removed an additional ComicInfo read when comicinfo is null when trying to load. But we've already done it once earlier, so there really isn't any point. * Added basic implementation for media errors. * MediaErrors will ignore duplicate errors when there are multiple issues on same file in a scan. * Fixed unit tests * Basic code in place to view and clear. Just UI Cleanup needed. * Slight css upgrade * Fixed up centering and simplified the code to use regular array instead of observables as it wasn't working. * Fixed unit tests * Fixed unit tests for real * Bump versions by dotnet-bump-version. * Expanded Metadata for EPUBs (#1965) * Fixed a bug breaking ability to save server settings * Explicitly capture more people roles from Epubs, else fallback to how we do it now. It seems to be getting called twice and 2nd time is overriding data. Not sure why * Refactored the code to clean it up * Added support for generating collections or reading list based on dc:title and collection title-type with an optional display-seq. * ReadingList/Collection support can't be done until VersOne supports. https://github.com/vers-one/EpubReader/issues/81 * Double include author for epub parsing and let the People code handle removing duplicates. * Bump versions by dotnet-bump-version. * Nothing changed, this is just to retrigger a stable build. (#1967) (#1968) * Adding paper book reader theme (#1976) * Adding paper book reader theme # Added - Added: Paper book reader theme * Fixing some leftover styles * adding book emulation to 2column layout for paper style * Adding migrations * removing migration and compressing image * Reverting DataContextModelSnapshot * checking out datacontextmodelsnapshot file * Bump versions by dotnet-bump-version. * Web Links (#1983) * Updated dependencies * Updated the default key to be 256 bits to meet security requirements. * Added basic implementation of web link resolving favicon. Needs lots more work and testing on all OSes. * Implemented ability to see links and click on them for an individual chapter. * Hooked up the ability to set Series web links. * Render out the web link * Refactored out the favicon so there is a backup in case it fails. Refactored the baseline image placeholders to be dark mode since that is the default. * Added Robbie's nice error weblink fallbacks. * Bump versions by dotnet-bump-version. * Updated Docker entrypoint (#1984) * Bump versions by dotnet-bump-version. * ISBN Support (#1985) * Fixed a bug where weblinks would always show * Started to try and support ico -> png conversion by manually grabbing image data out, but it's hard as hell. * Implemented ability to parse out ISBN codes for books and ISBN-13 codes for ComicInfo. I can't figure out ISBN-10. * Fixed Favicon not working on anything but windows * Implemented ISBN support into Kavita * Don't round so much when transforming bytes * Bump versions by dotnet-bump-version. * AVIF Support & Much More! (#1992) * Expand the list of potential favicon icons to grab. * Added a url mapping functionality to use alternative urls for fetching icons * Initial commit to streamline media encoding. No DB migration yet, No UI changes, no Task changes. * Started refactoring code so that webp queries use encoding format instead. * More refactoring to remove hardcoded webp references. * Moved manual migrations to their own folder to keep things organized. Manually drop the obsolete webp keys. * Removed old apis for converting media and now have one. Reworked where the conversion code was located and streamlined events and whatnot. * Make favicon encode setting aware * Cleaned up favicon conversion * Updated format counter to now just use Extension from MangaFile now that it's been out a while. * Tweaked jumpbar code to reduce a lookup to hashmap. * Added AVIF (8-bit only) support. * In UpdatePeopleList, use FirstOrDefault as Single adds extra checks that may not be needed. * You can now remove weblinks from edit series page and you can leave empty cells, they will just be removed on backend. * Forgot a file * Don't prompt to write a review, just show the pencil. It's the same amount of clicks if you do, less if you dont. * Fixed Refresh token using wrong Claim to look up the user. * Refactored how we refresh authentication to perform it every 10 m ins to ensure we always stay authenticated. * Changed Version update code to run more throughout the day. Updated some hangfire to newer method signatures. * Bump versions by dotnet-bump-version. * More Fixes (#1993) * Strip just isbn: from epub isbns and log when it's back (books) * Tweaked to allow invalid GTINs but only valid ISBN 10/13s will be saved to Kavita. * Fixed a bug with parsing series from a filename that is just a chapter range and no chapter/volume keywords. * Show the media issue count before you open accordion * Added a inpage filter for Media issues * Cleanup styles * Fixed up some code in epub isbn parsing when it's null * Encode filenames when downloading so that non english characters can be passed properly to UI. * Added support to parse ComicInfo's with Empty Tags. * Reset development settings. * Tweaked the code in generating reading lists to avoid extra work when not needed. * Fix comicvine's favicon * Fixed up a unit test * Tweaked the favicon code to ignore icons that have query parameters * More favicon work. Expanded ability to grab icons a bit. Added in ability to not keep requesting favicons when we failed to parse already. * Added a note for later * Fixed stats server url * Added more debugging * Fixed unit tests * Bump versions by dotnet-bump-version. * More Fixes from Recent PRs (#1995) * Added extra debugging for logout issue * Fixed the null issue with ISBN * Allow web links to be cleared out * More logging on refresh token * More key fallback when building Table of Contents * Added better fallback implementation for building table of contents based on the many different ways epubs are packed and referenced. * Updated dependencies * Fixed up refresh token refresh which was invalidating sessions for no reason. Added it to update last active time as well. * Bump versions by dotnet-bump-version. * Fixed a bug with config (#1996) * Bump versions by dotnet-bump-version. * Changed IsDocker check (#1998) * Refactored IsDocker to be completely static and changed to use an environment variable instead. * Removed file from another branch * Bump versions by dotnet-bump-version. * Migrated up to VersOne 3.3 with epub 3.3 support. (#1999) This enables collection and reading list support from epubs. * Bump versions by dotnet-bump-version. * More Bugfixes (EPUB Mainly) (#2004) * Fixed an issue with downloading where spaces turned into plus signs. * If the refresh token is invalid, but the auth token still has life in it, don't invalidate. * Fixed docker users unable to save settings * Show a default error icon until favicon loads * Fixed a bug in mappings (keys/files) to pages that caused some links not to map appropriately. Updated epub-reader to v3.3.2. * Expanded Table of Content generation by also checking for any files that are named Navigation.xhtml to have Kavita generate a simple ToC from (instead of just TOC.xhtml) * Added another hack to massage key to page lookups when rewriting anchors. * Cleaned up debugging notes * Bump versions by dotnet-bump-version. * More Polish (#2005) * Implemented sort title extraction from epub 3 files. * Added link to wiki for media errors * Fixed the hack to reduce JWT refresh token expiration * Fixed up a case where favicon downloading wasn't correcting links that started with // correctly. Added a fallback for sites that just don't pngs available. * Implemented a mechanism to fallback to Kavita's website for favicons which can be dynamically added/updated by the community. * Reworked the logic for bookwalker which will fail to get the base html, so we have to rely on the fallback handler. * Bump versions by dotnet-bump-version. * Angular 16 (#2007) * Removed adv, which isn't needed. * Updated zone * Updated to angular 16 * Updated to angular 16 (partially) * Updated to angular 16 * Package update for Angular 16 (and other dependencies) is complete. * Replaced all takeUntil(this.onDestroy) with new takeUntilDestroyed() * Updated all inputs that have ! to be required and deleted all unit tests. * Corrected how takeUntilDestroyed() is supposed to be implemented. * Bump versions by dotnet-bump-version. * Pipeline adjustment for Angular 16 (#2008) * Bump versions by dotnet-bump-version. * Try a different build (#2009) * Bump versions by dotnet-bump-version. * Continue Reading Bugfix (#2010) * Fixed an edge case where continue point wasn't considering any chapters that had progress. Continue point is now slightly faster and uses less memory. * Added a unit test for a user's case. Still not reproducible * Bump versions by dotnet-bump-version. * Ensure chapters are sorted when getting continue point (#2011) Fixes new behaviour in #1625 * Bump versions by dotnet-bump-version. * Strip more forms of comments from CSS before parsing/inlining. (#2014) Handle if ExCSS throws an exception during inlining and attempt to fallback to scoping css instead of inlining. I still cannot update past ExCSS v4.1.0 else NPEs for common css will be thrown. * Bump versions by dotnet-bump-version. * Misc Changes (#2015) * Updated ng-bootstrap * Fixed an issue where jumpbar would be disabled when it shouldn't have been. * When there are duplicate files that make up a volume, show the count on series detail. * Added basic ISBN searching which will return a chapter back. * Bump versions by dotnet-bump-version. * Fixed count for cards (#2016) * Bump versions by dotnet-bump-version. * Last Release before Release Testing (#2017) * Attempting to invalidate JWT on login (when locked out), but can't figure a way to get a JWT, since we don't store them. Just committing as I'm going to remove the middleware, this is not worth the performance and complexity. * Removed some security stuff that didn't line up. * Dropping Token Expiration down to 2 days to test during release testing. * Bump versions by dotnet-bump-version. * Removed old migrations for Kavita startup. Only migrations from v0.7.2 onwards are present. (#2019) * Bump versions by dotnet-bump-version. * Fixed up jumpbar not properly disabling/enabling (#2022) * Bump versions by dotnet-bump-version. * Fix StoryArc & StoryArcNumber mismatch (#2018) * Ensure StoryArc and StoryArcNumber are max length * Trim StoryArc to remove excess spaces. * Replaced with cleaner approach. * Update with majora2007 recommendations * Bump versions by dotnet-bump-version. * Last fixes before release (#2027) * Disable login button when a login is in-progress. This will help prevent spamming when internet is slow. * Fixed a bug where an empty space could cause an error when creating a library. * Apply Split Options throughout the codebase to add extra safe-guard on empty spaces and ensure trimming. * Bump versions by dotnet-bump-version. * Added NoContent responses when APIs don't find entities (#2028) * Bump versions by dotnet-bump-version. * Few More Fixes (#2032) * Fixed spreads stretching on PC * Fixed a bug where reading list dates couldn't be cleared out. * Reading list page refreshes after updating info in the modal * Fixed an issue where create library wouldn't take into account advanced settings. * Fixed an issue where selection of the first chapter of a series to pull series-level metadata could fail in cases where you had Volume 2 and Chapter 1, Volume 2 would be selected. * Bump versions by dotnet-bump-version. * Fixed a bug where scan series wouldn't trigger word count analysis nor cover generation. (#2035) * Bump versions by dotnet-bump-version. * Okay this should be the last (#2037) * Fixed improper date visualization for reading list detail page. * Correct not-read badge position (#2034) --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Fixed a bug where reading list month wasn't rendering correctly (#2039) * Bump versions by dotnet-bump-version. * Version bump (#2040) * Bump versions by dotnet-bump-version. * Bugfixes for a hotfix (#2052) * Nothing changed, this is just to retrigger a stable build. (#1967) * v0.7.3 - The Quality of Life Update (#2036) * Version bump * Okay this should be the last (#2037) * Fixed improper date visualization for reading list detail page. * Correct not-read badge position (#2034) --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Merged develop in --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * v0.7.3 - The Quality of Life Update (#2041) * Report Media Issues (#1964) * Started working on a report problems implementation. * Started code * Added logging to book and archive service. * Removed an additional ComicInfo read when comicinfo is null when trying to load. But we've already done it once earlier, so there really isn't any point. * Added basic implementation for media errors. * MediaErrors will ignore duplicate errors when there are multiple issues on same file in a scan. * Fixed unit tests * Basic code in place to view and clear. Just UI Cleanup needed. * Slight css upgrade * Fixed up centering and simplified the code to use regular array instead of observables as it wasn't working. * Fixed unit tests * Fixed unit tests for real * Bump versions by dotnet-bump-version. * Expanded Metadata for EPUBs (#1965) * Fixed a bug breaking ability to save server settings * Explicitly capture more people roles from Epubs, else fallback to how we do it now. It seems to be getting called twice and 2nd time is overriding data. Not sure why * Refactored the code to clean it up * Added support for generating collections or reading list based on dc:title and collection title-type with an optional display-seq. * ReadingList/Collection support can't be done until VersOne supports. https://github.com/vers-one/EpubReader/issues/81 * Double include author for epub parsing and let the People code handle removing duplicates. * Bump versions by dotnet-bump-version. * Nothing changed, this is just to retrigger a stable build. (#1967) (#1968) * Adding paper book reader theme (#1976) * Adding paper book reader theme # Added - Added: Paper book reader theme * Fixing some leftover styles * adding book emulation to 2column layout for paper style * Adding migrations * removing migration and compressing image * Reverting DataContextModelSnapshot * checking out datacontextmodelsnapshot file * Bump versions by dotnet-bump-version. * Web Links (#1983) * Updated dependencies * Updated the default key to be 256 bits to meet security requirements. * Added basic implementation of web link resolving favicon. Needs lots more work and testing on all OSes. * Implemented ability to see links and click on them for an individual chapter. * Hooked up the ability to set Series web links. * Render out the web link * Refactored out the favicon so there is a backup in case it fails. Refactored the baseline image placeholders to be dark mode since that is the default. * Added Robbie's nice error weblink fallbacks. * Bump versions by dotnet-bump-version. * Updated Docker entrypoint (#1984) * Bump versions by dotnet-bump-version. * ISBN Support (#1985) * Fixed a bug where weblinks would always show * Started to try and support ico -> png conversion by manually grabbing image data out, but it's hard as hell. * Implemented ability to parse out ISBN codes for books and ISBN-13 codes for ComicInfo. I can't figure out ISBN-10. * Fixed Favicon not working on anything but windows * Implemented ISBN support into Kavita * Don't round so much when transforming bytes * Bump versions by dotnet-bump-version. * AVIF Support & Much More! (#1992) * Expand the list of potential favicon icons to grab. * Added a url mapping functionality to use alternative urls for fetching icons * Initial commit to streamline media encoding. No DB migration yet, No UI changes, no Task changes. * Started refactoring code so that webp queries use encoding format instead. * More refactoring to remove hardcoded webp references. * Moved manual migrations to their own folder to keep things organized. Manually drop the obsolete webp keys. * Removed old apis for converting media and now have one. Reworked where the conversion code was located and streamlined events and whatnot. * Make favicon encode setting aware * Cleaned up favicon conversion * Updated format counter to now just use Extension from MangaFile now that it's been out a while. * Tweaked jumpbar code to reduce a lookup to hashmap. * Added AVIF (8-bit only) support. * In UpdatePeopleList, use FirstOrDefault as Single adds extra checks that may not be needed. * You can now remove weblinks from edit series page and you can leave empty cells, they will just be removed on backend. * Forgot a file * Don't prompt to write a review, just show the pencil. It's the same amount of clicks if you do, less if you dont. * Fixed Refresh token using wrong Claim to look up the user. * Refactored how we refresh authentication to perform it every 10 m ins to ensure we always stay authenticated. * Changed Version update code to run more throughout the day. Updated some hangfire to newer method signatures. * Bump versions by dotnet-bump-version. * More Fixes (#1993) * Strip just isbn: from epub isbns and log when it's back (books) * Tweaked to allow invalid GTINs but only valid ISBN 10/13s will be saved to Kavita. * Fixed a bug with parsing series from a filename that is just a chapter range and no chapter/volume keywords. * Show the media issue count before you open accordion * Added a inpage filter for Media issues * Cleanup styles * Fixed up some code in epub isbn parsing when it's null * Encode filenames when downloading so that non english characters can be passed properly to UI. * Added support to parse ComicInfo's with Empty Tags. * Reset development settings. * Tweaked the code in generating reading lists to avoid extra work when not needed. * Fix comicvine's favicon * Fixed up a unit test * Tweaked the favicon code to ignore icons that have query parameters * More favicon work. Expanded ability to grab icons a bit. Added in ability to not keep requesting favicons when we failed to parse already. * Added a note for later * Fixed stats server url * Added more debugging * Fixed unit tests * Bump versions by dotnet-bump-version. * More Fixes from Recent PRs (#1995) * Added extra debugging for logout issue * Fixed the null issue with ISBN * Allow web links to be cleared out * More logging on refresh token * More key fallback when building Table of Contents * Added better fallback implementation for building table of contents based on the many different ways epubs are packed and referenced. * Updated dependencies * Fixed up refresh token refresh which was invalidating sessions for no reason. Added it to update last active time as well. * Bump versions by dotnet-bump-version. * Fixed a bug with config (#1996) * Bump versions by dotnet-bump-version. * Changed IsDocker check (#1998) * Refactored IsDocker to be completely static and changed to use an environment variable instead. * Removed file from another branch * Bump versions by dotnet-bump-version. * Migrated up to VersOne 3.3 with epub 3.3 support. (#1999) This enables collection and reading list support from epubs. * Bump versions by dotnet-bump-version. * More Bugfixes (EPUB Mainly) (#2004) * Fixed an issue with downloading where spaces turned into plus signs. * If the refresh token is invalid, but the auth token still has life in it, don't invalidate. * Fixed docker users unable to save settings * Show a default error icon until favicon loads * Fixed a bug in mappings (keys/files) to pages that caused some links not to map appropriately. Updated epub-reader to v3.3.2. * Expanded Table of Content generation by also checking for any files that are named Navigation.xhtml to have Kavita generate a simple ToC from (instead of just TOC.xhtml) * Added another hack to massage key to page lookups when rewriting anchors. * Cleaned up debugging notes * Bump versions by dotnet-bump-version. * More Polish (#2005) * Implemented sort title extraction from epub 3 files. * Added link to wiki for media errors * Fixed the hack to reduce JWT refresh token expiration * Fixed up a case where favicon downloading wasn't correcting links that started with // correctly. Added a fallback for sites that just don't pngs available. * Implemented a mechanism to fallback to Kavita's website for favicons which can be dynamically added/updated by the community. * Reworked the logic for bookwalker which will fail to get the base html, so we have to rely on the fallback handler. * Bump versions by dotnet-bump-version. * Angular 16 (#2007) * Removed adv, which isn't needed. * Updated zone * Updated to angular 16 * Updated to angular 16 (partially) * Updated to angular 16 * Package update for Angular 16 (and other dependencies) is complete. * Replaced all takeUntil(this.onDestroy) with new takeUntilDestroyed() * Updated all inputs that have ! to be required and deleted all unit tests. * Corrected how takeUntilDestroyed() is supposed to be implemented. * Bump versions by dotnet-bump-version. * Pipeline adjustment for Angular 16 (#2008) * Bump versions by dotnet-bump-version. * Try a different build (#2009) * Bump versions by dotnet-bump-version. * Continue Reading Bugfix (#2010) * Fixed an edge case where continue point wasn't considering any chapters that had progress. Continue point is now slightly faster and uses less memory. * Added a unit test for a user's case. Still not reproducible * Bump versions by dotnet-bump-version. * Ensure chapters are sorted when getting continue point (#2011) Fixes new behaviour in #1625 * Bump versions by dotnet-bump-version. * Strip more forms of comments from CSS before parsing/inlining. (#2014) Handle if ExCSS throws an exception during inlining and attempt to fallback to scoping css instead of inlining. I still cannot update past ExCSS v4.1.0 else NPEs for common css will be thrown. * Bump versions by dotnet-bump-version. * Misc Changes (#2015) * Updated ng-bootstrap * Fixed an issue where jumpbar would be disabled when it shouldn't have been. * When there are duplicate files that make up a volume, show the count on series detail. * Added basic ISBN searching which will return a chapter back. * Bump versions by dotnet-bump-version. * Fixed count for cards (#2016) * Bump versions by dotnet-bump-version. * Last Release before Release Testing (#2017) * Attempting to invalidate JWT on login (when locked out), but can't figure a way to get a JWT, since we don't store them. Just committing as I'm going to remove the middleware, this is not worth the performance and complexity. * Removed some security stuff that didn't line up. * Dropping Token Expiration down to 2 days to test during release testing. * Bump versions by dotnet-bump-version. * Removed old migrations for Kavita startup. Only migrations from v0.7.2 onwards are present. (#2019) * Bump versions by dotnet-bump-version. * Fixed up jumpbar not properly disabling/enabling (#2022) * Bump versions by dotnet-bump-version. * Fix StoryArc & StoryArcNumber mismatch (#2018) * Ensure StoryArc and StoryArcNumber are max length * Trim StoryArc to remove excess spaces. * Replaced with cleaner approach. * Update with majora2007 recommendations * Bump versions by dotnet-bump-version. * Last fixes before release (#2027) * Disable login button when a login is in-progress. This will help prevent spamming when internet is slow. * Fixed a bug where an empty space could cause an error when creating a library. * Apply Split Options throughout the codebase to add extra safe-guard on empty spaces and ensure trimming. * Bump versions by dotnet-bump-version. * Added NoContent responses when APIs don't find entities (#2028) * Bump versions by dotnet-bump-version. * Few More Fixes (#2032) * Fixed spreads stretching on PC * Fixed a bug where reading list dates couldn't be cleared out. * Reading list page refreshes after updating info in the modal * Fixed an issue where create library wouldn't take into account advanced settings. * Fixed an issue where selection of the first chapter of a series to pull series-level metadata could fail in cases where you had Volume 2 and Chapter 1, Volume 2 would be selected. * Bump versions by dotnet-bump-version. * Fixed a bug where scan series wouldn't trigger word count analysis nor cover generation. (#2035) * Bump versions by dotnet-bump-version. * Okay this should be the last (#2037) * Fixed improper date visualization for reading list detail page. * Correct not-read badge position (#2034) --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Fixed a bug where reading list month wasn't rendering correctly (#2039) * Bump versions by dotnet-bump-version. * Version bump (#2040) * Bump versions by dotnet-bump-version. * Fixed bug in CI pipeline for main --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: Chris Plaatjes <kizaing@gmail.com> Co-authored-by: pssandhu <pssandhu@users.noreply.github.com> Co-authored-by: Jolyon Suthers <jolyon.suthers@gmail.com> Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * Reverted a scaling issue for fit to width * Fixed an issue where creating a new library wouldn't persist advanced options due to a conflict with default value. When deleting a library, give the library name in the prompt. * Fixed kbd tags in epubs with paper theme having a style conflict. * Fixed an edge case where the incorrect first cover could be chosen in some strange grouping situations. * Manually sort directories as some OSes don't return them in a natural sort order. * Fixed an issue where autocompleting when adding a directory could throw an error when you're typing. --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: Chris Plaatjes <kizaing@gmail.com> Co-authored-by: pssandhu <pssandhu@users.noreply.github.com> Co-authored-by: Jolyon Suthers <jolyon.suthers@gmail.com> * Bump versions by dotnet-bump-version. * [skipci] No User facing Changes (#2054) * Setup canary GA * Fixed bad repo * Aligned GA (#2059) * v0.7.4 - Kavita+ Launch (#2117) * Initial Canary Push (#2055) * Added AniList Token * Implemented the ability to set your AniList token. License check is not in place. * Added a check that validates AniList token is still valid. As I build out more support, I will add more checks. * Refactored the code to validate the license before allowing UI control to be edited. * Started license server stuff, but may need to change approach. Hooked up ability to scrobble rating events to KavitaPlus API. * Hooked in the ability to sync Mark Series as Read/Unread * Fixed up unit tests and only scrobble when a full chapter is read naturally. * Fixed up the Scrobbling service * Tweak one of the queries * Started an idea for Scrobble History, might rework into generic TaskHistory. * AniList Token now has a validation check. * Implemented a mechanism such that events are persisted to the database, processed every X hours to the API layer, then deleted from the database. * Hooked in code for want to read so we only send what's important. Will migrate these to bulk calls to lessen strain on API server. * Added some todos. Need to take a break. * Hooked up the ability to backfill scrobble events after turning it on. * Started on integrating license key into the server and ability to turn off scrobbling at the library level. Added sync history table for scrobbling and other API based information. * Started writing to sync table * Refactored the migrations to flatten them. Started working a basic license add flow and added in some of the cache. Lots to do. * Ensure that when we backfill scrobble events, we respect if a library has scrobbling turned on or not. * Hooked up the ability to send when the series was started to be read * Refactored the UI to streamline and group KavitaPlus Account Forms. * Aligning with API * Fixed bad merge * Fixed up inputting a user license. * Hooked up a cron task that validates licenses every 4 hours and on startup. * Reworked how the update license code works so that we always update the cache and we handle removing license from user. * Cleaned up some UI code * UserDto now has if there is a valid license or not. It's not exposed though as there is no need to expose the license key ever. * Fixed a strange encoding issue with extra ". Started working on having the UI aware of the license information. Refactored all code to properly pass the correct license to the API layer. * There is a circular dependency in the code. Fixed some theme code which wasn't checking the right variable. Reworked the JWT interceptor to be better at handling async code. Lots of misc code changes, DI circular issue is still present. * Fixed the DI issue and moved all things that need bootstrapping to app.component. * Hooked up the ability to not have a donation button show up if the server default user/admin has a valid KavitaPlus license. * Refactored how we extract out ids from weblinks * Ensure if API fails, we don't delete the record. * Refactored how rate checks occur for scrobbling processing. * Lots of testing and ensuring rate limit doesn't get destroyed. * Ensure the media item is valid for that user's providers set. * Refactored the loop code into one method to keep things much cleaner * Lots of code to get the scrobbling streamlined and foolproof. Unknown series are now reported on the UI. * Prevent duplicates for scrobble errors. * Ensure we are sending the correct type to the Scrobble Provider * Ensure we send the date of the scrobble event for upstream to use. * Replaced the dedicated run backfilling of scrobble events to just trigger when setting the anilist token for the first time. Streamlined a lot of the code for adding your license to ensure user understands how it works. * Fixed a bug where scan series wasn't triggering word count or cover generation. * Started the plumbing for recommendations * Merge conflicts * Recommendation plumbing is nearly complete. * Setup response caching and general cleanup * Fixed UI not showing the recommendation tab * Switched to prod url * Fixed broken unit tests due to Hangfire not being setup for unit tests * Fixed branch selection (#2056) * Damn you GA (#2058) * Bump versions by dotnet-bump-version. * Fixed GA not pulling the right branch and removed unneeded building from veresion job (#2060) * Bump versions by dotnet-bump-version. * Canary Second (#2071) * Just started * Started building the user review card. Fixed Recommendations not having user progress on them. * Fixed a bug where scrobbling ratings wasn't working. * Added a temp ability to trigger scrobbling processing for testing. * Cleaned up the design of review card. Added a temp way to trigger scrobbling. * Fixed clear scrobbling errors and refactored so reviews now load from DB and is streamlined. * Refactored so edit review is now a single module component and editable from the series detail page. * Removed SyncHistory table as it's no longer needed. Refactored read events to properly update to the latest progress information. Refactored to a new way of clearing events, so that user's can see their scrobble history. * Fixed a bug where Anilist token wouldn't show as set due to some state issue * Added the ability to see your own scrobble events * Avoid a potential collision with recommendations. * Fixed an issue where when checking for a license on UI, it wouldn't force the check (in case server was down on first check). * External reviews are implemented. * Fixed unit tests * Bump versions by dotnet-bump-version. * Made the api url dynamic based on dev more or not. (#2072) * Bump versions by dotnet-bump-version. * Canary Build 3 (#2079) * Updated reviews to have tagline support to match how Anilist has them. Cleaned up the KavitaPlus documentation and added a feature list. Review cards look much better. * Fixed up a NPE in scrobble event creation * Removed the ability to have images leak in the read more review card. Review's now show the user if they are a local user, else External. * Added caching to the reviews and recommendations that come from an external source. Max of 50MB will be used across whole instance. Entries are cached for 1 hour. * Reviews are looking much better * Added the ability for users to share their series reviews with other users on the server via a new opt-in mechanism. Fixed up some cache busting mechanism for reviews. * More review polish to align with better matching * Added the extra information for Recommendation matching. * Preview of the review is much cleaner now and the full body is styled better. * More anilist specific syntax * Fixed bad regex * Added the ability to bust cache. Spoilers are now implemented for reviews. Introduces: --review-spoiler-bg-color --review-spoiler-text-color * Bump versions by dotnet-bump-version. * Canary Build 4 (#2086) * Updated Kavita Plus feature list. Added a hover-over to the progress bars in the app to know exact percentage of reading for a chapter or series. * Added a button to go to external review. Changed how enums show in the documentation so you can see their string value too. Limited reviews to top 10 with proper ordering. Drastically cleaned up how we handle preview summary generation * Cleaned up the margin below review section * Fixed an issue where a processed scrobble event would get updated instead of a new event created. * By default, there is now a prompt on series review to add your own, which fills up the space nicely. Added the backend for Series Holds. * Scrobble History is now ordered by recent -> latest. Some minor cleanup in other files. * Added a simple way to see and toggle scrobble service from the series. * Fixed a bug where updating the user's last active time wasn't writing to database and causing a logout event. * Tweaked the registration email wording to be more clear for email field. * Improved OPDS Url generation and included using host name if defined. * Fixed the issues with choosing the correct series cover image. Added many unit tests to cover the edge cases. * Small cleanup * Fixed an issue where urls with , in them would break weblinks. * Fixed a bug where we weren't trying a png before we hit fallback for favicon parsing. * Ensure scrobbling tab isn't active without a license. Changed how updating user last active worked to supress more concurrency issues. * Fixed an issue where duplicate series could appear on newly added during a scan. * Bump versions by dotnet-bump-version. * Fixed a bad dto (#2087) * Bump versions by dotnet-bump-version. * Canary Build 4 (#2089) * New server-based auth is in place with the ability to register the instance. * Refactored to single install bound licensing. * Made the Kavita+ tab gold. * Change the JWTs to last 10 days. This is a self-hosted software and the usage doesn't need the level of 2 days expiration * Bump versions by dotnet-bump-version. * Canary Build 4 (#2090) * By default, a new library will only have scrobbling on if it's of type book or manga given current scrobble providers. * Started building out external reviews. * Added the ability to re-enter your license information. * Fixed side nav not extending enough * Fixed a bug with info cards * Integrated rating support, fixed review cards without a tagline, and misc fixes. * Streamlined where ratings are located on series detail page. * Aligned with other series lookups * Bump versions by dotnet-bump-version. * Canary Build 6 (#2092) * Cleaned up some messaging * Fixed up series detail * Cleanup * Bump versions by dotnet-bump-version. * Canary Build 6 (#2093) * Fixed scrobble token not being visible by default. * Added a loader for external reviews * Added the ability to edit series details (weblinks) from Scrobble Issues page. * Slightly lessened the focus on buttons * Fixed review cards so whenever you click your own review, it will open the edit modal. * Need for speed - Updated Kavita log to be much smaller and replaced all code ones with a 32x version. * Optimized a ton of our images to be much smaller and faster to load. * Added more MIME types for response compression * Edit Series modal name field should be readonly as it is directly mapped to file metadata or filename parsed. It shouldn't be changeable via the UI. * Removed the ability to update the Series name via Kavita UI/API as it is no longer editable. * Moved Image component to be standalone * Moved ReadMore component to be standalone * Moved PersonBadge component to be standalone * Moved IconAndTitle component to be standalone * Fixed some bugs with standalone. * Hooked in the ability to scrobble series reviews. * Refactored everything to use HashUtil token rather than InstallId. * Swapped over to a generated machine token and fixed an issue where after registering, the license would not say valid. * Added the missing migration for review scrobble events. * Clean up some wording around busting cache. * Fixed a bug where chapters within a volume could be unordered in the UI info screen. * Refactored to prepare for external series rendering on series detail. * Implemented external recs * Bump versions by dotnet-bump-version. * Canary Build 7 (#2097) * Aligned ExtractId to extract a long, since MAL id can be just that. * Fixed external series card not clicking correctly. Fixed a bug when extracting a Mal link. Fixed cancel button on license component. * Renamed user-license to license component given new direction for licensing. * Implemented card layout for recommendations * Moved more components over to be standalone and removed pipes module. This is going to take some time for sure. * Removed Cards and SharedCardsSideNav and SideNav over to standalone. This has been shaken out. * Cleaned up a bunch of extra space on reading list detail page. * Fixed rating popover not having a black triangle. * When checking license, show a loading indicator for validity icon. * Cache size can now be changed by admins if they want to give more memory for better browsing. * Added LastReadTime * Cleanup the scrobbling control text for Library Settings. * Fixed yet another edge case for getting series cover image where first volume is higher than 1 and the rest is just loose leaf chapters. * Changed OPDS Content Type to be application/atom+xml to align better with the spec. * Fixed unit tests * Bump versions by dotnet-bump-version. * Canary Build 7 (#2098) * Fixed the percentage readout on card item progress bar * Ensure scrobble control is always visible * Review card could show person icon in tablet viewport. * Changed how the ServerToken for node locking works as docker was giving different results each time. * After we update series metadata, bust cache * License componet cleanup on the styles * Moved license to admin module and removed feature modal as wiki is much easier to maintain. * Bump versions by dotnet-bump-version. * Canary Build 8 (#2100) * Fixed a very slight amount of the active nav tag bleeding outside the border radius * Switched how we count words in epub to handle languages that don't have spaces. * Updated dependencies and fixed a series cover image on list item view for recs. * Fixed a bug where external recs werent showing summary of the series. * Rewrote the rec loop to be cleaner * Added the ability to see series summary on series detail page on list view. Changed Scrobble Event page to show in server time and not utc. * Added tons of output to identify why unraid generates a new fingerprint each time. * Refactored scrobble event table to have filtering and pagination support. Fixed a few bad template issues and fixed loading scrobbling tab on refresh of page. * Aligned a few apis to use a default pagination rather than a higher level one. * Undo OPDS change as Chunky/Panels break. * Moved the holds code around * Don't show an empty review for the user, it eats up uneeded space and is ugly. * Cleaned up the review code * Fixed a bug with arrow on sortable table header. * More scrobbling debug information to ensure events are being processed correctly. * Applied a ton of code cleanup build warnings * Enhanced rec matching by prioritizing matching on weblinks before falling back to name matching. * Fixed the calculation of word count for epubs. * Bump versions by dotnet-bump-version. * Canary Build 9 (#2104) * Added another unit test * Changed how we create cover images to force the aspect ratio, which allows for Kavita to do some extra work later down the line. Prevents skewing from comic sources. * Code cleanup * Updated signatures to explicitly indicate they return a physical file. * Refactored the GA to be a bit more streamlined. * Fixed up how after cover conversion, how we refresh volume and series image links. * Undid the PhysicalFileResult stuff. * Fixed an issue in the epub reader where html tags within an anchor could break the navigation code for inner-links. * Fixed a bug in GetContinueChapter where a special could appear ahead of a loose leaf chapter. * Optimized aspect ratios for custom library images to avoid shift layout. Moved the series detail page down a bit to be inline with first row of actionables. * Finally fixed the media conversion issue where volumes and series wouldn't get their file links updated. * Added some new layout for license to allow a user to buy a sub after their last sub expired. * Added more metrics for fingerprinting to test on docker. * Tried to fix a bug with getnextchapter looping incorrectly, but unable to solve. * Cleanup some UI stuff to reduce bad calls. * Suppress annoying issues with reaching K+ when it's down (only affects local builds) * Fixed an edge case bug for picking the correct cover image for a series. * Fixed a bug where typeahead x wouldn't clear out the input field. * Renamed Clear -> Reset for metadata filter to be more informative of its function. * Don't allow duplicates for reading list characters. * Fixed a bug where when calculating recently updated, series with the same name but different libraries could get grouped. * Fixed an issue with fit to height where there could still be a small amount of scroll due to a timing issue with the image loading. * Don't show a loading if the user doesn't have a license for external ratings * Fixed bad stat url * Fixed up licensing to make it so you have to email me to get a sub renewed. * Updated deps * When scrobbling reading events, recalculate the highest chapter/volume during processing. * Code cleanup * Disabled some old test code that is likely not needed as it breaks a lot on netvips updates * Bump versions by dotnet-bump-version. * Canary Build 10 (#2105) * Aligned fingerprint to be unique * Updated email button to have a template * Fixed inability to progress to next chapter when last page is a spread and user is using split rendering. * Attempted fix at the column reader cutting off parts of the words. Can't fully reproduce, but added a bit of padding to help. * Aligned AniList icon to match that of weblinks. * Bump versions by dotnet-bump-version. * Canary Build 11 (#2108) * Fixed an issue with continuous reader in manga reader. * Aligned KavitaPlus->Kavita+ * Updated the readme * Adjusted first time registration messaging. * Fixed a bug where having just one type of weblink could cause a bad recommendation lookup * Removed manual invocation of scrobbling as testing is over for that feature. * Fixed a bad observerable for downloading logs from browser. * Don't get reviews/recs for comic libraries. Override user selection for scrobbling on Comics since there are no places to scrobble to. * Added a migration so all existing comic libraries will have scrobbling turned off. * Don't allow the UI to toggle scrobbling on a library with no providers. * Refactored the code to not throw generic 500 toasts on the UI. Added the ability to clear your license on Kavita side. * Converted reader settings to new accordion format. * Converted user preferences to new accordion format. * I couldn't convert CBL Reading modal to new accordion directives due to some weird bug. * Migrated the whole application to standalone components. This fixes the download progress bar not showing up. * Hooked up the ability to have reading list generate random items. Removed the old code as it's no longer needed. * Added random covers for collection's as well. * Added a speed up to not regenerate merged covers if we've already created them. * Fixed an issue where tooltips weren't styled correctly after updating a library. Migrated Library access modal to OnPush. * Fixed broken table styling. Fixed grid breakpoint css variables not using the ones from variables due to a missing import. * Misc fixes around tables and some api doc cleanup * Fixed a bug where when switching from webtoon back to a non-webtoon reading mode, if the browser size isn't large enough for double, the reader wouldn't go to single mode. * When combining external recs, normalize names to filter out differences, like capitalization. * Finally get to update ExCSS to the latest version! This adds much more css properties for epubs. * Ensure rejected reviews are saved as errors * A crap ton of code cleanup * Cleaned up some equality code in GenreHelper.cs * Fixed up the table styling after the bootstrap update changed it. * Bump versions by dotnet-bump-version. * Canary Build 12 (#2111) * Aligned GA (#2059) * Fixed the code around merging images to resize them. This will only look correct if this release's cover generation runs. * Misc code cleanup * Fixed an issue with epub column layout cutting off text * Collection detail page will now default sort by sort name. * Explicitly lazy load library icon images. * Make sure the full error message can be passed to the license component/user. * Use WhereIf in some places * Changed the hash util code for unraid again * Fixed up an issue with split render mode where last page wouldn't move into the next chapter. * Bump versions by dotnet-bump-version. * Don't ask me how, but i think I fixed the epub cutoff issue (#2112) * Bump versions by dotnet-bump-version. * Canary 14 (#2113) * Switched how we build the unraid fingerprint. * Fixed a bit of space below the image on fit to height * Removed some bad code * Bump versions by dotnet-bump-version. * Canary Build 15 (#2114) * When performing a scan series, force a recount of words/pages to ensure read time gets updated. * Fixed broken download logs button (develop) * Sped up the query for getting libraries and added caching for that api, which is helpful for users with larger library counts. * Fixed an issue in directory picker where if you had two folders with the same name, the 2nd to last wouldn't be clickable. * Added more destroy ref stuff. * Switched the buy/manage links over to be environment specific. * Bump versions by dotnet-bump-version. * Canary Build 16 (#2115) * Added the promo code for K+ and version bump. * Don't show see more if there isn't more to see on series detail. * Bump versions by dotnet-bump-version. * Last Build (#2116) * Merge * Close the view after removing a license key from server. * Bump versions by dotnet-bump-version. * Reset version to v0.7.4 for merge. * Bump versions by dotnet-bump-version. * Cleanup from the Release (#2127) * Added an FAQ link on the Kavita+ tab. * Don't query Kavita+ for ratings on comic libraries as there is no upstream provider yet. * Jumpbar keys are a little hard to click * Fixed an issue where libraries that don't allow scrobbling could be scrobbled when generating past history with read events. * Made the min/max release year on metadata filter number and removed the spin arrows for styling. * Fixed disable tabs color contrast due to bootstrap undocumented change. * Refactored whole codebase to unify caching mechanism. Upped the default cache memory amount to 75 to account for the extra data load. Still LRU. Fixed an issue where Cache key was using Port instead. Refactored all the Configuration code to use strongly typed deserialization. * Fixed an issue where get latest progress would throw an exception if there was no progress due to LINQ and MAX query. * Fixed a bug where Send to Device wasn't present on Series cards. * Hooked up the ability to change the cache size for Kavita via the UI. * Bump versions by dotnet-bump-version. * Overall Ratings (#2129) * Corrected tooltip for Cache * Ensure we sync the DB to what's in appsettings.json for Cache key. * Change the fingerprinting method for Windows installs exclusively to avoid churn due to how security updates are handled. * Hooked up the ability to see where reviews are from via an icon on the review card, rather than having to click or know that MAL has "external Review" as title. * Updated FAQ for Kavita+ to link directly to the FAQ * Added the ability for all ratings on a series to be shown to the user. Added favorite count on AL and MAL * Cleaned up so the check for Kavita+ license doesn't seem like it's running when no license is registered. * Tweaked the test instance buy link to test new product. * Bump versions by dotnet-bump-version. * Remove From On Deck (#2131) * Allow admins to customize the amount of progress time or last item added time for on deck calculation * Implemented the ability to remove series from on deck. They will be removed until the user reads a new chapter. Quite a few db lookup reduction calls for reading based stuff, like continue point, bookmarks, etc. * Bump versions by dotnet-bump-version. * Preparation for Release (#2135) * Don't allow Comic libraries to do any scrobbling as there aren't any Comic scrobbling providers yet. * Fixed a bug where if you have multiple libraries pointing the same folder (for whatever reason), the Scan Folder api could be rejected. * Handle if publication from an epub is empty to avoid a bad parse error * Cleaned up some hardcoded default strings. * Fixed up some defaulting code for the cache size. * Changed how moving something back to on deck works after it's been removed. Now any progress will trigger it, as epubs don't have chapters. * Ignore .caltrash, which is a Calibre managed folder, when scanning. * Added the ability to see Volume Last Read Date (or individual chapter) in details drawer. Hover over the clock for the full timestamp. * Bump versions by dotnet-bump-version. * Forgot 2 files in last PR (#2136) * Don't allow Comic libraries to do any scrobbling as there aren't any Comic scrobbling providers yet. * Fixed a bug where if you have multiple libraries pointing the same folder (for whatever reason), the Scan Folder api could be rejected. * Handle if publication from an epub is empty to avoid a bad parse error * Cleaned up some hardcoded default strings. * Fixed up some defaulting code for the cache size. * Changed how moving something back to on deck works after it's been removed. Now any progress will trigger it, as epubs don't have chapters. * Ignore .caltrash, which is a Calibre managed folder, when scanning. * Added the ability to see Volume Last Read Date (or individual chapter) in details drawer. Hover over the clock for the full timestamp. * Somehow some files got left off the commit * Bump versions by dotnet-bump-version. * Changed the fingerprinting code for Kavita+. Optimized System tab to be way faster. (#2140) * Bump versions by dotnet-bump-version. * Version bump (#2141) * Bump versions by dotnet-bump-version. * Personal Table of Contents (#2148) * Fixed a bad default setting for token key * Changed the payment link to support Google Pay * Fixed duplicate events occurring on newly added series from a scan. Fixed the version update code from not firing and made it check every 4-6 hours (random per user per restart) * Check for new releases on startup as well. Added Personal Table of Contents (called Bookmarks on epub and pdf reader). The idea is that sometimes you want to bookmark certain parts of pages to get back to quickly later. This mechanism will allow you to do that without having to edit the underlying ToC. * Added a button to update modal to show how to update for those unaware. * Darkened the link text within tables to be more visible. * Update link for how to update now is dynamic for docker users * Refactored to send proper star/end dates for scrobble read events for upcoming changes in the API. Added GoogleBooks Rating UI code if I go forward with API changes. * When Scrobbling, send when the first and last progress for the series was. Added OpenLibrary icon for upcoming enhancements for Kavita+. Changed the Update checker to execute at start. * Fixed backups not saving favicons in the correct place * Refactored the layout code for Personal ToC * More bugfixes around toc * Box alignment * Fixed up closing the overlay when bookmark mode is active * Fixed up closing the overlay when bookmark mode is active --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> * Bump versions by dotnet-bump-version. * Add files via upload (#2149) * Bump versions by dotnet-bump-version. * Misc Fixes (#2155) * Fixed default token key not being long enough and Kavita auto-generating * When scheduling nightly backup job, make it run at 2am to ensure everything else has ran. * Made the overlay system work better on mobile. In order to do this, had to implement my own copy button. * Tweaked the code to ensure we clear the selection doing anything and clicking off the overlay clears more reliably. * Cleaned up the overlay code * Added the ability to view the series that a rating is representing. Requires Kavita+ deployment. * When calculating overall average rating of server, if only review is yours, don't include it. When calculating overall average rating of server, scale to percentage (* 20) to match all other rating scales. * Fixed side nav on mobile without donate link not fully covering the height of the screen * Only trigger the task conversion warning on Media screen if you've touched the appropriate control. * Fixed a bug where bookmark directory wasn't able to be changed. * Fixed a bug where see More wouldn't show if there were just characters due to missing that check. * Fixed a typo in documentation * If a chapter has a range 1-6 and is fully read, when calculating highest chapter for Scrobbling, use the 6. * Bump versions by dotnet-bump-version. * Epub Reading Overlay Re-Design (#2156) * Removed DeviceId * Dependency updates part 1 * Dependency updates part 2 * Dependency updates part 3 * Dependency updates part 4 * Dependency updates done. Updated all backend and UI ones. * Refactored the book line overlay to sit at the top of the reader. It looks much better and will work a lot better for future work. * Removed an event that was causing series detail to load extra data when it didn't need to after editing series metadata. * Removed one more load request on series detail after updating edit series modal. * Bump versions by dotnet-bump-version. * Removed manual Migrate Series Relations migration from v0.7 release (5 releases ago). (#2158) Don't try to backup the DB if it doesn't exist. This will stop errors in log on first start. * Bump versions by dotnet-bump-version. * Rating Overhaul (#2159) * Switched Ratings to a float system. Allow rating something as 0%. Allow half step ratings. Added new css variable: --rating-star-color. By default, N/A will show for series that have no ratings. N/A ratings are not included in overall rating calculations. * Show extended entity properties on desktop for list view cards. * Refactored the code for series metadata detail to use a re-usable component to reduce the copy/paste for the Genres tags like sections. * List Item will show extended properties about a chapter/volume, like weblinks on Desktop viewports. * Refactored even further so all of series detail uses the same component code. Tweaked the spacing on the series detail area. List items will now show Characters and Tags which are helpful for more Hentai related content. * Fixed a bug with removing something from "OnDeckRemoval" table when something was read. * Bump versions by dotnet-bump-version. * Few fixes from last PR (#2160) * Added a migration for existing ratings * Fixed duplicating web links and changed so it has the see more functionality. * One more unit test * Bump versions by dotnet-bump-version. * v0.7.6 - Personal Table of Contents + Rating Overhaul (#2169) * Nothing changed, this is just to retrigger a stable build. (#1967) * v0.7.3 - The Quality of Life Update (#2036) * Version bump * Okay this should be the last (#2037) * Fixed improper date visualization for reading list detail page. * Correct not-read badge position (#2034) --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Merged develop in --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * v0.7.3 - The Quality of Life Update (#2041) * Report Media Issues (#1964) * Started working on a report problems implementation. * Started code * Added logging to book and archive service. * Removed an additional ComicInfo read when comicinfo is null when trying to load. But we've already done it once earlier, so there really isn't any point. * Added basic implementation for media errors. * MediaErrors will ignore duplicate errors when there are multiple issues on same file in a scan. * Fixed unit tests * Basic code in place to view and clear. Just UI Cleanup needed. * Slight css upgrade * Fixed up centering and simplified the code to use regular array instead of observables as it wasn't working. * Fixed unit tests * Fixed unit tests for real * Bump versions by dotnet-bump-version. * Expanded Metadata for EPUBs (#1965) * Fixed a bug breaking ability to save server settings * Explicitly capture more people roles from Epubs, else fallback to how we do it now. It seems to be getting called twice and 2nd time is overriding data. Not sure why * Refactored the code to clean it up * Added support for generating collections or reading list based on dc:title and collection title-type with an optional display-seq. * ReadingList/Collection support can't be done until VersOne supports. https://github.com/vers-one/EpubReader/issues/81 * Double include author for epub parsing and let the People code handle removing duplicates. * Bump versions by dotnet-bump-version. * Nothing changed, this is just to retrigger a stable build. (#1967) (#1968) * Adding paper book reader theme (#1976) * Adding paper book reader theme # Added - Added: Paper book reader theme * Fixing some leftover styles * adding book emulation to 2column layout for paper style * Adding migrations * removing migration and compressing image * Reverting DataContextModelSnapshot * checking out datacontextmodelsnapshot file * Bump versions by dotnet-bump-version. * Web Links (#1983) * Updated dependencies * Updated the default key to be 256 bits to meet security requirements. * Added basic implementation of web link resolving favicon. Needs lots more work and testing on all OSes. * Implemented ability to see links and click on them for an individual chapter. * Hooked up the ability to set Series web links. * Render out the web link * Refactored out the favicon so there is a backup in case it fails. Refactored the baseline image placeholders to be dark mode since that is the default. * Added Robbie's nice error weblink fallbacks. * Bump versions by dotnet-bump-version. * Updated Docker entrypoint (#1984) * Bump versions by dotnet-bump-version. * ISBN Support (#1985) * Fixed a bug where weblinks would always show * Started to try and support ico -> png conversion by manually grabbing image data out, but it's hard as hell. * Implemented ability to parse out ISBN codes for books and ISBN-13 codes for ComicInfo. I can't figure out ISBN-10. * Fixed Favicon not working on anything but windows * Implemented ISBN support into Kavita * Don't round so much when transforming bytes * Bump versions by dotnet-bump-version. * AVIF Support & Much More! (#1992) * Expand the list of potential favicon icons to grab. * Added a url mapping functionality to use alternative urls for fetching icons * Initial commit to streamline media encoding. No DB migration yet, No UI changes, no Task changes. * Started refactoring code so that webp queries use encoding format instead. * More refactoring to remove hardcoded webp references. * Moved manual migrations to their own folder to keep things organized. Manually drop the obsolete webp keys. * Removed old apis for converting media and now have one. Reworked where the conversion code was located and streamlined events and whatnot. * Make favicon encode setting aware * Cleaned up favicon conversion * Updated format counter to now just use Extension from MangaFile now that it's been out a while. * Tweaked jumpbar code to reduce a lookup to hashmap. * Added AVIF (8-bit only) support. * In UpdatePeopleList, use FirstOrDefault as Single adds extra checks that may not be needed. * You can now remove weblinks from edit series page and you can leave empty cells, they will just be removed on backend. * Forgot a file * Don't prompt to write a review, just show the pencil. It's the same amount of clicks if you do, less if you dont. * Fixed Refresh token using wrong Claim to look up the user. * Refactored how we refresh authentication to perform it every 10 m ins to ensure we always stay authenticated. * Changed Version update code to run more throughout the day. Updated some hangfire to newer method signatures. * Bump versions by dotnet-bump-version. * More Fixes (#1993) * Strip just isbn: from epub isbns and log when it's back (books) * Tweaked to allow invalid GTINs but only valid ISBN 10/13s will be saved to Kavita. * Fixed a bug with parsing series from a filename that is just a chapter range and no chapter/volume keywords. * Show the media issue count before you open accordion * Added a inpage filter for Media issues * Cleanup styles * Fixed up some code in epub isbn parsing when it's null * Encode filenames when downloading so that non english characters can be passed properly to UI. * Added support to parse ComicInfo's with Empty Tags. * Reset development settings. * Tweaked the code in generating reading lists to avoid extra work when not needed. * Fix comicvine's favicon * Fixed up a unit test * Tweaked the favicon code to ignore icons that have query parameters * More favicon work. Expanded ability to grab icons a bit. Added in ability to not keep requesting favicons when we failed to parse already. * Added a note for later * Fixed stats server url * Added more debugging * Fixed unit tests * Bump versions by dotnet-bump-version. * More Fixes from Recent PRs (#1995) * Added extra debugging for logout issue * Fixed the null issue with ISBN * Allow web links to be cleared out * More logging on refresh token * More key fallback when building Table of Contents * Added better fallback implementation for building table of contents based on the many different ways epubs are packed and referenced. * Updated dependencies * Fixed up refresh token refresh which was invalidating sessions for no reason. Added it to update last active time as well. * Bump versions by dotnet-bump-version. * Fixed a bug with config (#1996) * Bump versions by dotnet-bump-version. * Changed IsDocker check (#1998) * Refactored IsDocker to be completely static and changed to use an environment variable instead. * Removed file from another branch * Bump versions by dotnet-bump-version. * Migrated up to VersOne 3.3 with epub 3.3 support. (#1999) This enables collection and reading list support from epubs. * Bump versions by dotnet-bump-version. * More Bugfixes (EPUB Mainly) (#2004) * Fixed an issue with downloading where spaces turned into plus signs. * If the refresh token is invalid, but the auth token still has life in it, don't invalidate. * Fixed docker users unable to save settings * Show a default error icon until favicon loads * Fixed a bug in mappings (keys/files) to pages that caused some links not to map appropriately. Updated epub-reader to v3.3.2. * Expanded Table of Content generation by also checking for any files that are named Navigation.xhtml to have Kavita generate a simple ToC from (instead of just TOC.xhtml) * Added another hack to massage key to page lookups when rewriting anchors. * Cleaned up debugging notes * Bump versions by dotnet-bump-version. * More Polish (#2005) * Implemented sort title extraction from epub 3 files. * Added link to wiki for media errors * Fixed the hack to reduce JWT refresh token expiration * Fixed up a case where favicon downloading wasn't correcting links that started with // correctly. Added a fallback for sites that just don't pngs available. * Implemented a mechanism to fallback to Kavita's website for favicons which can be dynamically added/updated by the community. * Reworked the logic for bookwalker which will fail to get the base html, so we have to rely on the fallback handler. * Bump versions by dotnet-bump-version. * Angular 16 (#2007) * Removed adv, which isn't needed. * Updated zone * Updated to angular 16 * Updated to angular 16 (partially) * Updated to angular 16 * Package update for Angular 16 (and other dependencies) is complete. * Replaced all takeUntil(this.onDestroy) with new takeUntilDestroyed() * Updated all inputs that have ! to be required and deleted all unit tests. * Corrected how takeUntilDestroyed() is supposed to be implemented. * Bump versions by dotnet-bump-version. * Pipeline adjustment for Angular 16 (#2008) * Bump versions by dotnet-bump-version. * Try a different build (#2009) * Bump versions by dotnet-bump-version. * Continue Reading Bugfix (#2010) * Fixed an edge case where continue point wasn't considering any chapters that had progress. Continue point is now slightly faster and uses less memory. * Added a unit test for a user's case. Still not reproducible * Bump versions by dotnet-bump-version. * Ensure chapters are sorted when getting continue point (#2011) Fixes new behaviour in #1625 * Bump versions by dotnet-bump-version. * Strip more forms of comments from CSS before parsing/inlining. (#2014) Handle if ExCSS throws an exception during inlining and attempt to fallback to scoping css instead of inlining. I still cannot update past ExCSS v4.1.0 else NPEs for common css will be thrown. * Bump versions by dotnet-bump-version. * Misc Changes (#2015) * Updated ng-bootstrap * Fixed an issue where jumpbar would be disabled when it shouldn't have been. * When there are duplicate files that make up a volume, show the count on series detail. * Added basic ISBN searching which will return a chapter back. * Bump versions by dotnet-bump-version. * Fixed count for cards (#2016) * Bump versions by dotnet-bump-version. * Last Release before Release Testing (#2017) * Attempting to invalidate JWT on login (when locked out), but can't figure a way to get a JWT, since we don't store them. Just committing as I'm going to remove the middleware, this is not worth the performance and complexity. * Removed some security stuff that didn't line up. * Dropping Token Expiration down to 2 days to test during release testing. * Bump versions by dotnet-bump-version. * Removed old migrations for Kavita startup. Only migrations from v0.7.2 onwards are present. (#2019) * Bump versions by dotnet-bump-version. * Fixed up jumpbar not properly disabling/enabling (#2022) * Bump versions by dotnet-bump-version. * Fix StoryArc & StoryArcNumber mismatch (#2018) * Ensure StoryArc and StoryArcNumber are max length * Trim StoryArc to remove excess spaces. * Replaced with cleaner approach. * Update with majora2007 recommendations * Bump versions by dotnet-bump-version. * Last fixes before release (#2027) * Disable login button when a login is in-progress. This will help prevent spamming when internet is slow. * Fixed a bug where an empty space could cause an error when creating a library. * Apply Split Options throughout the codebase to add extra safe-guard on empty spaces and ensure trimming. * Bump versions by dotnet-bump-version. * Added NoContent responses when APIs don't find entities (#2028) * Bump versions by dotnet-bump-version. * Few More Fixes (#2032) * Fixed spreads stretching on PC * Fixed a bug where reading list dates couldn't be cleared out. * Reading list page refreshes after updating info in the modal * Fixed an issue where create library wouldn't take into account advanced settings. * Fixed an issue where selection of the first chapter of a series to pull series-level metadata could fail in cases where you had Volume 2 and Chapter 1, Volume 2 would be selected. * Bump versions by dotnet-bump-version. * Fixed a bug where scan series wouldn't trigger word count analysis nor cover generation. (#2035) * Bump versions by dotnet-bump-version. * Okay this should be the last (#2037) * Fixed improper date visualization for reading list detail page. * Correct not-read badge position (#2034) --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Fixed a bug where reading list month wasn't rendering correctly (#2039) * Bump versions by dotnet-bump-version. * Version bump (#2040) * Bump versions by dotnet-bump-version. * Fixed bug in CI pipeline for main --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: Chris Plaatjes <kizaing@gmail.com> Co-authored-by: pssandhu <pssandhu@users.noreply.github.com> Co-authored-by: Jolyon Suthers <jolyon.suthers@gmail.com> Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * v0.7.3.1 Hotfix (#2053) * Report Media Issues (#1964) * Started working on a report problems implementation. * Started code * Added logging to book and archive service. * Removed an additional ComicInfo read when comicinfo is null when trying to load. But we've already done it once earlier, so there really isn't any point. * Added basic implementation for media errors. * MediaErrors will ignore duplicate errors when there are multiple issues on same file in a scan. * Fixed unit tests * Basic code in place to view and clear. Just UI Cleanup needed. * Slight css upgrade * Fixed up centering and simplified the code to use regular array instead of observables as it wasn't working. * Fixed unit tests * Fixed unit tests for real * Bump versions by dotnet-bump-version. * Expanded Metadata for EPUBs (#1965) * Fixed a bug breaking ability to save server settings * Explicitly capture more people roles from Epubs, else fallback to how we do it now. It seems to be getting called twice and 2nd time is overriding data. Not sure why * Refactored the code to clean it up * Added support for generating collections or reading list based on dc:title and collection title-type with an optional display-seq. * ReadingList/Collection support can't be done until VersOne supports. https://github.com/vers-one/EpubReader/issues/81 * Double include author for epub parsing and let the People code handle removing duplicates. * Bump versions by dotnet-bump-version. * Nothing changed, this is just to retrigger a stable build. (#1967) (#1968) * Adding paper book reader theme (#1976) * Adding paper book reader theme # Added - Added: Paper book reader theme * Fixing some leftover styles * adding book emulation to 2column layout for paper style * Adding migrations * removing migration and compressing image * Reverting DataContextModelSnapshot * checking out datacontextmodelsnapshot file * Bump versions by dotnet-bump-version. * Web Links (#1983) * Updated dependencies * Updated the default key to be 256 bits to meet security requirements. * Added basic implementation of web link resolving favicon. Needs lots more work and testing on all OSes. * Implemented ability to see links and click on them for an individual chapter. * Hooked up the ability to set Series web links. * Render out the web link * Refactored out the favicon so there is a backup in case it fails. Refactored the baseline image placeholders to be dark mode since that is the default. * Added Robbie's nice error weblink fallbacks. * Bump versions by dotnet-bump-version. * Updated Docker entrypoint (#1984) * Bump versions by dotnet-bump-version. * ISBN Support (#1985) * Fixed a bug where weblinks would always show * Started to try and support ico -> png conversion by manually grabbing image data out, but it's hard as hell. * Implemented ability to parse out ISBN codes for books and ISBN-13 codes for ComicInfo. I can't figure out ISBN-10. * Fixed Favicon not working on anything but windows * Implemented ISBN support into Kavita * Don't round so much when transforming bytes * Bump versions by dotnet-bump-version. * AVIF Support & Much More! (#1992) * Expand the list of potential favicon icons to grab. * Added a url mapping functionality to use alternative urls for fetching icons * Initial commit to streamline media encoding. No DB migration yet, No UI changes, no Task changes. * Started refactoring code so that webp queries use encoding format instead. * More refactoring to remove hardcoded webp references. * Moved manual migrations to their own folder to keep things organized. Manually drop the obsolete webp keys. * Removed old apis for converting media and now have one. Reworked where the conversion code was located and streamlined events and whatnot. * Make favicon encode setting aware * Cleaned up favicon conversion * Updated format counter to now just use Extension from MangaFile now that it's been out a while. * Tweaked jumpbar code to reduce a lookup to hashmap. * Added AVIF (8-bit only) support. * In UpdatePeopleList, use FirstOrDefault as Single adds extra checks that may not be needed. * You can now remove weblinks from edit series page and you can leave empty cells, they will just be removed on backend. * Forgot a file * Don't prompt to write a review, just show the pencil. It's the same amount of clicks if you do, less if you dont. * Fixed Refresh token using wrong Claim to look up the user. * Refactored how we refresh authentication to perform it every 10 m ins to ensure we always stay authenticated. * Changed Version update code to run more throughout the day. Updated some hangfire to newer method signatures. * Bump versions by dotnet-bump-version. * More Fixes (#1993) * Strip just isbn: from epub isbns and log when it's back (books) * Tweaked to allow invalid GTINs but only valid ISBN 10/13s will be saved to Kavita. * Fixed a bug with parsing series from a filename that is just a chapter range and no chapter/volume keywords. * Show the media issue count before you open accordion * Added a inpage filter for Media issues * Cleanup styles * Fixed up some code in epub isbn parsing when it's null * Encode filenames when downloading so that non english characters can be passed properly to UI. * Added support to parse ComicInfo's with Empty Tags. * Reset development settings. * Tweaked the code in generating reading lists to avoid extra work when not needed. * Fix comicvine's favicon * Fixed up a unit test * Tweaked the favicon code to ignore icons that have query parameters * More favicon work. Expanded ability to grab icons a bit. Added in ability to not keep requesting favicons when we failed to parse already. * Added a note for later * Fixed stats server url * Added more debugging * Fixed unit tests * Bump versions by dotnet-bump-version. * More Fixes from Recent PRs (#1995) * Added extra debugging for logout issue * Fixed the null issue with ISBN * Allow web links to be cleared out * More logging on refresh token * More key fallback when building Table of Contents * Added better fallback implementation for building table of contents based on the many different ways epubs are packed and referenced. * Updated dependencies * Fixed up refresh token refresh which was invalidating sessions for no reason. Added it to update last active time as well. * Bump versions by dotnet-bump-version. * Fixed a bug with config (#1996) * Bump versions by dotnet-bump-version. * Changed IsDocker check (#1998) * Refactored IsDocker to be completely static and changed to use an environment variable instead. * Removed file from another branch * Bump versions by dotnet-bump-version. * Migrated up to VersOne 3.3 with epub 3.3 support. (#1999) This enables collection and reading list support from epubs. * Bump versions by dotnet-bump-version. * More Bugfixes (EPUB Mainly) (#2004) * Fixed an issue with downloading where spaces turned into plus signs. * If the refresh token is invalid, but the auth token still has life in it, don't invalidate. * Fixed docker users unable to save settings * Show a default error icon until favicon loads * Fixed a bug in mappings (keys/files) to pages that caused some links not to map appropriately. Updated epub-reader to v3.3.2. * Expanded Table of Content generation by also checking for any files that are named Navigation.xhtml to have Kavita generate a simple ToC from (instead of just TOC.xhtml) * Added another hack to massage key to page lookups when rewriting anchors. * Cleaned up debugging notes * Bump versions by dotnet-bump-version. * More Polish (#2005) * Implemented sort title extraction from epub 3 files. * Added link to wiki for media errors * Fixed the hack to reduce JWT refresh token expiration * Fixed up a case where favicon downloading wasn't correcting links that started with // correctly. Added a fallback for sites that just don't pngs available. * Implemented a mechanism to fallback to Kavita's website for favicons which can be dynamically added/updated by the community. * Reworked the logic for bookwalker which will fail to get the base html, so we have to rely on the fallback handler. * Bump versions by dotnet-bump-version. * Angular 16 (#2007) * Removed adv, which isn't needed. * Updated zone * Updated to angular 16 * Updated to angular 16 (partially) * Updated to angular 16 * Package update for Angular 16 (and other dependencies) is complete. * Replaced all takeUntil(this.onDestroy) with new takeUntilDestroyed() * Updated all inputs that have ! to be required and deleted all unit tests. * Corrected how takeUntilDestroyed() is supposed to be implemented. * Bump versions by dotnet-bump-version. * Pipeline adjustment for Angular 16 (#2008) * Bump versions by dotnet-bump-version. * Try a different build (#2009) * Bump versions by dotnet-bump-version. * Continue Reading Bugfix (#2010) * Fixed an edge case where continue point wasn't considering any chapters that had progress. Continue point is now slightly faster and uses less memory. * Added a unit test for a user's case. Still not reproducible * Bump versions by dotnet-bump-version. * Ensure chapters are sorted when getting continue point (#2011) Fixes new behaviour in #1625 * Bump versions by dotnet-bump-version. * Strip more forms of comments from CSS before parsing/inlining. (#2014) Handle if ExCSS throws an exception during inlining and attempt to fallback to scoping css instead of inlining. I still cannot update past ExCSS v4.1.0 else NPEs for common css will be thrown. * Bump versions by dotnet-bump-version. * Misc Changes (#2015) * Updated ng-bootstrap * Fixed an issue where jumpbar would be disabled when it shouldn't have been. * When there are duplicate files that make up a volume, show the count on series detail. * Added basic ISBN searching which will return a chapter back. * Bump versions by dotnet-bump-version. * Fixed count for cards (#2016) * Bump versions by dotnet-bump-version. * Last Release before Release Testing (#2017) * Attempting to invalidate JWT on login (when locked out), but can't figure a way to get a JWT, since we don't store them. Just committing as I'm going to remove the middleware, this is not worth the performance and complexity. * Removed some security stuff that didn't line up. * Dropping Token Expiration down to 2 days to test during release testing. * Bump versions by dotnet-bump-version. * Removed old migrations for Kavita startup. Only migrations from v0.7.2 onwards are present. (#2019) * Bump versions by dotnet-bump-version. * Fixed up jumpbar not properly disabling/enabling (#2022) * Bump versions by dotnet-bump-version. * Fix StoryArc & StoryArcNumber mismatch (#2018) * Ensure StoryArc and StoryArcNumber are max length * Trim StoryArc to remove excess spaces. * Replaced with cleaner approach. * Update with majora2007 recommendations * Bump versions by dotnet-bump-version. * Last fixes before release (#2027) * Disable login button when a login is in-progress. This will help prevent spamming when internet is slow. * Fixed a bug where an empty space could cause an error when creating a library. * Apply Split Options throughout the codebase to add extra safe-guard on empty spaces and ensure trimming. * Bump versions by dotnet-bump-version. * Added NoContent responses when APIs don't find entities (#2028) * Bump versions by dotnet-bump-version. * Few More Fixes (#2032) * Fixed spreads stretching on PC * Fixed a bug where reading list dates couldn't be cleared out. * Reading list page refreshes after updating info in the modal * Fixed an issue where create library wouldn't take into account advanced settings. * Fixed an issue where selection of the first chapter of a series to pull series-level metadata could fail in cases where you had Volume 2 and Chapter 1, Volume 2 would be selected. * Bump versions by dotnet-bump-version. * Fixed a bug where scan series wouldn't trigger word count analysis nor cover generation. (#2035) * Bump versions by dotnet-bump-version. * Okay this should be the last (#2037) * Fixed improper date visualization for reading list detail page. * Correct not-read badge position (#2034) --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Fixed a bug where reading list month wasn't rendering correctly (#2039) * Bump versions by dotnet-bump-version. * Version bump (#2040) * Bump versions by dotnet-bump-version. * Bugfixes for a hotfix (#2052) * Nothing changed, this is just to retrigger a stable build. (#1967) * v0.7.3 - The Quality of Life Update (#2036) * Version bump * Okay this should be the last (#2037) * Fixed improper date visualization for reading list detail page. * Correct not-read badge position (#2034) --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Merged develop in --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * v0.7.3 - The Quality of Life Update (#2041) * Report Media Issues (#1964) * Started working on a report problems implementation. * Started code * Added logging to book and archive service. * Removed an additional ComicInfo read when comicinfo is null when trying to load. But we've already done it once earlier, so there really isn't any point. * Added basic implementation for media errors. * MediaErrors will ignore duplicate errors when there are multiple issues on same file in a scan. * Fixed unit tests * Basic code in place to view and clear. Just UI Cleanup needed. * Slight css upgrade * Fixed up centering and simplified the code to use regular array instead of observables as it wasn't working. * Fixed unit tests * Fixed unit tests for real * Bump versions by dotnet-bump-version. * Expanded Metadata for EPUBs (#1965) * Fixed a bug breaking ability to save server settings * Explicitly capture more people roles from Epubs, else fallback to how we do it now. It seems to be getting called twice and 2nd time is overriding data. Not sure why * Refactored the code to clean it up * Added support for generating collections or reading list based on dc:title and collection title-type with an optional display-seq. * ReadingList/Collection support can't be done until VersOne supports. https://github.com/vers-one/EpubReader/issues/81 * Double include author for epub parsing and let the People code handle removing duplicates. * Bump versions by dotnet-bump-version. * Nothing changed, this is just to retrigger a stable build. (#1967) (#1968) * Adding paper book reader theme (#1976) * Adding paper book reader theme # Added - Added: Paper book reader theme * Fixing some leftover styles * adding book emulation to 2column layout for paper style * Adding migrations * removing migration and compressing image * Reverting DataContextModelSnapshot * checking out datacontextmodelsnapshot file * Bump versions by dotnet-bump-version. * Web Links (#1983) * Updated dependencies * Updated the default key to be 256 bits to meet security requirements. * Added basic implementation of web link resolving favicon. Needs lots more work and testing on all OSes. * Implemented ability to see links and click on them for an individual chapter. * Hooked up the ability to set Series web links. * Render out the web link * Refactored out the favicon so there is a backup in case it fails. Refactored the baseline image placeholders to be dark mode since that is the default. * Added Robbie's nice error weblink fallbacks. * Bump versions by dotnet-bump-version. * Updated Docker entrypoint (#1984) * Bump versions by dotnet-bump-version. * ISBN Support (#1985) * Fixed a bug where weblinks would always show * Started to try and support ico -> png conversion by manually grabbing image data out, but it's hard as hell. * Implemented ability to parse out ISBN codes for books and ISBN-13 codes for ComicInfo. I can't figure out ISBN-10. * Fixed Favicon not working on anything but windows * Implemented ISBN support into Kavita * Don't round so much when transforming bytes * Bump versions by dotnet-bump-version. * AVIF Support & Much More! (#1992) * Expand the list of potential favicon icons to grab. * Added a url mapping functionality to use alternative urls for fetching icons * Initial commit to streamline media encoding. No DB migration yet, No UI changes, no Task changes. * Started refactoring code so that webp queries use encoding format instead. * More refactoring to remove hardcoded webp references. * Moved manual migrations to their own folder to keep things organized. Manually drop the obsolete webp keys. * Removed old apis for converting media and now have one. Reworked where the conversion code was located and streamlined events and whatnot. * Make favicon encode setting aware * Cleaned up favicon conversion * Updated format counter to now just use Extension from MangaFile now that it's been out a while. * Tweaked jumpbar code to reduce a lookup to hashmap. * Added AVIF (8-bit only) support. * In UpdatePeopleList, use FirstOrDefault as Single adds extra checks that may not be needed. * You can now remove weblinks from edit series page and you can leave empty cells, they will just be removed on backend. * Forgot a file * Don't prompt to write a review, just show the pencil. It's the same amount of clicks if you do, less if you dont. * Fixed Refresh token using wrong Claim to look up the user. * Refactored how we refresh authentication to perform it every 10 m ins to ensure we always stay authenticated. * Changed Version update code to run more throughout the day. Updated some hangfire to newer method signatures. * Bump versions by dotnet-bump-version. * More Fixes (#1993) * Strip just isbn: from epub isbns and log when it's back (books) * Tweaked to allow invalid GTINs but only valid ISBN 10/13s will be saved to Kavita. * Fixed a bug with parsing series from a filename that is just a chapter range and no chapter/volume keywords. * Show the media issue count before you open accordion * Added a inpage filter for Media issues * Cleanup styles * Fixed up some code in epub isbn parsing when it's null * Encode filenames when downloading so that non english characters can be passed properly to UI. * Added support to parse ComicInfo's with Empty Tags. * Reset development settings. * Tweaked the code in generating reading lists to avoid extra work when not needed. * Fix comicvine's favicon * Fixed up a unit test * Tweaked the favicon code to ignore icons that have query parameters * More favicon work. Expanded ability to grab icons a bit. Added in ability to not keep requesting favicons when we failed to parse already. * Added a note for later * Fixed stats server url * Added more debugging * Fixed unit tests * Bump versions by dotnet-bump-version. * More Fixes from Recent PRs (#1995) * Added extra debugging for logout issue * Fixed the null issue with ISBN * Allow web links to be cleared out * More logging on refresh token * More key fallback when building Table of Contents * Added better fallback implementation for building table of contents based on the many different ways epubs are packed and referenced. * Updated dependencies * Fixed up refresh token refresh which was invalidating sessions for no reason. Added it to update last active time as well. * Bump versions by dotnet-bump-version. * Fixed a bug with config (#1996) * Bump versions by dotnet-bump-version. * Changed IsDocker check (#1998) * Refactored IsDocker to be completely static and changed to use an environment variable instead. * Removed file from another branch * Bump versions by dotnet-bump-version. * Migrated up to VersOne 3.3 with epub 3.3 support. (#1999) This enables collection and reading list support from epubs. * Bump versions by dotnet-bump-version. * More Bugfixes (EPUB Mainly) (#2004) * Fixed an issue with downloading where spaces turned into plus signs. * If the refresh token is invalid, but the auth token still has life in it, don't invalidate. * Fixed docker users unable to save settings * Show a default error icon until favicon loads * Fixed a bug in mappings (keys/files) to pages that caused some links not to map appropriately. Updated epub-reader to v3.3.2. * Expanded Table of Content generation by also checking for any files that are named Navigation.xhtml to have Kavita generate a simple ToC from (instead of just TOC.xhtml) * Added another hack to massage key to page lookups when rewriting anchors. * Cleaned up debugging notes * Bump versions by dotnet-bump-version. * More Polish (#2005) * Implemented sort title extraction from epub 3 files. * Added link to wiki for media errors * Fixed the hack to reduce JWT refresh token expiration * Fixed up a case where favicon downloading wasn't correcting links that started with // correctly. Added a fallback for sites that just don't pngs available. * Implemented a mechanism to fallback to Kavita's website for favicons which can be dynamically added/updated by the community. * Reworked the logic for bookwalker which will fail to get the base html, so we have to rely on the fallback handler. * Bump versions by dotnet-bump-version. * Angular 16 (#2007) * Removed adv, which isn't needed. * Updated zone * Updated to angular 16 * Updated to angular 16 (partially) * Updated to angular 16 * Package update for Angular 16 (and other dependencies) is complete. * Replaced all takeUntil(this.onDestroy) with new takeUntilDestroyed() * Updated all inputs that have ! to be required and deleted all unit tests. * Corrected how takeUntilDestroyed() is supposed to be implemented. * Bump versions by dotnet-bump-version. * Pipeline adjustment for Angular 16 (#2008) * Bump versions by dotnet-bump-version. * Try a different build (#2009) * Bump versions by dotnet-bump-version. * Continue Reading Bugfix (#2010) * Fixed an edge case where continue point wasn't considering any chapters that had progress. Continue point is now slightly faster and uses less memory. * Added a unit test for a user's case. Still not reproducible * Bump versions by dotnet-bump-version. * Ensure chapters are sorted when getting continue point (#2011) Fixes new behaviour in #1625 * Bump versions by dotnet-bump-version. * Strip more forms of comments from CSS before parsing/inlining. (#2014) Handle if ExCSS throws an exception during inlining and attempt to fallback to scoping css instead of inlining. I still cannot update past ExCSS v4.1.0 else NPEs for common css will be thrown. * Bump versions by dotnet-bump-version. * Misc Changes (#2015) * Updated ng-bootstrap * Fixed an issue where jumpbar would be disabled when it shouldn't have been. * When there are duplicate files that make up a volume, show the count on series detail. * Added basic ISBN searching which will return a chapter back. * Bump versions by dotnet-bump-version. * Fixed count for cards (#2016) * Bump versions by dotnet-bump-version. * Last Release before Release Testing (#2017) * Attempting to invalidate JWT on login (when locked out), but can't figure a way to get a JWT, since we don't store them. Just committing as I'm going to remove the middleware, this is not worth the performance and complexity. * Removed some security stuff that didn't line up. * Dropping Token Expiration down to 2 days to test during release testing. * Bump versions by dotnet-bump-version. * Removed old migrations for Kavita startup. Only migrations from v0.7.2 onwards are present. (#2019) * Bump versions by dotnet-bump-version. * Fixed up jumpbar not properly disabling/enabling (#2022) * Bump versions by dotnet-bump-version. * Fix StoryArc & StoryArcNumber mismatch (#2018) * Ensure StoryArc and StoryArcNumber are max length * Trim StoryArc to remove excess spaces. * Replaced with cleaner approach. * Update with majora2007 recommendations * Bump versions by dotnet-bump-version. * Last fixes before release (#2027) * Disable login button when a login is in-progress. This will help prevent spamming when internet is slow. * Fixed a bug where an empty space could cause an error when creating a library. * Apply Split Options throughout the codebase to add extra safe-guard on empty spaces and ensure trimming. * Bump versions by dotnet-bump-version. * Added NoContent responses when APIs don't find entities (#2028) * Bump versions by dotnet-bump-version. * Few More Fixes (#2032) * Fixed spreads stretching on PC * Fixed a bug where reading list dates couldn't be cleared out. * Reading list page refreshes after updating info in the modal * Fixed an issue where create library wouldn't take into account advanced settings. * Fixed an issue where selection of the first chapter of a series to pull series-level metadata could fail in cases where you had Volume 2 and Chapter 1, Volume 2 would be selected. * Bump versions by dotnet-bump-version. * Fixed a bug where scan series wouldn't trigger word count analysis nor cover generation. (#2035) * Bump versions by dotnet-bump-version. * Okay this should be the last (#2037) * Fixed improper date visualization for reading list detail page. * Correct not-read badge position (#2034) --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Fixed a bug where reading list month wasn't rendering correctly (#2039) * Bump versions by dotnet-bump-version. * Version bump (#2040) * Bump versions by dotnet-bump-version. * Fixed bug in CI pipeline for main --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: Chris Plaatjes <kizaing@gmail.com> Co-authored-by: pssandhu <pssandhu@users.noreply.github.com> Co-authored-by: Jolyon Suthers <jolyon.suthers@gmail.com> Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * Reverted a scaling issue for fit to width * Fixed an issue where creating a new library wouldn't persist advanced options due to a conflict with default value. When deleting a library, give the library name in the prompt. * Fixed kbd tags in epubs with paper theme having a style conflict. * Fixed an edge case where the incorrect first cover could be chosen in some strange grouping situations. * Manually sort directories as some OSes don't return them in a natural sort order. * Fixed an issue where autocompleting when adding a directory could throw an error when you're typing. --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: Chris Plaatjes <kizaing@gmail.com> Co-authored-by: pssandhu <pssandhu@users.noreply.github.com> Co-authored-by: Jolyon Suthers <jolyon.suthers@gmail.com> * Bump versions by dotnet-bump-version. * Version Bump --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: Chris Plaatjes <kizaing@gmail.com> Co-authored-by: pssandhu <pssandhu@users.noreply.github.com> Co-authored-by: Jolyon Suthers <jolyon.suthers@gmail.com> Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * v0.7.4 - Kavita+ Launch (#2118) * Report Media Issues (#1964) * Started working on a report problems implementation. * Started code * Added logging to book and archive service. * Removed an additional ComicInfo read when comicinfo is null when trying to load. But we've already done it once earlier, so there really isn't any point. * Added basic implementation for media errors. * MediaErrors will ignore duplicate errors when there are multiple issues on same file in a scan. * Fixed unit tests * Basic code in place to view and clear. Just UI Cleanup needed. * Slight css upgrade * Fixed up centering and simplified the code to use regular array instead of observables as it wasn't working. * Fixed unit tests * Fixed unit tests for real * Bump versions by dotnet-bump-version. * Expanded Metadata for EPUBs (#1965) * Fixed a bug breaking ability to save server settings * Explicitly capture more people roles from Epubs, else fallback to how we do it now. It seems to be getting called twice and 2nd time is overriding data. Not sure why * Refactored the code to clean it up * Added support for generating collections or reading list based on dc:title and collection title-type with an optional display-seq. * ReadingList/Collection support can't be done until VersOne supports. https://github.com/vers-one/EpubReader/issues/81 * Double include author for epub parsing and let the People code handle removing duplicates. * Bump versions by dotnet-bump-version. * Nothing changed, this is just to retrigger a stable build. (#1967) (#1968) * Adding paper book reader theme (#1976) * Adding paper book reader theme # Added - Added: Paper book reader theme * Fixing some leftover styles * adding book emulation to 2column layout for paper style * Adding migrations * removing migration and compressing image * Reverting DataContextModelSnapshot * checking out datacontextmodelsnapshot file * Bump versions by dotnet-bump-version. * Web Links (#1983) * Updated dependencies * Updated the default key to be 256 bits to meet security requirements. * Added basic implementation of web link resolving favicon. Needs lots more work and testing on all OSes. * Implemented ability to see links and click on them for an individual chapter. * Hooked up the ability to set Series web links. * Render out the web link * Refactored out the favicon so there is a backup in case it fails. Refactored the baseline image placeholders to be dark mode since that is the default. * Added Robbie's nice error weblink fallbacks. * Bump versions by dotnet-bump-version. * Updated Docker entrypoint (#1984) * Bump versions by dotnet-bump-version. * ISBN Support (#1985) * Fixed a bug where weblinks would always show * Started to try and support ico -> png conversion by manually grabbing image data out, but it's hard as hell. * Implemented ability to parse out ISBN codes for books and ISBN-13 codes for ComicInfo. I can't figure out ISBN-10. * Fixed Favicon not working on anything but windows * Implemented ISBN support into Kavita * Don't round so much when transforming bytes * Bump versions by dotnet-bump-version. * AVIF Support & Much More! (#1992) * Expand the list of potential favicon icons to grab. * Added a url mapping functionality to use alternative urls for fetching icons * Initial commit to streamline media encoding. No DB migration yet, No UI changes, no Task changes. * Started refactoring code so that webp queries use encoding format instead. * More refactoring to remove hardcoded webp references. * Moved manual migrations to their own folder to keep things organized. Manually drop the obsolete webp keys. * Removed old apis for converting media and now have one. Reworked where the conversion code was located and streamlined events and whatnot. * Make favicon encode setting aware * Cleaned up favicon conversion * Updated format counter to now just use Extension from MangaFile now that it's been out a while. * Tweaked jumpbar code to reduce a lookup to hashmap. * Added AVIF (8-bit only) support. * In UpdatePeopleList, use FirstOrDefault as Single adds extra checks that may not be needed. * You can now remove weblinks from edit series page and you can leave empty cells, they will just be removed on backend. * Forgot a file * Don't prompt to write a review, just show the pencil. It's the same amount of clicks if you do, less if you dont. * Fixed Refresh token using wrong Claim to look up the user. * Refactored how we refresh authentication to perform it every 10 m ins to ensure we always stay authenticated. * Changed Version update code to run more throughout the day. Updated some hangfire to newer method signatures. * Bump versions by dotnet-bump-version. * More Fixes (#1993) * Strip just isbn: from epub isbns and log when it's back (books) * Tweaked to allow invalid GTINs but only valid ISBN 10/13s will be saved to Kavita. * Fixed a bug with parsing series from a filename that is just a chapter range and no chapter/volume keywords. * Show the media issue count before you open accordion * Added a inpage filter for Media issues * Cleanup styles * Fixed up some code in epub isbn parsing when it's null * Encode filenames when downloading so that non english characters can be passed properly to UI. * Added support to parse ComicInfo's with Empty Tags. * Reset development settings. * Tweaked the code in generating reading lists to avoid extra work when not needed. * Fix comicvine's favicon * Fixed up a unit test * Tweaked the favicon code to ignore icons that have query parameters * More favicon work. Expanded ability to grab icons a bit. Added in ability to not keep requesting favicons when we failed to parse already. * Added a note for later * Fixed stats server url * Added more debugging * Fixed unit tests * Bump versions by dotnet-bump-version. * More Fixes from Recent PRs (#1995) * Added extra debugging for logout issue * Fixed the null issue with ISBN * Allow web links to be cleared out * More logging on refresh token * More key fallback when building Table of Contents * Added better fallback implementation for building table of contents based on the many different ways epubs are packed and referenced. * Updated dependencies * Fixed up refresh token refresh which was invalidating sessions for no reason. Added it to update last active time as well. * Bump versions by dotnet-bump-version. * Fixed a bug with config (#1996) * Bump versions by dotnet-bump-version. * Changed IsDocker check (#1998) * Refactored IsDocker to be completely static and changed to use an environment variable instead. * Removed file from another branch * Bump versions by dotnet-bump-version. * Migrated up to VersOne 3.3 with epub 3.3 support. (#1999) This enables collection and reading list support from epubs. * Bump versions by dotnet-bump-version. * More Bugfixes (EPUB Mainly) (#2004) * Fixed an issue with downloading where spaces turned into plus signs. * If the refresh token is invalid, but the auth token still has life in it, don't invalidate. * Fixed docker users unable to save settings * Show a default error icon until favicon loads * Fixed a bug in mappings (keys/files) to pages that caused some links not to map appropriately. Updated epub-reader to v3.3.2. * Expanded Table of Content generation by also checking for any files that are named Navigation.xhtml to have Kavita generate a simple ToC from (instead of just TOC.xhtml) * Added another hack to massage key to page lookups when rewriting anchors. * Cleaned up debugging notes * Bump versions by dotnet-bump-version. * More Polish (#2005) * Implemented sort title extraction from epub 3 files. * Added link to wiki for media errors * Fixed the hack to reduce JWT refresh token expiration * Fixed up a case where favicon downloading wasn't correcting links that started with // correctly. Added a fallback for sites that just don't pngs available. * Implemented a mechanism to fallback to Kavita's website for favicons which can be dynamically added/updated by the community. * Reworked the logic for bookwalker which will fail to get the base html, so we have to rely on the fallback handler. * Bump versions by dotnet-bump-version. * Angular 16 (#2007) * Removed adv, which isn't needed. * Updated zone * Updated to angular 16 * Updated to angular 16 (partially) * Updated to angular 16 * Package update for Angular 16 (and other dependencies) is complete. * Replaced all takeUntil(this.onDestroy) with new takeUntilDestroyed() * Updated all inputs that have ! to be required and deleted all unit tests. * Corrected how takeUntilDestroyed() is supposed to be implemented. * Bump versions by dotnet-bump-version. * Pipeline adjustment for Angular 16 (#2008) * Bump versions by dotnet-bump-version. * Try a different build (#2009) * Bump versions by dotnet-bump-version. * Continue Reading Bugfix (#2010) * Fixed an edge case where continue point wasn't considering any chapters that had progress. Continue point is now slightly faster and uses less memory. * Added a unit test for a user's case. Still not reproducible * Bump versions by dotnet-bump-version. * Ensure chapters are sorted when getting continue point (#2011) Fixes new behaviour in #1625 * Bump versions by dotnet-bump-version. * Strip more forms of comments from CSS before parsing/inlining. (#2014) Handle if ExCSS throws an exception during inlining and attempt to fallback to scoping css instead of inlining. I still cannot update past ExCSS v4.1.0 else NPEs for common css will be thrown. * Bump versions by dotnet-bump-version. * Misc Changes (#2015) * Updated ng-bootstrap * Fixed an issue where jumpbar would be disabled when it shouldn't have been. * When there are duplicate files that make up a volume, show the count on series detail. * Added basic ISBN searching which will return a chapter back. * Bump versions by dotnet-bump-version. * Fixed count for cards (#2016) * Bump versions by dotnet-bump-version. * Last Release before Release Testing (#2017) * Attempting to invalidate JWT on login (when locked out), but can't figure a way to get a JWT, since we don't store them. Just committing as I'm going to remove the middleware, this is not worth the performance and complexity. * Removed some security stuff that didn't line up. * Dropping Token Expiration down to 2 days to test during release testing. * Bump versions by dotnet-bump-version. * Removed old migrations for Kavita startup. Only migrations from v0.7.2 onwards are present. (#2019) * Bump versions by dotnet-bump-version. * Fixed up jumpbar not properly disabling/enabling (#2022) * Bump versions by dotnet-bump-version. * Fix StoryArc & StoryArcNumber mismatch (#2018) * Ensure StoryArc and StoryArcNumber are max length * Trim StoryArc to remove excess spaces. * Replaced with cleaner approach. * Update with majora2007 recommendations * Bump versions by dotnet-bump-version. * Last fixes before release (#2027) * Disable login button when a login is in-progress. This will help prevent spamming when internet is slow. * Fixed a bug where an empty space could cause an error when creating a library. * Apply Split Options throughout the codebase to add extra safe-guard on empty spaces and ensure trimming. * Bump versions by dotnet-bump-version. * Added NoContent responses when APIs don't find entities (#2028) * Bump versions by dotnet-bump-version. * Few More Fixes (#2032) * Fixed spreads stretching on PC * Fixed a bug where reading list dates couldn't be cleared out. * Reading list page refreshes after updating info in the modal * Fixed an issue where create library wouldn't take into account advanced settings. * Fixed an issue where selection of the first chapter of a series to pull series-level metadata could fail in cases where you had Volume 2 and Chapter 1, Volume 2 would be selected. * Bump versions by dotnet-bump-version. * Fixed a bug where scan series wouldn't trigger word count analysis nor cover generation. (#2035) * Bump versions by dotnet-bump-version. * Okay this should be the last (#2037) * Fixed improper date visualization for reading list detail page. * Correct not-read badge position (#2034) --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Fixed a bug where reading list month wasn't rendering correctly (#2039) * Bump versions by dotnet-bump-version. * Version bump (#2040) * Bump versions by dotnet-bump-version. * Bugfixes for a hotfix (#2052) * Nothing changed, this is just to retrigger a stable build. (#1967) * v0.7.3 - The Quality of Life Update (#2036) * Version bump * Okay this should be the last (#2037) * Fixed improper date visualization for reading list detail page. * Correct not-read badge position (#2034) --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Merged develop in --------- Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> * v0.7.3 - The Quality of Life Update (#2041) * Report Media Issues (#1964) * Started working on a report problems implementation. * Started code * Added logging to book and archive service. * Removed an additional ComicInfo read when comicinfo is null when trying to load. But we've already done it once earlier, so there really isn't any point. * Added basic implementation for media errors. * MediaErrors will ignore duplicate errors when there are multiple issues on same file in a scan. * Fixed unit tests * Basic code in place to view and clear. Just UI Cleanup needed. * Slight css upgrade * Fixed up centering and simplified the code to use regular array instead of observables as it wasn't working. * Fixed unit tests * Fixed unit tests for real * Bump versions by dotnet-bump-version. * Expanded Metadata for EPUBs (#1965) * Fixed a bug breaking ability to save server settings * Explicitly capture more people roles from Epubs, else fallback to how we do it now. It seems to be getting called twice and 2nd time is overriding data. Not sure why * Refactored the code to clean it up * Added support for generating collections or reading list based on dc:title and collection title-type with an optional display-seq. * ReadingList/Collection support can't be done until VersOne supports. https://github.com/vers-one/EpubReader/issues/81 * Double include author for epub parsing and let the People code handle removing duplicates. * Bump versions by dotnet-bump-version. * Nothing changed, this is just to retrigger a stable build. (#1967) (#1968) * Adding paper book reader theme (#1976) * Adding paper book reader theme # Added - Added: Paper book reader theme * Fixing some leftover styles * adding book emulation to 2column layout for paper style * Adding migrations * removing migration and compressing image * Reverting DataContextModelSnapshot * checking out datacontextmodelsnapshot file * Bump versions by dotnet-bump-version. * Web Links (#1983) * Updated dependencies * Updated the default key to be 256 bits to meet security requirements. * Added basic implementation of web link resolving favicon. Needs lots more work and testing on all OSes. * Implemented ability to see links and click on them for an individual chapter. * Hooked up the ability to set Series web links. * Render out the web link * Refactored out the favicon so there is a backup in case it fails. Refactored the baseline image placeholders to be dark mode since that is the default. * Added Robbie's nice error weblink fallbacks. * Bump versions by dotnet-bump-version. * Updated Docker entrypoint (#1984) * Bump versions by dotnet-bump-version. * ISBN Support (#1985) * Fixed a bug where weblinks would always show * Started to try and support ico -> png conversion by manually grabbing image data out, but it's hard as hell. * Implemented ability to parse out ISBN codes for books and ISBN-13 codes for ComicInfo. I can't figure out ISBN-10. * Fixed Favicon not working on anything but windows * Implemented ISBN support into Kavita * Don't round so much when transforming bytes * Bump versions by dotnet-bump-version. * AVIF Support & Much More! (#1992) * Expand the list of potential favicon icons to grab. * Added a url mapping functionality to use alternative urls for fetching icons * Initial commit to streamline media encoding. No DB migration yet, No UI changes, no Task changes. * Started refactoring code so that webp queries use encoding format instead. * More refactoring to remove hardcoded webp references. * Moved manual migrations to their own folder to keep things organized. Manually drop the obsolete webp keys. * Removed old apis for converting media and now have one. Reworked where the conversion code was located and streamlined events and whatnot. * Make favicon encode setting aware * Cleaned up favicon conversion * Updated format counter to now just use Extension from MangaFile now that it's been out a while. * Tweaked jumpbar code to reduce a lookup to hashmap. * Added AVIF (8-bit only) support. * In UpdatePeopleList, use FirstOrDefault as Single adds extra checks that may not be needed. * You can now remove weblinks from edit series page and you can leave empty cells, they will just be removed on backend. * Forgot a file * Don't prompt to write a review, just show the pencil. It's the same amount of clicks if you do, less if you dont. * Fixed Refresh token using wrong Claim to look up the user. * Refactored how we refresh authentication to perform it every 10 m ins to ensure we always stay authenticated. * Changed Version update code to run more throughout the day. Updated some hangfire to newer method signatures. * Bump versions by dotnet-bump-version. * More Fixes (#1993) * Strip just isbn: from epub isbns and log when it's back (books) * Tweaked to allow invalid GTINs but only valid ISBN 10/13s will be saved to Kavita. * Fixed a bug with parsing series from a filename that is just a chapter range and no chapter/volume keywords. * Show the media issue count before you open accordion * Added a inpage filter for Media issues * Cleanup styles * Fixed up some code in epub isbn parsing when it's null * Encode filenames when downloading so that non english characters can be passed properly to UI. * Added support to parse ComicInfo's with Empty Tags. * Reset development settings. * Tweaked the code in generating reading lists to avoid extra work when not needed. * Fix comicvine's favicon * Fixed up a unit test * Tweaked the favicon code to ignore icons that have query parameters * More favicon work. Expanded ability to grab icons a bit. Added in ability to not keep requesting favicons when we failed to parse already. * Added a note for later * Fixed stats server url * Added more debugging * Fixed unit tests * Bump versions by dotnet-bump-version. * More Fixes from Recent PRs (#1995) * Added extra debugging for logout issue * Fixed the null issue with ISBN * Allow web links to be cleared out * More logging on refresh token * More key fallback when building Table of Contents * Added better fallback implementation for building table of contents based on the many different ways epubs are packed and referenced. * Updated dependencies * Fixed up refresh token refresh which was invalidating sessions for no reason. Added it to update last active time as well. * Bump versions by dotnet-bump-version. * Fixed a bug with config (#1996) * Bump versions by dotnet-bump-version. * Changed IsDocker check (#1998) * Refactored IsDocker to be completely static and changed to use an environment variable instead. * Removed file from another branch * Bump versions by dotnet-bump-version. * Migrated up to VersOne 3.3 with epub 3.3 support. (#1999) This enables collection and reading list support from epubs. * Bump versions by dotnet-bump-version. * More Bugfixes (EPUB Mainly) (#2004) * Fixed an issue with downloading where spaces turned into plus signs. * If the refresh token is invalid, but the auth token still has life in it, don't invalidate. * Fixed docker users unable to save settings * Show a default error icon until favicon loads * Fixed a bug in mappings (keys/files) to pages that caused some links not to map appropriately. Updated epub-reader to v3.3.2. * Expanded Table of Content generation by also checking for any files that are named Navigation.xhtml to have Kavita generate a simple ToC from (instead of just TOC.xhtml) * Added another hack to massage key to page lookups when rewriting anchors. * Cleaned up debugging notes * Bump versions by dotnet-bump-version. * More Polish (#2005) * Implemented sort title extraction from epub 3 files. * Added link to wiki for media errors * Fixed the hack to reduce JWT refresh token expiration * Fixed up a case where favicon downloading wasn't correcting links that started with // correctly. Added a fallback for sites that just don't pngs available. * Implemented a mechanism to fallback to Kavita's website for favicons which can be dynamically added/updated by the community. * Reworked the logic for bookwalker which will fail to get the base html, so we have to rely on the fallback handler. * Bump versions by dotnet-bump-version. * Angular 16 (#2007) * Removed adv, which isn't needed. * Updated zone * Updated to angular 16 * Updated to angular 16 (partially) * Updated to angular 16 * Package update for Angular 16 (and other dependencies) is complete. * Replaced all takeUntil(this.onDestroy) with new takeUntilDestroyed() * Updated all inputs that have ! to be required and deleted all unit tests. * Corrected how takeUntilDestroyed() is supposed to be implemented. * Bump versions by dotnet-bump-version. * Pipeline adjustment for Angular 16 (#2008) * Bump versions by dotnet-bump-version. * Try a different build (#2009) * Bump versions by dotnet-bump-version. * Continue Reading Bugfix (#2010) * Fixed an edge case where continue point wasn't considering any chapters that had progress. Continue point is now slightly faster and uses less memory. * Added a unit test for a user's case. Still not reproducible * Bump versions by dotnet-bump-version. * Ensure chapters are sorted when getting continue point (#2011) Fixes new behaviour in #1625 * Bump versions by dotnet-bump-version. * Strip more forms of comments from CSS b… --------- Co-authored-by: Robbie Davis <robbie@therobbiedavis.com> Co-authored-by: Chris Plaatjes <kizaing@gmail.com> Co-authored-by: pssandhu <pssandhu@users.noreply.github.com> Co-authored-by: Jolyon Suthers <jolyon.suthers@gmail.com> Co-authored-by: Andre Smith <Hobogrammer@users.noreply.github.com> Co-authored-by: JShiesty <102483672+JShiesty-dev@users.noreply.github.com>
This commit is contained in:
parent
1035e911bb
commit
0b52c5b05f
102 changed files with 6898 additions and 1472 deletions
2
.github/workflows/build-and-test.yml
vendored
2
.github/workflows/build-and-test.yml
vendored
|
@ -268,7 +268,7 @@ jobs:
|
|||
|
||||
cd UI/Web || exit
|
||||
echo 'Installing web dependencies'
|
||||
npm install --legacy-peer-deps
|
||||
npm ci
|
||||
|
||||
echo 'Building UI'
|
||||
npm run prod
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.5" />
|
||||
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.5" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.6" />
|
||||
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.6" />
|
||||
<PackageReference Include="NSubstitute" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.9" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="NSubstitute" Version="5.0.0" />
|
||||
|
|
|
@ -381,6 +381,49 @@ public class ReaderServiceTests
|
|||
Assert.Equal("2", actualChapter.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldGetNextVolume_OnlyFloats()
|
||||
{
|
||||
// V1 -> V2
|
||||
await ResetDb();
|
||||
|
||||
var series = new SeriesBuilder("Test")
|
||||
.WithVolume(new VolumeBuilder("1.0")
|
||||
.WithChapter(new ChapterBuilder("1").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2.1")
|
||||
.WithChapter(new ChapterBuilder("21").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("2.2")
|
||||
.WithChapter(new ChapterBuilder("31").Build())
|
||||
.Build())
|
||||
|
||||
.WithVolume(new VolumeBuilder("3.1")
|
||||
.WithChapter(new ChapterBuilder("31").Build())
|
||||
.Build())
|
||||
|
||||
|
||||
.Build();
|
||||
series.Library = new LibraryBuilder("Test LIb", LibraryType.Manga).Build();
|
||||
|
||||
_context.Series.Add(series);
|
||||
|
||||
_context.AppUser.Add(new AppUser()
|
||||
{
|
||||
UserName = "majora2007"
|
||||
});
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
|
||||
var nextChapter = await _readerService.GetNextChapterIdAsync(1, 2, 2, 1);
|
||||
var actualChapter = await _unitOfWork.ChapterRepository.GetChapterAsync(nextChapter);
|
||||
Assert.Equal("31", actualChapter.Range);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetNextChapterIdAsync_ShouldRollIntoNextVolume()
|
||||
{
|
||||
|
|
|
@ -66,23 +66,23 @@
|
|||
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
|
||||
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.3.4" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.49" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.50" />
|
||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.9" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.8">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
|
||||
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />
|
||||
<PackageReference Include="Nager.ArticleNumber" Version="1.0.7" />
|
||||
<PackageReference Include="NetVips" Version="2.3.1" />
|
||||
<PackageReference Include="NetVips.Native" Version="8.14.2" />
|
||||
<PackageReference Include="NetVips.Native" Version="8.14.3" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.1.6" />
|
||||
<PackageReference Include="Serilog" Version="3.0.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
|
||||
|
@ -100,8 +100,8 @@
|
|||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.6" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.31.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.8" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.32.0" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="19.2.29" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||
<PackageReference Include="VersOne.Epub" Version="3.3.1" />
|
||||
|
|
|
@ -158,5 +158,4 @@ public class BookController : BaseApiController
|
|||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
|||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.DTOs;
|
||||
using API.Extensions;
|
||||
using API.Services.Plus;
|
||||
using EasyCaching.Core;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -69,7 +70,7 @@ public class RatingController : BaseApiController
|
|||
return Ok(new RatingDto()
|
||||
{
|
||||
Provider = ScrobbleProvider.Kavita,
|
||||
AverageScore = await _unitOfWork.SeriesRepository.GetAverageUserRating(seriesId),
|
||||
AverageScore = await _unitOfWork.SeriesRepository.GetAverageUserRating(seriesId, User.GetUserId()),
|
||||
FavoriteCount = 0
|
||||
});
|
||||
}
|
||||
|
|
|
@ -792,4 +792,59 @@ public class ReaderController : BaseApiController
|
|||
return _readerService.GetTimeEstimate(0, pagesLeft, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the user's personal table of contents for the given chapter
|
||||
/// </summary>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("ptoc")]
|
||||
public ActionResult<IEnumerable<PersonalToCDto>> GetPersonalToC(int chapterId)
|
||||
{
|
||||
return Ok(_unitOfWork.UserTableOfContentRepository.GetPersonalToC(User.GetUserId(), chapterId));
|
||||
}
|
||||
|
||||
[HttpDelete("ptoc")]
|
||||
public async Task<ActionResult> DeletePersonalToc([FromQuery] int chapterId, [FromQuery] int pageNum, [FromQuery] string title)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title)) return BadRequest("Name cannot be empty");
|
||||
if (pageNum < 0) return BadRequest("Must be valid page number");
|
||||
var toc = await _unitOfWork.UserTableOfContentRepository.Get(User.GetUserId(), chapterId, pageNum, title);
|
||||
if (toc == null) return Ok();
|
||||
_unitOfWork.UserTableOfContentRepository.Remove(toc);
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new personal table of content entry for a given chapter
|
||||
/// </summary>
|
||||
/// <remarks>The title and page number must be unique to that book</remarks>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("create-ptoc")]
|
||||
public async Task<ActionResult> CreatePersonalToC(CreatePersonalToCDto dto)
|
||||
{
|
||||
// Validate there isn't already an existing page title combo?
|
||||
if (string.IsNullOrWhiteSpace(dto.Title)) return BadRequest("Name cannot be empty");
|
||||
if (dto.PageNumber < 0) return BadRequest("Must be valid page number");
|
||||
var userId = User.GetUserId();
|
||||
if (await _unitOfWork.UserTableOfContentRepository.IsUnique(userId, dto.ChapterId, dto.PageNumber,
|
||||
dto.Title.Trim()))
|
||||
{
|
||||
return BadRequest("Duplicate ToC entry already exists");
|
||||
}
|
||||
|
||||
_unitOfWork.UserTableOfContentRepository.Attach(new AppUserTableOfContent()
|
||||
{
|
||||
Title = dto.Title.Trim(),
|
||||
ChapterId = dto.ChapterId,
|
||||
PageNumber = dto.PageNumber,
|
||||
SeriesId = dto.SeriesId,
|
||||
LibraryId = dto.LibraryId,
|
||||
BookScrollId = dto.BookScrollId,
|
||||
AppUserId = userId
|
||||
});
|
||||
await _unitOfWork.CommitAsync();
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,4 +7,5 @@ public class RatingDto
|
|||
public int AverageScore { get; set; }
|
||||
public int FavoriteCount { get; set; }
|
||||
public ScrobbleProvider Provider { get; set; }
|
||||
public string? ProviderUrl { get; set; }
|
||||
}
|
||||
|
|
12
API/DTOs/Reader/CreatePersonalToCDto.cs
Normal file
12
API/DTOs/Reader/CreatePersonalToCDto.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
namespace API.DTOs.Reader;
|
||||
|
||||
public class CreatePersonalToCDto
|
||||
{
|
||||
public required int ChapterId { get; set; }
|
||||
public required int VolumeId { get; set; }
|
||||
public required int SeriesId { get; set; }
|
||||
public required int LibraryId { get; set; }
|
||||
public required int PageNumber { get; set; }
|
||||
public required string Title { get; set; }
|
||||
public string? BookScrollId { get; set; }
|
||||
}
|
9
API/DTOs/Reader/PersonalToCDto.cs
Normal file
9
API/DTOs/Reader/PersonalToCDto.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace API.DTOs.Reader;
|
||||
|
||||
public class PersonalToCDto
|
||||
{
|
||||
public required int ChapterId { get; set; }
|
||||
public required int PageNumber { get; set; }
|
||||
public required string Title { get; set; }
|
||||
public string? BookScrollId { get; set; }
|
||||
}
|
|
@ -66,8 +66,16 @@ public class ScrobbleDto
|
|||
/// </summary>
|
||||
public DateTime? StartedReadingDateUtc { get; set; }
|
||||
/// <summary>
|
||||
/// The latest date the series was read. Will be null for non ReadingProgress events
|
||||
/// </summary>
|
||||
public DateTime? LatestReadingDateUtc { get; set; }
|
||||
/// <summary>
|
||||
/// The date that the series was scrobbled. Will be null for non ReadingProgress events
|
||||
/// </summary>
|
||||
public DateTime? ScrobbleDateUtc { get; set; }
|
||||
/// <summary>
|
||||
/// Optional but can help with matching
|
||||
/// </summary>
|
||||
public string? Isbn { get; set; }
|
||||
|
||||
}
|
||||
|
|
|
@ -10,9 +10,9 @@ public class ScrobbleEventDto
|
|||
public bool IsProcessed { get; set; }
|
||||
public int? VolumeNumber { get; set; }
|
||||
public int? ChapterNumber { get; set; }
|
||||
public DateTime? ProcessDateUtc { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public float? Rating { get; set; }
|
||||
public ScrobbleEventType ScrobbleEventType { get; set; }
|
||||
|
||||
}
|
||||
|
|
|
@ -30,9 +30,13 @@ public class SeriesDto : IHasReadTimeEstimate
|
|||
/// <summary>
|
||||
/// Rating from logged in user. Calculated at API-time.
|
||||
/// </summary>
|
||||
public int UserRating { get; set; }
|
||||
public MangaFormat Format { get; set; }
|
||||
public float UserRating { get; set; }
|
||||
/// <summary>
|
||||
/// If the user has set the rating or not
|
||||
/// </summary>
|
||||
public bool HasUserRated { get; set; }
|
||||
|
||||
public MangaFormat Format { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
|
||||
public bool NameLocked { get; set; }
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.DTOs;
|
||||
namespace API.DTOs;
|
||||
|
||||
public class UpdateSeriesRatingDto
|
||||
{
|
||||
public int SeriesId { get; init; }
|
||||
public int UserRating { get; init; }
|
||||
public float UserRating { get; init; }
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||
public DbSet<ScrobbleError> ScrobbleError { get; set; } = null!;
|
||||
public DbSet<ScrobbleHold> ScrobbleHold { get; set; } = null!;
|
||||
public DbSet<AppUserOnDeckRemoval> AppUserOnDeckRemoval { get; set; } = null!;
|
||||
public DbSet<AppUserTableOfContent> AppUserTableOfContent { get; set; } = null!;
|
||||
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
|
|
32
API/Data/ManualMigrations/MigrateExistingRatings.cs
Normal file
32
API/Data/ManualMigrations/MigrateExistingRatings.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// Introduced in v0.7.5.6 and v0.7.6, Ratings > 0 need to have "HasRatingSet"
|
||||
/// </summary>
|
||||
/// <remarks>Added in v0.7.5.6</remarks>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class MigrateExistingRatings
|
||||
{
|
||||
public static async Task Migrate(DataContext context, ILogger<Program> logger)
|
||||
{
|
||||
logger.LogCritical("Running MigrateExistingRatings migration - Please be patient, this may take some time. This is not an error");
|
||||
|
||||
foreach (var r in context.AppUserRating.Where(r => r.Rating > 0f))
|
||||
{
|
||||
r.HasBeenRated = true;
|
||||
context.Entry(r).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
if (context.ChangeTracker.HasChanges())
|
||||
{
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
logger.LogCritical("Running MigrateExistingRatings migration - Completed. This is not an error");
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities.Enums;
|
||||
using CsvHelper;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
internal sealed class SeriesRelationMigrationOutput
|
||||
{
|
||||
public required string SeriesName { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
public required string TargetSeriesName { get; set; }
|
||||
public int TargetId { get; set; }
|
||||
public RelationKind Relationship { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Introduced in v0.6.1.2 and v0.7, this exports to a temp file the existing series relationships. It is a 3 part migration.
|
||||
/// This will run first, to export the data, then the DB migration will change the way the DB is constructed, then the last migration
|
||||
/// will import said file and re-construct the relationships.
|
||||
/// </summary>
|
||||
public static class MigrateSeriesRelationsExport
|
||||
{
|
||||
private const string OutputFile = "config/relations.csv";
|
||||
private const string CompleteOutputFile = "config/relations-imported.csv";
|
||||
public static async Task Migrate(DataContext dataContext, ILogger<Program> logger)
|
||||
{
|
||||
logger.LogCritical("Running MigrateSeriesRelationsExport migration - Please be patient, this may take some time. This is not an error");
|
||||
if (BuildInfo.Version > new Version(0, 6, 1, 3)
|
||||
|| new FileInfo(OutputFile).Exists
|
||||
|| new FileInfo(CompleteOutputFile).Exists)
|
||||
{
|
||||
logger.LogCritical("Running MigrateSeriesRelationsExport migration - complete. Nothing to do");
|
||||
return;
|
||||
}
|
||||
|
||||
var seriesWithRelationships = await dataContext.Series
|
||||
.Where(s => s.Relations.Any())
|
||||
.Include(s => s.Relations)
|
||||
.ThenInclude(r => r.TargetSeries)
|
||||
.ToListAsync();
|
||||
|
||||
var records = new List<SeriesRelationMigrationOutput>();
|
||||
var excludedRelationships = new List<RelationKind>()
|
||||
{
|
||||
RelationKind.Parent,
|
||||
};
|
||||
foreach (var series in seriesWithRelationships)
|
||||
{
|
||||
foreach (var relationship in series.Relations.Where(r => !excludedRelationships.Contains(r.RelationKind)))
|
||||
{
|
||||
records.Add(new SeriesRelationMigrationOutput()
|
||||
{
|
||||
SeriesId = series.Id,
|
||||
SeriesName = series.Name,
|
||||
Relationship = relationship.RelationKind,
|
||||
TargetId = relationship.TargetSeriesId,
|
||||
TargetSeriesName = relationship.TargetSeries.Name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await using var writer = new StreamWriter(OutputFile);
|
||||
await using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
await csv.WriteRecordsAsync(records);
|
||||
}
|
||||
|
||||
await writer.DisposeAsync();
|
||||
|
||||
logger.LogCritical("{OutputFile} has a backup of all data", OutputFile);
|
||||
|
||||
logger.LogCritical("Deleting all relationships in the DB. This is not an error");
|
||||
var entities = await dataContext.SeriesRelation
|
||||
.Include(s => s.Series)
|
||||
.Include(s => s.TargetSeries)
|
||||
.Select(s => s)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var seriesWithRelationship in entities)
|
||||
{
|
||||
logger.LogCritical("Deleting {SeriesName} --{RelationshipKind}--> {TargetSeriesName}",
|
||||
seriesWithRelationship.Series.Name, seriesWithRelationship.RelationKind, seriesWithRelationship.TargetSeries.Name);
|
||||
dataContext.SeriesRelation.Remove(seriesWithRelationship);
|
||||
|
||||
await dataContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// In case of corrupted entities (where series were deleted but their Id still existed, we delete the rest of the table)
|
||||
dataContext.SeriesRelation.RemoveRange(dataContext.SeriesRelation);
|
||||
await dataContext.SaveChangesAsync();
|
||||
|
||||
|
||||
logger.LogCritical("Running MigrateSeriesRelationsExport migration - Completed. This is not an error");
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities.Metadata;
|
||||
using CsvHelper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// Introduced in v0.6.1.2 and v0.7, this imports to a temp file the existing series relationships. It is a 3 part migration.
|
||||
/// This will run last, to import the data and re-construct the relationships.
|
||||
/// </summary>
|
||||
public static class MigrateSeriesRelationsImport
|
||||
{
|
||||
private const string OutputFile = "config/relations.csv";
|
||||
private const string CompleteOutputFile = "config/relations-imported.csv";
|
||||
public static async Task Migrate(DataContext dataContext, ILogger<Program> logger)
|
||||
{
|
||||
logger.LogCritical("Running MigrateSeriesRelationsImport migration - Please be patient, this may take some time. This is not an error");
|
||||
if (!new FileInfo(OutputFile).Exists)
|
||||
{
|
||||
logger.LogCritical("Running MigrateSeriesRelationsImport migration - complete. Nothing to do");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogCritical("Loading backed up relationships into the DB");
|
||||
List<SeriesRelationMigrationOutput> records;
|
||||
using var reader = new StreamReader(OutputFile);
|
||||
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
|
||||
{
|
||||
records = csv.GetRecords<SeriesRelationMigrationOutput>().ToList();
|
||||
}
|
||||
|
||||
foreach (var relation in records)
|
||||
{
|
||||
logger.LogCritical("Importing {SeriesName} --{RelationshipKind}--> {TargetSeriesName}",
|
||||
relation.SeriesName, relation.Relationship, relation.TargetSeriesName);
|
||||
|
||||
// Filter out series that don't exist
|
||||
if (!await dataContext.Series.AnyAsync(s => s.Id == relation.SeriesId) ||
|
||||
!await dataContext.Series.AnyAsync(s => s.Id == relation.TargetId))
|
||||
continue;
|
||||
|
||||
await dataContext.SeriesRelation.AddAsync(new SeriesRelation()
|
||||
{
|
||||
SeriesId = relation.SeriesId,
|
||||
TargetSeriesId = relation.TargetId,
|
||||
RelationKind = relation.Relationship
|
||||
});
|
||||
|
||||
}
|
||||
await dataContext.SaveChangesAsync();
|
||||
|
||||
File.Move(OutputFile, CompleteOutputFile);
|
||||
|
||||
logger.LogCritical("Running MigrateSeriesRelationsImport migration - Completed. This is not an error");
|
||||
}
|
||||
}
|
2266
API/Data/Migrations/20230719173458_PersonalToC.Designer.cs
generated
Normal file
2266
API/Data/Migrations/20230719173458_PersonalToC.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
79
API/Data/Migrations/20230719173458_PersonalToC.cs
Normal file
79
API/Data/Migrations/20230719173458_PersonalToC.cs
Normal file
|
@ -0,0 +1,79 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class PersonalToC : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AppUserTableOfContent",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
PageNumber = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Title = table.Column<string>(type: "TEXT", nullable: true),
|
||||
SeriesId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ChapterId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
VolumeId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
LibraryId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
BookScrollId = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
CreatedUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
LastModified = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
LastModifiedUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
AppUserId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AppUserTableOfContent", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AppUserTableOfContent_AspNetUsers_AppUserId",
|
||||
column: x => x.AppUserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AppUserTableOfContent_Chapter_ChapterId",
|
||||
column: x => x.ChapterId,
|
||||
principalTable: "Chapter",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AppUserTableOfContent_Series_SeriesId",
|
||||
column: x => x.SeriesId,
|
||||
principalTable: "Series",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AppUserTableOfContent_AppUserId",
|
||||
table: "AppUserTableOfContent",
|
||||
column: "AppUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AppUserTableOfContent_ChapterId",
|
||||
table: "AppUserTableOfContent",
|
||||
column: "ChapterId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AppUserTableOfContent_SeriesId",
|
||||
table: "AppUserTableOfContent",
|
||||
column: "SeriesId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AppUserTableOfContent");
|
||||
}
|
||||
}
|
||||
}
|
2269
API/Data/Migrations/20230725133536_ChangeRatingScale.Designer.cs
generated
Normal file
2269
API/Data/Migrations/20230725133536_ChangeRatingScale.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
45
API/Data/Migrations/20230725133536_ChangeRatingScale.cs
Normal file
45
API/Data/Migrations/20230725133536_ChangeRatingScale.cs
Normal file
|
@ -0,0 +1,45 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ChangeRatingScale : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating",
|
||||
table: "AppUserRating",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER");
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "HasBeenRated",
|
||||
table: "AppUserRating",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "HasBeenRated",
|
||||
table: "AppUserRating");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "Rating",
|
||||
table: "AppUserRating",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ namespace API.Data.Migrations
|
|||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.8");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.9");
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppRole", b =>
|
||||
{
|
||||
|
@ -371,9 +371,12 @@ namespace API.Data.Migrations
|
|||
b.Property<int>("AppUserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Rating")
|
||||
b.Property<bool>("HasBeenRated")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float>("Rating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("Review")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
@ -407,6 +410,59 @@ namespace API.Data.Migrations
|
|||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserTableOfContent", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AppUserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("BookScrollId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ChapterId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastModified")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastModifiedUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("LibraryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("PageNumber")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("VolumeId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AppUserId");
|
||||
|
||||
b.HasIndex("ChapterId");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("AppUserTableOfContent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
@ -1746,6 +1802,33 @@ namespace API.Data.Migrations
|
|||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.AppUserTableOfContent", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.AppUser", "AppUser")
|
||||
.WithMany("TableOfContents")
|
||||
.HasForeignKey("AppUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Chapter", "Chapter")
|
||||
.WithMany()
|
||||
.HasForeignKey("ChapterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("API.Entities.Series", "Series")
|
||||
.WithMany()
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AppUser");
|
||||
|
||||
b.Navigation("Chapter");
|
||||
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("API.Entities.Chapter", b =>
|
||||
{
|
||||
b.HasOne("API.Entities.Volume", "Volume")
|
||||
|
@ -2130,6 +2213,8 @@ namespace API.Data.Migrations
|
|||
|
||||
b.Navigation("ScrobbleHolds");
|
||||
|
||||
b.Navigation("TableOfContents");
|
||||
|
||||
b.Navigation("UserPreferences");
|
||||
|
||||
b.Navigation("UserRoles");
|
||||
|
|
|
@ -6,6 +6,7 @@ using API.Data.ManualMigrations;
|
|||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -31,6 +32,8 @@ public interface IAppUserProgressRepository
|
|||
Task<bool> AnyUserProgressForSeriesAsync(int seriesId, int userId);
|
||||
Task<int> GetHighestFullyReadChapterForSeries(int seriesId, int userId);
|
||||
Task<int> GetHighestFullyReadVolumeForSeries(int seriesId, int userId);
|
||||
Task<DateTime?> GetLatestProgressForSeries(int seriesId, int userId);
|
||||
Task<DateTime?> GetFirstProgressForSeries(int seriesId, int userId);
|
||||
}
|
||||
#nullable disable
|
||||
public class AppUserProgressRepository : IAppUserProgressRepository
|
||||
|
@ -162,9 +165,9 @@ public class AppUserProgressRepository : IAppUserProgressRepository
|
|||
(appUserProgresses, chapter) => new {appUserProgresses, chapter})
|
||||
.Where(p => p.appUserProgresses.SeriesId == seriesId && p.appUserProgresses.AppUserId == userId &&
|
||||
p.appUserProgresses.PagesRead >= p.chapter.Pages)
|
||||
.Select(p => p.chapter.Number)
|
||||
.Select(p => p.chapter.Range)
|
||||
.ToListAsync();
|
||||
return list.Count == 0 ? 0 : list.DefaultIfEmpty().Where(d => d != null).Max(d => (int) Math.Floor(float.Parse(d)));
|
||||
return list.Count == 0 ? 0 : list.DefaultIfEmpty().Where(d => d != null).Max(d => (int) Math.Floor(Parser.MaxNumberFromRange(d)));
|
||||
}
|
||||
|
||||
public async Task<int> GetHighestFullyReadVolumeForSeries(int seriesId, int userId)
|
||||
|
@ -179,7 +182,23 @@ public class AppUserProgressRepository : IAppUserProgressRepository
|
|||
return list.Count == 0 ? 0 : list.DefaultIfEmpty().Max();
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
public async Task<DateTime?> GetLatestProgressForSeries(int seriesId, int userId)
|
||||
{
|
||||
var list = await _context.AppUserProgresses.Where(p => p.AppUserId == userId && p.SeriesId == seriesId)
|
||||
.Select(p => p.LastModifiedUtc)
|
||||
.ToListAsync();
|
||||
return list.Count == 0 ? null : list.DefaultIfEmpty().Max();
|
||||
}
|
||||
|
||||
public async Task<DateTime?> GetFirstProgressForSeries(int seriesId, int userId)
|
||||
{
|
||||
var list = await _context.AppUserProgresses.Where(p => p.AppUserId == userId && p.SeriesId == seriesId)
|
||||
.Select(p => p.LastModifiedUtc)
|
||||
.ToListAsync();
|
||||
return list.Count == 0 ? null : list.DefaultIfEmpty().Min();
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
public async Task<AppUserProgress?> GetUserProgressAsync(int chapterId, int userId)
|
||||
{
|
||||
return await _context.AppUserProgresses
|
||||
|
|
|
@ -137,7 +137,7 @@ public interface ISeriesRepository
|
|||
Task<IList<SeriesMetadataDto>> GetSeriesMetadataForIds(IEnumerable<int> seriesIds);
|
||||
Task<IList<Series>> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat, bool customOnly = true);
|
||||
Task<SeriesDto?> GetSeriesDtoByNamesAndMetadataIdsForUser(int userId, IEnumerable<string> names, LibraryType libraryType, string aniListUrl, string malUrl);
|
||||
Task<int> GetAverageUserRating(int seriesId);
|
||||
Task<int> GetAverageUserRating(int seriesId, int userId);
|
||||
Task RemoveFromOnDeck(int seriesId, int userId);
|
||||
Task ClearOnDeckRemoval(int seriesId, int userId);
|
||||
}
|
||||
|
@ -625,6 +625,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
if (rating != null)
|
||||
{
|
||||
s.UserRating = rating.Rating;
|
||||
s.HasUserRated = rating.HasBeenRated;
|
||||
}
|
||||
|
||||
if (userProgress.Count > 0)
|
||||
|
@ -1682,12 +1683,19 @@ public class SeriesRepository : ISeriesRepository
|
|||
/// Returns the Average rating for all users within Kavita instance
|
||||
/// </summary>
|
||||
/// <param name="seriesId"></param>
|
||||
public async Task<int> GetAverageUserRating(int seriesId)
|
||||
public async Task<int> GetAverageUserRating(int seriesId, int userId)
|
||||
{
|
||||
// If there is 0 or 1 rating and that rating is you, return 0 back
|
||||
var countOfRatingsThatAreUser = await _context.AppUserRating
|
||||
.Where(r => r.SeriesId == seriesId && r.HasBeenRated).CountAsync(u => u.AppUserId == userId);
|
||||
if (countOfRatingsThatAreUser == 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
var avg = (await _context.AppUserRating
|
||||
.Where(r => r.SeriesId == seriesId)
|
||||
.Where(r => r.SeriesId == seriesId && r.HasBeenRated)
|
||||
.AverageAsync(r => (int?) r.Rating));
|
||||
return avg.HasValue ? (int) avg.Value : 0;
|
||||
return avg.HasValue ? (int) (avg.Value * 20) : 0;
|
||||
}
|
||||
|
||||
public async Task RemoveFromOnDeck(int seriesId, int userId)
|
||||
|
@ -1707,7 +1715,7 @@ public class SeriesRepository : ISeriesRepository
|
|||
public async Task ClearOnDeckRemoval(int seriesId, int userId)
|
||||
{
|
||||
var existingEntry = await _context.AppUserOnDeckRemoval
|
||||
.Where(u => u.Id == userId && u.SeriesId == seriesId)
|
||||
.Where(u => u.AppUserId == userId && u.SeriesId == seriesId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (existingEntry == null) return;
|
||||
_context.AppUserOnDeckRemoval.Remove(existingEntry);
|
||||
|
|
64
API/Data/Repositories/UserTableOfContentRepository.cs
Normal file
64
API/Data/Repositories/UserTableOfContentRepository.cs
Normal file
|
@ -0,0 +1,64 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace API.Data.Repositories;
|
||||
#nullable enable
|
||||
|
||||
public interface IUserTableOfContentRepository
|
||||
{
|
||||
void Attach(AppUserTableOfContent toc);
|
||||
void Remove(AppUserTableOfContent toc);
|
||||
Task<bool> IsUnique(int userId, int chapterId, int page, string title);
|
||||
IEnumerable<PersonalToCDto> GetPersonalToC(int userId, int chapterId);
|
||||
Task<AppUserTableOfContent?> Get(int userId, int chapterId, int pageNum, string title);
|
||||
}
|
||||
|
||||
public class UserTableOfContentRepository : IUserTableOfContentRepository
|
||||
{
|
||||
private readonly DataContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public UserTableOfContentRepository(DataContext context, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public void Attach(AppUserTableOfContent toc)
|
||||
{
|
||||
_context.AppUserTableOfContent.Attach(toc);
|
||||
}
|
||||
|
||||
public void Remove(AppUserTableOfContent toc)
|
||||
{
|
||||
_context.AppUserTableOfContent.Remove(toc);
|
||||
}
|
||||
|
||||
public async Task<bool> IsUnique(int userId, int chapterId, int page, string title)
|
||||
{
|
||||
return await _context.AppUserTableOfContent.AnyAsync(t =>
|
||||
t.AppUserId == userId && t.PageNumber == page && t.Title == title && t.ChapterId == chapterId);
|
||||
}
|
||||
|
||||
public IEnumerable<PersonalToCDto> GetPersonalToC(int userId, int chapterId)
|
||||
{
|
||||
return _context.AppUserTableOfContent
|
||||
.Where(t => t.AppUserId == userId && t.ChapterId == chapterId)
|
||||
.ProjectTo<PersonalToCDto>(_mapper.ConfigurationProvider)
|
||||
.OrderBy(t => t.PageNumber)
|
||||
.AsEnumerable();
|
||||
}
|
||||
|
||||
public async Task<AppUserTableOfContent?> Get(int userId,int chapterId, int pageNum, string title)
|
||||
{
|
||||
return await _context.AppUserTableOfContent
|
||||
.Where(t => t.AppUserId == userId && t.ChapterId == chapterId && t.PageNumber == pageNum && t.Title == title)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
|
@ -27,6 +27,7 @@ public interface IUnitOfWork
|
|||
IDeviceRepository DeviceRepository { get; }
|
||||
IMediaErrorRepository MediaErrorRepository { get; }
|
||||
IScrobbleRepository ScrobbleRepository { get; }
|
||||
IUserTableOfContentRepository UserTableOfContentRepository { get; }
|
||||
bool Commit();
|
||||
Task<bool> CommitAsync();
|
||||
bool HasChanges();
|
||||
|
@ -66,6 +67,7 @@ public class UnitOfWork : IUnitOfWork
|
|||
public IDeviceRepository DeviceRepository => new DeviceRepository(_context, _mapper);
|
||||
public IMediaErrorRepository MediaErrorRepository => new MediaErrorRepository(_context, _mapper);
|
||||
public IScrobbleRepository ScrobbleRepository => new ScrobbleRepository(_context, _mapper);
|
||||
public IUserTableOfContentRepository UserTableOfContentRepository => new UserTableOfContentRepository(_context, _mapper);
|
||||
|
||||
/// <summary>
|
||||
/// Commits changes to the DB. Completes the open transaction.
|
||||
|
|
|
@ -37,9 +37,9 @@ public class AppUser : IdentityUser<int>, IHasConcurrencyToken
|
|||
/// </summary>
|
||||
public ICollection<Device> Devices { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// A list of Series the user doesn't want on deck
|
||||
/// A list of Table of Contents for a given Chapter
|
||||
/// </summary>
|
||||
//public ICollection<Series> OnDeckRemovals { get; set; } = null!;
|
||||
public ICollection<AppUserTableOfContent> TableOfContents { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// An API Key to interact with external services, like OPDS
|
||||
/// </summary>
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
|
||||
namespace API.Entities;
|
||||
|
||||
#nullable enable
|
||||
public class AppUserRating
|
||||
{
|
||||
public int Id { get; set; }
|
||||
/// <summary>
|
||||
/// A number between 0-5 that represents how good a series is.
|
||||
/// A number between 0-5.0 that represents how good a series is.
|
||||
/// </summary>
|
||||
public int Rating { get; set; }
|
||||
public float Rating { get; set; }
|
||||
/// <summary>
|
||||
/// If the rating has been explicitly set. Otherwise the 0.0 rating should be ignored as it's not rated
|
||||
/// </summary>
|
||||
public bool HasBeenRated { get; set; }
|
||||
/// <summary>
|
||||
/// A short summary the user can write when giving their review.
|
||||
/// </summary>
|
||||
|
@ -17,7 +21,7 @@ public class AppUserRating
|
|||
/// </summary>
|
||||
public string? Tagline { get; set; }
|
||||
public int SeriesId { get; set; }
|
||||
public Series Series { get; set; }
|
||||
public Series Series { get; set; } = null!;
|
||||
|
||||
|
||||
// Relationships
|
||||
|
|
49
API/Entities/AppUserTableOfContent.cs
Normal file
49
API/Entities/AppUserTableOfContent.cs
Normal file
|
@ -0,0 +1,49 @@
|
|||
using System;
|
||||
using API.Entities.Interfaces;
|
||||
|
||||
namespace API.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A personal table of contents for a given user linked with a given book
|
||||
/// </summary>
|
||||
public class AppUserTableOfContent : IEntityDate
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The page to bookmark
|
||||
/// </summary>
|
||||
public required int PageNumber { get; set; }
|
||||
/// <summary>
|
||||
/// The title of the bookmark. Defaults to Page {PageNumber} if not set
|
||||
/// </summary>
|
||||
public required string Title { get; set; }
|
||||
|
||||
public required int SeriesId { get; set; }
|
||||
public virtual Series Series { get; set; }
|
||||
|
||||
public required int ChapterId { get; set; }
|
||||
public virtual Chapter Chapter { get; set; }
|
||||
|
||||
public int VolumeId { get; set; }
|
||||
public int LibraryId { get; set; }
|
||||
/// <summary>
|
||||
/// For Book Reader, represents the nearest passed anchor on the screen that can be used to resume scroll point. If empty, the ToC point is the beginning of the page
|
||||
/// </summary>
|
||||
public string? BookScrollId { get; set; }
|
||||
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
public DateTime LastModified { get; set; }
|
||||
public DateTime LastModifiedUtc { get; set; }
|
||||
|
||||
// Relationships
|
||||
/// <summary>
|
||||
/// Navigational Property for EF. Links to a unique AppUser
|
||||
/// </summary>
|
||||
public AppUser AppUser { get; set; } = null!;
|
||||
/// <summary>
|
||||
/// User this table of content belongs to
|
||||
/// </summary>
|
||||
public int AppUserId { get; set; }
|
||||
}
|
|
@ -46,6 +46,7 @@ public class ScrobbleEvent : IEntityDate
|
|||
/// </summary>
|
||||
public DateTime? ProcessDateUtc { get; set; }
|
||||
|
||||
|
||||
public required int SeriesId { get; set; }
|
||||
public Series Series { get; set; }
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using API.Data.Migrations;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Account;
|
||||
using API.DTOs.CollectionTags;
|
||||
|
@ -19,6 +20,10 @@ using API.Entities.Metadata;
|
|||
using API.Entities.Scrobble;
|
||||
using API.Helpers.Converters;
|
||||
using AutoMapper;
|
||||
using CollectionTag = API.Entities.CollectionTag;
|
||||
using MediaError = API.Entities.MediaError;
|
||||
using PublicationStatus = API.Entities.Enums.PublicationStatus;
|
||||
using SiteTheme = API.Entities.SiteTheme;
|
||||
|
||||
namespace API.Helpers;
|
||||
|
||||
|
@ -211,6 +216,7 @@ public class AutoMapperProfiles : Profile
|
|||
.ConvertUsing<ServerSettingConverter>();
|
||||
|
||||
CreateMap<Device, DeviceDto>();
|
||||
CreateMap<AppUserTableOfContent, PersonalToCDto>();
|
||||
|
||||
}
|
||||
}
|
||||
|
|
36
API/Helpers/Builders/PlusSeriesDtoBuilder.cs
Normal file
36
API/Helpers/Builders/PlusSeriesDtoBuilder.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
using System.Linq;
|
||||
using API.DTOs;
|
||||
using API.Entities;
|
||||
using API.Services.Plus;
|
||||
|
||||
namespace API.Helpers.Builders;
|
||||
|
||||
public class PlusSeriesDtoBuilder : IEntityBuilder<PlusSeriesDto>
|
||||
{
|
||||
private readonly PlusSeriesDto _seriesDto;
|
||||
public PlusSeriesDto Build() => _seriesDto;
|
||||
|
||||
/// <summary>
|
||||
/// This must be a FULL Series
|
||||
/// </summary>
|
||||
/// <param name="series"></param>
|
||||
public PlusSeriesDtoBuilder(Series series)
|
||||
{
|
||||
_seriesDto = new PlusSeriesDto()
|
||||
{
|
||||
MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type),
|
||||
SeriesName = series.Name,
|
||||
AltSeriesName = series.LocalizedName,
|
||||
AniListId = ScrobblingService.ExtractId<int?>(series.Metadata.WebLinks,
|
||||
ScrobblingService.AniListWeblinkWebsite),
|
||||
MalId = ScrobblingService.ExtractId<long?>(series.Metadata.WebLinks,
|
||||
ScrobblingService.MalWeblinkWebsite),
|
||||
GoogleBooksId = ScrobblingService.ExtractId<string?>(series.Metadata.WebLinks,
|
||||
ScrobblingService.GoogleBooksWeblinkWebsite),
|
||||
VolumeCount = series.Volumes.Count,
|
||||
ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial),
|
||||
Year = series.Metadata.ReleaseYear
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -70,7 +70,8 @@ public class Program
|
|||
var logger = services.GetRequiredService<ILogger<Program>>();
|
||||
var context = services.GetRequiredService<DataContext>();
|
||||
var pendingMigrations = await context.Database.GetPendingMigrationsAsync();
|
||||
if (pendingMigrations.Any())
|
||||
var isDbCreated = await context.Database.CanConnectAsync();
|
||||
if (isDbCreated && pendingMigrations.Any())
|
||||
{
|
||||
logger.LogInformation("Performing backup as migrations are needed. Backup will be kavita.db in temp folder");
|
||||
var migrationDirectory = await GetMigrationDirectory(context, directoryService);
|
||||
|
@ -84,16 +85,6 @@ public class Program
|
|||
}
|
||||
}
|
||||
|
||||
// This must run before the migration
|
||||
try
|
||||
{
|
||||
await MigrateSeriesRelationsExport.Migrate(context, logger);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// If fresh install, could fail and we should just carry on as it's not applicable
|
||||
}
|
||||
|
||||
await context.Database.MigrateAsync();
|
||||
|
||||
await Seed.SeedRoles(services.GetRequiredService<RoleManager<AppRole>>());
|
||||
|
|
|
@ -6,7 +6,6 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using API.Data.Metadata;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
|
@ -898,7 +897,7 @@ public class BookService : IBookService
|
|||
/// <param name="mappings">Epub mappings</param>
|
||||
/// <param name="page">Page number we are loading</param>
|
||||
/// <returns></returns>
|
||||
public async Task<string> ScopePage(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, Dictionary<string, int> mappings, int page)
|
||||
private async Task<string> ScopePage(HtmlDocument doc, EpubBookRef book, string apiBase, HtmlNode body, Dictionary<string, int> mappings, int page)
|
||||
{
|
||||
await InlineStyles(doc, book, apiBase, body);
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ public class StartupTasksHostedService : IHostedService
|
|||
taskScheduler.ScheduleUpdaterTasks();
|
||||
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
// These methods will automatically check if stat collection is disabled to prevent sending any data regardless
|
||||
|
|
|
@ -9,6 +9,7 @@ using API.DTOs;
|
|||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using Flurl.Http;
|
||||
using Kavita.Common;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
|
@ -59,19 +60,7 @@ public class RatingService : IRatingService
|
|||
.WithHeader("x-kavita-version", BuildInfo.Version)
|
||||
.WithHeader("Content-Type", "application/json")
|
||||
.WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs))
|
||||
.PostJsonAsync(new PlusSeriesDto()
|
||||
{
|
||||
MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type),
|
||||
SeriesName = series.Name,
|
||||
AltSeriesName = series.LocalizedName,
|
||||
AniListId = (int?) ScrobblingService.ExtractId(series.Metadata.WebLinks,
|
||||
ScrobblingService.AniListWeblinkWebsite),
|
||||
MalId = ScrobblingService.ExtractId(series.Metadata.WebLinks,
|
||||
ScrobblingService.MalWeblinkWebsite),
|
||||
VolumeCount = series.Volumes.Count,
|
||||
ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial),
|
||||
Year = series.Metadata.ReleaseYear
|
||||
})
|
||||
.PostJsonAsync(new PlusSeriesDtoBuilder(series).Build())
|
||||
.ReceiveJson<IEnumerable<RatingDto>>();
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
|
@ -11,6 +11,7 @@ using API.Entities;
|
|||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services.Tasks.Scanner.Parser;
|
||||
using Flurl.Http;
|
||||
using Kavita.Common;
|
||||
|
@ -24,6 +25,7 @@ public record PlusSeriesDto
|
|||
{
|
||||
public int? AniListId { get; set; }
|
||||
public long? MalId { get; set; }
|
||||
public string? GoogleBooksId { get; set; }
|
||||
public string SeriesName { get; set; }
|
||||
public string? AltSeriesName { get; set; }
|
||||
public MediaFormat MediaFormat { get; set; }
|
||||
|
@ -134,19 +136,7 @@ public class RecommendationService : IRecommendationService
|
|||
.WithHeader("x-kavita-version", BuildInfo.Version)
|
||||
.WithHeader("Content-Type", "application/json")
|
||||
.WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs))
|
||||
.PostJsonAsync(new PlusSeriesDto()
|
||||
{
|
||||
MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type),
|
||||
SeriesName = series.Name,
|
||||
AltSeriesName = series.LocalizedName,
|
||||
AniListId = (int?) ScrobblingService.ExtractId(series.Metadata.WebLinks,
|
||||
ScrobblingService.AniListWeblinkWebsite),
|
||||
MalId = ScrobblingService.ExtractId(series.Metadata.WebLinks,
|
||||
ScrobblingService.MalWeblinkWebsite),
|
||||
VolumeCount = series.Volumes.Count,
|
||||
ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial),
|
||||
Year = series.Metadata.ReleaseYear
|
||||
})
|
||||
.PostJsonAsync(new PlusSeriesDtoBuilder(series).Build())
|
||||
.ReceiveJson<IEnumerable<MediaRecommendationDto>>();
|
||||
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ public interface IScrobblingService
|
|||
{
|
||||
Task CheckExternalAccessTokens();
|
||||
Task<bool> HasTokenExpired(int userId, ScrobbleProvider provider);
|
||||
Task ScrobbleRatingUpdate(int userId, int seriesId, int rating);
|
||||
Task ScrobbleRatingUpdate(int userId, int seriesId, float rating);
|
||||
Task ScrobbleReviewUpdate(int userId, int seriesId, string reviewTitle, string reviewBody);
|
||||
Task ScrobbleReadingUpdate(int userId, int seriesId);
|
||||
Task ScrobbleWantToReadUpdate(int userId, int seriesId, bool onWantToRead);
|
||||
|
@ -63,11 +63,14 @@ public class ScrobblingService : IScrobblingService
|
|||
|
||||
public const string AniListWeblinkWebsite = "https://anilist.co/manga/";
|
||||
public const string MalWeblinkWebsite = "https://myanimelist.net/manga/";
|
||||
public const string GoogleBooksWeblinkWebsite = "https://books.google.com/books?id=";
|
||||
|
||||
private static readonly IDictionary<string, int> WeblinkExtractionMap = new Dictionary<string, int>()
|
||||
{
|
||||
{AniListWeblinkWebsite, 0},
|
||||
{MalWeblinkWebsite, 0},
|
||||
{GoogleBooksWeblinkWebsite, 0},
|
||||
|
||||
};
|
||||
|
||||
private const int ScrobbleSleepTime = 700; // We can likely tie this to AniList's 90 rate / min ((60 * 1000) / 90)
|
||||
|
@ -208,8 +211,8 @@ public class ScrobblingService : IScrobblingService
|
|||
SeriesId = series.Id,
|
||||
LibraryId = series.LibraryId,
|
||||
ScrobbleEventType = ScrobbleEventType.Review,
|
||||
AniListId = (int?) ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite),
|
||||
MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite),
|
||||
AniListId = ExtractId<int?>(series.Metadata.WebLinks, AniListWeblinkWebsite),
|
||||
MalId = ExtractId<long?>(series.Metadata.WebLinks, MalWeblinkWebsite),
|
||||
AppUserId = userId,
|
||||
Format = LibraryTypeHelper.GetFormat(series.Library.Type),
|
||||
ReviewBody = reviewBody,
|
||||
|
@ -220,7 +223,7 @@ public class ScrobblingService : IScrobblingService
|
|||
_logger.LogDebug("Added Scrobbling Review update on {SeriesName} with Userid {UserId} ", series.Name, userId);
|
||||
}
|
||||
|
||||
public async Task ScrobbleRatingUpdate(int userId, int seriesId, int rating)
|
||||
public async Task ScrobbleRatingUpdate(int userId, int seriesId, float rating)
|
||||
{
|
||||
if (!await _licenseService.HasActiveLicense()) return;
|
||||
var token = await GetTokenForProvider(userId, ScrobbleProvider.AniList);
|
||||
|
@ -253,8 +256,8 @@ public class ScrobblingService : IScrobblingService
|
|||
SeriesId = series.Id,
|
||||
LibraryId = series.LibraryId,
|
||||
ScrobbleEventType = ScrobbleEventType.ScoreUpdated,
|
||||
AniListId = (int?) ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite),
|
||||
MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite),
|
||||
AniListId = ExtractId<int?>(series.Metadata.WebLinks, AniListWeblinkWebsite),
|
||||
MalId = ExtractId<long?>(series.Metadata.WebLinks, MalWeblinkWebsite),
|
||||
AppUserId = userId,
|
||||
Format = LibraryTypeHelper.GetFormat(series.Library.Type),
|
||||
Rating = rating
|
||||
|
@ -310,8 +313,8 @@ public class ScrobblingService : IScrobblingService
|
|||
SeriesId = series.Id,
|
||||
LibraryId = series.LibraryId,
|
||||
ScrobbleEventType = ScrobbleEventType.ChapterRead,
|
||||
AniListId = (int?) ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite),
|
||||
MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite),
|
||||
AniListId = ExtractId<int?>(series.Metadata.WebLinks, AniListWeblinkWebsite),
|
||||
MalId = ExtractId<long?>(series.Metadata.WebLinks, MalWeblinkWebsite),
|
||||
AppUserId = userId,
|
||||
VolumeNumber =
|
||||
await _unitOfWork.AppUserProgressRepository.GetHighestFullyReadVolumeForSeries(seriesId, userId),
|
||||
|
@ -353,8 +356,8 @@ public class ScrobblingService : IScrobblingService
|
|||
SeriesId = series.Id,
|
||||
LibraryId = series.LibraryId,
|
||||
ScrobbleEventType = onWantToRead ? ScrobbleEventType.AddWantToRead : ScrobbleEventType.RemoveWantToRead,
|
||||
AniListId = (int?) ExtractId(series.Metadata.WebLinks, AniListWeblinkWebsite),
|
||||
MalId = ExtractId(series.Metadata.WebLinks, MalWeblinkWebsite),
|
||||
AniListId = ExtractId<int?>(series.Metadata.WebLinks, AniListWeblinkWebsite),
|
||||
MalId = ExtractId<long?>(series.Metadata.WebLinks, MalWeblinkWebsite),
|
||||
AppUserId = userId,
|
||||
Format = LibraryTypeHelper.GetFormat(series.Library.Type),
|
||||
};
|
||||
|
@ -542,7 +545,7 @@ public class ScrobblingService : IScrobblingService
|
|||
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
public async Task ProcessUpdatesSinceLastSync()
|
||||
{
|
||||
// Check how many scrobbles we have available then only do those.
|
||||
// Check how many scrobble events we have available then only do those.
|
||||
_logger.LogInformation("Starting Scrobble Processing");
|
||||
var userRateLimits = new Dictionary<int, int>();
|
||||
var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey);
|
||||
|
@ -623,7 +626,7 @@ public class ScrobblingService : IScrobblingService
|
|||
readEvt.AppUser.Id);
|
||||
_unitOfWork.ScrobbleRepository.Update(readEvt);
|
||||
}
|
||||
progressCounter = await ProcessEvents(readEvents, userRateLimits, usersToScrobble.Count, progressCounter, totalProgress, evt => new ScrobbleDto()
|
||||
progressCounter = await ProcessEvents(readEvents, userRateLimits, usersToScrobble.Count, progressCounter, totalProgress, async evt => new ScrobbleDto()
|
||||
{
|
||||
Format = evt.Format,
|
||||
AniListId = evt.AniListId,
|
||||
|
@ -634,12 +637,14 @@ public class ScrobblingService : IScrobblingService
|
|||
AniListToken = evt.AppUser.AniListAccessToken,
|
||||
SeriesName = evt.Series.Name,
|
||||
LocalizedSeriesName = evt.Series.LocalizedName,
|
||||
StartedReadingDateUtc = evt.CreatedUtc,
|
||||
ScrobbleDateUtc = evt.LastModifiedUtc,
|
||||
Year = evt.Series.Metadata.ReleaseYear
|
||||
Year = evt.Series.Metadata.ReleaseYear,
|
||||
StartedReadingDateUtc = await _unitOfWork.AppUserProgressRepository.GetFirstProgressForSeries(evt.SeriesId, evt.AppUser.Id),
|
||||
LatestReadingDateUtc = await _unitOfWork.AppUserProgressRepository.GetLatestProgressForSeries(evt.SeriesId, evt.AppUser.Id),
|
||||
});
|
||||
|
||||
progressCounter = await ProcessEvents(ratingEvents, userRateLimits, usersToScrobble.Count, progressCounter, totalProgress, evt => new ScrobbleDto()
|
||||
progressCounter = await ProcessEvents(ratingEvents, userRateLimits, usersToScrobble.Count, progressCounter,
|
||||
totalProgress, evt => Task.FromResult(new ScrobbleDto()
|
||||
{
|
||||
Format = evt.Format,
|
||||
AniListId = evt.AniListId,
|
||||
|
@ -650,9 +655,10 @@ public class ScrobblingService : IScrobblingService
|
|||
LocalizedSeriesName = evt.Series.LocalizedName,
|
||||
Rating = evt.Rating,
|
||||
Year = evt.Series.Metadata.ReleaseYear
|
||||
});
|
||||
}));
|
||||
|
||||
progressCounter = await ProcessEvents(reviewEvents, userRateLimits, usersToScrobble.Count, progressCounter, totalProgress, evt => new ScrobbleDto()
|
||||
progressCounter = await ProcessEvents(reviewEvents, userRateLimits, usersToScrobble.Count, progressCounter,
|
||||
totalProgress, evt => Task.FromResult(new ScrobbleDto()
|
||||
{
|
||||
Format = evt.Format,
|
||||
AniListId = evt.AniListId,
|
||||
|
@ -665,21 +671,22 @@ public class ScrobblingService : IScrobblingService
|
|||
Year = evt.Series.Metadata.ReleaseYear,
|
||||
ReviewBody = evt.ReviewBody,
|
||||
ReviewTitle = evt.ReviewTitle
|
||||
});
|
||||
}));
|
||||
|
||||
progressCounter = await ProcessEvents(decisions, userRateLimits, usersToScrobble.Count, progressCounter, totalProgress, evt => new ScrobbleDto()
|
||||
{
|
||||
Format = evt.Format,
|
||||
AniListId = evt.AniListId,
|
||||
MALId = (int?) evt.MalId,
|
||||
ScrobbleEventType = evt.ScrobbleEventType,
|
||||
ChapterNumber = evt.ChapterNumber,
|
||||
VolumeNumber = evt.VolumeNumber,
|
||||
AniListToken = evt.AppUser.AniListAccessToken,
|
||||
SeriesName = evt.Series.Name,
|
||||
LocalizedSeriesName = evt.Series.LocalizedName,
|
||||
Year = evt.Series.Metadata.ReleaseYear
|
||||
});
|
||||
progressCounter = await ProcessEvents(decisions, userRateLimits, usersToScrobble.Count, progressCounter,
|
||||
totalProgress, evt => Task.FromResult(new ScrobbleDto()
|
||||
{
|
||||
Format = evt.Format,
|
||||
AniListId = evt.AniListId,
|
||||
MALId = (int?) evt.MalId,
|
||||
ScrobbleEventType = evt.ScrobbleEventType,
|
||||
ChapterNumber = evt.ChapterNumber,
|
||||
VolumeNumber = evt.VolumeNumber,
|
||||
AniListToken = evt.AppUser.AniListAccessToken,
|
||||
SeriesName = evt.Series.Name,
|
||||
LocalizedSeriesName = evt.Series.LocalizedName,
|
||||
Year = evt.Series.Metadata.ReleaseYear
|
||||
}));
|
||||
}
|
||||
catch (FlurlHttpException)
|
||||
{
|
||||
|
@ -693,7 +700,7 @@ public class ScrobblingService : IScrobblingService
|
|||
}
|
||||
|
||||
private async Task<int> ProcessEvents(IEnumerable<ScrobbleEvent> events, IDictionary<int, int> userRateLimits,
|
||||
int usersToScrobble, int progressCounter, int totalProgress, Func<ScrobbleEvent, ScrobbleDto> createEvent)
|
||||
int usersToScrobble, int progressCounter, int totalProgress, Func<ScrobbleEvent, Task<ScrobbleDto>> createEvent)
|
||||
{
|
||||
var license = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey);
|
||||
foreach (var evt in events)
|
||||
|
@ -714,7 +721,7 @@ public class ScrobblingService : IScrobblingService
|
|||
|
||||
try
|
||||
{
|
||||
var data = createEvent(evt);
|
||||
var data = await createEvent(evt);
|
||||
userRateLimits[evt.AppUserId] = await PostScrobbleUpdate(data, license.Value, evt);
|
||||
evt.IsProcessed = true;
|
||||
evt.ProcessDateUtc = DateTime.UtcNow;
|
||||
|
@ -784,17 +791,31 @@ public class ScrobblingService : IScrobblingService
|
|||
/// <param name="webLinks"></param>
|
||||
/// <param name="website"></param>
|
||||
/// <returns></returns>
|
||||
public static long? ExtractId(string webLinks, string website)
|
||||
public static T? ExtractId<T>(string webLinks, string website)
|
||||
{
|
||||
var index = WeblinkExtractionMap[website];
|
||||
foreach (var webLink in webLinks.Split(','))
|
||||
{
|
||||
if (!webLink.StartsWith(website)) continue;
|
||||
var tokens = webLink.Split(website)[1].Split('/');
|
||||
return long.Parse(tokens[index]);
|
||||
var value = tokens[index];
|
||||
if (typeof(T) == typeof(int))
|
||||
{
|
||||
if (int.TryParse(value, out var intValue))
|
||||
return (T)(object)intValue;
|
||||
}
|
||||
else if (typeof(T) == typeof(long))
|
||||
{
|
||||
if (long.TryParse(value, out var longValue))
|
||||
return (T)(object)longValue;
|
||||
}
|
||||
else if (typeof(T) == typeof(string))
|
||||
{
|
||||
return (T)(object)value;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
return default(T?);
|
||||
}
|
||||
|
||||
private async Task<int> SetAndCheckRateLimit(IDictionary<int, int> userRateLimits, AppUser user, string license)
|
||||
|
|
|
@ -9,6 +9,7 @@ using API.DTOs.SeriesDetail;
|
|||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Helpers;
|
||||
using API.Helpers.Builders;
|
||||
using API.Services.Plus;
|
||||
using Flurl.Http;
|
||||
using HtmlAgilityPack;
|
||||
|
@ -133,19 +134,7 @@ public class ReviewService : IReviewService
|
|||
.WithHeader("x-kavita-version", BuildInfo.Version)
|
||||
.WithHeader("Content-Type", "application/json")
|
||||
.WithTimeout(TimeSpan.FromSeconds(Configuration.DefaultTimeOutSecs))
|
||||
.PostJsonAsync(new PlusSeriesDto()
|
||||
{
|
||||
MediaFormat = LibraryTypeHelper.GetFormat(series.Library.Type),
|
||||
SeriesName = series.Name,
|
||||
AltSeriesName = series.LocalizedName,
|
||||
AniListId = (int?) ScrobblingService.ExtractId(series.Metadata.WebLinks,
|
||||
ScrobblingService.AniListWeblinkWebsite),
|
||||
MalId = ScrobblingService.ExtractId(series.Metadata.WebLinks,
|
||||
ScrobblingService.MalWeblinkWebsite),
|
||||
VolumeCount = series.Volumes.Count,
|
||||
ChapterCount = series.Volumes.SelectMany(v => v.Chapters).Count(c => !c.IsSpecial),
|
||||
Year = series.Metadata.ReleaseYear
|
||||
})
|
||||
.PostJsonAsync(new PlusSeriesDtoBuilder(series).Build())
|
||||
.ReceiveJson<IEnumerable<MediaReviewDto>>();
|
||||
|
||||
}
|
||||
|
|
|
@ -217,22 +217,14 @@ public class SeriesService : ISeriesService
|
|||
// Trigger code to cleanup tags, collections, people, etc
|
||||
await _taskScheduler.CleanupDbEntries();
|
||||
|
||||
if (updateSeriesMetadataDto.CollectionTags != null)
|
||||
if (updateSeriesMetadataDto.CollectionTags == null) return true;
|
||||
foreach (var tag in updateSeriesMetadataDto.CollectionTags)
|
||||
{
|
||||
foreach (var tag in updateSeriesMetadataDto.CollectionTags)
|
||||
{
|
||||
await _eventHub.SendMessageAsync(MessageFactory.SeriesAddedToCollection,
|
||||
MessageFactory.SeriesAddedToCollectionEvent(tag.Id,
|
||||
updateSeriesMetadataDto.SeriesMetadata.SeriesId), false);
|
||||
}
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.ScanSeries,
|
||||
MessageFactory.ScanSeriesEvent(series.LibraryId, series.Id, series.Name), false);
|
||||
|
||||
await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries();
|
||||
|
||||
return true;
|
||||
await _eventHub.SendMessageAsync(MessageFactory.SeriesAddedToCollection,
|
||||
MessageFactory.SeriesAddedToCollectionEvent(tag.Id,
|
||||
updateSeriesMetadataDto.SeriesMetadata.SeriesId), false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -302,7 +294,8 @@ public class SeriesService : ISeriesService
|
|||
new AppUserRating();
|
||||
try
|
||||
{
|
||||
userRating.Rating = Math.Clamp(updateSeriesRatingDto.UserRating, 0, 5);
|
||||
userRating.Rating = Math.Clamp(updateSeriesRatingDto.UserRating, 0f, 5f);
|
||||
userRating.HasBeenRated = true;
|
||||
userRating.SeriesId = updateSeriesRatingDto.SeriesId;
|
||||
|
||||
if (userRating.Id == 0)
|
||||
|
|
|
@ -61,6 +61,7 @@ public class TaskScheduler : ITaskScheduler
|
|||
public const string DefaultQueue = "default";
|
||||
public const string RemoveFromWantToReadTaskId = "remove-from-want-to-read";
|
||||
public const string UpdateYearlyStatsTaskId = "update-yearly-stats";
|
||||
public const string CheckForUpdateId = "check-updates";
|
||||
public const string CleanupDbTaskId = "cleanup-db";
|
||||
public const string CleanupTaskId = "cleanup";
|
||||
public const string BackupTaskId = "backup";
|
||||
|
@ -126,7 +127,13 @@ public class TaskScheduler : ITaskScheduler
|
|||
if (setting != null)
|
||||
{
|
||||
_logger.LogDebug("Scheduling Backup Task for {Setting}", setting);
|
||||
RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), () => CronConverter.ConvertToCronNotation(setting), RecurringJobOptions);
|
||||
var schedule = CronConverter.ConvertToCronNotation(setting);
|
||||
if (schedule == Cron.Daily())
|
||||
{
|
||||
// Override daily and make 2am so that everything on system has cleaned up and no blocking
|
||||
schedule = Cron.Daily(2);
|
||||
}
|
||||
RecurringJob.AddOrUpdate(BackupTaskId, () => _backupService.BackupDatabase(), () => schedule, RecurringJobOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -226,10 +233,8 @@ public class TaskScheduler : ITaskScheduler
|
|||
public void ScheduleUpdaterTasks()
|
||||
{
|
||||
_logger.LogInformation("Scheduling Auto-Update tasks");
|
||||
RecurringJob.AddOrUpdate("check-updates", () => CheckForUpdate(), Cron.Daily(Rnd.Next(5, 23)), new RecurringJobOptions()
|
||||
{
|
||||
TimeZone = TimeZoneInfo.Local
|
||||
});
|
||||
RecurringJob.AddOrUpdate(CheckForUpdateId, () => CheckForUpdate(), $"0 */{Rnd.Next(4, 6)} * * *", RecurringJobOptions);
|
||||
BackgroundJob.Enqueue(() => CheckForUpdate());
|
||||
}
|
||||
|
||||
public void ScanFolder(string folderPath, TimeSpan delay)
|
||||
|
|
|
@ -145,7 +145,7 @@ public class BackupService : IBackupService
|
|||
|
||||
private void CopyFaviconsToBackupDirectory(string tempDirectory)
|
||||
{
|
||||
_directoryService.CopyDirectoryToDirectory(_directoryService.FaviconDirectory, tempDirectory);
|
||||
_directoryService.CopyDirectoryToDirectory(_directoryService.FaviconDirectory, _directoryService.FileSystem.Path.Join(tempDirectory, "favicons"));
|
||||
}
|
||||
|
||||
private async Task CopyCoverImagesToBackupDirectory(string tempDirectory)
|
||||
|
|
|
@ -72,13 +72,11 @@ public class VersionUpdaterService : IVersionUpdaterService
|
|||
/// <summary>
|
||||
/// Fetches the latest release from Github
|
||||
/// </summary>
|
||||
/// <returns>Latest update or null if current version is greater than latest update</returns>
|
||||
public async Task<UpdateNotificationDto?> CheckForUpdate()
|
||||
/// <returns>Latest update</returns>
|
||||
public async Task<UpdateNotificationDto> CheckForUpdate()
|
||||
{
|
||||
var update = await GetGithubRelease();
|
||||
var dto = CreateDto(update);
|
||||
if (dto == null) return null;
|
||||
return new Version(dto.UpdateVersion) <= new Version(dto.CurrentVersion) ? null : dto;
|
||||
return CreateDto(update);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<UpdateNotificationDto>> GetAllReleases()
|
||||
|
|
|
@ -65,7 +65,7 @@ public class PresenceTracker : IPresenceTracker
|
|||
_unitOfWork.UserRepository.Update(user);
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
// Swallow the exception
|
||||
}
|
||||
|
|
|
@ -247,6 +247,9 @@ public class Startup
|
|||
// v0.7.4
|
||||
await MigrateDisableScrobblingOnComicLibraries.Migrate(unitOfWork, dataContext, logger);
|
||||
|
||||
// v0.7.6
|
||||
await MigrateExistingRatings.Migrate(dataContext, logger);
|
||||
|
||||
// Update the version in the DB after all migrations are run
|
||||
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
|
||||
installVersion.Value = BuildInfo.Version.ToString();
|
||||
|
|
|
@ -100,7 +100,7 @@ public static class Configuration
|
|||
{
|
||||
try
|
||||
{
|
||||
return GetJwtToken(GetAppSettingFilename()) != "super secret unguessable key";
|
||||
return !GetJwtToken(GetAppSettingFilename()).StartsWith("super secret unguessable key");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using DeviceId;
|
||||
using DeviceId.Components;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
|
||||
namespace Kavita.Common;
|
||||
|
||||
|
@ -62,27 +57,4 @@ public static class HashUtil
|
|||
|
||||
return id.ToString();
|
||||
}
|
||||
|
||||
private static string RunAndCapture(string filename, string args)
|
||||
{
|
||||
var p = new Process
|
||||
{
|
||||
StartInfo =
|
||||
{
|
||||
FileName = filename,
|
||||
Arguments = args,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true
|
||||
}
|
||||
};
|
||||
|
||||
p.Start();
|
||||
|
||||
// To avoid deadlocks, always read the output stream first and then wait.
|
||||
var output = p.StandardOutput.ReadToEnd();
|
||||
p.WaitForExit(1000);
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,17 +4,12 @@
|
|||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Company>kavitareader.com</Company>
|
||||
<Product>Kavita</Product>
|
||||
<AssemblyVersion>0.7.5.0</AssemblyVersion>
|
||||
<AssemblyVersion>0.7.6.0</AssemblyVersion>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<TieredPGO>true</TieredPGO>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DeviceId" Version="6.3.0" />
|
||||
<PackageReference Include="DeviceId.Linux" Version="6.3.0" />
|
||||
<PackageReference Include="DeviceId.Mac" Version="6.2.0" />
|
||||
<PackageReference Include="DeviceId.Windows" Version="6.2.0" />
|
||||
<PackageReference Include="DeviceId.Windows.Wmi" Version="6.2.1" />
|
||||
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
|
||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
||||
|
|
1079
UI/Web/package-lock.json
generated
1079
UI/Web/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -12,24 +12,24 @@
|
|||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^16.0.2",
|
||||
"@angular/cdk": "^16.0.1",
|
||||
"@angular/common": "^16.0.2",
|
||||
"@angular/compiler": "^16.0.2",
|
||||
"@angular/core": "^16.0.2",
|
||||
"@angular/forms": "^16.0.2",
|
||||
"@angular/localize": "^16.0.2",
|
||||
"@angular/platform-browser": "^16.0.2",
|
||||
"@angular/platform-browser-dynamic": "^16.0.2",
|
||||
"@angular/router": "^16.0.2",
|
||||
"@angular/animations": "^16.1.6",
|
||||
"@angular/cdk": "^16.1.5",
|
||||
"@angular/common": "^16.1.6",
|
||||
"@angular/compiler": "^16.1.6",
|
||||
"@angular/core": "^16.1.6",
|
||||
"@angular/forms": "^16.1.6",
|
||||
"@angular/localize": "^16.1.6",
|
||||
"@angular/platform-browser": "^16.1.6",
|
||||
"@angular/platform-browser-dynamic": "^16.1.6",
|
||||
"@angular/router": "^16.1.6",
|
||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||
"@iharbeck/ngx-virtual-scroller": "^16.0.0",
|
||||
"@iplab/ngx-file-upload": "^16.0.1",
|
||||
"@microsoft/signalr": "^7.0.5",
|
||||
"@microsoft/signalr": "^7.0.9",
|
||||
"@ng-bootstrap/ng-bootstrap": "^15.1.0",
|
||||
"@popperjs/core": "^2.11.7",
|
||||
"@swimlane/ngx-charts": "^20.1.2",
|
||||
"@tweenjs/tween.js": "^20.0.3",
|
||||
"@tweenjs/tween.js": "^21.0.0",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"bootstrap": "^5.2.3",
|
||||
"eventsource": "^2.0.2",
|
||||
|
@ -38,8 +38,9 @@
|
|||
"ng-circle-progress": "^1.7.1",
|
||||
"ngx-color-picker": "^14.0.0",
|
||||
"ngx-extended-pdf-viewer": "^16.2.16",
|
||||
"ngx-file-drop": "^15.0.0",
|
||||
"ngx-slider-v2": "^15.0.4",
|
||||
"ngx-file-drop": "^16.0.0",
|
||||
"ngx-slider-v2": "^16.0.2",
|
||||
"ngx-stars": "^1.6.5",
|
||||
"ngx-toastr": "^17.0.2",
|
||||
"rxjs": "^7.8.0",
|
||||
"screenfull": "^6.0.2",
|
||||
|
@ -48,22 +49,22 @@
|
|||
"zone.js": "^0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^16.0.2",
|
||||
"@angular-eslint/builder": "^16.0.2",
|
||||
"@angular-eslint/eslint-plugin": "^16.0.2",
|
||||
"@angular-eslint/eslint-plugin-template": "^16.0.2",
|
||||
"@angular-eslint/schematics": "^16.0.2",
|
||||
"@angular-eslint/template-parser": "^16.0.2",
|
||||
"@angular/cli": "^16.0.2",
|
||||
"@angular/compiler-cli": "^16.0.2",
|
||||
"@angular-devkit/build-angular": "^16.1.5",
|
||||
"@angular-eslint/builder": "^16.1.0",
|
||||
"@angular-eslint/eslint-plugin": "^16.1.0",
|
||||
"@angular-eslint/eslint-plugin-template": "^16.1.0",
|
||||
"@angular-eslint/schematics": "^16.1.0",
|
||||
"@angular-eslint/template-parser": "^16.1.0",
|
||||
"@angular/cli": "^16.1.5",
|
||||
"@angular/compiler-cli": "^16.1.6",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/node": "^20.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.48.1",
|
||||
"@typescript-eslint/parser": "5.59.6",
|
||||
"eslint": "^8.41.0",
|
||||
"@types/node": "^20.4.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
||||
"@typescript-eslint/parser": "^6.1.0",
|
||||
"eslint": "^8.45.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"ts-node": "~10.9.1",
|
||||
"typescript": "~5.0.4",
|
||||
"typescript": "^5.1.6",
|
||||
"webpack-bundle-analyzer": "^4.8.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,4 +5,5 @@ export interface Rating {
|
|||
meanScore: number;
|
||||
favoriteCount: number;
|
||||
provider: ScrobbleProvider;
|
||||
providerUrl: string | undefined;
|
||||
}
|
||||
|
|
8
UI/Web/src/app/_models/readers/personal-toc.ts
Normal file
8
UI/Web/src/app/_models/readers/personal-toc.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export interface PersonalToC {
|
||||
chapterId: number;
|
||||
pageNumber: number;
|
||||
title: string;
|
||||
bookScrollId: string | undefined;
|
||||
/* Ui Only */
|
||||
position: 0;
|
||||
}
|
|
@ -27,6 +27,7 @@ export interface Series {
|
|||
* User's rating (0-5)
|
||||
*/
|
||||
userRating: number;
|
||||
hasUserRated: boolean;
|
||||
libraryId: number;
|
||||
/**
|
||||
* DateTime the entity was created
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import {DestroyRef, inject, Injectable} from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
import {DestroyRef, Inject, inject, Injectable} from '@angular/core';
|
||||
import {DOCUMENT, Location} from '@angular/common';
|
||||
import { Router } from '@angular/router';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { ChapterInfo } from '../manga-reader/_models/chapter-info';
|
||||
|
@ -17,9 +17,8 @@ import { FileDimension } from '../manga-reader/_models/file-dimension';
|
|||
import screenfull from 'screenfull';
|
||||
import { TextResonse } from '../_types/text-response';
|
||||
import { AccountService } from './account.service';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { OnDestroy } from '@angular/core';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {PersonalToC} from "../_models/readers/personal-toc";
|
||||
|
||||
export const CHAPTER_ID_DOESNT_EXIST = -1;
|
||||
export const CHAPTER_ID_NOT_FETCHED = -2;
|
||||
|
@ -279,4 +278,51 @@ export class ReaderService {
|
|||
this.location.back();
|
||||
}
|
||||
}
|
||||
|
||||
removePersonalToc(chapterId: number, pageNumber: number, title: string) {
|
||||
return this.httpClient.delete(this.baseUrl + `reader/ptoc?chapterId=${chapterId}&pageNum=${pageNumber}&title=${encodeURIComponent(title)}`);
|
||||
}
|
||||
|
||||
getPersonalToC(chapterId: number) {
|
||||
return this.httpClient.get<Array<PersonalToC>>(this.baseUrl + 'reader/ptoc?chapterId=' + chapterId);
|
||||
}
|
||||
|
||||
createPersonalToC(libraryId: number, seriesId: number, volumeId: number, chapterId: number, pageNumber: number, title: string, bookScrollId: string | null) {
|
||||
return this.httpClient.post(this.baseUrl + 'reader/create-ptoc', {libraryId, seriesId, volumeId, chapterId, pageNumber, title, bookScrollId});
|
||||
}
|
||||
|
||||
getElementFromXPath(path: string) {
|
||||
const node = document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
if (node?.nodeType === Node.ELEMENT_NODE) {
|
||||
return node as Element;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param element
|
||||
* @param pureXPath Will ignore shortcuts like id('')
|
||||
*/
|
||||
getXPathTo(element: any, pureXPath = false): string {
|
||||
if (element === null) return '';
|
||||
if (!pureXPath) {
|
||||
if (element.id !== '') { return 'id("' + element.id + '")'; }
|
||||
if (element === document.body) { return element.tagName; }
|
||||
}
|
||||
|
||||
|
||||
let ix = 0;
|
||||
const siblings = element.parentNode?.childNodes || [];
|
||||
for (let sibling of siblings) {
|
||||
if (sibling === element) {
|
||||
return this.getXPathTo(element.parentNode) + '/' + element.tagName + '[' + (ix + 1) + ']';
|
||||
}
|
||||
if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
|
||||
ix++;
|
||||
}
|
||||
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +1,20 @@
|
|||
import {HttpClient, HttpParams} from '@angular/common/http';
|
||||
import {DestroyRef, inject, Injectable, OnDestroy} from '@angular/core';
|
||||
import { of, ReplaySubject, Subject } from 'rxjs';
|
||||
import { filter, map, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import {Injectable} from '@angular/core';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { Preferences } from '../_models/preferences/preferences';
|
||||
import { User } from '../_models/user';
|
||||
import { Router } from '@angular/router';
|
||||
import { EVENTS, MessageHubService } from './message-hub.service';
|
||||
import { ThemeService } from './theme.service';
|
||||
import { InviteUserResponse } from '../_models/auth/invite-user-response';
|
||||
import { UserUpdateEvent } from '../_models/events/user-update-event';
|
||||
import { UpdateEmailResponse } from '../_models/auth/update-email-response';
|
||||
import { AgeRating } from '../_models/metadata/age-rating';
|
||||
import { AgeRestriction } from '../_models/metadata/age-restriction';
|
||||
import { TextResonse } from '../_types/text-response';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {ScrobbleError} from "../_models/scrobbling/scrobble-error";
|
||||
import {ScrobbleEvent} from "../_models/scrobbling/scrobble-event";
|
||||
import {ScrobbleHold} from "../_models/scrobbling/scrobble-hold";
|
||||
import {PaginatedResult, Pagination} from "../_models/pagination";
|
||||
import {PaginatedResult} from "../_models/pagination";
|
||||
import {ScrobbleEventFilter} from "../_models/scrobbling/scrobble-event-filter";
|
||||
import {UtilityService} from "../shared/_services/utility.service";
|
||||
import {ReadingList} from "../_models/reading-list";
|
||||
|
||||
export enum ScrobbleProvider {
|
||||
Kavita = 0,
|
||||
AniList= 1,
|
||||
Mal = 2,
|
||||
GoogleBooks = 3
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
|
@ -34,7 +22,6 @@ export enum ScrobbleProvider {
|
|||
})
|
||||
export class ScrobblingService {
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
baseUrl = environment.apiUrl;
|
||||
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<ng-template #emailServiceTooltip>Use fully qualified URL of the email service. Do not include ending slash.</ng-template>
|
||||
<span class="visually-hidden" id="settings-emailservice-help"><ng-container [ngTemplateOutlet]="emailServiceTooltip"></ng-container></span>
|
||||
<div class="input-group">
|
||||
<input id="settings-emailservice" aria-describedby="settings-emailservice-help" class="form-control" formControlName="emailServiceUrl" type="url" autocapitalize="off" inputmode="url" aria-describedby="change-bookmarks-dir">
|
||||
<input id="settings-emailservice" aria-describedby="settings-emailservice-help" class="form-control" formControlName="emailServiceUrl" type="url" autocapitalize="off" inputmode="url">
|
||||
<button class="btn btn-outline-secondary" (click)="resetEmailServiceUrl()">
|
||||
Reset
|
||||
</button>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="row g-0">
|
||||
<p>WebP/AVIF can drastically reduce space requirements for files. WebP/AVIF is not supported on all browsers or versions. To learn if these settings are appropriate for your setup, visit <a href="https://caniuse.com/?search=webp" target="_blank" rel="noopener noreferrer">Can I Use WebP</a> or <a href="https://caniuse.com/?search=avif" target="_blank" rel="noopener noreferrer">Can I Use AVIF</a>.
|
||||
<b>You cannot convert back to PNG once you've gone to WebP/AVIF. You would need to refresh covers on your libraries to regenerate all covers. Bookmarks and favicons cannot be converted.</b></p>
|
||||
<div *ngIf="settingsForm.dirty" class="alert alert-danger" role="alert">You must trigger the media conversion task in Tasks Tab.</div>
|
||||
<div *ngIf="settingsForm.get('encodeMediaAs')?.dirty" class="alert alert-danger" role="alert">You must trigger the media conversion task in Tasks Tab.</div>
|
||||
<div class="col-md-6 col-sm-12 mb-3">
|
||||
<label for="settings-media-encodeMediaAs" class="form-label me-1">Save Media As</label>
|
||||
<i class="fa fa-info-circle" placement="right" [ngbTooltip]="encodeMediaAsTooltip" role="button" tabindex="0"></i>
|
||||
|
|
|
@ -47,6 +47,7 @@ export class ManageMediaSettingsComponent implements OnInit {
|
|||
saveSettings() {
|
||||
const modelSettings = Object.assign({}, this.serverSettings);
|
||||
modelSettings.encodeMediaAs = parseInt(this.settingsForm.get('encodeMediaAs')?.value, 10);
|
||||
modelSettings.bookmarksDirectory = this.settingsForm.get('bookmarksDirectory')?.value;
|
||||
|
||||
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe(async (settings: ServerSettings) => {
|
||||
this.serverSettings = settings;
|
||||
|
|
|
@ -13,29 +13,30 @@
|
|||
|
||||
<h3>More Info</h3>
|
||||
<hr/>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-4">Home page:</div>
|
||||
<div class="col"><a href="https://www.kavitareader.com" target="_blank" rel="noopener noreferrer">kavitareader.com</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">Wiki:</div>
|
||||
<div class="col"><a href="https://wiki.kavitareader.com" target="_blank" rel="noopener noreferrer">wiki.kavitareader.com</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">Discord:</div>
|
||||
<div class="col"><a href="https://discord.gg/b52wT37kt7" target="_blank" rel="noopener noreferrer">discord.gg/b52wT37kt7</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">Donations:</div>
|
||||
<div class="col"><a href="https://opencollective.com/kavita" target="_blank" rel="noopener noreferrer">opencollective.com/kavita</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">Source:</div>
|
||||
<div class="col"><a href="https://github.com/Kareadita/Kavita" target="_blank" rel="noopener noreferrer">github.com/Kareadita/Kavita</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">Feature Requests:</div>
|
||||
<div class="col"><a href="https://feats.kavitareader.com" target="_blank" rel="noopener noreferrer">https://feats.kavitareader.com</a><br/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">Wiki:</div>
|
||||
<div class="col"><a href="https://wiki.kavitareader.com" target="_blank" rel="noopener noreferrer">wiki.kavitareader.com</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">Discord:</div>
|
||||
<div class="col"><a href="https://discord.gg/b52wT37kt7" target="_blank" rel="noopener noreferrer">discord.gg/b52wT37kt7</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">Donations:</div>
|
||||
<div class="col"><a href="https://opencollective.com/kavita" target="_blank" rel="noopener noreferrer">opencollective.com/kavita</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">Source:</div>
|
||||
<div class="col"><a href="https://github.com/Kareadita/Kavita" target="_blank" rel="noopener noreferrer">github.com/Kareadita/Kavita</a></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4">Feature Requests:</div>
|
||||
<div class="col"><a href="https://feats.kavitareader.com" target="_blank" rel="noopener noreferrer">https://feats.kavitareader.com</a><br/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
<div class="overlay" *ngIf="selectedText.length > 0 || mode !== BookLineOverlayMode.None">
|
||||
|
||||
<div class="row g-0 justify-content-between">
|
||||
<ng-container [ngSwitch]="mode">
|
||||
<ng-container *ngSwitchCase="BookLineOverlayMode.None">
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-icon btn-sm" (click)="copy()">
|
||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
||||
<div>Copy</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-icon btn-sm" (click)="switchMode(BookLineOverlayMode.Bookmark)">
|
||||
<i class="fa-solid fa-book-bookmark" aria-hidden="true"></i>
|
||||
<div>Bookmark</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-icon btn-sm" (click)="reset()">
|
||||
<i class="fa-solid fa-times-circle" aria-hidden="true"></i>
|
||||
<div>Close</div>
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="BookLineOverlayMode.Bookmark">
|
||||
<form [formGroup]="bookmarkForm">
|
||||
<div class="input-group">
|
||||
<input id="bookmark-name" class="form-control" formControlName="name" type="text" placeholder="Bookmark Name"
|
||||
[class.is-invalid]="bookmarkForm.get('name')?.invalid && bookmarkForm.get('name')?.touched" aria-describedby="bookmark-name-btn">
|
||||
<button class="btn btn-outline-primary" id="bookmark-name-btn" (click)="createPTOC()">Save</button>
|
||||
<div id="bookmark-name-validations" class="invalid-feedback" *ngIf="bookmarkForm.dirty || bookmarkForm.touched">
|
||||
<div *ngIf="bookmarkForm.get('name')?.errors?.required" role="status">
|
||||
This field is required
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
|
@ -0,0 +1,8 @@
|
|||
.overlay {
|
||||
background-color: var(--br-actionbar-bg-color);
|
||||
color: var(--bs-body-bg);
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
z-index: 9999;
|
||||
width: 100vw;
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
import {
|
||||
ChangeDetectionStrategy, ChangeDetectorRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
ElementRef, EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
OnInit, Output,
|
||||
} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {fromEvent, merge, of} from "rxjs";
|
||||
import {catchError, filter, tap} from "rxjs/operators";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import getBoundingClientRect from "@popperjs/core/lib/dom-utils/getBoundingClientRect";
|
||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import {ReaderService} from "../../../_services/reader.service";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
enum BookLineOverlayMode {
|
||||
None = 0,
|
||||
Bookmark = 1
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-book-line-overlay',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule],
|
||||
templateUrl: './book-line-overlay.component.html',
|
||||
styleUrls: ['./book-line-overlay.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class BookLineOverlayComponent implements OnInit {
|
||||
@Input({required: true}) libraryId!: number;
|
||||
@Input({required: true}) seriesId!: number;
|
||||
@Input({required: true}) volumeId!: number;
|
||||
@Input({required: true}) chapterId!: number;
|
||||
@Input({required: true}) pageNumber: number = 0;
|
||||
@Input({required: true}) parent: ElementRef | undefined;
|
||||
@Output() refreshToC: EventEmitter<void> = new EventEmitter();
|
||||
|
||||
xPath: string = '';
|
||||
selectedText: string = '';
|
||||
mode: BookLineOverlayMode = BookLineOverlayMode.None;
|
||||
bookmarkForm: FormGroup = new FormGroup({
|
||||
name: new FormControl('', [Validators.required]),
|
||||
});
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly readerService = inject(ReaderService);
|
||||
|
||||
get BookLineOverlayMode() { return BookLineOverlayMode; }
|
||||
constructor(private elementRef: ElementRef, private toastr: ToastrService) {}
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
if (this.parent) {
|
||||
|
||||
const mouseUp$ = fromEvent<MouseEvent>(this.parent.nativeElement, 'mouseup');
|
||||
const touchEnd$ = fromEvent<TouchEvent>(this.parent.nativeElement, 'touchend');
|
||||
|
||||
merge(mouseUp$, touchEnd$)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((event: MouseEvent | TouchEvent) => {
|
||||
this.handleEvent(event);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleEvent(event: MouseEvent | TouchEvent) {
|
||||
const selection = window.getSelection();
|
||||
if (!event.target) return;
|
||||
|
||||
if ((!selection || selection.toString().trim() === '' || selection.toString().trim() === this.selectedText)) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedText = selection ? selection.toString().trim() : '';
|
||||
|
||||
if (this.selectedText.length > 0 && this.mode === BookLineOverlayMode.None) {
|
||||
this.xPath = this.readerService.getXPathTo(event.target);
|
||||
if (this.xPath !== '') {
|
||||
this.xPath = '//' + this.xPath;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
switchMode(mode: BookLineOverlayMode) {
|
||||
this.mode = mode;
|
||||
this.cdRef.markForCheck();
|
||||
if (this.mode === BookLineOverlayMode.Bookmark) {
|
||||
this.bookmarkForm.get('name')?.setValue(this.selectedText);
|
||||
this.focusOnBookmarkInput();
|
||||
}
|
||||
}
|
||||
|
||||
createPTOC() {
|
||||
this.readerService.createPersonalToC(this.libraryId, this.seriesId, this.volumeId, this.chapterId, this.pageNumber,
|
||||
this.bookmarkForm.get('name')?.value, this.xPath).pipe(catchError(err => {
|
||||
this.focusOnBookmarkInput();
|
||||
return of();
|
||||
})).subscribe(() => {
|
||||
this.reset();
|
||||
this.refreshToC.emit();
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
focusOnBookmarkInput() {
|
||||
if (this.mode !== BookLineOverlayMode.Bookmark) return;
|
||||
setTimeout(() => this.elementRef.nativeElement.querySelector('#bookmark-name')?.focus(), 10);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.bookmarkForm.reset();
|
||||
this.mode = BookLineOverlayMode.None;
|
||||
this.xPath = '';
|
||||
this.selectedText = '';
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
async copy() {
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
await navigator.clipboard.writeText(selection.toString());
|
||||
this.toastr.info('Copied to clipboard');
|
||||
}
|
||||
this.reset();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -2,6 +2,14 @@
|
|||
<div class="fixed-top" #stickyTop>
|
||||
<a class="visually-hidden-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">Skip to main content</a>
|
||||
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
|
||||
<app-book-line-overlay [parent]="bookContainerElemRef" *ngIf="page !== undefined"
|
||||
[libraryId]="libraryId"
|
||||
[volumeId]="volumeId"
|
||||
[chapterId]="chapterId"
|
||||
[seriesId]="seriesId"
|
||||
[pageNumber]="pageNum"
|
||||
(refreshToC)="refreshPersonalToC()">
|
||||
</app-book-line-overlay>
|
||||
<app-drawer #commentDrawer="drawer" [(isOpen)]="drawerOpen" [options]="{topOffset: topOffset}">
|
||||
<h5 header>
|
||||
Book Settings
|
||||
|
@ -63,7 +71,23 @@
|
|||
<li [ngbNavItem]="TabID.TableOfContents">
|
||||
<a ngbNavLink>Table of Contents</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-table-of-contents [chapters]="chapters" [chapterId]="chapterId" [pageNum]="pageNum" [currentPageAnchor]="currentPageAnchor" (loadChapter)="loadChapterPage($event)"></app-table-of-contents>
|
||||
<ul #subnav="ngbNav" ngbNav [(activeId)]="tocId" class="reader-pills nav nav-pills mb-2" [destroyOnHide]="false">
|
||||
<li [ngbNavItem]="TabID.TableOfContents">
|
||||
<a ngbNavLink>ToC</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-table-of-contents [chapters]="chapters" [chapterId]="chapterId" [pageNum]="pageNum"
|
||||
[currentPageAnchor]="currentPageAnchor" (loadChapter)="loadChapterPage($event)"></app-table-of-contents>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li [ngbNavItem]="TabID.PersonalTableOfContents">
|
||||
<a ngbNavLink>Bookmarks</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-personal-table-of-contents [chapterId]="chapterId" [pageNum]="pageNum" (loadChapter)="loadChapterPart($event)"
|
||||
[tocRefresh]="refreshPToC"></app-personal-table-of-contents>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="subnav" class="mt-3"></div>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -73,7 +97,7 @@
|
|||
</app-drawer>
|
||||
</div>
|
||||
|
||||
<div #readingSection class="reading-section {{ColumnLayout}} {{WritingStyleClass}}" [ngStyle]="{'width': PageWidthForPagination}" [ngClass]="{'immersive' : immersiveMode || !actionBarVisible}" [@isLoading]="isLoading ? true : false">
|
||||
<div #readingSection class="reading-section {{ColumnLayout}} {{WritingStyleClass}}" [ngStyle]="{'width': PageWidthForPagination}" [ngClass]="{'immersive' : immersiveMode || !actionBarVisible}" [@isLoading]="isLoading">
|
||||
|
||||
<ng-container *ngIf="clickToPaginate">
|
||||
<div class="left {{clickOverlayClass('left')}} no-observe" [ngClass]="{'immersive' : immersiveMode}"
|
||||
|
@ -90,8 +114,6 @@
|
|||
[ngStyle]="{'max-height': ColumnHeight, 'max-width': VerticalBookContentWidth, 'width': VerticalBookContentWidth, 'column-width': ColumnWidth}"
|
||||
[ngClass]="{'immersive': immersiveMode && actionBarVisible}"
|
||||
[innerHtml]="page" *ngIf="page !== undefined" (click)="toggleMenu($event)" (mousedown)="mouseDown($event)" (wheel)="onWheel($event)"></div>
|
||||
|
||||
|
||||
<div *ngIf="page !== undefined && (scrollbarNeeded || layoutMode !== BookPageLayoutMode.Default) && !(writingStyle === WritingStyle.Vertical && layoutMode === BookPageLayoutMode.Default)"
|
||||
(click)="$event.stopPropagation();"
|
||||
[ngClass]="{'bottom-bar': layoutMode !== BookPageLayoutMode.Default}">
|
||||
|
@ -99,7 +121,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #actionBar>
|
||||
<div class="action-bar row g-0 justify-content-between" *ngIf="!immersiveMode || drawerOpen || actionBarVisible">
|
||||
<button class="btn btn-outline-secondary btn-icon col-2 col-xs-1" (click)="movePage(readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD)"
|
||||
|
@ -131,5 +152,4 @@
|
|||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@ import {
|
|||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component, DestroyRef,
|
||||
ElementRef,
|
||||
ElementRef, EventEmitter,
|
||||
HostListener,
|
||||
inject,
|
||||
Inject,
|
||||
|
@ -16,8 +16,8 @@ import {
|
|||
import { DOCUMENT, Location, NgTemplateOutlet, NgIf, NgStyle, NgClass } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { forkJoin, fromEvent, of, Subject } from 'rxjs';
|
||||
import { catchError, debounceTime, take, takeUntil } from 'rxjs/operators';
|
||||
import { forkJoin, fromEvent, of } from 'rxjs';
|
||||
import { catchError, debounceTime, take } from 'rxjs/operators';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { NavService } from 'src/app/_services/nav.service';
|
||||
|
@ -46,13 +46,20 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
|||
import { TableOfContentsComponent } from '../table-of-contents/table-of-contents.component';
|
||||
import { NgbProgressbar, NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent, NgbNavOutlet, NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { DrawerComponent } from '../../../shared/drawer/drawer.component';
|
||||
import {BookLineOverlayComponent} from "../book-line-overlay/book-line-overlay.component";
|
||||
import {
|
||||
PersonalTableOfContentsComponent,
|
||||
PersonalToCEvent
|
||||
} from "../personal-table-of-contents/personal-table-of-contents.component";
|
||||
|
||||
|
||||
enum TabID {
|
||||
Settings = 1,
|
||||
TableOfContents = 2
|
||||
TableOfContents = 2,
|
||||
PersonalTableOfContents = 3
|
||||
}
|
||||
|
||||
|
||||
interface HistoryPoint {
|
||||
/**
|
||||
* Page Number
|
||||
|
@ -94,7 +101,7 @@ const elementLevelStyles = ['line-height', 'font-family'];
|
|||
])
|
||||
],
|
||||
standalone: true,
|
||||
imports: [NgTemplateOutlet, DrawerComponent, NgIf, NgbProgressbar, NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent, ReaderSettingsComponent, TableOfContentsComponent, NgbNavOutlet, NgStyle, NgClass, NgbTooltip]
|
||||
imports: [NgTemplateOutlet, DrawerComponent, NgIf, NgbProgressbar, NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent, ReaderSettingsComponent, TableOfContentsComponent, NgbNavOutlet, NgStyle, NgClass, NgbTooltip, BookLineOverlayComponent, PersonalTableOfContentsComponent]
|
||||
})
|
||||
export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
|
@ -150,6 +157,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
* Belongs to the drawer component
|
||||
*/
|
||||
activeTabId: TabID = TabID.Settings;
|
||||
/**
|
||||
* Sub Nav tab id
|
||||
*/
|
||||
tocId: TabID = TabID.TableOfContents;
|
||||
/**
|
||||
* Belongs to drawer component
|
||||
*/
|
||||
|
@ -280,6 +291,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
writingStyle: WritingStyle = WritingStyle.Horizontal;
|
||||
|
||||
/**
|
||||
* Used to refresh the Personal PoC
|
||||
*/
|
||||
refreshPToC: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
@ViewChild('bookContainer', {static: false}) bookContainerElemRef!: ElementRef<HTMLDivElement>;
|
||||
|
@ -666,6 +682,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
@HostListener('window:keydown', ['$event'])
|
||||
handleKeyPress(event: KeyboardEvent) {
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
const isInputFocused = activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA';
|
||||
if (isInputFocused) return;
|
||||
|
||||
if (event.key === KEY_CODES.RIGHT_ARROW) {
|
||||
this.movePage(this.readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.FORWARD : PAGING_DIRECTION.BACKWARDS);
|
||||
} else if (event.key === KEY_CODES.LEFT_ARROW) {
|
||||
|
@ -783,6 +803,15 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.loadPage('id("' + event.part + '")');
|
||||
}
|
||||
|
||||
/**
|
||||
* From personal table of contents/bookmark
|
||||
* @param event
|
||||
*/
|
||||
loadChapterPart(event: PersonalToCEvent) {
|
||||
this.setPageNum(event.pageNum);
|
||||
this.loadPage(event.scrollPart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a click handler for any anchors that have 'kavita-page'. If 'kavita-page' present, changes page to kavita-page and optionally passes a part value
|
||||
* from 'kavita-part', which will cause the reader to scroll to the marker.
|
||||
|
@ -987,7 +1016,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
else {
|
||||
this.reader.nativeElement.children
|
||||
// We need to check if we are paging back, because we need to adjust the scroll
|
||||
if (this.pagingDirection === PAGING_DIRECTION.BACKWARDS) {
|
||||
setTimeout(() => this.scrollService.scrollToX(this.bookContentElemRef.nativeElement.scrollWidth, this.bookContentElemRef.nativeElement));
|
||||
|
@ -1213,7 +1241,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
intersectingEntries.sort(this.sortElements);
|
||||
|
||||
if (intersectingEntries.length > 0) {
|
||||
let path = this.getXPathTo(intersectingEntries[0]);
|
||||
let path = this.readerService.getXPathTo(intersectingEntries[0]);
|
||||
if (path === '') { return; }
|
||||
if (!path.startsWith('id')) {
|
||||
path = '//html[1]/' + path;
|
||||
|
@ -1339,35 +1367,14 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
getElementFromXPath(path: string) {
|
||||
const node = this.document.evaluate(path, this.document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
const node = this.document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
||||
if (node?.nodeType === Node.ELEMENT_NODE) {
|
||||
return node as Element;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getXPathTo(element: any): string {
|
||||
if (element === null) return '';
|
||||
if (element.id !== '') { return 'id("' + element.id + '")'; }
|
||||
if (element === this.document.body) { return element.tagName; }
|
||||
|
||||
|
||||
let ix = 0;
|
||||
const siblings = element.parentNode?.childNodes || [];
|
||||
for (let sibling of siblings) {
|
||||
if (sibling === element) {
|
||||
return this.getXPathTo(element.parentNode) + '/' + element.tagName + '[' + (ix + 1) + ']';
|
||||
}
|
||||
if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
|
||||
ix++;
|
||||
}
|
||||
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off Incognito mode. This can only happen once if the user clicks the icon. This will modify URL state
|
||||
*/
|
||||
|
@ -1583,4 +1590,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.mousePosition.x = $event.screenX;
|
||||
this.mousePosition.y = $event.screenY;
|
||||
}
|
||||
|
||||
refreshPersonalToC() {
|
||||
this.refreshPToC.emit();
|
||||
}
|
||||
|
||||
protected readonly undefined = undefined;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<div class="table-of-contents">
|
||||
<div *ngIf="Pages.length === 0">
|
||||
<em>Nothing Bookmarked yet</em>
|
||||
</div>
|
||||
<ul>
|
||||
<li *ngFor="let page of Pages">
|
||||
<span (click)="loadChapterPage(page, '')">Page {{page}}</span>
|
||||
<ul class="chapter-title">
|
||||
<li class="ellipsis"
|
||||
[ngbTooltip]="bookmark.title"
|
||||
placement="right"
|
||||
*ngFor="let bookmark of bookmarks[page]" (click)="loadChapterPage(bookmark.pageNumber, bookmark.bookScrollId); $event.stopPropagation();">
|
||||
{{bookmark.title}}
|
||||
<button class="btn btn-icon ms-1" (click)="removeBookmark(bookmark); $event.stopPropagation();">
|
||||
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Delete {{bookmark.title}}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
|
@ -0,0 +1,15 @@
|
|||
.table-of-contents li {
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.chapter-title {
|
||||
padding-inline-start: 1rem;
|
||||
}
|
||||
.ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component, DestroyRef, EventEmitter,
|
||||
Inject,
|
||||
inject,
|
||||
Input,
|
||||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import {CommonModule, DOCUMENT} from '@angular/common';
|
||||
import {ReaderService} from "../../../_services/reader.service";
|
||||
import {PersonalToC} from "../../../_models/readers/personal-toc";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
|
||||
export interface PersonalToCEvent {
|
||||
pageNum: number;
|
||||
scrollPart: string | undefined;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-personal-table-of-contents',
|
||||
standalone: true,
|
||||
imports: [CommonModule, NgbTooltip],
|
||||
templateUrl: './personal-table-of-contents.component.html',
|
||||
styleUrls: ['./personal-table-of-contents.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class PersonalTableOfContentsComponent implements OnInit {
|
||||
|
||||
@Input({required: true}) chapterId!: number;
|
||||
@Input({required: true}) pageNum: number = 0;
|
||||
@Input({required: true}) tocRefresh!: EventEmitter<void>;
|
||||
@Output() loadChapter: EventEmitter<PersonalToCEvent> = new EventEmitter();
|
||||
|
||||
private readonly readerService = inject(ReaderService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
|
||||
bookmarks: {[key: number]: Array<PersonalToC>} = [];
|
||||
|
||||
get Pages() {
|
||||
return Object.keys(this.bookmarks).map(p => parseInt(p, 10));
|
||||
}
|
||||
|
||||
constructor(@Inject(DOCUMENT) private document: Document) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.tocRefresh.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||
this.load();
|
||||
});
|
||||
|
||||
this.load();
|
||||
}
|
||||
|
||||
load() {
|
||||
this.readerService.getPersonalToC(this.chapterId).subscribe(res => {
|
||||
res.forEach(t => {
|
||||
if (!this.bookmarks.hasOwnProperty(t.pageNumber)) {
|
||||
this.bookmarks[t.pageNumber] = [];
|
||||
}
|
||||
this.bookmarks[t.pageNumber].push(t);
|
||||
})
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
loadChapterPage(pageNum: number, scrollPart: string | undefined) {
|
||||
this.loadChapter.emit({pageNum, scrollPart});
|
||||
}
|
||||
|
||||
removeBookmark(bookmark: PersonalToC) {
|
||||
this.readerService.removePersonalToc(bookmark.chapterId, bookmark.pageNumber, bookmark.title).subscribe(() => {
|
||||
this.bookmarks[bookmark.pageNumber] = this.bookmarks[bookmark.pageNumber].filter(t => t.title != bookmark.title);
|
||||
|
||||
if (this.bookmarks[bookmark.pageNumber].length === 0) {
|
||||
delete this.bookmarks[bookmark.pageNumber];
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { BookChapterItem } from '../../_models/book-chapter-item';
|
||||
import { NgIf, NgFor } from '@angular/common';
|
||||
|
||||
|
@ -11,7 +10,7 @@ import { NgIf, NgFor } from '@angular/common';
|
|||
standalone: true,
|
||||
imports: [NgIf, NgFor]
|
||||
})
|
||||
export class TableOfContentsComponent implements OnDestroy {
|
||||
export class TableOfContentsComponent {
|
||||
|
||||
@Input({required: true}) chapterId!: number;
|
||||
@Input({required: true}) pageNum!: number;
|
||||
|
@ -20,17 +19,8 @@ export class TableOfContentsComponent implements OnDestroy {
|
|||
|
||||
@Output() loadChapter: EventEmitter<{pageNum: number, part: string}> = new EventEmitter();
|
||||
|
||||
private onDestroy: Subject<void> = new Subject();
|
||||
|
||||
pageAnchors: {[n: string]: number } = {};
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
cleanIdSelector(id: string) {
|
||||
const tokens = id.split('/');
|
||||
if (tokens.length > 0) {
|
||||
|
|
|
@ -5,12 +5,6 @@ import { environment } from 'src/environments/environment';
|
|||
import { BookChapterItem } from '../_models/book-chapter-item';
|
||||
import { BookInfo } from '../_models/book-info';
|
||||
|
||||
export interface BookPage {
|
||||
bookTitle: string;
|
||||
styles: string;
|
||||
html: string;
|
||||
}
|
||||
|
||||
export interface FontFamily {
|
||||
/**
|
||||
* What the user should see
|
||||
|
@ -32,7 +26,7 @@ export class BookService {
|
|||
constructor(private http: HttpClient) { }
|
||||
|
||||
getFontFamilies(): Array<FontFamily> {
|
||||
return [{title: 'default', family: 'default'}, {title: 'EBGaramond', family: 'EBGaramond'}, {title: 'Fira Sans', family: 'Fira_Sans'},
|
||||
return [{title: 'default', family: 'default'}, {title: 'EBGaramond', family: 'EBGaramond'}, {title: 'Fira Sans', family: 'Fira_Sans'},
|
||||
{title: 'Lato', family: 'Lato'}, {title: 'Libre Baskerville', family: 'Libre_Baskerville'}, {title: 'Merriweather', family: 'Merriweather'},
|
||||
{title: 'Nanum Gothic', family: 'Nanum_Gothic'}, {title: 'RocknRoll One', family: 'RocknRoll_One'}, {title: 'Open Dyslexic', family: 'OpenDyslexic2'}];
|
||||
}
|
||||
|
|
|
@ -9,16 +9,16 @@
|
|||
|
||||
<div class="offcanvas-body pb-3">
|
||||
<div class="d-flex">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="vertical" style="max-width: 135px;">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="vertical" style="max-width: 135px;">
|
||||
<li [ngbNavItem]="tabs[TabID.General]">
|
||||
<a ngbNavLink>General</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="container-fluid" style="overflow: auto">
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="d-none d-md-block col-md-2 col-lg-1">
|
||||
<div class="d-none d-md-block col-md-2 col-lg-1">
|
||||
<app-image class="me-2" width="74px" [imageUrl]="coverImageUrl"></app-image>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-10 col-lg-11">
|
||||
<ng-container *ngIf="summary.length > 0; else noSummary">
|
||||
<app-read-more [text]="summary" [maxLength]="250"></app-read-more>
|
||||
|
@ -28,8 +28,8 @@
|
|||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-entity-info-cards [entity]="data"></app-entity-info-cards>
|
||||
|
||||
<app-entity-info-cards [entity]="data" [libraryId]="libraryId"></app-entity-info-cards>
|
||||
|
||||
|
||||
<!-- 2 rows to show some tags-->
|
||||
|
@ -41,7 +41,7 @@
|
|||
<app-badge-expander [items]="chapterMetadata.writers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
@ -51,7 +51,7 @@
|
|||
<app-badge-expander [items]="chapterMetadata.genres">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge>{{item.title}}</app-tag-badge>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
@ -63,7 +63,7 @@
|
|||
<app-badge-expander [items]="chapterMetadata.publishers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
@ -73,7 +73,7 @@
|
|||
<app-badge-expander [items]="chapterMetadata.tags">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge>{{item.title}}</app-tag-badge>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
@ -98,7 +98,7 @@
|
|||
<li [ngbNavItem]="tabs[TabID.Cover]" [disabled]="(isAdmin$ | async) === false">
|
||||
<a ngbNavLink>{{tabs[TabID.Cover].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls"
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls"
|
||||
[showReset]="chapter.coverImageLocked"
|
||||
[showApplyButton]="true"
|
||||
(applyCover)="applyCoverImage($event)"
|
||||
|
@ -121,7 +121,7 @@
|
|||
<h5 class="mt-0 mb-1">
|
||||
<span >
|
||||
<span>
|
||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions"
|
||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions"
|
||||
[labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
||||
<ng-container *ngIf="chapter.number !== '0'; else specialHeader">
|
||||
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
||||
|
@ -143,7 +143,7 @@
|
|||
Pages: {{file.pages | number:''}}
|
||||
</div>
|
||||
<div class="col" *ngIf="data.hasOwnProperty('created')">
|
||||
Added:
|
||||
Added:
|
||||
<!-- TODO: This data.created can be removed after v0.5.5 release -->
|
||||
<ng-container *ngIf="file.created === '0001-01-01T00:00:00'; else fileDate">
|
||||
{{data.created | date: 'short' | defaultDate}}
|
||||
|
@ -166,4 +166,4 @@
|
|||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="tab-content {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'mt-3' : 'ms-4 flex-fill'}}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,107 +1,121 @@
|
|||
<div class="row g-0 mt-4 mb-3">
|
||||
<ng-container *ngIf="chapter !== undefined && chapter.releaseDate && (chapter.releaseDate | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Release Date" [clickable]="false" fontClasses="fa-regular fa-calendar" title="Release">
|
||||
{{chapter.releaseDate | date:'shortDate' | defaultDate}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.ageRating !== AgeRating.Unknown">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Age Rating" [clickable]="false" fontClasses="fas fa-eye" title="Age Rating">
|
||||
{{chapter.ageRating | ageRating | async}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
<div class="mt-4 mb-3">
|
||||
<div class="row g-0" *ngIf="chapterMetadata ">
|
||||
<!-- Tags and Characters are used a lot of Hentai and Doujinshi type content, so showing in list item has value add on first glance -->
|
||||
<app-metadata-detail [tags]="chapterMetadata.tags" [libraryId]="libraryId" [queryParam]="FilterQueryParam.Tags" heading="Tags">
|
||||
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<ng-container *ngIf="totalPages > 0">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Length" [clickable]="false" fontClasses="fa-regular fa-file-lines" title="Pages">
|
||||
{{totalPages | compactNumber}} Pages
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
<app-metadata-detail [tags]="chapterMetadata.characters" [libraryId]="libraryId" [queryParam]="FilterQueryParam.Character" heading="Characters">
|
||||
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
|
||||
</app-metadata-detail>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="chapter.files[0].format === MangaFormat.EPUB && totalWordCount > 0">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Length" [clickable]="false" fontClasses="fa-solid fa-book-open">
|
||||
{{totalWordCount | compactNumber}} Words
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
<div class="row g-0">
|
||||
<ng-container *ngIf="chapter !== undefined && chapter.releaseDate && (chapter.releaseDate | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Release Date" [clickable]="false" fontClasses="fa-regular fa-calendar" title="Release">
|
||||
{{chapter.releaseDate | date:'shortDate' | defaultDate}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.files[0].format === MangaFormat.EPUB && totalWordCount > 0 || chapter.files[0].format !== MangaFormat.EPUB">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Read Time" [clickable]="false" fontClasses="fa-regular fa-clock">
|
||||
<ng-container *ngIf="readingTime.maxHours === 0 || readingTime.minHours === 0; else normalReadTime"><1 Hour</ng-container>
|
||||
<ng-template #normalReadTime>
|
||||
{{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} Hour{{readingTime.minHours > 1 ? 's' : ''}}
|
||||
</ng-template>
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="chapter.ageRating !== AgeRating.Unknown">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Age Rating" [clickable]="false" fontClasses="fas fa-eye" title="Age Rating">
|
||||
{{chapter.ageRating | ageRating | async}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="showExtendedProperties && chapter.created && chapter.created !== '' && (chapter.created | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="Date Added" [clickable]="false" fontClasses="fa-solid fa-file-import" title="Date Added">
|
||||
{{chapter.created | date:'short' | defaultDate}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="totalPages > 0">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Length" [clickable]="false" fontClasses="fa-regular fa-file-lines" title="Pages">
|
||||
{{totalPages | compactNumber}} Pages
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="showExtendedProperties && size > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="Size" [clickable]="false" fontClasses="fa-solid fa-scale-unbalanced" title="ID">
|
||||
{{size | bytes}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="chapter.files[0].format === MangaFormat.EPUB && totalWordCount > 0">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Length" [clickable]="false" fontClasses="fa-solid fa-book-open">
|
||||
{{totalWordCount | compactNumber}} Words
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="showExtendedProperties">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="ID" [clickable]="false" fontClasses="fa-solid fa-fingerprint" title="ID">
|
||||
{{entity.id}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<ng-container *ngIf="WebLinks.length > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="Links" [clickable]="false" fontClasses="fa-solid fa-link" title="Links">
|
||||
<a class="me-1" [href]="link | safeHtml" *ngFor="let link of WebLinks" target="_blank" rel="noopener noreferrer" [title]="link">
|
||||
<img width="24" height="24" #img class="lazyload img-placeholder"
|
||||
src="data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
|
||||
[attr.data-src]="imageService.getWebLinkImage(link)"
|
||||
(error)="imageService.updateErroredWebLinkImage($event)"
|
||||
aria-hidden="true" alt="">
|
||||
</a>
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="chapter.files[0].format === MangaFormat.EPUB && totalWordCount > 0 || chapter.files[0].format !== MangaFormat.EPUB">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Read Time" [clickable]="false" fontClasses="fa-regular fa-clock">
|
||||
<ng-container *ngIf="readingTime.maxHours === 0 || readingTime.minHours === 0; else normalReadTime"><1 Hour</ng-container>
|
||||
<ng-template #normalReadTime>
|
||||
{{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} Hour{{readingTime.minHours > 1 ? 's' : ''}}
|
||||
</ng-template>
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.isbn.length > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="ISBN" [clickable]="false" fontClasses="fa-solid fa-barcode" title="ISBN">
|
||||
{{chapter.isbn}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="(chapter.lastReadingProgress | date: 'shortDate') !== '1/1/01'">
|
||||
<ng-container *ngIf="showExtendedProperties && chapter.created && chapter.created !== '' && (chapter.created | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="Last Read" [clickable]="false" fontClasses="fa-regular fa-clock" [ngbTooltip]="chapter.lastReadingProgress | date: 'medium'">
|
||||
{{chapter.lastReadingProgress | date: 'shortDate'}}
|
||||
</app-icon-and-title>
|
||||
<app-icon-and-title label="Date Added" [clickable]="false" fontClasses="fa-solid fa-file-import" title="Date Added">
|
||||
{{chapter.created | date:'short' | defaultDate}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="showExtendedProperties && size > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="Size" [clickable]="false" fontClasses="fa-solid fa-scale-unbalanced" title="ID">
|
||||
{{size | bytes}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="showExtendedProperties">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="ID" [clickable]="false" fontClasses="fa-solid fa-fingerprint" title="ID">
|
||||
{{entity.id}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<ng-container *ngIf="WebLinks.length > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="Links" [clickable]="false" fontClasses="fa-solid fa-link" title="Links">
|
||||
<a class="me-1" [href]="link | safeHtml" *ngFor="let link of WebLinks" target="_blank" rel="noopener noreferrer" [title]="link">
|
||||
<img width="24" height="24" #img class="lazyload img-placeholder"
|
||||
src="data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
|
||||
[attr.data-src]="imageService.getWebLinkImage(link)"
|
||||
(error)="imageService.updateErroredWebLinkImage($event)"
|
||||
aria-hidden="true" alt="">
|
||||
</a>
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.isbn.length > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="ISBN" [clickable]="false" fontClasses="fa-solid fa-barcode" title="ISBN">
|
||||
{{chapter.isbn}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="(chapter.lastReadingProgress | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="Last Read" [clickable]="false" fontClasses="fa-regular fa-clock" [ngbTooltip]="chapter.lastReadingProgress | date: 'medium'">
|
||||
{{chapter.lastReadingProgress | date: 'shortDate'}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -24,11 +24,13 @@ import {BytesPipe} from "../../pipe/bytes.pipe";
|
|||
import {CompactNumberPipe} from "../../pipe/compact-number.pipe";
|
||||
import {AgeRatingPipe} from "../../pipe/age-rating.pipe";
|
||||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {MetadataDetailComponent} from "../../series-detail/_components/metadata-detail/metadata-detail.component";
|
||||
import {FilterQueryParam} from "../../shared/_services/filter-utilities.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-entity-info-cards',
|
||||
standalone: true,
|
||||
imports: [CommonModule, IconAndTitleComponent, SafeHtmlPipe, DefaultDatePipe, BytesPipe, CompactNumberPipe, AgeRatingPipe, NgbTooltip],
|
||||
imports: [CommonModule, IconAndTitleComponent, SafeHtmlPipe, DefaultDatePipe, BytesPipe, CompactNumberPipe, AgeRatingPipe, NgbTooltip, MetadataDetailComponent],
|
||||
templateUrl: './entity-info-cards.component.html',
|
||||
styleUrls: ['./entity-info-cards.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
@ -36,6 +38,7 @@ import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
|||
export class EntityInfoCardsComponent implements OnInit {
|
||||
|
||||
@Input({required: true}) entity!: Volume | Chapter;
|
||||
@Input({required: true}) libraryId!: number;
|
||||
/**
|
||||
* This will pull extra information
|
||||
*/
|
||||
|
@ -75,8 +78,6 @@ export class EntityInfoCardsComponent implements OnInit {
|
|||
return this.chapter.webLinks.split(',');
|
||||
}
|
||||
|
||||
|
||||
|
||||
constructor(private utilityService: UtilityService, private seriesService: SeriesService, private readonly cdRef: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -127,8 +128,5 @@ export class EntityInfoCardsComponent implements OnInit {
|
|||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
getTimezone(timezone: string): string {
|
||||
const localDate = new Date(timezone);
|
||||
return localDate.toLocaleString('en-US', { timeZoneName: 'short' }).split(' ')[3];
|
||||
}
|
||||
protected readonly FilterQueryParam = FilterQueryParam;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<span class="d-none d-sm-inline-block">Read</span>
|
||||
</button>
|
||||
</h5>
|
||||
<!-- This isn't perfect, but it might work. TODO: Polish this-->
|
||||
|
||||
<h6 class="text-muted" [ngClass]="{'subtitle-with-actionables' : actions.length > 0}" *ngIf="Title !== '' && showTitle">{{Title}}</h6>
|
||||
<ng-container *ngIf="summary.length > 0">
|
||||
<div class="mt-2 ps-2">
|
||||
|
@ -30,7 +30,7 @@
|
|||
</div>
|
||||
</ng-container>
|
||||
<div class="ps-2 d-none d-md-inline-block">
|
||||
<app-entity-info-cards [entity]="entity" [showExtendedProperties]="false"></app-entity-info-cards>
|
||||
<app-entity-info-cards [entity]="entity" [libraryId]="libraryId" [includeMetadata]="ShowExtended" [showExtendedProperties]="ShowExtended"></app-entity-info-cards>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,15 +5,14 @@ import {
|
|||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { map, Observable, Subject, takeUntil } from 'rxjs';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { Download } from 'src/app/shared/_models/download';
|
||||
import { DownloadEvent, DownloadService } from 'src/app/shared/_services/download.service';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { LibraryType } from 'src/app/_models/library';
|
||||
import { RelationKind } from 'src/app/_models/series-detail/relation-kind';
|
||||
|
@ -42,6 +41,7 @@ export class ListItemComponent implements OnInit {
|
|||
* Volume or Chapter to render
|
||||
*/
|
||||
@Input({required: true}) entity!: Volume | Chapter;
|
||||
@Input({required: true}) libraryId!: number;
|
||||
/**
|
||||
* Image to show
|
||||
*/
|
||||
|
@ -103,8 +103,14 @@ export class ListItemComponent implements OnInit {
|
|||
return '';
|
||||
}
|
||||
|
||||
get ShowExtended() {
|
||||
return this.utilityService.getActiveBreakpoint() === Breakpoint.Desktop;
|
||||
}
|
||||
|
||||
constructor(private utilityService: UtilityService, private downloadService: DownloadService,
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
|
||||
|
||||
constructor(public utilityService: UtilityService, private downloadService: DownloadService,
|
||||
private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
|
|
@ -72,6 +72,7 @@ export class DashboardComponent implements OnInit {
|
|||
|
||||
|
||||
this.seriesService.getSeries(seriesAddedEvent.seriesId).subscribe(series => {
|
||||
if (this.recentlyAddedSeries.filter(s => s.id === series.id).length > 0) return;
|
||||
this.recentlyAddedSeries = [series, ...this.recentlyAddedSeries];
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
|
|
@ -9,8 +9,8 @@ import {
|
|||
OnInit
|
||||
} from '@angular/core';
|
||||
import { NgbModal, NgbModalRef, NgbPopover } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
|
||||
import { map, shareReplay, takeUntil } from 'rxjs/operators';
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||
import { map, shareReplay } from 'rxjs/operators';
|
||||
import { ConfirmConfig } from 'src/app/shared/confirm-dialog/_models/confirm-config';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { UpdateNotificationModalComponent } from 'src/app/shared/update-notification/update-notification-modal.component';
|
||||
|
@ -79,7 +79,7 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(event => {
|
||||
this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event: Message<NotificationProgressEvent>) => {
|
||||
if (event.event === EVENTS.NotificationProgress) {
|
||||
this.processNotificationProgressEvent(event);
|
||||
} else if (event.event === EVENTS.Error) {
|
||||
|
@ -94,6 +94,9 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
|||
this.infoSource.next(values);
|
||||
this.activeEvents += 1;
|
||||
this.cdRef.markForCheck();
|
||||
} else if (event.event === EVENTS.UpdateAvailable) {
|
||||
console.log('event: ', event);
|
||||
this.handleUpdateAvailableClick(event.payload);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -150,10 +153,15 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
|
||||
handleUpdateAvailableClick(message: NotificationProgressEvent) {
|
||||
handleUpdateAvailableClick(message: NotificationProgressEvent | UpdateVersionEvent) {
|
||||
if (this.updateNotificationModalRef != null) { return; }
|
||||
this.updateNotificationModalRef = this.modalService.open(UpdateNotificationModalComponent, { scrollable: true, size: 'lg' });
|
||||
this.updateNotificationModalRef.componentInstance.updateData = message.body as UpdateVersionEvent;
|
||||
if (message.hasOwnProperty('body')) {
|
||||
this.updateNotificationModalRef.componentInstance.updateData = (message as NotificationProgressEvent).body as UpdateVersionEvent;
|
||||
} else {
|
||||
this.updateNotificationModalRef.componentInstance.updateData = message as UpdateVersionEvent;
|
||||
}
|
||||
|
||||
this.updateNotificationModalRef.closed.subscribe(() => {
|
||||
this.updateNotificationModalRef = null;
|
||||
});
|
||||
|
@ -176,7 +184,7 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
config.header = event.title;
|
||||
config.content = event.subTitle;
|
||||
var result = await this.confirmService.alert(event.subTitle || event.title, config);
|
||||
const result = await this.confirmService.alert(event.subTitle || event.title, config);
|
||||
if (result) {
|
||||
this.removeErrorOrInfo(event);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ export class ProviderImagePipe implements PipeTransform {
|
|||
return 'assets/images/ExternalServices/AniList.png';
|
||||
case ScrobbleProvider.Mal:
|
||||
return 'assets/images/ExternalServices/MAL.png';
|
||||
case ScrobbleProvider.GoogleBooks:
|
||||
return 'assets/images/ExternalServices/GoogleBooks.png';
|
||||
case ScrobbleProvider.Kavita:
|
||||
return 'assets/images/logo-32.png';
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import { NgIf, NgTemplateOutlet } from '@angular/common';
|
|||
import { SplashContainerComponent } from '../splash-container/splash-container.component';
|
||||
|
||||
/**
|
||||
* This is exclusivly used to register the first user on the server and nothing else
|
||||
* This is exclusively used to register the first user on the server and nothing else
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-register',
|
||||
|
@ -28,9 +28,9 @@ export class RegisterComponent {
|
|||
password: new FormControl('', [Validators.required, Validators.maxLength(32), Validators.minLength(6), Validators.pattern("^.{6,32}$")]),
|
||||
});
|
||||
|
||||
constructor(private router: Router, private accountService: AccountService,
|
||||
constructor(private router: Router, private accountService: AccountService,
|
||||
private toastr: ToastrService, private memberService: MemberService) {
|
||||
|
||||
|
||||
this.memberService.adminExists().pipe(take(1)).subscribe(adminExists => {
|
||||
if (adminExists) {
|
||||
this.router.navigateByUrl('login');
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
popoverTitle="Your Rating + Overall" popoverClass="md-popover">
|
||||
<span class="badge rounded-pill me-1">
|
||||
<img class="me-1" ngSrc="assets/images/logo-32.png" width="24" height="24" alt="">
|
||||
{{userRating * 20}}
|
||||
<ng-container *ngIf="overallRating > 0; else noOverallRating"> + {{overallRating}}%</ng-container>
|
||||
<ng-template #noOverallRating>%</ng-template>
|
||||
<ng-container *ngIf="hasUserRated; else notYetRated">{{userRating * 20}}</ng-container>
|
||||
<ng-template #notYetRated>N/A</ng-template>
|
||||
<ng-container *ngIf="overallRating > 0"> + {{overallRating}}</ng-container>
|
||||
<ng-container *ngIf="hasUserRated || overallRating > 0">%</ng-container>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
@ -22,13 +23,12 @@
|
|||
</div>
|
||||
|
||||
<ng-template #popContent>
|
||||
<ngb-rating class="rating-star" [(rate)]="userRating" (rateChange)="updateRating($event)" [resettable]="false">
|
||||
<ng-template let-fill="fill" let-index="index">
|
||||
<span class="star" [class.filled]="(index < userRating) && userRating > 0">★</span>
|
||||
</ng-template>
|
||||
</ngb-rating> {{userRating * 20}}%
|
||||
<ngx-stars [initialStars]="userRating" (ratingOutput)="updateRating($event)"
|
||||
[maxStars]="5" [color]="starColor"></ngx-stars>
|
||||
{{userRating * 20}}%
|
||||
</ng-template>
|
||||
|
||||
<ng-template #externalPopContent let-rating="rating">
|
||||
<i class="fa-solid fa-heart" aria-hidden="true"></i> {{rating.favoriteCount}}
|
||||
<div><i class="fa-solid fa-heart" aria-hidden="true"></i> {{rating.favoriteCount}}</div>
|
||||
<a *ngIf="rating.providerUrl" [href]="rating.providerUrl" target="_blank" rel="noreferrer nofollow">Entry</a>
|
||||
</ng-template>
|
||||
|
|
|
@ -19,4 +19,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
.rating-star {
|
||||
i {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding-right: 0.1rem;
|
||||
color: #d3d3d3;
|
||||
}
|
||||
|
||||
.filled {
|
||||
color: var(--primary-color);
|
||||
|
||||
}
|
||||
}
|
||||
::ng-deep .star {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
|
|
@ -16,11 +16,13 @@ import {LoadingComponent} from "../../../shared/loading/loading.component";
|
|||
import {AccountService} from "../../../_services/account.service";
|
||||
import {LibraryType} from "../../../_models/library";
|
||||
import {ProviderNamePipe} from "../../../pipe/provider-name.pipe";
|
||||
import {NgxStarsModule} from "ngx-stars";
|
||||
import {ThemeService} from "../../../_services/theme.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-external-rating',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ProviderImagePipe, NgOptimizedImage, NgbRating, NgbPopover, LoadingComponent, ProviderNamePipe],
|
||||
imports: [CommonModule, ProviderImagePipe, NgOptimizedImage, NgbRating, NgbPopover, LoadingComponent, ProviderNamePipe, NgxStarsModule],
|
||||
templateUrl: './external-rating.component.html',
|
||||
styleUrls: ['./external-rating.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
@ -29,15 +31,19 @@ import {ProviderNamePipe} from "../../../pipe/provider-name.pipe";
|
|||
export class ExternalRatingComponent implements OnInit {
|
||||
@Input({required: true}) seriesId!: number;
|
||||
@Input({required: true}) userRating!: number;
|
||||
@Input({required: true}) hasUserRated!: boolean;
|
||||
@Input({required: true}) libraryType!: LibraryType;
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly seriesService = inject(SeriesService);
|
||||
private readonly accountService = inject(AccountService);
|
||||
private readonly themeService = inject(ThemeService);
|
||||
|
||||
ratings: Array<Rating> = [];
|
||||
isLoading: boolean = false;
|
||||
overallRating: number = -1;
|
||||
|
||||
starColor = this.themeService.getCssVariable('--rating-star-color');
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
|
@ -58,9 +64,11 @@ export class ExternalRatingComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
updateRating(rating: any) {
|
||||
updateRating(rating: number) {
|
||||
this.seriesService.updateRating(this.seriesId, rating).subscribe(() => {
|
||||
this.userRating = rating;
|
||||
this.hasUserRated = true;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<div class="row g-0 mb-1" *ngIf="tags.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>{{heading}}</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="tags">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<ng-container *ngIf="itemTemplate; else useTitle">
|
||||
<span (click)="goTo(queryParam, item.id)">
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: position }"></ng-container>
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-template #useTitle>
|
||||
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(queryParam, item.id)" [selectionMode]="TagBadgeCursor.Clickable">
|
||||
<ng-container [ngTemplateOutlet]="titleTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: position }"></ng-container>
|
||||
</app-tag-badge>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,37 @@
|
|||
import {ChangeDetectionStrategy, Component, ContentChild, inject, Input, TemplateRef} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {A11yClickDirective} from "../../../shared/a11y-click.directive";
|
||||
import {BadgeExpanderComponent} from "../../../shared/badge-expander/badge-expander.component";
|
||||
import {TagBadgeComponent, TagBadgeCursor} from "../../../shared/tag-badge/tag-badge.component";
|
||||
import {FilterQueryParam} from "../../../shared/_services/filter-utilities.service";
|
||||
import {Router} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'app-metadata-detail',
|
||||
standalone: true,
|
||||
imports: [CommonModule, A11yClickDirective, BadgeExpanderComponent, TagBadgeComponent],
|
||||
templateUrl: './metadata-detail.component.html',
|
||||
styleUrls: ['./metadata-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class MetadataDetailComponent {
|
||||
|
||||
@Input({required: true}) tags: Array<any> = [];
|
||||
@Input({required: true}) libraryId!: number;
|
||||
@Input({required: true}) heading!: string;
|
||||
@Input() queryParam: FilterQueryParam = FilterQueryParam.None;
|
||||
@ContentChild('titleTemplate') titleTemplate!: TemplateRef<any>;
|
||||
@ContentChild('itemTemplate') itemTemplate?: TemplateRef<any>;
|
||||
|
||||
private readonly router = inject(Router);
|
||||
protected readonly TagBadgeCursor = TagBadgeCursor;
|
||||
|
||||
|
||||
goTo(queryParamName: FilterQueryParam, filter: any) {
|
||||
if (queryParamName === FilterQueryParam.None) return;
|
||||
let params: any = {};
|
||||
params[queryParamName] = filter;
|
||||
params[FilterQueryParam.Page] = 1;
|
||||
this.router.navigate(['library', this.libraryId], {queryParams: params});
|
||||
}
|
||||
}
|
|
@ -174,7 +174,7 @@
|
|||
<ng-template #storylineListLayout>
|
||||
<ng-container *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackByStoryLineIdentity">
|
||||
<ng-container *ngIf="!item.isChapter; else chapterListItem">
|
||||
<app-list-item [imageUrl]="imageService.getVolumeCoverImage(item.volume.id)"
|
||||
<app-list-item [imageUrl]="imageService.getVolumeCoverImage(item.volume.id)" [libraryId]="libraryId"
|
||||
[seriesName]="series.name" [entity]="item.volume" *ngIf="item.volume.number !== 0"
|
||||
[actions]="volumeActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
||||
[pagesRead]="item.volume.pagesRead" [totalPages]="item.volume.pages" (read)="openVolume(item.volume)"
|
||||
|
@ -185,7 +185,7 @@
|
|||
</app-list-item>
|
||||
</ng-container>
|
||||
<ng-template #chapterListItem>
|
||||
<app-list-item [imageUrl]="imageService.getChapterCoverImage(item.chapter.id)"
|
||||
<app-list-item [imageUrl]="imageService.getChapterCoverImage(item.chapter.id)" [libraryId]="libraryId"
|
||||
[seriesName]="series.name" [entity]="item.chapter" *ngIf="!item.chapter.isSpecial"
|
||||
[actions]="chapterActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
||||
[pagesRead]="item.chapter.pagesRead" [totalPages]="item.chapter.pages" (read)="openChapter(item.chapter)"
|
||||
|
@ -219,7 +219,7 @@
|
|||
</ng-container>
|
||||
<ng-template #volumeListLayout>
|
||||
<ng-container *ngFor="let volume of scroll.viewPortItems; let idx = index; trackBy: trackByVolumeIdentity">
|
||||
<app-list-item [imageUrl]="imageService.getVolumeCoverImage(volume.id)"
|
||||
<app-list-item [imageUrl]="imageService.getVolumeCoverImage(volume.id)" [libraryId]="libraryId"
|
||||
[seriesName]="series.name" [entity]="volume"
|
||||
[actions]="volumeActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
||||
[pagesRead]="volume.pagesRead" [totalPages]="volume.pages" (read)="openVolume(volume)"
|
||||
|
@ -256,7 +256,7 @@
|
|||
</ng-container>
|
||||
<ng-template #chapterListLayout>
|
||||
<div *ngFor="let chapter of scroll.viewPortItems; let idx = index; trackBy: trackByChapterIdentity">
|
||||
<app-list-item [imageUrl]="imageService.getChapterCoverImage(chapter.id)"
|
||||
<app-list-item [imageUrl]="imageService.getChapterCoverImage(chapter.id)" [libraryId]="libraryId"
|
||||
[seriesName]="series.name" [entity]="chapter" *ngIf="!chapter.isSpecial"
|
||||
[actions]="chapterActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
||||
[pagesRead]="chapter.pagesRead" [totalPages]="chapter.pages" (read)="openChapter(chapter)"
|
||||
|
@ -290,7 +290,7 @@
|
|||
</ng-container>
|
||||
<ng-template #specialListLayout>
|
||||
<ng-container *ngFor="let chapter of scroll.viewPortItems; let idx = index; trackBy: trackByChapterIdentity">
|
||||
<app-list-item [imageUrl]="imageService.getChapterCoverImage(chapter.id)"
|
||||
<app-list-item [imageUrl]="imageService.getChapterCoverImage(chapter.id)" [libraryId]="libraryId"
|
||||
[seriesName]="series.name" [entity]="chapter"
|
||||
[actions]="chapterActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
||||
[pagesRead]="chapter.pagesRead" [totalPages]="chapter.pages" (read)="openChapter(chapter)"
|
||||
|
|
|
@ -71,7 +71,7 @@ import { TagBadgeComponent } from '../../../shared/tag-badge/tag-badge.component
|
|||
import { CardActionablesComponent } from '../../../cards/card-item/card-actionables/card-actionables.component';
|
||||
import { SideNavCompanionBarComponent } from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
|
||||
|
||||
interface RelatedSeris {
|
||||
interface RelatedSeriesPair {
|
||||
series: Series;
|
||||
relation: RelationKind;
|
||||
}
|
||||
|
@ -153,7 +153,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
* Track by function for Chapter to tell when to refresh card data
|
||||
*/
|
||||
trackByChapterIdentity = (index: number, item: Chapter) => `${item.title}_${item.number}_${item.volumeId}_${item.pagesRead}`;
|
||||
trackByRelatedSeriesIdentify = (index: number, item: RelatedSeris) => `${item.series.name}_${item.series.libraryId}_${item.series.pagesRead}_${item.relation}`;
|
||||
trackByRelatedSeriesIdentify = (index: number, item: RelatedSeriesPair) => `${item.series.name}_${item.series.libraryId}_${item.series.pagesRead}_${item.relation}`;
|
||||
trackBySeriesIdentify = (index: number, item: Series) => `${item.name}_${item.libraryId}_${item.pagesRead}`;
|
||||
trackByStoryLineIdentity = (index: number, item: StoryLineItem) => {
|
||||
if (item.isChapter) {
|
||||
|
@ -174,7 +174,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
/**
|
||||
* Related Series. Sorted by backend
|
||||
*/
|
||||
relations: Array<RelatedSeris> = [];
|
||||
relations: Array<RelatedSeriesPair> = [];
|
||||
/**
|
||||
* Recommended Series
|
||||
*/
|
||||
|
@ -342,7 +342,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
this.libraryId = parseInt(libraryId, 10);
|
||||
this.seriesImage = this.imageService.getSeriesCoverImage(this.seriesId);
|
||||
this.cdRef.markForCheck();
|
||||
this.loadSeries(this.seriesId);
|
||||
this.loadSeries(this.seriesId, true);
|
||||
|
||||
this.pageExtrasGroup.get('renderMode')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((val: PageLayoutMode | null) => {
|
||||
if (val == null) return;
|
||||
|
@ -493,7 +493,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
});
|
||||
}
|
||||
|
||||
loadSeries(seriesId: number) {
|
||||
loadSeries(seriesId: number, loadExternal: boolean = false) {
|
||||
this.seriesService.getMetadata(seriesId).subscribe(metadata => {
|
||||
this.seriesMetadata = metadata;
|
||||
this.cdRef.markForCheck();
|
||||
|
@ -517,7 +517,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
this.libraryType = results.libType;
|
||||
this.series = results.series;
|
||||
|
||||
if (this.libraryType !== LibraryType.Comic) {
|
||||
if (this.libraryType !== LibraryType.Comic && loadExternal) {
|
||||
this.loadReviews(true);
|
||||
}
|
||||
|
||||
|
@ -591,7 +591,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
}
|
||||
|
||||
createRelatedSeries(series: Series, relation: RelationKind) {
|
||||
return {series, relation} as RelatedSeris;
|
||||
return {series, relation} as RelatedSeriesPair;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -751,11 +751,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
modalRef.closed.subscribe((closeResult: {success: boolean, series: Series, coverImageUpdate: boolean}) => {
|
||||
window.scrollTo(0, 0);
|
||||
if (closeResult.success) {
|
||||
this.seriesService.getSeries(this.seriesId).subscribe(s => {
|
||||
this.series = s;
|
||||
this.cdRef.detectChanges();
|
||||
});
|
||||
|
||||
this.loadSeries(this.seriesId);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,219 +2,121 @@
|
|||
<app-read-more [text]="seriesSummary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-2 mb-2">
|
||||
<div class="col-md-4">
|
||||
<h5>Ratings</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-external-rating [seriesId]="series.id" [userRating]="series.userRating" [libraryType]="libraryType"></app-external-rating>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-metadata-detail [tags]="['']" [libraryId]="series.libraryId" heading="Ratings">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-external-rating [seriesId]="series.id" [userRating]="series.userRating" [hasUserRated]="series.hasUserRated" [libraryType]="libraryType"></app-external-rating>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
|
||||
<ng-container *ngIf="WebLinks as links">
|
||||
<div class="row g-0 mt-2 mb-2" *ngIf="links.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Links</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<a class="col me-1" [href]="link | safeHtml" target="_blank" rel="noopener noreferrer" *ngFor="let link of links" [title]="link">
|
||||
<img width="24" height="24" class="lazyload img-placeholder"
|
||||
[src]="imageService.errorWebLinkImage"
|
||||
[attr.data-src]="imageService.getWebLinkImage(link)"
|
||||
(error)="imageService.updateErroredWebLinkImage($event)"
|
||||
aria-hidden="true" alt="">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<app-metadata-detail [tags]="links" [libraryId]="series.libraryId" heading="Links">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<a class="col me-1" [href]="item | safeHtml" target="_blank" rel="noopener noreferrer" [title]="item">
|
||||
<img width="24" height="24" class="lazyload img-placeholder"
|
||||
[src]="imageService.errorWebLinkImage"
|
||||
[attr.data-src]="imageService.getWebLinkImage(item)"
|
||||
(error)="imageService.updateErroredWebLinkImage($event)"
|
||||
aria-hidden="true" alt="">
|
||||
</a>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<div class="row g-0" *ngIf="seriesMetadata.genres && seriesMetadata.genres.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Genres</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.genres">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Genres, item.id)" [selectionMode]="TagBadgeCursor.Clickable">{{item.title}}</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0" *ngIf="seriesMetadata.tags && seriesMetadata.tags.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Tags</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.tags">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Tags, item.id)" [selectionMode]="TagBadgeCursor.Clickable">{{item.title}}</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.collectionTags && seriesMetadata.collectionTags.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Collections</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.collectionTags">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('collections', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
|
||||
{{item.title}}
|
||||
</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0 mt-1" *ngIf="readingLists && readingLists.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Reading Lists</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="readingLists">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('lists', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
|
||||
<span *ngIf="item.promoted">
|
||||
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">(promoted)</span>
|
||||
</span>
|
||||
{{item.title}}
|
||||
</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.writers && seriesMetadata.writers.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Writers/Authors</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.writers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Writers, item.id)" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<app-metadata-detail [tags]="seriesMetadata.genres" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Genres" heading="Genres">
|
||||
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.tags" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Tags" heading="Tags">
|
||||
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.collectionTags" [libraryId]="series.libraryId" heading="Collections">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('collections', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
|
||||
{{item.title}}
|
||||
</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
|
||||
<app-metadata-detail [tags]="readingLists" [libraryId]="series.libraryId" heading="Reading Lists">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('lists', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
|
||||
<span *ngIf="item.promoted">
|
||||
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">(promoted)</span>
|
||||
</span>
|
||||
{{item.title}}
|
||||
</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.writers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Writers" heading="Writers/Authors">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
|
||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" id="extended-series-metadata">
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.coverArtists && seriesMetadata.coverArtists.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Cover Artists</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.coverArtists">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.CoverArtists, item.id)" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<app-metadata-detail [tags]="seriesMetadata.coverArtists" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.CoverArtists" heading="Cover Artists">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.characters && seriesMetadata.characters.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Characters</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.characters">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Character, item.id)" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<app-metadata-detail [tags]="seriesMetadata.characters" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Character" heading="Characters">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.colorists && seriesMetadata.colorists.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Colorists</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.colorists">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Colorist, item.id)" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<app-metadata-detail [tags]="seriesMetadata.colorists" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Colorist" heading="Colorists">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.editors && seriesMetadata.editors.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Editors</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.editors">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Editor, item.id)" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<app-metadata-detail [tags]="seriesMetadata.editors" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Editor" heading="Editors">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.inkers && seriesMetadata.inkers.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Inkers</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.inkers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Inker, item.id)" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<app-metadata-detail [tags]="seriesMetadata.inkers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Inker" heading="Inkers">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.letterers && seriesMetadata.letterers.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Letterers</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.letterers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Letterer, item.id)" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.translators && seriesMetadata.translators.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Translators</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.translators">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Translator, item.id)" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<app-metadata-detail [tags]="seriesMetadata.letterers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Letterer" heading="Letterers">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.pencillers && seriesMetadata.pencillers.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Pencillers</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.pencillers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Penciller, item.id)" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<app-metadata-detail [tags]="seriesMetadata.translators" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Translator" heading="Translators">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.pencillers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Penciller" heading="Pencillers">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="seriesMetadata.publishers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Publisher" heading="Publishers">
|
||||
<ng-template #itemTemplate let-item>
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.publishers && seriesMetadata.publishers.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Publishers</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.publishers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Publisher, item.id)" [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
|
@ -226,5 +128,4 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<!-- This first row will have random information about the series-->
|
||||
<app-series-info-cards [series]="series" [seriesMetadata]="seriesMetadata" (goTo)="handleGoTo($event)" [hasReadingProgress]="hasReadingProgress"></app-series-info-cards>
|
||||
|
|
|
@ -19,12 +19,15 @@ import {PersonBadgeComponent} from "../../../shared/person-badge/person-badge.co
|
|||
import {NgbCollapse} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {SeriesInfoCardsComponent} from "../../../cards/series-info-cards/series-info-cards.component";
|
||||
import {LibraryType} from "../../../_models/library";
|
||||
import {MetadataDetailComponent} from "../metadata-detail/metadata-detail.component";
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-metadata-detail',
|
||||
standalone: true,
|
||||
imports: [CommonModule, TagBadgeComponent, BadgeExpanderComponent, SafeHtmlPipe, ExternalRatingComponent, ReadMoreComponent, A11yClickDirective, PersonBadgeComponent, NgbCollapse, SeriesInfoCardsComponent],
|
||||
imports: [CommonModule, TagBadgeComponent, BadgeExpanderComponent, SafeHtmlPipe, ExternalRatingComponent,
|
||||
ReadMoreComponent, A11yClickDirective, PersonBadgeComponent, NgbCollapse, SeriesInfoCardsComponent,
|
||||
MetadataDetailComponent],
|
||||
templateUrl: './series-metadata-detail.component.html',
|
||||
styleUrls: ['./series-metadata-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
@ -74,11 +77,11 @@ export class SeriesMetadataDetailComponent implements OnChanges {
|
|||
this.seriesMetadata.letterers.length > 0 ||
|
||||
this.seriesMetadata.pencillers.length > 0 ||
|
||||
this.seriesMetadata.publishers.length > 0 ||
|
||||
this.seriesMetadata.characters.length > 0 ||
|
||||
this.seriesMetadata.translators.length > 0;
|
||||
|
||||
if (this.seriesMetadata !== null) {
|
||||
this.seriesSummary = (this.seriesMetadata.summary === null ? '' : this.seriesMetadata.summary).replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
this.seriesSummary = (this.seriesMetadata?.summary === null ? '' : this.seriesMetadata.summary).replace(/\n/g, '<br>');
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,11 @@ export enum FilterQueryParam {
|
|||
/**
|
||||
* This is a pagination control
|
||||
*/
|
||||
Page = 'page'
|
||||
Page = 'page',
|
||||
/**
|
||||
* Special case for the UI. Does not trigger filtering
|
||||
*/
|
||||
None = 'none'
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
|
@ -46,19 +50,19 @@ export class FilterUtilitiesService {
|
|||
|
||||
/**
|
||||
* Updates the window location with a custom url based on filter and pagination objects
|
||||
* @param pagination
|
||||
* @param filter
|
||||
* @param pagination
|
||||
* @param filter
|
||||
*/
|
||||
updateUrlFromFilter(pagination: Pagination, filter: SeriesFilter | undefined) {
|
||||
const params = '?page=' + pagination.currentPage;
|
||||
|
||||
|
||||
const url = this.urlFromFilter(window.location.href.split('?')[0] + params, filter);
|
||||
window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(url, pagination));
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches the page query param in the window location.
|
||||
* @param pagination
|
||||
* Patches the page query param in the window location.
|
||||
* @param pagination
|
||||
*/
|
||||
updateUrlFromPagination(pagination: Pagination) {
|
||||
window.history.replaceState(window.location.href, '', this.replacePaginationOnUrl(window.location.href, pagination));
|
||||
|
@ -127,7 +131,7 @@ export class FilterUtilitiesService {
|
|||
if (filter.seriesNameQuery !== '') {
|
||||
params += `&${FilterQueryParam.Name}=${encodeURIComponent(filter.seriesNameQuery)}`;
|
||||
}
|
||||
|
||||
|
||||
return currentUrl + params;
|
||||
}
|
||||
|
||||
|
@ -262,7 +266,7 @@ export class FilterUtilitiesService {
|
|||
anyChanged = true;
|
||||
}
|
||||
|
||||
// Rating, seriesName,
|
||||
// Rating, seriesName,
|
||||
const rating = snapshot.queryParamMap.get(FilterQueryParam.Rating);
|
||||
if (rating !== undefined && rating !== null && parseInt(rating, 10) > 0) {
|
||||
filter.rating = parseInt(rating, 10);
|
||||
|
@ -301,7 +305,7 @@ export class FilterUtilitiesService {
|
|||
filter.seriesNameQuery = decodeURIComponent(searchNameQuery);
|
||||
anyChanged = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
return [filter, false]; // anyChanged. Testing out if having a filter active but keep drawer closed by default works better
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<div class="modal-header">
|
||||
<h4 class="modal-title">New Update Available!</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
|
||||
|
||||
</button>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5>{{updateData.updateTitle}}</h5>
|
||||
|
@ -10,6 +8,7 @@
|
|||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn {{updateData.isDocker ? 'btn-primary' : 'btn-secondary'}}" (click)="close()">Close</button>
|
||||
<a *ngIf="!updateData.isDocker" href="{{updateData.updateUrl}}" class="btn btn-primary" target="_blank" rel="noopener noreferrer" (click)="close()">Download</a>
|
||||
</div>
|
||||
<a class="btn btn-icon" [href]="updateUrl" target="_blank" rel="noopener noreferrer">How to Update</a>
|
||||
<button type="button" class="btn {{updateData.isDocker ? 'btn-primary' : 'btn-secondary'}}" (click)="close()">Close</button>
|
||||
<a *ngIf="!updateData.isDocker" href="{{updateData.updateUrl}}" class="btn btn-primary" target="_blank" rel="noopener noreferrer" (click)="close()">Download</a>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core';
|
||||
import {NgbActiveModal, NgbModalModule} from '@ng-bootstrap/ng-bootstrap';
|
||||
import { UpdateVersionEvent } from 'src/app/_models/events/update-version-event';
|
||||
import {CommonModule} from "@angular/common";
|
||||
|
@ -14,12 +14,21 @@ import {SafeHtmlPipe} from "../../pipe/safe-html.pipe";
|
|||
styleUrls: ['./update-notification-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class UpdateNotificationModalComponent {
|
||||
export class UpdateNotificationModalComponent implements OnInit {
|
||||
|
||||
@Input({required: true}) updateData!: UpdateVersionEvent;
|
||||
updateUrl: string = 'https://wiki.kavitareader.com/en/install/windows-install#updating-kavita';
|
||||
|
||||
constructor(public modal: NgbActiveModal) { }
|
||||
|
||||
ngOnInit() {
|
||||
if (this.updateData.isDocker) {
|
||||
this.updateUrl = 'https://wiki.kavitareader.com/en/install/docker-install#updating-kavita';
|
||||
} else {
|
||||
this.updateUrl = 'https://wiki.kavitareader.com/en/install/windows-install#updating-kavita';
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.modal.close({success: false, series: undefined});
|
||||
}
|
||||
|
|
|
@ -120,6 +120,10 @@
|
|||
border-top-left-radius: var(--side-nav-border-radius);
|
||||
border-top-right-radius: var(--side-nav-border-radius);
|
||||
}
|
||||
|
||||
&.no-donate {
|
||||
height: calc((var(--vh)*100) - 56px);
|
||||
}
|
||||
}
|
||||
|
||||
.side-nav-overlay {
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 832 B After Width: | Height: | Size: 3.7 KiB |
BIN
UI/Web/src/assets/images/ExternalServices/GoogleBooks.png
Normal file
BIN
UI/Web/src/assets/images/ExternalServices/GoogleBooks.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 523 B |
Binary file not shown.
Before Width: | Height: | Size: 465 B After Width: | Height: | Size: 3.6 KiB |
BIN
UI/Web/src/assets/images/ExternalServices/OpenLibrary.png
Normal file
BIN
UI/Web/src/assets/images/ExternalServices/OpenLibrary.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1 KiB |
|
@ -5,6 +5,6 @@ export const environment = {
|
|||
production: true,
|
||||
apiUrl: `${BASE_URL}api/`,
|
||||
hubUrl:`${BASE_URL}hubs/`,
|
||||
buyLink: 'https://buy.stripe.com/fZe6qsbrJ8bye88cMO?prefilled_promo_code=FREETRIAL',
|
||||
buyLink: 'https://buy.stripe.com/3cs7uw67p2Re7JK4gj?prefilled_promo_code=FREETRIAL',
|
||||
manageLink: 'https://billing.stripe.com/p/login/28oaFRa3HdHWb5ecMM'
|
||||
};
|
||||
|
|
|
@ -26,6 +26,10 @@ a.read-more-link {
|
|||
}
|
||||
}
|
||||
|
||||
td > a:not(.dark-exempt) {
|
||||
color: var(--primary-color-darker-shade);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue