Reader Fixes and Enhancements (#880)

* Don't show an exception when bookmarking doesn't have anything to change.

* Cleaned up the bookmark code a bit.

* Implemented fullscreen mode in the web reader. Refactored User Settings to move Password and 3rd Party Clients to a tab rather than accordion. Removed color filters for web reader.

* Implemented fullscreen mode into book reader

* Added some code for toggling fullscreen which re-renders the screen to ensure the fitting works optimially

* Fixed an issue where moving from FitToScreen -> Split (L/R) wouldn't render the screen correctly due to canvas not being reset.

* Fixed bad optimization and scaling when drawing fit to screen

* Removed left/right highlights on page direction change in favor for icons. Double arrow will dictate the page change.

* Reduced overlay auto close time to 3 seconds

* Updated the paginging direction overlay to use icons and colors. Added a blur effect on menus

* Removed debug flags
This commit is contained in:
Joseph Milazzo 2022-01-02 18:10:37 -07:00 committed by GitHub
parent ca5c67020e
commit 720c52f494
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1620 additions and 166 deletions

View File

@ -468,18 +468,15 @@ namespace API.Controllers
_unitOfWork.UserRepository.Update(user); _unitOfWork.UserRepository.Update(user);
} }
await _unitOfWork.CommitAsync();
if (await _unitOfWork.CommitAsync())
{
return Ok();
}
} }
catch (Exception) catch (Exception)
{ {
await _unitOfWork.RollbackAsync(); await _unitOfWork.RollbackAsync();
return BadRequest("Could not save bookmark");
} }
return BadRequest("Could not save bookmark"); return Ok();
} }
/// <summary> /// <summary>

View File

@ -169,7 +169,7 @@ namespace API.Controllers
_logger.LogInformation("Server Settings updated"); _logger.LogInformation("Server Settings updated");
_taskScheduler.ScheduleTasks(); await _taskScheduler.ScheduleTasks();
return Ok(updateSettingsDto); return Ok(updateSettingsDto);
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Data.Migrations
{
public partial class FullscreenPref : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "FullscreenMode",
table: "AppUserPreferences",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "FullscreenMode",
table: "AppUserPreferences");
}
}
}

View File

@ -183,6 +183,9 @@ namespace API.Data.Migrations
b.Property<bool>("BookReaderTapToPaginate") b.Property<bool>("BookReaderTapToPaginate")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("FullscreenMode")
.HasColumnType("INTEGER");
b.Property<int>("PageSplitOption") b.Property<int>("PageSplitOption")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

View File

@ -514,7 +514,7 @@ public class SeriesRepository : ISeriesRepository
var retSeries = query.Where(s => s.AppUserId == userId var retSeries = query.Where(s => s.AppUserId == userId
&& s.PagesRead > 0 && s.PagesRead > 0
&& s.PagesRead < s.Series.Pages) && s.PagesRead < s.Series.Pages)
.OrderByDescending(s => s.LastModified) .OrderByDescending(s => s.LastModified) // TODO: This needs to be Chapter Created (Max)
.ThenByDescending(s => s.Series.LastModified) .ThenByDescending(s => s.Series.LastModified)
.Select(s => s.Series) .Select(s => s.Series)
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider) .ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)

View File

@ -4,10 +4,19 @@ namespace API.Entities.Enums
{ {
public enum LibraryType public enum LibraryType
{ {
/// <summary>
/// Uses Manga regex for filename parsing
/// </summary>
[Description("Manga")] [Description("Manga")]
Manga = 0, Manga = 0,
/// <summary>
/// Uses Comic regex for filename parsing
/// </summary>
[Description("Comic")] [Description("Comic")]
Comic = 1, Comic = 1,
/// <summary>
/// Uses Manga regex for filename parsing also uses epub metadata
/// </summary>
[Description("Book")] [Description("Book")]
Book = 2, Book = 2,
} }

View File

@ -1,3 +1,4 @@
import { PageSplitOption } from './page-split-option'; import { PageSplitOption } from './page-split-option';
import { READER_MODE } from './reader-mode'; import { READER_MODE } from './reader-mode';
import { ReadingDirection } from './reading-direction'; import { ReadingDirection } from './reading-direction';

View File

@ -193,4 +193,48 @@ export class ReaderService {
} }
return params; return params;
} }
enterFullscreen(el: Element, callback?: VoidFunction) {
if (!document.fullscreenElement) {
if (el.requestFullscreen) {
el.requestFullscreen().then(() => {
if (callback) {
callback();
}
});
}
// else if (el.mozRequestFullScreen) {
// el.mozRequestFullScreen();
// } else if (el.webkitRequestFullscreen) {
// el.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
// } else if (el.msRequestFullscreen) {
// el.msRequestFullscreen();
// }
}
}
exitFullscreen(callback?: VoidFunction) {
if (document.exitFullscreen && this.checkFullscreenMode()) {
document.exitFullscreen().then(() => {
if (callback) {
callback();
}
});
}
// else if (document.msExitFullscreen) {
// document.msExitFullscreen();
// } else if (document.mozCancelFullScreen) {
// document.mozCancelFullScreen();
// } else if (document.webkitExitFullscreen) {
// document.webkitExitFullscreen();
// }
}
/**
*
* @returns If document is in fullscreen mode
*/
checkFullscreenMode() {
return document.fullscreenElement != null;
}
} }

View File

@ -1,4 +1,4 @@
<div class="container-flex {{darkMode ? 'dark-mode' : ''}}"> <div class="container-flex {{darkMode ? 'dark-mode' : ''}}" #reader>
<div class="fixed-top" #stickyTop> <div class="fixed-top" #stickyTop>
<a class="sr-only sr-only-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">Skip to main content</a> <a class="sr-only sr-only-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">Skip to main content</a>
<ng-container [ngTemplateOutlet]="actionBar"></ng-container> <ng-container [ngTemplateOutlet]="actionBar"></ng-container>
@ -57,6 +57,17 @@
<span class="sr-only" id="tap-pagination-help">The ability to click the sides of the page to page left and right</span> <span class="sr-only" id="tap-pagination-help">The ability to click the sides of the page to page left and right</span>
<button (click)="toggleClickToPaginate()" class="btn btn-icon" aria-labelledby="tap-pagination"><i class="fa fa-arrows-alt-h {{clickToPaginate ? 'icon-primary-color' : ''}}" aria-hidden="true"></i><span *ngIf="darkMode">&nbsp;{{clickToPaginate ? 'On' : 'Off'}}</span></button> <button (click)="toggleClickToPaginate()" class="btn btn-icon" aria-labelledby="tap-pagination"><i class="fa fa-arrows-alt-h {{clickToPaginate ? 'icon-primary-color' : ''}}" aria-hidden="true"></i><span *ngIf="darkMode">&nbsp;{{clickToPaginate ? 'On' : 'Off'}}</span></button>
</div> </div>
<div class="controls">
<label id="fullscreen">Fullscreen&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="fullscreenTooltip" role="button" tabindex="0" aria-describedby="fullscreen-help"></i></label>
<ng-template #fullscreenTooltip>Put reader in fullscreen mode</ng-template>
<span class="sr-only" id="fullscreen-help">
<ng-container [ngTemplateOutlet]="fullscreenTooltip"></ng-container>
</span>
<button (click)="toggleFullscreen()" class="btn btn-icon" aria-labelledby="fullscreen">
<i class="fa {{this.isFullscreen ? 'fa-compress-alt' : 'fa-expand-alt'}} {{isFullscreen ? 'icon-primary-color' : ''}}" aria-hidden="true"></i>
<span *ngIf="darkMode">&nbsp;{{isFullscreen ? 'Exit' : 'Enter'}}</span>
</button>
</div>
<div class="row no-gutters justify-content-between"> <div class="row no-gutters justify-content-between">
<button (click)="resetSettings()" class="btn btn-primary col">Reset to Defaults</button> <button (click)="resetSettings()" class="btn btn-primary col">Reset to Defaults</button>
</div> </div>

View File

