Reader Bugs + New Features (#1536)
* Updated a typo in manage tasks of Reoccuring -> Recurring * Fixed a bug in MinimumNumberFromRange where a regex wasn't properly constructed which could skew results. * Fixed a bug where Volume numbers that were a float wouldn't render correctly in the manga reader menu. * Added the ability to double click on the image to bookmark it. Optimized the bookmark and unbookmark flows to remove 2 DB calls and reworked some flow of calls to speed it up. Fixed some logic where when using double (manga) flow, both of the images wouldn't show the bookmark effect, despite both of them being saved. Likewise, fixed a bug where both images weren't updating UI state, so switching from double (manga) to single, the second image wouldn't show as bookmarked without a refresh. * Double click works perfectly for bookmarking * Collection cover image chooser will now prompt with all series covers by default. Reset button is now moved up to the first slot if applicable. * When a Completed series is fully read by a user, a nightly task will now remove that series from their Want to Read list. * Added ability to trigger Want to Read cleanup from Tasks page. * Moved the brightness readout to the label line and fixed a bootstrap migration bug where small buttons weren't actually small. * Implemented ability to filter against release year (min or max or both). * Fixed a log message that wasn't properly formatted when scan finished an no files changes. * Cleaned up some code and merged some methods * Implemented sort by Release year metadata filter. * Fixed the code that finds ComicInfo.xml inside archives to only check the root and check explicitly for casing, so it must be ComicInfo.xml. * Dependency updates * Refactored some strings into consts and used TriggerJob rather than just enqueuing * Fixed the prefetcher which wasn't properly loading in the correct order as it was designed. * Cleaned up all traces of CircularArray from MangaReader * Removed a debug code * Fixed a bug with webtoon reader in fullscreen mode where continuous reader wouldn't trigger * When cleaning up series from users' want to read lists, include both completed and cancelled. * Fixed a bug where small images wouldn't have the pagination area extend to the bottom on manga reader * Added a new method for hashing during prod builds and ensure we always use aot * Fixed a bug where the save button wouldn't enable when color change occured. * Cleaned up some issues in one of contributor's PR.
This commit is contained in:
parent
52c10510b2
commit
9cf4cf742b
49 changed files with 408 additions and 221 deletions
|
|
@ -6,6 +6,11 @@ export interface FilterItem<T> {
|
|||
selected: boolean;
|
||||
}
|
||||
|
||||
export interface Range<T> {
|
||||
min: T;
|
||||
max: T;
|
||||
}
|
||||
|
||||
export interface SeriesFilter {
|
||||
formats: Array<MangaFormat>;
|
||||
libraries: Array<number>,
|
||||
|
|
@ -30,6 +35,7 @@ export interface SeriesFilter {
|
|||
languages: Array<string>;
|
||||
publicationStatus: Array<number>;
|
||||
seriesNameQuery: string;
|
||||
releaseYearRange: Range<number> | null;
|
||||
}
|
||||
|
||||
export interface SortOptions {
|
||||
|
|
@ -42,7 +48,8 @@ export enum SortField {
|
|||
Created = 2,
|
||||
LastModified = 3,
|
||||
LastChapterAdded = 4,
|
||||
TimeToRead = 5
|
||||
TimeToRead = 5,
|
||||
ReleaseYear = 6,
|
||||
}
|
||||
|
||||
export interface ReadStatus {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ export class ServerService {
|
|||
return this.httpClient.post(this.baseUrl + 'server/clear-cache', {});
|
||||
}
|
||||
|
||||
cleanupWantToRead() {
|
||||
return this.httpClient.post(this.baseUrl + 'server/cleanup-want-to-read', {});
|
||||
}
|
||||
|
||||
backupDatabase() {
|
||||
return this.httpClient.post(this.baseUrl + 'server/backup-db', {});
|
||||
}
|
||||
|
|
@ -42,7 +46,7 @@ export class ServerService {
|
|||
return this.httpClient.get<boolean>(this.baseUrl + 'server/accessible');
|
||||
}
|
||||
|
||||
getReoccuringJobs() {
|
||||
getRecurringJobs() {
|
||||
return this.httpClient.get<Job[]>(this.baseUrl + 'server/jobs');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<div class="container-fluid">
|
||||
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
|
||||
<h4>Reoccuring Tasks</h4>
|
||||
<h4>Recurring Tasks</h4>
|
||||
<div class="mb-3">
|
||||
<label for="settings-tasks-scan" class="form-label">Library Scan</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="taskScanTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #taskScanTooltip>How often Kavita will scan and refresh metadata around manga files.</ng-template>
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>Reoccuring Tasks</h4>
|
||||
<h4>Recurring Tasks</h4>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let task of reoccuringTasks$ | async; index as i">
|
||||
<tr *ngFor="let task of recurringTasks$ | async; index as i">
|
||||
<td>
|
||||
{{task.title | titlecase}}
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { SettingsService } from '../settings.service';
|
||||
import { ServerSettings } from '../_models/server-settings';
|
||||
import { catchError, finalize, shareReplay, take, takeWhile } from 'rxjs/operators';
|
||||
import { shareReplay, take } from 'rxjs/operators';
|
||||
import { defer, forkJoin, Observable, of } from 'rxjs';
|
||||
import { ServerService } from 'src/app/_services/server.service';
|
||||
import { Job } from 'src/app/_models/job/job';
|
||||
|
|
@ -32,7 +31,7 @@ export class ManageTasksSettingsComponent implements OnInit {
|
|||
taskFrequencies: Array<string> = [];
|
||||
logLevels: Array<string> = [];
|
||||
|
||||
reoccuringTasks$: Observable<Array<Job>> = of([]);
|
||||
recurringTasks$: Observable<Array<Job>> = of([]);
|
||||
adhocTasks: Array<AdhocTask> = [
|
||||
{
|
||||
name: 'Convert Bookmarks to WebP',
|
||||
|
|
@ -46,6 +45,12 @@ export class ManageTasksSettingsComponent implements OnInit {
|
|||
api: this.serverService.clearCache(),
|
||||
successMessage: 'Cache has been cleared'
|
||||
},
|
||||
{
|
||||
name: 'Clean up Want to Read',
|
||||
description: 'Removes any series that users have fully read that are within want to read and have a publication status of Completed. Runs every 24 hours.',
|
||||
api: this.serverService.cleanupWantToRead(),
|
||||
successMessage: 'Want to Read has been cleaned up'
|
||||
},
|
||||
{
|
||||
name: 'Backup Database',
|
||||
description: 'Takes a backup of the database, bookmarks, themes, manually uploaded covers, and config files',
|
||||
|
|
@ -93,7 +98,7 @@ export class ManageTasksSettingsComponent implements OnInit {
|
|||
this.settingsForm.addControl('taskBackup', new FormControl(this.serverSettings.taskBackup, [Validators.required]));
|
||||
});
|
||||
|
||||
this.reoccuringTasks$ = this.serverService.getReoccuringJobs().pipe(shareReplay());
|
||||
this.recurringTasks$ = this.serverService.getRecurringJobs().pipe(shareReplay());
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
|
|
@ -110,7 +115,7 @@ export class ManageTasksSettingsComponent implements OnInit {
|
|||
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe(async (settings: ServerSettings) => {
|
||||
this.serverSettings = settings;
|
||||
this.resetForm();
|
||||
this.reoccuringTasks$ = this.serverService.getReoccuringJobs().pipe(shareReplay());
|
||||
this.recurringTasks$ = this.serverService.getRecurringJobs().pipe(shareReplay());
|
||||
this.toastr.success('Server settings updated');
|
||||
}, (err: any) => {
|
||||
console.error('error: ', err);
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ export class EditCollectionTagsComponent implements OnInit {
|
|||
|
||||
this.pagination = series.pagination;
|
||||
this.series = series.result;
|
||||
this.imageUrls.push(...this.series.map(s => this.imageService.getSeriesCoverImage(s.id)));
|
||||
this.selections = new SelectionModel<Series>(true, this.series);
|
||||
this.isLoading = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,16 @@
|
|||
</form>
|
||||
|
||||
<div class="row g-0 chooser" style="padding-top: 10px">
|
||||
<div class="image-card col-auto"
|
||||
*ngIf="showReset" tabindex="0" aria-label="Reset cover image" (click)="reset()"
|
||||
[ngClass]="{'selected': !showApplyButton && selectedIndex === -1}">
|
||||
<app-image class="card-img-top" title="Reset Cover Image" height="230px" width="158px" [imageUrl]="imageService.resetCoverImage"></app-image>
|
||||
<ng-container *ngIf="showApplyButton">
|
||||
<br>
|
||||
<button style="width: 100%;" class="btn btn-secondary" aria-label="Reset to generated image" (click)="resetImage()">Reset</button>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
<div class="image-card col-auto"
|
||||
*ngFor="let url of imageUrls; let idx = index;" tabindex="0" attr.aria-label="Image {{idx + 1}}" (click)="selectImage(idx)"
|
||||
[ngClass]="{'selected': !showApplyButton && selectedIndex === idx}">
|
||||
|
|
@ -60,16 +70,6 @@
|
|||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="image-card col-auto"
|
||||
*ngIf="showReset" tabindex="0" aria-label="Reset cover image" (click)="reset()"
|
||||
[ngClass]="{'selected': !showApplyButton && selectedIndex === -1}">
|
||||
<app-image class="card-img-top" title="Reset Cover Image" height="230px" width="158px" [imageUrl]="imageService.resetCoverImage"></app-image>
|
||||
<ng-container *ngIf="showApplyButton">
|
||||
<br>
|
||||
<button style="width: 100%;" class="btn btn-secondary" aria-label="Reset to generated image" (click)="resetImage()">Reset</button>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
@Inject(DOCUMENT) private document: Document, private scrollService: ScrollService,
|
||||
private readonly cdRef: ChangeDetectorRef) {
|
||||
// This will always exist at this point in time since this is used within manga reader
|
||||
const reader = document.querySelector('.reader');
|
||||
const reader = document.querySelector('.reading-area');
|
||||
if (reader !== null) {
|
||||
this.readerElemRef = new ElementRef(reader as HTMLDivElement);
|
||||
}
|
||||
|
|
@ -182,7 +182,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
* gets promoted to fullscreen.
|
||||
*/
|
||||
initScrollHandler() {
|
||||
console.log('Setting up Scroll handler on ', this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body);
|
||||
fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body, 'scroll')
|
||||
//fromEvent(this.document.body, 'scroll')
|
||||
.pipe(debounceTime(20), takeUntil(this.onDestroy))
|
||||
.subscribe((event) => this.handleScrollEvent(event));
|
||||
}
|
||||
|
|
@ -263,6 +265,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||
*/
|
||||
handleScrollEvent(event?: any) {
|
||||
const verticalOffset = this.getVerticalOffset();
|
||||
console.log('offset: ', verticalOffset);
|
||||
|
||||
if (verticalOffset > this.prevScrollPosition) {
|
||||
this.scrollingDirection = PAGING_DIRECTION.FORWARD;
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@
|
|||
</div>
|
||||
|
||||
<div style="margin-left: auto; padding-right: 3%;">
|
||||
<button class="btn btn-icon btn-small" title="Shortcuts" (click)="openShortcutModal()">
|
||||
<button class="btn btn-icon btn-sm" title="Shortcuts" (click)="openShortcutModal()">
|
||||
<i class="fa-regular fa-rectangle-list" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Keyboard Shortcuts Modal</span>
|
||||
</button>
|
||||
<button *ngIf="!bookmarkMode && hasBookmarkRights" class="btn btn-icon btn-small" role="checkbox" [attr.aria-checked]="CurrentPageBookmarked"
|
||||
<button *ngIf="!bookmarkMode && hasBookmarkRights" class="btn btn-icon btn-sm" role="checkbox" [attr.aria-checked]="CurrentPageBookmarked"
|
||||
title="{{CurrentPageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}" (click)="bookmarkPage()">
|
||||
<i class="{{CurrentPageBookmarked ? 'fa' : 'far'}} fa-bookmark" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{CurrentPageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}</span>
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div (click)="toggleMenu()" class="reading-area"
|
||||
<div class="reading-area"
|
||||
[ngStyle]="{'background-color': backgroundColor, 'height': readerMode === ReaderMode.Webtoon ? 'inherit' : 'calc(var(--vh)*100)'}" #readingArea>
|
||||
<ng-container *ngIf="readerMode !== ReaderMode.Webtoon; else webtoon">
|
||||
<div class="image-container" [ngClass]="{'d-none': !renderWithCanvas }" [style.filter]="'brightness(' + generalSettingsForm.get('darkness')?.value + '%)'">
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
'fit-to-width-double-offset' : FittingOption === FITTING_OPTION.WIDTH && ShouldRenderDoublePage,
|
||||
'fit-to-height-double-offset': FittingOption === FITTING_OPTION.HEIGHT && ShouldRenderDoublePage,
|
||||
'original-double-offset' : FittingOption === FITTING_OPTION.ORIGINAL && ShouldRenderDoublePage}"
|
||||
[style.filter]="'brightness(' + generalSettingsForm.get('darkness')?.value + '%)' | safeStyle">
|
||||
[style.filter]="'brightness(' + generalSettingsForm.get('darkness')?.value + '%)' | safeStyle" (dblclick)="bookmarkPage($event)">
|
||||
<img #image [src]="canvasImage.src" id="image-1"
|
||||
class="{{getFittingOptionClass()}} {{readerMode === ReaderMode.LeftRight || readerMode === ReaderMode.UpDown ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}">
|
||||
|
||||
|
|
@ -98,8 +98,8 @@
|
|||
<div class="mb-3" *ngIf="pageOptions != undefined && pageOptions.ceil != undefined">
|
||||
<span class="visually-hidden" id="slider-info"></span>
|
||||
<div class="row g-0">
|
||||
<button class="btn btn-small btn-icon col-1" [disabled]="prevChapterDisabled" (click)="loadPrevChapter();resetMenuCloseTimer();" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-small btn-icon col-1" [disabled]="prevPageDisabled || pageNum === 0" (click)="goToPage(0);resetMenuCloseTimer();" title="First Page"><i class="fa fa-step-backward" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-sm btn-icon col-1" [disabled]="prevChapterDisabled" (click)="loadPrevChapter();resetMenuCloseTimer();" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-sm btn-icon col-1" [disabled]="prevPageDisabled || pageNum === 0" (click)="goToPage(0);resetMenuCloseTimer();" title="First Page"><i class="fa fa-step-backward" aria-hidden="true"></i></button>
|
||||
<div class="col custom-slider" *ngIf="pageOptions.ceil > 0; else noSlider">
|
||||
<ngx-slider [options]="pageOptions" [value]="pageNum" aria-describedby="slider-info" [manualRefresh]="refreshSlider" (userChangeEnd)="sliderPageUpdate($event);startMenuCloseTimer()" (userChange)="sliderDragUpdate($event)" (userChangeStart)="cancelMenuCloseTimer();"></ngx-slider>
|
||||
</div>
|
||||
|
|
@ -108,8 +108,8 @@
|
|||
<ngx-slider [options]="pageOptions" [value]="pageNum" aria-describedby="slider-info" (userChangeEnd)="startMenuCloseTimer()" (userChangeStart)="cancelMenuCloseTimer();"></ngx-slider>
|
||||
</div>
|
||||
</ng-template>
|
||||
<button class="btn btn-small btn-icon col-2" [disabled]="nextPageDisabled || pageNum >= maxPages - 1" (click)="goToPage(this.maxPages);resetMenuCloseTimer();" title="Last Page"><i class="fa fa-step-forward" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-small btn-icon col-1" [disabled]="nextChapterDisabled" (click)="loadNextChapter();resetMenuCloseTimer();" title="Next Chapter/Volume"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-sm btn-icon col-2" [disabled]="nextPageDisabled || pageNum >= maxPages - 1" (click)="goToPage(this.maxPages);resetMenuCloseTimer();" title="Last Page"><i class="fa fa-step-forward" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-sm btn-icon col-1" [disabled]="nextChapterDisabled" (click)="loadNextChapter();resetMenuCloseTimer();" title="Next Chapter/Volume"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pt-4 ms-2 me-2">
|
||||
|
|
@ -217,10 +217,9 @@
|
|||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<label for="darkness" class="form-label range-label">Brightness</label>
|
||||
<label for="darkness" class="form-label range-label">Brightness</label><span class="ms-1 range-text">{{generalSettingsForm.get('darkness')?.value + '%'}}</span>
|
||||
<input type="range" class="form-range" id="darkness"
|
||||
min="10" max="100" step="1" formControlName="darkness">
|
||||
<span class="range-text">{{generalSettingsForm.get('darkness')?.value + '%'}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import { PageSplitOption } from '../_models/preferences/page-split-option';
|
|||
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';
|
||||
import { MemberService } from '../_services/member.service';
|
||||
import { Stack } from '../shared/data-structures/stack';
|
||||
import { ChangeContext, LabelType, Options } from '@angular-slider/ngx-slider';
|
||||
|
|
@ -27,7 +26,7 @@ import { ShortcutsModalComponent } from '../reader-shared/_modals/shortcuts-moda
|
|||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { LayoutMode } from './_models/layout-mode';
|
||||
|
||||
const PREFETCH_PAGES = 8;
|
||||
const PREFETCH_PAGES = 10;
|
||||
|
||||
const CHAPTER_ID_NOT_FETCHED = -2;
|
||||
const CHAPTER_ID_DOESNT_EXIST = -1;
|
||||
|
|
@ -162,10 +161,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
renderWithCanvas: boolean = false;
|
||||
|
||||
/**
|
||||
* A circular array of size PREFETCH_PAGES + 2. Maintains prefetched Images around the current page to load from to avoid loading animation.
|
||||
* A circular array of size PREFETCH_PAGES. Maintains prefetched Images around the current page to load from to avoid loading animation.
|
||||
* @see CircularArray
|
||||
*/
|
||||
cachedImages!: CircularArray<HTMLImageElement>;
|
||||
cachedImages!: Array<HTMLImageElement>;
|
||||
/**
|
||||
* A stack of the chapter ids we come across during continuous reading mode. When we traverse a boundary, we use this to avoid extra API calls.
|
||||
* @see Stack
|
||||
|
|
@ -289,6 +288,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
*/
|
||||
rightPaginationOffset = 0;
|
||||
|
||||
bookmarkPageHandler = this.bookmarkPage.bind(this);
|
||||
|
||||
getPageUrl = (pageNum: number) => {
|
||||
if (this.bookmarkMode) return this.readerService.getBookmarkPageUrl(this.seriesId, this.user.apiKey, pageNum);
|
||||
return this.readerService.getPageUrl(this.chapterId, pageNum);
|
||||
|
|
@ -328,7 +329,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
const result = !(
|
||||
this.isCoverImage()
|
||||
|| this.isCoverImage(this.pageNum - 1)
|
||||
|| this.isCoverImage(this.pageNum - 1) // This is because we use prev page and hence the cover will re-show
|
||||
|| this.isWideImage(this.canvasImage)
|
||||
|| this.isWideImage(this.canvasImageNext)
|
||||
);
|
||||
|
|
@ -357,7 +358,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
if (this.isWideImage() || this.FittingOption === FITTING_OPTION.WIDTH) {
|
||||
return this.WindowHeight;
|
||||
}
|
||||
return this.image?.nativeElement.height + 'px';
|
||||
return Math.max(this.readingArea?.nativeElement?.clientHeight, this.image?.nativeElement.height) + 'px';
|
||||
}
|
||||
|
||||
get RightPaginationOffset() {
|
||||
|
|
@ -511,7 +512,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
});
|
||||
|
||||
this.generalSettingsForm.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((changes: SimpleChanges) => {
|
||||
this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value; // TODO: Do I need cd check here?
|
||||
this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value;
|
||||
const needsSplitting = this.isWideImage();
|
||||
// If we need to split on a menu change, then we need to re-render.
|
||||
if (needsSplitting) {
|
||||
|
|
@ -542,6 +543,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
fromEvent(this.readingArea.nativeElement, 'click').pipe(debounceTime(200)).subscribe((event: MouseEvent | any) => {
|
||||
if (event.detail > 1) return;
|
||||
this.toggleMenu();
|
||||
});
|
||||
|
||||
if (this.canvas) {
|
||||
this.ctx = this.canvas.nativeElement.getContext('2d', { alpha: false });
|
||||
this.canvasImage.onload = () => this.renderPage();
|
||||
|
|
@ -678,12 +684,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.inSetup = false;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
const images = [];
|
||||
for (let i = 0; i < PREFETCH_PAGES + 2; i++) {
|
||||
images.push(new Image());
|
||||
this.cachedImages = [];
|
||||
for (let i = 0; i < PREFETCH_PAGES; i++) {
|
||||
this.cachedImages.push(new Image())
|
||||
}
|
||||
|
||||
this.cachedImages = new CircularArray<HTMLImageElement>(images, 0);
|
||||
this.goToPageEvent = new BehaviorSubject<number>(this.pageNum);
|
||||
|
||||
this.render();
|
||||
|
|
@ -751,14 +756,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
const images = [];
|
||||
for (let i = 0; i < PREFETCH_PAGES + 2; i++) {
|
||||
images.push(new Image());
|
||||
this.cachedImages = [];
|
||||
for (let i = 0; i < PREFETCH_PAGES; i++) {
|
||||
this.cachedImages.push(new Image());
|
||||
}
|
||||
|
||||
this.cachedImages = new CircularArray<HTMLImageElement>(images, 0);
|
||||
|
||||
|
||||
this.render();
|
||||
}, () => {
|
||||
|
|
@ -1071,7 +1073,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
*/
|
||||
setCanvasImage() {
|
||||
if (this.layoutMode === LayoutMode.Single) {
|
||||
const img = this.cachedImages.arr.find(img => this.readerService.imageUrlToPageNum(img.src) === this.pageNum);
|
||||
const img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === this.pageNum);
|
||||
if (img) {
|
||||
this.canvasImage = img; // If we tried to use this for double, then the loadPage might not render correctly when switching layout mode
|
||||
} else {
|
||||
|
|
@ -1294,26 +1296,21 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
}
|
||||
|
||||
/**
|
||||
* Maintains a circular array of images (that are requested from backend) around the user's current page. This allows for quick loading (seemless to user)
|
||||
* Maintains an array of images (that are requested from backend) around the user's current page. This allows for quick loading (seemless to user)
|
||||
* and also maintains page info (wide image, etc) due to onload event.
|
||||
*/
|
||||
prefetch() {
|
||||
let index = 1;
|
||||
for(let i = 1; i <= PREFETCH_PAGES - 3; i++) {
|
||||
const numOffset = this.pageNum + i;
|
||||
if (numOffset > this.maxPages - 1) continue;
|
||||
|
||||
this.cachedImages.applyFor((item, _) => {
|
||||
const offsetIndex = this.pageNum + index;
|
||||
const urlPageNum = this.readerService.imageUrlToPageNum(item.src);
|
||||
const index = numOffset % this.cachedImages.length;
|
||||
if (this.readerService.imageUrlToPageNum(this.cachedImages[index].src) !== numOffset) {
|
||||
this.cachedImages[index].src = this.getPageUrl(numOffset);
|
||||
}
|
||||
}
|
||||
|
||||
if (urlPageNum === offsetIndex || urlPageNum === this.pageNum) {
|
||||
index += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (offsetIndex < this.maxPages - 1) {
|
||||
item.src = this.getPageUrl(offsetIndex);
|
||||
index += 1;
|
||||
}
|
||||
}, this.cachedImages.size() - 3);
|
||||
//console.log(this.pageNum, ' Prefetched pages: ', this.cachedImages.map(img => this.readerService.imageUrlToPageNum(img.src)));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1490,7 +1487,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
// We must set this here because loadPage from render doesn't call if we aren't page splitting
|
||||
if (this.readerMode !== ReaderMode.Webtoon) {
|
||||
this.canvasImage = this.cachedImages.current();
|
||||
this.canvasImage = this.cachedImages[this.pageNum & this.cachedImages.length];
|
||||
this.isLoading = true;
|
||||
}
|
||||
|
||||
|
|
@ -1524,7 +1521,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
/**
|
||||
* Bookmarks the current page for the chapter
|
||||
*/
|
||||
bookmarkPage() {
|
||||
bookmarkPage(event: MouseEvent | undefined = undefined) {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
const pageNum = this.pageNum;
|
||||
const isDouble = this.layoutMode === LayoutMode.Double || this.layoutMode === LayoutMode.DoubleReversed;
|
||||
|
||||
|
|
@ -1533,40 +1534,42 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
if (isDouble) apis.push(this.readerService.unbookmark(this.seriesId, this.volumeId, this.chapterId, pageNum + 1));
|
||||
forkJoin(apis).pipe(take(1)).subscribe(() => {
|
||||
delete this.bookmarks[pageNum];
|
||||
if (isDouble) delete this.bookmarks[pageNum + 1];
|
||||
});
|
||||
} else {
|
||||
let apis = [this.readerService.bookmark(this.seriesId, this.volumeId, this.chapterId, pageNum)];
|
||||
if (isDouble) apis.push(this.readerService.bookmark(this.seriesId, this.volumeId, this.chapterId, pageNum + 1));
|
||||
forkJoin(apis).pipe(take(1)).subscribe(() => {
|
||||
this.bookmarks[pageNum] = 1;
|
||||
if (isDouble) this.bookmarks[pageNum + 1] = 1;
|
||||
});
|
||||
}
|
||||
|
||||
// Show an effect on the image to show that it was bookmarked
|
||||
this.showBookmarkEffectEvent.next(pageNum);
|
||||
if (this.readerMode != ReaderMode.Webtoon) {
|
||||
if (this.readerMode === ReaderMode.Webtoon) return;
|
||||
|
||||
let elements:Array<Element | ElementRef> = [];
|
||||
if (this.renderWithCanvas && this.canvas) {
|
||||
elements.push(this.canvas?.nativeElement);
|
||||
} else {
|
||||
const image1 = this.document.querySelector('#image-1');
|
||||
if (image1 != null) elements.push(image1);
|
||||
let elements:Array<Element | ElementRef> = [];
|
||||
if (this.renderWithCanvas && this.canvas) {
|
||||
elements.push(this.canvas?.nativeElement);
|
||||
} else {
|
||||
const image1 = this.document.querySelector('#image-1');
|
||||
if (image1 != null) elements.push(image1);
|
||||
|
||||
if (this.layoutMode === LayoutMode.Double) {
|
||||
const image2 = this.document.querySelector('#image-2');
|
||||
if (image2 != null) elements.push(image2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (elements.length > 0) {
|
||||
elements.forEach(elem => this.renderer.addClass(elem, 'bookmark-effect'));
|
||||
setTimeout(() => {
|
||||
elements.forEach(elem => this.renderer.removeClass(elem, 'bookmark-effect'));
|
||||
}, 1000);
|
||||
if (this.layoutMode !== LayoutMode.Single) {
|
||||
const image2 = this.document.querySelector('#image-2');
|
||||
if (image2 != null) elements.push(image2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (elements.length > 0) {
|
||||
elements.forEach(elem => this.renderer.addClass(elem, 'bookmark-effect'));
|
||||
setTimeout(() => {
|
||||
elements.forEach(elem => this.renderer.removeClass(elem, 'bookmark-effect'));
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export class FilterSettings {
|
|||
languageDisabled = false;
|
||||
publicationStatusDisabled = false;
|
||||
searchNameDisabled = false;
|
||||
releaseYearDisabled = false;
|
||||
presets: SeriesFilter | undefined;
|
||||
/**
|
||||
* Should the filter section be open by default
|
||||
|
|
|
|||
|
|
@ -325,6 +325,21 @@
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-2 me-3">
|
||||
<form [formGroup]="releaseYearRange" class="d-flex justify-content-between">
|
||||
<div class="mb-3">
|
||||
<label for="release-year-min" class="form-label">Release Year</label>
|
||||
<input type="text" id="release-year-min" formControlName="min" class="form-control" style="width: 62px" placeholder="Min">
|
||||
</div>
|
||||
<div style="margin-top: 37px !important">
|
||||
<i class="fa-solid fa-minus" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="mb-3" style="margin-top: 0.5rem">
|
||||
<label for="release-year-max" class="form-label"><span class="visually-hidden">Max</span></label>
|
||||
<input type="text" id="release-year-max" formControlName="max" class="form-control" style="width: 62px" placeholder="Max">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-2 me-3">
|
||||
<form [formGroup]="sortGroup">
|
||||
<div class="mb-3">
|
||||
|
|
@ -341,11 +356,11 @@
|
|||
<option [value]="SortField.LastModified">Last Modified</option>
|
||||
<option [value]="SortField.LastChapterAdded">Item Added</option>
|
||||
<option [value]="SortField.TimeToRead">Time to Read</option>
|
||||
<option [value]="SortField.ReleaseYear">Release Year</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-2 me-3"></div>
|
||||
<div class="col-md-2 me-3 mt-4">
|
||||
<button class="btn btn-secondary col-12" (click)="clear()">Clear</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { distinctUntilChanged, forkJoin, map, Observable, of, ReplaySubject, Subject, takeUntil } from 'rxjs';
|
||||
import { FilterUtilitiesService } from '../shared/_services/filter-utilities.service';
|
||||
|
|
@ -69,6 +69,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||
readProgressGroup!: FormGroup;
|
||||
sortGroup!: FormGroup;
|
||||
seriesNameGroup!: FormGroup;
|
||||
releaseYearRange!: FormGroup;
|
||||
isAscendingSort: boolean = true;
|
||||
|
||||
updateApplied: number = 0;
|
||||
|
|
@ -120,6 +121,11 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||
seriesNameQuery: new FormControl({value: this.filter.seriesNameQuery || '', disabled: this.filterSettings.searchNameDisabled}, [])
|
||||
});
|
||||
|
||||
this.releaseYearRange = new FormGroup({
|
||||
min: new FormControl({value: undefined, disabled: this.filterSettings.releaseYearDisabled}, [Validators.min(1000), Validators.max(9999)]),
|
||||
max: new FormControl({value: undefined, disabled: this.filterSettings.releaseYearDisabled}, [Validators.min(1000), Validators.max(9999)])
|
||||
});
|
||||
|
||||
this.readProgressGroup.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(changes => {
|
||||
this.filter.readStatus.read = this.readProgressGroup.get('read')?.value;
|
||||
this.filter.readStatus.inProgress = this.readProgressGroup.get('inProgress')?.value;
|
||||
|
|
@ -163,6 +169,15 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.releaseYearRange.valueChanges.pipe(
|
||||
distinctUntilChanged(),
|
||||
takeUntil(this.onDestroy)
|
||||
)
|
||||
.subscribe(changes => {
|
||||
this.filter.releaseYearRange = {min: this.releaseYearRange.get('min')?.value, max: this.releaseYearRange.get('max')?.value};
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.loadFromPresetsAndSetup();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -336,6 +336,7 @@ export class FilterUtilitiesService {
|
|||
languages: [],
|
||||
publicationStatus: [],
|
||||
seriesNameQuery: '',
|
||||
releaseYearRange: null
|
||||
};
|
||||
|
||||
return data;
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ import { IconAndTitleComponent } from './icon-and-title/icon-and-title.component
|
|||
PersonBadgeComponent, // Used Series Detail
|
||||
BadgeExpanderComponent, // Used Series Detail/Metadata
|
||||
|
||||
IconAndTitleComponent // Used in Series Detail/Metadata
|
||||
IconAndTitleComponent, // Used in Series Detail/Metadata
|
||||
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@
|
|||
<label for="settings-backgroundcolor-option" class="form-label">Background Color</label>
|
||||
<input [value]="user.preferences.backgroundColor"
|
||||
class="form-control"
|
||||
(colorPickerChange)="settingsForm.markAsTouched()"
|
||||
(colorPickerChange)="handleBackgroundColorChange()"
|
||||
[style.background]="user.preferences.backgroundColor"
|
||||
[cpAlphaChannel]="'disabled'"
|
||||
[(colorPicker)]="user.preferences.backgroundColor"/>
|
||||
|
|
|
|||
|
|
@ -252,4 +252,10 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||
transformKeyToOpdsUrl(key: string) {
|
||||
return `${location.origin}/api/opds/${key}`;
|
||||
}
|
||||
|
||||
handleBackgroundColorChange() {
|
||||
this.settingsForm.markAsDirty();
|
||||
this.settingsForm.markAsTouched();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@
|
|||
--manga-reader-overlay-filter: blur(10px);
|
||||
--manga-reader-overlay-bg-color: rgba(0,0,0,0.5);
|
||||
--manga-reader-overlay-text-color: white;
|
||||
--manga-reader-bg-color: black;
|
||||
--manga-reader-bg-color: black; // TODO: Remove this
|
||||
--manga-reader-next-highlight-bg-color: rgba(65, 225, 100, 0.5);
|
||||
--manga-reader-prev-highlight-bg-color: rgba(65, 105, 225, 0.5);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue