From 77b8551620ff43475de5a32dd799ee70a4c97996 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Tue, 7 Jun 2022 18:48:25 -0500 Subject: [PATCH] Bugfix/reader issues (#1311) * Updated the design of icon and text to show a label. * Fixed a bug when fit to height and there is overflow on horizontal, the pagination area is stuck to the original width and after scrolling right, the pagination area doesn't move. * Attempt to fix a border showing on eink readers white mode with book reader * Removed debug code * Added back in pagination controls * fixing viewport overflow issue * Ensure volume detail drawer shows all pages for the volume, not just first chapter. Don't show release date when it's not a real date. Non-ongoing series will now show a different icon. * Fixing drawer viewport issue * Fix for book cover extending across multiple pages (#1269) * Attaching book-reader scale style to parent instead of image * Fixing some width issues on images * Making sure max-height is respected * Fixing duplicate styles which caused excessive breaks * Updated the readme to reflect new UX and some tweaks to wordings. (#1270) * Bump versions by dotnet-bump-version. * version bump (#1271) * Bump versions by dotnet-bump-version. * UX Changes, Tasks, WebP, and More! (#1280) * When account updates occur for a user, send an event to them to tell them to refresh their account information (if they are on the site at the time). This way if we revoke permissions, the site will reactively adapt. * Some cleanup on the user preferences to remove some calls we don't need anymore. * Removed old bulk cleanup bookmark code as it's no longer needed. * Tweaked the messaging for stat collection to reflect what we collect now versus when this was initially implemented. * Implemented the ability for users to configure their servers to save bookmarks as webP. Reorganized the tabs for Admin dashboard to account for upcoming features. * Implemented the ability to bulk convert bookmarks (as many times as the user wants). Added a display of Reoccurring Jobs to the Tasks admin tab. Currently it's just placeholder, but will be enhanced further later in the release. * Tweaked the wording around the convert switch. * Moved System actions to the task tab * Added a controller just for Tachiyomi so we can have dedicated APIs for that client. Deprecated an existing API on the Reader route. * Fixed the unit tests * Bump versions by dotnet-bump-version. * Implemented the ability to read format tag and force special status. (#1284) * Bump versions by dotnet-bump-version. * Implemented the ability to parse some volume and chapter keywords for chinese. (#1285) * Bump versions by dotnet-bump-version. * Word Count (#1286) * Adding some code for Robbie * See more on series detail metadata area is now at the bottom on the section * Cleaned up subtitle headings to use a single class for offset with actionables * Added some markup for the new design, waiting for Robbie to finish it off * styling age-rating badge * Started hooking up basic analyze file service and hooks in the UI. Basic code to implement the count is implemented and in benchmarks. * Hooked up analyze ui to backend * Refactored Series Detail metadata area to use a new icon/title design * Cleaned up the new design * Pushing for robbie to do css * Massive performance improvement to scan series where we only need to scan folders reported that have series in them, rather than the whole library. * Removed theme page as we no longer need it. Added WordCount to DTOs so the UI can show them. Added new pipe to format numbers in compact mode. * Hooked up actual reading time based on user's words per hour * Refactor some magic numbers to consts * Hooked in progress reporting for series word count * Hooked up analyze files * Re-implemented time to read on comics * Removed the word Last Read * Show proper language name instead of iso tag on series detail page. Added some error handling on word count code. * Reworked error handling * Fixed some security vulnerabilities in npm. * Handle a case where there are no text nodes and instead of returning an empty list, htmlagilitypack returns null. * Tweaked the styles a bit on the icon-and-title * Code cleanup Co-authored-by: Robbie Davis * Bump versions by dotnet-bump-version. * Tweaked when we calculate min reading time * Don't use plural if there is only 1 hour for reading * Fixed the logic for caluclating time to read on comics * Bump versions by dotnet-bump-version. * Drawers, Estimated Reading Time, Korean Parsing Support (#1297) * Started building out idea around detail drawer. Need code from word count to continue * Fixed the logic for caluclating time to read on comics * Adding styles * more styling fixes * Cleaned up the styles a bit more so it's at least functional. Not sure on the feature, might abandon. * Pulled Robbie's changes in and partially migrated them to the drawer. * Add offset overrides for offcanvas so it takes our header into account * Implemented a basic time left to finish the series (or at least what's in Kavita). Rough around the edges. * Cleaned up the drawer code. * Added Quick Catch ups to recommended page. Updated the timeout for scan tasks to ensure we don't run 2 at the same time. * Quick catchups implemented * Added preliminary support for Korean filename parsing. Reduced an array alloc that is called many thousands of times per scan. * Fixing drawer overflow * Fixed a calculation bug with average reading time. * Small spacing changes to drawer * Don't show estimated reading time if the user hasn't read anything * Bump eventsource from 1.1.1 to 2.0.2 in /UI/Web Bumps [eventsource](https://github.com/EventSource/eventsource) from 1.1.1 to 2.0.2. - [Release notes](https://github.com/EventSource/eventsource/releases) - [Changelog](https://github.com/EventSource/eventsource/blob/master/HISTORY.md) - [Commits](https://github.com/EventSource/eventsource/compare/v1.1.1...v2.0.2) --- updated-dependencies: - dependency-name: eventsource dependency-type: direct:production ... Signed-off-by: dependabot[bot] * Added image to series detail drawer Co-authored-by: Robbie Davis Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Time Estimation Cleanup (#1301) * Moved the calculation for time to read to the backend. Tweaked some logic around showing est time to complete. * Added debug logging to help pinpoint a duplicate issue in Kavita. * More combination logic is error checked in a special way for Robbie to reproduce an issue. * Migrated chapter detail card to use backend for time calculation. Ensure we take all chapters into account for volume time calcs * Tweaked messaging for some critical logs to include file * Ensure pages count uses comma separated number * Moved Hangfire annotations to interface level. Adjusted word count service to always recalculate when user requests via analyze series files. * Bump versions by dotnet-bump-version. * Jump Bar Testing (#1302) * Implemented a basic jump bar for the library view. This currently just interacts with existing pagination controls and is not inlined with infinite scroll yet. This is a first pass implementation. * Refactored time estimates into the reading service. * Cleaned up when the jump bar is shown to mimic pagination controls * Cleanup up code in reader service. * Scroll to card when selecting a jump key that is shown on the current page. * Ensure estimated times always has the smaller number on left hand side. * Fixed a bug with a missing vertical rule * Fixed an off by 1 pixel for search overlay * Bump versions by dotnet-bump-version. * Jumpbar Tweaks (#1305) * Adjusted the detail drawer to be slightly larger. * Attempted to shorten the jump bar on smaller screens. Robbie needs to take a look at this. * Adding plex-like styling to jumpbar * style fixes * style fixes * More fixes *sigh* * fix height issue * Fixing jumpbar on mobile * viewport height fix * added --primary-color-scrollbar for overflow across the app Co-authored-by: Robbie Davis * Bump versions by dotnet-bump-version. Co-authored-by: Joseph Milazzo Co-authored-by: majora2007 Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump versions by dotnet-bump-version. * Close drawer when opening a chapter to read * The actionables were not working on chapters in the detail drawer for a volume. Added a distinct read action instead of having the user to have hidden knowledge you can click the cover images. * Debug code in. Fixed a bug where book reader pagination wasn't covering from top to bottom when immersive mode was on. Trying to better ensure images don't span multiple virtual pages in column mode. Changed icon/title so label is bolded * Updated some dependencies for security issues. Fixed up the fix for images on column view wrapping to next virtual page. Lots of css tweaks to the layout code to make it easier to work with. * When switching from column layout to default, scroll back to where the user was. * Fixed some overlap on default mode with bottom bar * Series Detail uses the cover image update mechanism builtin, instead of the old way to handle. Added some text when there is no summary on a volume/chapter. * Side nav filter now has clear button within field. Filter no longer shows when side nav is collapsed. Co-authored-by: Robbie Davis Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- API/API.csproj | 8 +- API/config/appsettings.Development.json | 2 +- Kavita.Common/Kavita.Common.csproj | 2 +- .../app/_services/action-factory.service.ts | 4 + UI/Web/src/app/app.component.ts | 16 +--- .../book-reader/book-reader.component.html | 14 +-- .../book-reader/book-reader.component.scss | 85 ++++++++++++++----- .../book-reader/book-reader.component.ts | 30 ++++++- .../card-detail-drawer.component.html | 61 +++++++------ .../card-detail-drawer.component.scss | 2 +- .../card-detail-drawer.component.ts | 65 ++++++++++++-- .../card-detail-layout.component.html | 64 ++++++++++++-- .../card-detail-layout.component.scss | 24 +++++- .../card-actionables.component.html | 1 + .../manga-reader/manga-reader.component.html | 4 +- .../manga-reader/manga-reader.component.scss | 2 +- .../manga-reader/manga-reader.component.ts | 41 ++++++--- .../app/manga-reader/manga-reader.module.ts | 3 +- .../series-detail.component.html | 8 +- .../series-detail/series-detail.component.ts | 14 +-- .../series-metadata-detail.component.html | 26 +++--- .../icon-and-title.component.html | 3 + .../icon-and-title.component.scss | 6 ++ .../icon-and-title.component.ts | 1 + .../sidenav/side-nav/side-nav.component.html | 6 +- .../sidenav/side-nav/side-nav.component.scss | 7 ++ UI/Web/src/styles.scss | 4 +- 27 files changed, 355 insertions(+), 148 deletions(-) diff --git a/API/API.csproj b/API/API.csproj index 82f0e9b4d..60e3c9ebd 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -46,7 +46,7 @@ - + @@ -63,14 +63,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/API/config/appsettings.Development.json b/API/config/appsettings.Development.json index 0d7c12bda..78d892e05 100644 --- a/API/config/appsettings.Development.json +++ b/API/config/appsettings.Development.json @@ -5,7 +5,7 @@ "TokenKey": "super secret unguessable key", "Logging": { "LogLevel": { - "Default": "Debug", + "Default": "Critical", "Microsoft": "Information", "Microsoft.Hosting.Lifetime": "Error", "Hangfire": "Information", diff --git a/Kavita.Common/Kavita.Common.csproj b/Kavita.Common/Kavita.Common.csproj index 1d3c93525..5d6127b13 100644 --- a/Kavita.Common/Kavita.Common.csproj +++ b/Kavita.Common/Kavita.Common.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index 8532fbdc9..5d728f80c 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -65,6 +65,10 @@ export enum Action { * Open Series detail page for said series */ ViewSeries = 13, + /** + * Open the reader for entity + */ + Read = 14, } export interface ActionItem { diff --git a/UI/Web/src/app/app.component.ts b/UI/Web/src/app/app.component.ts index 01d9baafa..0705db710 100644 --- a/UI/Web/src/app/app.component.ts +++ b/UI/Web/src/app/app.component.ts @@ -36,13 +36,11 @@ export class AppComponent implements OnInit { } @HostListener('window:resize', ['$event']) - onResize(){ - this.setDocHeight(); - } - @HostListener('window:orientationchange', ['$event']) - onOrientationChange() { - this.setDocHeight(); + setDocHeight() { + // Sets a CSS variable for the actual device viewport height. Needed for mobile dev. + const vh = window.innerHeight * 0.01; + this.document.documentElement.style.setProperty('--vh', `${vh}px`); } ngOnInit(): void { @@ -59,10 +57,4 @@ export class AppComponent implements OnInit { this.libraryService.getLibraryNames().pipe(take(1)).subscribe(() => {/* No Operation */}); } } - - setDocHeight() { - // Sets a CSS variable for the actual device viewport height. Needed for mobile dev. - let vh = window.innerHeight * 0.01; - this.document.documentElement.style.setProperty('--vh', `${vh}px`); - } } diff --git a/UI/Web/src/app/book-reader/book-reader/book-reader.component.html b/UI/Web/src/app/book-reader/book-reader/book-reader.component.html index 7750b91b5..9a10e7ce8 100644 --- a/UI/Web/src/app/book-reader/book-reader/book-reader.component.html +++ b/UI/Web/src/app/book-reader/book-reader/book-reader.component.html @@ -72,24 +72,26 @@ -
+
-
-
-
+
-
-
+
diff --git a/UI/Web/src/app/book-reader/book-reader/book-reader.component.scss b/UI/Web/src/app/book-reader/book-reader/book-reader.component.scss index 568ce5210..be2b9c444 100644 --- a/UI/Web/src/app/book-reader/book-reader/book-reader.component.scss +++ b/UI/Web/src/app/book-reader/book-reader/book-reader.component.scss @@ -130,6 +130,7 @@ $action-bar-height: 38px; overflow: auto; height: calc(var(--vh, 1vh) * 100); position: relative; + // This is completely invisible, everything else renders over it &.column-layout-1 { height: calc(var(--vh) * 100); @@ -145,8 +146,11 @@ $action-bar-height: 38px; //overflow: auto; // This will break progress reporting height: 100vh; padding-top: $action-bar-height; + padding-bottom: $action-bar-height; position: relative; + //background-color: green !important; + &.column-layout-1 { height: calc((var(--vh, 1vh) * 100) - $action-bar-height); } @@ -154,12 +158,20 @@ $action-bar-height: 38px; &.column-layout-2 { height: calc((var(--vh, 1vh) * 100) - $action-bar-height); } + + &.immersive { + height: calc((var(--vh, 1vh) * 100)); + //padding-top: 0px; + //padding-bottom: 0px; + } } .book-container { position: relative; height: 100%; + //background-color: purple !important; + &.column-layout-1 { height: calc((var(--vh, 1vh) * 100) - $action-bar-height); } @@ -173,13 +185,18 @@ $action-bar-height: 38px; position: relative; padding: 20px 0; margin: 0px 0px; + //background-color: red !important; &.column-layout-1 { - height: calc((var(--vh) * 100) - calc($action-bar-height * 2)); + height: calc((var(--vh) * 100) - calc($action-bar-height)); // * 2 } &.column-layout-2 { - height: calc((var(--vh) * 100) - calc($action-bar-height * 2)); + height: calc((var(--vh) * 100) - calc($action-bar-height)); // * 2 + } + + &.immersive { + height: calc((var(--vh, 1vh) * 100) - $action-bar-height); } a, :link { @@ -203,6 +220,12 @@ $action-bar-height: 38px; box-shadow: var(--drawer-pagination-horizontal-rule); } +.bottom-bar { + position: fixed; + width: 100%; + bottom: 0px; +} + // This is essentially fitting the text to height and when you press next you are scrolling over by page width @@ -213,10 +236,6 @@ $action-bar-height: 38px; overflow: hidden; word-break: break-word; overflow-wrap: break-word; - - &.debug { - column-rule: 20px solid rebeccapurple; - } } @@ -229,10 +248,6 @@ $action-bar-height: 38px; overflow: hidden; word-break: break-word; overflow-wrap: break-word; - - &.debug { - column-rule: 20px solid rebeccapurple; - } } @@ -249,7 +264,8 @@ $action-bar-height: 38px; // This is applied to images in the backend ::ng-deep .kavita-scale-width-container { width: auto; - max-height: calc((var(--vh)*100) - 116px) !important; + // * 4 is just for extra buffer which is needed based on testing. --book-reader-content-max-height is set by us on calculation of columnHeight + max-height: calc(var(--book-reader-content-max-height) - ($action-bar-height * 4)), calc((var(--vh)*100) - ($action-bar-height * 4)) !important; } // This is applied to images in the backend @@ -272,13 +288,21 @@ $action-bar-height: 38px; .right { position: absolute; - right: 0px; // with scrollbar: 17px + right: 0px; top: $action-bar-height; - width: 20%; // with scrollbar: 18% - - z-index: 2; + width: 20%; + z-index: 3; cursor: pointer; background: transparent; + border-color: transparent; + border: none !important; + opacity: 0; + outline: none; + //background-color: aqua; + + &.immersive { + top: 0px; + } } // This class pushes the click area to the left a bit to let users click the scrollbar @@ -287,9 +311,18 @@ $action-bar-height: 38px; right: 17px; top: $action-bar-height; width: 18%; - z-index: 2; + z-index: 3; cursor: pointer; background: transparent; + border-color: transparent; + border: none !important; + opacity: 0; + outline: none; + //background-color: aqua; + + &.immersive { + top: 0px; + } } .left { @@ -298,16 +331,24 @@ $action-bar-height: 38px; top: $action-bar-height; width: 20%; background: transparent; - - z-index: 2; + border-color: transparent; + border: none !important; + z-index: 3; cursor: pointer; + opacity: 0; + outline: none; + //background-color: aqua; + + &.immersive { + top: 0px; + } } .highlight { - background-color: rgba(65, 225, 100, 0.5) !important; - animation: fadein .5s both; + background-color: rgba(65, 225, 100, 0.5) !important; + animation: fadein .5s both; } .highlight-2 { background-color: rgba(65, 105, 225, 0.5) !important; @@ -333,13 +374,13 @@ $action-bar-height: 38px; background-color: unset; &:hover, &:focus { - border-color: var(--br-actionbar-button-hover-border-color); // #545b62; + border-color: var(--br-actionbar-button-hover-border-color); } } span { background-color: unset; - color: var(--br-actionbar-button-text-color); // #6c757d; + color: var(--br-actionbar-button-text-color); } i { diff --git a/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts index ca438e477..c38fbcabb 100644 --- a/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts +++ b/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts @@ -352,7 +352,9 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { get ColumnHeight() { if (this.layoutMode !== BookPageLayoutMode.Default) { // Take the height after page loads, subtract the top/bottom bar - return this.windowHeight - (this.topOffset *2) + 'px'; + const height = this.windowHeight - (this.topOffset * 2); + this.document.documentElement.style.setProperty('--book-reader-content-max-height', `${height}px`); + return height + 'px'; } return 'unset'; } @@ -370,10 +372,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { get PageHeightForPagination() { if (this.layoutMode === BookPageLayoutMode.Default) { - return (this.readingSectionElemRef?.nativeElement?.scrollHeight || 0) - (this.topOffset * 2) + 'px'; + return (this.readingSectionElemRef?.nativeElement?.scrollHeight || 0) - ((this.topOffset * (this.immersiveMode ? 0 : 1)) * 2) + 'px'; } - return this.ColumnHeight; + if (this.immersiveMode) return this.windowHeight + 'px'; + return (this.windowHeight) - (this.topOffset * 2) + 'px'; } @@ -808,7 +811,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { const images = this.readingSectionElemRef?.nativeElement.querySelectorAll('img') || []; if (this.layoutMode !== BookPageLayoutMode.Default) { - const height = this.ColumnHeight; + const height = (parseInt(this.ColumnHeight.replace('px', ''), 10) - (this.topOffset * 2)) + 'px'; Array.from(images).forEach(img => { this.renderer.setStyle(img, 'max-height', height); }); @@ -1209,6 +1212,12 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { return; } setTimeout(() => {this.scrollbarNeeded = this.readingHtml.nativeElement.clientHeight > this.reader.nativeElement.clientHeight;}); + + // When I switch layout, I might need to resume the progress point. + if (mode === BookPageLayoutMode.Default) { + const lastSelector = this.lastSeenScrollPartPath; + setTimeout(() => this.scrollTo(lastSelector)); + } } updateReadingDirection(readingDirection: ReadingDirection) { @@ -1220,6 +1229,19 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (this.immersiveMode && !this.drawerOpen) { this.actionBarVisible = false; } + + this.updateReadingSectionHeight(); + } + + updateReadingSectionHeight() { + setTimeout(() => { + console.log('setting height on ', this.readingSectionElemRef) + if (this.immersiveMode) { + this.renderer.setStyle(this.readingSectionElemRef, 'height', 'calc(var(--vh, 1vh) * 100)', RendererStyleFlags2.Important); + } else { + this.renderer.setStyle(this.readingSectionElemRef, 'height', 'calc(var(--vh, 1vh) * 100 - ' + this.topOffset + 'px)', RendererStyleFlags2.Important); + } + }); } // Table of Contents diff --git a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html index 819a09a3e..18b32fef3 100644 --- a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html +++ b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.html @@ -43,24 +43,29 @@
-
- +
+ + + + + No Summary available. +
- +
- - {{chapter.pages | number:''}} Pages + + {{totalPages | number:''}} Pages
- +
- + {{chapterMetadata.releaseDate | date:'shortDate'}}
@@ -69,7 +74,7 @@
- + {{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} Hour{{readingTime.minHours > 1 ? 's' : ''}}
@@ -78,7 +83,7 @@
- + {{chapterMetadata.wordCount | compactNumber}} Words
@@ -88,12 +93,30 @@
- + {{ageRating}}
+ + +
+
+ + {{chapter.created | date:'short' || '-'}} + +
+
+ + +
+
+ + {{data.id}} + +
+
@@ -182,24 +205,6 @@
  • {{tabs[TabID.Files].title}} -
    - -
    - - Created: {{chapter.created | date:'short' || '-'}} - -
    -
    - - -
    -
    - - ID: {{data.id}} - -
    -
    -

    {{utilityService.formatChapterName(libraryType) + 's'}}

    • diff --git a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.scss b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.scss index 2db0d16b3..39570d23f 100644 --- a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.scss +++ b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.scss @@ -12,5 +12,5 @@ .tab-content { overflow: auto; - height: calc(45vh - 63px); // drawer height - offcanvas heading height + height: calc(40vh - 63px); // drawer height - offcanvas heading height } diff --git a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts index ba43deba8..ecb9a38b7 100644 --- a/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts +++ b/UI/Web/src/app/cards/card-detail-drawer/card-detail-drawer.component.ts @@ -2,7 +2,9 @@ import { Component, Input, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { NgbActiveOffcanvas } from '@ng-bootstrap/ng-bootstrap'; import { ToastrService } from 'ngx-toastr'; -import { Observable, of, take } from 'rxjs'; +import { finalize, Observable, of, take, takeWhile, tap } from 'rxjs'; +import { Download } from 'src/app/shared/_models/download'; +import { DownloadService } from 'src/app/shared/_services/download.service'; import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service'; import { Chapter } from 'src/app/_models/chapter'; import { ChapterMetadata } from 'src/app/_models/chapter-metadata'; @@ -18,7 +20,7 @@ import { ActionService } from 'src/app/_services/action.service'; import { ImageService } from 'src/app/_services/image.service'; import { LibraryService } from 'src/app/_services/library.service'; import { MetadataService } from 'src/app/_services/metadata.service'; -import { MAX_PAGES_PER_MINUTE, MAX_WORDS_PER_HOUR, MIN_PAGES_PER_MINUTE, MIN_WORDS_PER_HOUR, ReaderService } from 'src/app/_services/reader.service'; +import { ReaderService } from 'src/app/_services/reader.service'; import { SeriesService } from 'src/app/_services/series.service'; import { UploadService } from 'src/app/_services/upload.service'; @@ -81,6 +83,13 @@ export class CardDetailDrawerComponent implements OnInit { readingTime: HourEstimateRange = {maxHours: 1, minHours: 1, avgHours: 1, hasProgress: false}; minHoursToRead: number = 1; maxHoursToRead: number = 1; + /** + * We use a separate variable because if this is a volume, we need a sum of all chapters + */ + totalPages: number = 0; + + download$: Observable | null = null; + downloadInProgress: boolean = false; @@ -109,7 +118,7 @@ export class CardDetailDrawerComponent implements OnInit { private accountService: AccountService, private actionFactoryService: ActionFactoryService, private actionService: ActionService, private router: Router, private libraryService: LibraryService, private seriesService: SeriesService, private readerService: ReaderService, public metadataService: MetadataService, - public activeOffcanvas: NgbActiveOffcanvas) { } + public activeOffcanvas: NgbActiveOffcanvas, private downloadService: DownloadService) { } ngOnInit(): void { this.isChapter = this.utilityService.isChapter(this.data); @@ -123,13 +132,13 @@ export class CardDetailDrawerComponent implements OnInit { this.metadataService.getAgeRating(this.chapterMetadata.ageRating).subscribe(ageRating => this.ageRating = ageRating); - let totalPages = this.chapter.pages; + this.totalPages = this.chapter.pages; if (!this.isChapter) { // Need to account for multiple chapters if this is a volume - totalPages = this.utilityService.asVolume(this.data).chapters.map(c => c.pages).reduce((sum, d) => sum + d); + this.totalPages = this.utilityService.asVolume(this.data).chapters.map(c => c.pages).reduce((sum, d) => sum + d); } - this.readerService.getManualTimeToRead(this.chapterMetadata.wordCount, totalPages, this.chapter.files[0].format === MangaFormat.EPUB).subscribe((time) => this.readingTime = time); + this.readerService.getManualTimeToRead(this.chapterMetadata.wordCount, this.totalPages, this.chapter.files[0].format === MangaFormat.EPUB).subscribe((time) => this.readingTime = time); }); @@ -153,6 +162,7 @@ export class CardDetailDrawerComponent implements OnInit { }); this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this)).filter(item => item.action !== Action.Edit); + this.chapterActions.push({title: 'Read', action: Action.Read, callback: this.handleChapterActionCallback.bind(this), requiresAdmin: false}); if (this.isChapter) { this.chapters.push(this.data as Chapter); @@ -245,22 +255,59 @@ export class CardDetailDrawerComponent implements OnInit { case(Action.AddToReadingList): this.actionService.addChapterToReadingList(chapter, this.seriesId); break; + case (Action.IncognitoRead): + this.readChapter(chapter, true); + break; + case (Action.Download): + this.download(chapter); + break; + case (Action.Read): + this.readChapter(chapter, false); + break; default: break; } } - readChapter(chapter: Chapter) { + readChapter(chapter: Chapter, incognito: boolean = false) { if (chapter.pages === 0) { this.toastr.error('There are no pages. Kavita was not able to read this archive.'); return; } + const params = this.readerService.getQueryParamsObject(incognito, false); if (chapter.files.length > 0 && chapter.files[0].format === MangaFormat.EPUB) { - this.router.navigate(['library', this.libraryId, 'series', this.seriesId, 'book', chapter.id]); + this.router.navigate(['library', this.libraryId, 'series', this.seriesId, 'book', chapter.id], {queryParams: params}); } else { - this.router.navigate(['library', this.libraryId, 'series', this.seriesId, 'manga', chapter.id]); + this.router.navigate(['library', this.libraryId, 'series', this.seriesId, 'manga', chapter.id], {queryParams: params}); } + this.close(); + } + + download(chapter: Chapter) { + if (this.downloadInProgress === true) { + this.toastr.info('Download is already in progress. Please wait.'); + return; + } + + this.downloadService.downloadChapterSize(chapter.id).pipe(take(1)).subscribe(async (size) => { + const wantToDownload = await this.downloadService.confirmSize(size, 'chapter'); + console.log('want to download: ', wantToDownload); + if (!wantToDownload) { return; } + this.downloadInProgress = true; + this.download$ = this.downloadService.downloadChapter(chapter).pipe( + tap(val => { + console.log(val); + }), + takeWhile(val => { + return val.state != 'DONE'; + }), + finalize(() => { + this.download$ = null; + this.downloadInProgress = false; + })); + this.download$.subscribe(() => {}); + }); } } diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html index a06f80af6..66b4c51d0 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html @@ -12,18 +12,23 @@
  • - -
    -
    - +
    +
    + + +
    + +
    + +
    - - + +
    -
    +
    @@ -34,9 +39,52 @@ - + +
    + + + +
  • +
    + + + + of {{pagination.totalPages}} +
    +
  • +
    + +
    +
    + +
    +
    diff --git a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.scss b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.scss index 50c6e4e18..005ce9057 100644 --- a/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.scss +++ b/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.scss @@ -1,12 +1,23 @@ -.content-container { - display: inline-flex; +.viewport-container { + display: flex; + flex-direction: row; width: 100%; - height: calc((var(--vh) *100) - 152px); + height: calc((var(--vh) *100) - 162px); + margin-bottom: 10px; +} + +.content-container { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + margin-bottom: 10px; } .card-container { display: inline-block; width: 100%; + overflow-y: auto; } .grid { @@ -15,12 +26,17 @@ grid-gap: 0.5rem; justify-content: space-around; width: 100%; - max-height: calc((var(--vh) *100) - 152px); overflow-y: auto; overflow-x: hidden; align-items: start; } +@media (max-width: 576px) { + .grid { + grid-gap: 0.3rem; + } +} + .jump-bar { display: flex; flex-flow: column; diff --git a/UI/Web/src/app/cards/card-item/card-actionables/card-actionables.component.html b/UI/Web/src/app/cards/card-item/card-actionables/card-actionables.component.html index 2509711cb..504366f5d 100644 --- a/UI/Web/src/app/cards/card-item/card-actionables/card-actionables.component.html +++ b/UI/Web/src/app/cards/card-item/card-actionables/card-actionables.component.html @@ -7,4 +7,5 @@
    + \ No newline at end of file diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.html b/UI/Web/src/app/manga-reader/manga-reader.component.html index 655d2f9a7..f18ac2070 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.html +++ b/UI/Web/src/app/manga-reader/manga-reader.component.html @@ -41,13 +41,13 @@
    -
    +
    -
    +
    diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.scss b/UI/Web/src/app/manga-reader/manga-reader.component.scss index 681425de2..5ca1a0d1d 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.scss +++ b/UI/Web/src/app/manga-reader/manga-reader.component.scss @@ -249,7 +249,7 @@ img { } $pagination-bg: rgba(0, 0, 0, 0); - //$pagination-bg: rgba(0, 0, 255, 0.4); + //$pagination-bg: rgba(0, 0, 255, 0.4); // DEBUG CODE .pagination-area { cursor: pointer; diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/manga-reader.component.ts index b128b2331..9ddc1aa4c 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/manga-reader.component.ts @@ -1,7 +1,7 @@ import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, Inject, OnDestroy, OnInit, Renderer2, SimpleChanges, ViewChild } from '@angular/core'; import { DOCUMENT, Location } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; -import { take, takeUntil } from 'rxjs/operators'; +import { debounceTime, take, takeUntil } from 'rxjs/operators'; import { User } from '../_models/user'; import { AccountService } from '../_services/account.service'; import { ReaderService } from '../_services/reader.service'; @@ -10,7 +10,7 @@ import { NavService } from '../_services/nav.service'; import { ReadingDirection } from '../_models/preferences/reading-direction'; import { ScalingOption } from '../_models/preferences/scaling-option'; import { PageSplitOption } from '../_models/preferences/page-split-option'; -import { BehaviorSubject, forkJoin, ReplaySubject, Subject } from 'rxjs'; +import { BehaviorSubject, forkJoin, fromEvent, ReplaySubject, Subject } from 'rxjs'; import { ToastrService } from 'ngx-toastr'; import { Breakpoint, KEY_CODES, UtilityService } from '../shared/_services/utility.service'; import { CircularArray } from '../shared/data-structures/circular-array'; @@ -260,6 +260,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { */ backgroundColor: string = '#FFFFFF'; + /** + * This is here as absolute layout requires us to calculate a negative right property for the right pagination when there is overflow. This is calculated on scroll. + */ + rightPaginationOffset = 0; + getPageUrl = (pageNum: number) => { if (this.bookmarkMode) return this.readerService.getBookmarkPageUrl(this.seriesId, this.user.apiKey, pageNum); return this.readerService.getPageUrl(this.chapterId, pageNum); @@ -303,6 +308,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { return this.image?.nativeElement.height + 'px'; } + + get RightPaginationOffset() { + if (this.readerMode === ReaderMode.LeftRight && this.generalSettingsForm.get('fittingOption')?.value === FITTING_OPTION.HEIGHT) { + return (this.readingArea?.nativeElement?.scrollLeft || 0) * -1; + } + return 0; + } + get splitIconClass() { if (this.isSplitLeftToRight()) { return 'left-side'; @@ -369,8 +382,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { return; } - - this.libraryId = parseInt(libraryId, 10); this.seriesId = parseInt(seriesId, 10); this.chapterId = parseInt(chapterId, 10); @@ -449,12 +460,23 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { } ngAfterViewInit() { - if (!this.canvas) { - return; - } - this.ctx = this.canvas.nativeElement.getContext('2d', { alpha: false }); - this.canvasImage.onload = () => this.renderPage(); + + fromEvent(this.readingArea.nativeElement, 'scroll').pipe(debounceTime(20), takeUntil(this.onDestroy)).subscribe(evt => { + if (this.readerMode === ReaderMode.Webtoon) return; + if (this.readerMode === ReaderMode.LeftRight && this.generalSettingsForm.get('fittingOption')?.value === FITTING_OPTION.HEIGHT) { + this.rightPaginationOffset = (this.readingArea.nativeElement.scrollLeft) * -1; + return; + } + this.rightPaginationOffset = 0; + }); this.getWindowDimensions(); + + if (this.canvas) { + this.ctx = this.canvas.nativeElement.getContext('2d', { alpha: false }); + this.canvasImage.onload = () => this.renderPage(); + } + + } ngOnDestroy() { @@ -562,7 +584,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { }); return; - } forkJoin({ diff --git a/UI/Web/src/app/manga-reader/manga-reader.module.ts b/UI/Web/src/app/manga-reader/manga-reader.module.ts index 0d15ebe03..2ac83713e 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.module.ts +++ b/UI/Web/src/app/manga-reader/manga-reader.module.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MangaReaderComponent } from './manga-reader.component'; import { ReactiveFormsModule } from '@angular/forms'; -import { NgbButtonsModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; import { MangaReaderRoutingModule } from './manga-reader.router.module'; import { SharedModule } from '../shared/shared.module'; import { NgxSliderModule } from '@angular-slider/ngx-slider'; @@ -19,7 +19,6 @@ import { ReaderSharedModule } from '../reader-shared/reader-shared.module'; MangaReaderRoutingModule, ReactiveFormsModule, - NgbButtonsModule, NgbDropdownModule, NgxSliderModule, SharedModule, diff --git a/UI/Web/src/app/series-detail/series-detail.component.html b/UI/Web/src/app/series-detail/series-detail.component.html index d23ca893b..9770a2dad 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.html +++ b/UI/Web/src/app/series-detail/series-detail.component.html @@ -73,12 +73,12 @@
    @@ -90,7 +90,7 @@
    @@ -105,7 +105,7 @@
    diff --git a/UI/Web/src/app/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/series-detail.component.ts index 6cde05998..f3ba69ae5 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/series-detail.component.ts @@ -4,7 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { NgbModal, NgbNavChangeEvent, NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap'; import { ToastrService } from 'ngx-toastr'; import { forkJoin, Subject } from 'rxjs'; -import { finalize, take, takeUntil, takeWhile } from 'rxjs/operators'; +import { finalize, mergeMap, take, takeUntil, takeWhile } from 'rxjs/operators'; import { BulkSelectionService } from '../cards/bulk-selection.service'; import { CardDetailsModalComponent } from '../cards/_modals/card-details-modal/card-details-modal.component'; import { EditSeriesModalComponent } from '../cards/_modals/edit-series-modal/edit-series-modal.component'; @@ -96,11 +96,6 @@ export class SeriesDetailComponent implements OnInit, OnDestroy { seriesImage: string = ''; downloadInProgress: boolean = false; - /** - * Tricks the cover images for volume/chapter cards to update after we update one of them - */ - coverImageOffset: number = 0; - /** * If an action is currently being done, don't let the user kick off another action */ @@ -357,8 +352,6 @@ export class SeriesDetailComponent implements OnInit, OnDestroy { } loadSeries(seriesId: number) { - this.coverImageOffset = 0; - this.seriesService.getMetadata(seriesId).subscribe(metadata => this.seriesMetadata = metadata); this.readingListService.getReadingListsForSeries(seriesId).subscribe(lists => { this.readingLists = lists; @@ -567,11 +560,6 @@ export class SeriesDetailComponent implements OnInit, OnDestroy { drawerRef.componentInstance.parentName = this.series?.name; drawerRef.componentInstance.seriesId = this.series?.id; drawerRef.componentInstance.libraryId = this.series?.libraryId; - drawerRef.closed.subscribe((result: {coverImageUpdate: boolean}) => { - if (result.coverImageUpdate) { - this.coverImageOffset += 1; - } - }); } openEditSeriesModal() { diff --git a/UI/Web/src/app/series-detail/series-metadata-detail/series-metadata-detail.component.html b/UI/Web/src/app/series-detail/series-metadata-detail/series-metadata-detail.component.html index cccd28348..a321d6140 100644 --- a/UI/Web/src/app/series-detail/series-metadata-detail/series-metadata-detail.component.html +++ b/UI/Web/src/app/series-detail/series-metadata-detail/series-metadata-detail.component.html @@ -6,7 +6,7 @@
    - + {{metadataService.getAgeRating(this.seriesMetadata.ageRating) | async}}
    @@ -15,7 +15,7 @@
    - + {{seriesMetadata.releaseYear}}
    @@ -24,7 +24,7 @@
    - + {{seriesMetadata.language | defaultValue:'en' | languageName | async}}
    @@ -33,16 +33,18 @@
    - - {{seriesMetadata.publicationStatus | publicationStatus}} - + + + {{pubStatus}} + +
    - + {{utilityService.mangaFormat(series.format)}}
    @@ -51,7 +53,7 @@
    - + {{series.latestReadDate | date:'shortDate'}}
    @@ -62,7 +64,7 @@
    - + {{series.wordCount | compactNumber}} Words
    @@ -72,7 +74,7 @@
    - + {{series.pages | number:''}} Pages
    @@ -83,7 +85,7 @@
    - + {{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} Hour{{readingTime.minHours > 1 ? 's' : ''}}
    @@ -93,7 +95,7 @@
    - + ~{{readingTimeLeft.avgHours}} Hour{{readingTimeLeft.avgHours > 1 ? 's' : ''}} Left
    diff --git a/UI/Web/src/app/shared/icon-and-title/icon-and-title.component.html b/UI/Web/src/app/shared/icon-and-title/icon-and-title.component.html index 064f88f33..c3308b8a0 100644 --- a/UI/Web/src/app/shared/icon-and-title/icon-and-title.component.html +++ b/UI/Web/src/app/shared/icon-and-title/icon-and-title.component.html @@ -1,5 +1,8 @@
    +
    + {{label}} +
    diff --git a/UI/Web/src/app/shared/icon-and-title/icon-and-title.component.scss b/UI/Web/src/app/shared/icon-and-title/icon-and-title.component.scss index 44b8535c7..3c5193f1d 100644 --- a/UI/Web/src/app/shared/icon-and-title/icon-and-title.component.scss +++ b/UI/Web/src/app/shared/icon-and-title/icon-and-title.component.scss @@ -12,4 +12,10 @@ .text { padding-top: 5px; text-align: center; +} + +.label { + padding-bottom: 5px; + text-align: center; + font-weight: bold; } \ No newline at end of file diff --git a/UI/Web/src/app/shared/icon-and-title/icon-and-title.component.ts b/UI/Web/src/app/shared/icon-and-title/icon-and-title.component.ts index fe5be4b32..0d63e08d8 100644 --- a/UI/Web/src/app/shared/icon-and-title/icon-and-title.component.ts +++ b/UI/Web/src/app/shared/icon-and-title/icon-and-title.component.ts @@ -11,6 +11,7 @@ export class IconAndTitleComponent implements OnInit { */ @Input() clickable: boolean = true; @Input() title: string = ''; + @Input() label: string = ''; /** * Font classes used to display font */ diff --git a/UI/Web/src/app/sidenav/side-nav/side-nav.component.html b/UI/Web/src/app/sidenav/side-nav/side-nav.component.html index e0f66775c..775f4c73d 100644 --- a/UI/Web/src/app/sidenav/side-nav/side-nav.component.html +++ b/UI/Web/src/app/sidenav/side-nav/side-nav.component.html @@ -12,11 +12,11 @@ -
    +
    -
    +
    - +