Manga Reader Enhancements (#3027)

Co-authored-by: Zackaree <github@zackaree.com>
Co-authored-by: Marius Werkmeister <46057569+Marsimplodation@users.noreply.github.com>
This commit is contained in:
Joe Milazzo 2024-06-29 11:23:23 -05:00 committed by GitHub
parent 850d4f8e12
commit e4224fbfa4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1402 additions and 456 deletions

View File

@ -12,9 +12,9 @@
<LangVersion>latestmajor</LangVersion>
</PropertyGroup>
<!-- <Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">-->
<!-- <Exec Command="swagger tofile &#45;&#45;output ../openapi.json bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll v1" />-->
<!-- </Target>-->
<Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">
<Exec Command="swagger tofile --output ../openapi.json bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll v1" />
</Target>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>false</DebugSymbols>

View File

@ -30,25 +30,25 @@ public class CblController : BaseApiController
/// The first step in a cbl import. This validates the cbl file that if an import occured, would it be successful.
/// If this returns errors, the cbl will always be rejected by Kavita.
/// </summary>
/// <param name="file">FormBody with parameter name of cbl</param>
/// <param name="cbl">FormBody with parameter name of cbl</param>
/// <param name="comicVineMatching">Use comic vine matching or not. Defaults to false</param>
/// <returns></returns>
[HttpPost("validate")]
public async Task<ActionResult<CblImportSummaryDto>> ValidateCbl([FromForm(Name = "cbl")] IFormFile file,
[FromForm(Name = "comicVineMatching")] bool comicVineMatching = false)
public async Task<ActionResult<CblImportSummaryDto>> ValidateCbl(IFormFile cbl, bool comicVineMatching = false)
{
var userId = User.GetUserId();
try
{
var cbl = await SaveAndLoadCblFile(file);
var importSummary = await _readingListService.ValidateCblFile(userId, cbl, comicVineMatching);
importSummary.FileName = file.FileName;
var cblReadingList = await SaveAndLoadCblFile(cbl);
var importSummary = await _readingListService.ValidateCblFile(userId, cblReadingList, comicVineMatching);
importSummary.FileName = cbl.FileName;
return Ok(importSummary);
}
catch (ArgumentNullException)
{
return Ok(new CblImportSummaryDto()
{
FileName = file.FileName,
FileName = cbl.FileName,
Success = CblImportResult.Fail,
Results = new List<CblBookResult>()
{
@ -63,7 +63,7 @@ public class CblController : BaseApiController
{
return Ok(new CblImportSummaryDto()
{
FileName = file.FileName,
FileName = cbl.FileName,
Success = CblImportResult.Fail,
Results = new List<CblBookResult>()
{
@ -80,25 +80,26 @@ public class CblController : BaseApiController
/// <summary>
/// Performs the actual import (assuming dryRun = false)
/// </summary>
/// <param name="file">FormBody with parameter name of cbl</param>
/// <param name="cbl">FormBody with parameter name of cbl</param>
/// <param name="dryRun">If true, will only emulate the import but not perform. This should be done to preview what will happen</param>
/// <param name="comicVineMatching">Use comic vine matching or not. Defaults to false</param>
/// <returns></returns>
[HttpPost("import")]
public async Task<ActionResult<CblImportSummaryDto>> ImportCbl([FromForm(Name = "cbl")] IFormFile file,
[FromForm(Name = "dryRun")] bool dryRun = false, [FromForm(Name = "comicVineMatching")] bool comicVineMatching = false)
public async Task<ActionResult<CblImportSummaryDto>> ImportCbl(IFormFile cbl, bool dryRun = false, bool comicVineMatching = false)
{
try
{
var userId = User.GetUserId();
var cbl = await SaveAndLoadCblFile(file);
var importSummary = await _readingListService.CreateReadingListFromCbl(userId, cbl, dryRun, comicVineMatching);
importSummary.FileName = file.FileName;
var cblReadingList = await SaveAndLoadCblFile(cbl);
var importSummary = await _readingListService.CreateReadingListFromCbl(userId, cblReadingList, dryRun, comicVineMatching);
importSummary.FileName = cbl.FileName;
return Ok(importSummary);
} catch (ArgumentNullException)
{
return Ok(new CblImportSummaryDto()
{
FileName = file.FileName,
FileName = cbl.FileName,
Success = CblImportResult.Fail,
Results = new List<CblBookResult>()
{
@ -113,7 +114,7 @@ public class CblController : BaseApiController
{
return Ok(new CblImportSummaryDto()
{
FileName = file.FileName,
FileName = cbl.FileName,
Success = CblImportResult.Fail,
Results = new List<CblBookResult>()
{

View File

@ -29,7 +29,7 @@
<div infinite-scroll [infiniteScrollDistance]="1" [infiniteScrollThrottle]="50">
<ng-container *ngFor="let item of webtoonImages | async; let index = index;">
<img src="{{item.src}}" style="display: block"
<img src="{{item.src}}" style="display: block; width: {{widthOverride$ | async}}"
[style.filter]="(darkness$ | async) ?? '' | safeStyle"
class="mx-auto {{pageNum === item.page && showDebugOutline() ? 'active': ''}} {{areImagesWiderThanWindow ? 'full-width' : ''}}"
rel="nofollow" alt="image" (load)="onImageLoad($event)" id="page-{{item.page}}" [attr.page]="item.page" ondragstart="return false;" onselectstart="return false;">

View File

@ -25,7 +25,6 @@ import { WebtoonImage } from '../../_models/webtoon-image';
import { ManagaReaderService } from '../../_service/managa-reader.service';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {TranslocoDirective} from "@ngneat/transloco";
import {MangaReaderComponent} from "../manga-reader/manga-reader.component";
import {InfiniteScrollModule} from "ngx-infinite-scroll";
import {ReaderSetting} from "../../_models/reader-setting";
import {SafeStylePipe} from "../../../_pipes/safe-style.pipe";
@ -174,6 +173,14 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy,
*/
debugLogFilter: Array<string> = ['[PREFETCH]', '[Intersection]', '[Visibility]', '[Image Load]'];
/**
* Width override for maunal width control
* 2 observables needed to avoid flickering, probably due to data races, when changing the width
* this allows to precicely define execution order
*/
widthOverride$ : Observable<string> = new Observable<string>();
widthSliderValue$ : Observable<string> = new Observable<string>();
get minPageLoaded() {
return Math.min(...Object.values(this.imagesLoaded));
}
@ -232,6 +239,31 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy,
takeUntilDestroyed(this.destroyRef)
);
this.widthSliderValue$ = this.readerSettings$.pipe(
map(values => (parseInt(values.widthSlider) <= 0) ? '' : values.widthSlider + '%'),
takeUntilDestroyed(this.destroyRef)
);
this.widthOverride$ = this.widthSliderValue$;
//perfom jump so the page stays in view
this.widthSliderValue$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
this.currentPageElem = this.document.querySelector('img#page-' + this.pageNum);
if(!this.currentPageElem)
return;
let images = Array.from(document.querySelectorAll('img[id^="page-"]')) as HTMLImageElement[];
images.forEach((img) => {
this.renderer.setStyle(img, "width", val);
});
this.widthOverride$ = this.widthSliderValue$;
this.prevScrollPosition = this.currentPageElem.getBoundingClientRect().top;
this.currentPageElem.scrollIntoView();
this.cdRef.markForCheck();
});
if (this.goToPage) {
this.goToPage.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(page => {
const isSamePage = this.pageNum === page;
@ -381,7 +413,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy,
this.cdRef.markForCheck();
}
if (totalScroll === totalHeight && !this.atBottom) {
if (totalHeight != 0 && totalScroll >= totalHeight && !this.atBottom) {
this.atBottom = true;
this.cdRef.markForCheck();
this.setPageNum(this.totalPages);
@ -392,6 +424,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy,
document.body.scrollTop = this.previousScrollHeightMinusTop + (SPACER_SCROLL_INTO_PX / 2);
this.cdRef.markForCheck();
});
this.checkIfShouldTriggerContinuousReader()
} else if (totalScroll >= totalHeight + SPACER_SCROLL_INTO_PX && this.atBottom) {
// This if statement will fire once we scroll into the spacer at all
this.loadNextChapter.emit();

View File

@ -276,6 +276,18 @@
min="10" max="100" step="1" formControlName="darkness">
</div>
<div class="col-md-6 col-sm-12">
<label for="width-override-slider" class="form-label">{{t('width-override-label')}}:
@if (widthOverrideLabel$ | async; as widthOverrideLabel) {
{{ widthOverrideLabel ? widthOverrideLabel : t('off') }}
}
@else {
{{t('off')}}
}
</label>
<input id="width-override-slider" type="range" min="0" max="100" class="form-range" formControlName="widthSlider">
</div>
<div class="col-md-6 col-sm-12">
<button class="btn btn-primary" (click)="savePref()">{{t('save-globally')}}</button>

View File

@ -398,6 +398,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
* Show and log debug information
*/
debugMode: boolean = false;
/**
* Width override label for maunal width control
*/
widthOverrideLabel$ : Observable<string> = new Observable<string>();
// Renderer interaction
readerSettings$!: Observable<ReaderSetting>;
@ -513,6 +517,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
autoCloseMenu: new FormControl(this.autoCloseMenu),
pageSplitOption: new FormControl(this.pageSplitOption),
fittingOption: new FormControl(this.mangaReaderService.translateScalingOption(this.scalingOption)),
widthSlider: new FormControl('none'),
layoutMode: new FormControl(this.layoutMode),
darkness: new FormControl(100),
emulateBook: new FormControl(this.user.preferences.emulateBook),
@ -549,7 +554,51 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {});
//only enable the width override slider under certain conditions
// width mode selected
// splitting is set to fit to screen, otherwise disable
// when disable set the value to 0
// to use the default of the current single page reader
this.generalSettingsForm.get('pageSplitOption')?.valueChanges.pipe(
tap(val => {
const fitting = this.generalSettingsForm.get('fittingOption')?.value;
const widthOverrideControl = this.generalSettingsForm.get('widthSlider')!;
if (PageSplitOption.FitSplit == val && FITTING_OPTION.WIDTH == fitting) {
widthOverrideControl?.enable();
} else {
widthOverrideControl?.setValue(0);
widthOverrideControl?.disable();
}
}),
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {});
//only enable the width override slider under certain conditions
// width mode selected
// splitting is set to fit to screen, otherwise disable
// when disable set the value to 0
// to use the default of the current single page reader
this.generalSettingsForm.get('fittingOption')?.valueChanges.pipe(
tap(val => {
const splitting = this.generalSettingsForm.get('pageSplitOption')?.value;
const widthOverrideControl = this.generalSettingsForm.get('widthSlider')!;
if (PageSplitOption.FitSplit == splitting && FITTING_OPTION.WIDTH == val){
widthOverrideControl?.enable();
} else {
widthOverrideControl?.setValue(0);
widthOverrideControl?.disable();
}
}),
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {});
//send the current width override value to the label
this.widthOverrideLabel$ = this.readerSettings$?.pipe(
map(values => (parseInt(values.widthSlider) <= 0) ? '' : values.widthSlider + '%'),
takeUntilDestroyed(this.destroyRef)
);
this.generalSettingsForm.get('layoutMode')?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
@ -560,11 +609,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (this.layoutMode === LayoutMode.Single) {
this.generalSettingsForm.get('pageSplitOption')?.setValue(this.user.preferences.pageSplitOption);
this.generalSettingsForm.get('pageSplitOption')?.enable();
this.generalSettingsForm.get('widthSlider')?.enable();
this.generalSettingsForm.get('fittingOption')?.enable();
this.generalSettingsForm.get('emulateBook')?.enable();
} else {
this.generalSettingsForm.get('pageSplitOption')?.setValue(PageSplitOption.NoSplit);
this.generalSettingsForm.get('pageSplitOption')?.disable();
this.generalSettingsForm.get('widthSlider')?.disable();
this.generalSettingsForm.get('fittingOption')?.setValue(this.mangaReaderService.translateScalingOption(ScalingOption.FitToHeight));
this.generalSettingsForm.get('fittingOption')?.disable();
this.generalSettingsForm.get('emulateBook')?.enable();
@ -696,6 +747,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return {
pageSplit: parseInt(this.generalSettingsForm.get('pageSplitOption')?.value, 10),
fitting: (this.generalSettingsForm.get('fittingOption')?.value as FITTING_OPTION),
widthSlider: this.generalSettingsForm.get('widthSlider')?.value,
layoutMode: this.layoutMode,
darkness: parseInt(this.generalSettingsForm.get('darkness')?.value + '', 10) || 100,
pagingDirection: this.pagingDirection,

View File

@ -3,6 +3,7 @@
[style.filter]="(darkness$ | async) ?? '' | safeStyle" [style.height]="(imageContainerHeight$ | async) ?? '' | safeStyle">
@if(currentImage) {
<img alt=" "
style="width: {{widthOverride$ | async}}"
#image
[src]="currentImage.src"
id="image-1"

View File

@ -53,6 +53,11 @@ export class SingleRendererComponent implements OnInit, ImageRenderer {
pageNum: number = 0;
maxPages: number = 1;
/**
* Width override for maunal width control
*/
widthOverride$ : Observable<string> = new Observable<string>();
get ReaderMode() {return ReaderMode;}
get LayoutMode() {return LayoutMode;}
@ -67,6 +72,13 @@ export class SingleRendererComponent implements OnInit, ImageRenderer {
takeUntilDestroyed(this.destroyRef)
);
//handle manual width
this.widthOverride$ = this.readerSettings$.pipe(
map(values => (parseInt(values.widthSlider) <= 0) ? '' : values.widthSlider + '%'),
takeUntilDestroyed(this.destroyRef)
);
this.emulateBookClass$ = this.readerSettings$.pipe(
map(data => data.emulateBook),
map(enabled => enabled ? 'book-shadow' : ''),

View File

@ -1,7 +1,7 @@
export enum FITTING_OPTION {
HEIGHT = 'full-height',
WIDTH = 'full-width',
ORIGINAL = 'original'
ORIGINAL = 'original',
}
/**
@ -12,9 +12,9 @@ export enum SPLIT_PAGE_PART {
LEFT_PART = 'left',
RIGHT_PART = 'right'
}
export enum PAGING_DIRECTION {
FORWARD = 1,
BACKWARDS = -1,
}

View File

@ -6,9 +6,10 @@ import { FITTING_OPTION, PAGING_DIRECTION } from "./reader-enums";
export interface ReaderSetting {
pageSplit: PageSplitOption;
fitting: FITTING_OPTION;
widthSlider: string;
layoutMode: LayoutMode;
darkness: number;
pagingDirection: PAGING_DIRECTION;
readerMode: ReaderMode;
emulateBook: boolean;
}
}

View File

@ -1699,6 +1699,8 @@
"image-scaling-label": "Image Scaling",
"height": "Height",
"width": "Width",
"width-override-label": "Width Override",
"off": "Off",
"original": "Original",
"auto-close-menu-label": "{{user-preferences.auto-close-menu-label}}",
"swipe-enabled-label": "Swipe Enabled",

File diff suppressed because it is too large Load Diff