Fit Split to Screen (#769)

* Updated readme with new host information and new feature site.

* Implemented basic fit to screen splitting option for manga reader such that the reader will try to fit the whole cover on the screen via scaling it.

Updated a bunch of defaults in the preferences to give a better experience for first installs.

* Refactored the stat scheduling code slightly to clean it up and have better logging.

* Replaced @import with @use to lower css bundling.

* Changed up the defaults for the reading preferences to give a better experience. Fixed a duplicate render on automatic scaling due to emitting a valuechange with automatic scaling changing fit.

Implemented basic form of fit to screen. Still needs some tweaking and optimization.

* Update link to new feature server and update kavita homepage to use www.

* Updated the serverInfo to match backend. Tweaked some of the css for the changelog

* Added publish date for changelog

* First page works except for tablet

* I'm stumped, taking a break

* Hide the arrow for nav events

* Ensure specials in reading lists don't have their extensions visible

* Testing out removing no-connection

* Fixed a bug in infinite scroller where next chapter spacer when clicked would emit for prev chapter load. Fixed an issue where next/prev chapter loaders would execute when they shouldn't.

* Fit Split is working in all cases as of this code. New optimization is still needed.

* Fit to screen is now working well

* Updated the bookmark effect to look much better

* Updated new issue template to inform users to request features on our site.

* Removed an empty migration
This commit is contained in:
Joseph Milazzo 2021-11-18 08:55:52 -06:00 committed by GitHub
parent 199398df95
commit 3bfbd042a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 227 additions and 108 deletions

View file

@ -28,7 +28,7 @@
<ng-container *ngFor="let item of webtoonImages | async; let index = index;">
<img src="{{item.src}}" style="display: block" class="mx-auto {{pageNum === item.page && showDebugOutline() ? 'active': ''}} {{areImagesWiderThanWindow ? 'full-width' : ''}}" *ngIf="pageNum >= pageNum - bufferPages && pageNum <= pageNum + bufferPages" rel="nofollow" alt="image" (load)="onImageLoad($event)" id="page-{{item.page}}" [attr.page]="item.page" ondragstart="return false;" onselectstart="return false;">
</ng-container>
<div *ngIf="atBottom" class="spacer bottom" role="alert" (click)="loadPrevChapter.emit()">
<div *ngIf="atBottom" class="spacer bottom" role="alert" (click)="loadNextChapter.emit()">
<div>
<button class="btn btn-icon mx-auto">
<i class="fa fa-angle-double-down animate" aria-hidden="true"></i>

View file

@ -102,9 +102,7 @@
<div class="col-6">
<div class="form-group">
<select class="form-control" id="page-splitting" formControlName="pageSplitOption">
<option [value]="1">Right to Left</option>
<option [value]="0">Left to Right</option>
<option [value]="2">None</option>
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text}}</option>
</select>
</div>
</div>

View file

@ -1,4 +1,4 @@
@import '../../theme/colors';
@use '../../theme/colors';
$center-width: 50%;
$side-width: 25%;
@ -178,7 +178,7 @@ canvas {
height: 2px;
}
.custom-slider .ngx-slider .ngx-slider-selection {
background: $primary-color;
background: colors.$primary-color;
}
.custom-slider .ngx-slider .ngx-slider-pointer {
@ -186,7 +186,7 @@ canvas {
height: 16px;
top: auto; /* to remove the default positioning */
bottom: 0;
background-color: $primary-color; // #333;
background-color: colors.$primary-color; // #333;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
@ -217,7 +217,7 @@ canvas {
}
.custom-slider .ngx-slider .ngx-slider-tick.ngx-slider-selected {
background: $primary-color;
background: colors.$primary-color;
}
}
@ -237,19 +237,14 @@ canvas {
.bookmark-effect {
animation: bookmark 1s cubic-bezier(0.165, 0.84, 0.44, 1);
animation: bookmark 0.7s cubic-bezier(0.165, 0.84, 0.44, 1);
}
@keyframes bookmark {
0%, 100% {
filter: opacity(1);
border: 0px;
}
50% {
filter: opacity(0.25);
border: 5px solid colors.$primary-color;
}
}
// DEBUG
.active-image {
border: 5px solid red;
}
}

View file

@ -12,7 +12,7 @@ import { ScalingOption } from '../_models/preferences/scaling-option';
import { PageSplitOption } from '../_models/preferences/page-split-option';
import { forkJoin, ReplaySubject, Subject } from 'rxjs';
import { ToastrService } from 'ngx-toastr';
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
import { KEY_CODES, UtilityService, Breakpoint } 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';
@ -20,7 +20,7 @@ import { ChangeContext, LabelType, Options } from '@angular-slider/ngx-slider';
import { trigger, state, style, transition, animate } from '@angular/animations';
import { ChapterInfo } from './_models/chapter-info';
import { COLOR_FILTER, FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from './_models/reader-enums';
import { scalingOptions } from '../_models/preferences/preferences';
import { pageSplitOptions, scalingOptions } from '../_models/preferences/preferences';
import { READER_MODE } from '../_models/preferences/reader-mode';
import { MangaFormat } from '../_models/manga-format';
import { LibraryService } from '../_services/library.service';
@ -96,12 +96,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
scalingOptions = scalingOptions;
readingDirection = ReadingDirection.LeftToRight;
scalingOption = ScalingOption.FitToHeight;
pageSplitOption = PageSplitOption.SplitRightToLeft;
pageSplitOption = PageSplitOption.FitSplit;
currentImageSplitPart: SPLIT_PAGE_PART = SPLIT_PAGE_PART.NO_SPLIT;
pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD;
colorMode: COLOR_FILTER = COLOR_FILTER.NONE;
autoCloseMenu: boolean = true;
readerMode: READER_MODE = READER_MODE.MANGA_LR;
pageSplitOptions = pageSplitOptions;
isLoading = true;
@ -266,6 +268,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return ReadingDirection;
}
get PageSplitOption(): typeof PageSplitOption {
return PageSplitOption;
}
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
public readerService: ReaderService, private location: Location,
private formBuilder: FormBuilder, private navService: NavService,
@ -313,7 +319,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.generalSettingsForm = this.formBuilder.group({
autoCloseMenu: this.autoCloseMenu,
pageSplitOption: this.pageSplitOption + '',
pageSplitOption: this.pageSplitOption,
fittingOption: this.translateScalingOption(this.scalingOption)
});
@ -321,8 +327,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.generalSettingsForm.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((changes: SimpleChanges) => {
this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value;
// On change of splitting, re-render the page if the page is already split
const needsSplitting = this.canvasImage.width > this.canvasImage.height;
const needsSplitting = this.isCoverImage();
// If we need to split on a menu change, then we need to re-render.
if (needsSplitting) {
this.loadPage();
}
@ -619,15 +625,20 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
isSplitLeftToRight() {
return (this.generalSettingsForm?.get('pageSplitOption')?.value + '') === (PageSplitOption.SplitLeftToRight + '');
return parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10) === PageSplitOption.SplitLeftToRight;
}
/**
*
* @returns If the current model reflects no split of fit split
*/
isNoSplit() {
return (this.generalSettingsForm?.get('pageSplitOption')?.value + '') === (PageSplitOption.NoSplit + '');
const splitValue = parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10);
return splitValue === PageSplitOption.NoSplit || splitValue === PageSplitOption.FitSplit;
}
updateSplitPage() {
const needsSplitting = this.canvasImage.width > this.canvasImage.height;
const needsSplitting = this.isCoverImage();
if (!needsSplitting || this.isNoSplit()) {
this.currentImageSplitPart = SPLIT_PAGE_PART.NO_SPLIT;
return;
@ -739,6 +750,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
loadNextChapter() {
if (this.nextPageDisabled) { return; }
if (this.nextChapterDisabled) { return; }
this.isLoading = true;
if (this.nextChapterId === CHAPTER_ID_NOT_FETCHED || this.nextChapterId === this.chapterId) {
this.readerService.getNextChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
@ -752,6 +764,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
loadPrevChapter() {
if (this.prevPageDisabled) { return; }
if (this.prevChapterDisabled) { return; }
this.isLoading = true;
this.continuousChaptersStack.pop();
const prevChapter = this.continuousChaptersStack.peek();
@ -819,21 +832,23 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (needsScaling) {
this.canvas.nativeElement.width = isSafari ? 4_096 : 16_384;
this.canvas.nativeElement.height = isSafari ? 4_096 : 16_384;
} else if (this.isCoverImage()) {
//this.canvas.nativeElement.width = this.canvasImage.width / 2;
//this.canvas.nativeElement.height = this.canvasImage.height;
} else {
this.canvas.nativeElement.width = this.canvasImage.width;
this.canvas.nativeElement.height = this.canvasImage.height;
}
}
return true;
}
renderPage() {
if (this.ctx && this.canvas) {
this.canvasImage.onload = null;
if (!this.setCanvasSize()) return;
this.setCanvasSize();
const needsSplitting = this.canvasImage.width > this.canvasImage.height;
const needsSplitting = this.isCoverImage();
this.updateSplitPage();
if (needsSplitting && this.currentImageSplitPart === SPLIT_PAGE_PART.LEFT_PART) {
@ -844,31 +859,39 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.ctx.drawImage(this.canvasImage, 0, 0, this.canvasImage.width, this.canvasImage.height, -this.canvasImage.width / 2, 0, this.canvasImage.width, this.canvasImage.height);
} else {
if (!this.firstPageRendered && this.scalingOption === ScalingOption.Automatic) {
let newScale = this.generalSettingsForm.get('fittingOption')?.value;
this.updateScalingForFirstPageRender();
}
// Fit Split on a page that needs splitting
if (this.shouldRenderAsFitSplit()) {
const windowWidth = window.innerWidth
|| document.documentElement.clientWidth
|| document.body.clientWidth;
const windowHeight = window.innerHeight
|| document.documentElement.clientHeight
|| document.body.clientHeight;
const widthRatio = windowWidth / this.canvasImage.width;
const heightRatio = windowHeight / this.canvasImage.height;
// Given that we now have image dimensions, assuming this isn't a split image,
// Try to reset one time based on who's dimension (width/height) is smaller
if (widthRatio < heightRatio) {
newScale = FITTING_OPTION.WIDTH;
} else if (widthRatio > heightRatio) {
newScale = FITTING_OPTION.HEIGHT;
|| document.documentElement.clientHeight
|| document.body.clientHeight;
// If the user's screen is wider than the image, just pretend this is no split, as it will render nicer
this.canvas.nativeElement.width = windowWidth;
this.canvas.nativeElement.height = windowHeight;
const ratio = this.canvasImage.width / this.canvasImage.height;
let newWidth = windowWidth;
let newHeight = newWidth / ratio;
if (newHeight > windowHeight) {
newHeight = windowHeight;
newWidth = newHeight * ratio;
}
this.generalSettingsForm.get('fittingOption')?.setValue(newScale);
this.firstPageRendered = true;
// Optimization: When the screen is larger than newWidth, allow no split rendering to occur for a better fit
if (windowWidth > newWidth) {
this.ctx.drawImage(this.canvasImage, 0, 0);
} else {
this.ctx.drawImage(this.canvasImage, 0, 0, newWidth, newHeight);
}
} else {
this.ctx.drawImage(this.canvasImage, 0, 0);
}
this.ctx.drawImage(this.canvasImage, 0, 0);
}
// Reset scroll on non HEIGHT Fits
if (this.getFit() !== FITTING_OPTION.HEIGHT) {
window.scrollTo(0, 0);
@ -878,6 +901,41 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.isLoading = false;
}
updateScalingForFirstPageRender() {
const windowWidth = window.innerWidth
|| document.documentElement.clientWidth
|| document.body.clientWidth;
const windowHeight = window.innerHeight
|| document.documentElement.clientHeight
|| document.body.clientHeight;
const needsSplitting = this.isCoverImage();
let newScale = this.generalSettingsForm.get('fittingOption')?.value;
const widthRatio = windowWidth / (this.canvasImage.width / (needsSplitting ? 2 : 1));
const heightRatio = windowHeight / (this.canvasImage.height);
// Given that we now have image dimensions, assuming this isn't a split image,
// Try to reset one time based on who's dimension (width/height) is smaller
if (widthRatio < heightRatio) {
newScale = FITTING_OPTION.WIDTH;
} else if (widthRatio > heightRatio) {
newScale = FITTING_OPTION.HEIGHT;
}
this.firstPageRendered = true;
this.generalSettingsForm.get('fittingOption')?.setValue(newScale, {emitEvent: false});
}
isCoverImage() {
return this.canvasImage.width > this.canvasImage.height;
}
shouldRenderAsFitSplit() {
if (!this.isCoverImage() || parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10) !== PageSplitOption.FitSplit) return false;
return true;
}
prefetch() {
let index = 1;