Book Reader Redesign with e-ink focus (#1246)
* Refactored the drawer into offcanvas component. Had to write some hacks to emulate how bootstrap's javascript implementation works as ngBootstrap doesn't have a component yet. * Cleaned up some of the code * Rewrote drawer to align it with the new design * First pass, refactored table of content into it's own component * Refactored all of the settings logic into a separate component. Everything is broken. * More settings on on reactive form * More code cleanup on settings * Misc fixes around the drawer code. Fixed a bug where range sliders were inheriting background color of normal text inputs * Fixed dark mode with book reader. We now clear the theme from the main app so book reader is self-contained. Styles for dark mode are injected into the reading-section. Styles that were previously in scss are now only for the actual menu system. * Cleaned up drawer styling on header * Removed an ngIf statement for click to paginate * Tweaked the accent style to have smaller font size and adjusted style on light mode. Cleaned up some clearTimeout code in a further effort to streamline codebase. * Refactored Dark mode into a basic theme. Currently styles are hardcoded. * Patched book theme in from themes branch * Patched in the backend for Book Theme (not tested yet) * Fixed a bug in seeding code for book themes. Started integration of themes into the reader settings * Everything except managing themes is working. Themes are a bit shakey, having second thoughts if we should have them or not. * Reverted the ability to do custom user book themes. Code is stable with system themes. * Stablize the Styles (#1128) * Fixed a bug where adding multiple series to reading list would throw an error on UI, but it was successful. * When a series has a reading list, we now show the connection on Series detail. * Removed all baseurl code from UI and not-connected component since we no longer use it. * Fixed tag badges not showing a border. Added last read time to the series detail page * Fixed up error interceptor to remove no-connection code * Changed implementation for series detail. Book libraries will never send chapters back. Volume 0 volumes will not be sent in volumes ever. Fixed up more renaming logic on books to send more accurate representations to the UI. * Cleaned up the selected tab and tab display logic * Fixed a bad where statement in reading lists for series * Fixed up tab logic again * Fixed a small margin on search backdrop * Made badge expander button smaller to align with badges * Fixed a few UIs due to .form-group and .form-row being removed * Updated Theme component page to help with style testing * Added more components to theme tester * Cleaned up some styling * Fixed opacity on search item hover * Bump versions by dotnet-bump-version. * Tweaked the accordion styles for light mode * Set dark book theme as default. Refactored resetSettings to be much cleaner * Started the refactor to allow book themes to affect global css variables * Fixed some issues with my css variable declarations * Fixed a close model state update * Lots of work, but dark mode on the book reader is basically done. We have to code the themes much like the site themes * Some black theme enhancements * Started working on column layout in book reader. * Cleaned up the CSS on Reader Settings * Hooked up reading direction * Got column and double column layout working * Implemented some basic virtual paging and hooked in book color theme and layout mode into user preferences. * Migration wrote, can edit page layout and color theme on book reader. Removed book dark mode since no longer needed. Fixed a bug on login/register forms where when input is focused, text is white and not black. * When loading book reader, apply column layout. * Lots of work around 2 column layout, working on images not splitting. Still not working, committing so i can merge develop in and validate code with new manga reader. * Fixed images being split into 2 BUT regression on each page boundary, total reading height is smaller and smaller * Fixed some rendering bugs where toggling column layouts would shrink images on screen constantly. Fixed a bug where bottom bar wouldn't render on column layout in some conditions (this might need to be reworked) * Started progress on progress work * Updated .NET to 6.0.4 * Fixed a bug where DataContextModelSnapshot was being removed on build thus new migrations were broken. * Tweaked the code around progress saving so that we don't loose track of last scroll element on page load * Trying to restore progress, but stuck * Extra merge stuff * Fixed a bug where volumes that are a range fail to generate series detail * No gutters on whole app. Book reader backend now applies the image class automatically at the backend. * Added wiki documentation into invite user flow and register admin user to help users understand email isn't required and they can host their own service. * Removed bottom padding * Refactored the document height to be set and removed on nav service, so the book reader and manga reader aren't broken. * Fixed the height of the action bar to simplify logic and keep the code cleaner. Refactored book service image scoping to be much more streamlined and efficient * Fixed the height of action bar to 62px and adjusted code to use the hardcoded px. (code commented) * Removed commented out code from fixed action bar height * Progress restoration seems to be working * Code cleanup * Ensure the bottom action bar is at the bottom of the viewport on small pages * Fixed book fonts not setting properly and added OpenDyslexic font. * Fixed up some font issues * Updated drawer so all sections are open by default * Switched some LINQ to use MinBy * When navigating between pages and column layout, adjust the shift for the user. * Removed some debug code * Blacklist .qpkg folders and don't scan Recently-Snapshot or recycle folders. * Renamed the scale width to be scoped to kavita to avoid conflicts. * Refactored ngx-sliders out to use normal range instead. Changed up the preferences to separate image and book settinngs into own accordion. * updated user preferences for new migration options (not committed yet) * Removed some debug code * Remove console.logs * Migration committed, let's release this to users. * A lot of crazy code just to ensure that when you close drawer the toggle reflectst that state.
This commit is contained in:
parent
641e0a71e9
commit
2723a6cd10
79 changed files with 3708 additions and 1190 deletions
114
UI/Web/src/app/book-reader/_models/book-black-theme.ts
Normal file
114
UI/Web/src/app/book-reader/_models/book-black-theme.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
// Important note about themes. Must have one section with .reader-container that contains color, background-color and rest of the styles must be scoped to .book-content
|
||||
export const BookBlackTheme = `
|
||||
:root .brtheme-black {
|
||||
/* General */
|
||||
--color-scheme: dark;
|
||||
--bs-body-color: black;
|
||||
--hr-color: rgba(239, 239, 239, 0.125);
|
||||
--accent-bg-color: rgba(1, 4, 9, 0.5);
|
||||
--accent-text-color: lightgrey;
|
||||
--body-text-color: #efefef;
|
||||
--btn-icon-filter: invert(1) grayscale(100%) brightness(200%);
|
||||
|
||||
/* Drawer */
|
||||
--drawer-bg-color: #292929;
|
||||
--drawer-text-color: white;
|
||||
|
||||
/* Accordion */
|
||||
--accordion-header-text-color: rgba(74, 198, 148, 0.9);
|
||||
--accordion-header-bg-color: rgba(52, 60, 70, 0.5);
|
||||
--accordion-body-bg-color: #292929;
|
||||
--accordion-body-border-color: rgba(239, 239, 239, 0.125);
|
||||
--accordion-body-text-color: var(--body-text-color);
|
||||
--accordion-header-collapsed-text-color: rgba(74, 198, 148, 0.9);
|
||||
--accordion-header-collapsed-bg-color: #292929;
|
||||
--accordion-button-focus-border-color: unset;
|
||||
--accordion-button-focus-box-shadow: unset;
|
||||
--accordion-active-body-bg-color: #292929;
|
||||
|
||||
/* Buttons */
|
||||
--btn-focus-boxshadow-color: rgb(255 255 255 / 50%);
|
||||
--btn-primary-text-color: white;
|
||||
--btn-primary-bg-color: var(--primary-color);
|
||||
--btn-primary-border-color: var(--primary-color);
|
||||
--btn-primary-hover-text-color: white;
|
||||
--btn-primary-hover-bg-color: var(--primary-color-darker-shade);
|
||||
--btn-primary-hover-border-color: var(--primary-color-darker-shade);
|
||||
--btn-alt-bg-color: #424c72;
|
||||
--btn-alt-border-color: #444f75;
|
||||
--btn-alt-hover-bg-color: #3b4466;
|
||||
--btn-alt-focus-bg-color: #343c59;
|
||||
--btn-alt-focus-boxshadow-color: rgb(255 255 255 / 50%);
|
||||
--btn-fa-icon-color: white;
|
||||
--btn-disabled-bg-color: #343a40;
|
||||
--btn-disabled-text-color: white;
|
||||
--btn-disabled-border-color: #6c757d;
|
||||
|
||||
/* Nav (Tabs) */
|
||||
--nav-tab-border-color: rgba(44, 118, 88, 0.7);
|
||||
--nav-tab-text-color: var(--body-text-color);
|
||||
--nav-tab-bg-color: var(--primary-color);
|
||||
--nav-tab-hover-border-color: var(--primary-color);
|
||||
--nav-tab-active-text-color: white;
|
||||
--nav-tab-border-hover-color: transparent;
|
||||
--nav-tab-hover-text-color: var(--body-text-color);
|
||||
--nav-tab-hover-bg-color: transparent;
|
||||
--nav-tab-border-top: rgba(44, 118, 88, 0.7);
|
||||
--nav-tab-border-left: rgba(44, 118, 88, 0.7);
|
||||
--nav-tab-border-bottom: rgba(44, 118, 88, 0.7);
|
||||
--nav-tab-border-right: rgba(44, 118, 88, 0.7);
|
||||
--nav-tab-hover-border-top: rgba(44, 118, 88, 0.7);
|
||||
--nav-tab-hover-border-left: rgba(44, 118, 88, 0.7);
|
||||
--nav-tab-hover-border-bottom: var(--bs-body-bg);
|
||||
--nav-tab-hover-border-right: rgba(44, 118, 88, 0.7);
|
||||
--nav-tab-active-hover-bg-color: var(--primary-color);
|
||||
--nav-link-bg-color: var(--primary-color);
|
||||
--nav-link-active-text-color: white;
|
||||
--nav-link-text-color: white;
|
||||
|
||||
|
||||
|
||||
/* Reading Bar */
|
||||
--br-actionbar-button-text-color: white;
|
||||
--br-actionbar-button-hover-border-color: #6c757d;
|
||||
--br-actionbar-bg-color: black;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.book-content *:not(input), .book-content *:not(select), .book-content *:not(code), .book-content *:not(:link), .book-content *:not(.ngx-toastr) {
|
||||
color: #dcdcdc !important;
|
||||
}
|
||||
|
||||
.book-content code {
|
||||
color: #e83e8c !important;
|
||||
}
|
||||
|
||||
.book-content :link, .book-content a {
|
||||
color: #8db2e5 !important;
|
||||
}
|
||||
|
||||
.book-content img, .book-content img[src] {
|
||||
z-index: 1;
|
||||
filter: brightness(0.85) !important;
|
||||
background-color: initial !important;
|
||||
}
|
||||
|
||||
.reader-container {
|
||||
color: #dcdcdc !important;
|
||||
background-image: none !important;
|
||||
background-color: black !important;
|
||||
}
|
||||
|
||||
.book-content *:not(code), .book-content *:not(a) {
|
||||
background-color: black;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
border-radius: unset;
|
||||
color: #dcdcdc !important;
|
||||
}
|
||||
|
||||
.book-content :visited, .book-content :visited *, .book-content :visited *[class] {color: rgb(211, 138, 138) !important}
|
||||
.book-content :link:not(cite), :link .book-content *:not(cite) {color: #8db2e5 !important}
|
||||
`;
|
||||
119
UI/Web/src/app/book-reader/_models/book-dark-theme.ts
Normal file
119
UI/Web/src/app/book-reader/_models/book-dark-theme.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
// Important note about themes. Styles must be scoped to .book-content if not css variable overrides
|
||||
export const BookDarkTheme = `
|
||||
:root .brtheme-dark {
|
||||
/* General */
|
||||
--color-scheme: dark;
|
||||
--bs-body-color: #292929;
|
||||
--hr-color: rgba(239, 239, 239, 0.125);
|
||||
--accent-bg-color: rgba(1, 4, 9, 0.5);
|
||||
--accent-text-color: lightgrey;
|
||||
--body-text-color: #efefef;
|
||||
--btn-icon-filter: invert(1) grayscale(100%) brightness(200%);
|
||||
|
||||
/* Drawer */
|
||||
--drawer-bg-color: #292929;
|
||||
--drawer-text-color: white;
|
||||
|
||||
/* Accordion */
|
||||
--accordion-header-text-color: rgba(74, 198, 148, 0.9);
|
||||
--accordion-header-bg-color: rgba(52, 60, 70, 0.5);
|
||||
--accordion-body-bg-color: #292929;
|
||||
--accordion-body-border-color: rgba(239, 239, 239, 0.125);
|
||||
--accordion-body-text-color: var(--body-text-color);
|
||||
--accordion-header-collapsed-text-color: rgba(74, 198, 148, 0.9);
|
||||
--accordion-header-collapsed-bg-color: #292929;
|
||||
--accordion-button-focus-border-color: unset;
|
||||
--accordion-button-focus-box-shadow: unset;
|
||||
--accordion-active-body-bg-color: #292929;
|
||||
|
||||
/* Buttons */
|
||||
--btn-focus-boxshadow-color: rgb(255 255 255 / 50%);
|
||||
--btn-primary-text-color: white;
|
||||
--btn-primary-bg-color: var(--primary-color);
|
||||
--btn-primary-border-color: var(--primary-color);
|
||||
--btn-primary-hover-text-color: white;
|
||||
--btn-primary-hover-bg-color: var(--primary-color-darker-shade);
|
||||
--btn-primary-hover-border-color: var(--primary-color-darker-shade);
|
||||
--btn-alt-bg-color: #424c72;
|
||||
--btn-alt-border-color: #444f75;
|
||||
--btn-alt-hover-bg-color: #3b4466;
|
||||
--btn-alt-focus-bg-color: #343c59;
|
||||
--btn-alt-focus-boxshadow-color: rgb(255 255 255 / 50%);
|
||||
--btn-fa-icon-color: white;
|
||||
--btn-disabled-bg-color: #343a40;
|
||||
--btn-disabled-text-color: white;
|
||||
--btn-disabled-border-color: #6c757d;
|
||||
|
||||
/* Nav (Tabs) */
|
||||
--nav-tab-border-color: rgba(44, 118, 88, 0.7);
|
||||
--nav-tab-text-color: var(--body-text-color);
|
||||
--nav-tab-bg-color: var(--primary-color);
|
||||
--nav-tab-hover-border-color: var(--primary-color);
|
||||
--nav-tab-active-text-color: white;
|
||||
--nav-tab-border-hover-color: transparent;
|
||||
--nav-tab-hover-text-color: var(--body-text-color);
|
||||
--nav-tab-hover-bg-color: transparent;
|
||||
--nav-tab-border-top: rgba(44, 118, 88, 0.7);
|
||||
--nav-tab-border-left: rgba(44, 118, 88, 0.7);
|
||||
--nav-tab-border-bottom: rgba(44, 118, 88, 0.7);
|
||||
--nav-tab-border-right: rgba(44, 118, 88, 0.7);
|
||||
--nav-tab-hover-border-top: rgba(44, 118, 88, 0.7);
|
||||
--nav-tab-hover-border-left: rgba(44, 118, 88, 0.7);
|
||||
--nav-tab-hover-border-bottom: var(--bs-body-bg);
|
||||
--nav-tab-hover-border-right: rgba(44, 118, 88, 0.7);
|
||||
--nav-tab-active-hover-bg-color: var(--primary-color);
|
||||
--nav-link-bg-color: var(--primary-color);
|
||||
--nav-link-active-text-color: white;
|
||||
--nav-link-text-color: white;
|
||||
|
||||
/* Checkboxes/Switch */
|
||||
--checkbox-checked-bg-color: var(--primary-color);
|
||||
--checkbox-border-color: var(--input-focused-border-color);
|
||||
--checkbox-focus-border-color: var(--primary-color);
|
||||
--checkbox-focus-boxshadow-color: rgb(255 255 255 / 50%);
|
||||
|
||||
|
||||
|
||||
/* Reading Bar */
|
||||
--br-actionbar-button-text-color: white;
|
||||
--br-actionbar-button-hover-border-color: #6c757d;
|
||||
--br-actionbar-bg-color: black;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.book-content *:not(input), .book-content *:not(select), .book-content *:not(code), .book-content *:not(:link), .book-content *:not(.ngx-toastr) {
|
||||
color: #dcdcdc !important;
|
||||
}
|
||||
|
||||
.book-content code {
|
||||
color: #e83e8c !important;
|
||||
}
|
||||
|
||||
.book-content :link, .book-content a {
|
||||
color: #8db2e5 !important;
|
||||
}
|
||||
|
||||
.book-content img, .book-content img[src] {
|
||||
z-index: 1;
|
||||
filter: brightness(0.85) !important;
|
||||
background-color: initial !important;
|
||||
}
|
||||
|
||||
.reader-container {
|
||||
color: #dcdcdc !important;
|
||||
background-image: none !important;
|
||||
background-color: #292929 !important;
|
||||
}
|
||||
|
||||
.book-content *:not(code), .book-content *:not(a) {
|
||||
background-color: #292929;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
border-radius: unset;
|
||||
color: #dcdcdc !important;
|
||||
}
|
||||
|
||||
.book-content :visited, .book-content :visited *, .book-content :visited *[class] {color: rgb(211, 138, 138) !important}
|
||||
.book-content :link:not(cite), :link .book-content *:not(cite) {color: #8db2e5 !important}
|
||||
`;
|
||||
7
UI/Web/src/app/book-reader/_models/book-white-theme.ts
Normal file
7
UI/Web/src/app/book-reader/_models/book-white-theme.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// Important note about themes. Must have one section with .reader-container that contains color, background-color and rest of the styles must be scoped to .book-content
|
||||
export const BookWhiteTheme = `
|
||||
:root() .brtheme-white {
|
||||
--brtheme-link-text-color: green;
|
||||
--brtheme-bg-color: lightgrey;
|
||||
}
|
||||
`;
|
||||
15
UI/Web/src/app/book-reader/_models/theme-font.ts
Normal file
15
UI/Web/src/app/book-reader/_models/theme-font.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* A font family to inject into the book reader
|
||||
*/
|
||||
export interface ThemeFont {
|
||||
/**
|
||||
* Name/Font-family
|
||||
*/
|
||||
fontFamily: string;
|
||||
/**
|
||||
* Where the font is loaded from?
|
||||
*/
|
||||
fontSrc: string;
|
||||
format: 'truetype';
|
||||
|
||||
}
|
||||
|
|
@ -5,12 +5,14 @@ import { BookReaderRoutingModule } from './book-reader.router.module';
|
|||
import { SharedModule } from '../shared/shared.module';
|
||||
import { SafeStylePipe } from './safe-style.pipe';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { NgbProgressbarModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { PipeModule } from '../pipe/pipe.module';
|
||||
import { NgbAccordionModule, NgbNavModule, NgbProgressbarModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TableOfContentsComponent } from './table-of-contents/table-of-contents.component';
|
||||
import { ReaderSettingsComponent } from './reader-settings/reader-settings.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [BookReaderComponent, SafeStylePipe],
|
||||
declarations: [BookReaderComponent, SafeStylePipe, TableOfContentsComponent, ReaderSettingsComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
BookReaderRoutingModule,
|
||||
|
|
@ -19,6 +21,9 @@ import { PipeModule } from '../pipe/pipe.module';
|
|||
NgbProgressbarModule,
|
||||
NgbTooltipModule,
|
||||
PipeModule,
|
||||
NgbTooltipModule,
|
||||
NgbAccordionModule, // Drawer
|
||||
NgbNavModule, // Drawer
|
||||
], exports: [
|
||||
BookReaderComponent,
|
||||
SafeStylePipe
|
||||
|
|
|
|||
|
|
@ -2,74 +2,11 @@
|
|||
<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-drawer #commentDrawer="drawer" [isOpen]="drawerOpen" [style.--drawer-width]="'300px'" [options]="{topOffset: topOffset}" [style.--drawer-background-color]="drawerBackgroundColor" (drawerClosed)="closeDrawer()">
|
||||
<div header>
|
||||
<h2 style="margin-top: 0.5rem">Book Settings
|
||||
<!-- Temp use times rather than btn-close due to some styling issue -->
|
||||
<button type="button" class="btn btn-icon" style="font-size: 2rem" aria-label="Close" (click)="commentDrawer.close()">×</button>
|
||||
</h2>
|
||||
</div>
|
||||
<div body class="drawer-body">
|
||||
<div class="control-container">
|
||||
<div class="controls">
|
||||
|
||||
<form [formGroup]="settingsForm">
|
||||
<div class="mb-3">
|
||||
<label for="library-type" class="form-label">Font Family</label>
|
||||
<select class="form-control" id="library-type" formControlName="bookReaderFontFamily">
|
||||
<option [value]="opt" *ngFor="let opt of fontFamilies; let i = index">{{opt | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<label id="fontsize" class="form-label">Font Size</label>
|
||||
<button (click)="updateFontSize(-10)" class="btn btn-icon" title="Decrease" aria-labelledby="fontsize"><i class="fa fa-minus" aria-hidden="true"></i></button>
|
||||
<span>{{pageStyles['font-size']}}</span>
|
||||
<button (click)="updateFontSize(10)" class="btn btn-icon" title="Increase" aria-labelledby="fontsize"><i class="fa fa-plus" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<label id="linespacing" class="form-label">Line Spacing</label>
|
||||
<button (click)="updateLineSpacing(-10)" class="btn btn-icon" title="Decrease" aria-labelledby="linespacing"><i class="fa fa-minus" aria-hidden="true"></i></button>
|
||||
<span>{{pageStyles['line-height']}}</span>
|
||||
<button (click)="updateLineSpacing(10)" class="btn btn-icon" title="Increase" aria-labelledby="linespacing"><i class="fa fa-plus" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<label id="margin" class="form-label">Margin</label>
|
||||
<button (click)="updateMargin(-5)" class="btn btn-icon" title="Remove Margin" aria-labelledby="margin"><i class="fa fa-minus" aria-hidden="true"></i></button>
|
||||
<span>{{pageStyles['margin-right']}}</span>
|
||||
<button (click)="updateMargin(5)" class="btn btn-icon" title="Add Margin" aria-labelledby="margin"><i class="fa fa-plus" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<label id="readingdirection" class="form-label">Reading Direction</label>
|
||||
<button (click)="toggleReadingDirection()" class="btn btn-icon" aria-labelledby="readingdirection" title="{{readingDirection === 0 ? 'Left to Right' : 'Right to Left'}}"><i class="fa {{readingDirection === 0 ? 'fa-arrow-right' : 'fa-arrow-left'}} " aria-hidden="true"></i><span class="d-none d-sm-block"> {{readingDirection === 0 ? 'Left to Right' : 'Right to Left'}}</span></button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<label id="darkmode" class="form-label">Dark Mode</label>
|
||||
<button (click)="toggleDarkMode(false)" class="btn btn-icon" aria-labelledby="darkmode" title="Off"><i class="fa fa-sun" aria-hidden="true"></i></button>
|
||||
<button (click)="toggleDarkMode(true)" class="btn btn-icon" aria-labelledby="darkmode" title="On"><i class="fa fa-moon" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<label id="tap-pagination" class="form-label">Tap Pagination <i class="fa fa-info-circle" aria-hidden="true" placement="top" [ngbTooltip]="tapPaginationTooltip" role="button" tabindex="0" aria-describedby="tap-pagination-help"></i></label>
|
||||
<ng-template #tapPaginationTooltip>The ability to click the sides of the page to page left and right</ng-template>
|
||||
<span class="visually-hidden" 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> {{clickToPaginate ? 'On' : 'Off'}}</button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<label id="fullscreen" class="form-label">Fullscreen <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="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="darkMode"> {{isFullscreen ? 'Exit' : 'Enter'}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="row g-0 justify-content-between">
|
||||
<button (click)="resetSettings()" class="btn btn-primary col">Reset to Defaults</button>
|
||||
</div>
|
||||
</div>
|
||||
<app-drawer #commentDrawer="drawer" [(isOpen)]="drawerOpen" [options]="{topOffset: topOffset}">
|
||||
<h5 header>
|
||||
Book Settings
|
||||
</h5>
|
||||
<div subheader>
|
||||
<div class="row g-0">
|
||||
<button class="btn btn-small btn-icon col-1" style="padding-left: 0px" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
|
||||
<div class="col-1 page-stub ps-1">{{pageNum}}</div>
|
||||
|
|
@ -79,51 +16,56 @@
|
|||
<div class="col-1 btn-icon page-stub pe-1" (click)="goToPage(maxPages - 1)" title="Go to last page">{{maxPages - 1}}</div>
|
||||
<button class="btn btn-small btn-icon col-1" style="padding-right: 0px; padding-left: 0px" [disabled]="nextChapterDisabled" (click)="loadNextChapter()" title="Next Chapter/Volume"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<div class="table-of-contents">
|
||||
<h3>Table of Contents</h3>
|
||||
<div *ngIf="chapters.length === 0">
|
||||
<em>This book does not have Table of Contents set in the metadata or a toc file</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>
|
||||
</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>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
<div body class="drawer-body">
|
||||
<!-- TODO: Center align the tab pills -->
|
||||
<nav role="navigation">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-pills mb-2" [destroyOnHide]="false">
|
||||
<li [ngbNavItem]="TabID.Settings">
|
||||
<a ngbNavLink>Settings</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-reader-settings
|
||||
(colorThemeUpdate)="setOverrideStyles($event)"
|
||||
(styleUpdate)="updateReaderStyles($event)"
|
||||
(clickToPaginateChanged)="showPaginationOverlay($event)"
|
||||
(fullscreen)="toggleFullscreen()"
|
||||
(layoutModeUpdate)="updateLayoutMode($event)"
|
||||
(readingDirection)="readingDirection = $event"
|
||||
></app-reader-settings>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="TabID.TableOfContents">
|
||||
<a ngbNavLink>Table of Contents</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>
|
||||
</ul>
|
||||
</nav>
|
||||
<div [ngbNavOutlet]="nav" class="mt-3"></div>
|
||||
</div>
|
||||
</app-drawer>
|
||||
</div>
|
||||
|
||||
<div #readingSection class="reading-section" [ngStyle]="{'padding-top': topOffset + 20 + 'px'}"
|
||||
[@isLoading]="isLoading ? true : false" (click)="handleReaderClick($event)">
|
||||
<div #readingSection class="reading-section {{ColumnLayout}}" [ngStyle]="{'padding-top': '62px'}"
|
||||
[@isLoading]="isLoading ? true : false">
|
||||
|
||||
<div #readingHtml class="book-content" [ngStyle]="{'padding-bottom': topOffset + 20 + 'px', 'margin': '0px 0px'}"
|
||||
<div #readingHtml class="book-content" [ngStyle]="{'max-height': ColumnHeight, 'column-width': ColumnWidth}"
|
||||
[innerHtml]="page" *ngIf="page !== undefined"></div>
|
||||
|
||||
<div class="left {{clickOverlayClass('left')}} no-observe" [ngStyle]="{'padding-top': topOffset + 'px'}" (click)="prevPage()" *ngIf="clickToPaginate" tabindex="-1"></div>
|
||||
<div class="{{scrollbarNeeded ? 'right-with-scrollbar' : 'right'}} {{clickOverlayClass('right')}} no-observe" [ngStyle]="{'padding-top': topOffset + 'px'}" (click)="nextPage()" *ngIf="clickToPaginate" tabindex="-1"></div>
|
||||
<ng-container *ngIf="clickToPaginate">
|
||||
<div class="left {{clickOverlayClass('left')}} no-observe" [ngStyle]="{'padding-top': topOffset + 'px'}" (click)="prevPage()" tabindex="-1"></div>
|
||||
<div class="{{scrollbarNeeded ? 'right-with-scrollbar' : 'right'}} {{clickOverlayClass('right')}} no-observe" [ngStyle]="{'padding-top': topOffset + 'px'}" (click)="nextPage()" tabindex="-1"></div>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="page !== undefined && scrollbarNeeded" (click)="$event.stopPropagation();">
|
||||
<div *ngIf="page !== undefined && (scrollbarNeeded || layoutMode !== BookPageLayoutMode.Default)" (click)="$event.stopPropagation();">
|
||||
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #actionBar>
|
||||
<div class="reading-bar row g-0 justify-content-between">
|
||||
<div class="action-bar row g-0 justify-content-between">
|
||||
<button class="btn btn-outline-secondary btn-icon col-2 col-xs-1" (click)="prevPage()"
|
||||
[disabled]="IsPrevDisabled"
|
||||
title="{{readingDirection === ReadingDirection.LeftToRight ? 'Previous' : 'Next'}} Page">
|
||||
|
|
@ -139,8 +81,8 @@
|
|||
</div>
|
||||
</ng-container>
|
||||
<ng-template #showTitle>
|
||||
<span class="book-title-text">{{bookTitle}}</span>
|
||||
<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" [title]="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><span class="d-none d-sm-block"> Close</span></button>
|
||||
|
|
|
|||
|
|
@ -28,135 +28,77 @@
|
|||
src: url(../../../assets/fonts/RocknRoll_One/RocknRollOne-Regular.ttf) format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "OpenDyslexic2";
|
||||
src: url(../../../assets/fonts/OpenDyslexic2/OpenDyslexic-Regular.otf) format("opentype");
|
||||
}
|
||||
|
||||
:root {
|
||||
--br-actionbar-button-text-color: #6c757d;
|
||||
--accordion-body-bg-color: black;
|
||||
--accordion-header-bg-color: grey;
|
||||
--br-actionbar-button-hover-border-color: #6c757d;
|
||||
--br-actionbar-bg-color: white;
|
||||
}
|
||||
|
||||
|
||||
$dark-form-background-no-opacity: rgb(1, 4, 9);
|
||||
$primary-color: #0062cc;
|
||||
|
||||
|
||||
// Drawer
|
||||
.control-container {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.table-of-contents li {
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.page-stub {
|
||||
margin-top: 6px;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
// Drawer End
|
||||
|
||||
.fixed-top {
|
||||
z-index: 1022;
|
||||
}
|
||||
|
||||
.dark-mode {
|
||||
|
||||
color: #dcdcdc !important;
|
||||
background-image: none !important;
|
||||
background-color: #292929 !important;
|
||||
|
||||
*:not(code), *:not(a) {
|
||||
background-color: #292929;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
border-radius: unset;
|
||||
color: #dcdcdc !important;
|
||||
}
|
||||
|
||||
*:not(input), *:not(code), *:not(:link) {
|
||||
color: #dcdcdc !important;
|
||||
}
|
||||
|
||||
code {
|
||||
color: #e83e8c !important;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
:link, a {
|
||||
color: #8db2e5 !important;
|
||||
}
|
||||
|
||||
img, img[src] {
|
||||
z-index: 1;
|
||||
filter: brightness(0.85) !important;
|
||||
background-color: initial !important;
|
||||
}
|
||||
|
||||
:visited, :visited *, :visited *[class] {color: rgb(211, 138, 138) !important}
|
||||
:link:not(cite), :link *:not(cite) {color: #8db2e5 !important}
|
||||
.dark-mode .overlay {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.reading-bar {
|
||||
background-color: white;
|
||||
|
||||
.action-bar {
|
||||
background-color: var(--br-actionbar-bg-color);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 6px 0 rgb(0 0 0 / 70%);
|
||||
}
|
||||
max-height: 62px;
|
||||
|
||||
.dark-mode {
|
||||
.reading-bar, .book-title, .drawer-body, .drawer-container {
|
||||
background-color: $dark-form-background-no-opacity;
|
||||
.book-title-text {
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
button {
|
||||
background-color: $dark-form-background-no-opacity;
|
||||
}
|
||||
|
||||
.btn {
|
||||
&.btn-secondary {
|
||||
border-color: transparent;
|
||||
|
||||
&:hover, &:focus {
|
||||
border-color: #545b62;
|
||||
}
|
||||
@media(max-width: 875px) {
|
||||
.book-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.btn-outline-secondary {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
border-color: #545b62;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
i {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .dark-mode .drawer-container {
|
||||
.header, body, *:not(.progress-bar) {
|
||||
background-color: $dark-form-background-no-opacity !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: 875px) {
|
||||
.book-title {
|
||||
display: none;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.book-title {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.reading-section {
|
||||
max-height: 100vh;
|
||||
width: 100%;
|
||||
//overflow: auto; // This will break progress reporting
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.reader-container {
|
||||
|
|
@ -166,6 +108,40 @@ $primary-color: #0062cc;
|
|||
|
||||
.book-content {
|
||||
position: relative;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
margin: 0px 0px;
|
||||
height: calc(var(--vh)*100); // This will ensure bottom bar extends to the bottom of the screen
|
||||
|
||||
a, :link {
|
||||
color: var(--brtheme-link-text-color);
|
||||
}
|
||||
|
||||
background-color: var(--brtheme-bg-color);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// This is essentially fitting the text to height and when you press next you are scrolling over by page width
|
||||
.column-layout-1 {
|
||||
.book-content {
|
||||
column-count: 1;
|
||||
column-gap: 20px;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.column-layout-2 {
|
||||
.book-content {
|
||||
column-count: 2;
|
||||
column-gap: 20px;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// A bunch of resets so books render correctly
|
||||
|
|
@ -175,18 +151,15 @@ $primary-color: #0062cc;
|
|||
}
|
||||
}
|
||||
|
||||
.drawer-body {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.chapter-title {
|
||||
padding-inline-start: 0px
|
||||
}
|
||||
|
||||
::ng-deep .scale-width {
|
||||
// This is applied to images in the backend
|
||||
::ng-deep .kavita-scale-width {
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
object-position: top center;
|
||||
break-inside: avoid;
|
||||
break-before: column;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -195,9 +168,8 @@ $primary-color: #0062cc;
|
|||
color: $primary-color;
|
||||
}
|
||||
|
||||
.dark-mode .overlay {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.right {
|
||||
|
|
@ -246,14 +218,17 @@ $primary-color: #0062cc;
|
|||
animation: fadein .5s both;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.btn {
|
||||
&.btn-secondary {
|
||||
color: #6c757d;
|
||||
color: var(--br-actionbar-button-text-color);
|
||||
border-color: transparent;
|
||||
background-color: unset;
|
||||
|
||||
&:hover, &:focus {
|
||||
border-color: #545b62;
|
||||
border-color: var(--br-actionbar-button-hover-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -262,18 +237,18 @@ $primary-color: #0062cc;
|
|||
background-color: unset;
|
||||
|
||||
&:hover, &:focus {
|
||||
border-color: #545b62;
|
||||
border-color: var(--br-actionbar-button-hover-border-color); // #545b62;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
background-color: unset;
|
||||
color: #6c757d;
|
||||
color: var(--br-actionbar-button-text-color); // #6c757d;
|
||||
}
|
||||
|
||||
i {
|
||||
background-color: unset;
|
||||
color: #6c757d;
|
||||
color: var(--br-actionbar-button-text-color);
|
||||
}
|
||||
|
||||
&:active {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -10,6 +10,16 @@ export interface BookPage {
|
|||
html: string;
|
||||
}
|
||||
|
||||
export interface FontFamily {
|
||||
/**
|
||||
* What the user should see
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* The actual font face
|
||||
*/
|
||||
family: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
|
@ -20,8 +30,10 @@ export class BookService {
|
|||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
getFontFamilies() {
|
||||
return ['default', 'EBGaramond', 'Fira Sans', 'Lato', 'Libre Baskerville', 'Merriweather', 'Nanum Gothic', 'RocknRoll One'];
|
||||
getFontFamilies(): Array<FontFamily> {
|
||||
return [{title: 'default', family: 'default'}, {title: 'EBGaramond', family: 'EBGaramond'}, {title: 'Fira Sans', family: 'Fira_Sans'},
|
||||
{title: 'Lato', family: 'Lato'}, {title: 'Libre Baskerville', family: 'Libre_Baskerville'}, {title: 'Merriweather', family: 'Merriweather'},
|
||||
{title: 'Nanum Gothic', family: 'Nanum_Gothic'}, {title: 'RocknRoll One', family: 'RocknRoll_One'}, {title: 'Open Dyslexic', family: 'OpenDyslexic2'}];
|
||||
}
|
||||
|
||||
getBookChapters(chapterId: number) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
<!-- IDEA: Move the whole reader drawer into this component and have it self contained -->
|
||||
<form [formGroup]="settingsForm">
|
||||
<ngb-accordion [closeOthers]="false" #acc="ngbAccordion" [activeIds]="['general-panel', 'reader-panel', 'color-panel']">
|
||||
<ngb-panel id="general-panel" title="General Settings">
|
||||
<ng-template ngbPanelHeader>
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button" ngbPanelToggle type="button" [attr.aria-expanded]="acc.isExpanded('general-panel')" aria-controls="collapseOne">
|
||||
General Settings
|
||||
</button>
|
||||
</h2>
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
<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;">
|
||||
<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 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;">
|
||||
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 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;">
|
||||
<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 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>
|
||||
|
||||
</ngb-panel>
|
||||
|
||||
<ngb-panel id="reader-panel" title="Reader Settings">
|
||||
<ng-template ngbPanelHeader>
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button" ngbPanelToggle type="button" [attr.aria-expanded]="acc.isExpanded('reader-panel')" aria-controls="collapseOne">
|
||||
Reader Settings
|
||||
</button>
|
||||
</h2>
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
<div class="controls">
|
||||
<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"> {{readingDirectionModel === ReadingDirection.LeftToRight ? 'Left to Right' : 'Right to Left'}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<label for="tap-pagination" class="form-label">Tap Pagination</label>
|
||||
<div class="accent" id="tap-pagination-help">Click the edges of the screen to paginate</div>
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="tap-pagination" formControlName="bookReaderTapToPaginate" class="form-check-input" aria-labelledby="tap-pagination-help">
|
||||
<label>{{settingsForm.get('bookReaderTapToPaginate')?.value ? 'On' : 'Off'}} </label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<label id="fullscreen" class="form-label">Fullscreen <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="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"> {{isFullscreen ? 'Exit' : 'Enter'}}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<label id="layout-mode" class="form-label">Layout Mode</label>
|
||||
<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">Default</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>
|
||||
</ngb-panel>
|
||||
|
||||
<ngb-panel id="color-panel" title="Color Theme">
|
||||
<ng-template ngbPanelHeader>
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button" ngbPanelToggle type="button" [attr.aria-expanded]="acc.isExpanded('color-panel')" aria-controls="collapseOne">
|
||||
Color Theme
|
||||
</button>
|
||||
</h2>
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
<div class="controls">
|
||||
<ng-container *ngFor="let theme of themes">
|
||||
<button class="btn btn-icon" (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>
|
||||
</ngb-panel>
|
||||
|
||||
</ngb-accordion>
|
||||
</form>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
.dot {
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.active {
|
||||
border: 1px solid var(--primary-color);
|
||||
}
|
||||
|
|
@ -0,0 +1,272 @@
|
|||
import { DOCUMENT } from '@angular/common';
|
||||
import { Component, EventEmitter, Inject, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { Subject, take, takeUntil } from 'rxjs';
|
||||
import { BookPageLayoutMode } from 'src/app/_models/book-page-layout-mode';
|
||||
import { BookTheme } from 'src/app/_models/preferences/book-theme';
|
||||
import { ReadingDirection } from 'src/app/_models/preferences/reading-direction';
|
||||
import { ThemeProvider } from 'src/app/_models/preferences/site-theme';
|
||||
import { User } from 'src/app/_models/user';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { ThemeService } from 'src/app/_services/theme.service';
|
||||
import { BookService, FontFamily } from '../book.service';
|
||||
import { BookBlackTheme } from '../_models/book-black-theme';
|
||||
import { BookDarkTheme } from '../_models/book-dark-theme';
|
||||
import { BookWhiteTheme } from '../_models/book-white-theme';
|
||||
|
||||
/**
|
||||
* Used for book reader. Do not use for other components
|
||||
*/
|
||||
export interface PageStyle {
|
||||
'font-family': string;
|
||||
'font-size': string;
|
||||
'line-height': string;
|
||||
'margin-left': string;
|
||||
'margin-right': string;
|
||||
}
|
||||
|
||||
export const bookColorThemes = [
|
||||
{
|
||||
name: 'Dark',
|
||||
colorHash: '#292929',
|
||||
isDarkTheme: true,
|
||||
isDefault: true,
|
||||
provider: ThemeProvider.System,
|
||||
selector: 'brtheme-dark',
|
||||
content: BookDarkTheme
|
||||
},
|
||||
{
|
||||
name: 'Black',
|
||||
colorHash: '#000000',
|
||||
isDarkTheme: true,
|
||||
isDefault: false,
|
||||
provider: ThemeProvider.System,
|
||||
selector: 'brtheme-black',
|
||||
content: BookBlackTheme
|
||||
},
|
||||
{
|
||||
name: 'White',
|
||||
colorHash: '#FFFFFF',
|
||||
isDarkTheme: false,
|
||||
isDefault: false,
|
||||
provider: ThemeProvider.System,
|
||||
selector: 'brtheme-white',
|
||||
content: BookWhiteTheme
|
||||
},
|
||||
];
|
||||
|
||||
const mobileBreakpointMarginOverride = 700;
|
||||
|
||||
@Component({
|
||||
selector: 'app-reader-settings',
|
||||
templateUrl: './reader-settings.component.html',
|
||||
styleUrls: ['./reader-settings.component.scss']
|
||||
})
|
||||
export class ReaderSettingsComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* Outputs when clickToPaginate is changed
|
||||
*/
|
||||
@Output() clickToPaginateChanged: EventEmitter<boolean> = new EventEmitter();
|
||||
/**
|
||||
* Outputs when a style is updated and the reader needs to render it
|
||||
*/
|
||||
@Output() styleUpdate: EventEmitter<PageStyle> = new EventEmitter();
|
||||
/**
|
||||
* Outputs when a theme/dark mode is updated
|
||||
*/
|
||||
@Output() colorThemeUpdate: EventEmitter<BookTheme> = new EventEmitter();
|
||||
/**
|
||||
* Outputs when a layout mode is updated
|
||||
*/
|
||||
@Output() layoutModeUpdate: EventEmitter<BookPageLayoutMode> = new EventEmitter();
|
||||
/**
|
||||
* Outputs when fullscreen is toggled
|
||||
*/
|
||||
@Output() fullscreen: EventEmitter<void> = new EventEmitter();
|
||||
/**
|
||||
* Outputs when reading direction is changed
|
||||
*/
|
||||
@Output() readingDirection: EventEmitter<ReadingDirection> = new EventEmitter();
|
||||
|
||||
user!: User;
|
||||
/**
|
||||
* List of all font families user can select from
|
||||
*/
|
||||
fontOptions: Array<string> = [];
|
||||
fontFamilies: Array<FontFamily> = [];
|
||||
/**
|
||||
* Internal property used to capture all the different css properties to render on all elements
|
||||
*/
|
||||
pageStyles!: PageStyle;
|
||||
|
||||
readingDirectionModel: ReadingDirection = ReadingDirection.LeftToRight;
|
||||
|
||||
activeTheme: BookTheme | undefined;
|
||||
|
||||
isFullscreen: boolean = false;
|
||||
|
||||
settingsForm: FormGroup = new FormGroup({});
|
||||
|
||||
/**
|
||||
* System provided themes
|
||||
*/
|
||||
themes: Array<BookTheme> = bookColorThemes;
|
||||
|
||||
|
||||
private onDestroy: Subject<void> = new Subject();
|
||||
|
||||
|
||||
get BookPageLayoutMode(): typeof BookPageLayoutMode {
|
||||
return BookPageLayoutMode;
|
||||
}
|
||||
|
||||
get ReadingDirection() {
|
||||
return ReadingDirection;
|
||||
}
|
||||
|
||||
|
||||
|
||||
constructor(private bookService: BookService, private accountService: AccountService, @Inject(DOCUMENT) private document: Document, private themeService: ThemeService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.fontFamilies = this.bookService.getFontFamilies();
|
||||
this.fontOptions = this.fontFamilies.map(f => f.title);
|
||||
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.user = user;
|
||||
|
||||
if (this.user.preferences.bookReaderFontFamily === undefined) {
|
||||
this.user.preferences.bookReaderFontFamily = 'default';
|
||||
}
|
||||
if (this.user.preferences.bookReaderFontSize === undefined || this.user.preferences.bookReaderFontSize < 50) {
|
||||
this.user.preferences.bookReaderFontSize = 100;
|
||||
}
|
||||
if (this.user.preferences.bookReaderLineSpacing === undefined || this.user.preferences.bookReaderLineSpacing < 100) {
|
||||
this.user.preferences.bookReaderLineSpacing = 100;
|
||||
}
|
||||
if (this.user.preferences.bookReaderMargin === undefined) {
|
||||
this.user.preferences.bookReaderMargin = 0;
|
||||
}
|
||||
if (this.user.preferences.bookReaderReadingDirection === undefined) {
|
||||
this.user.preferences.bookReaderReadingDirection = ReadingDirection.LeftToRight;
|
||||
}
|
||||
|
||||
|
||||
this.readingDirectionModel = this.user.preferences.bookReaderReadingDirection;
|
||||
|
||||
this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.user.preferences.bookReaderFontFamily, []));
|
||||
this.settingsForm.get('bookReaderFontFamily')!.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(fontName => {
|
||||
const familyName = this.fontFamilies.filter(f => f.title === fontName)[0].family;
|
||||
if (familyName === 'default') {
|
||||
this.pageStyles['font-family'] = 'inherit';
|
||||
} else {
|
||||
this.pageStyles['font-family'] = "'" + familyName + "'";
|
||||
}
|
||||
|
||||
this.styleUpdate.emit(this.pageStyles);
|
||||
});
|
||||
|
||||
this.settingsForm.addControl('bookReaderFontSize', new FormControl(this.user.preferences.bookReaderFontSize, []));
|
||||
this.settingsForm.get('bookReaderFontSize')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(value => {
|
||||
this.pageStyles['font-size'] = value + '%';
|
||||
this.styleUpdate.emit(this.pageStyles);
|
||||
});
|
||||
|
||||
this.settingsForm.addControl('bookReaderTapToPaginate', new FormControl(this.user.preferences.bookReaderTapToPaginate, []));
|
||||
this.settingsForm.get('bookReaderTapToPaginate')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(value => {
|
||||
this.clickToPaginateChanged.emit(value);
|
||||
});
|
||||
|
||||
|
||||
this.settingsForm.addControl('bookReaderLineSpacing', new FormControl(this.user.preferences.bookReaderLineSpacing, []));
|
||||
this.settingsForm.get('bookReaderLineSpacing')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(value => {
|
||||
this.pageStyles['line-height'] = value + '%';
|
||||
this.styleUpdate.emit(this.pageStyles);
|
||||
});
|
||||
|
||||
this.settingsForm.addControl('bookReaderMargin', new FormControl(this.user.preferences.bookReaderMargin, []));
|
||||
this.settingsForm.get('bookReaderMargin')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe(value => {
|
||||
this.pageStyles['margin-left'] = value + '%';
|
||||
this.pageStyles['margin-right'] = value + '%';
|
||||
this.styleUpdate.emit(this.pageStyles);
|
||||
});
|
||||
|
||||
this.settingsForm.addControl('layoutMode', new FormControl(this.user.preferences.bookReaderLayoutMode || BookPageLayoutMode.Default, []));
|
||||
this.settingsForm.get('layoutMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((layoutMode: BookPageLayoutMode) => {
|
||||
console.log(layoutMode);
|
||||
this.layoutModeUpdate.emit(layoutMode);
|
||||
});
|
||||
|
||||
this.setTheme(this.user.preferences.bookReaderThemeName || this.themeService.defaultBookTheme);
|
||||
this.resetSettings();
|
||||
} else {
|
||||
this.resetSettings();
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
|
||||
resetSettings() {
|
||||
if (this.user) {
|
||||
this.setPageStyles(this.user.preferences.bookReaderFontFamily, this.user.preferences.bookReaderFontSize + '%', this.user.preferences.bookReaderMargin + '%', this.user.preferences.bookReaderLineSpacing + '%');
|
||||
} else {
|
||||
this.setPageStyles();
|
||||
}
|
||||
|
||||
this.settingsForm.get('bookReaderFontFamily')?.setValue(this.user.preferences.bookReaderFontFamily);
|
||||
this.styleUpdate.emit(this.pageStyles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to be used by resetSettings. Pass items in with quantifiers
|
||||
*/
|
||||
setPageStyles(fontFamily?: string, fontSize?: string, margin?: string, lineHeight?: string, colorTheme?: string) {
|
||||
const windowWidth = window.innerWidth
|
||||
|| this.document.documentElement.clientWidth
|
||||
|| this.document.body.clientWidth;
|
||||
|
||||
|
||||
let defaultMargin = '15%';
|
||||
if (windowWidth <= mobileBreakpointMarginOverride) {
|
||||
defaultMargin = '5%';
|
||||
}
|
||||
this.pageStyles = {
|
||||
'font-family': fontFamily || this.pageStyles['font-family'] || 'default',
|
||||
'font-size': fontSize || this.pageStyles['font-size'] || '100%',
|
||||
'margin-left': margin || this.pageStyles['margin-left'] || defaultMargin,
|
||||
'margin-right': margin || this.pageStyles['margin-right'] || defaultMargin,
|
||||
'line-height': lineHeight || this.pageStyles['line-height'] || '100%'
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
setTheme(themeName: string) {
|
||||
const theme = this.themes.find(t => t.name === themeName);
|
||||
this.activeTheme = theme;
|
||||
this.colorThemeUpdate.emit(theme);
|
||||
}
|
||||
|
||||
toggleReadingDirection() {
|
||||
if (this.readingDirectionModel === ReadingDirection.LeftToRight) {
|
||||
this.readingDirectionModel = ReadingDirection.RightToLeft;
|
||||
} else {
|
||||
this.readingDirectionModel = ReadingDirection.LeftToRight;
|
||||
}
|
||||
|
||||
this.readingDirection.emit(this.readingDirectionModel);
|
||||
}
|
||||
|
||||
toggleFullscreen() {
|
||||
this.fullscreen.emit();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<div class="table-of-contents">
|
||||
<h3>Table of Contents</h3>
|
||||
<div *ngIf="chapters.length === 0">
|
||||
<em>This book does not have Table of Contents set in the metadata or a toc file</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>
|
||||
</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>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
.table-of-contents li {
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.chapter-title {
|
||||
padding-inline-start: 0px
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import { AfterViewInit, Component, ElementRef, EventEmitter, Inject, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { BookChapterItem } from '../_models/book-chapter-item';
|
||||
|
||||
@Component({
|
||||
selector: 'app-table-of-contents',
|
||||
templateUrl: './table-of-contents.component.html',
|
||||
styleUrls: ['./table-of-contents.component.scss']
|
||||
})
|
||||
export class TableOfContentsComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() chapterId!: number;
|
||||
@Input() pageNum!: number;
|
||||
@Input() currentPageAnchor!: string;
|
||||
@Input() chapters:Array<BookChapterItem> = [];
|
||||
|
||||
@Output() loadChapter: EventEmitter<{pageNum: number, part: string}> = new EventEmitter();
|
||||
|
||||
|
||||
|
||||
private onDestroy: Subject<void> = new Subject();
|
||||
|
||||
|
||||
pageAnchors: {[n: string]: number } = {};
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
cleanIdSelector(id: string) {
|
||||
const tokens = id.split('/');
|
||||
if (tokens.length > 0) {
|
||||
return tokens[0];
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
loadChapterPage(pageNum: number, part: string) {
|
||||
this.loadChapter.emit({pageNum, part});
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue