Last Read Filter + A lot of bug fixes (#3312)

This commit is contained in:
Joe Milazzo 2024-10-27 09:39:10 -05:00 committed by GitHub
parent 953d80de1a
commit 6b13db129e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 620 additions and 198 deletions

View file

@ -18,4 +18,5 @@ export interface UserCollection {
totalSourceCount: number;
missingSeriesFromSource: string | null;
ageRating: AgeRating;
itemCount: number;
}

View file

@ -34,7 +34,8 @@ export enum FilterField
AverageRating = 28,
Imprint = 29,
Team = 30,
Location = 31
Location = 31,
ReadLast = 32
}

View file

@ -3,39 +3,40 @@ import { MangaFormat } from "./manga-format";
import {IHasCover} from "./common/i-has-cover";
export interface ReadingListItem {
pagesRead: number;
pagesTotal: number;
seriesName: string;
seriesFormat: MangaFormat;
seriesId: number;
chapterId: number;
order: number;
chapterNumber: string;
volumeNumber: string;
libraryId: number;
id: number;
releaseDate: string;
title: string;
libraryType: LibraryType;
libraryName: string;
summary?: string;
pagesRead: number;
pagesTotal: number;
seriesName: string;
seriesFormat: MangaFormat;
seriesId: number;
chapterId: number;
order: number;
chapterNumber: string;
volumeNumber: string;
libraryId: number;
id: number;
releaseDate: string;
title: string;
libraryType: LibraryType;
libraryName: string;
summary?: string;
}
export interface ReadingList extends IHasCover {
id: number;
title: string;
summary: string;
promoted: boolean;
coverImageLocked: boolean;
items: Array<ReadingListItem>;
/**
* If this is empty or null, the cover image isn't set. Do not use this externally.
*/
coverImage?: string;
primaryColor: string;
secondaryColor: string;
startingYear: number;
startingMonth: number;
endingYear: number;
endingMonth: number;
id: number;
title: string;
summary: string;
promoted: boolean;
coverImageLocked: boolean;
items: Array<ReadingListItem>;
/**
* If this is empty or null, the cover image isn't set. Do not use this externally.
*/
coverImage?: string;
primaryColor: string;
secondaryColor: string;
startingYear: number;
startingMonth: number;
endingYear: number;
endingMonth: number;
itemCount: number;
}

View file

