mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
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:
parent
850d4f8e12
commit
e4224fbfa4
@ -12,9 +12,9 @@
|
||||
<LangVersion>latestmajor</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- <Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">-->
|
||||
<!-- <Exec Command="swagger tofile --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>
|
||||
|
@ -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>()
|
||||
{
|
||||
|
@ -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;">
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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' : ''),
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
1692
openapi.json
1692
openapi.json
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user