Random Fixes (#3549)

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
Joe Milazzo 2025-02-15 17:25:18 -06:00 committed by GitHub
parent ea81a2f432
commit 39726f8c4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 425 additions and 107 deletions

View file

@ -109,6 +109,7 @@ export class ReaderService {
return this.httpClient.post<PageBookmark[]>(this.baseUrl + 'reader/all-bookmarks', filter);
}
getBookmarks(chapterId: number) {
return this.httpClient.get<PageBookmark[]>(this.baseUrl + 'reader/chapter-bookmarks?chapterId=' + chapterId);
}

View file

@ -1,5 +1,5 @@
<ng-container *transloco="let t; read: 'related-tab'">
<div style="padding-bottom: 1rem;">
<div class="pb-2">
@if (relations.length > 0) {
<app-carousel-reel [items]="relations" [title]="t('relations-title')">
<ng-template #carouselItem let-item>
@ -30,5 +30,18 @@
</ng-template>
</app-carousel-reel>
}
@if (bookmarks.length > 0) {
<app-carousel-reel [items]="bookmarks" [title]="t('bookmarks-title')">
<ng-template #carouselItem let-item>
<app-card-item [entity]="item" [title]="t('bookmarks-title')" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"
[suppressArchiveWarning]="true"
[linkUrl]="'/library/' + libraryId + '/series/' + item.seriesId + '/manga/0?bookmarkMode=true'"
(clicked)="viewBookmark(item)"
[count]="bookmarks.length"
[allowSelection]="false"></app-card-item>
</ng-template>
</app-carousel-reel>
}
</div>
</ng-container>

View file

@ -1,4 +1,4 @@
import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core';
import {ChangeDetectionStrategy, Component, inject, Input, OnInit} from '@angular/core';
import {ReadingList} from "../../_models/reading-list";
import {CardItemComponent} from "../../cards/card-item/card-item.component";
import {CarouselReelComponent} from "../../carousel/_components/carousel-reel/carousel-reel.component";
@ -9,6 +9,7 @@ import {Router} from "@angular/router";
import {SeriesCardComponent} from "../../cards/series-card/series-card.component";
import {Series} from "../../_models/series";
import {RelationKind} from "../../_models/series-detail/relation-kind";
import {PageBookmark} from "../../_models/readers/page-bookmark";
export interface RelatedSeriesPair {
series: Series;
@ -28,7 +29,7 @@ export interface RelatedSeriesPair {
styleUrl: './related-tab.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RelatedTabComponent {
export class RelatedTabComponent implements OnInit {
protected readonly imageService = inject(ImageService);
protected readonly router = inject(Router);
@ -36,6 +37,12 @@ export class RelatedTabComponent {
@Input() readingLists: Array<ReadingList> = [];
@Input() collections: Array<UserCollection> = [];
@Input() relations: Array<RelatedSeriesPair> = [];
@Input() bookmarks: Array<PageBookmark> = [];
@Input() libraryId!: number;
ngOnInit() {
console.log('bookmarks: ', this.bookmarks);
}
openReadingList(readingList: ReadingList) {
this.router.navigate(['lists', readingList.id]);
@ -45,4 +52,8 @@ export class RelatedTabComponent {
this.router.navigate(['collections', collection.id]);
}
viewBookmark(bookmark: PageBookmark) {
this.router.navigate(['library', this.libraryId, 'series', bookmark.seriesId, 'manga', 0], {queryParams: {incognitoMode: false, bookmarkMode: true}});
}
}

View file

@ -40,6 +40,18 @@
font-display: swap;
}
@font-face {
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;
}
:root {
--br-actionbar-button-text-color: #6c757d;
--accordion-body-bg-color: black;

View file

@ -103,7 +103,7 @@ export const BookWhiteTheme = `
.book-content *:not(input), .book-content *:not(select), .book-content *:not(code), .book-content *:not(:link), .book-content *:not(.ngx-toastr) {
color: black !important;
color: black;
}
.book-content code {
@ -125,7 +125,7 @@ export const BookWhiteTheme = `
box-shadow: none;
text-shadow: none;
border-radius: unset;
color: #dcdcdc !important;
color: #dcdcdc;
}
.book-content :visited, .book-content :visited *, .book-content :visited *[class] {

View file

@ -28,7 +28,7 @@ export class BookService {
getFontFamilies(): Array<FontFamily> {
return [{title: 'default', family: 'default'}, {title: 'EBGaramond', family: 'EBGaramond'}, {title: 'Fira Sans', family: 'Fira_Sans'},
{title: 'Lato', family: 'Lato'}, {title: 'Libre Baskerville', family: 'Libre_Baskerville'}, {title: 'Merriweather', family: 'Merriweather'},
{title: 'Nanum Gothic', family: 'Nanum_Gothic'}, {title: 'RocknRoll One', family: 'RocknRoll_One'}, {title: 'Open Dyslexic', family: 'OpenDyslexic2'}];
{title: 'Nanum Gothic', family: 'Nanum_Gothic'}, {title: 'Open Dyslexic', family: 'OpenDyslexic2'}, {title: 'RocknRoll One', family: 'RocknRoll_One'}, {title: 'Fast Font Serif (Bionic)', family: 'FastFontSerif'}, {title: 'Fast Font Sans (Bionic)', family: 'FastFontSans'}];
}
getBookChapters(chapterId: number) {

View file

@ -7,27 +7,29 @@
<h5 subtitle>{{t('series-count', {num: series.length | number})}}</h5>
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout *ngIf="filter"
[isLoading]="loadingBookmarks"
[items]="series"
[filterSettings]="filterSettings"
[trackByIdentity]="trackByIdentity"
[refresh]="refresh"
[jumpBarKeys]="jumpbarKeys"
(applyFilter)="updateFilter($event)"
>
<ng-template #cardItem let-item let-position="idx">
<app-card-item [entity]="item" [title]="item.name" [imageUrl]="imageService.getSeriesCoverImage(item.id)"
[suppressArchiveWarning]="true" (clicked)="viewBookmarks(item)" [count]="seriesIds[item.id]" [allowSelection]="true"
[actions]="actions"
[selected]="bulkSelectionService.isCardSelected('bookmark', position)"
(selection)="bulkSelectionService.handleCardSelection('bookmark', position, series.length, $event)"
></app-card-item>
</ng-template>
@if (filter) {
<app-card-detail-layout
[isLoading]="loadingBookmarks"
[items]="series"
[filterSettings]="filterSettings"
[trackByIdentity]="trackByIdentity"
[refresh]="refresh"
[jumpBarKeys]="jumpbarKeys"
(applyFilter)="updateFilter($event)"
>
<ng-template #cardItem let-item let-position="idx">
<app-card-item [entity]="item" [title]="item.name" [imageUrl]="imageService.getSeriesCoverImage(item.id)"
[suppressArchiveWarning]="true" (clicked)="viewBookmarks(item)" [count]="seriesIds[item.id]" [allowSelection]="true"
[actions]="actions"
[selected]="bulkSelectionService.isCardSelected('bookmark', position)"
(selection)="bulkSelectionService.handleCardSelection('bookmark', position, series.length, $event)"
></app-card-item>
</ng-template>
<ng-template #noData>
{{t('no-data')}} <a [href]="WikiLink.Bookmarks" rel="noopener noreferrer" target="_blank">{{t('no-data-2')}}<i class="fa fa-external-link-alt ms-1" aria-hidden="true"></i></a>
</ng-template>
</app-card-detail-layout>
<ng-template #noData>
{{t('no-data')}} <a [href]="WikiLink.Bookmarks" rel="noopener noreferrer" target="_blank">{{t('no-data-2')}}<i class="fa fa-external-link-alt ms-1" aria-hidden="true"></i></a>
</ng-template>
</app-card-detail-layout>
}
</ng-container>
</div>
</div>

View file

@ -15,7 +15,6 @@ import { FilterSettings } from 'src/app/metadata-filter/filter-settings';
import { ConfirmService } from 'src/app/shared/confirm.service';
import {DownloadService} from 'src/app/shared/_services/download.service';
import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service';
import { KEY_CODES } from 'src/app/shared/_services/utility.service';
import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
import { PageBookmark } from 'src/app/_models/readers/page-bookmark';
import { Pagination } from 'src/app/_models/pagination';
@ -103,13 +102,13 @@ export class BookmarksComponent implements OnInit {
async handleAction(action: ActionItem<Series>, series: Series) {
switch (action.action) {
case(Action.Delete):
this.clearBookmarks(series);
await this.clearBookmarks(series);
break;
case(Action.DownloadBookmark):
this.downloadBookmarks(series);
break;
case(Action.ViewSeries):
this.router.navigate(['library', series.libraryId, 'series', series.id]);
await this.router.navigate(['library', series.libraryId, 'series', series.id]);
break;
default:
break;

View file

@ -9,7 +9,7 @@
<div class="progress-banner">
@if (read > 0 && read < total && total > 0 && read !== total) {
<p ngbTooltip="{{((read / total) * 100) | number:'1.0-1'}}% Read">
<p ngbTooltip="{{((read / total) * 100) | number:'1.0-1'}}% Read" container="body">
<ngb-progressbar type="primary" height="5px" [value]="read" [max]="total"></ngb-progressbar>
</p>
}

View file

@ -9,7 +9,7 @@
<div class="progress-banner">
@if (chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages && chapter.pages > 0 && chapter.pagesRead !== chapter.pages) {
<p ngbTooltip="{{((chapter.pagesRead / chapter.pages) * 100) | number:'1.0-1'}}% Read">
<p ngbTooltip="{{((chapter.pagesRead / chapter.pages) * 100) | number:'1.0-1'}}% Read" container="body">
<ngb-progressbar type="primary" height="5px" [value]="chapter.pagesRead" [max]="chapter.pages"></ngb-progressbar>
</p>
}
@ -37,7 +37,7 @@
</div>
}
@if (chapter.files.length > 1) {
@if (chapter.files.length > 1 && chapter.files[0].format !== MangaFormat.IMAGE) {
<div class="count">
<span class="badge bg-primary">{{chapter.files.length}}</span>
</div>

View file

@ -35,6 +35,7 @@ import {ReaderService} from "../../_services/reader.service";
import {LibraryType} from "../../_models/library/library";
import {Device} from "../../_models/device/device";
import {ActionService} from "../../_services/action.service";
import {MangaFormat} from "../../_models/manga-format";
@Component({
selector: 'app-chapter-card',
@ -49,8 +50,7 @@ import {ActionService} from "../../_services/action.service";
EntityTitleComponent,
CardActionablesComponent,
RouterLink,
TranslocoDirective,
DefaultValuePipe
TranslocoDirective
],
templateUrl: './chapter-card.component.html',
styleUrl: './chapter-card.component.scss',
@ -213,4 +213,5 @@ export class ChapterCardComponent implements OnInit {
protected readonly LibraryType = LibraryType;
protected readonly MangaFormat = MangaFormat;
}

View file

@ -9,7 +9,7 @@
<div class="progress-banner">
@if (series.pagesRead > 0 && series.pagesRead < series.pages && series.pages > 0 && series.pagesRead !== series.pages) {
<p ngbTooltip="{{((series.pagesRead / series.pages) * 100) | number:'1.0-1'}}%">
<p ngbTooltip="{{((series.pagesRead / series.pages) * 100) | number:'1.0-1'}}%" container="body">
<ngb-progressbar type="primary" height="5px" [value]="series.pagesRead" [max]="series.pages"></ngb-progressbar>
</p>
}

View file

@ -9,7 +9,7 @@
<div class="progress-banner">
@if (volume.pagesRead > 0 && volume.pagesRead < volume.pages && volume.pages > 0 && volume.pagesRead !== volume.pages) {
<p ngbTooltip="{{((volume.pagesRead / volume.pages) * 100) | number:'1.0-1'}}% Read">
<p ngbTooltip="{{((volume.pagesRead / volume.pages) * 100) | number:'1.0-1'}}% Read" container="body">
<ngb-progressbar type="primary" height="5px" [value]="volume.pagesRead" [max]="volume.pages"></ngb-progressbar>
</p>
}

View file

@ -199,7 +199,9 @@
<div class="{{SplitIconClass}}"></div>
</div>
<select class="form-control" id="page-splitting" formControlName="pageSplitOption">
<option *ngFor="let opt of pageSplitOptionsTranslated" [value]="opt.value">{{opt.text}}</option>
@for (opt of pageSplitOptionsTranslated; track opt.value) {
<option [value]="opt.value">{{opt.text}}</option>
}
</select>
</div>
@ -216,42 +218,45 @@
<div class="row mb-2">
<div class="col-md-6 col-sm-12">
<label for="layout-mode" class="form-label">Layout Mode</label>&nbsp;
<ng-container [ngSwitch]="layoutMode">
<ng-container *ngSwitchCase="LayoutMode.Single">
@switch (layoutMode) {
@case (LayoutMode.Single) {
<div class="split-double">
<span class="fa-stack fa-1x">
<i class="fa-regular fa-square-full fa-stack-2x"></i>
<i class="fa fa-image fa-stack-1x"></i>
</span>
<span class="fa-stack fa-1x">
<i class="fa-regular fa-square-full fa-stack-2x"></i>
<i class="fa fa-image fa-stack-1x"></i>
</span>
</div>
</ng-container>
<ng-container *ngSwitchCase="LayoutMode.Double">
}
@case (LayoutMode.Double) {
<div class="split-double">
<span class="fa-stack fa-1x">
<i class="fa-regular fa-square-full fa-stack-2x"></i>
<i class="fab fa-1 fa-stack-1x"></i>
</span>
<span class="fa-stack fa-1x">
<i class="fa-regular fa-square-full fa-stack-2x"></i>
<i class="fab fa-1 fa-stack-1x"></i>
</span>
<span class="fa-stack fa right">
<i class="fa-regular fa-square-full fa-stack-2x"></i>
<i class="fab fa-2 fa-stack-1x"></i>
</span>
<i class="fa-regular fa-square-full fa-stack-2x"></i>
<i class="fab fa-2 fa-stack-1x"></i>
</span>
</div>
</ng-container>
<ng-container *ngSwitchCase="LayoutMode.DoubleReversed">
}
@case (LayoutMode.DoubleReversed) {
<div class="split-double">
<span class="fa-stack fa-1x">
<i class="fa-regular fa-square-full fa-stack-2x"></i>
<i class="fab fa-2 fa-stack-1x"></i>
</span>
<span class="fa-stack fa-1x">
<i class="fa-regular fa-square-full fa-stack-2x"></i>
<i class="fab fa-2 fa-stack-1x"></i>
</span>
<span class="fa-stack fa right">
<i class="fa-regular fa-square-full fa-stack-2x"></i>
<i class="fab fa-1 fa-stack-1x"></i>
</span>
<i class="fa-regular fa-square-full fa-stack-2x"></i>
<i class="fab fa-1 fa-stack-1x"></i>
</span>
</div>
</ng-container>
</ng-container>
}
}
<select class="form-control" id="layout-mode" formControlName="layoutMode">
<option [value]="opt.value" *ngFor="let opt of layoutModesTranslated">{{opt.text}}</option>
@for (opt of layoutModesTranslated; track opt.value) {
<option [value]="opt.value">{{opt.text}}</option>
}
</select>
</div>
<div class="col-md-3 col-sm-12">

View file

@ -8,12 +8,11 @@ import {
EventEmitter,
HostListener,
inject,
Inject,
OnDestroy,
OnInit,
ViewChild
} from '@angular/core';
import {AsyncPipe, DOCUMENT, NgClass, NgFor, NgStyle, NgSwitch, NgSwitchCase, PercentPipe} from '@angular/common';
import {AsyncPipe, NgClass, NgStyle, PercentPipe} from '@angular/common';
import {ActivatedRoute, Router} from '@angular/router';
import {
BehaviorSubject,
@ -33,7 +32,7 @@ import {
import {ChangeContext, LabelType, NgxSliderModule, Options} from '@angular-slider/ngx-slider';
import {animate, state, style, transition, trigger} from '@angular/animations';
import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {NgbModal, NgbProgressbar} from '@ng-bootstrap/ng-bootstrap';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import {ToastrService} from 'ngx-toastr';
import {ShortcutsModalComponent} from 'src/app/reader-shared/_modals/shortcuts-modal/shortcuts-modal.component';
import {Stack} from 'src/app/shared/data-structures/stack';
@ -126,7 +125,7 @@ enum KeyDirection {
standalone: true,
imports: [NgStyle, LoadingComponent, SwipeDirective, CanvasRendererComponent, SingleRendererComponent,
DoubleRendererComponent, DoubleReverseRendererComponent, DoubleNoCoverRendererComponent, InfiniteScrollerComponent,
NgxSliderModule, ReactiveFormsModule, NgFor, NgSwitch, NgSwitchCase, FittingIconPipe, ReaderModeIconPipe,
NgxSliderModule, ReactiveFormsModule, FittingIconPipe, ReaderModeIconPipe,
FullscreenIconPipe, TranslocoDirective, PercentPipe, NgClass, AsyncPipe, DblClickDirective]
})
export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
@ -275,7 +274,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
step: 1,
boundPointerLabels: true,
showSelectionBar: true,
translate: (value: number, label: LabelType) => {
translate: (_: number, label: LabelType) => {
if (label == LabelType.Floor) {
return 1 + '';
} else if (label === LabelType.Ceil) {
@ -467,7 +466,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
constructor(@Inject(DOCUMENT) private document: Document) {
constructor() {
this.navService.hideNavBar();
this.navService.hideSideNav();
this.cdRef.markForCheck();
@ -784,6 +783,17 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return pageNum;
}
switchToWebtoonReaderIfPagesLikelyWebtoon() {
if (this.readerMode === ReaderMode.Webtoon) return;
if (this.mangaReaderService.shouldBeWebtoonMode()) {
this.readerMode = ReaderMode.Webtoon;
this.toastr.info(translate('manga-reader.webtoon-override'));
this.readerModeSubject.next(this.readerMode);
this.cdRef.markForCheck();
}
}
disableDoubleRendererIfScreenTooSmall() {
if (window.innerWidth > window.innerHeight) {
this.generalSettingsForm.get('layoutMode')?.enable();
@ -991,6 +1001,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.inSetup = false;
this.disableDoubleRendererIfScreenTooSmall();
this.switchToWebtoonReaderIfPagesLikelyWebtoon();
// From bookmarks, create map of pages to make lookup time O(1)

View file

@ -6,6 +6,7 @@ import { ChapterInfo } from '../_models/chapter-info';
import { DimensionMap } from '../_models/file-dimension';
import { FITTING_OPTION } from '../_models/reader-enums';
import { BookmarkInfo } from 'src/app/_models/manga-reader/bookmark-info';
import {ReaderMode} from "../../_models/preferences/reader-mode";
@Injectable({
providedIn: 'root'
@ -150,6 +151,35 @@ export class ManagaReaderService {
}
}
/**
* If the page dimensions are all "webtoon-like", then reader mode will be converted for the user
*/
shouldBeWebtoonMode() {
const pages = Object.values(this.pageDimensions);
let webtoonScore = 0;
pages.forEach(info => {
const aspectRatio = info.height / info.width;
let score = 0;
// Strong webtoon indicator: If aspect ratio is at least 2:1
if (aspectRatio >= 2) {
score += 1;
}
// Boost score if width is small (≤ 800px, common in webtoons)
if (info.width <= 800) {
score += 0.5; // Adjust weight as needed
}
webtoonScore += score;
});
// If at least 50% of the pages fit the webtoon criteria, switch to Webtoon mode.
return webtoonScore / pages.length >= 0.5;
}
applyBookmarkEffect(elements: Array<Element | ElementRef>) {
if (elements.length > 0) {
@ -160,7 +190,4 @@ export class ManagaReaderService {
}
}
}

View file

@ -266,15 +266,19 @@
</li>
}
@if (hasRelations || readingLists.length > 0 || collections.length > 0) {
@if (hasRelations || readingLists.length > 0 || collections.length > 0 || bookmarks.length > 0) {
<li [ngbNavItem]="TabID.Related">
<a ngbNavLink>
{{t(TabID.Related)}}
<span class="badge rounded-pill text-bg-secondary">{{relations.length + readingLists.length + collections.length}}</span>
<span class="badge rounded-pill text-bg-secondary">{{relations.length + readingLists.length + collections.length + (bookmarks.length > 0 ? 1 : 0)}}</span>
</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabID.Related; prefetch on idle) {
<app-related-tab [readingLists]="readingLists" [collections]="collections" [relations]="relations"></app-related-tab>
<app-related-tab [readingLists]="readingLists"
[collections]="collections"
[relations]="relations"
[libraryId]="libraryId"
[bookmarks]="bookmarks"></app-related-tab>
}
</ng-template>
</li>

View file

@ -1,11 +1,4 @@
import {
AsyncPipe,
DOCUMENT,
Location,
NgClass,
NgStyle,
NgTemplateOutlet
} from '@angular/common';
import {AsyncPipe, DOCUMENT, Location, NgClass, NgStyle, NgTemplateOutlet} from '@angular/common';
import {
AfterContentChecked,
ChangeDetectionStrategy,
@ -121,7 +114,7 @@ import {UserCollection} from "../../../_models/collection-tag";
import {CoverImageComponent} from "../../../_single-module/cover-image/cover-image.component";
import {DefaultModalOptions} from "../../../_models/default-modal-options";
import {LicenseService} from "../../../_services/license.service";
import {PageBookmark} from "../../../_models/readers/page-bookmark";
enum TabID {
@ -233,6 +226,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
reviews: Array<UserReview> = [];
plusReviews: Array<UserReview> = [];
bookmarks: Array<PageBookmark> = [];
ratings: Array<Rating> = [];
libraryType: LibraryType = LibraryType.Manga;
seriesMetadata: SeriesMetadata | null = null;
@ -712,7 +706,24 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
this.collectionTagService.allCollectionsForSeries(seriesId, false).subscribe(tags => {
this.collections = tags;
this.cdRef.markForCheck();
})
});
this.readerService.getBookmarksForSeries(seriesId).subscribe(bookmarks => {
if (bookmarks.length > 0) {
this.bookmarks = Object.values(
bookmarks.reduce((acc, bookmark) => {
if (!acc[bookmark.seriesId]) {
acc[bookmark.seriesId] = bookmark; // Select the first one per seriesId
}
return acc;
}, {} as Record<number, PageBookmark>)
);
} else {
this.bookmarks = [];
}
this.cdRef.markForCheck();
});
this.readerService.getTimeLeft(seriesId).subscribe((timeLeft) => {
this.readingTimeLeft = timeLeft;