@ -111,6 +111,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('readingHtml', {static: false}) readingHtml!: ElementRef<HTMLDivElement>; @ViewChild('readingHtml', {static: false}) readingHtml!: ElementRef<HTMLDivElement>;
@ViewChild('readingSection', {static: false}) readingSectionElemRef!: ElementRef<HTMLDivElement>; @ViewChild('readingSection', {static: false}) readingSectionElemRef!: ElementRef<HTMLDivElement>;
@ViewChild('stickyTop', {static: false}) stickyTopElemRef!: ElementRef<HTMLDivElement>; @ViewChild('stickyTop', {static: false}) stickyTopElemRef!: ElementRef<HTMLDivElement>;
@ViewChild('reader', {static: true}) reader!: ElementRef;
/** /**
* Next Chapter Id. This is not garunteed to be a valid ChapterId. Prefetched on page load (non-blocking). * Next Chapter Id. This is not garunteed to be a valid ChapterId. Prefetched on page load (non-blocking).
@ -185,6 +186,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
*/ */
originalBodyColor: string | undefined; originalBodyColor: string | undefined;
/**
* If the web browser is in fullscreen mode
*/
isFullscreen: boolean = false;
darkModeStyles = ` darkModeStyles = `
*:not(input), *:not(select), *:not(code), *:not(:link), *:not(.ngx-toastr) { *:not(input), *:not(select), *:not(code), *:not(:link), *:not(.ngx-toastr) {
color: #dcdcdc !important; color: #dcdcdc !important;
@ -365,6 +371,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.clickToPaginateVisualOverlayTimeout2 = undefined; this.clickToPaginateVisualOverlayTimeout2 = undefined;
} }
this.readerService.exitFullscreen();
this.onDestroy.next(); this.onDestroy.next();
this.onDestroy.complete(); this.onDestroy.complete();
} }
@ -1014,4 +1022,17 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */}); this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */});
} }
toggleFullscreen() {
this.isFullscreen = this.readerService.checkFullscreenMode();
if (this.isFullscreen) {
this.readerService.exitFullscreen(() => {
this.isFullscreen = false;
});
} else {
this.readerService.enterFullscreen(this.reader.nativeElement, () => {
this.isFullscreen = true;
});
}
}
} }

View File

@ -15,8 +15,3 @@ export enum PAGING_DIRECTION {
BACKWARDS = -1, BACKWARDS = -1,
} }
export enum COLOR_FILTER {
NONE = '',
SEPIA = 'filter-sepia',
DARK = 'filter-dark'
}

View File

@ -1,4 +1,4 @@
<div class="reader"> <div class="reader" #reader>
<div class="fixed-top overlay" *ngIf="menuOpen" [@slideFromTop]="menuOpen"> <div class="fixed-top overlay" *ngIf="menuOpen" [@slideFromTop]="menuOpen">
<div style="display: flex; margin-top: 5px;"> <div style="display: flex; margin-top: 5px;">
<button class="btn btn-icon" style="height: 100%" title="Back" (click)="closeReader()"> <button class="btn btn-icon" style="height: 100%" title="Back" (click)="closeReader()">
@ -24,7 +24,7 @@
</ng-container> </ng-container>
<div (click)="toggleMenu()" class="reading-area"> <div (click)="toggleMenu()" class="reading-area">
<canvas #content class="{{getFittingOptionClass()}} {{this.colorMode}} {{readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD ? '' : 'd-none'}} " <canvas #content class="{{getFittingOptionClass()}} {{readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD ? '' : 'd-none'}} {{showClickOverlay ? 'blur' : ''}}"
ondragstart="return false;" onselectstart="return false;"> ondragstart="return false;" onselectstart="return false;">
</canvas> </canvas>
<div class="webtoon-images" *ngIf="readerMode === READER_MODE.WEBTOON && !isLoading"> <div class="webtoon-images" *ngIf="readerMode === READER_MODE.WEBTOON && !isLoading">
@ -39,8 +39,18 @@
[bookmarkPage]="showBookmarkEffectEvent"></app-infinite-scroller> [bookmarkPage]="showBookmarkEffectEvent"></app-infinite-scroller>
</div> </div>
<ng-container *ngIf="readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD"> <ng-container *ngIf="readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD">
<div class="{{readerMode === READER_MODE.MANGA_LR ? 'right' : 'bottom'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, 'right')"></div> <div class="pagination-area {{readerMode === READER_MODE.MANGA_LR ? 'right' : 'bottom'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, 'right')">
<div class="{{readerMode === READER_MODE.MANGA_LR ? 'left' : 'top'}} {{clickOverlayClass('left')}}" (click)="handlePageChange($event, 'left')"></div> <div *ngIf="showClickOverlay">
<i class="fa fa-angle-{{readingDirection === ReadingDirection.LeftToRight ? 'double-' : ''}}{{readerMode === READER_MODE.MANGA_LR ? 'right' : 'down'}}"
title="Next Page" aria-hidden="true"></i>
</div>
</div>
<div class="pagination-area {{readerMode === READER_MODE.MANGA_LR ? 'left' : 'top'}} {{clickOverlayClass('left')}}" (click)="handlePageChange($event, 'left')">
<div *ngIf="showClickOverlay">
<i class="fa fa-angle-{{readingDirection === ReadingDirection.RightToLeft ? 'double-' : ''}}{{readerMode === READER_MODE.MANGA_LR ? 'left' : 'up'}}"
title="Previous Page" aria-hidden="true"></i>
</div>
</div>
</ng-container> </ng-container>
</div> </div>
@ -78,9 +88,9 @@
</button> </button>
</div> </div>
<div class="col"> <div class="col">
<button class="btn btn-icon {{this.colorMode}}" [disabled]="readerMode === READER_MODE.WEBTOON" title="Color Options: {{colorOptionName}}" (click)="toggleColorMode();resetMenuCloseTimer();"> <button class="btn btn-icon" title="{{this.isFullscreen ? 'Collapse' : 'Fullscreen'}}" (click)="toggleFullscreen();resetMenuCloseTimer();">
<i class="fa fa-palette" aria-hidden="true"></i> <i class="fa {{this.isFullscreen ? 'fa-compress-alt' : 'fa-expand-alt'}}" aria-hidden="true"></i>
<span class="sr-only"></span> <span class="sr-only">{{this.isFullscreen ? 'Collapse' : 'Fullscreen'}}</span>
</button> </button>
</div> </div>
<div class="col"> <div class="col">

View File

@ -6,8 +6,6 @@ $side-width: 25%;
$dash-width: 3px; $dash-width: 3px;
$pointer-offset: 5px; $pointer-offset: 5px;
$secondary-color: #CCC;
@media(min-width: 600px) { @media(min-width: 600px) {
.overlay .left .i { .overlay .left .i {
@ -18,7 +16,6 @@ $secondary-color: #CCC;
} }
} }
.btn-icon { .btn-icon {
color: white; color: white;
} }
@ -35,6 +32,10 @@ canvas {
} }
} }
.loading { .loading {
position: absolute; position: absolute;
left: 48%; left: 48%;
@ -48,14 +49,6 @@ canvas {
white-space: nowrap; white-space: nowrap;
} }
.filter-dark {
filter: brightness(0.65);
}
.filter-sepia {
filter: sepia(80%) hue-rotate(349deg) saturate(200%) brightness(0.65);
}
.bottom-menu { .bottom-menu {
padding: 20px 20px; padding: 20px 20px;
} }
@ -63,6 +56,7 @@ canvas {
.overlay { .overlay {
background-color: rgba(0,0,0,0.5); background-color: rgba(0,0,0,0.5);
backdrop-filter: blur(10px);
color: white; color: white;
} }
@ -226,14 +220,27 @@ canvas {
width: 100%; width: 100%;
} }
.highlight { .pagination-area {
display: flex;
align-items: center;
justify-content: center;
i {
color: white;
font-size: 42px;
}
}
.highlight {
background-color: rgba(65, 225, 100, 0.5) !important; background-color: rgba(65, 225, 100, 0.5) !important;
animation: fadein .5s both; animation: fadein .5s both;
} backdrop-filter: blur(10px);
.highlight-2 { }
.highlight-2 {
background-color: rgba(65, 105, 225, 0.5) !important; background-color: rgba(65, 105, 225, 0.5) !important;
animation: fadein .5s both; animation: fadein .5s both;
} backdrop-filter: blur(10px);
}
.bookmark-effect { .bookmark-effect {

View File

@ -1,5 +1,5 @@
import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, OnDestroy, OnInit, Renderer2, SimpleChanges, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, Inject, OnDestroy, OnInit, Renderer2, SimpleChanges, ViewChild } from '@angular/core';
import { Location } from '@angular/common'; import { DOCUMENT, Location } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { take, takeUntil } from 'rxjs/operators'; import { take, takeUntil } from 'rxjs/operators';
import { User } from '../_models/user'; import { User } from '../_models/user';
@ -19,7 +19,7 @@ import { Stack } from '../shared/data-structures/stack';
import { ChangeContext, LabelType, Options } from '@angular-slider/ngx-slider'; import { ChangeContext, LabelType, Options } from '@angular-slider/ngx-slider';
import { trigger, state, style, transition, animate } from '@angular/animations'; import { trigger, state, style, transition, animate } from '@angular/animations';
import { ChapterInfo } from './_models/chapter-info'; import { ChapterInfo } from './_models/chapter-info';
import { COLOR_FILTER, FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from './_models/reader-enums'; import { FITTING_OPTION, PAGING_DIRECTION, SPLIT_PAGE_PART } from './_models/reader-enums';
import { pageSplitOptions, scalingOptions } from '../_models/preferences/preferences'; import { pageSplitOptions, scalingOptions } from '../_models/preferences/preferences';
import { READER_MODE } from '../_models/preferences/reader-mode'; import { READER_MODE } from '../_models/preferences/reader-mode';
import { MangaFormat } from '../_models/manga-format'; import { MangaFormat } from '../_models/manga-format';
@ -32,7 +32,7 @@ const CHAPTER_ID_NOT_FETCHED = -2;
const CHAPTER_ID_DOESNT_EXIST = -1; const CHAPTER_ID_DOESNT_EXIST = -1;
const ANIMATION_SPEED = 200; const ANIMATION_SPEED = 200;
const OVERLAY_AUTO_CLOSE_TIME = 6000; const OVERLAY_AUTO_CLOSE_TIME = 3000;
const CLICK_OVERLAY_TIMEOUT = 3000; const CLICK_OVERLAY_TIMEOUT = 3000;
@ -99,7 +99,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
pageSplitOption = PageSplitOption.FitSplit; pageSplitOption = PageSplitOption.FitSplit;
currentImageSplitPart: SPLIT_PAGE_PART = SPLIT_PAGE_PART.NO_SPLIT; currentImageSplitPart: SPLIT_PAGE_PART = SPLIT_PAGE_PART.NO_SPLIT;
pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD; pagingDirection: PAGING_DIRECTION = PAGING_DIRECTION.FORWARD;
colorMode: COLOR_FILTER = COLOR_FILTER.NONE; isFullscreen: boolean = false;
autoCloseMenu: boolean = true; autoCloseMenu: boolean = true;
readerMode: READER_MODE = READER_MODE.MANGA_LR; readerMode: READER_MODE = READER_MODE.MANGA_LR;
@ -107,6 +107,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
isLoading = true; isLoading = true;
@ViewChild('reader') reader!: ElementRef;
@ViewChild('content') canvas: ElementRef | undefined; @ViewChild('content') canvas: ElementRef | undefined;
private ctx!: CanvasRenderingContext2D; private ctx!: CanvasRenderingContext2D;
private canvasImage = new Image(); private canvasImage = new Image();
@ -249,17 +250,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} }
} }
get colorOptionName() {
switch(this.colorMode) {
case COLOR_FILTER.NONE:
return 'None';
case COLOR_FILTER.DARK:
return 'Dark';
case COLOR_FILTER.SEPIA:
return 'Sepia';
}
}
get READER_MODE(): typeof READER_MODE { get READER_MODE(): typeof READER_MODE {
return READER_MODE; return READER_MODE;
} }
@ -277,7 +267,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
private formBuilder: FormBuilder, private navService: NavService, private formBuilder: FormBuilder, private navService: NavService,
private toastr: ToastrService, private memberService: MemberService, private toastr: ToastrService, private memberService: MemberService,
private libraryService: LibraryService, private utilityService: UtilityService, private libraryService: LibraryService, private utilityService: UtilityService,
private renderer: Renderer2) { private renderer: Renderer2, @Inject(DOCUMENT) private document: Document) {
this.navService.hideNavBar(); this.navService.hideNavBar();
} }
@ -325,6 +315,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.updateForm(); this.updateForm();
this.generalSettingsForm.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((changes: SimpleChanges) => { this.generalSettingsForm.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((changes: SimpleChanges) => {
this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value; this.autoCloseMenu = this.generalSettingsForm.get('autoCloseMenu')?.value;
const needsSplitting = this.isCoverImage(); const needsSplitting = this.isCoverImage();
@ -365,6 +356,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.onDestroy.complete(); this.onDestroy.complete();
this.goToPageEvent.complete(); this.goToPageEvent.complete();
this.showBookmarkEffectEvent.complete(); this.showBookmarkEffectEvent.complete();
this.readerService.exitFullscreen();
} }
@HostListener('window:keyup', ['$event']) @HostListener('window:keyup', ['$event'])
@ -407,6 +399,17 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} }
} }
clickOverlayClass(side: 'right' | 'left') {
if (!this.showClickOverlay) {
return '';
}
if (this.readingDirection === ReadingDirection.LeftToRight) {
return side === 'right' ? 'highlight' : 'highlight-2';
}
return side === 'right' ? 'highlight-2' : 'highlight';
}
init() { init() {
this.nextChapterId = CHAPTER_ID_NOT_FETCHED; this.nextChapterId = CHAPTER_ID_NOT_FETCHED;
this.prevChapterId = CHAPTER_ID_NOT_FETCHED; this.prevChapterId = CHAPTER_ID_NOT_FETCHED;
@ -631,6 +634,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
/** /**
* *
* @returns If the current model reflects no split of fit split * @returns If the current model reflects no split of fit split
* @remarks Fit to Screen falls under no split
*/ */
isNoSplit() { isNoSplit() {
const splitValue = parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10); const splitValue = parseInt(this.generalSettingsForm?.get('pageSplitOption')?.value, 10);
@ -832,9 +836,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (needsScaling) { if (needsScaling) {
this.canvas.nativeElement.width = isSafari ? 4_096 : 16_384; this.canvas.nativeElement.width = isSafari ? 4_096 : 16_384;
this.canvas.nativeElement.height = 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 { } else {
this.canvas.nativeElement.width = this.canvasImage.width; this.canvas.nativeElement.width = this.canvasImage.width;
this.canvas.nativeElement.height = this.canvasImage.height; this.canvas.nativeElement.height = this.canvasImage.height;
@ -883,11 +884,16 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// Optimization: When the screen is larger than newWidth, allow no split rendering to occur for a better fit // Optimization: When the screen is larger than newWidth, allow no split rendering to occur for a better fit
if (windowWidth > newWidth) { if (windowWidth > newWidth) {
//console.log('Using raw draw');
this.setCanvasSize();
this.ctx.drawImage(this.canvasImage, 0, 0); this.ctx.drawImage(this.canvasImage, 0, 0);
} else { } else {
//console.log('Using scaled draw');
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
this.ctx.drawImage(this.canvasImage, 0, 0, newWidth, newHeight); this.ctx.drawImage(this.canvasImage, 0, 0, newWidth, newHeight);
} }
} else { } else {
//console.log('Normal Render')
this.ctx.drawImage(this.canvasImage, 0, 0); this.ctx.drawImage(this.canvasImage, 0, 0);
} }
} }
@ -984,16 +990,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} }
} }
clickOverlayClass(side: 'right' | 'left') {
if (!this.showClickOverlay) {
return '';
}
if (this.readingDirection === ReadingDirection.LeftToRight) {
return side === 'right' ? 'highlight' : 'highlight-2';
}
return side === 'right' ? 'highlight-2' : 'highlight';
}
sliderDragUpdate(context: ChangeContext) { sliderDragUpdate(context: ChangeContext) {
// This will update the value for value except when in webtoon due to how the webtoon reader // This will update the value for value except when in webtoon due to how the webtoon reader
@ -1079,20 +1075,24 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return goToPageNum; return goToPageNum;
} }
toggleColorMode() { toggleFullscreen() {
switch(this.colorMode) { this.isFullscreen = this.readerService.checkFullscreenMode();
case COLOR_FILTER.NONE: if (this.isFullscreen) {
this.colorMode = COLOR_FILTER.DARK; this.readerService.exitFullscreen(() => {
break; this.isFullscreen = false;
case COLOR_FILTER.DARK: this.firstPageRendered = false;
this.colorMode = COLOR_FILTER.SEPIA; this.render();
break; });
case COLOR_FILTER.SEPIA: } else {
this.colorMode = COLOR_FILTER.NONE; this.readerService.enterFullscreen(this.reader.nativeElement, () => {
break; this.isFullscreen = true;
this.firstPageRendered = false;
this.render();
});
} }
} }
toggleReaderMode() { toggleReaderMode() {
switch(this.readerMode) { switch(this.readerMode) {
case READER_MODE.MANGA_LR: case READER_MODE.MANGA_LR:
@ -1163,4 +1163,14 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.toastr.info('Incognito mode is off. Progress will now start being tracked.'); this.toastr.info('Incognito mode is off. Progress will now start being tracked.');
this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */}); this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */});
} }
getWindowDimensions() {
const windowWidth = window.innerWidth
|| document.documentElement.clientWidth
|| document.body.clientWidth;
const windowHeight = window.innerHeight
|| document.documentElement.clientHeight
|| document.body.clientHeight;
return [windowWidth, windowHeight];
}
} }

