Epub and OPDS Fixes (#4038)

Co-authored-by: Amelia <77553571+Fesaa@users.noreply.github.com>
This commit is contained in:
Joe Milazzo 2025-09-21 17:31:47 -05:00 committed by GitHub
parent 82d85363b0
commit 21448a86ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 4174 additions and 43 deletions

View File

@ -417,7 +417,8 @@ public class OpdsController : BaseApiController
// Ensure libraries follow SideNav order
var userSideNavStreams = await _unitOfWork.UserRepository.GetSideNavStreams(userId);
foreach (var library in userSideNavStreams.Where(s => s.StreamType == SideNavStreamType.Library).Select(sideNavStream => sideNavStream.Library))
foreach (var library in userSideNavStreams.Where(s => s.StreamType == SideNavStreamType.Library)
.Select(sideNavStream => sideNavStream.Library))
{
feed.Entries.Add(new FeedEntry()
{
@ -593,6 +594,8 @@ public class OpdsController : BaseApiController
var items = (await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId, GetUserParams(pageNumber))).ToList();
var totalItems = (await _unitOfWork.ReadingListRepository.GetReadingListItemDtosByIdAsync(readingListId, userId)).Count();
// Check if there is reading progress or not, if so, inject a "continue-reading" item
var firstReadReadingListItem = items.FirstOrDefault(i => i.PagesRead > 0);
@ -618,8 +621,9 @@ public class OpdsController : BaseApiController
CreateChapter(apiKey, $"{item.Order} - {item.SeriesName}: {item.Title}",
item.Summary ?? string.Empty, item.ChapterId, item.VolumeId, item.SeriesId, prefix, baseUrl));
}
}
AddPagination(feed, pageNumber, totalItems, UserParams.Default.PageSize, $"{prefix}{apiKey}/reading-list/{readingListId}/");
return CreateXmlResult(SerializeXml(feed));
}
@ -868,7 +872,6 @@ public class OpdsController : BaseApiController
}
feed.Total = feed.Entries.Count;
return CreateXmlResult(SerializeXml(feed));
}
@ -1127,6 +1130,45 @@ public class OpdsController : BaseApiController
feed.StartIndex = (Math.Max(list.CurrentPage - 1, 0) * list.PageSize) + 1;
}
private static void AddPagination(Feed feed, int currentPage, int totalItems, int pageSize, string href)
{
var url = href;
if (href.Contains('?'))
{
url += "&amp;";
}
else
{
url += "?";
}
var pageNumber = Math.Max(currentPage, 1);
var totalPages = totalItems / pageSize;
if (pageNumber > 1)
{
feed.Links.Add(CreateLink(FeedLinkRelation.Prev, FeedLinkType.AtomNavigation, url + "pageNumber=" + (pageNumber - 1)));
}
if (pageNumber + 1 <= totalPages)
{
feed.Links.Add(CreateLink(FeedLinkRelation.Next, FeedLinkType.AtomNavigation, url + "pageNumber=" + (pageNumber + 1)));
}
// Update self to point to current page
var selfLink = feed.Links.SingleOrDefault(l => l.Rel == FeedLinkRelation.Self);
if (selfLink != null)
{
selfLink.Href = url + "pageNumber=" + pageNumber;
}
feed.Total = totalItems;
feed.ItemsPerPage = pageSize;
feed.StartIndex = (Math.Max(currentPage - 1, 0) * pageSize) + 1;
}
private static FeedEntry CreateSeries(SeriesDto seriesDto, SeriesMetadataDto metadata, string apiKey, string prefix, string baseUrl)
{
return new FeedEntry()

View File

@ -111,6 +111,10 @@ public sealed record UserReadingProfileDto
[Required]
public bool BookReaderImmersiveMode { get; set; } = false;
/// <inheritdoc cref="AppUserReadingProfile.BookReaderEpubPageCalculationMethod"/>
[Required]
public EpubPageCalculationMethod BookReaderEpubPageCalculationMethod { get; set; } = EpubPageCalculationMethod.Default;
#endregion
#region PdfReader

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -732,6 +732,9 @@ namespace API.Data.Migrations
.HasColumnType("TEXT")
.HasDefaultValue("#000000");
b.Property<int>("BookReaderEpubPageCalculationMethod")
.HasColumnType("INTEGER");
b.Property<string>("BookReaderFontFamily")
.HasColumnType("TEXT");

View File

@ -139,6 +139,10 @@ public class AppUserReadingProfile
/// </summary>
/// <remarks>Defaults to false</remarks>
public bool BookReaderImmersiveMode { get; set; } = false;
/// <summary>
/// Book Reader Option: Different calculation modes for the page due to a bleed bug that devs cannot reproduce reliably or fix
/// </summary>
public EpubPageCalculationMethod BookReaderEpubPageCalculationMethod { get; set; } = EpubPageCalculationMethod.Default;
#endregion
#region PdfReader

View File

@ -0,0 +1,14 @@
using System.ComponentModel;
namespace API.Entities.Enums;
/// <summary>
/// Due to a bleeding text bug in the Epub reader with 1/2 column layout, multiple calculation modes are present
/// </summary>
public enum EpubPageCalculationMethod
{
[Description("Default")]
Default = 0,
[Description("Calculation 1")]
Calculation1 = 1,
}

View File

@ -445,6 +445,7 @@ public class ReadingProfileService(IUnitOfWork unitOfWork, ILocalizationService
existingProfile.BookThemeName = dto.BookReaderThemeName;
existingProfile.BookReaderLayoutMode = dto.BookReaderLayoutMode;
existingProfile.BookReaderImmersiveMode = dto.BookReaderImmersiveMode;
existingProfile.BookReaderEpubPageCalculationMethod = dto.BookReaderEpubPageCalculationMethod;
// PDF Reading
existingProfile.PdfTheme = dto.PdfTheme;

View File

@ -10,9 +10,8 @@ import {PdfTheme} from "./pdf-theme";
import {PdfScrollMode} from "./pdf-scroll-mode";
import {PdfLayoutMode} from "./pdf-layout-mode";
import {PdfSpreadMode} from "./pdf-spread-mode";
import {Series} from "../series";
import {Library} from "../library/library";
import {UserBreakpoint} from "../../shared/_services/utility.service";
import {EpubPageCalculationMethod} from "../readers/epub-page-calculation-method";
export enum ReadingProfileKind {
Default = 0,
@ -53,6 +52,7 @@ export interface ReadingProfile {
bookReaderThemeName: string;
bookReaderLayoutMode: BookPageLayoutMode;
bookReaderImmersiveMode: boolean;
bookReaderEpubPageCalculationMethod: EpubPageCalculationMethod;
// PDF Reader
pdfTheme: PdfTheme;

View File

@ -0,0 +1,6 @@
export enum EpubPageCalculationMethod {
Default = 0,
Calculation1 = 1
}
export const allCalcMethods = [EpubPageCalculationMethod.Default, EpubPageCalculationMethod.Calculation1];

View File

@ -0,0 +1,20 @@
import {Pipe, PipeTransform} from '@angular/core';
import {EpubPageCalculationMethod} from "../_models/readers/epub-page-calculation-method";
import {translate} from "@jsverse/transloco";
@Pipe({
name: 'epubPageCalcMethod'
})
export class EpubPageCalcMethodPipe implements PipeTransform {
transform(value: EpubPageCalculationMethod) {
switch (value) {
case EpubPageCalculationMethod.Default:
return translate('epub-page-calc-method-pipe.default');
case EpubPageCalculationMethod.Calculation1:
return translate('epub-page-calc-method-pipe.calc1');
}
}
}

View File

@ -15,13 +15,13 @@ import {translate} from "@jsverse/transloco";
import {ToastrService} from "ngx-toastr";
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {UserBreakpoint, UtilityService} from "../shared/_services/utility.service";
import {LayoutMeasurementService} from "./layout-measurement.service";
import {environment} from "../../environments/environment";
import {EpubFont} from "../_models/preferences/epub-font";
import {FontService} from "./font.service";
import {EpubPageCalculationMethod} from "../_models/readers/epub-page-calculation-method";
export interface ReaderSettingUpdate {
setting: 'pageStyle' | 'clickToPaginate' | 'fullscreen' | 'writingStyle' | 'layoutMode' | 'readingDirection' | 'immersiveMode' | 'theme';
setting: 'pageStyle' | 'clickToPaginate' | 'fullscreen' | 'writingStyle' | 'layoutMode' | 'readingDirection' | 'immersiveMode' | 'theme' | 'pageCalcMethod';
object: any;
}
@ -35,12 +35,10 @@ export type BookReadingProfileFormGroup = FormGroup<{
bookReaderWritingStyle: FormControl<WritingStyle>;
bookReaderThemeName: FormControl<string>;
bookReaderLayoutMode: FormControl<BookPageLayoutMode>;
bookReaderImmersiveMode:FormControl <boolean>;
bookReaderImmersiveMode: FormControl<boolean>;
bookReaderEpubPageCalculationMethod: FormControl<EpubPageCalculationMethod>;
}>
const COLUMN_GAP = 20; //px gap between columns
@Injectable()
export class EpubReaderSettingsService {
private readonly destroyRef = inject(DestroyRef);
@ -51,7 +49,6 @@ export class EpubReaderSettingsService {
private readonly toastr = inject(ToastrService);
private readonly document = inject(DOCUMENT);
private readonly fb = inject(NonNullableFormBuilder);
private readonly layoutMeasurements = inject(LayoutMeasurementService);
// Core signals - these will be the single source of truth
private readonly _currentReadingProfile = signal<ReadingProfile | null>(null);
@ -67,6 +64,7 @@ export class EpubReaderSettingsService {
private readonly _activeTheme = signal<BookTheme | undefined>(undefined);
private readonly _clickToPaginate = signal<boolean>(false);
private readonly _layoutMode = signal<BookPageLayoutMode>(BookPageLayoutMode.Default);
private readonly _pageCalcMode = signal<EpubPageCalculationMethod>(EpubPageCalculationMethod.Default);
private readonly _immersiveMode = signal<boolean>(false);
private readonly _isFullscreen = signal<boolean>(false);
@ -91,6 +89,7 @@ export class EpubReaderSettingsService {
public readonly immersiveMode = this._immersiveMode.asReadonly();
public readonly isFullscreen = this._isFullscreen.asReadonly();
public readonly epubFonts = this._epubFonts.asReadonly();
public readonly pageCalcMode = this._pageCalcMode.asReadonly();
// Computed signals for derived state
public readonly layoutMode = computed(() => {
@ -210,6 +209,18 @@ export class EpubReaderSettingsService {
});
}
});
effect(() => {
const pageCalcMethod = this._pageCalcMode();
if (!this.isInitialized) return;
if (pageCalcMethod) {
this.settingUpdateSubject.next({
setting: 'pageCalcMethod',
object: pageCalcMethod
});
}
});
}
@ -252,7 +263,7 @@ export class EpubReaderSettingsService {
*/
private setupDefaultsFromProfile(profile: ReadingProfile): void {
// Set defaults if undefined
if (profile.bookReaderFontFamily === undefined) {
if (profile.bookReaderFontFamily === undefined || profile.bookReaderFontFamily === 'default') {
profile.bookReaderFontFamily = FontService.DefaultEpubFont;
}
if (profile.bookReaderFontSize === undefined || profile.bookReaderFontSize < 50) {
@ -273,6 +284,9 @@ export class EpubReaderSettingsService {
if (profile.bookReaderLayoutMode === undefined) {
profile.bookReaderLayoutMode = BookPageLayoutMode.Default;
}
if (profile.bookReaderEpubPageCalculationMethod === undefined) {
profile.bookReaderEpubPageCalculationMethod = EpubPageCalculationMethod.Default;
}
// Update signals from profile
this._readingDirection.set(profile.bookReaderReadingDirection);
@ -280,6 +294,7 @@ export class EpubReaderSettingsService {
this._clickToPaginate.set(profile.bookReaderTapToPaginate);
this._layoutMode.set(profile.bookReaderLayoutMode);
this._immersiveMode.set(profile.bookReaderImmersiveMode);
this._pageCalcMode.set(profile.bookReaderEpubPageCalculationMethod);
// Set up page styles
this.setPageStyles(
@ -378,6 +393,11 @@ export class EpubReaderSettingsService {
this.settingsForm.get('bookReaderWritingStyle')?.setValue(value);
}
updatePageCalcMethod(value: EpubPageCalculationMethod) {
this._pageCalcMode.set(value);
this.settingsForm.get('bookReaderEpubPageCalculationMethod')?.setValue(value);
}
updateFullscreen(value: boolean) {
this._isFullscreen.set(value);
if (!this._isInitialized()) return;
@ -472,6 +492,7 @@ export class EpubReaderSettingsService {
bookReaderThemeName: this.fb.control(profile.bookReaderThemeName),
bookReaderLayoutMode: this.fb.control(this._layoutMode()),
bookReaderImmersiveMode: this.fb.control(this._immersiveMode()),
bookReaderEpubPageCalculationMethod: this.fb.control(this._pageCalcMode())
});
// Set up value change subscriptions
@ -586,6 +607,15 @@ export class EpubReaderSettingsService {
this.isUpdatingFromForm = false;
});
// Page Calc Method
this.settingsForm.get('bookReaderEpubPageCalculationMethod')?.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(value => {
this.isUpdatingFromForm = true;
this._pageCalcMode.set(value as EpubPageCalculationMethod);
this.isUpdatingFromForm = false;
});
// Update implicit profile on form changes (debounced) - ONLY source of profile updates
this.settingsForm.valueChanges.pipe(
debounceTime(500),
@ -648,6 +678,7 @@ export class EpubReaderSettingsService {
data.bookReaderImmersiveMode = this._immersiveMode();
data.bookReaderReadingDirection = this._readingDirection();
data.bookReaderWritingStyle = this._writingStyle();
data.bookReaderEpubPageCalculationMethod = this._pageCalcMode();
const activeTheme = this._activeTheme();
if (activeTheme) {

View File

@ -56,7 +56,7 @@
<ng-template #topActionBar>
@if (shouldShowMenu()) {
<div class="reader-header">
<div class="action-bar px-2">
<div class="action-bar top-action-bar px-2">
<div>
<button class="btn btn-secondary me-2" (click)="toggleDrawer()">
<i class="fa fa-bars" aria-hidden="true"></i>
@ -146,10 +146,10 @@
</span>
<div class="d-none d-sm-block">
<span class="me-1"></span>
<span class="mx-1"></span>
{{t('completion-label', {percent: (virtualizedPageNum() / virtualizedMaxPages()) | percent})}}
@if (readingTimeLeftResource.value(); as timeLeft) {
<span class="me-1"></span>
<span class="mx-1"></span>
<span class="time-left">
<i class="fa-solid fa-clock" aria-hidden="true"></i>
{{timeLeft! | readTimeLeft:true }}

View File

@ -79,7 +79,7 @@ $action-bar-height: 38px;
}
@media (min-width: 876px) {
.action-bar {
.top-action-bar {
grid-template-columns: 1fr auto 1fr;
}
@ -94,7 +94,7 @@ $action-bar-height: 38px;
/* Mobile - 2 columns */
@media (max-width: 875px) {
.action-bar {
.top-action-bar {
grid-template-columns: auto 1fr;
}

View File

@ -69,6 +69,7 @@ import {environment} from "../../../../environments/environment";
import {LoadPageEvent} from "../_drawers/view-bookmarks-drawer/view-bookmark-drawer.component";
import {FontService} from "../../../_services/font.service";
import afterFrame from "afterframe";
import {EpubPageCalculationMethod} from "../../../_models/readers/epub-page-calculation-method";
interface HistoryPoint {
@ -159,11 +160,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
private readonly colorscapeService = inject(ColorscapeService);
private readonly fontService = inject(FontService);
protected readonly BookPageLayoutMode = BookPageLayoutMode;
protected readonly WritingStyle = WritingStyle;
protected readonly ReadingDirection = ReadingDirection;
protected readonly PAGING_DIRECTION = PAGING_DIRECTION;
libraryId!: number;
seriesId!: number;
volumeId!: number;
@ -385,6 +381,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
protected readonly readingDirection = this.readerSettingsService.readingDirection;
protected readonly writingStyle = this.readerSettingsService.writingStyle;
protected readonly clickToPaginate = this.readerSettingsService.clickToPaginate;
protected readonly pageCalcMode = this.readerSettingsService.pageCalcMode;
protected columnWidth!: Signal<string>;
protected columnHeight!: Signal<string>;
@ -962,6 +959,9 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// Attempt to restore the reading position
this.snapScrollOnResize();
afterFrame(() => {
this.injectImageBookmarkIndicators(true);
});
}
/**
@ -1171,6 +1171,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
const promptConfig = {...this.confirmService.defaultPrompt};
promptConfig.header = translate('book-reader.go-to-page');
promptConfig.content = translate('book-reader.go-to-page-prompt', {totalPages: this.maxPages()});
promptConfig.bookReader = true;
const goToPageNum = await this.confirmService.prompt(undefined, promptConfig);
@ -1307,6 +1308,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
border-radius: 2px;
background: ${backgroundColor} !important;
color: ${textColor} !important;
font-family: var(--_fa-family) !important;
`;
@ -1681,9 +1683,12 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
*/
pageWidth = computed(() => {
this.windowWidth(); // Ensure re-compute when windows size changes (element clientWidth isn't a signal)
this.pageCalcMode();
console.log('page width recalulated')
const calculationMethod = this.pageCalcMode();
const marginLeft = this.pageStyles()['margin-left'];
const columnGapModifier = this.layoutMode() === BookPageLayoutMode.Default ? 0 : 1;
const columnGapModifier = this.columnGapModifier();
if (this.readingSectionElemRef == null) return 0;
const margin = (this.convertVwToPx(parseInt(marginLeft, 10)) * 2);
@ -1692,7 +1697,25 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// console.log('page size calc, margin: ', margin)
// console.log('page size calc, col gap: ', ((COLUMN_GAP / 2) * columnGapModifier));
// console.log("clientWidth", this.readingSectionElemRef.nativeElement.clientWidth, "window", window.innerWidth, "margin", margin, "left", marginLeft)
return this.readingSectionElemRef.nativeElement.clientWidth - margin + ((COLUMN_GAP) * columnGapModifier);
// console.log('clientWidth: ', this.readingSectionElemRef.nativeElement.clientWidth, 'offsetWidth:', this.readingSectionElemRef.nativeElement.offsetWidth, 'bbox:', this.readingSectionElemRef.nativeElement.getBoundingClientRect().width);
if (calculationMethod === EpubPageCalculationMethod.Default) {
return this.readingSectionElemRef.nativeElement.clientWidth - margin + (((COLUMN_GAP) * columnGapModifier));
} else {
return this.readingSectionElemRef.nativeElement.clientWidth - margin + (((COLUMN_GAP) * columnGapModifier) + 10);
}
});
columnGapModifier = computed(() => {
const calculationMethod = this.pageCalcMode();
switch(this.layoutMode()) {
case BookPageLayoutMode.Default:
return 0;
case BookPageLayoutMode.Column1:
return 1;
case BookPageLayoutMode.Column2:
return calculationMethod === EpubPageCalculationMethod.Default ? 1 : 1.25;
}
});
pageHeight = computed(() => {
@ -2436,4 +2459,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
protected readonly Breakpoint = Breakpoint;
protected readonly environment = environment;
protected readonly BookPageLayoutMode = BookPageLayoutMode;
protected readonly WritingStyle = WritingStyle;
protected readonly ReadingDirection = ReadingDirection;
protected readonly PAGING_DIRECTION = PAGING_DIRECTION;
}

View File

@ -119,7 +119,7 @@
<ng-template #fullscreenTooltip>{{t('fullscreen-tooltip')}}</ng-template>
<span class="visually-hidden" id="fullscreen-help">
<ng-container [ngTemplateOutlet]="fullscreenTooltip" />
</span>
</span>
<button (click)="toggleFullscreen()" class="btn btn-icon" aria-labelledby="fullscreen">
<i class="fa {{isFullscreen() ? 'fa-compress-alt' : 'fa-expand-alt'}} {{isFullscreen() ? 'icon-primary-color' : ''}}" aria-hidden="true"></i>
@if (activeTheme()?.isDarkTheme) {
@ -133,7 +133,7 @@
<ng-template #layoutTooltip><span [innerHTML]="t('layout-mode-tooltip')"></span></ng-template>
<span class="visually-hidden" id="layout-help">
<ng-container [ngTemplateOutlet]="layoutTooltip" />
</span>
</span>
<br>
<div class="btn-group d-flex justify-content-center" role="group" [attr.aria-label]="t('layout-mode-label')">
<input type="radio" formControlName="bookReaderLayoutMode" [value]="BookPageLayoutMode.Default" class="btn-check" id="layout-mode-default" autocomplete="off">
@ -149,6 +149,25 @@
</div>
</div>
<div class="controls mt-2" style="display:flex; justify-content:space-between; align-items:center;">
<label id="page-calc-method" class="form-label">{{t('page-calc-method-label')}}
<i class="fa fa-info-circle ms-1" aria-hidden="true" placement="top"
[ngbTooltip]="pageCalcMethodTooltip" role="button" tabindex="1" aria-describedby="fullscreen-help"></i>
</label>
<ng-template #pageCalcMethodTooltip>{{t('page-calc-method-tooltip')}}</ng-template>
<span class="visually-hidden" id="page-calc-method-help">
<ng-container [ngTemplateOutlet]="pageCalcMethodTooltip" />
</span>
<br>
<div>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderEpubPageCalculationMethod">
@for (opt of calcMethods; track opt) {
<option [ngValue]="opt">{{opt | epubPageCalcMethod}}</option>
}
</select>
</div>
</div>
</ng-template>
</div>

View File

@ -22,9 +22,9 @@ import {
import {TranslocoDirective} from "@jsverse/transloco";
import {ReadingProfile, ReadingProfileKind} from "../../../_models/preferences/reading-profiles";
import {BookReadingProfileFormGroup, EpubReaderSettingsService} from "../../../_services/epub-reader-settings.service";
import {LayoutMode} from "../../../manga-reader/_models/layout-mode";
import {FontService} from "../../../_services/font.service";
import {EpubFont} from "../../../_models/preferences/epub-font";
import {EpubPageCalcMethodPipe} from "../../../_pipes/epub-page-calc-method.pipe";
import {allCalcMethods, EpubPageCalculationMethod} from "../../../_models/readers/epub-page-calculation-method";
/**
* Used for book reader. Do not use for other components
@ -85,9 +85,9 @@ export const bookColorThemes = [
templateUrl: './reader-settings.component.html',
styleUrls: ['./reader-settings.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ReactiveFormsModule, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, NgbAccordionButton,
NgbAccordionCollapse, NgbAccordionBody, NgbTooltip, NgTemplateOutlet, NgClass, NgStyle,
TitleCasePipe, TranslocoDirective]
imports: [ReactiveFormsModule, NgbAccordionDirective, NgbAccordionItem, NgbAccordionHeader, NgbAccordionButton,
NgbAccordionCollapse, NgbAccordionBody, NgbTooltip, NgTemplateOutlet, NgClass, NgStyle,
TitleCasePipe, TranslocoDirective, EpubPageCalcMethodPipe]
})
export class ReaderSettingsComponent implements OnInit {
@ -116,9 +116,7 @@ export class ReaderSettingsComponent implements OnInit {
protected parentReadingProfile!: Signal<ReadingProfile | null>;
protected currentReadingProfile!: Signal<ReadingProfile | null>;
protected epubFonts!: Signal<EpubFont[]>;
protected isVerticalLayout!: Signal<boolean>;
protected pageCalcMode!: Signal<EpubPageCalculationMethod>;
async ngOnInit() {
@ -135,6 +133,7 @@ export class ReaderSettingsComponent implements OnInit {
this.parentReadingProfile = this.readerSettingsService.parentReadingProfile;
this.currentReadingProfile = this.readerSettingsService.currentReadingProfile;
this.epubFonts = this.readerSettingsService.epubFonts;
this.pageCalcMode = this.readerSettingsService.pageCalcMode;
this.themes = this.readerSettingsService.getThemes();
@ -166,7 +165,6 @@ export class ReaderSettingsComponent implements OnInit {
toggleFullscreen() {
this.readerSettingsService.toggleFullscreen();
this.cdRef.markForCheck();
}
// menu only code
@ -183,4 +181,5 @@ export class ReaderSettingsComponent implements OnInit {
protected readonly WritingStyle = WritingStyle;
protected readonly ReadingDirection = ReadingDirection;
protected readonly BookPageLayoutMode = BookPageLayoutMode;
protected readonly calcMethods = allCalcMethods;
}

View File

@ -9,4 +9,8 @@ export class ConfirmConfig {
* If the close button shouldn't be rendered
*/
disableEscape: boolean = false;
/**
* Enables book theme css classes to style the popup properly
*/
bookReader?: boolean = false;
}

View File

@ -1,23 +1,25 @@
<ng-container *transloco="let t">
<div class="modal-header">
<div class="modal-header" [class.book-reader]="config.bookReader">
<h4 class="modal-title" id="modal-basic-title">{{config.header | confirmTranslate}}</h4>
@if (!config.disableEscape) {
<button type="button" class="btn-close" [attr.aria-label]="t('common.close')" (click)="close()"></button>
<button type="button" class="btn-unstyled ms-auto" [attr.aria-label]="t('close')" (click)="close()">
<i class="fas fa-times"></i>
</button>
}
</div>
@if (config._type === 'prompt') {
<div class="modal-body" style="overflow-x: auto">
<div class="modal-body" style="overflow-x: auto" [class.book-reader]="config.bookReader">
<form [formGroup]="formGroup">
<div [innerHtml]="(config.content | confirmTranslate)! | safeHtml"></div>
<input type="text" class="form-control" aria-labelledby="modal-basic-title" formControlName="prompt" />
</form>
</div>
} @else {
<div class="modal-body" style="overflow-x: auto" [innerHtml]="(config.content | confirmTranslate)! | safeHtml"></div>
<div class="modal-body" [class.book-reader]="config.bookReader" style="overflow-x: auto" [innerHtml]="(config.content | confirmTranslate)! | safeHtml"></div>
}
<div class="modal-footer">
<div class="modal-footer" [class.book-reader]="config.bookReader">
@for(btn of config.buttons; track btn) {
<div>
<button type="button" class="btn btn-{{btn.type}}" (click)="clickButton(btn)">{{btn.text | confirmTranslate}}</button>

View File

@ -0,0 +1,5 @@
.book-reader {
color: var(--drawer-text-color);
background-color: var(--drawer-bg-color);
}

View File

@ -308,6 +308,22 @@
</app-setting-switch>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('page-calc-method-label')" [subtitle]="t('page-calc-method-tooltip')">
<ng-template #view>
{{readingProfileForm.get('bookReaderEpubPageCalculationMethod')!.value | epubPageCalcMethod}}
</ng-template>
<ng-template #edit>
<select class="form-select" aria-describedby="book-reader-heading"
formControlName="bookReaderEpubPageCalculationMethod">
@for (opt of calcMethods; track opt) {
<option [ngValue]="opt">{{opt | epubPageCalcMethod}}</option>
}
</select>
</ng-template>
</app-setting-item>
</div>
<div class="row g-0 mt-4 mb-4">
<app-setting-item [title]="t('reading-direction-label')" [subtitle]="t('reading-direction-tooltip')">
<ng-template #view>

View File

@ -32,7 +32,6 @@ import {AccountService} from "../../_services/account.service";
import {debounceTime, distinctUntilChanged, tap} from "rxjs/operators";
import {SentenceCasePipe} from "../../_pipes/sentence-case.pipe";
import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms";
import {BookService} from "../../book-reader/_services/book.service";
import {BookPageLayoutMode} from "../../_models/readers/book-page-layout-mode";
import {PdfTheme} from "../../_models/preferences/pdf-theme";
import {PdfScrollMode} from "../../_models/preferences/pdf-scroll-mode";
@ -65,6 +64,8 @@ import {ColorscapeService} from "../../_services/colorscape.service";
import {Color} from "@iplab/ngx-color-picker";
import {FontService} from "../../_services/font.service";
import {EpubFont} from "../../_models/preferences/epub-font";
import {EpubPageCalcMethodPipe} from "../../_pipes/epub-page-calc-method.pipe";
import {allCalcMethods} from "../../_models/readers/epub-page-calculation-method";
enum TabId {
ImageReader = "image-reader",
@ -104,6 +105,7 @@ enum TabId {
NgbTooltip,
BreakpointPipe,
SettingColorPickerComponent,
EpubPageCalcMethodPipe,
],
templateUrl: './manage-reading-profiles.component.html',
styleUrl: './manage-reading-profiles.component.scss',
@ -115,7 +117,6 @@ export class ManageReadingProfilesComponent implements OnInit {
protected readonly colorscapeService = inject(ColorscapeService);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly accountService = inject(AccountService);
private readonly bookService = inject(BookService);
private readonly destroyRef = inject(DestroyRef);
private readonly toastr = inject(ToastrService);
private readonly confirmService = inject(ConfirmService);
@ -231,6 +232,7 @@ export class ManageReadingProfilesComponent implements OnInit {
this.readingProfileForm.addControl('bookReaderLayoutMode', new FormControl(this.selectedProfile.bookReaderLayoutMode || BookPageLayoutMode.Default, []));
this.readingProfileForm.addControl('bookReaderThemeName', new FormControl(this.selectedProfile.bookReaderThemeName || bookColorThemes[0].name, []));
this.readingProfileForm.addControl('bookReaderImmersiveMode', new FormControl(this.selectedProfile.bookReaderImmersiveMode, []));
this.readingProfileForm.addControl('bookReaderEpubPageCalculationMethod', new FormControl(this.selectedProfile.bookReaderEpubPageCalculationMethod, []));
// Pdf reader
this.readingProfileForm.addControl('pdfTheme', new FormControl(this.selectedProfile.pdfTheme || PdfTheme.Dark, []));
@ -340,6 +342,7 @@ export class ManageReadingProfilesComponent implements OnInit {
}
protected readonly readingDirections = readingDirections;
protected readonly calcMethods = allCalcMethods;
protected readonly pdfSpreadModes = pdfSpreadModes;
protected readonly pageSplitOptions = pageSplitOptions;
protected readonly bookLayoutModes = bookLayoutModes;

View File

@ -1383,6 +1383,8 @@
"layout-mode-option-1col": "1 Column",
"layout-mode-option-2col": "2 Column",
"color-theme-title": "Color Theme",
"page-calc-method-label": "Page Calculation",
"page-calc-method-tooltip": "If you are experiencing text bleeding on column mode, try an alternative method of calculation",
"line-spacing-min-label": "1x",
"line-spacing-max-label": "2.5x",
@ -3023,6 +3025,11 @@
"hours-left": "Hours left"
},
"epub-page-calc-method-pipe": {
"default": "Default",
"calc1": "Calc 1"
},
"metadata-setting-field-pipe": {
"covers": "Covers",
"age-rating": "{{metadata-fields.age-rating-title}}",
@ -3234,6 +3241,9 @@
"pdf-theme-label": "Theme",
"pdf-theme-tooltip": "Color theme of the reader",
"page-calc-method-label": "{{reader-settings.page-calc-method-label}}",
"page-calc-method-tooltip": "{{reader-settings.page-calc-method-tooltip}}",
"reading-profile-series-settings-title": "Series",
"reading-profile-library-settings-title": "Library",
"delete": "{{common.delete}}"