@ -72,6 +72,8 @@ export class FilterFieldPipe implements PipeTransform {
return translate('filter-field-pipe.want-to-read');
case FilterField.ReadingDate:
return translate('filter-field-pipe.read-date');
case FilterField.ReadLast:
return translate('filter-field-pipe.read-last');
case FilterField.AverageRating:
return translate('filter-field-pipe.average-rating');
default:

View file

@ -18,7 +18,7 @@
</div>
</div>
<ng-template #submenu let-list="list">
@for(action of list; track action.id) {
@for(action of list; track action.title) {
<!-- Non Submenu items -->
@if (action.children === undefined || action?.children?.length === 0 || action.dynamicList !== undefined) {
@if (action.dynamicList !== undefined && (action.dynamicList | async | dynamicList); as dList) {

View file

@ -0,0 +1,39 @@
@if (publishers.length > 0) {
<div class="publisher-wrapper">
<div class="publisher-flipper" [class.is-flipped]="isFlipped">
<div class="publisher-side publisher-front">
<div class="publisher-img-container d-inline-flex align-items-center me-2 position-relative">
<app-image
[imageUrl]="imageService.getPersonImage(currentPublisher!.id)"
[classes]="'me-2'"
[hideOnError]="true"
width="32px"
height="32px"
aria-hidden="true">
</app-image>
<div class="position-relative d-inline-block"
(click)="openPublisher(currentPublisher!.id)">
{{currentPublisher!.name}}
</div>
</div>
</div>
<div class="publisher-side publisher-back">
<div class="publisher-img-container d-inline-flex align-items-center me-2 position-relative">
<app-image
[imageUrl]="imageService.getPersonImage(nextPublisher!.id)"
[classes]="'me-2'"
[hideOnError]="true"
width="32px"
height="32px"
aria-hidden="true">
</app-image>
<div class="position-relative d-inline-block"
(click)="openPublisher(nextPublisher!.id)">
{{nextPublisher!.name}}
</div>
</div>
</div>
</div>
</div>
}

View file

@ -0,0 +1,45 @@
//.publisher-flipper-container {
// perspective: 1000px;
//}
//
//.publisher-img-container {
// transform-style: preserve-3d;
// transition: transform 0.5s;
//}
// jumpbar example
.publisher-wrapper {
perspective: 1000px;
height: 32px;
}
.publisher-flipper {
position: relative;
width: 100%;
height: 100%;
text-align: left;
transition: transform 0.6s;
transform-style: preserve-3d;
}
.publisher-flipper.is-flipped {
transform: rotateX(180deg);
}
.publisher-side {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
transform-style: preserve-3d;
}
.publisher-front {
z-index: 2;
}
.publisher-back {
transform: rotateX(180deg);
}

View file

@ -0,0 +1,81 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnDestroy, OnInit} from '@angular/core';
import {ImageComponent} from "../../shared/image/image.component";
import {FilterField} from "../../_models/metadata/v2/filter-field";
import {Person} from "../../_models/metadata/person";
import {ImageService} from "../../_services/image.service";
import {FilterComparison} from "../../_models/metadata/v2/filter-comparison";
import {FilterUtilitiesService} from "../../shared/_services/filter-utilities.service";
import {Router} from "@angular/router";
const ANIMATION_TIME = 3000;
@Component({
selector: 'app-publisher-flipper',
standalone: true,
imports: [
ImageComponent
],
templateUrl: './publisher-flipper.component.html',
styleUrl: './publisher-flipper.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PublisherFlipperComponent implements OnInit, OnDestroy {
protected readonly imageService = inject(ImageService);
private readonly filterUtilityService = inject(FilterUtilitiesService);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly router = inject(Router);
@Input() publishers: Array<Person> = [];
currentPublisher: Person | undefined = undefined;
nextPublisher: Person | undefined = undefined;
currentIndex = 0;
isFlipped = false;
private intervalId: any;
ngOnInit() {
if (this.publishers.length > 0) {
this.currentPublisher = this.publishers[0];
this.nextPublisher = this.publishers[1] || this.publishers[0];
if (this.publishers.length > 1) {
this.startFlipping();
}
}
}
ngOnDestroy() {
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
private startFlipping() {
this.intervalId = setInterval(() => {
// First flip
this.isFlipped = true;
this.cdRef.markForCheck();
// Update content after flip animation completes
setTimeout(() => {
// Update indices and content
this.currentIndex = (this.currentIndex + 1) % this.publishers.length;
this.currentPublisher = this.publishers[this.currentIndex];
this.nextPublisher = this.publishers[(this.currentIndex + 1) % this.publishers.length];
// Reset flip
this.isFlipped = false;
this.cdRef.markForCheck();
}, ANIMATION_TIME); // Full transition time to ensure flip completes
}, ANIMATION_TIME);
}
openPublisher(filter: string | number) {
// TODO: once we build out publisher person-detail page, we can redirect there
this.filterUtilityService.applyFilter(['all-series'], FilterField.Publisher, FilterComparison.Equal, `${filter}`).subscribe();
}
}

View file

@ -12,6 +12,7 @@
<app-carousel-reel [items]="collections" [title]="t('collections-title')">
<ng-template #carouselItem let-item>
<app-card-item [title]="item.title" [entity]="item"
[count]="item.itemCount"
[suppressLibraryLink]="true" [imageUrl]="imageService.getCollectionCoverImage(item.id)"
(clicked)="openCollection(item)" [linkUrl]="'/collections/' + item.id" [showFormat]="false"></app-card-item>
</ng-template>
@ -23,6 +24,7 @@
<app-carousel-reel [items]="readingLists" [title]="t('reading-lists-title')">
<ng-template #carouselItem let-item>
<app-card-item [title]="item.title" [entity]="item"
[count]="item.itemCount"
[suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)"
(clicked)="openReadingList(item)" [linkUrl]="'/lists/' + item.id" [showFormat]="false"></app-card-item>
</ng-template>

View file

@ -6,21 +6,21 @@
<div class="row g-0 mt-2">
@if (settingsForm.get('hostName'); as formControl) {
<app-setting-item [title]="t('host-name-label')" [subtitle]="t('host-name-tooltip')">
<app-setting-item [title]="t('host-name-label')" [subtitle]="t('host-name-tooltip')" [control]="formControl">
<ng-template #view>
{{formControl.value | defaultValue}}
</ng-template>
<ng-template #edit>
<div class="input-group">
<input id="settings-hostname" aria-describedby="hostname-validations" class="form-control" formControlName="hostName" type="text"
[class.is-invalid]="formControl.invalid && formControl.touched">
[class.is-invalid]="formControl.invalid && !formControl.untouched">
<button type="button" class="btn btn-outline-secondary" (click)="autofillGmail()">{{t('gmail-label')}}</button>
<button type="button" class="btn btn-outline-secondary" (click)="autofillOutlook()">{{t('outlook-label')}}</button>
</div>
@if(settingsForm.dirty || settingsForm.touched) {
<div id="hostname-validations" class="invalid-feedback">
@if (formControl.errors?.pattern) {
@if (formControl.errors; as errors) {
<div id="hostname-validations" class="invalid-feedback" style="display: inline-block">
@if (errors.pattern) {
<div>{{t('host-name-validation')}}</div>
}
</div>

View file

@ -198,10 +198,6 @@ export class ManageLibraryComponent implements OnInit {
async applyBulkAction() {
if (!this.bulkMode) {
this.resetBulkMode();
}
// Get Selected libraries
let selected = this.selections.selected();
@ -218,32 +214,54 @@ export class ManageLibraryComponent implements OnInit {
switch(this.bulkAction) {
case (Action.Scan):
await this.confirmService.alert(translate('toasts.bulk-scan'));
this.libraryService.scanMultipleLibraries(selected.map(l => l.id)).subscribe();
this.bulkMode = true;
this.cdRef.markForCheck();
this.libraryService.scanMultipleLibraries(selected.map(l => l.id)).subscribe(_ => this.resetBulkMode());
break;
case Action.RefreshMetadata:
if (!await this.confirmService.confirm(translate('toasts.bulk-covers'))) return;
this.libraryService.refreshMetadataMultipleLibraries(selected.map(l => l.id), true, false).subscribe(() => this.getLibraries());
this.bulkMode = true;
this.cdRef.markForCheck();
this.libraryService.refreshMetadataMultipleLibraries(selected.map(l => l.id), true, false).subscribe(() => {
this.getLibraries();
this.resetBulkMode();
});
break
case Action.AnalyzeFiles:
this.libraryService.analyzeFilesMultipleLibraries(selected.map(l => l.id)).subscribe(() => this.getLibraries());
this.bulkMode = true;
this.cdRef.markForCheck();
this.libraryService.analyzeFilesMultipleLibraries(selected.map(l => l.id)).subscribe(() => {
this.getLibraries();
this.resetBulkMode();
});
break;
case Action.GenerateColorScape:
this.libraryService.refreshMetadataMultipleLibraries(selected.map(l => l.id), false, true).subscribe(() => this.getLibraries());
this.bulkMode = true;
this.cdRef.markForCheck();
this.libraryService.refreshMetadataMultipleLibraries(selected.map(l => l.id), false, true).subscribe(() => {
this.getLibraries();
this.resetBulkMode();
});
break;
case Action.CopySettings:
// Remove the source library from the list
if (selected.length === 1 && selected[0].id === this.sourceCopyToLibrary!.id) {
return;
}
this.bulkMode = true;
this.cdRef.markForCheck();
const includeType = this.bulkForm.get('includeType')!.value + '' == 'true';
this.libraryService.copySettingsFromLibrary(this.sourceCopyToLibrary!.id, selected.map(l => l.id), includeType).subscribe(() => this.getLibraries());
this.libraryService.copySettingsFromLibrary(this.sourceCopyToLibrary!.id, selected.map(l => l.id), includeType).subscribe(() => {
this.getLibraries();
this.resetBulkMode();
});
break;
}
}
async handleBulkAction(action: ActionItem<Library>, library : Library | null) {
this.bulkMode = true;
this.bulkAction = action.action;
this.cdRef.markForCheck();
@ -252,9 +270,11 @@ export class ManageLibraryComponent implements OnInit {
case(Action.RefreshMetadata):
case(Action.GenerateColorScape):
case (Action.Delete):
case (Action.AnalyzeFiles):
await this.applyBulkAction();
break;
case (Action.CopySettings):
// Prompt the user for the library then wait for them to manually trigger applyBulkAction
const ref = this.modalService.open(CopySettingsFromLibraryModalComponent, {size: 'lg', fullscreen: 'md'});
ref.componentInstance.libraries = this.libraries;

View file

@ -39,7 +39,7 @@
@if (count > 1) {
<div class="count">
<span class="badge bg-primary">{{count}}</span>
<span class="badge bg-primary">{{count | compactNumber}}</span>
</div>
}
@ -76,7 +76,7 @@
<span class="card-format">
@if (showFormat) {
<app-series-format [format]="format"></app-series-format>
}
}
</span>
<span class="card-title" placement="top" id="{{title}}_{{entity.id}}" [ngbTooltip]="tooltipTitle" (click)="handleClick($event)" tabindex="0">
@ -84,16 +84,15 @@
<span class="me-1"><app-promoted-icon [promoted]="isPromoted"></app-promoted-icon></span>
}
@if (linkUrl) {
<a class="dark-exempt btn-icon" href="javascript:void(0);" [routerLink]="linkUrl">{{title}}</a>
<a class="dark-exempt btn-icon" [routerLink]="linkUrl">{{title}}</a>
} @else {
{{title}}
}
</span>
<span class="card-actions">
@if (actions && actions.length > 0) {
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="title"></app-card-actionables>
}
</span>
</div>

View file

@ -47,6 +47,7 @@ import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
import {PromotedIconComponent} from "../../shared/_components/promoted-icon/promoted-icon.component";
import {SeriesFormatComponent} from "../../shared/series-format/series-format.component";
import {BrowsePerson} from "../../_models/person/browse-person";
import {CompactNumberPipe} from "../../_pipes/compact-number.pipe";
export type CardEntity = Series | Volume | Chapter | UserCollection | PageBookmark | RecentlyAddedItem | NextExpectedChapter | BrowsePerson;
@ -70,7 +71,8 @@ export type CardEntity = Series | Volume | Chapter | UserCollection | PageBookma
PromotedIconComponent,
SeriesFormatComponent,
DecimalPipe,
NgTemplateOutlet
NgTemplateOutlet,
CompactNumberPipe
],
templateUrl: './card-item.component.html',
styleUrls: ['./card-item.component.scss'],
@ -257,6 +259,8 @@ export class CardItemComponent implements OnInit {
}
this.cdRef.markForCheck();
} else {
this.tooltipTitle = this.title;
}

View file

@ -1,27 +1,28 @@
<div class="card-item-container card">
<div class="overlay">
<app-image [styles]="{'border-radius': '.25rem .25rem 0 0'}" height="232.91px" width="160px" classes="extreme-blur"
[imageUrl]="imageUrl"></app-image>
<ng-container *transloco="let t; read: 'next-expected-card'">
<div class="card-item-container card">
<div class="overlay">
<app-image [styles]="{'border-radius': '.25rem .25rem 0 0'}" height="232.91px" width="160px" classes="extreme-blur"
[imageUrl]="imageUrl"></app-image>
<div class="card-overlay"></div>
</div>
<div class="card-overlay"></div>
</div>
@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>
@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>{{t('upcoming-title')}}</div>
<span [innerHTML]="info"></span>
</div>
</div>
</div>
}
}
}
<div class="card-title-container">
<span class="card-title" tabindex="0">
{{title}}
</span>
<div class="card-title-container">
<span class="card-title" tabindex="0">
{{title}}
</span>
</div>
</div>
</div>
</ng-container>

View file

@ -1,22 +1,10 @@
@use '../../../card-item-common';
::ng-deep .extreme-blur {
filter: brightness(50%) blur(4px)
:host ::ng-deep .extreme-blur {
filter: brightness(50%) blur(4px);
}
.overlay-information {
background-color: transparent;
.card-title-container {
justify-content: center;
}
.upcoming-header {
font-size: 0.8rem;
font-weight: bold;
}
.card-title {
width: 146px;
}
.card-content {
font-size: 0.8rem;
}

View file

@ -3,12 +3,12 @@ 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";
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
import {translate} from "@jsverse/transloco";
import {translate, TranslocoDirective} from "@jsverse/transloco";
@Component({
selector: 'app-next-expected-card',
standalone: true,
imports: [ImageComponent, SafeHtmlPipe],
imports: [ImageComponent, SafeHtmlPipe, TranslocoDirective],
templateUrl: './next-expected-card.component.html',
styleUrl: './next-expected-card.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush

View file

@ -17,6 +17,7 @@
<app-card-item [title]="item.title" [entity]="item" [actions]="collectionTagActions"
[imageUrl]="imageService.getCollectionCoverImage(item.id)"
[linkUrl]="'/collections/' + item.id"
[count]="item.itemCount"
(clicked)="loadCollection(item)"
(selection)="bulkSelectionService.handleCardSelection('collection', position, collections.length, $event)"
[selected]="bulkSelectionService.isCardSelected('collection', position)" [allowSelection]="true" [showFormat]="false">

View file

@ -3,7 +3,9 @@
<div class="row g-0">
<div class="col-md-3 me-2 col-10 mb-2">
<select class="form-select me-2" formControlName="input">
<option *ngFor="let field of availableFields" [value]="field">{{field | filterField}}</option>
@for (field of availableFields; track field) {
<option [value]="field">{{field | filterField}}</option>
}
</select>
</div>
@ -16,19 +18,19 @@
</div>
<div class="col-md-4 col-10 mb-2">
@if (formGroup.get('comparison')?.value != FilterComparison.IsEmpty) {
<ng-container *ngIf="predicateType$ | async as predicateType">
<ng-container [ngSwitch]="predicateType">
<ng-container *ngSwitchCase="PredicateType.Text">
@if (formGroup.get('comparison')?.value !== FilterComparison.IsEmpty) {
@if (predicateType$ | async; as predicateType) {
@switch (predicateType) {
@case (PredicateType.Text) {
<input type="text" class="form-control me-2" autocomplete="true" formControlName="filterValue">
</ng-container>
<ng-container *ngSwitchCase="PredicateType.Number">
}
@case (PredicateType.Number) {
<input type="number" inputmode="numeric" class="form-control me-2" formControlName="filterValue" min="0">
</ng-container>
<ng-container *ngSwitchCase="PredicateType.Boolean">
}
@case (PredicateType.Boolean) {
<input type="checkbox" class="form-check-input mt-2 me-2" style="font-size: 1.5rem" formControlName="filterValue">
</ng-container>
<ng-container *ngSwitchCase="PredicateType.Date">
}
@case (PredicateType.Date) {
<div class="input-group">
<input
class="form-control"
@ -42,27 +44,21 @@
/>
<button class="btn btn-outline-secondary fa-solid fa-calendar-days" (click)="d.toggle()" type="button"></button>
</div>
</ng-container>
<ng-container *ngSwitchCase="PredicateType.Dropdown">
<ng-container *ngIf="dropdownOptions$ | async as opts">
<ng-container *ngTemplateOutlet="dropdown; context: { options: opts, multipleAllowed: MultipleDropdownAllowed }"></ng-container>
<ng-template #dropdown let-options="options" let-multipleAllowed="multipleAllowed">
<select2 [data]="options"
formControlName="filterValue"
[hideSelectedItems]="true"
[multiple]="multipleAllowed"
[infiniteScroll]="true"
[resettable]="true">
</select2>
</ng-template>
</ng-container>
</ng-container>
</ng-container>
</ng-container>
}
@case (PredicateType.Dropdown) {
@if (dropdownOptions$ | async; as opts) {
<select2 [data]="opts"
formControlName="filterValue"
[hideSelectedItems]="true"
[multiple]="MultipleDropdownAllowed"
[infiniteScroll]="true"
[resettable]="true">
</select2>
}
}
}
}
}
</div>
<div class="col pt-2 ms-2">

View file

@ -19,7 +19,7 @@ import {LibraryService} from 'src/app/_services/library.service';
import {CollectionTagService} from 'src/app/_services/collection-tag.service';
import {FilterComparison} from 'src/app/_models/metadata/v2/filter-comparison';
import {allFields, FilterField} from 'src/app/_models/metadata/v2/filter-field';
import {AsyncPipe, NgForOf, NgIf, NgSwitch, NgSwitchCase, NgTemplateOutlet} from "@angular/common";
import {AsyncPipe, NgTemplateOutlet} from "@angular/common";
import {FilterFieldPipe} from "../../../_pipes/filter-field.pipe";
import {FilterComparisonPipe} from "../../../_pipes/filter-comparison.pipe";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@ -56,17 +56,22 @@ const unitLabels: Map<FilterField, FilterRowUi> = new Map([
[FilterField.AverageRating, new FilterRowUi('unit-average-rating')],
[FilterField.ReadProgress, new FilterRowUi('unit-reading-progress')],
[FilterField.UserRating, new FilterRowUi('unit-user-rating')],
[FilterField.ReadLast, new FilterRowUi('unit-read-last')],
]);
const StringFields = [FilterField.SeriesName, FilterField.Summary, FilterField.Path, FilterField.FilePath];
const NumberFields = [FilterField.ReadTime, FilterField.ReleaseYear, FilterField.ReadProgress, FilterField.UserRating, FilterField.AverageRating];
const DropdownFields = [FilterField.PublicationStatus, FilterField.Languages, FilterField.AgeRating,
FilterField.Translators, FilterField.Characters, FilterField.Publisher,
FilterField.Editor, FilterField.CoverArtist, FilterField.Letterer,
FilterField.Colorist, FilterField.Inker, FilterField.Penciller,
FilterField.Writers, FilterField.Genres, FilterField.Libraries,
FilterField.Formats, FilterField.CollectionTags, FilterField.Tags,
FilterField.Imprint, FilterField.Team, FilterField.Location
const NumberFields = [
FilterField.ReadTime, FilterField.ReleaseYear, FilterField.ReadProgress,
FilterField.UserRating, FilterField.AverageRating, FilterField.ReadLast
];
const DropdownFields = [
FilterField.PublicationStatus, FilterField.Languages, FilterField.AgeRating,
FilterField.Translators, FilterField.Characters, FilterField.Publisher,
FilterField.Editor, FilterField.CoverArtist, FilterField.Letterer,
FilterField.Colorist, FilterField.Inker, FilterField.Penciller,
FilterField.Writers, FilterField.Genres, FilterField.Libraries,
FilterField.Formats, FilterField.CollectionTags, FilterField.Tags,
FilterField.Imprint, FilterField.Team, FilterField.Location
];
const BooleanFields = [FilterField.WantToRead];
const DateFields = [FilterField.ReadingDate];
@ -89,7 +94,6 @@ const FieldsThatShouldIncludeIsEmpty = [
FilterField.Colorist, FilterField.Inker, FilterField.Penciller,
FilterField.Writers, FilterField.Imprint, FilterField.Team,
FilterField.Location,
];
const StringComparisons = [
@ -130,10 +134,6 @@ const BooleanComparisons = [
AsyncPipe,
FilterFieldPipe,
FilterComparisonPipe,
NgSwitch,
NgSwitchCase,
NgForOf,
NgIf,
Select2Module,
NgTemplateOutlet,
TagBadgeComponent,
@ -159,6 +159,8 @@ export class MetadataFilterRowComponent implements OnInit {
private readonly destroyRef = inject(DestroyRef);
private readonly dateParser = inject(NgbDateParserFormatter);
protected readonly FilterComparison = FilterComparison;
formGroup: FormGroup = new FormGroup({
'comparison': new FormControl<FilterComparison>(FilterComparison.Equal, []),
'filterValue': new FormControl<string | number>('', []),
@ -425,12 +427,10 @@ export class MetadataFilterRowComponent implements OnInit {
onDateSelect(event: NgbDate) {
onDateSelect(_: NgbDate) {
this.propagateFilterUpdate();
}
updateIfDateFilled() {
this.propagateFilterUpdate();
}
protected readonly FilterComparison = FilterComparison;
}

View file

@ -22,11 +22,14 @@
>
<ng-template #cardItem let-item let-position="idx" >
<app-card-item [title]="item.title" [entity]="item" [actions]="actions[item.id]"
[suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)"
[suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)"
[linkUrl]="'/lists/' + item.id"
[count]="item.itemCount"
(clicked)="handleClick(item)"
(selection)="bulkSelectionService.handleCardSelection('readingList', position, lists.length, $event)"
[selected]="bulkSelectionService.isCardSelected('readingList', position)" [allowSelection]="true" [showFormat]="false"></app-card-item>
[selected]="bulkSelectionService.isCardSelected('readingList', position)"
[allowSelection]="true" [showFormat]="false"
(selection)="bulkSelectionService.handleCardSelection('readingList', position, lists.length, $event)">
</app-card-item>
</ng-template>
<ng-template #noData>

View file

@ -7,6 +7,8 @@
<div class="position-relative d-inline-block" (click)="openGeneric(FilterField.Publisher, entity.publishers[0].id)">{{entity.publishers[0].name}}</div>
</div>
}
<!-- TODO: Figure out if I can implement this animation (ROBBIE)-->
<!-- <app-publisher-flipper [publishers]="entity.publishers"></app-publisher-flipper>-->
<span class="me-2">
<app-age-rating-image [rating]="ageRating"></app-age-rating-image>
</span>

View file

@ -17,4 +17,4 @@
.word-count {
font-size: 0.8rem;
}
}

View file

@ -19,6 +19,7 @@ import {MangaFormatPipe} from "../../../_pipes/manga-format.pipe";
import {MangaFormat} from "../../../_models/manga-format";
import {MangaFormatIconPipe} from "../../../_pipes/manga-format-icon.pipe";
import {SeriesFormatComponent} from "../../../shared/series-format/series-format.component";
import {PublisherFlipperComponent} from "../../../_single-modules/publisher-flipper/publisher-flipper.component";
@Component({
selector: 'app-metadata-detail-row',
@ -33,7 +34,8 @@ import {SeriesFormatComponent} from "../../../shared/series-format/series-format
ImageComponent,
MangaFormatPipe,
MangaFormatIconPipe,
SeriesFormatComponent
SeriesFormatComponent,
PublisherFlipperComponent
],
templateUrl: './metadata-detail-row.component.html',
styleUrl: './metadata-detail-row.component.scss',

View file

@ -1,7 +1,9 @@
import {
AsyncPipe,
DecimalPipe,
DOCUMENT, JsonPipe, Location,
DOCUMENT,
JsonPipe,
Location,
NgClass,
NgOptimizedImage,
NgStyle,
@ -35,19 +37,17 @@ import {
NgbNavItem,
NgbNavLink,
NgbNavOutlet,
NgbOffcanvas,
NgbProgressbar,
NgbTooltip
} from '@ng-bootstrap/ng-bootstrap';
import {ToastrService} from 'ngx-toastr';
import {catchError, forkJoin, Observable, of, shareReplay, tap} from 'rxjs';
import {catchError, forkJoin, Observable, of, tap} from 'rxjs';
import {map} from 'rxjs/operators';
import {BulkSelectionService} from 'src/app/cards/bulk-selection.service';
import {
EditSeriesModalCloseResult,
EditSeriesModalComponent
} from 'src/app/cards/_modals/edit-series-modal/edit-series-modal.component';
import {TagBadgeCursor} from 'src/app/shared/tag-badge/tag-badge.component';
import {DownloadEvent, DownloadService} from 'src/app/shared/_services/download.service';
import {Breakpoint, KEY_CODES, UtilityService} from 'src/app/shared/_services/utility.service';
import {Chapter, LooseLeafOrDefaultNumber, SpecialVolumeNumber} from 'src/app/_models/chapter';
@ -65,7 +65,6 @@ import {Volume} from 'src/app/_models/volume';
import {AccountService} from 'src/app/_services/account.service';
import {Action, ActionFactoryService, ActionItem} from 'src/app/_services/action-factory.service';
import {ActionService} from 'src/app/_services/action.service';
import {DeviceService} from 'src/app/_services/device.service';
import {ImageService} from 'src/app/_services/image.service';
import {LibraryService} from 'src/app/_services/library.service';
import {EVENTS, MessageHubService} from 'src/app/_services/message-hub.service';
@ -704,6 +703,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
this.cdRef.markForCheck();
if (![PublicationStatus.Ended, PublicationStatus.OnGoing].includes(this.seriesMetadata.publicationStatus)) return;
this.seriesService.getNextExpectedChapterDate(seriesId).subscribe(date => {
if (date == null || date.expectedDate === null) {
if (this.nextExpectedChapter !== undefined) {
@ -716,7 +716,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
this.nextExpectedChapter = date;
this.cdRef.markForCheck();
})
});
});
this.seriesService.isWantToRead(seriesId).subscribe(isWantToRead => {
@ -850,7 +850,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
shouldShowStorylineTab() {
if (this.libraryType === LibraryType.ComicVine) return false;
// Edge case for bad pdf parse
if (this.libraryType === LibraryType.Book && (this.volumes.length === 0 && this.chapters.length === 0 && this.storyChapters.length > 0)) return true;
if ((this.libraryType === LibraryType.Book || this.libraryType === LibraryType.LightNovel) && (this.volumes.length === 0 && this.chapters.length === 0 && this.storyChapters.length > 0)) return true;
return (this.libraryType !== LibraryType.Book && this.libraryType !== LibraryType.LightNovel && this.libraryType !== LibraryType.Comic)
&& (this.volumes.length > 0 || this.chapters.length > 0);

View file

@ -11,6 +11,7 @@ import {TranslocoDirective} from "@jsverse/transloco";
import {NgTemplateOutlet} from "@angular/common";
import {SafeHtmlPipe} from "../../../_pipes/safe-html.pipe";
import {filter, fromEvent, tap} from "rxjs";
import {AbstractControl, FormControl} from "@angular/forms";
@Component({
selector: 'app-setting-item',
@ -36,6 +37,7 @@ export class SettingItemComponent {
@Input() subtitle: string | undefined = undefined;
@Input() labelId: string | undefined = undefined;
@Input() toggleOnViewClick: boolean = true;
@Input() control: AbstractControl<any> | null = null;
@Output() editMode = new EventEmitter<boolean>();
/**
@ -67,6 +69,7 @@ export class SettingItemComponent {
.pipe(
filter((event: Event) => {
if (!this.toggleOnViewClick) return false;
if (this.control != null && this.control.invalid) return false;
const mouseEvent = event as MouseEvent;
const selection = window.getSelection();
@ -86,6 +89,7 @@ export class SettingItemComponent {
if (!this.toggleOnViewClick) return;
if (!this.canEdit) return;
if (this.control != null && this.control.invalid) return;
this.isEditMode = !this.isEditMode;
this.editMode.emit(this.isEditMode);

View file

@ -252,6 +252,7 @@ export class TypeaheadComponent implements OnInit {
case KEY_CODES.ESC_KEY:
this.hasFocus = false;
event.stopPropagation();
event.preventDefault();
break;
default:
break;

View file

@ -1156,7 +1156,7 @@
"reset": "{{common.reset}}",
"test": "Test",
"host-name-label": "Host Name",
"host-name-tooltip": "Domain Name (of Reverse Proxy). If set, email generation will always use this.",
"host-name-tooltip": "Domain Name (of Reverse Proxy). Required for email functionality. If no reverse proxy, use any url.",
"host-name-validation": "Host name must start with http(s) and not end in /",
"sender-address-label": "Sender Address",
@ -1207,6 +1207,7 @@
"bulk-action-label": "Bulk Action"
},
"copy-settings-from-library-modal": {
"close": "{{common.close}}",
"select": "Select",
@ -1831,7 +1832,8 @@
"unit-reading-date": "Date",
"unit-average-rating": "Kavita+ external rating, percent",
"unit-reading-progress": "Percent",
"unit-user-rating": "{{metadata-filter-row.unit-reading-progress}}"
"unit-user-rating": "{{metadata-filter-row.unit-reading-progress}}",
"unit-read-last": "Days from TODAY"
},
"sort-field-pipe": {
@ -2097,7 +2099,8 @@
},
"next-expected-card": {
"title": "~{{date}}"
"title": "~{{date}}",
"upcoming-title": "Upcoming"
},
"server-stats": {
@ -2270,6 +2273,7 @@
"file-path": "File Path",
"want-to-read": "Want to Read",
"read-date": "Reading Date",
"read-last": "Read Last",
"average-rating": "Average Rating"
},