View File

@ -2,7 +2,7 @@
<h2>User Dashboard</h2> <h2>User Dashboard</h2>
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-tabs nav-pills"> <ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-tabs nav-pills">
<li *ngFor="let tab of tabs" [ngbNavItem]="tab"> <li *ngFor="let tab of tabs" [ngbNavItem]="tab">
<a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ tab.title | titlecase }}</a> <a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ tab.title | sentenceCase }}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<ng-container *ngIf="tab.fragment === ''"> <ng-container *ngIf="tab.fragment === ''">
<p> <p>
@ -48,7 +48,7 @@
</ng-template> </ng-template>
<ng-template ngbPanelContent> <ng-template ngbPanelContent>
<form [formGroup]="settingsForm" *ngIf="user !== undefined"> <form [formGroup]="settingsForm" *ngIf="user !== undefined">
<h3 id="manga-header">Manga</h3> <h3 id="manga-header">Image Reader</h3>
<div class="form-group"> <div class="form-group">
<label for="settings-reading-direction">Reading Direction</label>&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="readingDirectionTooltip" role="button" tabindex="0"></i> <label for="settings-reading-direction">Reading Direction</label>&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="readingDirectionTooltip" role="button" tabindex="0"></i>
<ng-template #readingDirectionTooltip>Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</ng-template> <ng-template #readingDirectionTooltip>Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</ng-template>
@ -95,7 +95,7 @@
</div> </div>
</div> </div>
<hr> <hr>
<h3>Books</h3> <h3>Book Reader</h3>
<div class="form-group"> <div class="form-group">
<label id="dark-mode-label">Dark Mode</label> <label id="dark-mode-label">Dark Mode</label>
<div class="form-group"> <div class="form-group">
@ -166,15 +166,28 @@
</ng-template> </ng-template>
</ngb-panel> </ngb-panel>
<!--
<ngb-panel id="password-panel" title="Password"> <ngb-panel id="api-panel" title="3rd Party Clients">
<ng-template ngbPanelHeader> <ng-template ngbPanelHeader>
<div class="d-flex align-items-center justify-content-between"> <div class="d-flex align-items-center justify-content-between">
<button ngbPanelToggle class="btn container-fluid text-left pl-0 accordion-header">Password</button> <button ngbPanelToggle class="btn container-fluid text-left pl-0 accordion-header">3rd Party Clients</button>
<span class="pull-right"><i class="fa fa-angle-{{acc.isExpanded('password-panel') ? 'down' : 'up'}}" aria-hidden="true"></i></span> <span class="pull-right"><i class="fa fa-angle-{{acc.isExpanded('api-panel') ? 'down' : 'up'}}" aria-hidden="true"></i></span>
</div> </div>
</ng-template> </ng-template>
<ng-template ngbPanelContent> <ng-template ngbPanelContent>
<p>All 3rd Party clients will either use the API key or the Connection Url below. These are like passwords, keep it private.</p>
<p class="alert alert-warning" role="alert" *ngIf="!opdsEnabled">OPDS is not enabled on this server.</p>
<app-api-key tooltipText="The API key is like a password. Keep it secret, Keep it safe."></app-api-key>
<app-api-key title="OPDS URL" [showRefresh]="false" [transform]="makeUrl"></app-api-key>
</ng-template>
</ngb-panel> -->
</ngb-accordion>
</ng-container>
<ng-container *ngIf="tab.fragment === 'bookmarks'">
<app-series-bookmarks></app-series-bookmarks>
</ng-container>
<ng-container *ngIf="tab.fragment === 'password'">
<ng-container *ngIf="isAuthenticationEnabled || isAdmin; else authDisabled"> <ng-container *ngIf="isAuthenticationEnabled || isAdmin; else authDisabled">
<p>Change your Password</p> <p>Change your Password</p>
<div class="alert alert-danger" role="alert" *ngIf="resetPasswordErrors.length > 0"> <div class="alert alert-danger" role="alert" *ngIf="resetPasswordErrors.length > 0">
@ -211,26 +224,12 @@
<ng-template #authDisabled> <ng-template #authDisabled>
<p class="text-warning">Authentication is disabled on this server. A password is not required to authenticate.</p> <p class="text-warning">Authentication is disabled on this server. A password is not required to authenticate.</p>
</ng-template> </ng-template>
</ng-template> </ng-container>
</ngb-panel> <ng-container *ngIf="tab.fragment === 'clients'">
<ngb-panel id="api-panel" title="3rd Party Clients">
<ng-template ngbPanelHeader>
<div class="d-flex align-items-center justify-content-between">
<button ngbPanelToggle class="btn container-fluid text-left pl-0 accordion-header">3rd Party Clients</button>
<span class="pull-right"><i class="fa fa-angle-{{acc.isExpanded('api-panel') ? 'down' : 'up'}}" aria-hidden="true"></i></span>
</div>
</ng-template>
<ng-template ngbPanelContent>
<p>All 3rd Party clients will either use the API key or the Connection Url below. These are like passwords, keep it private.</p> <p>All 3rd Party clients will either use the API key or the Connection Url below. These are like passwords, keep it private.</p>
<p class="alert alert-warning" role="alert" *ngIf="!opdsEnabled">OPDS is not enabled on this server.</p> <p class="alert alert-warning" role="alert" *ngIf="!opdsEnabled">OPDS is not enabled on this server.</p>
<app-api-key tooltipText="The API key is like a password. Keep it secret, Keep it safe."></app-api-key> <app-api-key tooltipText="The API key is like a password. Keep it secret, Keep it safe."></app-api-key>
<app-api-key title="OPDS URL" [showRefresh]="false" [transform]="makeUrl"></app-api-key> <app-api-key title="OPDS URL" [showRefresh]="false" [transform]="makeUrl"></app-api-key>
</ng-template>
</ngb-panel>
</ngb-accordion>
</ng-container>
<ng-container *ngIf="tab.fragment === 'bookmarks'">
<app-series-bookmarks></app-series-bookmarks>
</ng-container> </ng-container>
</ng-template> </ng-template>
</li> </li>

