More Epub Fixes (#4017)

Co-authored-by: Amelia <77553571+Fesaa@users.noreply.github.com>
This commit is contained in:
Joe Milazzo 2025-09-05 09:45:16 -05:00 committed by GitHub
parent b6c5987185
commit 8def92ff73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
172 changed files with 2204 additions and 1520 deletions

View File

@ -30,7 +30,6 @@ public abstract class AbstractDbTest(ITestOutputHelper testOutputHelper): Abstra
.EnableSensitiveDataLogging()
.Options;
var connection = RelationalOptionsExtension.Extract(contextOptions).Connection;
var context = new DataContext(contextOptions);
await context.Database.EnsureCreatedAsync();
@ -59,9 +58,7 @@ public abstract class AbstractDbTest(ITestOutputHelper testOutputHelper): Abstra
{
try
{
await context.Database.EnsureCreatedAsync();
var filesystem = CreateFileSystem();
await Seed.SeedSettings(context, new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), filesystem));
var setting = await context.ServerSetting.Where(s => s.Key == ServerSettingKey.CacheDirectory).SingleAsync();

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using API.Data;
using API.Entities;
using API.Entities.Enums;
using API.Entities.Metadata;
using API.Helpers;
using API.Helpers.Builders;
using API.Services;
@ -158,6 +159,58 @@ public class SeriesRepositoryTests
}
}
[Theory]
[InlineData(12345, null, 12345)] // Case 1: Prioritize existing ExternalSeries id
[InlineData(0, "https://anilist.co/manga/100664/Ijiranaide-Nagatorosan/", 100664)] // Case 2: Extract from weblink if no external series id
[InlineData(0, "", null)] // Case 3: Return null if neither exist
public async Task GetPlusSeriesDto_Should_PrioritizeAniListId_Correctly(int externalAniListId, string? webLinks, int? expectedAniListId)
{
// Arrange
await ResetDb();
var series = new SeriesBuilder("Test Series")
.WithFormat(MangaFormat.Archive)
.Build();
var library = new LibraryBuilder("Test Library", LibraryType.Manga)
.WithFolderPath(new FolderPathBuilder("C:/data/manga/").Build())
.WithSeries(series)
.Build();
// Set up ExternalSeriesMetadata
series.ExternalSeriesMetadata = new ExternalSeriesMetadata()
{
AniListId = externalAniListId,
CbrId = 0,
MalId = 0,
GoogleBooksId = string.Empty
};
// Set up SeriesMetadata with WebLinks
series.Metadata = new SeriesMetadata()
{
WebLinks = webLinks,
ReleaseYear = 2021
};
_unitOfWork.LibraryRepository.Add(library);
_unitOfWork.SeriesRepository.Add(series);
await _unitOfWork.CommitAsync();
// Act
var result = await _unitOfWork.SeriesRepository.GetPlusSeriesDto(series.Id);
// Assert
Assert.NotNull(result);
Assert.Equal(expectedAniListId, result.AniListId);
Assert.Equal("Test Series", result.SeriesName);
}
// TODO: GetSeriesDtoForLibraryIdV2Async Tests (On Deck)
}

View File

@ -4,7 +4,7 @@
/// <summary>
/// Represents information about a potential Series for Kavita+
/// </summary>
public sealed record PlusSeriesRequestDto
public class PlusSeriesRequestDto
{
public int? AniListId { get; set; }
public long? MalId { get; set; }

View File

@ -752,7 +752,10 @@ public class SeriesRepository : ISeriesRepository
public async Task<PlusSeriesRequestDto?> GetPlusSeriesDto(int seriesId)
{
return await _context.Series
// I need to check Weblinks when AniListId/MalId is already set in ExternalSeries
// Updating stale data should prioritize ExernalSeriesMetada before Weblinks, to priorize prior matches
var result = await _context.Series
.Where(s => s.Id == seriesId)
.Include(s => s.ExternalSeriesMetadata)
.Select(series => new PlusSeriesRequestDto()
@ -760,13 +763,16 @@ public class SeriesRepository : ISeriesRepository
MediaFormat = series.Library.Type.ConvertToPlusMediaFormat(series.Format),
SeriesName = series.Name,
AltSeriesName = series.LocalizedName,
AniListId = ScrobblingService.ExtractId<int?>(series.Metadata.WebLinks,
ScrobblingService.AniListWeblinkWebsite),
MalId = ScrobblingService.ExtractId<long?>(series.Metadata.WebLinks,
ScrobblingService.MalWeblinkWebsite),
AniListId = series.ExternalSeriesMetadata.AniListId != 0
? series.ExternalSeriesMetadata.AniListId
: ScrobblingService.ExtractId<int?>(series.Metadata.WebLinks, ScrobblingService.AniListWeblinkWebsite),
MalId = series.ExternalSeriesMetadata.MalId != 0
? series.ExternalSeriesMetadata.MalId
: ScrobblingService.ExtractId<long?>(series.Metadata.WebLinks, ScrobblingService.MalWeblinkWebsite),
CbrId = series.ExternalSeriesMetadata.CbrId,
GoogleBooksId = ScrobblingService.ExtractId<string?>(series.Metadata.WebLinks,
ScrobblingService.GoogleBooksWeblinkWebsite),
GoogleBooksId = !string.IsNullOrEmpty(series.ExternalSeriesMetadata.GoogleBooksId)
? series.ExternalSeriesMetadata.GoogleBooksId
: ScrobblingService.ExtractId<string?>(series.Metadata.WebLinks, ScrobblingService.GoogleBooksWeblinkWebsite),
MangaDexId = ScrobblingService.ExtractId<string?>(series.Metadata.WebLinks,
ScrobblingService.MangaDexWeblinkWebsite),
VolumeCount = series.Volumes.Count,
@ -774,6 +780,8 @@ public class SeriesRepository : ISeriesRepository
Year = series.Metadata.ReleaseYear
})
.FirstOrDefaultAsync();
return result;
}
public async Task<int> GetCountAsync()

View File

@ -121,8 +121,17 @@ public class ExternalMetadataService : IExternalMetadataService
{
// Find all Series that are eligible and limit
var ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesThatNeedExternalMetadata(25);
if (ids.Count == 0) return;
ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesThatNeedExternalMetadata(25, true);
if (ids.Count == 0)
{
ids = await _unitOfWork.ExternalSeriesMetadataRepository.GetSeriesThatNeedExternalMetadata(25, true);
}
if (ids.Count == 0)
{
_logger.LogInformation("[Kavita+ Data Refresh] No series need matching or refreshing (stale data)");
return;
}
_logger.LogInformation("[Kavita+ Data Refresh] Started Refreshing {Count} series data from Kavita+: {Ids}", ids.Count, string.Join(',', ids));
var count = 0;
@ -137,7 +146,7 @@ public class ExternalMetadataService : IExternalMetadataService
count++;
successfulMatches.Add(seriesId);
}
await Task.Delay(6000); // Currently AL is degraded and has 30 requests/min, give a little padding since this is a background request
await Task.Delay(10000); // Currently AL is degraded and has 30 requests/min, give a little padding since this is a background request
}
_logger.LogInformation("[Kavita+ Data Refresh] Finished Refreshing {Count} / {Total} series data from Kavita+: {Ids}", count, ids.Count, string.Join(',', successfulMatches));
}

2047
UI/Web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,17 +18,17 @@
"private": true,
"dependencies": {
"@angular-slider/ngx-slider": "^20.0.0",
"@angular/animations": "^20.1.4",
"@angular/cdk": "^20.1.4",
"@angular/common": "^20.1.4",
"@angular/compiler": "^20.1.4",
"@angular/core": "^20.1.4",
"@angular/forms": "^20.1.4",
"@angular/localize": "^20.1.4",
"@angular/platform-browser": "^20.1.4",
"@angular/platform-browser-dynamic": "^20.1.4",
"@angular/router": "^20.1.4",
"@fortawesome/fontawesome-free": "^7.0.0",
"@angular/animations": "^20.2.3",
"@angular/cdk": "^20.2.1",
"@angular/common": "^20.2.3",
"@angular/compiler": "^20.2.3",
"@angular/core": "^20.2.3",
"@angular/forms": "^20.2.3",
"@angular/localize": "^20.2.3",
"@angular/platform-browser": "^20.2.3",
"@angular/platform-browser-dynamic": "^20.2.3",
"@angular/router": "^20.2.3",
"@fortawesome/fontawesome-free": "^7.0.1",
"@iharbeck/ngx-virtual-scroller": "^19.0.1",
"@iplab/ngx-color-picker": "^20.0.0",
"@iplab/ngx-file-upload": "^20.0.0",
@ -43,6 +43,7 @@
"@siemens/ngx-datatable": "^22.4.1",
"@swimlane/ngx-charts": "^23.0.0-alpha.0",
"@tweenjs/tween.js": "^25.0.0",
"afterframe": "^1.0.2",
"bootstrap": "^5.3.2",
"charts.css": "^1.2.0",
"file-saver": "^2.0.5",
@ -68,16 +69,16 @@
"@angular-eslint/eslint-plugin-template": "^20.1.1",
"@angular-eslint/schematics": "^20.1.1",
"@angular-eslint/template-parser": "^20.1.1",
"@angular/build": "^20.1.4",
"@angular/cli": "^20.1.4",
"@angular/compiler-cli": "^20.1.4",
"@angular/build": "^20.2.1",
"@angular/cli": "^20.2.1",
"@angular/compiler-cli": "^20.2.3",
"@types/d3": "^7.4.3",
"@types/file-saver": "^2.0.7",
"@types/luxon": "^3.6.2",
"@types/marked": "^5.0.2",
"@types/node": "^24.0.14",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.38.0",
"@typescript-eslint/parser": "^8.42.0",
"eslint": "^9.31.0",
"jsonminify": "^0.4.2",
"karma-coverage": "~2.2.0",

View File

@ -1,4 +1,4 @@
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {HttpClient} from '@angular/common/http';
import {DestroyRef, inject, Injectable} from '@angular/core';
import {Observable, of, ReplaySubject, shareReplay} from 'rxjs';
import {filter, map, switchMap, tap} from 'rxjs/operators';
@ -48,6 +48,10 @@ export class AccountService {
private readonly destroyRef = inject(DestroyRef);
private readonly licenseService = inject(LicenseService);
private readonly localizationService = inject(LocalizationService);
private readonly httpClient = inject(HttpClient);
private readonly router = inject(Router);
private readonly messageHub = inject(MessageHubService);
private readonly themeService = inject(ThemeService);
baseUrl = environment.apiUrl;
userKey = 'kavita-user';
@ -72,9 +76,8 @@ export class AccountService {
private isOnline: boolean = true;
constructor(private httpClient: HttpClient, private router: Router,
private messageHub: MessageHubService, private themeService: ThemeService) {
messageHub.messages$.pipe(filter(evt => evt.event === EVENTS.UserUpdate),
constructor() {
this.messageHub.messages$.pipe(filter(evt => evt.event === EVENTS.UserUpdate),
map(evt => evt.payload as UserUpdateEvent),
filter(userUpdateEvent => userUpdateEvent.userName === this.currentUser?.username),
switchMap(() => this.refreshAccount()))

View File

@ -1,8 +1,5 @@
import {inject, Injectable, signal} from '@angular/core';
import {NgbOffcanvas} from "@ng-bootstrap/ng-bootstrap";
import {
ViewAnnotationsDrawerComponent
} from "../book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component";
import {
LoadPageEvent,
ViewBookmarkDrawerComponent
@ -50,11 +47,16 @@ export class EpubReaderMenuService {
}
openViewAnnotationsDrawer(loadAnnotationCallback: (annotation: Annotation) => void) {
async openViewAnnotationsDrawer(loadAnnotationCallback: (annotation: Annotation) => void) {
if (this.offcanvasService.hasOpenOffcanvas()) {
this.offcanvasService.dismiss();
}
// This component needs to be imported dynamically as something breaks within Angular if it's not.
// I do not know what, but this fixes the drawer from not showing up in a production build.
const module = await import('../book-reader/_components/_drawers/view-annotations-drawer/view-annotations-drawer.component');
const ViewAnnotationsDrawerComponent = module.ViewAnnotationsDrawerComponent;
const ref = this.offcanvasService.open(ViewAnnotationsDrawerComponent, {position: 'end'});
ref.componentInstance.loadAnnotation.subscribe((annotation: Annotation) => {
loadAnnotationCallback(annotation);

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'actionable'">
<ng-container *transloco="let t; prefix: 'actionable'">
<div class="modal-container">
<div class="modal-header">
<h4 class="modal-title">

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'actionable'">
<ng-container *transloco="let t; prefix: 'actionable'">
@if (actions.length > 0) {
@if ((utilityService.activeBreakpoint$ | async)! <= Breakpoint.Tablet) {
<button [disabled]="disabled" class="btn {{btnClass}} px-3" id="actions-{{labelBy}}"

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'series-detail'">
<ng-container *transloco="let t; prefix: 'series-detail'">
@if(mobileSeriesImgBackground === 'true') {
<app-image [styles]="{'background': 'none'}" [imageUrl]="coverImage"></app-image>
} @else {

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'details-tab'">
<ng-container *transloco="let t; prefix: 'details-tab'">
<div class="details pb-3">
@if (readingTime) {

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'edit-chapter-modal'">
<ng-container *transloco="let t; prefix: 'edit-chapter-modal'">
<div class="modal-container">
<div class="modal-header">
<h4 class="modal-title">{{t('title')}} <app-entity-title [libraryType]="libraryType" [entity]="chapter" [prioritizeTitleName]="false"></app-entity-title></h4>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'edit-volume-modal'">
<ng-container *transloco="let t; prefix: 'edit-volume-modal'">
<div class="modal-container">
<div class="modal-header">
<h4 class="modal-title">{{t('title')}} <app-entity-title [libraryType]="libraryType" [entity]="volume" [prioritizeTitleName]="false"></app-entity-title></h4>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read:'match-series-modal'">
<ng-container *transloco="let t; prefix:'match-series-modal'">
<div>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read:'match-series-result-item'">
<ng-container *transloco="let t; prefix:'match-series-result-item'">
<div class="match-item-container p-3 mt-3 {{isDarkMode ? 'dark' : 'light'}}">
<div class="d-flex clickable match-item" (click)="selectItem()">
<div class="me-1">

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'related-tab'">
<ng-container *transloco="let t; prefix: 'related-tab'">
<div class="pb-2">
@if (relations.length > 0) {
<app-carousel-reel [items]="relations" [title]="t('relations-title')">

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read:'review-card-modal'">
<ng-container *transloco="let t; prefix:'review-card-modal'">
<div>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read:'review-card'">
<ng-container *transloco="let t; prefix:'review-card'">
<div class="card review-card clickable mb-3" (click)="showModal()">
<div class="row g-0">
<div class="col-md-2 col-sm-2 col-2 d-block p-2">

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read:'review-modal'">
<ng-container *transloco="let t; prefix:'review-modal'">
<div>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>

View File

@ -1,4 +1,4 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, OnInit} from '@angular/core';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input} from '@angular/core';
import {CarouselReelComponent} from "../../carousel/_components/carousel-reel/carousel-reel.component";
import {ReviewCardComponent} from "../review-card/review-card.component";
import {TranslocoDirective} from "@jsverse/transloco";
@ -6,13 +6,13 @@ import {UserReview} from "../review-card/user-review";
import {User} from "../../_models/user";
import {AccountService} from "../../_services/account.service";
import {
ReviewModalComponent, ReviewModalCloseAction,
ReviewModalCloseEvent
ReviewModalCloseAction,
ReviewModalCloseEvent,
ReviewModalComponent
} from "../review-modal/review-modal.component";
import {DefaultModalOptions} from "../../_models/default-modal-options";
import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
import {Series} from "../../_models/series";
import {Volume} from "../../_models/volume";
import {Chapter} from "../../_models/chapter";
@Component({
@ -28,19 +28,19 @@ import {Chapter} from "../../_models/chapter";
})
export class ReviewsComponent {
private readonly accountService = inject(AccountService);
private readonly modalService = inject(NgbModal);
private readonly cdRef = inject(ChangeDetectorRef);
@Input({required: true}) userReviews!: Array<UserReview>;
@Input({required: true}) plusReviews!: Array<UserReview>;
@Input({required: true}) series!: Series;
@Input() volumeId: number | undefined;
@Input() chapter: Chapter | undefined;
user: User | undefined;
constructor(
private accountService: AccountService,
private modalService: NgbModal,
private cdRef: ChangeDetectorRef) {
user: User | undefined = undefined;
constructor() {
this.accountService.currentUser$.subscribe(user => {
if (user) {
this.user = user;

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read:'spoiler'">
<ng-container *transloco="let t; prefix:'spoiler'">
<div (click)="toggle()" [attr.aria-expanded]="!isCollapsed" class="btn spoiler" tabindex="0">
@if (isCollapsed) {
<span>{{t('click-to-show')}}</span>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read:'user-scrobble-history'">
<ng-container *transloco="let t; prefix:'user-scrobble-history'">
@let currentUser = accountService.currentUser$ | async;

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read:'copy-settings-from-library-modal'">
<ng-container *transloco="let t; prefix:'copy-settings-from-library-modal'">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read:'directory-picker'">
<ng-container *transloco="let t; prefix:'directory-picker'">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read:'library-access-modal'">
<ng-container *transloco="let t; prefix:'library-access-modal'">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read:'reset-password-modal'">
<ng-container *transloco="let t; prefix:'reset-password-modal'">
<form [formGroup]="resetPasswordForm">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{t('title', {username: member.username | sentenceCase})}}</h4>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read:'email-history'">
<ng-container *transloco="let t; prefix:'email-history'">
<p>{{t('description')}}</p>
<ngx-datatable

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'invite-user'">
<ng-container *transloco="let t; prefix: 'invite-user'">
<div class="modal-container">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'library-selector'">
<ng-container *transloco="let t; prefix: 'library-selector'">
<div class="d-flex justify-content-between">
<div class="col-auto">

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'license'">
<ng-container *transloco="let t; prefix: 'license'">
<div class="position-relative">
<a class="position-absolute custom-position btn btn-outline-primary" [href]="WikiLink.KavitaPlusFAQ" target="_blank" rel="noreferrer nofollow">{{t('faq-title')}}</a>
@ -7,14 +7,16 @@
<div>
<p>{{t('kavita+-desc-part-1')}} <a [href]="WikiLink.KavitaPlus" target="_blank" rel="noreferrer nofollow">{{t('kavita+-desc-part-2')}}</a> {{t('kavita+-desc-part-3')}}</p>
<p>{{t('kavita+-warning')}}</p>
<form [formGroup]="formGroup">
<div class="mt-2">
<app-setting-item [title]="t('title')" (editMode)="updateEditMode($event)" [isEditMode]="!isViewMode" [showEdit]="hasLicense" [fixedExtras]="true">
<app-setting-item [title]="t('title')" (editMode)="updateEditMode($event)" [isEditMode]="!isViewMode()" [showEdit]="hasLicense()" [fixedExtras]="true">
<ng-template #titleExtra>
<button class="btn btn-icon btn-sm" (click)="loadLicenseInfo(true)">
@if (isChecking) {
<app-loading [loading]="isChecking" size="spinner-border-sm"></app-loading>
} @else if (hasLicense) {
@if (isChecking()) {
<app-loading [loading]="isChecking()" size="spinner-border-sm"></app-loading>
} @else if (hasLicense()) {
<span>
<i class="fa-solid fa-refresh" tabindex="0" [ngbTooltip]="t('check')"></i>
</span>
@ -22,15 +24,15 @@
</button>
</ng-template>
<ng-template #view>
@if (hasLicense) {
@if (hasLicense()) {
<span class="me-1">*********</span>
@if (isChecking) {
@if (isChecking()) {
<div class="spinner-border spinner-border-sm text-primary" role="status">
<span class="visually-hidden">{{t('loading')}}</span>
</div>
} @else {
@if (licenseInfo?.isActive) {
@if (licenseInfo()?.isActive) {
<i [ngbTooltip]="t('license-valid')" class="fa-solid fa-check-circle successful-validation ms-1">
<span class="visually-hidden">{{t('license-valid')}}</span>
</i>
@ -40,7 +42,7 @@
</i>
}
}
@if (!isChecking && hasLicense && !licenseInfo) {
@if (!isChecking() && hasLicense() && !licenseInfo) {
<div><span class="error">{{t('license-mismatch')}}</span></div>
}
@ -86,29 +88,29 @@
</button>
<button type="submit" class="flex-fill btn btn-primary" aria-describedby="license-key-header"
[disabled]="!formGroup.get('email')?.value || !formGroup.get('licenseKey')?.value" (click)="saveForm()">
@if (!isSaving) {
@if (!isSaving()) {
<span>{{t('activate-save')}}</span>
}
<app-loading [loading]="isSaving" size="spinner-border-sm"></app-loading>
<app-loading [loading]="isSaving()" size="spinner-border-sm"></app-loading>
</button>
</div>
</ng-template>
<ng-template #titleActions>
@if (hasLicense) {
@if (licenseInfo?.isActive) {
<a class="btn btn-outline-primary btn-sm me-1" [href]="manageLink" target="_blank" rel="noreferrer nofollow">{{t('manage')}}</a>
@if (hasLicense()) {
@if (licenseInfo()?.isActive) {
<a class="btn btn-outline-primary btn-sm me-1" [href]="manageLink()" target="_blank" rel="noreferrer nofollow">{{t('manage')}}</a>
} @else {
<a class="btn btn-outline-primary btn-sm me-1"
[ngbTooltip]="t('invalid-license-tooltip')"
href="mailto:kavitareader@gmail.com?subject=Kavita+Subscription+Renewal&body=Description%3A%0D%0A%0D%0ALicense%20Key%3A%0D%0A%0D%0AYour%20Email%3A"
href="mailto:kavitareader@gmail.com?subject=Kavita%20Subscription%20Renewal&body=Description%3A%0D%0A%0D%0ALicense%20Key%3A%0D%0A%0D%0AYour%20Email%3A"
>{{t('renew')}}</a>
}
} @else {
<a class="btn btn-secondary btn-sm me-1" [href]="buyLink" target="_blank" rel="noreferrer nofollow">{{t('buy')}}</a>
<button class="btn btn-primary btn-sm" (click)="toggleViewMode()">{{isViewMode ? t('activate') : t('cancel')}}</button>
<button class="btn btn-primary btn-sm" (click)="toggleViewMode()">{{isViewMode() ? t('activate') : t('cancel')}}</button>
}
</ng-template>
</app-setting-item>
@ -116,8 +118,9 @@
</form>
@let licInfo = licenseInfo();
@if (hasLicense() && licInfo) {
@if (hasLicense && licenseInfo) {
<div class="setting-section-break"></div>
<div class="row g-0 mt-3">
@ -125,11 +128,11 @@
<div class="mb-2 col-md-6 col-sm-12">
<app-setting-item [canEdit]="false" [showEdit]="false" [title]="t('license-active-label')">
<ng-template #view>
@if (isChecking) {
@if (isChecking()) {
{{null | defaultValue}}
} @else {
<i class="fas {{licenseInfo.isActive ? 'fa-check-circle' : 'fa-circle-xmark error'}}">
<span class="visually-hidden">{{licenseInfo.isActive ? t('valid') : t('invalid')}]</span>
<i class="fas {{licInfo.isActive ? 'fa-check-circle' : 'fa-circle-xmark error'}}">
<span class="visually-hidden">{{licInfo.isActive ? t('valid') : t('invalid')}]</span>
</i>
}
</ng-template>
@ -139,7 +142,7 @@
<div class="mb-2 col-md-6 col-sm-12">
<app-setting-item [canEdit]="false" [showEdit]="false" [title]="t('supported-version-label')">
<ng-template #view>
<i class="fas {{licenseInfo.isValidVersion ? 'fa-check-circle' : 'fa-circle-xmark error'}}">
<i class="fas {{licInfo.isValidVersion ? 'fa-check-circle' : 'fa-circle-xmark error'}}">
<span class="visually-hidden">{{isValidVersion ? t('valid') : t('invalid')}]</span>
</i>
</ng-template>
@ -149,7 +152,7 @@
<div class="mb-2 col-md-6 col-sm-12">
<app-setting-item [canEdit]="false" [showEdit]="false" [title]="t('expiration-label')">
<ng-template #view>
{{licenseInfo.expirationDate | utcToLocalTime | defaultValue}}
{{licInfo.expirationDate | utcToLocalTime | defaultValue}}
</ng-template>
</app-setting-item>
</div>
@ -157,7 +160,7 @@
<div class="mb-2 col-md-6 col-sm-12">
<app-setting-item [canEdit]="false" [showEdit]="false" [title]="t('total-subbed-months-label')">
<ng-template #view>
{{licenseInfo.totalMonthsSubbed | number}}
{{licInfo.totalMonthsSubbed | number}}
</ng-template>
</app-setting-item>
</div>
@ -166,8 +169,8 @@
<app-setting-item [canEdit]="false" [showEdit]="false" [title]="t('email-label')">
<ng-template #view>
<span (click)="toggleEmailShow()" class="col-12 clickable">
@if (showEmail) {
{{licenseInfo.registeredEmail}}
@if (showEmail()) {
{{licInfo.registeredEmail}}
} @else {
***************
}
@ -181,9 +184,15 @@
<!-- Actions around license -->
<h3>{{t('actions-title')}}</h3>
<div class="mt-2 mb-2">
<app-setting-button [subtitle]="t('cancel-tooltip')">
<a class="btn btn-danger btn-sm mt-1" [href]="manageLink()" target="_blank" rel="noreferrer nofollow">{{t('cancel-label')}}</a>
</app-setting-button>
</div>
<div class="mt-2 mb-2">
<app-setting-button [subtitle]="t('delete-tooltip')">
<button type="button" class="flex-fill btn btn-danger mt-1" aria-describedby="license-key-header" (click)="deleteLicense()">
<button type="button" class="flex-fill btn btn-danger btn-sm mt-1" aria-describedby="license-key-header" (click)="deleteLicense()">
{{t('activate-delete')}}
</button>
</app-setting-button>
@ -191,7 +200,7 @@
<div class="mt-2 mb-2">
<app-setting-button [subtitle]="t('manage-tooltip')">
<a class="btn btn-primary btn-sm mt-1" [href]="manageLink" target="_blank" rel="noreferrer nofollow">{{t('manage')}}</a>
<a class="btn btn-primary btn-sm mt-1" [href]="manageLink()" target="_blank" rel="noreferrer nofollow">{{t('manage')}}</a>
</app-setting-button>
</div>
</div>

View File

@ -1,15 +1,10 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, DestroyRef, inject,
OnInit
} from '@angular/core';
import { FormControl, FormGroup, Validators, ReactiveFormsModule } from "@angular/forms";
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, inject, model, OnInit} from '@angular/core';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
import {AccountService} from "../../_services/account.service";
import {ToastrService} from "ngx-toastr";
import {ConfirmService} from "../../shared/confirm.service";
import { LoadingComponent } from '../../shared/loading/loading.component';
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
import {LoadingComponent} from '../../shared/loading/loading.component';
import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
import {environment} from "../../../environments/environment";
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {WikiLink} from "../../_models/wiki";
@ -34,58 +29,59 @@ import {LicenseService} from "../../_services/license.service";
export class LicenseComponent implements OnInit {
private readonly cdRef = inject(ChangeDetectorRef);
private readonly destroyRef = inject(DestroyRef);
private readonly toastr = inject(ToastrService);
private readonly confirmService = inject(ConfirmService);
protected readonly accountService = inject(AccountService);
protected readonly licenseService = inject(LicenseService);
protected readonly WikiLink = WikiLink;
protected readonly buyLink = environment.buyLink;
formGroup: FormGroup = new FormGroup({});
isViewMode: boolean = true;
isChecking: boolean = true;
isSaving: boolean = false;
formGroup: FormGroup = new FormGroup({
'licenseKey': new FormControl('', [Validators.required]),
'email': new FormControl('', [Validators.required]),
'discordId': new FormControl('', [Validators.pattern(/\d+/)])
});
isViewMode = model<boolean>(true);
isChecking = model<boolean>(true);
isSaving = model<boolean>(false);
hasLicense = model<boolean>(false);
licenseInfo = model<LicenseInfo | null>(null);
showEmail = model<boolean>(false);
/**
* Either the normal manageLink or with a prefilled email to ease the user
*/
readonly manageLink = computed(() => {
const email = this.licenseInfo()?.registeredEmail;
if (!email) return environment.manageLink;
return environment.manageLink + '?prefilled_email=' + encodeURIComponent(email);
})
hasLicense: boolean = false;
licenseInfo: LicenseInfo | null = null;
showEmail: boolean = false;
buyLink = environment.buyLink;
manageLink = environment.manageLink;
ngOnInit(): void {
this.formGroup.addControl('licenseKey', new FormControl('', [Validators.required]));
this.formGroup.addControl('email', new FormControl('', [Validators.required]));
this.formGroup.addControl('discordId', new FormControl('', [Validators.pattern(/\d+/)]));
this.loadLicenseInfo().subscribe();
}
loadLicenseInfo(forceCheck = false) {
this.isChecking = true;
this.cdRef.markForCheck();
this.isChecking.set(true);
return this.licenseService.hasAnyLicense()
.pipe(
tap(res => {
this.hasLicense = res;
this.isChecking = false;
this.cdRef.markForCheck();
this.hasLicense.set(res);
this.isChecking.set(false);
}),
filter(hasLicense => hasLicense),
tap(_ => {
this.isChecking = true;
this.cdRef.markForCheck();
this.isChecking.set(true);
}),
switchMap(_ => this.licenseService.licenseInfo(forceCheck)),
tap(licenseInfo => {
this.licenseInfo = licenseInfo;
this.isChecking = false;
this.cdRef.markForCheck();
this.licenseInfo.set(licenseInfo);
this.isChecking.set(false);
})
);
}
@ -99,30 +95,37 @@ export class LicenseComponent implements OnInit {
}
saveForm() {
this.isSaving = true;
this.cdRef.markForCheck();
const hadActiveLicenseBefore = this.licenseInfo?.isActive;
this.licenseService.updateUserLicense(this.formGroup.get('licenseKey')!.value.trim(), this.formGroup.get('email')!.value.trim(), this.formGroup.get('discordId')!.value.trim())
.subscribe(() => {
this.isSaving.set(true);
this.resetForm();
this.isViewMode = true;
this.isSaving = false;
this.cdRef.markForCheck();
this.loadLicenseInfo().subscribe(async (info) => {
if (info?.isActive && !hadActiveLicenseBefore) {
await this.confirmService.info(translate('license.k+-unlocked-description'), translate('license.k+-unlocked'));
} else {
this.toastr.info(translate('toasts.k+-license-saved'));
}
});
}, async (err) => {
await this.handleError(err);
const hadActiveLicenseBefore = this.licenseInfo()?.isActive;
const license = this.formGroup.get('licenseKey')!.value.trim();
const email = this.formGroup.get('email')!.value.trim();
const discordId = this.formGroup.get('discordId')!.value.trim();
this.licenseService.updateUserLicense(license, email, discordId)
.subscribe({
next: () => {
this.resetForm();
this.isViewMode.set(true);
this.isSaving.set(false);
this.cdRef.markForCheck();
this.loadLicenseInfo().subscribe(async (info) => {
if (info?.isActive && !hadActiveLicenseBefore) {
await this.confirmService.info(translate('license.k+-unlocked-description'), translate('license.k+-unlocked'));
} else {
this.toastr.info(translate('toasts.k+-license-saved'));
}
});
},
error: async err => {
await this.handleError(err);
}
});
}
private async handleError(err: any) {
this.isSaving = false;
this.isSaving.set(false);
this.cdRef.markForCheck();
if (err.hasOwnProperty('error')) {
@ -163,8 +166,7 @@ export class LicenseComponent implements OnInit {
}
forceSave() {
this.isSaving = false;
this.cdRef.markForCheck();
this.isSaving.set(false);
this.licenseService.resetLicense(this.formGroup.get('licenseKey')!.value.trim(), this.formGroup.get('email')!.value.trim())
.subscribe(_ => {
@ -179,10 +181,9 @@ export class LicenseComponent implements OnInit {
this.licenseService.deleteLicense().subscribe(() => {
this.resetForm();
this.isViewMode = true;
this.licenseInfo = null;
this.hasLicense = false;
//this.hasValidLicense = false;
this.isViewMode.set(true);
this.licenseInfo.set(null);
this.hasLicense.set(false);
this.cdRef.markForCheck();
});
}
@ -197,38 +198,16 @@ export class LicenseComponent implements OnInit {
});
}
//
// validateLicense(forceCheck = false) {
// return of().pipe(
// startWith(null),
// tap(_ => {
// this.isChecking = true;
// this.cdRef.markForCheck();
// }),
// switchMap(_ => this.licenseService.licenseInfo(forceCheck)),
// tap(licenseInfo => {
// this.licenseInfo = licenseInfo;
// //this.hasValidLicense = licenseInfo?.isActive || false;
// this.isChecking = false;
// this.cdRef.markForCheck();
// })
// )
//
// }
updateEditMode(mode: boolean) {
this.isViewMode = !mode;
this.cdRef.markForCheck();
this.isViewMode.set(!mode);
}
toggleViewMode() {
this.isViewMode = !this.isViewMode;
this.cdRef.markForCheck();
this.isViewMode.update(v => !v);
this.resetForm();
}
toggleEmailShow() {
this.showEmail = !this.showEmail;
this.cdRef.markForCheck();
this.showEmail.update(v => !v);
}
}

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'manage-email-settings'">
<ng-container *transloco="let t; prefix: 'manage-email-settings'">
<div class="position-relative">
<button type="button" class="btn btn-outline-primary position-absolute custom-position" (click)="test()">{{t('test')}}</button>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'manage-library'">
<ng-container *transloco="let t; prefix: 'manage-library'">
<div class="position-relative">
<div class="position-absolute custom-position-2">
<app-card-actionables [inputActions]="bulkActions" btnClass="btn-outline-primary ms-1" [label]="t('bulk-action-label')"

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read:'manage-matched-metadata'">
<ng-container *transloco="let t; prefix:'manage-matched-metadata'">
<p>{{t('description')}}</p>
<form [formGroup]="filterGroup">

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'manage-media-issues'">
<ng-container *transloco="let t; prefix: 'manage-media-issues'">
<p>{{t('description-part-1')}} <a rel="noopener noreferrer" target="_blank" [href]="WikiLink.MediaIssues">{{t('description-part-2')}}</a></p>
<form [formGroup]="formGroup">

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'manage-media-settings'">
<ng-container *transloco="let t; prefix: 'manage-media-settings'">
<form [formGroup]="settingsForm">
<div class="mb-4">
<p>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read:'manage-metadata-settings'">
<ng-container *transloco="let t; prefix:'manage-metadata-settings'">
<p>{{t('description')}}</p>
@if (isLoaded) {

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'manage-scrobble-errors'">
<ng-container *transloco="let t; prefix: 'manage-scrobble-errors'">
<h4>{{t('title')}}</h4>
<p>{{t('description')}}</p>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'manage-settings'">
<ng-container *transloco="let t; prefix: 'manage-settings'">
<div class="position-relative">
<button type="button" class="btn btn-outline-primary position-absolute custom-position" (click)="resetToDefaults()">{{t('reset-to-default')}}</button>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'manage-system'">
<ng-container *transloco="let t; prefix: 'manage-system'">
@if (serverInfo) {
<div class="mb-3">
<h3>{{t('title')}}</h3>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'manage-tasks-settings'">
<ng-container *transloco="let t; prefix: 'manage-tasks-settings'">
@if (serverSettings) {
<form [formGroup]="settingsForm">

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read:'manage-user-tokens'">
<ng-container *transloco="let t; prefix:'manage-user-tokens'">
<p>{{t('description')}}</p>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'manage-users'">
<ng-container *transloco="let t; prefix: 'manage-users'">
<div class="position-relative">
<button class="btn btn-outline-primary position-absolute custom-position" (click)="inviteUser()">
<i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden">&nbsp;{{t('invite')}}</span>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read:'role-selector'">
<ng-container *transloco="let t; prefix:'role-selector'">
<div class="d-flex justify-content-between">
<div class="col-auto">
<h4>{{t('title')}}</h4>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'all-filters'">
<ng-container *transloco="let t; prefix: 'all-filters'">
<div class="main-container">
<app-side-nav-companion-bar [hasFilter]="false">
<h4 title>

View File

@ -1,5 +1,5 @@
<div class="main-container container-fluid">
<ng-container *transloco="let t; read: 'all-series'">
<ng-container *transloco="let t; prefix: 'all-series'">
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
<h4 title>
{{title}}

View File

@ -1,5 +1,5 @@
<div class="main-container container-fluid">
<ng-container *transloco="let t; read: 'announcements'">
<ng-container *transloco="let t; prefix: 'announcements'">
<app-side-nav-companion-bar>
<h4 title>
{{t('title')}}

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read:'changelog-update-item'">
<ng-container *transloco="let t; prefix:'changelog-update-item'">
@if (update) {
<div class="update-details">
@if (update.blogPart) {

View File

@ -1,5 +1,5 @@
<div class="main-container container-fluid">
<ng-container *transloco="let t; read: 'changelog'">
<ng-container *transloco="let t; prefix: 'changelog'">
<div class="changelog">
@for(update of updates; track update; let indx = $index) {

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read:'new-version-modal'">
<ng-container *transloco="let t; prefix:'new-version-modal'">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>
</div>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read:'out-of-date-modal'">
<ng-container *transloco="let t; prefix:'out-of-date-modal'">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'update-notification-modal'">
<ng-container *transloco="let t; prefix: 'update-notification-modal'">
<div class="modal-header">
<h4 class="modal-title">{{t('title')}}</h4>
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>

View File

@ -32,6 +32,7 @@
[selected]="slot.slotNumber === selectedSlotIndex()"
(selectPicker)="selectSlot(index, slot)"
[editMode]="isEditMode()"
[canChangeEditMode]="canChangeEditMode()"
(editModeChange)="isEditMode.set($event)"
[first]="$first"
[last]="$last"

View File

@ -31,6 +31,7 @@ export class HighlightBarComponent {
isCollapsed = model<boolean>(true);
canCollapse = model<boolean>(true);
isEditMode = model<boolean>(false);
canChangeEditMode = model<boolean>(true);
slots = this.annotationService.slots;
@ -53,6 +54,8 @@ export class HighlightBarComponent {
}
toggleEditMode() {
if (!this.canChangeEditMode()) return;
const existingEdit = this.isEditMode();
this.isEditMode.set(!existingEdit);
}

View File

@ -8,6 +8,6 @@
<div class="offcanvas-body">
<app-table-of-contents [chapters]="chapters()" [chapterId]="chapterId()!" [pageNum]="pageNum()"
(loadChapter)="loadChapterPage($event)" />
(loadChapter)="loadChapterPage($event)" [loading]="loading()" />
</div>
</ng-container>

View File

@ -5,7 +5,7 @@ import {
effect,
EventEmitter,
inject,
model
model, signal
} from '@angular/core';
import {TranslocoDirective} from "@jsverse/transloco";
import {NgbActiveOffcanvas} from "@ng-bootstrap/ng-bootstrap";
@ -40,6 +40,7 @@ export class ViewTocDrawerComponent {
* The actual pages from the epub, used for showing on table of contents. This must be here as we need access to it for scroll anchors
*/
chapters = model<Array<BookChapterItem>>([]);
loading = signal(true);
loadPage: EventEmitter<LoadPageEvent | null> = new EventEmitter<LoadPageEvent | null>();
@ -54,6 +55,7 @@ export class ViewTocDrawerComponent {
}
this.bookService.getBookChapters(id).subscribe(bookChapters => {
this.loading.set(false);
this.chapters.set(bookChapters);
this.cdRef.markForCheck();
});

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'book-line-overlay'">
<ng-container *transloco="let t; prefix: 'book-line-overlay'">
@if(selectedText.length > 0 || mode !== BookLineOverlayMode.None) {
<div class="overlay">

View File

@ -43,7 +43,7 @@
<div #readingHtml class="book-content {{layoutMode() | columnLayoutClass}} {{writingStyle() | writingStyleClass}}"
[ngStyle]="{'max-height': columnHeight(), 'max-width': verticalBookContentWidth(), 'width': verticalBookContentWidth(), 'column-width': columnWidth()}"
[ngClass]="{'immersive': immersiveMode() && actionBarVisible}"
[ngClass]="{'immersive': immersiveMode() && actionBarVisible, 'debug': debugMode()}"
[innerHtml]="page()" (click)="toggleMenu($event)" (mousedown)="mouseDown($event)" (wheel)="onWheel($event)"></div>
@if (shouldShowBottomActionBar()) {
@ -89,6 +89,13 @@
<i class="fa fa-reply" aria-hidden="true"></i>
</button>
}
@if (debugMode()) {
<button class="btn btn-secondary btn-icon me-1" (click)="debugInsertViewportView()">
V
</button>
}
<button class="btn btn-secondary btn-icon me-1" (click)="viewBookmarkImages()">
<i class="fa-solid fa-book-bookmark" aria-hidden="true"></i>
</button>
@ -103,14 +110,6 @@
</button>
</div>
</div>
<div class="progress-bar-container">
@if (isLoading()) {
<ngb-progressbar type="primary" height="10px" [value]="1" [max]="1" [striped]="true"></ngb-progressbar>
} @else {
<ngb-progressbar type="primary" height="10px" [value]="virtualizedPageNum()" [max]="virtualizedMaxPages()" (click)="goToPage()" [showValue]="true"></ngb-progressbar>
}
</div>
</div>
}
</ng-template>
@ -146,6 +145,12 @@
</span>
}
}
@if (debugMode()) {
@let vp = getVirtualPage();
{{vp[0] * vp[2]}} / {{vp[1] * vp[2]}}
}
</div>
<button class="btn btn-outline-secondary btn-icon col-2 col-xs-1"

View File

@ -1,66 +1,66 @@
@font-face {
font-family: "Fira_Sans";
src: url(../../../../assets/fonts/Fira_Sans/FiraSans-Regular.woff2) format("woff2");
font-display: swap;
font-family: "Fira_Sans";
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.woff2) format("woff2");
font-display: swap;
font-family: "Lato";
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.woff2) format("woff2");
font-display: swap;
font-family: "Libre_Baskerville";
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.woff2) format("woff2");
font-display: swap;
font-family: "Merriweather";
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.woff2) format("woff2");
font-display: swap;
font-family: "Nanum_Gothic";
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.woff2) format("woff2");
font-display: swap;
font-family: "RocknRoll_One";
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.woff2) format("woff2");
font-display: swap;
font-family: "OpenDyslexic2";
src: url(../../../../assets/fonts/OpenDyslexic2/OpenDyslexic-Regular.woff2) format("woff2");
font-display: swap;
}
@font-face {
font-family: "FastFontSerif";
src: url(../../../../assets/fonts/Fast_Font/Fast_Serif.woff2) format("woff2");
font-display: swap;
font-family: "FastFontSerif";
src: url(../../../../assets/fonts/Fast_Font/Fast_Serif.woff2) format("woff2");
font-display: swap;
}
@font-face {
font-family: "FastFontSans";
src: url(../../../../assets/fonts/Fast_Font/Fast_Sans.woff2) format("woff2");
font-display: swap;
font-family: "FastFontSans";
src: url(../../../../assets/fonts/Fast_Font/Fast_Sans.woff2) format("woff2");
font-display: swap;
}
: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;
--default-state-scrollbar: var(--primary-color-scrollbar);
--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;
--default-state-scrollbar: var(--primary-color-scrollbar);
}
@ -85,150 +85,115 @@ $action-bar-height: 38px;
}
}
// Drawer
.control-container {
padding-bottom: 5px;
}
.page-stub {
margin-top: 6px;
padding-left: 2px;
padding-right: 2px;
}
.drawer-body {
overflow: auto;
.reader-pills {
justify-content: center;
margin: 0 0.25rem;
li a {
border: 1px solid var(--primary-color);
margin: 0 0.25rem;
.active {
border: unset;
}
}
}
}
// Drawer End
.fixed-top {
z-index: 1022;
direction: ltr;
z-index: 1022;
direction: ltr;
}
.dark-mode .overlay {
opacity: 0;
opacity: 0;
}
.action-bar {
background-color: var(--br-actionbar-bg-color);
background-color: var(--br-actionbar-bg-color);
overflow: hidden;
box-shadow: 0 0 6px 0 rgb(0 0 0 / 70%);
max-height: $action-bar-height;
height: $action-bar-height;
.book-title-text {
text-align: center;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
box-shadow: 0 0 6px 0 rgb(0 0 0 / 70%);
max-height: $action-bar-height;
height: $action-bar-height;
.book-title-text {
text-align: center;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
@media(max-width: 875px) {
.book-title {
display: none;
}
}
}
@media(max-width: 875px) {
.book-title {
margin-top: 10px;
text-align: center;
text-transform: capitalize;
max-height: inherit;
display: none;
}
}
.next-page-highlight {
color: var(--primary-color);
}
.book-title {
margin-top: 10px;
text-align: center;
text-transform: capitalize;
max-height: inherit;
}
.next-page-highlight {
color: var(--primary-color);
}
}
.reader-container {
outline: none; // Only the reading section itself shouldn't receive any outline. We use it to shift focus in fullscreen mode
overflow: auto;
height: calc(var(--vh, 1vh) * 100);
position: relative;
// This is completely invisible, everything else renders over it
outline: none; // Only the reading section itself shouldn't receive any outline. We use it to shift focus in fullscreen mode
overflow: auto;
height: calc(var(--vh, 1vh) * 100);
position: relative;
// This is completely invisible, everything else renders over it
&.column-layout-1 {
height: calc(var(--vh) * 100);
}
&.column-layout-1 {
height: calc(var(--vh) * 100);
}
&.column-layout-2 {
height: calc(var(--vh) * 100);
}
&.column-layout-2 {
height: calc(var(--vh) * 100);
}
&.writing-style-vertical {
direction: rtl;
}
&.writing-style-vertical {
direction: rtl;
}
}
.reading-section {
width: 100%;
//overflow: auto; // This will break progress reporting
height: 100vh;
padding-top: $action-bar-height;
padding-bottom: $action-bar-height;
position: relative;
direction: ltr;
width: 100%;
//overflow: auto; // This will break progress reporting
height: 100vh;
padding-top: $action-bar-height;
padding-bottom: $action-bar-height;
position: relative;
direction: ltr;
//background-color: green !important;
//background-color: green !important;
&.column-layout-1 {
height: calc((var(--vh, 1vh) * 100) - $action-bar-height);
}
&.column-layout-1 {
height: calc((var(--vh, 1vh) * 100) - $action-bar-height);
}
&.column-layout-2 {
height: calc((var(--vh, 1vh) * 100) - $action-bar-height);
}
&.column-layout-2 {
height: calc((var(--vh, 1vh) * 100) - $action-bar-height);
}
&.immersive {
height: calc((var(--vh, 1vh) * 100));
//padding-top: 0px;
//padding-bottom: 0px;
}
&.immersive {
height: calc((var(--vh, 1vh) * 100));
}
&.writing-style-vertical {
writing-mode: vertical-rl;
height: 100%;
}
&.writing-style-vertical {
writing-mode: vertical-rl;
height: 100%;
}
}
.book-container {
position: relative;
height: 100%;
position: relative;
height: 100%;
//background-color: purple !important;
&.column-layout-1 {
height: calc((var(--vh, 1vh) * 100) - $action-bar-height);
}
&.column-layout-1 {
height: calc((var(--vh, 1vh) * 100) - $action-bar-height);
}
&.column-layout-2 {
height: calc((var(--vh, 1vh) * 100) - $action-bar-height);
}
&.column-layout-2 {
height: calc((var(--vh, 1vh) * 100) - $action-bar-height);
}
&.writing-style-vertical {
// Fixes an issue where chrome will cut of margins, doesn't seem to affect other browsers
overflow: auto;
}
&.writing-style-vertical {
// Fixes an issue where chrome will cut of margins, doesn't seem to affect other browsers
overflow: auto;
}
}
.book-content {
@ -237,45 +202,50 @@ $action-bar-height: 38px;
padding: 20px 0px;
&.column-layout-1 {
height: calc((var(--vh) * 100) - calc($action-bar-height)); // * 2
height: calc((var(--vh) * 100) - calc($action-bar-height)); // * 2
&.writing-style-vertical {
padding: 0 10px 0 0;
margin: 20px 0;
}
&.writing-style-vertical {
padding: 0 10px 0 0;
margin: 20px 0;
}
}
&.column-layout-2 {
height: calc((var(--vh) * 100) - calc($action-bar-height)); // * 2
&.column-layout-2 {
height: calc((var(--vh) * 100) - calc($action-bar-height)); // * 2
column-fill: balance;
&.writing-style-vertical {
padding: 0 10px 0 0;
margin: 20px 0;
}
&.debug {
column-rule: 2px dashed #666666;
}
&.writing-style-vertical {
height: auto;
padding: 0 10px 0 0;
margin: 20px 0;
}
}
// &.immersive {
// // Note: I removed this for bug: https://github.com/Kareadita/Kavita/issues/1726
// //height: calc((var(--vh, 1vh) * 100) - $action-bar-height);
// }
&.writing-style-vertical {
height: auto;
}
a, :link {
color: var(--brtheme-link-text-color);
}
// &.immersive {
// // Note: I removed this for bug: https://github.com/Kareadita/Kavita/issues/1726
// //height: calc((var(--vh, 1vh) * 100) - $action-bar-height);
// }
background-color: var(--brtheme-bg-color);
a, :link {
color: var(--brtheme-link-text-color);
}
background-color: var(--brtheme-bg-color);
}
.pagination-cont {
background: var(--br-actionbar-bg-color);
border-radius: 5px;
padding: 5px 15px;
margin: 0 0 5px;
border: var(--drawer-pagination-border);
background: var(--br-actionbar-bg-color);
border-radius: 5px;
padding: 5px 15px;
margin: 0 0 5px;
border: var(--drawer-pagination-border);
}
.virt-pagination-cont {
@ -285,70 +255,74 @@ $action-bar-height: 38px;
}
.bottom-bar {
position: fixed;
width: 100%;
bottom: 0;
left: 0;
writing-mode: horizontal-tb;
position: fixed;
width: 100%;
bottom: 0;
left: 0;
writing-mode: horizontal-tb;
}
// 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;
}
.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;
.book-content {
column-count: 2;
column-gap: 20px;
overflow: hidden;
word-break: break-word;
overflow-wrap: break-word;
&.debug {
overflow: scroll;
overflow-y: hidden;
}
}
}
// A bunch of resets so books render correctly
::ng-deep .book-content {
& a, & :link {
color: #8db2e5;
}
& a, & :link {
color: #8db2e5;
}
}
// This is applied to images in the backend
::ng-deep .kavita-scale-width-container {
width: auto;
max-height: calc(var(--book-reader-content-max-height) - ($action-bar-height)) !important;
max-width: calc(var(--book-reader-content-max-width)) !important;
position: var(--book-reader-content-position) !important;
top: var(--book-reader-content-top) !important;
left: var(--book-reader-content-left) !important;
transform: var(--book-reader-content-transform) !important;
width: auto;
max-height: calc(var(--book-reader-content-max-height) - ($action-bar-height)) !important;
max-width: calc(var(--book-reader-content-max-width)) !important;
position: var(--book-reader-content-position) !important;
top: var(--book-reader-content-top) !important;
left: var(--book-reader-content-left) !important;
transform: var(--book-reader-content-transform) !important;
}
// This is applied to images in the backend
::ng-deep .kavita-scale-width {
max-height: calc(var(--book-reader-content-max-height) - ($action-bar-height)) !important;
max-width: calc(var(--book-reader-content-max-width)) !important;
object-fit: contain;
object-position: top center;
break-inside: avoid;
break-before: column;
max-height: 100vh;
max-height: calc(var(--book-reader-content-max-height) - ($action-bar-height)) !important;
max-width: calc(var(--book-reader-content-max-width)) !important;
object-fit: contain;
object-position: top center;
break-inside: avoid;
break-before: column;
max-height: 100vh;
}
// Click to Paginate styles
.icon-primary-color {
color: $primary-color;
color: $primary-color;
}
$pagination-color: transparent;
@ -361,63 +335,63 @@ $pagination-opacity: 0;
.right {
position: absolute;
right: 0px;
top: $action-bar-height;
width: 20vw;
z-index: 3;
background: $pagination-color;
border-color: transparent;
border: none !important;
opacity: 0;
outline: none;
cursor: pointer;
position: absolute;
right: 0px;
top: $action-bar-height;
width: 20vw;
z-index: 3;
background: $pagination-color;
border-color: transparent;
border: none !important;
opacity: 0;
outline: none;
cursor: pointer;
&.immersive {
top: 0;
}
&.immersive {
top: 0;
}
&.no-pointer-events {
pointer-events: none;
}
&.no-pointer-events {
pointer-events: none;
}
}
// This class pushes the click area to the left a bit to let users click the scrollbar
.right-with-scrollbar {
position: absolute;
right: 17px;
top: $action-bar-height;
width: 18%;
z-index: 3;
background: $pagination-color;
opacity: $pagination-opacity;
border-color: transparent;
border: none !important;
outline: none;
cursor: pointer;
position: absolute;
right: 17px;
top: $action-bar-height;
width: 18%;
z-index: 3;
background: $pagination-color;
opacity: $pagination-opacity;
border-color: transparent;
border: none !important;
outline: none;
cursor: pointer;
&.immersive {
top: 0;
}
&.immersive {
top: 0;
}
}
.left {
position: absolute;
left: 0px;
top: $action-bar-height;
width: 20vw;
background: $pagination-color;
opacity: $pagination-opacity;
border-color: transparent;
border: none !important;
z-index: 3;
outline: none;
height: 100vw;
cursor: pointer;
position: absolute;
left: 0px;
top: $action-bar-height;
width: 20vw;
background: $pagination-color;
opacity: $pagination-opacity;
border-color: transparent;
border: none !important;
z-index: 3;
outline: none;
height: 100vw;
cursor: pointer;
&.immersive {
top: 0px;
}
&.immersive {
top: 0px;
}
}
@ -436,40 +410,40 @@ $pagination-opacity: 0;
// TODO: Figure out why book-reader has it's own button overrides
.btn {
&.btn-secondary {
color: var(--br-actionbar-button-text-color);
border-color: transparent;
background-color: unset;
&.btn-secondary {
color: var(--br-actionbar-button-text-color);
border-color: transparent;
background-color: unset;
&:hover, &:focus {
border-color: var(--br-actionbar-button-hover-border-color);
}
&:hover, &:focus {
border-color: var(--br-actionbar-button-hover-border-color);
}
}
&.btn-outline-secondary {
border-color: transparent;
background-color: unset;
&.btn-outline-secondary {
border-color: transparent;
background-color: unset;
&:hover, &:focus {
border-color: var(--br-actionbar-button-hover-border-color);
}
&:hover, &:focus {
border-color: var(--br-actionbar-button-hover-border-color);
}
}
span {
background-color: unset;
color: var(--br-actionbar-button-text-color);
}
span {
background-color: unset;
color: var(--br-actionbar-button-text-color);
}
i {
background-color: unset;
color: var(--br-actionbar-button-text-color);
}
i {
background-color: unset;
color: var(--br-actionbar-button-text-color);
}
&:active {
* {
color: white;
}
&:active {
* {
color: white;
}
}
}
@ -480,17 +454,6 @@ $pagination-opacity: 0;
background-color: var(--bs-body-bg);
}
.progress-bar-container {
border-top: 1px solid var(--bs-border-color-translucent);
background-color: var(--br-actionbar-bg-color); // TODO: BUG: This isn't coloring well
max-height: 10px;
.progress {
border-radius: 0;
background-color: var(--br-actionbar-bg-color);
}
}
.page-progress-slider {
margin: 0;
}

View File

@ -47,7 +47,7 @@ import {ThemeService} from 'src/app/_services/theme.service';
import {ScrollService} from 'src/app/_services/scroll.service';
import {PAGING_DIRECTION} from 'src/app/manga-reader/_models/reader-enums';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {NgbProgressbar, NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
import {BookLineOverlayComponent} from "../book-line-overlay/book-line-overlay.component";
import {translate, TranslocoDirective} from "@jsverse/transloco";
import {ReadingProfile} from "../../../_models/preferences/reading-profiles";
@ -67,6 +67,7 @@ import {LayoutMeasurementService} from "../../../_services/layout-measurement.se
import {ColorscapeService} from "../../../_services/colorscape.service";
import {environment} from "../../../../environments/environment";
import {LoadPageEvent} from "../_drawers/view-bookmarks-drawer/view-bookmark-drawer.component";
import afterFrame from "afterframe";
interface HistoryPoint {
@ -124,7 +125,7 @@ const SCROLL_DELAY = 10;
])
],
imports: [NgTemplateOutlet, NgStyle, NgClass, NgbTooltip,
BookLineOverlayComponent, TranslocoDirective, ColumnLayoutClassPipe, WritingStyleClassPipe, ReadTimeLeftPipe, PercentPipe, NgxSliderModule, NgbProgressbar],
BookLineOverlayComponent, TranslocoDirective, ColumnLayoutClassPipe, WritingStyleClassPipe, ReadTimeLeftPipe, PercentPipe, NgxSliderModule],
providers: [EpubReaderSettingsService, LayoutMeasurementService],
})
export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
@ -354,6 +355,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
*/
firstLoad: boolean = true;
/**
* Injects information to help debug issues
*/
debugMode = model<boolean>(!environment.production && true);
@ViewChild('bookContainer', {static: false}) bookContainerElemRef!: ElementRef<HTMLDivElement>;
@ -361,10 +367,12 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
* book-content class
*/
@ViewChild('readingHtml', {static: false}) bookContentElemRef!: ElementRef<HTMLDivElement>;
@ViewChild('readingHtml', { read: ViewContainerRef }) readingContainer!: ViewContainerRef;
@ViewChild('readingSection', {static: false}) readingSectionElemRef!: ElementRef<HTMLDivElement>;
@ViewChild('stickyTop', {static: false}) stickyTopElemRef!: ElementRef<HTMLDivElement>;
@ViewChild('reader', {static: false}) reader!: ElementRef;
@ViewChild('readingHtml', { read: ViewContainerRef }) readingContainer!: ViewContainerRef;
protected readonly layoutMode = this.readerSettingsService.layoutMode;
@ -525,15 +533,23 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
const layoutMode = this.layoutMode();
const writingStyle = this.writingStyle();
const windowWidth = this.windowWidth();
const base = writingStyle === WritingStyle.Vertical ? this.pageHeight() : this.pageWidth();
// console.log('window width: ', windowWidth)
// console.log('book content width: ', this.readingSectionElemRef?.nativeElement?.clientWidth);
// console.log('column width: ', base / 4);
switch (layoutMode) {
case BookPageLayoutMode.Default:
return 'unset';
case BookPageLayoutMode.Column1:
return ((base / 2) - 4) + 'px';
case BookPageLayoutMode.Column2:
return (base / 4) + 'px'
//return (this.readingSectionElemRef?.nativeElement?.clientWidth - this.getMargin() + 1) / 2 + 'px';
return (((this.readingSectionElemRef?.nativeElement?.clientWidth ?? base)) / 4) + 1 + 'px'
//return ((base) / 4) + 6 + 'px'
default:
return 'unset';
}
@ -562,6 +578,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (layoutMode !== BookPageLayoutMode.Default && writingStyle !== WritingStyle.Horizontal) {
console.log('verticalBookContentWidth: ', verticalPageWidth)
return `${verticalPageWidth}px`;
}
return '';
@ -974,6 +991,9 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
case KEY_CODES.F:
this.applyFullscreen();
break;
case KEY_CODES.SPACE:
this.actionBarVisible.update(x => !x);
break;
}
}
@ -1093,30 +1113,30 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
*/
addLinkClickHandlers() {
const links = this.readingSectionElemRef.nativeElement.querySelectorAll('a');
links.forEach((link: any) => {
link.addEventListener('click', (e: any) => {
e.stopPropagation();
let targetElem = e.target;
if (e.target.nodeName !== 'A' && e.target.parentNode.nodeName === 'A') {
// Certain combos like <a><sup>text</sup></a> can cause the target to be the sup tag and not the anchor
targetElem = e.target.parentNode;
}
if (!targetElem.attributes.hasOwnProperty('kavita-page')) { return; }
const page = parseInt(targetElem.attributes['kavita-page'].value, 10);
if (this.adhocPageHistory.peek()?.page !== this.pageNum()) {
this.adhocPageHistory.push({page: this.pageNum(), scrollPart: this.readerService.scopeBookReaderXpath(this.lastSeenScrollPartPath)});
}
links.forEach((link: any) => {
link.addEventListener('click', (e: any) => {
e.stopPropagation();
let targetElem = e.target;
if (e.target.nodeName !== 'A' && e.target.parentNode.nodeName === 'A') {
// Certain combos like <a><sup>text</sup></a> can cause the target to be the sup tag and not the anchor
targetElem = e.target.parentNode;
}
if (!targetElem.attributes.hasOwnProperty('kavita-page')) { return; }
const page = parseInt(targetElem.attributes['kavita-page'].value, 10);
if (this.adhocPageHistory.peek()?.page !== this.pageNum()) {
this.adhocPageHistory.push({page: this.pageNum(), scrollPart: this.readerService.scopeBookReaderXpath(this.lastSeenScrollPartPath)});
}
const partValue = targetElem.attributes.hasOwnProperty('kavita-part') ? targetElem.attributes['kavita-part'].value : undefined;
if (partValue && page === this.pageNum()) {
this.scrollTo(targetElem.attributes['kavita-part'].value);
return;
}
const partValue = targetElem.attributes.hasOwnProperty('kavita-part') ? targetElem.attributes['kavita-part'].value : undefined;
if (partValue && page === this.pageNum()) {
this.scrollTo(targetElem.attributes['kavita-part'].value);
return;
}
this.setPageNum(page);
this.loadPage(partValue);
});
this.setPageNum(page);
this.loadPage(partValue);
});
});
}
moveFocus() {
@ -1159,39 +1179,41 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
loadPage(part?: string | undefined, scrollTop?: number | undefined) {
console.log('load page called with: part: ', part, 'scrollTop: ', scrollTop);
this.isLoading.set(true);
this.cdRef.markForCheck();
this.bookService.getBookPage(this.chapterId, this.pageNum()).subscribe(content => {
this.isSingleImagePage = this.checkSingleImagePage(content) // This needs be performed before we set this.page to avoid image jumping
this.isSingleImagePage = this.checkSingleImagePage(content); // This needs be performed before we set this.page to avoid image jumping
this.updateSingleImagePageStyles();
this.page.set(this.domSanitizer.bypassSecurityTrustHtml(content));
this.scrollService.unlock();
this.setupObservers();
this.cdRef.markForCheck();
setTimeout(() => {
afterFrame(() => {
this.addLinkClickHandlers();
this.applyPageStyles(this.pageStyles());
const imgs = this.readingSectionElemRef.nativeElement.querySelectorAll('img');
if (imgs === null || imgs.length === 0) {
if (imgs !== null && imgs.length > 0) {
Promise.all(Array.from(imgs ?? [])
.filter(img => !img.complete)
.map(img => new Promise(resolve => { img.onload = img.onerror = resolve; })))
.then(() => {
this.setupPage(part, scrollTop);
this.updateImageSizes();
this.injectImageBookmarkIndicators();
});
} else {
this.setupPage(part, scrollTop);
return;
}
Promise.all(Array.from(imgs)
.filter(img => !img.complete)
.map(img => new Promise(resolve => { img.onload = img.onerror = resolve; })))
.then(() => {
this.setupPage(part, scrollTop);
this.updateImageSizes();
this.injectImageBookmarkIndicators();
this.setupObservers();
});
this.firstLoad = false;
}, SCROLL_DELAY);
});
});
}
@ -1367,7 +1389,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
try {
this.setupPageScroll(part, scrollTop);
this.scrollWithinPage(part, scrollTop);
} catch (ex) {
console.error(ex);
}
@ -1383,14 +1405,34 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
private setupPageScroll(part?: string | undefined, scrollTop?: number) {
private scroll(lambda: () => void) {
afterFrame(() => {
setTimeout(lambda, SCROLL_DELAY)
});
}
private scrollWithinPage(part?: string | undefined, scrollTop?: number) {
if (part !== undefined && part !== '') {
this.scrollTo(this.readerService.scopeBookReaderXpath(part));
console.log('Scrolling via part: ', part);
this.scroll(() => this.scrollTo(this.readerService.scopeBookReaderXpath(part)));
// afterFrame(() => {
// setTimeout(() => this.scrollTo(this.readerService.scopeBookReaderXpath(part)), SCROLL_DELAY)
// })
//
// setTimeout(() => {
// afterFrame(() => this.scrollTo(this.readerService.scopeBookReaderXpath(part)));
// }, SCROLL_DELAY);
return;
}
if (scrollTop !== undefined && scrollTop !== 0) {
setTimeout(() => this.scrollService.scrollTo(scrollTop, this.reader.nativeElement));
// setTimeout(() => {
// afterFrame(() => this.scrollService.scrollTo(scrollTop, this.reader.nativeElement));
// }, SCROLL_DELAY);
console.log('Scrolling via scrollTop: ', scrollTop);
this.scroll(() => this.scrollService.scrollTo(scrollTop, this.reader.nativeElement));
return;
}
@ -1399,31 +1441,57 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (layoutMode === BookPageLayoutMode.Default) {
if (writingStyle === WritingStyle.Vertical) {
setTimeout(()=> this.scrollService.scrollToX(this.bookContentElemRef.nativeElement.clientWidth, this.reader.nativeElement), SCROLL_DELAY);
console.log('Scrolling via x axis: ', this.bookContentElemRef.nativeElement.clientWidth, ' via ', this.reader.nativeElement);
this.scroll(() => this.scrollService.scrollToX(this.bookContentElemRef.nativeElement.clientWidth, this.reader.nativeElement));
//
// setTimeout(() => {
// afterFrame(()=> this.scrollService.scrollToX(this.bookContentElemRef.nativeElement.clientWidth, this.reader.nativeElement));
// }, SCROLL_DELAY);
return;
}
setTimeout(() => this.scrollService.scrollTo(0, this.reader.nativeElement), SCROLL_DELAY);
// setTimeout(() => {
// afterFrame(() => this.scrollService.scrollTo(0, this.reader.nativeElement));
// }, SCROLL_DELAY);
console.log('Scrolling via x axis to 0: ', 0, ' via ', this.reader.nativeElement);
this.scroll(() => this.scrollService.scrollToX(0, this.reader.nativeElement));
return;
}
if (writingStyle === WritingStyle.Vertical) {
if (this.pagingDirection === PAGING_DIRECTION.BACKWARDS) {
setTimeout(() => this.scrollService.scrollTo(this.bookContentElemRef.nativeElement.scrollHeight, this.bookContentElemRef.nativeElement, 'auto'), SCROLL_DELAY);
// setTimeout(() => {
// afterFrame(() => this.scrollService.scrollTo(this.bookContentElemRef.nativeElement.scrollHeight, this.bookContentElemRef.nativeElement, 'auto'));
// }, SCROLL_DELAY);
console.log('(Vertical) Scrolling via x axis to: ', this.bookContentElemRef.nativeElement.scrollHeight, ' via ', this.bookContentElemRef.nativeElement);
this.scroll(() => this.scrollService.scrollTo(this.bookContentElemRef.nativeElement.scrollHeight, this.bookContentElemRef.nativeElement, 'auto'));
return;
}
setTimeout(() => this.scrollService.scrollTo(0, this.bookContentElemRef.nativeElement,'auto' ), SCROLL_DELAY);
// setTimeout(() => {
// afterFrame(() => this.scrollService.scrollTo(0, this.bookContentElemRef.nativeElement, 'auto'));
// }, SCROLL_DELAY);
console.log('(Vertical) Scrolling via x axis to 0: ', 0, ' via ', this.bookContentElemRef.nativeElement);
this.scroll(() => this.scrollService.scrollTo(0, this.bookContentElemRef.nativeElement, 'auto'));
return;
}
// We need to check if we are paging back, because we need to adjust the scroll
if (this.pagingDirection === PAGING_DIRECTION.BACKWARDS) {
setTimeout(() => this.scrollService.scrollToX(this.bookContentElemRef.nativeElement.scrollWidth, this.bookContentElemRef.nativeElement), SCROLL_DELAY);
// setTimeout(() => {
// afterFrame(() => this.scrollService.scrollToX(this.bookContentElemRef.nativeElement.scrollWidth, this.bookContentElemRef.nativeElement));
// }, SCROLL_DELAY);
console.log('(Page Back) Scrolling via x axis to: ', this.bookContentElemRef.nativeElement.scrollWidth, ' via ', this.bookContentElemRef.nativeElement);
this.scroll(() => this.scrollService.scrollToX(this.bookContentElemRef.nativeElement.scrollWidth, this.bookContentElemRef.nativeElement));
return;
}
setTimeout(() => this.scrollService.scrollToX(0, this.bookContentElemRef.nativeElement), SCROLL_DELAY);
setTimeout(() => {
afterFrame(() => this.scrollService.scrollToX(0, this.bookContentElemRef.nativeElement));
}, SCROLL_DELAY);
console.log('Scrolling via x axis to 0: ', 0, ' via ', this.bookContentElemRef.nativeElement);
this.scroll(() => this.scrollService.scrollToX(0, this.bookContentElemRef.nativeElement));
}
private setupAnnotationElements() {
@ -1436,11 +1504,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return;
}
const [,, pageWidth] = this.getVirtualPage();
const actualWidth = this.bookContentElemRef.nativeElement.scrollWidth;
const lastPageWidth = actualWidth % pageWidth;
const pageSize = this.pageSize();
const [_, totalScroll] = this.getScrollOffsetAndTotalScroll();
const lastPageSize = totalScroll % pageSize;
if (lastPageWidth >= pageWidth / 2 || lastPageWidth === 0) {
if (lastPageSize >= pageSize / 2 || lastPageSize === 0) {
// The last page needs more than one column, no pages will be duplicated
return;
}
@ -1595,7 +1663,12 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (this.readingSectionElemRef == null) return 0;
const margin = (this.convertVwToPx(parseInt(marginLeft, 10)) * 2);
return this.readingSectionElemRef.nativeElement.clientWidth - margin + (COLUMN_GAP * columnGapModifier);
// console.log('page size calc, client width: ', this.readingSectionElemRef.nativeElement.clientWidth)
// 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);
});
pageHeight = computed(() => {
@ -1663,9 +1736,13 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
pageSize = computed(() => {
return this.writingStyle() === WritingStyle.Vertical
? this.pageHeight()
: this.pageWidth();
const height = this.pageHeight();
const width = this.pageWidth();
const writingStyle = this.writingStyle();
return writingStyle === WritingStyle.Vertical
? height
: width;
});
@ -1772,10 +1849,20 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return;
}
if (pageLevelStyles.includes(item[0])) {
this.renderer.setStyle(this.bookContentElemRef.nativeElement, item[0], item[1], RendererStyleFlags2.Important);
let value = item[1];
// Convert vw for margin into fixed pixels otherwise when paging, 2 column mode will bleed text between columns
if (item[0].startsWith('margin')) {
const vw = parseInt(item[1].replace('vw', ''), 10);
value = `${this.convertVwToPx(vw)}px`;
}
this.renderer.setStyle(this.bookContentElemRef.nativeElement, item[0], value, RendererStyleFlags2.Important);
}
});
const individualElementStyles = Object.entries(pageStyles).filter(item => elementLevelStyles.includes(item[0]));
for(let i = 0; i < this.bookContentElemRef.nativeElement.children.length; i++) {
const elem = this.bookContentElemRef.nativeElement.children.item(i);
@ -1892,7 +1979,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
const writingStyle = this.writingStyle();
if (layout !== BookPageLayoutMode.Default) {
setTimeout(() => this.scrollService.scrollIntoView(element as HTMLElement, {timeout, scrollIntoViewOptions: {'block': 'start', 'inline': 'start'}}));
afterFrame(() => this.scrollService.scrollIntoView(element as HTMLElement, {timeout, scrollIntoViewOptions: {'block': 'start', 'inline': 'start'}}));
return;
}
@ -1900,12 +1987,12 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
case WritingStyle.Vertical:
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
const scrollLeft = element.getBoundingClientRect().left + window.scrollX - (windowWidth - element.getBoundingClientRect().width);
setTimeout(() => this.scrollService.scrollToX(scrollLeft, this.reader.nativeElement, 'smooth'), SCROLL_DELAY);
afterFrame(() => this.scrollService.scrollToX(scrollLeft, this.reader.nativeElement, 'smooth'));
break;
case WritingStyle.Horizontal:
const fromTopOffset = element.getBoundingClientRect().top + window.scrollY + TOP_OFFSET;
// We need to use a delay as webkit browsers (aka Apple devices) don't always have the document rendered by this point
setTimeout(() => this.scrollService.scrollTo(fromTopOffset, this.reader.nativeElement), SCROLL_DELAY);
afterFrame(() => this.scrollService.scrollTo(fromTopOffset, this.reader.nativeElement));
}
}
@ -1983,7 +2070,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.updateImageSizes()
}, 200);
this.updateSingleImagePageStyles()
this.updateSingleImagePageStyles();
// Calculate if bottom actionbar is needed. On a timeout to get accurate heights
// if (this.bookContentElemRef == null) {
@ -2108,7 +2195,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
handleReaderClick(event: MouseEvent) {
if (!this.clickToPaginate()) {
if (!this.clickToPaginate() && !this.immersiveMode()) {
event.preventDefault();
event.stopPropagation();
this.toggleMenu(event);
@ -2176,8 +2263,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
viewAnnotations() {
this.epubMenuService.openViewAnnotationsDrawer((annotation: Annotation) => {
async viewAnnotations() {
await this.epubMenuService.openViewAnnotationsDrawer((annotation: Annotation) => {
if (this.pageNum() != annotation.pageNumber) {
this.setPageNum(annotation.pageNumber);
}
@ -2197,27 +2284,93 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
private debugVirtualPaging() {
if (this.layoutMode() === BookPageLayoutMode.Default) return;
/**
* With queries and pure math, determines the actual viewport the user can see.
*
* NOTE: On Scroll LayoutMode, the height/bottom are not correct
*/
getViewportBoundingRect() {
const margin = this.getMargin();
const [currentVirtualPage, _, pageSize] = this.getVirtualPage();
const visibleBoundingBox = this.bookContentElemRef.nativeElement.getBoundingClientRect();
const [scrollOffset, totalScroll] = this.getScrollOffsetAndTotalScroll();
const pageSize = this.pageSize();
const [currentVirtualPage, totalVirtualPages] = this.getVirtualPage();
let bookContentPadding = 20;
let bookPadding = getComputedStyle(this.bookContentElemRef?.nativeElement!).paddingTop;
if (bookPadding) {
bookContentPadding = parseInt(bookPadding.toString().replace('px', ''), 10);
}
console.log('Virtual Paging Debug:', {
scrollOffset,
totalScroll,
pageSize,
currentVirtualPage,
totalVirtualPages,
layoutMode: this.layoutMode(),
writingStyle: this.writingStyle(),
bookContentWidth: this.bookContentElemRef?.nativeElement?.clientWidth,
bookContentHeight: this.bookContentElemRef?.nativeElement?.clientHeight,
scrollWidth: this.bookContentElemRef?.nativeElement?.scrollWidth,
scrollHeight: this.bookContentElemRef?.nativeElement?.scrollHeight
// Adjust the bounding box for what is actually visible
const bottomBarHeight = this.document.querySelector('.bottom-bar')?.getBoundingClientRect().height ?? 38;
const topBarHeight = this.document.querySelector('.fixed-top')?.getBoundingClientRect().height ?? 48;
// console.log('bottom: ', visibleBoundingBox.bottom) // TODO: Bottom isn't ideal in scroll mode
const left = margin;
const top = topBarHeight;
const bottom = visibleBoundingBox.bottom - bottomBarHeight + bookContentPadding; // bookContent has a 20px padding top/bottom
const width = pageSize;
const height = bottom - top;
const right = left + width;
console.log('Visible Viewport', {
left, right, top, bottom, width, height
});
return {
left, right, top, bottom, width, height
}
}
debugInsertViewportView() {
const viewport = this.getViewportBoundingRect();
// Insert a debug element to help visualize
document.querySelector('#test')?.remove();
// Create and inject the red rectangle div
const redRect = document.createElement('div');
redRect.id = 'test';
redRect.style.position = 'absolute';
redRect.style.left = `${viewport.left}px`;
redRect.style.top = `${viewport.top}px`;
redRect.style.width = `${viewport.width}px`;
redRect.style.height = `${viewport.height}px`;
redRect.style.border = '5px solid red';
redRect.style.pointerEvents = 'none';
redRect.style.zIndex = '1000';
// Inject into the document
document.body.appendChild(redRect);
}
/**
* Get actual px margin (just one side), falls back to vw -> px mapping calculation
*/
getMargin() {
const pageStyles = this.pageStyles();
let usedComputed = false;
let margin = this.convertVwToPx(parseInt(pageStyles['margin-left'], 10));
const computedMargin = getComputedStyle(this.bookContentElemRef?.nativeElement!).marginLeft;
if (computedMargin) {
margin = parseInt(computedMargin.toString().replace('px', ''), 10);
usedComputed = true;
}
// Sometimes computed will be 0 when first loading which can cause issues (first load)
if (usedComputed && margin < this.convertVwToPx(parseInt(pageStyles['margin-left'], 10))) {
console.warn('Computed margin was 0px when we expected non-zero. Defaulted back to derived vw->px value');
return this.convertVwToPx(parseInt(pageStyles['margin-left'], 10));
}
return margin;
}
protected readonly Breakpoint = Breakpoint;
protected readonly environment = environment;
}

View File

@ -3,22 +3,28 @@
@let bookChapters = chapters();
@if (bookChapters.length === 0) {
<div>
<em>{{t('no-data')}}</em>
</div>
} @else if (bookChapters.length === 1) {
@if (loading()) {
<app-loading [loading]="true"></app-loading>
} @else {
<div>
<em>{{t('no-data')}}</em>
</div>
}
} @else if (isDisplayingChildrenOnly()) {
<div>
<ul>
@for(chapter of bookChapters[0].children; track chapter.title) {
<li class="{{chapter.page === pageNum() ? 'active': ''}}">
@for(chapter of displayedChapters(); track chapter.title) {
<li [id]="`${$index}`" class="{{isChapterSelected(chapter) ? 'active': ''}}">
<a href="javascript:void(0);" (click)="loadChapterPage(chapter.page, chapter.part)">{{chapter.title}}</a>
</li>
}
</ul>
</div>
} @else {
@for (chapterGroup of bookChapters; track chapterGroup.title + chapterGroup.children.length) {
<ul class="chapter-title">
@for (chapterGroup of displayedChapters(); track chapterGroup.title + chapterGroup.children.length) {
<ul [id]="`${$index}`" class="chapter-title">
<li class="{{isChapterSelected(chapterGroup) ? 'active': ''}}" (click)="loadChapterPage(chapterGroup.page, '')">
{{chapterGroup.title}}
</li>

View File

@ -2,32 +2,62 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
computed, effect,
EventEmitter,
inject,
model,
model, OnInit,
Output
} from '@angular/core';
import {BookChapterItem} from '../../_models/book-chapter-item';
import {TranslocoDirective} from "@jsverse/transloco";
import {LoadingComponent} from "../../../shared/loading/loading.component";
import {DOCUMENT} from "@angular/common";
@Component({
selector: 'app-table-of-contents',
templateUrl: './table-of-contents.component.html',
styleUrls: ['./table-of-contents.component.scss'],
imports: [TranslocoDirective],
imports: [TranslocoDirective, LoadingComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TableOfContentsComponent {
private readonly cdRef = inject(ChangeDetectorRef);
private readonly document = inject(DOCUMENT)
chapterId = model.required<number>();
pageNum = model.required<number>();
currentPageAnchor = model<string>();
chapters = model.required<Array<BookChapterItem>>();
loading = model.required<boolean>();
displayedChapters = computed(() => {
const chapters = this.chapters();
if (chapters.length === 1) {
return chapters[0].children;
}
return chapters;
});
isDisplayingChildrenOnly = computed(() => this.chapters().length === 1);
@Output() loadChapter: EventEmitter<{pageNum: number, part: string}> = new EventEmitter();
constructor() {
effect(() => {
const selectedChapterIdx = this.displayedChapters()
.findIndex(chapter => this.isChapterSelected(chapter));
if (selectedChapterIdx < 0) return;
setTimeout(() => {
const chapterElement = this.document.getElementById(`${selectedChapterIdx}`);
if (chapterElement) {
chapterElement.scrollIntoView({behavior: 'smooth'});
}
}, 10); // Some delay to allow the items to be rendered into the DOM
});
}
cleanIdSelector(id: string) {
const tokens = id.split('/');
@ -46,7 +76,7 @@ export class TableOfContentsComponent {
isChapterSelected(chapterGroup: BookChapterItem) {
const currentPageNum = this.pageNum();
const chapters = this.chapters();
const chapters = this.displayedChapters();
if (chapterGroup.page === currentPageNum) {
return true;

View File

@ -1,5 +1,5 @@
<div class="main-container container-fluid">
<ng-container *transloco="let t; read: 'bookmarks'">
<ng-container *transloco="let t; prefix: 'bookmarks'">
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
<h4 title>
{{t('title')}}

View File

@ -1,5 +1,5 @@
<div class="main-container container-fluid">
<ng-container *transloco="let t; read:'browse-genres'" >
<ng-container *transloco="let t; prefix:'browse-genres'" >
<app-side-nav-companion-bar [hasFilter]="false">
<h2 title>
<span>{{t('title')}}</span>

View File

@ -1,5 +1,5 @@
<div class="main-container container-fluid">
<ng-container *transloco="let t; read:'browse-people'" >
<ng-container *transloco="let t; prefix:'browse-people'" >
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
<h2 title>
<span>{{t('title')}}</span>

View File

@ -1,5 +1,5 @@
<div class="main-container container-fluid">
<ng-container *transloco="let t; read:'browse-tags'" >
<ng-container *transloco="let t; prefix:'browse-tags'" >
<app-side-nav-companion-bar [hasFilter]="false">
<h2 title>
<span>{{t('title')}}</span>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'bulk-add-to-collection'">
<ng-container *transloco="let t; prefix: 'bulk-add-to-collection'">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{t('title')}}</h4>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'edit-collection-tags'">
<ng-container *transloco="let t; prefix: 'edit-collection-tags'">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{t('title', {collectionName: tag.title})}}</h4>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'edit-series-modal'">
<ng-container *transloco="let t; prefix: 'edit-series-modal'">
@if (series) {
<div class="modal-container">
<div class="modal-header">

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'bulk-operations'">
<ng-container *transloco="let t; prefix: 'bulk-operations'">
@if (bulkSelectionService.selections$ | async; as selectionCount) {
@if (selectionCount > 0) {
<div class="bulk-select-container" [ngStyle]="{'margin-left': marginLeft + 'px', 'margin-right': marginRight + 'px'}">

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'card-detail-layout'">
<ng-container *transloco="let t; prefix: 'card-detail-layout'">
<app-loading [loading]="isLoading"></app-loading>
@if (header().length > 0) {

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'card-item'">
<ng-container *transloco="let t; prefix: 'card-item'">
<div class="card-item-container card {{selected ? 'selected-highlight' : ''}}">
<div class="overlay" (click)="handleClick($event)">
@if (total > 0 || suppressArchiveWarning) {

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'card-item'">
<ng-container *transloco="let t; prefix: 'card-item'">
<div class="card-item-container card position-relative {{selected ? 'selected-highlight' : ''}}" >
<div class="overlay" (click)="handleClick($event)">
@if (chapter.pages > 0 || suppressArchiveWarning) {

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'cover-image-chooser'">
<ng-container *transloco="let t; prefix: 'cover-image-chooser'">
<div class="container-fluid" style="padding-left: 0; padding-right: 0">
<form [formGroup]="form">
<ngx-file-drop (onFileDrop)="dropped($event)"

View File

@ -4,7 +4,6 @@ import {
Component,
EventEmitter,
inject,
Inject,
Input,
OnInit,
Output
@ -43,6 +42,7 @@ export class CoverImageChooserComponent implements OnInit {
public readonly toastr = inject(ToastrService);
public readonly uploadService = inject(UploadService);
private readonly colorscapeService = inject(ColorscapeService)
private readonly document = inject(DOCUMENT)
/**
* If buttons show under images to allow immediate selection of cover images.
@ -87,8 +87,6 @@ export class CoverImageChooserComponent implements OnInit {
acceptableExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif'].join(',');
mode: 'file' | 'url' | 'all' = 'all';
constructor(@Inject(DOCUMENT) private document: Document) { }
ngOnInit(): void {
this.form = this.fb.group({
coverImageUrl: new FormControl('', [])

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'download-indicator'">
<ng-container *transloco="let t; prefix: 'download-indicator'">
@if (download$ | async; as download) {
<span class="download">
<app-circular-loader [currentValue]="download.progress"></app-circular-loader>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'edit-chapter-progress'">
<ng-container *transloco="let t; prefix: 'edit-chapter-progress'">
<!-- <ngx-datatable-->
<!-- class="bootstrap"-->
<!-- [rows]="items.controls"-->

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'edit-series-relation'">
<ng-container *transloco="let t; prefix: 'edit-series-relation'">
<div class="container-fluid">
<p>

View File

@ -1,3 +1,3 @@
<ng-container *transloco="let t; read: 'entity-title'">
<ng-container *transloco="let t; prefix: 'entity-title'">
{{renderText | defaultValue}}
</ng-container>

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'external-series-card'">
<ng-container *transloco="let t; prefix: 'external-series-card'">
@if (data !== undefined) {
<div class="card-item-container card clickable position-relative">
<div class="overlay" (click)="handleClick()">

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'next-expected-card'">
<ng-container *transloco="let t; prefix: 'next-expected-card'">
<div class="card-item-container card">
<div class="overlay">
<app-image [styles]="{'border-radius': '.25rem .25rem 0 0'}" height="232.91px" width="160px" classes="extreme-blur"

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'card-item'">
<ng-container *transloco="let t; prefix: 'card-item'">
<div class="card-item-container card {{selected ? 'selected-highlight' : ''}}">
<div class="overlay" (click)="handleClick($event)">
@if(entity.coverImage) {

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'card-item'">
<ng-container *transloco="let t; prefix: 'card-item'">
<div class="card-item-container card position-relative {{selected ? 'selected-highlight' : ''}}">
<div class="overlay" (click)="handleClick()">
@if (series.pages > 0) {

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'card-item'">
<ng-container *transloco="let t; prefix: 'card-item'">
<div class="card-item-container card {{selected ? 'selected-highlight' : ''}}" >
<div class="overlay position-relative" (click)="handleClick($event)">
@if (volume.pages > 0 || suppressArchiveWarning) {

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'carousel-reel'">
<ng-container *transloco="let t; prefix: 'carousel-reel'">
@if (alwaysShow || items && items.length > 0) {
<div class="carousel-container mb-3">

View File

@ -47,7 +47,7 @@
}
.title-icon {
color: var(--body-text-color);
font-size: 0.9rem;
cursor: pointer;
color: var(--body-text-color);
font-size: 0.9rem;
cursor: pointer;
}

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'series-detail'">
<ng-container *transloco="let t; prefix: 'series-detail'">
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid" #scrollingBlock>

View File

@ -1,5 +1,5 @@
<div class="main-container container-fluid">
<ng-container *transloco="let t; read: 'all-collections'">
<ng-container *transloco="let t; prefix: 'all-collections'">
<app-side-nav-companion-bar [hasFilter]="false" (filterOpen)="filterOpen.emit($event)">
<h4 title>{{t('title')}}</h4>
<h5 subtitle>{{t('item-count', {num: collections.length | number})}}</h5>

View File

@ -1,5 +1,5 @@
<div class="main-container container-fluid">
<ng-container *transloco="let t; read: 'collection-detail'">
<ng-container *transloco="let t; prefix: 'collection-detail'">
<div #companionBar>
@if (series) {
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'collection-owner'">
<ng-container *transloco="let t; prefix: 'collection-owner'">
@if (accountService.currentUser$ | async; as user) {
<div class="fw-light text-accent">
{{t('collection-created-label', {owner: collection.owner})}}

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'import-mal-collection-modal'">
<ng-container *transloco="let t; prefix: 'import-mal-collection-modal'">
<p>{{t('description')}}</p>
<ul>

View File

@ -1,7 +1,7 @@
<div class="main-container">
<app-side-nav-companion-bar></app-side-nav-companion-bar>
<ng-container *transloco="let t; read: 'dashboard'">
<ng-container *transloco="let t; prefix: 'dashboard'">
@if (libraries$ | async; as libraries) {
@if (libraries.length === 0) {
@if (accountService.isAdmin$ | async; as isAdmin) {

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'infinite-scroller'">
<ng-container *transloco="let t; prefix: 'infinite-scroller'">
@if (showDebugBar()) {
<div class="fixed-top overlay">

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'manga-reader'">
<ng-container *transloco="let t; prefix: 'manga-reader'">
<div class="reader" #reader [ngStyle]="{overflow: (isFullscreen ? 'auto' : 'visible')}">
@if(debugMode) {
<div class="fixed-top overlay">

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'metadata-builder'">
<ng-container *transloco="let t; prefix: 'metadata-builder'">
@if (filter) {
<form [formGroup]="formGroup">

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'metadata-filter-row'">
<ng-container *transloco="let t; prefix: 'metadata-filter-row'">
<form [formGroup]="formGroup">
<div class="row g-0">
<div class="col-md-3 me-2 col-10 mb-2">

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'metadata-filter'">
<ng-container *transloco="let t; prefix: 'metadata-filter'">
@if (toggleService.toggleState$ | async; as isOpen) {
@if (utilityService.getActiveBreakpoint(); as activeBreakpoint) {
@if (activeBreakpoint >= Breakpoint.Tablet) {

View File

@ -1,4 +1,4 @@
<ng-container *transloco="let t; read: 'events-widget'">
<ng-container *transloco="let t; prefix: 'events-widget'">
@if (accountService.isAdmin$ | async) {
@if (downloadService.activeDownloads$ | async; as activeDownloads) {

Some files were not shown because too many files have changed in this diff Show More