Fixed tooltips being slightly opaque.

Refactored Amelia's attempt at clearing up the section code and added some new controls to make paging easier.

Book/Manga reader go to page is now using a custom styled prompt instead of the basic window.prompt.

This code contains goToSection() that is not implemented.
This commit is contained in:
Joseph Milazzo 2025-06-01 09:10:05 -05:00
parent 3d097fbc33
commit b0b0c18d73
14 changed files with 186 additions and 73 deletions

View file

@ -20,37 +20,44 @@
</div>
<div subheader>
<div class="pagination-cont">
<ng-container *ngIf="layoutMode !== BookPageLayoutMode.Default">
<div class="virt-pagination-cont">
<div class="g-0 text-center">
{{t('page-label')}}
</div>
<div class="d-flex align-items-center justify-content-between text-center row g-0" *ngIf="getVirtualPage() as vp" >
<button class="btn btn-small btn-icon col-1" (click)="prevPage()" [title]="t('prev-page')">
<i class="fa-solid fa-caret-left" aria-hidden="true"></i>
</button>
<div class="col-1">{{vp[0]}}</div>
<div class="col-8">
<ngb-progressbar [title]="t('virtual-pages')" type="primary" height="5px" (click)="loadPage()" [value]="vp[0]" [max]="vp[1]"></ngb-progressbar>
</div>
<div class="col-1 btn-icon" (click)="loadPage()" [title]="t('go-to-last-page')">{{vp[1]}}</div>
<button class="btn btn-small btn-icon col-1" (click)="nextPage()" [title]="t('next-page')"><i class="fa-solid fa-caret-right" aria-hidden="true"></i></button>
</div>
</div>
</ng-container>
<div class="g-0 text-center">
{{t('pagination-header')}}
</div>
<div class="d-flex align-items-center justify-content-between text-center row g-0">
<button class="btn btn-small btn-icon col-1" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" [title]="t('prev-chapter')"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
<div class="col-1" (click)="goToPage(0)">{{pageNum}}</div>
<div class="col-8">
<button class="col-1 btn btn-small btn-icon" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" [title]="t('prev-chapter')"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
<button class="col-1 btn btn-small btn-icon" (click)="prevPage()" [title]="t('prev-page')">
<i class="fa-solid fa-caret-left" aria-hidden="true"></i>
</button>
<div class="col-1" (click)="goToPage(0)" [title]="t('go-to-first-page')">{{pageNum}}</div>
<div class="col-5">
<ngb-progressbar class="clickable" [title]="t('go-to-page')" (click)="goToPage()" type="primary" height="5px" [value]="pageNum" [max]="maxPages - 1"></ngb-progressbar>
</div>
<button class="col-1 btn btn-small btn-icon" (click)="nextPage()" [title]="t('next-page')"><i class="fa-solid fa-caret-right" aria-hidden="true"></i></button>
<div class="col-1 btn-icon" (click)="goToPage(maxPages - 1)" [title]="t('go-to-last-page')">{{maxPages - 1}}</div>
<button class="btn btn-small btn-icon col-1" [disabled]="nextChapterDisabled" (click)="loadNextChapter()" [title]="t('next-chapter')"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
<button class="col-1 btn btn-small btn-icon" [disabled]="nextChapterDisabled" (click)="loadNextChapter()" [title]="t('next-chapter')"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
</div>
<!-- Column mode needs virtual pages -->
@if (layoutMode !== BookPageLayoutMode.Default) {
@let vp = getVirtualPage();
<div class="virt-pagination-cont">
<div class="g-0 text-center">
{{t('section-label')}}<i class="fa fa-question-circle ms-1" tabindex="0" [ngbTooltip]="t('section-tooltip')" [autoClose]="false"></i>
</div>
<div class="d-flex align-items-center justify-content-between text-center row g-0">
<button class="btn btn-small btn-icon col-1" (click)="prevPage()" [title]="t('prev-section')" [disabled]="vp[0] === 1">
<i class="fa-solid fa-caret-left" aria-hidden="true"></i>
</button>
<div class="col-1">{{vp[0]}}</div>
<div class="col-8">
<!-- class="clickable" [title]="t('go-to-section')" (click)="loadSection()" TODO: Implement this -->
<ngb-progressbar type="primary" height="5px" [value]="vp[0]" [max]="vp[1]"></ngb-progressbar>
</div>
<div class="col-1 btn-icon" (click)="loadPage()" [title]="t('go-to-last-section')">{{vp[1]}}</div>
<button class="btn btn-small btn-icon col-1" (click)="nextPage()" [title]="t('next-section')"><i class="fa-solid fa-caret-right" aria-hidden="true"></i></button>
</div>
</div>
}
</div>
</div>
<div body class="drawer-body">

View file

@ -277,9 +277,9 @@ $action-bar-height: 38px;
}
.virt-pagination-cont {
padding-bottom: 5px;
margin-bottom: 5px;
box-shadow: var(--drawer-pagination-horizontal-rule);
padding-bottom: 5px;
margin-bottom: 5px;
box-shadow: var(--drawer-pagination-horizontal-rule);
}
.bottom-bar {

View file

@ -63,6 +63,7 @@ import {
PersonalToCEvent
} from "../personal-table-of-contents/personal-table-of-contents.component";
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {ConfirmService} from "../../../shared/confirm.service";
enum TabID {
@ -133,6 +134,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
private readonly utilityService = inject(UtilityService);
private readonly libraryService = inject(LibraryService);
private readonly themeService = inject(ThemeService);
private readonly confirmService = inject(ConfirmService);
private readonly cdRef = inject(ChangeDetectorRef);
protected readonly BookPageLayoutMode = BookPageLayoutMode;
@ -730,7 +732,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
@HostListener('window:keydown', ['$event'])
handleKeyPress(event: KeyboardEvent) {
async handleKeyPress(event: KeyboardEvent) {
const activeElement = document.activeElement as HTMLElement;
const isInputFocused = activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA';
if (isInputFocused) return;
@ -748,7 +750,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
event.stopPropagation();
event.preventDefault();
} else if (event.key === KEY_CODES.G) {
this.goToPage();
await this.goToPage();
} else if (event.key === KEY_CODES.F) {
this.toggleFullscreen()
}
@ -905,37 +907,69 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
promptForPage() {
const question = translate('book-reader.go-to-page-prompt', {totalPages: this.maxPages - 1});
const goToPageNum = window.prompt(question, '');
async promptForPage() {
const promptConfig = {...this.confirmService.defaultPrompt};
promptConfig.header = translate('book-reader.go-to-page');
promptConfig.content = translate('book-reader.go-to-page-prompt', {totalPages: this.maxPages - 1});
const goToPageNum = await this.confirmService.prompt(undefined, promptConfig);
if (goToPageNum === null || goToPageNum.trim().length === 0) { return null; }
return goToPageNum;
}
goToPage(pageNum?: number) {
async promptForSection() {
const [_, totalVirtualPages, _2] = this.getVirtualPage();
const promptConfig = {...this.confirmService.defaultPrompt};
promptConfig.header = translate('book-reader.go-to-section');
promptConfig.content = translate('book-reader.go-to-section-prompt', {totalSections: totalVirtualPages});
const goToPageNum = await this.confirmService.prompt(undefined, promptConfig);
if (goToPageNum === null || goToPageNum.trim().length === 0 || !/^[0-9]+$/.test(goToPageNum)) { return null; }
return Math.min(Math.max(parseInt(goToPageNum, 10), 0), totalVirtualPages - 1) + '';
}
async goToPage(pageNum?: number) {
let page = pageNum;
if (pageNum === null || pageNum === undefined) {
const goToPageNum = this.promptForPage();
const goToPageNum = await this.promptForPage();
if (goToPageNum === null) { return; }
page = parseInt(goToPageNum.trim(), 10);
}
if (page === undefined || this.pageNum === page) { return; }
if (page > this.maxPages) {
page = this.maxPages;
if (page > this.maxPages - 1) {
page = this.maxPages - 1;
} else if (page < 0) {
page = 0;
}
if (!(page === 0 || page === this.maxPages - 1)) {
page -= 1;
}
this.pageNum = page;
this.loadPage();
}
async loadSection(sectionNum?: number) {
let section = sectionNum;
if (sectionNum === null || sectionNum === undefined) {
const goToPageNum = await this.promptForSection();
if (goToPageNum === null) { return; }
section = parseInt(goToPageNum.trim(), 10);
}
if (section === undefined || this.pageNum === section) { return; }
if (section > this.maxPages - 1) {
section = this.maxPages - 1;
} else if (section < 0) {
section = 0;
}
// HACK
}

View file

@ -31,9 +31,8 @@ export class TableOfContentsComponent implements OnChanges {
@Output() loadChapter: EventEmitter<{pageNum: number, part: string}> = new EventEmitter();
ngOnChanges(changes: SimpleChanges) {
console.log('Current Page: ', this.pageNum, this.currentPageAnchor);
//console.log('Current Page: ', this.pageNum, this.currentPageAnchor);
this.cdRef.markForCheck();
}
cleanIdSelector(id: string) {

View file

@ -70,6 +70,7 @@ import {LoadingComponent} from '../../../shared/loading/loading.component';
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {shareReplay} from "rxjs/operators";
import {DblClickDirective} from "../../../_directives/dbl-click.directive";
import {ConfirmService} from "../../../shared/confirm.service";
const PREFETCH_PAGES = 10;
@ -150,9 +151,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
private readonly modalService = inject(NgbModal);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly toastr = inject(ToastrService);
public readonly readerService = inject(ReaderService);
public readonly utilityService = inject(UtilityService);
public readonly mangaReaderService = inject(MangaReaderService);
private readonly confirmService = inject(ConfirmService);
protected readonly readerService = inject(ReaderService);
protected readonly utilityService = inject(UtilityService);
protected readonly mangaReaderService = inject(MangaReaderService);
protected readonly KeyDirection = KeyDirection;
protected readonly ReaderMode = ReaderMode;
@ -647,7 +650,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
@HostListener('window:keyup', ['$event'])
handleKeyPress(event: KeyboardEvent) {
async handleKeyPress(event: KeyboardEvent) {
switch (this.readerMode) {
case ReaderMode.LeftRight:
if (event.key === KEY_CODES.RIGHT_ARROW) {
@ -682,7 +685,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} else if (event.key === KEY_CODES.SPACE) {
this.toggleMenu();
} else if (event.key === KEY_CODES.G) {
const goToPageNum = this.promptForPage();
const goToPageNum = await this.promptForPage();
if (goToPageNum === null) { return; }
this.goToPage(parseInt(goToPageNum.trim(), 10));
} else if (event.key === KEY_CODES.B) {
@ -1593,9 +1596,16 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
// This is menu only code
promptForPage() {
const question = translate('book-reader.go-to-page-prompt', {totalPages: this.maxPages});
const goToPageNum = window.prompt(question, '');
async promptForPage() {
// const question = translate('book-reader.go-to-page-prompt', {totalPages: this.maxPages});
// const goToPageNum = window.prompt(question, '');
const promptConfig = {...this.confirmService.defaultPrompt};
promptConfig.header = translate('book-reader.go-to-page');
promptConfig.content = translate('book-reader.go-to-page-prompt', {totalPages: this.maxPages});
const goToPageNum = await this.confirmService.prompt(undefined, promptConfig);
if (goToPageNum === null || goToPageNum.trim().length === 0) { return null; }
return goToPageNum;
}

View file

@ -1,7 +1,7 @@
import { ConfirmButton } from './confirm-button';
import {ConfirmButton} from './confirm-button';
export class ConfirmConfig {
_type: 'confirm' | 'alert' | 'info' = 'confirm';
_type: 'confirm' | 'alert' | 'info' | 'prompt' = 'confirm';
header: string = 'Confirm';
content: string = '';
buttons: Array<ConfirmButton> = [];

View file

@ -5,8 +5,18 @@
<button type="button" class="btn-close" [attr.aria-label]="t('common.close')" (click)="close()"></button>
}
</div>
<div class="modal-body" style="overflow-x: auto" [innerHtml]="(config.content | confirmTranslate)! | safeHtml">
</div>
@if (config._type === 'prompt') {
<div class="modal-body" style="overflow-x: auto">
<form [formGroup]="formGroup">
<div [innerHtml]="(config.content | confirmTranslate)! | safeHtml"></div>
<input type="text" class="form-control" aria-labelledby="modal-basic-title" formControlName="prompt" />
</form>
</div>
} @else {
<div class="modal-body" style="overflow-x: auto" [innerHtml]="(config.content | confirmTranslate)! | safeHtml"></div>
}
<div class="modal-footer">
@for(btn of config.buttons; track btn) {
<div>

View file

@ -1,15 +1,15 @@
import {Component, inject, OnInit} from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ConfirmButton } from './_models/confirm-button';
import { ConfirmConfig } from './_models/confirm-config';
import {CommonModule} from "@angular/common";
import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap';
import {ConfirmButton} from './_models/confirm-button';
import {ConfirmConfig} from './_models/confirm-config';
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
import {TranslocoDirective} from "@jsverse/transloco";
import {ConfirmTranslatePipe} from "../../_pipes/confirm-translate.pipe";
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
@Component({
selector: 'app-confirm-dialog',
imports: [SafeHtmlPipe, TranslocoDirective, ConfirmTranslatePipe],
imports: [SafeHtmlPipe, TranslocoDirective, ConfirmTranslatePipe, ReactiveFormsModule],
templateUrl: './confirm-dialog.component.html',
styleUrls: ['./confirm-dialog.component.scss']
})
@ -18,6 +18,9 @@ export class ConfirmDialogComponent implements OnInit {
protected readonly modal = inject(NgbActiveModal);
config!: ConfirmConfig;
formGroup = new FormGroup({
'prompt': new FormControl('', []),
})
ngOnInit(): void {
if (this.config) {
@ -37,6 +40,10 @@ export class ConfirmDialogComponent implements OnInit {
}
clickButton(button: ConfirmButton) {
if (this.config._type === 'prompt') {
this.modal.close(button.type === 'primary' ? this.formGroup.get('prompt')?.value : '');
return;
}
this.modal.close(button.type === 'primary');
}

View file

@ -1,10 +1,8 @@
import { Injectable } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { take } from 'rxjs/operators';
import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component';
import { ConfirmConfig } from './confirm-dialog/_models/confirm-config';
import {translate} from "@jsverse/transloco";
import {ConfirmButton} from "./confirm-dialog/_models/confirm-button";
import {Injectable} from '@angular/core';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import {take} from 'rxjs/operators';
import {ConfirmDialogComponent} from './confirm-dialog/confirm-dialog.component';
import {ConfirmConfig} from './confirm-dialog/_models/confirm-config';
@Injectable({
@ -15,6 +13,7 @@ export class ConfirmService {
defaultConfirm = new ConfirmConfig();
defaultAlert = new ConfirmConfig();
defaultInfo = new ConfirmConfig();
defaultPrompt = new ConfirmConfig();
constructor(private modalService: NgbModal) {
this.defaultConfirm.buttons = [
@ -33,6 +32,13 @@ export class ConfirmService {
];
this.defaultInfo.header = 'confirm.info';
this.defaultInfo._type = 'info';
this.defaultPrompt.buttons = [
{text: 'confirm.cancel', type: 'secondary'},
{text: 'confirm.ok', type: 'primary'}
];
this.defaultPrompt.header = 'confirm.prompt';
this.defaultPrompt._type = 'prompt';
}
public async confirm(content?: string, config?: ConfirmConfig): Promise<boolean> {
@ -114,4 +120,32 @@ export class ConfirmService {
});
});
}
public async prompt(title: string | undefined = undefined, config: ConfirmConfig | undefined = undefined): Promise<string> {
return new Promise((resolve, reject) => {
if (title === undefined && config === undefined) {
console.error('Confirm must have either text or a config object passed');
return reject(false);
}
if (title !== undefined && config === undefined) {
config = this.defaultPrompt;
config.header = title;
}
if (title !== undefined && title !== '' && config!.header === '') {
config!.header = title;
}
const modalRef = this.modalService.open(ConfirmDialogComponent);
modalRef.componentInstance.config = config;
modalRef.closed.pipe(take(1)).subscribe(result => {
return resolve(result);
});
modalRef.dismissed.pipe(take(1)).subscribe(() => {
return resolve('');
});
});
}
}

View file

@ -858,10 +858,12 @@
"book-reader": {
"title": "Book Settings",
"page-label": "Page",
"section-label": "Section",
"section-tooltip": "A virtual page when using Column mode",
"pagination-header": "Section",
"pagination-header": "Page",
"go-to-page": "Go to page",
"go-to-first-page": "Go to first page",
"go-to-last-page": "Go to last page",
"prev-page": "Prev Page",
"next-page": "Next Page",
@ -869,6 +871,10 @@
"next-chapter": "Next Chapter/Volume",
"skip-header": "Skip to main content",
"virtual-pages": "virtual pages",
"next-section": "Next section",
"prev-section": "Prev section",
"go-to-section": "Go to section",
"go-to-last-section": "Go to last section",
"settings-header": "Settings",
"table-of-contents-header": "Table of Contents",
@ -882,7 +888,8 @@
"incognito-mode-label": "Incognito Mode",
"next": "Next",
"previous": "Previous",
"go-to-page-prompt": "There are {{totalPages}} pages. What page do you want to go to?"
"go-to-page-prompt": "There are {{totalPages}} pages. What page do you want to go to?",
"go-to-section-prompt": "There are {{totalSections}} sections. What section do you want to go to?"
},
"personal-table-of-contents": {
@ -2579,7 +2586,8 @@
"confirm": "Confirm",
"info": "Info",
"cancel": "{{common.cancel}}",
"ok": "Ok"
"ok": "Ok",
"prompt": "Question"
},
"toasts": {

View file

@ -47,6 +47,7 @@
@use './theme/components/table';
@use './theme/components/alerts';
@use './theme/components/typeahead';
@use './theme/components/tooltip';
@use './theme/utilities/headings';
@use './theme/utilities/utilities';

View file

@ -31,9 +31,9 @@
border-color: var(--btn-outline-primary-border-color);
&:hover {
color: var(--btn-outline-primary-hover-text-color);
background-color: var(--btn-outline-primary-hover-bg-color);
border-color: var(--btn-outline-primary-hover-border-color);
color: var(--btn-outline-primary-hover-text-color) !important;
background-color: var(--btn-outline-primary-hover-bg-color) !important;
border-color: var(--btn-outline-primary-hover-border-color) !important;
}
}

View file

@ -0,0 +1,4 @@
.tooltip {
--bs-tooltip-opacity: 1;
//--bs-tooltip-color:
}

View file

@ -59,7 +59,6 @@
--select2-option-highlighted-background: var(--dropdown-item-hover-bg-color);
/* Theming colors that performs a gradient for background. Can be disabled else automatically applied based on cover image colors.
* --colorscape-primary-color and the alpha variants will be updated in real time. the default variant is fixed and represents the default state and should
* match the non-default/alpha on launch.