mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-09-29 15:30:50 -04:00
More Epub Fixes (#4017)
Co-authored-by: Amelia <77553571+Fesaa@users.noreply.github.com>
This commit is contained in:
parent
b6c5987185
commit
8def92ff73
@ -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();
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -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; }
|
||||
|
@ -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()
|
||||
|
@ -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
2047
UI/Web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -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()))
|
||||
|
@ -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);
|
||||
|
@ -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">
|
||||
|
@ -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}}"
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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')">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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')"
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
||||
|
@ -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"> {{t('invite')}}</span>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}}
|
||||
|
@ -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')}}
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -32,6 +32,7 @@
|
||||
[selected]="slot.slotNumber === selectedSlotIndex()"
|
||||
(selectPicker)="selectSlot(index, slot)"
|
||||
[editMode]="isEditMode()"
|
||||
[canChangeEditMode]="canChangeEditMode()"
|
||||
(editModeChange)="isEditMode.set($event)"
|
||||
[first]="$first"
|
||||
[last]="$last"
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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">
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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')}}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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'}">
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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)"
|
||||
|
@ -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('', [])
|
||||
|
@ -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>
|
||||
|
@ -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"-->
|
||||
|
@ -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>
|
||||
|
@ -1,3 +1,3 @@
|
||||
<ng-container *transloco="let t; read: 'entity-title'">
|
||||
<ng-container *transloco="let t; prefix: 'entity-title'">
|
||||
{{renderText | defaultValue}}
|
||||
</ng-container>
|
||||
|
@ -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()">
|
||||
|
@ -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"
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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">
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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})}}
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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) {
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user