UX Pass 5 (#3128)
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
dbc4f35107
commit
c93af3e56f
126 changed files with 1989 additions and 2877 deletions
|
|
@ -629,7 +629,7 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
await this.actionService.refreshSeriesMetadata(this.series);
|
||||
break;
|
||||
case Action.GenerateColorScape:
|
||||
await this.actionService.refreshSeriesMetadata(this.series, undefined, false);
|
||||
await this.actionService.refreshSeriesMetadata(this.series, undefined, false, true);
|
||||
break;
|
||||
case Action.AnalyzeFiles:
|
||||
this.actionService.analyzeFilesForSeries(this.series);
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
<ng-container *transloco="let t; read: 'card-detail-drawer'">
|
||||
<div class="offcanvas-header">
|
||||
<h5 class="offcanvas-title">
|
||||
<span class="modal-title" id="modal-basic-title">
|
||||
<app-entity-title [libraryType]="libraryType" [entity]="data" [seriesName]="parentName"></app-entity-title>
|
||||
</span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close text-reset" aria-label="Close" (click)="activeOffcanvas.dismiss()"></button>
|
||||
</div>
|
||||
|
||||
<div class="offcanvas-body pb-3">
|
||||
<div class="d-flex">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="vertical" style="max-width: 135px;">
|
||||
<li [ngbNavItem]="tabs[TabID.General]">
|
||||
<a ngbNavLink>{{t(tabs[TabID.General].title)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="container-fluid" style="overflow: auto">
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="d-none d-md-block col-md-2 col-lg-1">
|
||||
<app-image class="me-2" width="74px" [imageUrl]="coverImageUrl"></app-image>
|
||||
</div>
|
||||
<div class="col-md-10 col-lg-11">
|
||||
<ng-container *ngIf="summary.length > 0; else noSummary">
|
||||
<app-read-more [text]="summary" [maxLength]="250"></app-read-more>
|
||||
</ng-container>
|
||||
<ng-template #noSummary>
|
||||
{{t('no-summary')}}
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-entity-info-cards [entity]="data" [libraryId]="libraryId"></app-entity-info-cards>
|
||||
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[TabID.Metadata]">
|
||||
<a ngbNavLink>{{t(tabs[TabID.Metadata].title)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-chapter-metadata-detail [chapter]="chapter"></app-chapter-metadata-detail>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[TabID.Progress]">
|
||||
<a ngbNavLink>{{t(tabs[TabID.Progress].title)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-edit-chapter-progress [chapter]="chapter"></app-edit-chapter-progress>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[TabID.Cover]" [disabled]="(accountService.isAdmin$ | async) === false">
|
||||
<a ngbNavLink>{{t(tabs[TabID.Cover].title)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateCoverImageIndex($event)"
|
||||
(selectedBase64Url)="applyCoverImage($event)" [showReset]="chapter.coverImageLocked"
|
||||
(resetClicked)="resetCoverImage()"></app-cover-image-chooser>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="tabs[TabID.Files]" [disabled]="(accountService.isAdmin$ | async) === false">
|
||||
<a ngbNavLink>{{t(tabs[TabID.Files].title)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
@if (!utilityService.isChapter(data)) {
|
||||
<h4>{{utilityService.formatChapterName(libraryType) + 's'}}</h4>
|
||||
}
|
||||
<ul class="list-unstyled">
|
||||
<li class="d-flex my-4" *ngFor="let chapter of chapters">
|
||||
<a (click)="readChapter(chapter)" href="javascript:void(0);" [title]="t('read')">
|
||||
<app-image class="me-2" width="74px" [imageUrl]="imageService.getChapterCoverImage(chapter.id)"></app-image>
|
||||
</a>
|
||||
<div class="flex-grow-1">
|
||||
<h5 class="mt-0 mb-1">
|
||||
<span>
|
||||
<span>
|
||||
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions"
|
||||
[labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>
|
||||
<ng-container *ngIf="chapter.minNumber !== LooseLeafOrSpecialNumber; else specialHeader">
|
||||
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
|
||||
</ng-container>
|
||||
</span>
|
||||
<span class="badge bg-primary rounded-pill ms-1">
|
||||
<span *ngIf="chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages">{{chapter.pagesRead}} / {{chapter.pages}}</span>
|
||||
<span *ngIf="chapter.pagesRead === 0">{{t('unread') | uppercase}}</span>
|
||||
<span *ngIf="chapter.pagesRead === chapter.pages">{{t('read') | uppercase}}</span>
|
||||
</span>
|
||||
</span>
|
||||
<ng-template #specialHeader>{{t('files')}}</ng-template>
|
||||
</h5>
|
||||
<ul class="list-group">
|
||||
@for (file of chapter.files; track file.id) {
|
||||
<li class="list-group-item no-hover">
|
||||
<span>{{file.filePath}}</span>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
{{t('pages')}} {{file.pages | number:''}}
|
||||
</div>
|
||||
@if (data.hasOwnProperty('created')) {
|
||||
<div class="col">
|
||||
{{t('added')}} {{file.created | date: 'short' | defaultDate}}
|
||||
</div>
|
||||
}
|
||||
<div class="col">
|
||||
{{t('size')}} {{file.bytes | bytes}}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="nav" class="tab-content {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'mt-3' : 'ms-4 flex-fill'}}"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
.hide-if-empty:empty {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.offcanvas-body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.offcanvas-header {
|
||||
padding: 1rem 1rem 0;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
overflow: auto;
|
||||
height: calc(40vh - (46px + 1rem)); // drawer height - offcanvas heading height
|
||||
}
|
||||
|
||||
.h6 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
|
@ -1,278 +0,0 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
inject,
|
||||
Input,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
NgbActiveOffcanvas,
|
||||
NgbNav,
|
||||
NgbNavContent,
|
||||
NgbNavItem,
|
||||
NgbNavLink,
|
||||
NgbNavOutlet
|
||||
} from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { DownloadService } from 'src/app/shared/_services/download.service';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import {Chapter, LooseLeafOrDefaultNumber} from 'src/app/_models/chapter';
|
||||
import { Device } from 'src/app/_models/device/device';
|
||||
import { LibraryType } from 'src/app/_models/library/library';
|
||||
import { MangaFile } from 'src/app/_models/manga-file';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import { Volume } from 'src/app/_models/volume';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { ActionItem, ActionFactoryService, Action } from 'src/app/_services/action-factory.service';
|
||||
import { ActionService } from 'src/app/_services/action.service';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { LibraryService } from 'src/app/_services/library.service';
|
||||
import { ReaderService } from 'src/app/_services/reader.service';
|
||||
import { UploadService } from 'src/app/_services/upload.service';
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {EntityTitleComponent} from "../entity-title/entity-title.component";
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {ReadMoreComponent} from "../../shared/read-more/read-more.component";
|
||||
import {EntityInfoCardsComponent} from "../entity-info-cards/entity-info-cards.component";
|
||||
import {CoverImageChooserComponent} from "../cover-image-chooser/cover-image-chooser.component";
|
||||
import {ChapterMetadataDetailComponent} from "../chapter-metadata-detail/chapter-metadata-detail.component";
|
||||
import {DefaultDatePipe} from "../../_pipes/default-date.pipe";
|
||||
import {BytesPipe} from "../../_pipes/bytes.pipe";
|
||||
import {BadgeExpanderComponent} from "../../shared/badge-expander/badge-expander.component";
|
||||
import {TagBadgeComponent} from "../../shared/tag-badge/tag-badge.component";
|
||||
import {PersonBadgeComponent} from "../../shared/person-badge/person-badge.component";
|
||||
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
||||
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
|
||||
import {EditChapterProgressComponent} from "../edit-chapter-progress/edit-chapter-progress.component";
|
||||
import {CarouselTabsComponent} from "../../carousel/_components/carousel-tabs/carousel-tabs.component";
|
||||
import {CarouselTabComponent} from "../../carousel/_components/carousel-tab/carousel-tab.component";
|
||||
|
||||
enum TabID {
|
||||
General = 0,
|
||||
Metadata = 1,
|
||||
Cover = 2,
|
||||
Progress = 3,
|
||||
Files = 4
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-card-detail-drawer',
|
||||
standalone: true,
|
||||
imports: [CommonModule, EntityTitleComponent, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, ImageComponent, ReadMoreComponent, EntityInfoCardsComponent, CoverImageChooserComponent, ChapterMetadataDetailComponent, CardActionablesComponent, DefaultDatePipe, BytesPipe, NgbNavOutlet, BadgeExpanderComponent, TagBadgeComponent, PersonBadgeComponent, TranslocoDirective, EditChapterProgressComponent, CarouselTabsComponent, CarouselTabComponent],
|
||||
templateUrl: './card-detail-drawer.component.html',
|
||||
styleUrls: ['./card-detail-drawer.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CardDetailDrawerComponent implements OnInit {
|
||||
|
||||
protected readonly utilityService = inject(UtilityService);
|
||||
protected readonly imageService = inject(ImageService);
|
||||
private readonly uploadService = inject(UploadService);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
protected readonly accountService = inject(AccountService);
|
||||
private readonly actionFactoryService = inject(ActionFactoryService);
|
||||
private readonly actionService = inject(ActionService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly libraryService = inject(LibraryService);
|
||||
private readonly readerService = inject(ReaderService);
|
||||
protected readonly activeOffcanvas = inject(NgbActiveOffcanvas);
|
||||
private readonly downloadService = inject(DownloadService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
protected readonly MangaFormat = MangaFormat;
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
protected readonly LibraryType = LibraryType;
|
||||
protected readonly TabID = TabID;
|
||||
protected readonly LooseLeafOrSpecialNumber = LooseLeafOrDefaultNumber;
|
||||
|
||||
@Input() parentName = '';
|
||||
@Input() seriesId: number = 0;
|
||||
@Input() libraryId: number = 0;
|
||||
@Input({required: true}) data!: Volume | Chapter;
|
||||
|
||||
/**
|
||||
* If this is a volume, this will be first chapter for said volume.
|
||||
*/
|
||||
chapter!: Chapter;
|
||||
isChapter = false;
|
||||
chapters: Chapter[] = [];
|
||||
|
||||
imageUrls: Array<string> = [];
|
||||
/**
|
||||
* Cover image for the entity
|
||||
*/
|
||||
coverImageUrl!: string;
|
||||
|
||||
isAdmin$: Observable<boolean> = of(false);
|
||||
|
||||
|
||||
actions: ActionItem<any>[] = [];
|
||||
chapterActions: ActionItem<Chapter>[] = [];
|
||||
libraryType: LibraryType = LibraryType.Manga;
|
||||
|
||||
|
||||
tabs = [
|
||||
{title: 'general-tab', disabled: false},
|
||||
{title: 'metadata-tab', disabled: false},
|
||||
{title: 'cover-tab', disabled: false},
|
||||
{title: 'progress-tab', disabled: false},
|
||||
{title: 'info-tab', disabled: false}
|
||||
];
|
||||
active = this.tabs[0];
|
||||
|
||||
summary: string = '';
|
||||
downloadInProgress: boolean = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.imageUrls = this.chapters.map(c => this.imageService.getChapterCoverImage(c.id));
|
||||
this.isChapter = this.utilityService.isChapter(this.data);
|
||||
this.chapter = this.utilityService.isChapter(this.data) ? (this.data as Chapter) : (this.data as Volume).chapters[0];
|
||||
|
||||
|
||||
if (this.isChapter) {
|
||||
this.coverImageUrl = this.imageService.getChapterCoverImage(this.data.id);
|
||||
this.summary = this.utilityService.asChapter(this.data).summary || '';
|
||||
this.chapters.push(this.data as Chapter);
|
||||
} else {
|
||||
this.coverImageUrl = this.imageService.getVolumeCoverImage(this.data.id);
|
||||
this.summary = this.utilityService.asVolume(this.data).chapters[0].summary || '';
|
||||
this.chapters.push(...(this.data as Volume).chapters);
|
||||
}
|
||||
|
||||
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this))
|
||||
.filter(item => item.action !== Action.Edit);
|
||||
this.chapterActions.push({title: 'read', description: '', action: Action.Read, callback: this.handleChapterActionCallback.bind(this), requiresAdmin: false, children: []});
|
||||
if (this.isChapter) {
|
||||
const chapter = this.utilityService.asChapter(this.data);
|
||||
this.chapterActions = this.actionFactoryService.filterSendToAction(this.chapterActions, chapter);
|
||||
} else {
|
||||
this.chapterActions = this.actionFactoryService.filterSendToAction(this.chapterActions, this.chapters[0]);
|
||||
}
|
||||
|
||||
this.libraryService.getLibraryType(this.libraryId).subscribe(type => {
|
||||
this.libraryType = type;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
|
||||
const collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
||||
this.chapters.forEach((c: Chapter) => {
|
||||
c.files.sort((a: MangaFile, b: MangaFile) => collator.compare(a.filePath, b.filePath));
|
||||
});
|
||||
|
||||
this.imageUrls = this.chapters.map(c => this.imageService.getChapterCoverImage(c.id));
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
close() {
|
||||
this.activeOffcanvas.close();
|
||||
}
|
||||
|
||||
formatChapterNumber(chapter: Chapter) {
|
||||
if (chapter.minNumber === LooseLeafOrDefaultNumber) {
|
||||
return '1';
|
||||
}
|
||||
return chapter.range + '';
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>, chapter: Chapter) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action, chapter);
|
||||
}
|
||||
}
|
||||
|
||||
applyCoverImage(coverUrl: string) {
|
||||
this.uploadService.updateChapterCoverImage(this.chapter.id, coverUrl).subscribe(() => {});
|
||||
}
|
||||
|
||||
updateCoverImageIndex(selectedIndex: number) {
|
||||
if (selectedIndex <= 0) return;
|
||||
this.applyCoverImage(this.imageUrls[selectedIndex]);
|
||||
}
|
||||
|
||||
resetCoverImage() {
|
||||
this.uploadService.resetChapterCoverLock(this.chapter.id).subscribe(() => {
|
||||
this.toastr.info(translate('toasts.regen-cover'));
|
||||
});
|
||||
}
|
||||
|
||||
markChapterAsRead(chapter: Chapter) {
|
||||
if (this.seriesId === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionService.markChapterAsRead(this.libraryId, this.seriesId, chapter, () => { this.cdRef.markForCheck(); });
|
||||
}
|
||||
|
||||
markChapterAsUnread(chapter: Chapter) {
|
||||
if (this.seriesId === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionService.markChapterAsUnread(this.libraryId, this.seriesId, chapter, () => { this.cdRef.markForCheck(); });
|
||||
}
|
||||
|
||||
handleChapterActionCallback(action: ActionItem<Chapter>, chapter: Chapter) {
|
||||
switch (action.action) {
|
||||
case(Action.MarkAsRead):
|
||||
this.markChapterAsRead(chapter);
|
||||
break;
|
||||
case(Action.MarkAsUnread):
|
||||
this.markChapterAsUnread(chapter);
|
||||
break;
|
||||
case(Action.AddToReadingList):
|
||||
this.actionService.addChapterToReadingList(chapter, this.seriesId);
|
||||
break;
|
||||
case (Action.IncognitoRead):
|
||||
this.readChapter(chapter, true);
|
||||
break;
|
||||
case (Action.Download):
|
||||
this.download(chapter);
|
||||
break;
|
||||
case (Action.Read):
|
||||
this.readChapter(chapter, false);
|
||||
break;
|
||||
case (Action.SendTo):
|
||||
{
|
||||
const device = (action._extra!.data as Device);
|
||||
this.actionService.sendToDevice([chapter.id], device);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
readChapter(chapter: Chapter, incognito: boolean = false) {
|
||||
if (chapter.pages === 0) {
|
||||
this.toastr.error(translate('toasts.no-pages'));
|
||||
return;
|
||||
}
|
||||
|
||||
const params = this.readerService.getQueryParamsObject(incognito, false);
|
||||
this.router.navigate(this.readerService.getNavigationArray(this.libraryId, this.seriesId, chapter.id, chapter.files[0].format), {queryParams: params});
|
||||
this.close();
|
||||
}
|
||||
|
||||
download(chapter: Chapter) {
|
||||
if (this.downloadInProgress) {
|
||||
this.toastr.info(translate('toasts.download-in-progress'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.downloadInProgress = true;
|
||||
this.cdRef.markForCheck();
|
||||
this.downloadService.download('chapter', chapter, (d) => {
|
||||
if (d) return;
|
||||
this.downloadInProgress = false;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,8 @@
|
|||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
align-items: start;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
|
|
@ -92,13 +94,33 @@
|
|||
|
||||
.virtual-scroller, virtual-scroller {
|
||||
width: 100%;
|
||||
height: calc(var(--vh) * 100 - 173px);
|
||||
height: calc(var(--vh) * 100 - 143px);
|
||||
mask-image: linear-gradient(to bottom, transparent, black 0%, black 95%, transparent 100%);
|
||||
-webkit-mask-image: linear-gradient(to bottom, transparent, black 0%, black 95%, transparent 100%);
|
||||
overflow: auto;
|
||||
|
||||
}
|
||||
|
||||
virtual-scroller.empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vertical.selfScroll {
|
||||
&::-webkit-scrollbar {
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: transparent; /*makes it invisible when not hovering*/
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255,255,255,0.3); /*On hover, it will turn grey*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
|
|
|
|||
|
|
@ -44,14 +44,19 @@
|
|||
}
|
||||
|
||||
<div class="card-overlay"></div>
|
||||
@if (overlayInformation | safeHtml; as info) {
|
||||
@if (info) {
|
||||
<div class="overlay-information {{centerOverlay ? 'overlay-information--centered' : ''}}">
|
||||
<div class="position-relative">
|
||||
<span class="card-title library mx-auto" style="width: auto;" [ngbTooltip]="info" placement="top" [innerHTML]="info"></span>
|
||||
</div>
|
||||
|
||||
@if (showReadButton) {
|
||||
<div class="series overlay-information">
|
||||
<div class="overlay-information--centered">
|
||||
<span class="card-title library mx-auto" style="width: auto;">
|
||||
<span (click)="clickRead($event)">
|
||||
<div>
|
||||
<i class="fa-solid fa-book" aria-hidden="true"></i>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="card-body meta-title">
|
||||
|
|
|
|||
|
|
@ -134,13 +134,17 @@ export class CardItemComponent implements OnInit {
|
|||
*/
|
||||
@Input() count: number = 0;
|
||||
/**
|
||||
* Additional information to show on the overlay area. Will always render.
|
||||
* Show a read button. Emits on (readClicked)
|
||||
*/
|
||||
@Input() overlayInformation: string = '';
|
||||
@Input() showReadButton: boolean = false;
|
||||
/**
|
||||
* If overlay is enabled, should the text be centered or not
|
||||
*/
|
||||
@Input() centerOverlay = false;
|
||||
/**
|
||||
* Will generate a button to instantly read
|
||||
*/
|
||||
@Input() hasReadButton = false;
|
||||
/**
|
||||
* Event emitted when item is clicked
|
||||
*/
|
||||
|
|
@ -149,6 +153,7 @@ export class CardItemComponent implements OnInit {
|
|||
* When the card is selected.
|
||||
*/
|
||||
@Output() selection = new EventEmitter<boolean>();
|
||||
@Output() readClicked = new EventEmitter<Series | Volume | Chapter | UserCollection | PageBookmark | RecentlyAddedItem | NextExpectedChapter>();
|
||||
@ContentChild('subtitle') subtitleTemplate!: TemplateRef<any>;
|
||||
/**
|
||||
* Library name item belongs to
|
||||
|
|
@ -229,9 +234,10 @@ export class CardItemComponent implements OnInit {
|
|||
const nextDate = (this.entity as NextExpectedChapter);
|
||||
|
||||
const tokens = nextDate.title.split(':');
|
||||
this.overlayInformation = `
|
||||
<i class="fa-regular fa-clock mb-2" style="font-size: 26px" aria-hidden="true"></i>
|
||||
<div>${tokens[0]}</div><div>${tokens[1]}</div>`;
|
||||
// this.overlayInformation = `
|
||||
// <i class="fa-regular fa-clock mb-2" style="font-size: 26px" aria-hidden="true"></i>
|
||||
// <div>${tokens[0]}</div><div>${tokens[1]}</div>`;
|
||||
// // todo: figure out where this caller is
|
||||
this.centerOverlay = true;
|
||||
|
||||
if (nextDate.expectedDate) {
|
||||
|
|
@ -387,4 +393,11 @@ export class CardItemComponent implements OnInit {
|
|||
// return a.isAllowed(a, this.entity);
|
||||
// });
|
||||
}
|
||||
|
||||
clickRead(event: any) {
|
||||
event.stopPropagation();
|
||||
if (this.bulkSelectionService.hasSelections()) return;
|
||||
|
||||
this.readClicked.emit(this.entity);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,132 +0,0 @@
|
|||
<ng-container *transloco="let t; read: 'chapter-metadata-detail'">
|
||||
<ng-container *ngIf="chapter !== undefined">
|
||||
<span *ngIf="chapter.writers.length === 0 && chapter.coverArtists.length === 0
|
||||
&& chapter.pencillers.length === 0 && chapter.inkers.length === 0
|
||||
&& chapter.colorists.length === 0 && chapter.letterers.length === 0
|
||||
&& chapter.editors.length === 0 && chapter.publishers.length === 0
|
||||
&& chapter.characters.length === 0 && chapter.translators.length === 0
|
||||
&& chapter.imprints.length === 0 && chapter.locations.length === 0
|
||||
&& chapter.teams.length === 0">
|
||||
{{t('no-data')}}
|
||||
</span>
|
||||
<div class="container-flex row row-cols-auto row-cols-lg-5 g-2 g-lg-3 me-0 mt-2">
|
||||
<div class="col-auto mt-2" *ngIf="chapter.writers && chapter.writers.length > 0">
|
||||
<h6>{{t('writers-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.writers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.coverArtists && chapter.coverArtists.length > 0">
|
||||
<h6>{{t('cover-artists-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.coverArtists">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.pencillers && chapter.pencillers.length > 0">
|
||||
<h6>{{t('pencillers-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.pencillers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.inkers && chapter.inkers.length > 0">
|
||||
<h6>{{t('inkers-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.inkers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.colorists && chapter.colorists.length > 0">
|
||||
<h6>{{t('colorists-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.colorists">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.letterers && chapter.letterers.length > 0">
|
||||
<h6>{{t('letterers-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.letterers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.editors && chapter.editors.length > 0">
|
||||
<h6>{{t('editors-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.editors">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.publishers && chapter.publishers.length > 0">
|
||||
<h6>{{t('publishers-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.publishers">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.imprints && chapter.imprints.length > 0">
|
||||
<h6>{{t('imprints-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.imprints">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.characters && chapter.characters.length > 0">
|
||||
<h6>{{t('characters-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.characters">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.teams && chapter.teams.length > 0">
|
||||
<h6>{{t('teams-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.teams">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.locations && chapter.locations.length > 0">
|
||||
<h6>{{t('locations-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.locations">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="col-auto mt-2" *ngIf="chapter.translators && chapter.translators.length > 0">
|
||||
<h6>{{t('translators-title')}}</h6>
|
||||
<app-badge-expander [items]="chapter.translators">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-person-badge [person]="item"></app-person-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {BadgeExpanderComponent} from "../../shared/badge-expander/badge-expander.component";
|
||||
import {PersonBadgeComponent} from "../../shared/person-badge/person-badge.component";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {Chapter} from "../../_models/chapter";
|
||||
|
||||
@Component({
|
||||
selector: 'app-chapter-metadata-detail',
|
||||
standalone: true,
|
||||
imports: [CommonModule, BadgeExpanderComponent, PersonBadgeComponent, TranslocoDirective],
|
||||
templateUrl: './chapter-metadata-detail.component.html',
|
||||
styleUrls: ['./chapter-metadata-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ChapterMetadataDetailComponent {
|
||||
@Input() chapter: Chapter | undefined;
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
<ng-container *transloco="let t; read: 'entity-info-cards'">
|
||||
|
||||
<div class="mt-3 mb-3">
|
||||
<div class="row g-0" *ngIf="chapter ">
|
||||
<!-- Tags and Characters are used a lot of Hentai and Doujinshi type content, so showing in list item has value add on first glance -->
|
||||
<app-metadata-detail [tags]="chapter.tags" [libraryId]="libraryId" [queryParam]="FilterField.Tags" heading="Tags">
|
||||
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
|
||||
</app-metadata-detail>
|
||||
|
||||
<app-metadata-detail [tags]="chapter.characters" [libraryId]="libraryId" [queryParam]="FilterField.Characters" heading="Characters">
|
||||
<ng-template #titleTemplate let-item>{{item.name}}</ng-template>
|
||||
</app-metadata-detail>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<ng-container *ngIf="chapter !== undefined && chapter.releaseDate && (chapter.releaseDate | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('release-date-tooltip')" [clickable]="false" fontClasses="fa-regular fa-calendar" [title]="t('release-date-title')">
|
||||
{{chapter.releaseDate | date:'shortDate' | defaultDate}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.ageRating !== AgeRating.Unknown">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('age-rating-title')" [clickable]="false" fontClasses="fas fa-eye" [title]="t('age-rating-title')">
|
||||
{{chapter.ageRating | ageRating}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="totalPages > 0">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-regular fa-file-lines">
|
||||
{{t('pages-count', {num: totalPages | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.files[0].format === MangaFormat.EPUB && totalWordCount > 0">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-solid fa-book-open">
|
||||
{{t('words-count', {num: totalWordCount | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.files[0].format === MangaFormat.EPUB && totalWordCount > 0 || chapter.files[0].format !== MangaFormat.EPUB">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title [label]="t('read-time-title')" [clickable]="false" fontClasses="fa-regular fa-clock">
|
||||
<ng-container *ngIf="readingTime.maxHours === 0 || readingTime.minHours === 0; else normalReadTime">{{t('less-than-hour')}}</ng-container>
|
||||
<ng-template #normalReadTime>
|
||||
{{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} {{readingTime.minHours > 1 ? t('hours') : t('hour')}}
|
||||
</ng-template>
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="showExtendedProperties && chapter.createdUtc && chapter.createdUtc !== '' && (chapter.createdUtc | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('date-added-title')" [clickable]="false" fontClasses="fa-solid fa-file-import" [title]="t('date-added-title')">
|
||||
{{chapter.createdUtc | utcToLocalTime | translocoDate: {dateStyle: 'short', timeStyle: 'short' } | defaultDate}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="showExtendedProperties && size > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('size-title')" [clickable]="false" fontClasses="fa-solid fa-scale-unbalanced" [title]="t('size-title')">
|
||||
{{size | bytes}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="showExtendedProperties">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('id-title')" [clickable]="false" fontClasses="fa-solid fa-fingerprint" [title]="t('id-title')">
|
||||
{{entity.id}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<ng-container *ngIf="WebLinks.length > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('links-title')" [clickable]="false" fontClasses="fa-solid fa-link" [title]="t('links-title')">
|
||||
<a class="me-1" [href]="link | safeHtml" *ngFor="let link of WebLinks" target="_blank" rel="noopener noreferrer" [title]="link">
|
||||
<app-image height="24px" width="24px" aria-hidden="true" [imageUrl]="imageService.getWebLinkImage(link)"
|
||||
[errorImage]="imageService.errorWebLinkImage"></app-image>
|
||||
</a>
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.isbn.length > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('isbn-title')" [clickable]="false" fontClasses="fa-solid fa-barcode" [title]="t('isbn-title')">
|
||||
{{chapter.isbn}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="(chapter.lastReadingProgress | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('last-read-title')" [clickable]="false" fontClasses="fa-regular fa-clock" [ngbTooltip]="chapter.lastReadingProgress | date: 'medium'">
|
||||
{{chapter.lastReadingProgress | date: 'shortDate'}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="isChapter">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title [label]="t('sort-order-title')" [clickable]="false" fontClasses="fa-solid fa-arrow-down-1-9" [title]="t('sort-order-title')">
|
||||
{{chapter.sortOrder}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Input,
|
||||
OnInit,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { HourEstimateRange } from 'src/app/_models/series-detail/hour-estimate-range';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import { AgeRating } from 'src/app/_models/metadata/age-rating';
|
||||
import { Volume } from 'src/app/_models/volume';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {IconAndTitleComponent} from "../../shared/icon-and-title/icon-and-title.component";
|
||||
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
|
||||
import {DefaultDatePipe} from "../../_pipes/default-date.pipe";
|
||||
import {BytesPipe} from "../../_pipes/bytes.pipe";
|
||||
import {CompactNumberPipe} from "../../_pipes/compact-number.pipe";
|
||||
import {AgeRatingPipe} from "../../_pipes/age-rating.pipe";
|
||||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {MetadataDetailComponent} from "../../series-detail/_components/metadata-detail/metadata-detail.component";
|
||||
import {TranslocoModule} from "@jsverse/transloco";
|
||||
import {TranslocoLocaleModule} from "@jsverse/transloco-locale";
|
||||
import {FilterField} from "../../_models/metadata/v2/filter-field";
|
||||
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-entity-info-cards',
|
||||
standalone: true,
|
||||
imports: [CommonModule, IconAndTitleComponent, SafeHtmlPipe, DefaultDatePipe, BytesPipe, CompactNumberPipe,
|
||||
AgeRatingPipe, NgbTooltip, MetadataDetailComponent, TranslocoModule, CompactNumberPipe, TranslocoLocaleModule,
|
||||
UtcToLocalTimePipe, ImageComponent],
|
||||
templateUrl: './entity-info-cards.component.html',
|
||||
styleUrls: ['./entity-info-cards.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class EntityInfoCardsComponent implements OnInit {
|
||||
|
||||
protected readonly AgeRating = AgeRating;
|
||||
protected readonly MangaFormat = MangaFormat;
|
||||
protected readonly FilterField = FilterField;
|
||||
|
||||
public readonly imageService = inject(ImageService);
|
||||
|
||||
|
||||
@Input({required: true}) entity!: Volume | Chapter;
|
||||
@Input({required: true}) libraryId!: number;
|
||||
|
||||
/**
|
||||
* Hide more system based fields, like id or Date Added
|
||||
*/
|
||||
@Input() showExtendedProperties: boolean = true;
|
||||
|
||||
isChapter = false;
|
||||
chapter!: Chapter;
|
||||
|
||||
ageRating!: string;
|
||||
totalPages: number = 0;
|
||||
totalWordCount: number = 0;
|
||||
readingTime: HourEstimateRange = {maxHours: 1, minHours: 1, avgHours: 1};
|
||||
size: number = 0;
|
||||
|
||||
get WebLinks() {
|
||||
if (this.chapter.webLinks === '') return [];
|
||||
return this.chapter.webLinks.split(',');
|
||||
}
|
||||
|
||||
constructor(private utilityService: UtilityService, private seriesService: SeriesService, private readonly cdRef: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isChapter = this.utilityService.isChapter(this.entity);
|
||||
|
||||
this.chapter = this.utilityService.isChapter(this.entity) ? (this.entity as Chapter) : (this.entity as Volume).chapters[0];
|
||||
|
||||
|
||||
if (this.isChapter) {
|
||||
this.size = this.utilityService.asChapter(this.entity).files.reduce((sum, v) => sum + v.bytes, 0);
|
||||
} else {
|
||||
this.size = this.utilityService.asVolume(this.entity).chapters.reduce((sum1, chapter) => {
|
||||
return sum1 + chapter.files.reduce((sum2, file) => {
|
||||
return sum2 + file.bytes;
|
||||
}, 0);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
|
||||
this.totalPages = this.chapter.pages;
|
||||
if (!this.isChapter) {
|
||||
this.totalPages = this.utilityService.asVolume(this.entity).pages;
|
||||
}
|
||||
|
||||
this.totalWordCount = this.chapter.wordCount;
|
||||
if (!this.isChapter) {
|
||||
this.totalWordCount = this.utilityService.asVolume(this.entity).chapters.map(c => c.wordCount).reduce((sum, d) => sum + d);
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (this.isChapter) {
|
||||
this.readingTime.minHours = this.chapter.minHoursToRead;
|
||||
this.readingTime.maxHours = this.chapter.maxHoursToRead;
|
||||
this.readingTime.avgHours = this.chapter.avgHoursToRead;
|
||||
} else {
|
||||
const vol = this.utilityService.asVolume(this.entity);
|
||||
this.readingTime.minHours = vol.minHoursToRead;
|
||||
this.readingTime.maxHours = vol.maxHoursToRead;
|
||||
this.readingTime.avgHours = vol.avgHoursToRead;
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,12 @@
|
|||
@switch (libraryType) {
|
||||
@case (LibraryType.Comic) {
|
||||
@if (titleName !== '' && prioritizeTitleName) {
|
||||
@if (isChapter && includeChapter) {
|
||||
{{t('issue-num') + ' ' + number + ' - ' }}
|
||||
}
|
||||
|
||||
{{titleName}}
|
||||
} @else {
|
||||
{{seriesName.length > 0 ? seriesName + ' - ' : ''}}
|
||||
@if (includeVolume && volumeTitle !== '') {
|
||||
{{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
|
||||
}
|
||||
|
|
@ -14,9 +17,12 @@
|
|||
|
||||
@case (LibraryType.ComicVine) {
|
||||
@if (titleName !== '' && prioritizeTitleName) {
|
||||
@if (isChapter && includeChapter) {
|
||||
{{t('issue-num') + ' ' + number + ' - ' }}
|
||||
}
|
||||
|
||||
{{titleName}}
|
||||
} @else {
|
||||
{{seriesName.length > 0 ? seriesName + ' - ' : ''}}
|
||||
@if (includeVolume && volumeTitle !== '') {
|
||||
{{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
|
||||
}
|
||||
|
|
@ -26,12 +32,15 @@
|
|||
|
||||
@case (LibraryType.Manga) {
|
||||
@if (titleName !== '' && prioritizeTitleName) {
|
||||
@if (isChapter && includeChapter) {
|
||||
{{t('chapter') + ' ' + number + ' - ' }}
|
||||
}
|
||||
{{titleName}}
|
||||
} @else {
|
||||
{{seriesName.length > 0 ? seriesName + ' - ' : ''}}
|
||||
@if (includeVolume && volumeTitle !== '') {
|
||||
{{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
|
||||
}
|
||||
|
||||
{{number !== LooseLeafOrSpecial ? (isChapter ? (t('chapter') + ' ') + number : volumeTitle) : t('special')}}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,12 +28,15 @@ export class EntityTitleComponent implements OnInit {
|
|||
* Library type for which the entity belongs
|
||||
*/
|
||||
@Input() libraryType: LibraryType = LibraryType.Manga;
|
||||
@Input() seriesName: string = '';
|
||||
@Input({required: true}) entity!: Volume | Chapter;
|
||||
/**
|
||||
* When generating the title, should this prepend 'Volume number' before the Chapter wording
|
||||
*/
|
||||
@Input() includeVolume: boolean = false;
|
||||
/**
|
||||
* When generating the title, should this prepend 'Chapter number' before the Chapter titlename
|
||||
*/
|
||||
@Input() includeChapter: boolean = false;
|
||||
/**
|
||||
* When a titleName (aka a title) is available on the entity, show it over Volume X Chapter Y
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
<div class="list-item-container d-flex flex-row g-0 mb-2 p-2">
|
||||
<div class="pe-2">
|
||||
<app-image [imageUrl]="imageUrl" [height]="imageHeight" [styles]="{'max-height': '200px'}" [width]="imageWidth"></app-image>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="g-0">
|
||||
<h5 class="mb-0">
|
||||
<ng-content select="[title]"></ng-content>
|
||||
</h5>
|
||||
@if (summary && summary.length > 0) {
|
||||
<div class="mt-2 ps-2">
|
||||
<app-read-more [text]="summary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
.list-item-container {
|
||||
background: var(--card-list-item-bg-color);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {NgbProgressbar, NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {ReadMoreComponent} from "../../shared/read-more/read-more.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-external-list-item',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ImageComponent, NgbProgressbar, NgbTooltip, ReadMoreComponent],
|
||||
templateUrl: './external-list-item.component.html',
|
||||
styleUrls: ['./external-list-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ExternalListItemComponent {
|
||||
|
||||
/**
|
||||
* Image to show
|
||||
*/
|
||||
@Input() imageUrl: string = '';
|
||||
|
||||
/**
|
||||
* Size of the Image Height. Defaults to 232.91px.
|
||||
*/
|
||||
@Input() imageHeight: string = '232.91px';
|
||||
/**
|
||||
* Size of the Image Width Defaults to 160px.
|
||||
*/
|
||||
@Input() imageWidth: string = '160px';
|
||||
@Input() summary: string | null = '';
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
<ng-container *transloco="let t; read: 'list-item'">
|
||||
<div class="list-item-container d-flex flex-row g-0 mb-2 p-2">
|
||||
<div class="pe-2">
|
||||
<app-image [imageUrl]="imageUrl" [height]="imageHeight" [styles]="{'max-height': '200px'}" [width]="imageWidth"></app-image>
|
||||
<div class="not-read-badge" *ngIf="pagesRead === 0 && totalPages > 0"></div>
|
||||
<span class="download">
|
||||
<app-download-indicator [download$]="download$"></app-download-indicator>
|
||||
</span>
|
||||
<div class="progress-banner" *ngIf="pagesRead < totalPages && totalPages > 0 && pagesRead !== totalPages"
|
||||
ngbTooltip="{{(pagesRead / totalPages) | number:'1.0-1'}}% Read">
|
||||
<p><ngb-progressbar type="primary" height="5px" [value]="pagesRead" [max]="totalPages"></ngb-progressbar></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="g-0">
|
||||
<h5 class="mb-0">
|
||||
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="seriesName" iconClass="fa-ellipsis-v"></app-card-actionables>
|
||||
<ng-content select="[title]"></ng-content>
|
||||
<button class="btn btn-primary float-end" (click)="read.emit()">
|
||||
<span>
|
||||
<i class="fa fa-book me-1" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="d-none d-sm-inline-block">{{t('read')}}</span>
|
||||
</button>
|
||||
</h5>
|
||||
|
||||
<h6 class="text-muted" [ngClass]="{'subtitle-with-actionables' : actions.length > 0}" *ngIf="Title !== '' && showTitle">{{Title}}</h6>
|
||||
<ng-container *ngIf="summary.length > 0">
|
||||
<div class="mt-2 ps-2">
|
||||
<app-read-more [text]="summary" [blur]="pagesRead === 0 && blur" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="ps-2 d-none d-md-inline-block" style="width: 100%">
|
||||
<app-entity-info-cards [entity]="entity" [libraryId]="libraryId" [showExtendedProperties]="ShowExtended"></app-entity-info-cards>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
// with summary and cards, we have a height of 220px, we might want to default to 220px and let it grow from there to help with virtualization
|
||||
|
||||
.download {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
position: absolute;
|
||||
top: 55px;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.progress-banner {
|
||||
height: 5px;
|
||||
|
||||
.progress {
|
||||
color: var(--card-progress-bar-color);
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.list-item-container {
|
||||
background: var(--card-list-item-bg-color);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.not-read-badge {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 110px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 0 var(--card-progress-triangle-size) var(--card-progress-triangle-size) 0;
|
||||
border-color: transparent var(--primary-color) transparent transparent;
|
||||
}
|
||||
|
||||
.subtitle-with-actionables {
|
||||
font-size: 0.75rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component, DestroyRef,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { Download } from 'src/app/shared/_models/download';
|
||||
import { DownloadEvent, DownloadService } from 'src/app/shared/_services/download.service';
|
||||
import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { LibraryType } from 'src/app/_models/library/library';
|
||||
import { RelationKind } from 'src/app/_models/series-detail/relation-kind';
|
||||
import { Volume } from 'src/app/_models/volume';
|
||||
import { Action, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {ReadMoreComponent} from "../../shared/read-more/read-more.component";
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {DownloadIndicatorComponent} from "../download-indicator/download-indicator.component";
|
||||
import {EntityInfoCardsComponent} from "../entity-info-cards/entity-info-cards.component";
|
||||
import {NgbProgressbar, NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
||||
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-list-item',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReadMoreComponent, ImageComponent, DownloadIndicatorComponent, EntityInfoCardsComponent, CardActionablesComponent, NgbProgressbar, NgbTooltip, TranslocoDirective],
|
||||
templateUrl: './list-item.component.html',
|
||||
styleUrls: ['./list-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ListItemComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* Volume or Chapter to render
|
||||
*/
|
||||
@Input({required: true}) entity!: Volume | Chapter;
|
||||
@Input({required: true}) libraryId!: number;
|
||||
/**
|
||||
* Image to show
|
||||
*/
|
||||
@Input() imageUrl: string = '';
|
||||
/**
|
||||
* Actions to show
|
||||
*/
|
||||
@Input() actions: ActionItem<any>[] = []; // Volume | Chapter
|
||||
/**
|
||||
* Library type to help with formatting title
|
||||
*/
|
||||
@Input() libraryType: LibraryType = LibraryType.Manga;
|
||||
/**
|
||||
* Name of the Series to show under the title
|
||||
*/
|
||||
@Input() seriesName: string = '';
|
||||
|
||||
/**
|
||||
* Size of the Image Height. Defaults to 232.91px.
|
||||
*/
|
||||
@Input() imageHeight: string = '232.91px';
|
||||
/**
|
||||
* Size of the Image Width Defaults to 160px.
|
||||
*/
|
||||
@Input() imageWidth: string = '160px';
|
||||
@Input() seriesLink: string = '';
|
||||
|
||||
@Input() pagesRead: number = 0;
|
||||
@Input() totalPages: number = 0;
|
||||
|
||||
@Input() relation: RelationKind | undefined = undefined;
|
||||
|
||||
/**
|
||||
* When generating the title, should this prepend 'Volume number' before the Chapter wording
|
||||
*/
|
||||
@Input() includeVolume: boolean = false;
|
||||
/**
|
||||
* Show's the title if available on entity
|
||||
*/
|
||||
@Input() showTitle: boolean = true;
|
||||
/**
|
||||
* Blur the summary for the list item
|
||||
*/
|
||||
@Input() blur: boolean = false;
|
||||
|
||||
@Output() read: EventEmitter<void> = new EventEmitter<void>();
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
actionInProgress: boolean = false;
|
||||
summary: string = '';
|
||||
isChapter: boolean = false;
|
||||
|
||||
|
||||
download$: Observable<DownloadEvent | null> | null = null;
|
||||
downloadInProgress: boolean = false;
|
||||
|
||||
get Title() {
|
||||
if (this.isChapter) return (this.entity as Chapter).titleName;
|
||||
return '';
|
||||
}
|
||||
|
||||
get ShowExtended() {
|
||||
return this.utilityService.getActiveBreakpoint() === Breakpoint.Desktop;
|
||||
}
|
||||
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
|
||||
|
||||
constructor(public utilityService: UtilityService, private downloadService: DownloadService,
|
||||
private toastr: ToastrService, private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isChapter = this.utilityService.isChapter(this.entity);
|
||||
if (this.isChapter) {
|
||||
this.summary = this.utilityService.asChapter(this.entity).summary || '';
|
||||
} else {
|
||||
this.summary = this.utilityService.asVolume(this.entity).chapters[0].summary || '';
|
||||
}
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
|
||||
this.download$ = this.downloadService.activeDownloads$.pipe(takeUntilDestroyed(this.destroyRef), map((events) => {
|
||||
if(this.utilityService.isVolume(this.entity)) return events.find(e => e.entityType === 'volume' && e.subTitle === this.downloadService.downloadSubtitle('volume', (this.entity as Volume))) || null;
|
||||
if(this.utilityService.isChapter(this.entity)) return events.find(e => e.entityType === 'chapter' && e.subTitle === this.downloadService.downloadSubtitle('chapter', (this.entity as Chapter))) || null;
|
||||
return null;
|
||||
}));
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (action.action == Action.Download) {
|
||||
if (this.downloadInProgress) {
|
||||
this.toastr.info(translate('toasts.download-in-progress'));
|
||||
return;
|
||||
}
|
||||
|
||||
const statusUpdate = (d: Download | undefined) => {
|
||||
if (d) return;
|
||||
this.downloadInProgress = false;
|
||||
};
|
||||
|
||||
if (this.utilityService.isVolume(this.entity)) {
|
||||
const volume = this.utilityService.asVolume(this.entity);
|
||||
this.downloadService.download('volume', volume, statusUpdate);
|
||||
} else if (this.utilityService.isChapter(this.entity)) {
|
||||
const chapter = this.utilityService.asChapter(this.entity);
|
||||
this.downloadService.download('chapter', chapter, statusUpdate);
|
||||
}
|
||||
return; // Don't propagate the download from a card
|
||||
}
|
||||
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action, this.entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,21 +6,22 @@
|
|||
<div class="card-overlay"></div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="entity.title | safeHtml as info">
|
||||
<div class="card-body meta-title" *ngIf="info !== ''">
|
||||
<div class="card-content d-flex flex-column pt-2 pb-2 justify-content-center align-items-center text-center">
|
||||
@if (entity.title | safeHtml; as info) {
|
||||
@if (info !== '') {
|
||||
<div class="card-body meta-title">
|
||||
<div class="card-content d-flex flex-column pt-2 pb-2 justify-content-center align-items-center text-center">
|
||||
|
||||
<div class="upcoming-header"><i class="fa-regular fa-clock me-1" aria-hidden="true"></i>Upcoming</div>
|
||||
<span [innerHTML]="info"></span>
|
||||
<div class="upcoming-header"><i class="fa-regular fa-clock me-1" aria-hidden="true"></i>Upcoming</div>
|
||||
<span [innerHTML]="info"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
|
||||
<div class="card-title-container">
|
||||
<span class="card-title" tabindex="0">
|
||||
{{title}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="card-title-container">
|
||||
<span class="card-title" tabindex="0">
|
||||
{{title}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter";
|
||||
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
||||
|
|
@ -9,7 +8,7 @@ import {translate} from "@jsverse/transloco";
|
|||
@Component({
|
||||
selector: 'app-next-expected-card',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ImageComponent, SafeHtmlPipe],
|
||||
imports: [ImageComponent, SafeHtmlPipe],
|
||||
templateUrl: './next-expected-card.component.html',
|
||||
styleUrl: './next-expected-card.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
|
|||
|
|
@ -62,8 +62,9 @@
|
|||
</div>
|
||||
|
||||
<div class="card-title-container">
|
||||
<span class="card-title" [ngbTooltip]="series.name" id="{{series.id}}" tabindex="0">
|
||||
<a class="dark-exempt btn-icon" routerLink="/library/{{libraryId}}/series/{{series.id}}">
|
||||
<span class="card-title" [ngbTooltip]="series.name" id="{{series.id}}">
|
||||
<app-series-format [format]="series.format"></app-series-format>
|
||||
<a class="dark-exempt btn-icon ms-1" routerLink="/library/{{libraryId}}/series/{{series.id}}">
|
||||
{{series.name}}
|
||||
</a>
|
||||
</span>
|
||||
|
|
@ -71,7 +72,7 @@
|
|||
@if (actions && actions.length > 0) {
|
||||
<span class="card-actions float-end">
|
||||
<app-card-actionables (actionHandler)="handleSeriesActionCallback($event, series)" [actions]="actions" [labelBy]="series.name"></app-card-actionables>
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import {BulkSelectionService} from "../bulk-selection.service";
|
|||
import {User} from "../../_models/user";
|
||||
import {ScrollService} from "../../_services/scroll.service";
|
||||
import {ReaderService} from "../../_services/reader.service";
|
||||
import {SeriesFormatComponent} from "../../shared/series-format/series-format.component";
|
||||
|
||||
function deepClone(obj: any): any {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
|
|
@ -67,7 +68,7 @@ function deepClone(obj: any): any {
|
|||
@Component({
|
||||
selector: 'app-series-card',
|
||||
standalone: true,
|
||||
imports: [CommonModule, CardItemComponent, RelationshipPipe, CardActionablesComponent, DefaultValuePipe, DownloadIndicatorComponent, EntityTitleComponent, FormsModule, ImageComponent, NgbProgressbar, NgbTooltip, RouterLink, TranslocoDirective],
|
||||
imports: [CommonModule, CardItemComponent, RelationshipPipe, CardActionablesComponent, DefaultValuePipe, DownloadIndicatorComponent, EntityTitleComponent, FormsModule, ImageComponent, NgbProgressbar, NgbTooltip, RouterLink, TranslocoDirective, SeriesFormatComponent],
|
||||
templateUrl: './series-card.component.html',
|
||||
styleUrls: ['./series-card.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
@ -284,7 +285,7 @@ export class SeriesCardComponent implements OnInit, OnChanges {
|
|||
}
|
||||
|
||||
async refreshMetadata(series: Series, forceUpdate = false) {
|
||||
await this.actionService.refreshSeriesMetadata(series, undefined, forceUpdate);
|
||||
await this.actionService.refreshSeriesMetadata(series, undefined, forceUpdate, forceUpdate);
|
||||
}
|
||||
|
||||
async scanLibrary(series: Series) {
|
||||
|
|
|
|||
|
|
@ -1,126 +0,0 @@
|
|||
<ng-container *transloco="let t; read: 'series-info-cards'">
|
||||
<div class="row g-0 mt-3">
|
||||
<ng-container *ngIf="seriesMetadata.releaseYear > 0">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('release-date-title')" [clickable]="false" fontClasses="fa-regular fa-calendar" [title]="t('release-year-tooltip')">
|
||||
{{seriesMetadata.releaseYear}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="seriesMetadata">
|
||||
<ng-container *ngIf="seriesMetadata.ageRating">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('age-rating-title')" [clickable]="true" fontClasses="fas fa-eye" (click)="handleGoTo(FilterField.AgeRating, seriesMetadata.ageRating)" [title]="t('age-rating-title')">
|
||||
{{this.seriesMetadata.ageRating | ageRating}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="seriesMetadata.language !== null && seriesMetadata.language !== ''">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('language-title')" [clickable]="true" fontClasses="fas fa-language" (click)="handleGoTo(FilterField.Languages, seriesMetadata.language)" [title]="t('language-title')">
|
||||
{{seriesMetadata.language | defaultValue:'en' | languageName | async}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<ng-container *ngIf="seriesMetadata.publicationStatus | publicationStatus as pubStatus">
|
||||
<app-icon-and-title [label]="t('publication-status-title')" [clickable]="true" fontClasses="fa-solid fa-hourglass-{{pubStatus === t('ongoing') ? 'empty' : 'end'}}"
|
||||
(click)="handleGoTo(FilterField.PublicationStatus, seriesMetadata.publicationStatus)"
|
||||
[ngbTooltip]="t('publication-status-tooltip') + (seriesMetadata.totalCount === 0 ? '' : ' (' + seriesMetadata.maxCount + ' / ' + seriesMetadata.totalCount + ')')">
|
||||
{{pubStatus}}
|
||||
</app-icon-and-title>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="vr m-2 d-none d-lg-block"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="accountService.hasValidLicense$ | async">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('scrobbling-title')" [clickable]="libraryAllowsScrobbling"
|
||||
fontClasses="fa-solid fa-tower-{{(isScrobbling && libraryAllowsScrobbling) ? 'broadcast' : 'observation'}}"
|
||||
(click)="toggleScrobbling($event)"
|
||||
[ngbTooltip]="t('scrobbling-tooltip')">
|
||||
<ng-container *ngIf="libraryAllowsScrobbling; else noScrobble">
|
||||
{{ isScrobbling ? t('on') : t('off') }}
|
||||
</ng-container>
|
||||
<ng-template #noScrobble>
|
||||
{{t('disabled')}}
|
||||
</ng-template>
|
||||
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="series">
|
||||
<ng-container>
|
||||
<div class="d-none d-md-block col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('format-title')" [clickable]="true"
|
||||
[fontClasses]="series.format | mangaFormatIcon"
|
||||
(click)="handleGoTo(FilterField.Formats, series.format)" [title]="t('format-title')">
|
||||
{{series.format | mangaFormat}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="series.latestReadDate && series.latestReadDate !== '' && (series.latestReadDate | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="d-none d-md-block col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('last-read-title')" [clickable]="false" fontClasses="fa-regular fa-clock" [title]="t('last-read-title')">
|
||||
{{series.latestReadDate | timeAgo}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="series.format === MangaFormat.EPUB; else showPages">
|
||||
<ng-container *ngIf="series.wordCount > 0">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-solid fa-book-open">
|
||||
{{t('words-count', {num: series.wordCount | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
<ng-template #showPages>
|
||||
<div class="d-none d-md-block col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-regular fa-file-lines">
|
||||
{{t('pages-count', {num: series.pages | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngIf="series.format === MangaFormat.EPUB && series.wordCount > 0 || series.format !== MangaFormat.EPUB">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('read-time-title')" [clickable]="false" fontClasses="fa-regular fa-clock">
|
||||
<ng-container *ngIf="readingTime.maxHours === 0 || readingTime.minHours === 0; else normalReadTime">{{t('less-than-hour')}}</ng-container>
|
||||
<ng-template #normalReadTime>
|
||||
{{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} {{readingTime.minHours > 1 ? t('hours') : t('hour')}}
|
||||
</ng-template>
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="hasReadingProgress && showReadingTimeLeft && readingTimeLeft && readingTimeLeft.avgHours !== 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Time Left" [clickable]="false" fontClasses="fa-solid fa-clock">
|
||||
{{readingTimeLeft | readTimeLeft}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import {debounceTime, filter, map} from 'rxjs';
|
||||
import {UtilityService} from 'src/app/shared/_services/utility.service';
|
||||
import {UserProgressUpdateEvent} from 'src/app/_models/events/user-progress-update-event';
|
||||
import {HourEstimateRange} from 'src/app/_models/series-detail/hour-estimate-range';
|
||||
import {MangaFormat} from 'src/app/_models/manga-format';
|
||||
import {Series} from 'src/app/_models/series';
|
||||
import {SeriesMetadata} from 'src/app/_models/metadata/series-metadata';
|
||||
import {AccountService} from 'src/app/_services/account.service';
|
||||
import {EVENTS, MessageHubService} from 'src/app/_services/message-hub.service';
|
||||
import {ReaderService} from 'src/app/_services/reader.service';
|
||||
import {FilterField} from "../../_models/metadata/v2/filter-field";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {ScrobblingService} from "../../_services/scrobbling.service";
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {IconAndTitleComponent} from "../../shared/icon-and-title/icon-and-title.component";
|
||||
import {AgeRatingPipe} from "../../_pipes/age-rating.pipe";
|
||||
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
||||
import {LanguageNamePipe} from "../../_pipes/language-name.pipe";
|
||||
import {PublicationStatusPipe} from "../../_pipes/publication-status.pipe";
|
||||
import {MangaFormatPipe} from "../../_pipes/manga-format.pipe";
|
||||
import {TimeAgoPipe} from "../../_pipes/time-ago.pipe";
|
||||
import {CompactNumberPipe} from "../../_pipes/compact-number.pipe";
|
||||
import {MangaFormatIconPipe} from "../../_pipes/manga-format-icon.pipe";
|
||||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {ReadTimeLeftPipe} from "../../_pipes/read-time-left.pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-info-cards',
|
||||
standalone: true,
|
||||
imports: [CommonModule, IconAndTitleComponent, AgeRatingPipe, DefaultValuePipe, LanguageNamePipe, PublicationStatusPipe, MangaFormatPipe, TimeAgoPipe, CompactNumberPipe, MangaFormatIconPipe, NgbTooltip, TranslocoDirective, ReadTimeLeftPipe],
|
||||
templateUrl: './series-info-cards.component.html',
|
||||
styleUrls: ['./series-info-cards.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class SeriesInfoCardsComponent implements OnInit, OnChanges {
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
public readonly utilityService = inject(UtilityService);
|
||||
private readonly readerService = inject(ReaderService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly messageHub = inject(MessageHubService);
|
||||
public readonly accountService = inject(AccountService);
|
||||
private readonly scrobbleService = inject(ScrobblingService);
|
||||
|
||||
@Input({required: true}) series!: Series;
|
||||
@Input({required: true}) seriesMetadata!: SeriesMetadata;
|
||||
@Input() hasReadingProgress: boolean = false;
|
||||
@Input() readingTimeLeft: HourEstimateRange | undefined;
|
||||
/**
|
||||
* If this should make an API call to request readingTimeLeft
|
||||
*/
|
||||
@Input() showReadingTimeLeft: boolean = true;
|
||||
@Output() goTo: EventEmitter<{queryParamName: FilterField, filter: any}> = new EventEmitter();
|
||||
|
||||
readingTime: HourEstimateRange = {avgHours: 0, maxHours: 0, minHours: 0};
|
||||
isScrobbling: boolean = true;
|
||||
libraryAllowsScrobbling: boolean = true;
|
||||
|
||||
|
||||
protected readonly MangaFormat = MangaFormat;
|
||||
protected readonly FilterField = FilterField;
|
||||
|
||||
|
||||
constructor() {
|
||||
// Listen for progress events and re-calculate getTimeLeft
|
||||
this.messageHub.messages$.pipe(filter(event => event.event === EVENTS.UserProgressUpdate),
|
||||
map(evt => evt.payload as UserProgressUpdateEvent),
|
||||
debounceTime(500),
|
||||
takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(updateEvent => {
|
||||
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
|
||||
if (user === undefined || user.username !== updateEvent.username) return;
|
||||
if (updateEvent.seriesId !== this.series.id) return;
|
||||
this.getReadingTimeLeft();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.series !== null) {
|
||||
this.getReadingTimeLeft();
|
||||
this.readingTime.minHours = this.series.minHoursToRead;
|
||||
this.readingTime.maxHours = this.series.maxHoursToRead;
|
||||
this.readingTime.avgHours = this.series.avgHoursToRead;
|
||||
this.scrobbleService.hasHold(this.series.id).subscribe(res => {
|
||||
this.isScrobbling = !res;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.scrobbleService.libraryAllowsScrobbling(this.series.id).subscribe(res => {
|
||||
this.libraryAllowsScrobbling = res;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
handleGoTo(queryParamName: FilterField, filter: any) {
|
||||
// Ignore the default case added as this query combo would never be valid
|
||||
if (filter + '' === '' && queryParamName === FilterField.SeriesName) return;
|
||||
this.goTo.emit({queryParamName, filter});
|
||||
}
|
||||
|
||||
private getReadingTimeLeft() {
|
||||
if (this.showReadingTimeLeft) this.readerService.getTimeLeft(this.series.id).subscribe((timeLeft) => {
|
||||
this.readingTimeLeft = timeLeft;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
toggleScrobbling(evt: any) {
|
||||
evt.stopPropagation();
|
||||
if (this.isScrobbling) {
|
||||
this.scrobbleService.addHold(this.series.id).subscribe(() => {
|
||||
this.isScrobbling = !this.isScrobbling;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
} else {
|
||||
this.scrobbleService.removeHold(this.series.id).subscribe(() => {
|
||||
this.isScrobbling = !this.isScrobbling;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue