Localization - First Pass (#2174)

* Started designing the backend localization service

* Worked in Transloco for initial PoC

* Worked in Transloco for initial PoC

* Translated the login screen

* translated dashboard screen

* Started work on the backend

* Fixed a logic bug

* translated edit-user screen

* Hooked up the backend for having a locale property.

* Hooked up the ability to view the available locales and switch to them.

* Made the localization service languages be derived from what's in langs/ directory.

* Fixed up localization switching

* Switched when we check for a license on UI bootstrap

* Tweaked some code

* Fixed the bug where dashboard wasn't loading and made it so language switching is working.

* Fixed a bug on dashboard with languagePath

* Converted user-scrobble-history.component.html

* Converted spoiler.component.html

* Converted review-series-modal.component.html

* Converted review-card-modal.component.html

* Updated the readme

* Translated using Weblate (English)

Currently translated at 100.0% (54 of 54 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/en/

* Converted review-card.component.html

* Deleted dead component

* Converted want-to-read.component.html

* Added translation using Weblate (Korean)

* Translated using Weblate (Spanish)

Currently translated at 40.7% (22 of 54 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Korean)

Currently translated at 62.9% (34 of 54 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/

* Converted user-preferences.component.html

* Translated using Weblate (Korean)

Currently translated at 92.5% (50 of 54 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/

* Converted user-holds.component.html

* Converted theme-manager.component.html

* Converted restriction-selector.component.html

* Converted manage-devices.component.html

* Converted edit-device.component.html

* Converted change-password.component.html

* Converted change-email.component.html

* Converted change-age-restriction.component.html

* Converted api-key.component.html

* Converted anilist-key.component.html

* Converted typeahead.component.html

* Converted user-stats-info-cards.component.html

* Converted user-stats.component.html

* Converted top-readers.component.html

* Converted some pipes and ensure translation is loaded before the app.

* Finished all but one pipe for localization

* Converted directory-picker.component.html

* Converted library-access-modal.component.html

* Converted a few components

* Converted a few components

* Converted a few components

* Converted a few components

* Converted a few components

* Merged weblate in

* ... -> … update

* Updated the readme

* Updateded all fonts to be woff2

* Cleaned up some strings to increase re-use

* Removed an old flow (that doesn't exist in backend any longer) from when we introduced emails on Kavita.

* Converted Series detail

* Lots more converted

* Lots more converted & hooked up the ability to flatten during prod build the language files.

* Lots more converted

* Lots more converted & fixed a bunch of broken pipes due to inject()

* Lots more converted

* Lots more converted

* Lots more converted & fixed some bad keys

* Lots more converted

* Fixed some bugs with admin dasbhoard nested tabs not rendering on first load due to not using onpush change detection

* Fixed up some localization errors and fixed forgot password error when the user doesn't have change password permission

* Fixed a stupid build issue again

* Started adding errors for interceptor and backend.

* Finished off manga-reader

* More translations

* Few fixes

* Fixed a bug where character tag badges weren't showing the name on chapter info

* All components are translated

* All toasts are translated

* All confirm/alerts are translated

* Trying something new for the backend

* Migrated the localization strings for the backend into a new file.

* Updated the localization service to be able to do backend localization with fallback to english.

* Cleaned up some external reviews code to reduce looping

* Localized AccountController.cs

* 60% done with controllers

* All controllers are done

* All KavitaExceptions are covered

* Some shakeout fixes

* Prep for initial merge

* Everything is done except options and basic shakeout proves response times are good. Unit tests are broken.

* Fixed up the unit tests

* All unit tests are now working

* Removed some quantifier

* I'm not sure I can support localization for some Volume/Chapter/Book strings within the codebase.

---------

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
Co-authored-by: majora2007 <kavitareader@gmail.com>
Co-authored-by: expertjun <jtrobin@naver.com>
Co-authored-by: ThePromidius <thepromidiusyt@gmail.com>
This commit is contained in:
Joe Milazzo 2023-08-03 10:33:51 -05:00 committed by GitHub
parent 670bf82c38
commit 3b23d63234
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
389 changed files with 13652 additions and 7925 deletions

View file

@ -1,44 +1,46 @@
<div class="overlay" *ngIf="selectedText.length > 0 || mode !== BookLineOverlayMode.None">
<ng-container *transloco="let t; read: 'book-line-overlay'">
<div class="overlay" *ngIf="selectedText.length > 0 || mode !== BookLineOverlayMode.None">
<div class="row g-0 justify-content-between">
<ng-container [ngSwitch]="mode">
<ng-container *ngSwitchCase="BookLineOverlayMode.None">
<div class="row g-0 justify-content-between">
<ng-container [ngSwitch]="mode">
<ng-container *ngSwitchCase="BookLineOverlayMode.None">
<div class="col-auto">
<button class="btn btn-icon btn-sm" (click)="copy()">
<i class="fa-solid fa-copy" aria-hidden="true"></i>
<div>Copy</div>
<div>{{t('copy')}}</div>
</button>
</div>
<div class="col-auto">
<button class="btn btn-icon btn-sm" (click)="switchMode(BookLineOverlayMode.Bookmark)">
<i class="fa-solid fa-book-bookmark" aria-hidden="true"></i>
<div>Bookmark</div>
<div>{{t('bookmark')}}</div>
</button>
</div>
<div class="col-auto">
<button class="btn btn-icon btn-sm" (click)="reset()">
<i class="fa-solid fa-times-circle" aria-hidden="true"></i>
<div>Close</div>
<div>{{t('close')}}</div>
</button>
</div>
</ng-container>
<ng-container *ngSwitchCase="BookLineOverlayMode.Bookmark">
<form [formGroup]="bookmarkForm">
<div class="input-group">
<input id="bookmark-name" class="form-control" formControlName="name" type="text" placeholder="Bookmark Name"
[class.is-invalid]="bookmarkForm.get('name')?.invalid && bookmarkForm.get('name')?.touched" aria-describedby="bookmark-name-btn">
<button class="btn btn-outline-primary" id="bookmark-name-btn" (click)="createPTOC()">Save</button>
<div id="bookmark-name-validations" class="invalid-feedback" *ngIf="bookmarkForm.dirty || bookmarkForm.touched">
<div *ngIf="bookmarkForm.get('name')?.errors?.required" role="status">
This field is required
</ng-container>
<ng-container *ngSwitchCase="BookLineOverlayMode.Bookmark">
<form [formGroup]="bookmarkForm">
<div class="input-group">
<input id="bookmark-name" class="form-control" formControlName="name" type="text" [placeholder]="t('book-label')"
[class.is-invalid]="bookmarkForm.get('name')?.invalid && bookmarkForm.get('name')?.touched" aria-describedby="bookmark-name-btn">
<button class="btn btn-outline-primary" id="bookmark-name-btn" (click)="createPTOC()">{{t('save')}}</button>
<div id="bookmark-name-validations" class="invalid-feedback" *ngIf="bookmarkForm.dirty || bookmarkForm.touched">
<div *ngIf="bookmarkForm.get('name')?.errors?.required" role="status">
{{t('required-field')}}
</div>
</div>
</div>
</div>
</form>
</form>
</ng-container>
</ng-container>
</ng-container>
</div>
</div>
</div>
</ng-container>

View file

@ -9,12 +9,12 @@ import {
} from '@angular/core';
import {CommonModule} from '@angular/common';
import {fromEvent, merge, of} from "rxjs";
import {catchError, filter, tap} from "rxjs/operators";
import {catchError} from "rxjs/operators";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import getBoundingClientRect from "@popperjs/core/lib/dom-utils/getBoundingClientRect";
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
import {ReaderService} from "../../../_services/reader.service";
import {ToastrService} from "ngx-toastr";
import {translate, TranslocoModule} from "@ngneat/transloco";
enum BookLineOverlayMode {
None = 0,
@ -24,7 +24,7 @@ enum BookLineOverlayMode {
@Component({
selector: 'app-book-line-overlay',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
imports: [CommonModule, ReactiveFormsModule, TranslocoModule],
templateUrl: './book-line-overlay.component.html',
styleUrls: ['./book-line-overlay.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
@ -132,7 +132,7 @@ export class BookLineOverlayComponent implements OnInit {
const selection = window.getSelection();
if (selection) {
await navigator.clipboard.writeText(selection.toString());
this.toastr.info('Copied to clipboard');
this.toastr.info(translate('toasts.copied-to-clipboard'));
}
this.reset();
}

View file

@ -1,155 +1,159 @@
<div class="container-flex {{darkMode ? 'dark-mode' : ''}} reader-container {{ColumnLayout}} {{WritingStyleClass}}" tabindex="0" #reader>
<ng-container *transloco="let t; read: 'book-reader'">
<div class="container-flex {{darkMode ? 'dark-mode' : ''}} reader-container {{ColumnLayout}} {{WritingStyleClass}}" tabindex="0" #reader>
<div class="fixed-top" #stickyTop>
<a class="visually-hidden-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">Skip to main content</a>
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
<app-book-line-overlay [parent]="bookContainerElemRef" *ngIf="page !== undefined"
[libraryId]="libraryId"
[volumeId]="volumeId"
[chapterId]="chapterId"
[seriesId]="seriesId"
[pageNumber]="pageNum"
(refreshToC)="refreshPersonalToC()">
<a class="visually-hidden-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">{{t('skip-header')}}</a>
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
<app-book-line-overlay [parent]="bookContainerElemRef" *ngIf="page !== undefined"
[libraryId]="libraryId"
[volumeId]="volumeId"
[chapterId]="chapterId"
[seriesId]="seriesId"
[pageNumber]="pageNum"
(refreshToC)="refreshPersonalToC()">
</app-book-line-overlay>
<app-drawer #commentDrawer="drawer" [(isOpen)]="drawerOpen" [options]="{topOffset: topOffset}">
<h5 header>
Book Settings
</h5>
<div subheader>
<div class="pagination-cont">
<ng-container *ngIf="layoutMode !== BookPageLayoutMode.Default">
<div class="virt-pagination-cont">
<div class="g-0 text-center">
Page
</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="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="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="Go to last page">{{vp[1]}}</div>
<button class="btn btn-small btn-icon col-1" (click)="nextPage()" title="Next Page"><i class="fa-solid fa-caret-right" aria-hidden="true"></i></button>
</div>
</div>
</ng-container>
<div class="g-0 text-center">
Section
</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="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
<div class="col-1" (click)="goToPage(0)">{{pageNum}}</div>
<div class="col-8">
<ngb-progressbar style="cursor: pointer" title="Go to page" (click)="goToPage()" type="primary" height="5px" [value]="pageNum" [max]="maxPages - 1"></ngb-progressbar>
</div>
<div class="col-1 btn-icon" (click)="goToPage(maxPages - 1)" title="Go to last page">{{maxPages - 1}}</div>
<button class="btn btn-small btn-icon col-1" [disabled]="nextChapterDisabled" (click)="loadNextChapter()" title="Next Chapter/Volume"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
</div>
<app-drawer #commentDrawer="drawer" [(isOpen)]="drawerOpen" [options]="{topOffset: topOffset}">
<h5 header>
{{t('title')}}
</h5>
<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')}}
</div>
</div>
<div body class="drawer-body">
<nav role="navigation">
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="reader-pills nav nav-pills mb-2" [destroyOnHide]="false">
<li [ngbNavItem]="TabID.Settings">
<a ngbNavLink>Settings</a>
<ng-template ngbNavContent>
<app-reader-settings
(colorThemeUpdate)="updateColorTheme($event)"
(styleUpdate)="updateReaderStyles($event)"
(clickToPaginateChanged)="showPaginationOverlay($event)"
(fullscreen)="toggleFullscreen()"
(bookReaderWritingStyle)="updateWritingStyle($event)"
(layoutModeUpdate)="updateLayoutMode($event)"
(readingDirection)="updateReadingDirection($event)"
(immersiveMode)="updateImmersiveMode($event)"
></app-reader-settings>
</ng-template>
</li>
<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>
<li [ngbNavItem]="TabID.TableOfContents">
<a ngbNavLink>Table of Contents</a>
<ng-template ngbNavContent>
<ul #subnav="ngbNav" ngbNav [(activeId)]="tocId" class="reader-pills nav nav-pills mb-2" [destroyOnHide]="false">
<li [ngbNavItem]="TabID.TableOfContents">
<a ngbNavLink>ToC</a>
<ng-template ngbNavContent>
<app-table-of-contents [chapters]="chapters" [chapterId]="chapterId" [pageNum]="pageNum"
[currentPageAnchor]="currentPageAnchor" (loadChapter)="loadChapterPage($event)"></app-table-of-contents>
</ng-template>
</li>
<li [ngbNavItem]="TabID.PersonalTableOfContents">
<a ngbNavLink>Bookmarks</a>
<ng-template ngbNavContent>
<app-personal-table-of-contents [chapterId]="chapterId" [pageNum]="pageNum" (loadChapter)="loadChapterPart($event)"
[tocRefresh]="refreshPToC"></app-personal-table-of-contents>
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="subnav" class="mt-3"></div>
</ng-template>
</li>
</ul>
</nav>
<div [ngbNavOutlet]="nav" class="mt-3"></div>
<div class="g-0 text-center">
{{t('pagination-header')}}
</div>
</app-drawer>
<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">
<ngb-progressbar style="cursor: pointer" [title]="t('go-to-page')" (click)="goToPage()" type="primary" height="5px" [value]="pageNum" [max]="maxPages - 1"></ngb-progressbar>
</div>
<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>
</div>
</div>
</div>
<div body class="drawer-body">
<nav role="navigation">
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="reader-pills nav nav-pills mb-2" [destroyOnHide]="false">
<li [ngbNavItem]="TabID.Settings">
<a ngbNavLink>{{t('settings-header')}}</a>
<ng-template ngbNavContent>
<app-reader-settings
(colorThemeUpdate)="updateColorTheme($event)"
(styleUpdate)="updateReaderStyles($event)"
(clickToPaginateChanged)="showPaginationOverlay($event)"
(fullscreen)="toggleFullscreen()"
(bookReaderWritingStyle)="updateWritingStyle($event)"
(layoutModeUpdate)="updateLayoutMode($event)"
(readingDirection)="updateReadingDirection($event)"
(immersiveMode)="updateImmersiveMode($event)"
></app-reader-settings>
</ng-template>
</li>
<li [ngbNavItem]="TabID.TableOfContents">
<a ngbNavLink>{{t('table-of-contents-header')}}</a>
<ng-template ngbNavContent>
<ul #subnav="ngbNav" ngbNav [(activeId)]="tocId" class="reader-pills nav nav-pills mb-2" [destroyOnHide]="false">
<li [ngbNavItem]="TabID.TableOfContents">
<a ngbNavLink>{{t('toc-header')}}</a>
<ng-template ngbNavContent>
<app-table-of-contents [chapters]="chapters" [chapterId]="chapterId" [pageNum]="pageNum"
[currentPageAnchor]="currentPageAnchor" (loadChapter)="loadChapterPage($event)"></app-table-of-contents>
</ng-template>
</li>
<li [ngbNavItem]="TabID.PersonalTableOfContents">
<a ngbNavLink>{{t('bookmarks-header')}}</a>
<ng-template ngbNavContent>
<app-personal-table-of-contents [chapterId]="chapterId" [pageNum]="pageNum" (loadChapter)="loadChapterPart($event)"
[tocRefresh]="refreshPToC"></app-personal-table-of-contents>
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="subnav" class="mt-3"></div>
</ng-template>
</li>
</ul>
</nav>
<div [ngbNavOutlet]="nav" class="mt-3"></div>
</div>
</app-drawer>
</div>
<div #readingSection class="reading-section {{ColumnLayout}} {{WritingStyleClass}}" [ngStyle]="{'width': PageWidthForPagination}" [ngClass]="{'immersive' : immersiveMode || !actionBarVisible}" [@isLoading]="isLoading">
<ng-container *ngIf="clickToPaginate">
<div class="left {{clickOverlayClass('left')}} no-observe" [ngClass]="{'immersive' : immersiveMode}"
(click)="movePage(readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD)"
tabindex="-1" [ngStyle]="{height: PageHeightForPagination}"></div>
<div class="{{scrollbarNeeded ? 'right-with-scrollbar' : 'right'}} {{clickOverlayClass('right')}} no-observe"
[ngClass]="{'immersive' : immersiveMode}"
(click)="movePage(readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.FORWARD : PAGING_DIRECTION.BACKWARDS)"
tabindex="-1" [ngStyle]="{height: PageHeightForPagination}"></div>
</ng-container>
<div #bookContainer class="book-container {{WritingStyleClass}}" [ngClass]="{'immersive' : immersiveMode}">
<ng-container *ngIf="clickToPaginate">
<div class="left {{clickOverlayClass('left')}} no-observe" [ngClass]="{'immersive' : immersiveMode}"
(click)="movePage(readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD)"
tabindex="-1" [ngStyle]="{height: PageHeightForPagination}"></div>
<div class="{{scrollbarNeeded ? 'right-with-scrollbar' : 'right'}} {{clickOverlayClass('right')}} no-observe"
[ngClass]="{'immersive' : immersiveMode}"
(click)="movePage(readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.FORWARD : PAGING_DIRECTION.BACKWARDS)"
tabindex="-1" [ngStyle]="{height: PageHeightForPagination}"></div>
</ng-container>
<div #bookContainer class="book-container {{WritingStyleClass}}" [ngClass]="{'immersive' : immersiveMode}">
<div #readingHtml class="book-content {{ColumnLayout}} {{WritingStyleClass}}"
[ngStyle]="{'max-height': ColumnHeight, 'max-width': VerticalBookContentWidth, 'width': VerticalBookContentWidth, 'column-width': ColumnWidth}"
[ngClass]="{'immersive': immersiveMode && actionBarVisible}"
[innerHtml]="page" *ngIf="page !== undefined" (click)="toggleMenu($event)" (mousedown)="mouseDown($event)" (wheel)="onWheel($event)"></div>
<div *ngIf="page !== undefined && (scrollbarNeeded || layoutMode !== BookPageLayoutMode.Default) && !(writingStyle === WritingStyle.Vertical && layoutMode === BookPageLayoutMode.Default)"
(click)="$event.stopPropagation();"
[ngClass]="{'bottom-bar': layoutMode !== BookPageLayoutMode.Default}">
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
</div>
<div #readingHtml class="book-content {{ColumnLayout}} {{WritingStyleClass}}"
[ngStyle]="{'max-height': ColumnHeight, 'max-width': VerticalBookContentWidth, 'width': VerticalBookContentWidth, 'column-width': ColumnWidth}"
[ngClass]="{'immersive': immersiveMode && actionBarVisible}"
[innerHtml]="page" *ngIf="page !== undefined" (click)="toggleMenu($event)" (mousedown)="mouseDown($event)" (wheel)="onWheel($event)"></div>
<div *ngIf="page !== undefined && (scrollbarNeeded || layoutMode !== BookPageLayoutMode.Default) && !(writingStyle === WritingStyle.Vertical && layoutMode === BookPageLayoutMode.Default)"
(click)="$event.stopPropagation();"
[ngClass]="{'bottom-bar': layoutMode !== BookPageLayoutMode.Default}">
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
</div>
</div>
</div>
<ng-template #actionBar>
<div class="action-bar row g-0 justify-content-between" *ngIf="!immersiveMode || drawerOpen || actionBarVisible">
<button class="btn btn-outline-secondary btn-icon col-2 col-xs-1" (click)="movePage(readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD)"
<div class="action-bar row g-0 justify-content-between" *ngIf="!immersiveMode || drawerOpen || actionBarVisible">
<button class="btn btn-outline-secondary btn-icon col-2 col-xs-1" (click)="movePage(readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.BACKWARDS : PAGING_DIRECTION.FORWARD)"
[disabled]="readingDirection === ReadingDirection.LeftToRight ? IsPrevDisabled : IsNextDisabled"
title="{{readingDirection === ReadingDirection.LeftToRight ? 'Previous' : 'Next'}} Page">
<i class="fa {{(readingDirection === ReadingDirection.LeftToRight ? IsPrevChapter : IsNextChapter) ? 'fa-angle-double-left' : 'fa-angle-left'}} {{readingDirection === ReadingDirection.RightToLeft ? 'next-page-highlight' : ''}}" aria-hidden="true"></i>
</button>
<button *ngIf="!this.adhocPageHistory.isEmpty()" class="btn btn-outline-secondary btn-icon col-2 col-xs-1" (click)="goBack()" title="Go Back">
<i class="fa fa-reply" aria-hidden="true"></i>
</button>
<button class="btn btn-secondary col-2 col-xs-1" (click)="toggleDrawer()">
<i class="fa fa-bars" aria-hidden="true"></i></button>
<div class="book-title col-2 d-none d-sm-block">
<ng-container *ngIf="isLoading; else showTitle">
<div class="spinner-border spinner-border-sm text-primary" style="border-radius: 50%;" role="status">
<span class="visually-hidden">Loading book...</span>
</div>
</ng-container>
<ng-template #showTitle>
<span *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" aria-label="Incognito mode is on. Toggle to turn off.">(<i class="fa fa-glasses" aria-hidden="true"></i><span class="visually-hidden">Incognito Mode</span>)</span>
<span class="book-title-text ms-1" [ngbTooltip]="bookTitle">{{bookTitle}}</span>
</ng-template>
title="{{readingDirection === ReadingDirection.LeftToRight ? t('previous') : t('next')}} Page">
<i class="fa {{(readingDirection === ReadingDirection.LeftToRight ? IsPrevChapter : IsNextChapter) ? 'fa-angle-double-left' : 'fa-angle-left'}} {{readingDirection === ReadingDirection.RightToLeft ? 'next-page-highlight' : ''}}" aria-hidden="true"></i>
</button>
<button *ngIf="!this.adhocPageHistory.isEmpty()" class="btn btn-outline-secondary btn-icon col-2 col-xs-1" (click)="goBack()" [title]="t('go-back')">
<i class="fa fa-reply" aria-hidden="true"></i>
</button>
<button class="btn btn-secondary col-2 col-xs-1" (click)="toggleDrawer()">
<i class="fa fa-bars" aria-hidden="true"></i></button>
<div class="book-title col-2 d-none d-sm-block">
<ng-container *ngIf="isLoading; else showTitle">
<div class="spinner-border spinner-border-sm text-primary" style="border-radius: 50%;" role="status">
<span class="visually-hidden">{{t('loading-book')}}</span>
</div>
<button class="btn btn-secondary col-2 col-xs-1" (click)="closeReader()"><i class="fa fa-times-circle" aria-hidden="true"></i></button>
<button class="btn btn-outline-secondary btn-icon col-2 col-xs-1"
[disabled]="readingDirection === ReadingDirection.LeftToRight ? IsNextDisabled : IsPrevDisabled"
(click)="movePage(readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.FORWARD : PAGING_DIRECTION.BACKWARDS)" title="{{readingDirection === ReadingDirection.LeftToRight ? 'Next' : 'Previous'}} Page">
<i class="fa {{(readingDirection === ReadingDirection.LeftToRight ? IsNextChapter : IsPrevChapter) ? 'fa-angle-double-right' : 'fa-angle-right'}} {{readingDirection === ReadingDirection.LeftToRight ? 'next-page-highlight' : ''}}" aria-hidden="true"></i>
</button>
</ng-container>
<ng-template #showTitle>
<span *ngIf="incognitoMode" (click)="turnOffIncognito()" role="button" [attr.aria-label]="t('incognito-mode-alt')">
(<i class="fa fa-glasses" aria-hidden="true"></i><span class="visually-hidden">{{t('incognito-mode-label')}}</span>)</span>
<span class="book-title-text ms-1" [ngbTooltip]="bookTitle">{{bookTitle}}</span>
</ng-template>
</div>
<button class="btn btn-secondary col-2 col-xs-1" (click)="closeReader()"><i class="fa fa-times-circle" aria-hidden="true"></i></button>
<button class="btn btn-outline-secondary btn-icon col-2 col-xs-1"
[disabled]="readingDirection === ReadingDirection.LeftToRight ? IsNextDisabled : IsPrevDisabled"
(click)="movePage(readingDirection === ReadingDirection.LeftToRight ? PAGING_DIRECTION.FORWARD : PAGING_DIRECTION.BACKWARDS)" title="{{readingDirection === ReadingDirection.LeftToRight ? t('next') : t('previous')}} Page">
<i class="fa {{(readingDirection === ReadingDirection.LeftToRight ? IsNextChapter : IsPrevChapter) ? 'fa-angle-double-right' : 'fa-angle-right'}} {{readingDirection === ReadingDirection.LeftToRight ? 'next-page-highlight' : ''}}" aria-hidden="true"></i>
</button>
</div>
</ng-template>
</div>
</div>
</ng-container>

View file

@ -1,42 +1,42 @@
@font-face {
font-family: "Fira_Sans";
src: url(../../../../assets/fonts/Fira_Sans/FiraSans-Regular.ttf) format("truetype");
src: url(../../../../assets/fonts/Fira_Sans/FiraSans-Regular.woff2) format("woff2");
font-display: swap;
}
@font-face {
font-family: "Lato";
src: url(../../../../assets/fonts/Lato/Lato-Regular.ttf) format("truetype");
src: url(../../../../assets/fonts/Lato/Lato-Regular.woff2) format("woff2");
font-display: swap;
}
@font-face {
font-family: "Libre_Baskerville";
src: url(../../../../assets/fonts/Libre_Baskerville/LibreBaskerville-Regular.ttf) format("truetype");
src: url(../../../../assets/fonts/Libre_Baskerville/LibreBaskerville-Regular.woff2) format("woff2");
font-display: swap;
}
@font-face {
font-family: "Merriweather";
src: url(../../../../assets/fonts/Merriweather/Merriweather-Regular.ttf) format("truetype");
src: url(../../../../assets/fonts/Merriweather/Merriweather-Regular.woff2) format("woff2");
font-display: swap;
}
@font-face {
font-family: "Nanum_Gothic";
src: url(../../../../assets/fonts/Nanum_Gothic/NanumGothic-Regular.ttf) format("truetype");
src: url(../../../../assets/fonts/Nanum_Gothic/NanumGothic-Regular.woff2) format("woff2");
font-display: swap;
}
@font-face {
font-family: "RocknRoll_One";
src: url(../../../../assets/fonts/RocknRoll_One/RocknRollOne-Regular.ttf) format("truetype");
src: url(../../../../assets/fonts/RocknRoll_One/RocknRollOne-Regular.woff2) format("woff2");
font-display: swap;
}
@font-face {
font-family: "OpenDyslexic2";
src: url(../../../../assets/fonts/OpenDyslexic2/OpenDyslexic-Regular.otf) format("opentype");
src: url(../../../../assets/fonts/OpenDyslexic2/OpenDyslexic-Regular.woff2) format("woff2");
font-display: swap;
}

View file

@ -51,6 +51,7 @@ import {
PersonalTableOfContentsComponent,
PersonalToCEvent
} from "../personal-table-of-contents/personal-table-of-contents.component";
import {translate, TranslocoModule} from "@ngneat/transloco";
enum TabID {
@ -101,7 +102,7 @@ const elementLevelStyles = ['line-height', 'font-family'];
])
],
standalone: true,
imports: [NgTemplateOutlet, DrawerComponent, NgIf, NgbProgressbar, NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent, ReaderSettingsComponent, TableOfContentsComponent, NgbNavOutlet, NgStyle, NgClass, NgbTooltip, BookLineOverlayComponent, PersonalTableOfContentsComponent]
imports: [NgTemplateOutlet, DrawerComponent, NgIf, NgbProgressbar, NgbNav, NgbNavItem, NgbNavItemRole, NgbNavLink, NgbNavContent, ReaderSettingsComponent, TableOfContentsComponent, NgbNavOutlet, NgStyle, NgClass, NgbTooltip, BookLineOverlayComponent, PersonalTableOfContentsComponent, TranslocoModule]
})
export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
@ -577,7 +578,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.memberService.hasReadingProgress(this.libraryId).pipe(take(1)).subscribe(hasProgress => {
if (!hasProgress) {
this.toggleDrawer();
this.toastr.info('You can modify book settings, save those settings for all books, and view table of contents from the drawer.');
this.toastr.info(translate('toasts.book-settings-info'));
}
});
@ -782,12 +783,14 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// Load chapter Id onto route but don't reload
const newRoute = this.readerService.getNextChapterUrl(this.router.url, this.chapterId, this.incognitoMode, this.readingListMode, this.readingListId);
window.history.replaceState({}, '', newRoute);
this.toastr.info(direction + ' ' + this.utilityService.formatChapterName(this.libraryType).toLowerCase() + ' loaded', '', {timeOut: 3000});
const msg = translate(direction === 'Next' ? 'toasts.load-next-chapter' : 'toasts.load-prev-chapter', {entity: this.utilityService.formatChapterName(this.libraryType).toLowerCase()});
this.toastr.info(msg, '', {timeOut: 3000});
this.cdRef.markForCheck();
this.init();
} else {
// This will only happen if no actual chapter can be found
this.toastr.warning('Could not find ' + direction.toLowerCase() + ' ' + this.utilityService.formatChapterName(this.libraryType).toLowerCase());
const msg = translate(direction === 'Next' ? 'toasts.no-next-chapter' : 'toasts.no-prev-chapter', {entity: this.utilityService.formatChapterName(this.libraryType).toLowerCase()});
this.toastr.warning(msg);
this.isLoading = false;
if (direction === 'Prev') {
this.prevPageDisabled = true;

View file

@ -1,22 +1,24 @@
<div class="table-of-contents">
<div *ngIf="Pages.length === 0">
<em>Nothing Bookmarked yet</em>
<ng-container *transloco="let t; read: 'personal-table-of-contents'">
<div class="table-of-contents">
<div *ngIf="Pages.length === 0">
<em>{{t('no-data')}}}</em>
</div>
<ul>
<li *ngFor="let page of Pages">
<span (click)="loadChapterPage(page, '')">{{t('page', {value: page})}}</span>
<ul class="chapter-title">
<li class="ellipsis"
[ngbTooltip]="bookmark.title"
placement="right"
*ngFor="let bookmark of bookmarks[page]" (click)="loadChapterPage(bookmark.pageNumber, bookmark.bookScrollId); $event.stopPropagation();">
{{bookmark.title}}
<button class="btn btn-icon ms-1" (click)="removeBookmark(bookmark); $event.stopPropagation();">
<i class="fa-solid fa-trash" aria-hidden="true"></i>
<span class="visually-hidden">{{t('delete', {bookmarkName: bookmark.title})}}</span>
</button>
</li>
</ul>
</li>
</ul>
</div>
<ul>
<li *ngFor="let page of Pages">
<span (click)="loadChapterPage(page, '')">Page {{page}}</span>
<ul class="chapter-title">
<li class="ellipsis"
[ngbTooltip]="bookmark.title"
placement="right"
*ngFor="let bookmark of bookmarks[page]" (click)="loadChapterPage(bookmark.pageNumber, bookmark.bookScrollId); $event.stopPropagation();">
{{bookmark.title}}
<button class="btn btn-icon ms-1" (click)="removeBookmark(bookmark); $event.stopPropagation();">
<i class="fa-solid fa-trash" aria-hidden="true"></i>
<span class="visually-hidden">Delete {{bookmark.title}}</span>
</button>
</li>
</ul>
</li>
</ul>
</div>
</ng-container>

View file

@ -13,6 +13,7 @@ import {ReaderService} from "../../../_services/reader.service";
import {PersonalToC} from "../../../_models/readers/personal-toc";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
import {TranslocoModule} from "@ngneat/transloco";
export interface PersonalToCEvent {
pageNum: number;
@ -22,7 +23,7 @@ export interface PersonalToCEvent {
@Component({
selector: 'app-personal-table-of-contents',
standalone: true,
imports: [CommonModule, NgbTooltip],
imports: [CommonModule, NgbTooltip, TranslocoModule],
templateUrl: './personal-table-of-contents.component.html',
styleUrls: ['./personal-table-of-contents.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush

View file

@ -1,169 +1,172 @@
<!-- IDEA: Move the whole reader drawer into this component and have it self contained -->
<form [formGroup]="settingsForm">
<ng-container *transloco="let t; read: 'reader-settings'">
<!-- IDEA: Move the whole reader drawer into this component and have it self contained -->
<form [formGroup]="settingsForm">
<div ngbAccordion [closeOthers]="false" #acc="ngbAccordion">
<div ngbAccordionItem id="general-panel" title="General Settings" [collapsed]="false">
<h2 class="accordion-header" ngbAccordionHeader>
<button ngbAccordionButton class="accordion-button" type="button" [attr.aria-expanded]="acc.isExpanded('general-panel')" aria-controls="collapseOne">
General Settings
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="control-container" >
<div class="controls">
<div class="mb-3">
<label for="library-type" class="form-label">Font Family</label>
<select class="form-select" id="library-type" formControlName="bookReaderFontFamily">
<option [value]="opt" *ngFor="let opt of fontOptions; let i = index">{{opt | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0 controls">
<label for="fontsize" class="form-label col-6">Font Size</label>
<span class="col-6 float-end" style="display: inline-flex;">
<div ngbAccordionItem id="general-panel" title="General Settings" [collapsed]="false">
<h2 class="accordion-header" ngbAccordionHeader>
<button ngbAccordionButton class="accordion-button" type="button" [attr.aria-expanded]="acc.isExpanded('general-panel')" aria-controls="collapseOne">
{{t('general-settings-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="control-container" >
<div class="controls">
<div class="mb-3">
<label for="library-type" class="form-label">{{t('font-family-label')}}</label>
<select class="form-select" id="library-type" formControlName="bookReaderFontFamily">
<option [value]="opt" *ngFor="let opt of fontOptions; let i = index">{{opt | titlecase}}</option>
</select>
</div>
</div>
<div class="row g-0 controls">
<label for="fontsize" class="form-label col-6">{{t('font-size-label')}}</label>
<span class="col-6 float-end" style="display: inline-flex;">
<i class="fa-solid fa-font" style="font-size: 12px;"></i>
<input type="range" class="form-range ms-2 me-2" id="fontsize" min="50" max="300" step="10" formControlName="bookReaderFontSize" [ngbTooltip]="settingsForm.get('bookReaderFontSize')?.value + '%'">
<i class="fa-solid fa-font" style="font-size: 24px;"></i>
</span>
</div>
</div>
<div class="row g-0 controls">
<label for="linespacing" class="form-label col-6">Line Spacing</label>
<span class="col-6 float-end" style="display: inline-flex;">
<div class="row g-0 controls">
<label for="linespacing" class="form-label col-6">{{t('line-spacing-label')}}</label>
<span class="col-6 float-end" style="display: inline-flex;">
1x
<input type="range" class="form-range ms-2 me-2" id="linespacing" min="100" max="200" step="10" formControlName="bookReaderLineSpacing" [ngbTooltip]="settingsForm.get('bookReaderLineSpacing')?.value + '%'">
2.5x
</span>
</div>
</div>
<div class="row g-0 controls">
<label for="margin" class="form-label col-6">Margin</label>
<span class="col-6 float-end" style="display: inline-flex;">
<div class="row g-0 controls">
<label for="margin" class="form-label col-6">{{t('margin-label')}}</label>
<span class="col-6 float-end" style="display: inline-flex;">
<i class="fa-solid fa-outdent"></i>
<input type="range" class="form-range ms-2 me-2" id="margin" min="0" max="30" step="5" formControlName="bookReaderMargin" [ngbTooltip]="settingsForm.get('bookReaderMargin')?.value + '%'">
<i class="fa-solid fa-indent"></i>
</span>
</div>
</div>
<div class="row g-0 justify-content-between mt-2">
<button (click)="resetSettings()" class="btn btn-primary col">Reset to Defaults</button>
</div>
</div>
</ng-template>
<div class="row g-0 justify-content-between mt-2">
<button (click)="resetSettings()" class="btn btn-primary col">{{t('reset-to-defaults')}}</button>
</div>
</div>
</div>
</ng-template>
</div>
</div>
<div ngbAccordionItem id="reader-panel" title="Reader Settings" [collapsed]="false">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded('reader-panel')" aria-controls="collapseOne">
Reader Settings
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label id="readingdirection" class="form-label">Reading Direction</label>
<button (click)="toggleReadingDirection()" class="btn btn-icon" aria-labelledby="readingdirection" title="{{readingDirectionModel === ReadingDirection.LeftToRight ? 'Left to Right' : 'Right to Left'}}">
<i class="fa {{readingDirectionModel === ReadingDirection.LeftToRight ? 'fa-arrow-right' : 'fa-arrow-left'}} " aria-hidden="true"></i>
<span class="phone-hidden">&nbsp;{{readingDirectionModel === ReadingDirection.LeftToRight ? 'Left to Right' : 'Right to Left'}}</span>
</button>
</div>
<div class="controls" style="display: flex; justify-content: space-between; align-items: center; ">
<label for="writing-style" class="form-label">Writing Style <i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="writingStyleTooltip" role="button" tabindex="0" aria-describedby="writingStyle-help"></i></label>
<ng-template #writingStyleTooltip>Changes the direction of the text. Horizontal is left to right, vertical is top to bottom.</ng-template>
<span class="visually-hidden" id="writingStyle-help"><ng-container [ngTemplateOutlet]="writingStyleTooltip"></ng-container></span>
<button (click)="toggleWritingStyle()" id="writing-style" class="btn btn-icon" aria-labelledby="writingStyle-help" title="{{writingStyleModel === WritingStyle.Horizontal ? 'Horizontal' : 'Vertical'}}">
<i class="fa {{writingStyleModel === WritingStyle.Horizontal ? 'fa-arrows-left-right' : 'fa-arrows-up-down' }}" aria-hidden="true"></i>
<span class="phone-hidden"> {{writingStyleModel === WritingStyle.Horizontal ? 'Horizontal' : 'Vertical' }}</span>
</button>
</div>
<div ngbAccordionItem id="reader-panel" title="Reader Settings" [collapsed]="false">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded('reader-panel')" aria-controls="collapseOne">
{{t('reader-settings-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label id="readingdirection" class="form-label">{{t('reading-direction-label')}}</label>
<button (click)="toggleReadingDirection()" class="btn btn-icon" aria-labelledby="readingdirection" title="{{readingDirectionModel === ReadingDirection.LeftToRight ? t('left-to-right') : t('right-to-left')}}">
<i class="fa {{readingDirectionModel === ReadingDirection.LeftToRight ? 'fa-arrow-right' : 'fa-arrow-left'}} " aria-hidden="true"></i>
<span class="phone-hidden">&nbsp;{{readingDirectionModel === ReadingDirection.LeftToRight ? t('left-to-right') : t('right-to-left')}}</span>
</button>
</div>
<div class="controls" style="display: flex; justify-content: space-between; align-items: center; ">
<label for="writing-style" class="form-label">{{t('writing-style-label')}}<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top" [ngbTooltip]="writingStyleTooltip" role="button" tabindex="0" aria-describedby="writingStyle-help"></i></label>
<ng-template #writingStyleTooltip>{{t('writing-style-tooltip')}}</ng-template>
<span class="visually-hidden" id="writingStyle-help"><ng-container [ngTemplateOutlet]="writingStyleTooltip"></ng-container></span>
<button (click)="toggleWritingStyle()" id="writing-style" class="btn btn-icon" aria-labelledby="writingStyle-help" title="{{writingStyleModel === WritingStyle.Horizontal ? t('horizontal') : t('vertical')}}">
<i class="fa {{writingStyleModel === WritingStyle.Horizontal ? 'fa-arrows-left-right' : 'fa-arrows-up-down' }}" aria-hidden="true"></i>
<span class="phone-hidden"> {{writingStyleModel === WritingStyle.Horizontal ? t('horizontal') : t('vertical') }}</span>
</button>
</div>
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label for="tap-pagination" class="form-label">{{t('tap-to-paginate-label')}}<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top" [ngbTooltip]="tapPaginationTooltip" role="button" tabindex="0" aria-describedby="tapPagination-help"></i></label>
<ng-template #tapPaginationTooltip>{{t('tap-to-paginate-tooltip')}}</ng-template>
<span class="visually-hidden" id="tapPagination-help">
<ng-container [ngTemplateOutlet]="tapPaginationTooltip"></ng-container>
</span>
<div class="form-check form-switch">
<input type="checkbox" id="tap-pagination" formControlName="bookReaderTapToPaginate" class="form-check-input" aria-labelledby="tapPagination-help">
<label>{{settingsForm.get('bookReaderTapToPaginate')?.value ? t('on') : t('off')}} </label>
</div>
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label for="tap-pagination" class="form-label">Tap Pagination&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="tapPaginationTooltip" role="button" tabindex="0" aria-describedby="tapPagination-help"></i></label>
<ng-template #tapPaginationTooltip>Click the edges of the screen to paginate</ng-template>
<span class="visually-hidden" id="tapPagination-help">
<ng-container [ngTemplateOutlet]="tapPaginationTooltip"></ng-container>
</span>
<div class="form-check form-switch">
<input type="checkbox" id="tap-pagination" formControlName="bookReaderTapToPaginate" class="form-check-input" aria-labelledby="tapPagination-help">
<label>{{settingsForm.get('bookReaderTapToPaginate')?.value ? 'On' : 'Off'}} </label>
</div>
</div>
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label for="immersive-mode" class="form-label">Immersive Mode&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="immersiveModeTooltip" role="button" tabindex="0" aria-describedby="immersiveMode-help"></i></label>
<ng-template #immersiveModeTooltip>This will hide the menu behind a click on the reader document and turn tap to paginate on</ng-template>
<span class="visually-hidden" id="immersiveMode-help">
</div>
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label for="immersive-mode" class="form-label">{{t('immersive-mode-label')}}<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top" [ngbTooltip]="immersiveModeTooltip" role="button" tabindex="0" aria-describedby="immersiveMode-help"></i></label>
<ng-template #immersiveModeTooltip>{{t('immersive-mode-tooltip')}}</ng-template>
<span class="visually-hidden" id="immersiveMode-help">
<ng-container [ngTemplateOutlet]="immersiveModeTooltip"></ng-container>
</span>
<div class="form-check form-switch">
<input type="checkbox" id="immersive-mode" formControlName="bookReaderImmersiveMode" class="form-check-input" aria-labelledby="immersiveMode-help">
<label>{{settingsForm.get('bookReaderImmersiveMode')?.value ? 'On' : 'Off'}} </label>
</div>
<div class="form-check form-switch">
<input type="checkbox" id="immersive-mode" formControlName="bookReaderImmersiveMode" class="form-check-input" aria-labelledby="immersiveMode-help">
<label>{{settingsForm.get('bookReaderImmersiveMode')?.value ? t('on') : t('off')}} </label>
</div>
<!-- TODO: move this inline style into a class -->
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label id="fullscreen" class="form-label">Fullscreen&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="top"
[ngbTooltip]="fullscreenTooltip" role="button" tabindex="1" aria-describedby="fullscreen-help"></i></label>
<ng-template #fullscreenTooltip>Put reader in fullscreen mode</ng-template>
<span class="visually-hidden" 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="activeTheme?.isDarkTheme">&nbsp;{{isFullscreen ? 'Exit' : 'Enter'}}</span>
</div>
<div class="controls" style="display:flex; justify-content:space-between; align-items:center;">
<label id="fullscreen" class="form-label">{{t('fullscreen-label')}}<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top"
[ngbTooltip]="fullscreenTooltip" role="button" tabindex="1" aria-describedby="fullscreen-help"></i></label>
<ng-template #fullscreenTooltip>{{t('fullscreen-tooltip')}}</ng-template>
<span class="visually-hidden" 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="activeTheme?.isDarkTheme">&nbsp;{{isFullscreen ? t('exit') : t('enter')}}</span>
</button>
</div>
<div class="controls">
<label id="layout-mode" class="form-label" style="margin-bottom:0.5rem">{{t('layout-mode-label')}}<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top" [ngbTooltip]="layoutTooltip" role="button" tabindex="1" aria-describedby="layout-help"></i></label>
<ng-template #layoutTooltip><span [innerHTML]="t('layout-mode-tooltip')"></span></ng-template>
<span class="visually-hidden" id="layout-help">
<ng-container [ngTemplateOutlet]="layoutTooltip"></ng-container>
</span>
<br>
<div class="btn-group d-flex justify-content-center" role="group" [attr.aria-label]="t('layout-mode-label')">
<input type="radio" formControlName="layoutMode" [value]="BookPageLayoutMode.Default" class="btn-check" id="layout-mode-default" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-default">{{t('layout-mode-option-scroll')}}</label>
<input type="radio" formControlName="layoutMode" [value]="BookPageLayoutMode.Column1" class="btn-check" id="layout-mode-col1" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-col1">{{t('layout-mode-option-1col')}}</label>
<input type="radio" formControlName="layoutMode" [value]="BookPageLayoutMode.Column2" class="btn-check" id="layout-mode-col2" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-col2">{{t('layout-mode-option-2col')}}</label>
</div>
</div>
</ng-template>
</div>
</div>
</div>
<div ngbAccordionItem id="color-panel" [title]="t('color-theme-title')" [collapsed]="false">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded('color-panel')" aria-controls="collapseOne">
{{t('color-theme-title')}}
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="controls">
<ng-container *ngFor="let theme of themes">
<button class="btn btn-icon color" (click)="setTheme(theme.name)" [ngClass]="{'active': activeTheme?.name === theme.name}">
<div class="dot" [ngStyle]="{'background-color': theme.colorHash}"></div>
{{t('theme.translationKey')}}
</button>
</div>
<div class="controls">
<label id="layout-mode" class="form-label" style="margin-bottom:0.5rem">Layout Mode&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="layoutTooltip" role="button" tabindex="1" aria-describedby="layout-help"></i></label>
<ng-template #layoutTooltip>Scroll: Mirrors epub file (usually one long scrolling page per chapter).<br/>1 Column: Creates a single virtual page at a time.<br/>2 Column: Creates two virtual pages at a time laid out side-by-side.</ng-template>
<span class="visually-hidden" id="layout-help">
<ng-container [ngTemplateOutlet]="layoutTooltip"></ng-container>
</span>
<br>
<div class="btn-group d-flex justify-content-center" role="group" aria-label="Layout Mode">
<input type="radio" formControlName="layoutMode" [value]="BookPageLayoutMode.Default" class="btn-check" id="layout-mode-default" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-default">Scroll</label>
<input type="radio" formControlName="layoutMode" [value]="BookPageLayoutMode.Column1" class="btn-check" id="layout-mode-col1" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-col1">1 Column</label>
<input type="radio" formControlName="layoutMode" [value]="BookPageLayoutMode.Column2" class="btn-check" id="layout-mode-col2" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-col2">2 Column</label>
</div>
</div>
</ng-template>
</div>
</div>
</div>
<div ngbAccordionItem id="color-panel" title="Color Theme" [collapsed]="false">
<h2 class="accordion-header" ngbAccordionHeader>
<button class="accordion-button" ngbAccordionButton type="button" [attr.aria-expanded]="acc.isExpanded('color-panel')" aria-controls="collapseOne">
Color Theme
</button>
</h2>
<div ngbAccordionCollapse>
<div ngbAccordionBody>
<ng-template>
<div class="controls">
<ng-container *ngFor="let theme of themes">
<button class="btn btn-icon color" (click)="setTheme(theme.name)" [ngClass]="{'active': activeTheme?.name === theme.name}">
<div class="dot" [ngStyle]="{'background-color': theme.colorHash}"></div>
{{theme.name}}
</button>
</ng-container>
</div>
</ng-template>
</div>
</ng-container>
</div>
</ng-template>
</div>
</div>
</div>
</div>
</form>
</form>
</ng-container>

View file

@ -26,6 +26,7 @@ import { BookWhiteTheme } from '../../_models/book-white-theme';
import { BookPaperTheme } from '../../_models/book-paper-theme';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import { NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
import {TranslocoModule} from "@ngneat/transloco";
/**
* Used for book reader. Do not use for other components
@ -46,7 +47,8 @@ export const bookColorThemes = [
isDefault: true,
provider: ThemeProvider.System,
selector: 'brtheme-dark',
content: BookDarkTheme
content: BookDarkTheme,
translationKey: 'theme-dark'
},
{
name: 'Black',
@ -55,7 +57,8 @@ export const bookColorThemes = [
isDefault: false,
provider: ThemeProvider.System,
selector: 'brtheme-black',
content: BookBlackTheme
content: BookBlackTheme,
translationKey: 'theme-black'
},
{
name: 'White',
@ -64,7 +67,8 @@ export const bookColorThemes = [
isDefault: false,
provider: ThemeProvider.System,
selector: 'brtheme-white',
content: BookWhiteTheme
content: BookWhiteTheme,
translationKey: 'theme-white'
},
{
name: 'Paper',
@ -73,7 +77,8 @@ export const bookColorThemes = [
isDefault: false,
provider: ThemeProvider.System,
selector: 'brtheme-paper',
content: BookPaperTheme
content: BookPaperTheme,
translationKey: 'theme-paper'
},
];
@ -85,7 +90,7 @@ const mobileBreakpointMarginOverride = 700;
styleUrls: ['./reader-settings.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [ReactiveFormsModule, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, NgFor, NgbTooltip, NgTemplateOutlet, NgIf, NgClass, NgStyle, TitleCasePipe]
imports: [ReactiveFormsModule, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, NgbAccordionToggle, NgbAccordionButton, NgbCollapse, NgbAccordionCollapse, NgbAccordionBody, NgFor, NgbTooltip, NgTemplateOutlet, NgIf, NgClass, NgStyle, TitleCasePipe, TranslocoModule]
})
export class ReaderSettingsComponent implements OnInit {
/**

View file

@ -1,24 +1,26 @@
<div class="table-of-contents">
<ng-container *transloco="let t; read: 'table-of-contents'">
<div class="table-of-contents">
<div *ngIf="chapters.length === 0">
<em>This book does not have Table of Contents set in the metadata or a toc file</em>
<em>{{t('no-data')}}}</em>
</div>
<div *ngIf="chapters.length === 1; else nestedChildren">
<ul>
<li *ngFor="let chapter of chapters[0].children">
<a href="javascript:void(0);" (click)="loadChapterPage(chapter.page, chapter.part)">{{chapter.title}}</a>
</li>
</ul>
<ul>
<li *ngFor="let chapter of chapters[0].children">
<a href="javascript:void(0);" (click)="loadChapterPage(chapter.page, chapter.part)">{{chapter.title}}</a>
</li>
</ul>
</div>
<ng-template #nestedChildren>
<ul *ngFor="let chapterGroup of chapters" class="chapter-title">
<li class="{{chapterGroup.page === pageNum ? 'active': ''}}" (click)="loadChapterPage(chapterGroup.page, '')">
{{chapterGroup.title}}
</li>
<ul *ngFor="let chapter of chapterGroup.children">
<li class="{{cleanIdSelector(chapter.part) === currentPageAnchor ? 'active' : ''}}">
<a href="javascript:void(0);" (click)="loadChapterPage(chapter.page, chapter.part)">{{chapter.title}}</a>
</li>
</ul>
<ul *ngFor="let chapterGroup of chapters" class="chapter-title">
<li class="{{chapterGroup.page === pageNum ? 'active': ''}}" (click)="loadChapterPage(chapterGroup.page, '')">
{{chapterGroup.title}}
</li>
<ul *ngFor="let chapter of chapterGroup.children">
<li class="{{cleanIdSelector(chapter.part) === currentPageAnchor ? 'active' : ''}}">
<a href="javascript:void(0);" (click)="loadChapterPage(chapter.page, chapter.part)">{{chapter.title}}</a>
</li>
</ul>
</ul>
</ng-template>
</div>
</div>
</ng-container>

View file

@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { BookChapterItem } from '../../_models/book-chapter-item';
import { NgIf, NgFor } from '@angular/common';
import {TranslocoModule} from "@ngneat/transloco";
@Component({
selector: 'app-table-of-contents',
@ -8,7 +9,7 @@ import { NgIf, NgFor } from '@angular/common';
styleUrls: ['./table-of-contents.component.scss'],
changeDetection: ChangeDetectionStrategy.Default,
standalone: true,
imports: [NgIf, NgFor]
imports: [NgIf, NgFor, TranslocoModule]
})
export class TableOfContentsComponent {