View File

@ -55,6 +55,8 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
tabs: Array<{title: string, fragment: string}> = [ tabs: Array<{title: string, fragment: string}> = [
{title: 'Preferences', fragment: ''}, {title: 'Preferences', fragment: ''},
{title: 'Bookmarks', fragment: 'bookmarks'}, {title: 'Bookmarks', fragment: 'bookmarks'},
{title: 'Password', fragment: 'password'},
{title: '3rd Party Clients', fragment: 'clients'},
]; ];
active = this.tabs[0]; active = this.tabs[0];
opdsEnabled: boolean = false; opdsEnabled: boolean = false;

View File

@ -7,6 +7,7 @@ import { ReactiveFormsModule } from '@angular/forms';
import { NgxSliderModule } from '@angular-slider/ngx-slider'; import { NgxSliderModule } from '@angular-slider/ngx-slider';
import { UserSettingsRoutingModule } from './user-settings-routing.module'; import { UserSettingsRoutingModule } from './user-settings-routing.module';
import { ApiKeyComponent } from './api-key/api-key.component'; import { ApiKeyComponent } from './api-key/api-key.component';
import { SharedModule } from '../shared/shared.module';
@ -24,6 +25,7 @@ import { ApiKeyComponent } from './api-key/api-key.component';
NgbTooltipModule, NgbTooltipModule,
NgxSliderModule, NgxSliderModule,
UserSettingsRoutingModule, UserSettingsRoutingModule,
SharedModule // SentenceCase pipe
] ]
}) })
export class UserSettingsModule { } export class UserSettingsModule { }