diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.html b/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.html index 7d128880b..8a3411164 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.html +++ b/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.html @@ -13,12 +13,12 @@
- + @for (filterType of filterTypes; track trackByFilterType(i, filterType); let i = $index) { @if (filterStreams[filterType] | async; as filters) { @if (filters.length > 0) { - - + + {{ filterLabels[filterType] || (filterType | titlecase) }} @if (activeFilters[filterType]?.length) { @@ -27,29 +27,33 @@ } - + - -
- @for (filter of filters; track trackByFilter(j, filter); let j = $index) { -
+
- {{ getFilterValueDisplay(filter) }} - -
- } - @if (truncatedFilters[filterType]) { -
- Showing first 500 items -
- } -
- - + (click)="handleFilterClick(filterType, getFilterValueId(filter))"> + {{ getFilterValueDisplay(filter) }} + +
+ + @if (truncatedFilters[filterType]) { +
+ Showing first 500 items +
+ } + } +
+
} } } diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.scss b/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.scss index cb174cf12..cb0ec710e 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.scss +++ b/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.scss @@ -51,16 +51,13 @@ } .filter-list { - max-height: 27.5rem; - overflow-y: auto; - overscroll-behavior: contain; padding-right: 0.25rem; } .filter-row { cursor: pointer; transition: colors 200ms ease-in-out; - padding-bottom: 0.25rem; + height: 26px; display: flex; flex-direction: row; align-items: center; diff --git a/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.ts b/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.ts index 957e4418c..aef33bfaf 100644 --- a/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.ts +++ b/booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.ts @@ -7,6 +7,7 @@ import {Shelf} from '../../../model/shelf.model'; import {EntityType} from '../book-browser.component'; import {Book, ReadStatus} from '../../../model/book.model'; import {Accordion, AccordionContent, AccordionHeader, AccordionPanel} from 'primeng/accordion'; +import {CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollViewport} from '@angular/cdk/scrolling'; import {AsyncPipe, NgClass, TitleCasePipe} from '@angular/common'; import {Badge} from 'primeng/badge'; import {FormsModule} from '@angular/forms'; @@ -28,8 +29,6 @@ export interface Filter { } export type FilterType = - | 'library' - | 'shelf' | 'author' | 'category' | 'series' @@ -42,13 +41,10 @@ export type FilterType = | 'tag' | 'language' | 'bookType' - | 'shelfStatus' | 'fileSize' | 'pageCount' | 'amazonRating' - | 'goodreadsRating' - | 'ranobedbRating' - | 'hardcoverRating'; + | 'goodreadsRating'; export const ratingRanges = [ {id: '0to1', label: '0 to 1', min: 0, max: 1, sortIndex: 0}, @@ -176,6 +172,9 @@ function getReadStatusName(status?: ReadStatus | null): string { AccordionPanel, AccordionHeader, AccordionContent, + CdkVirtualScrollViewport, + CdkFixedSizeVirtualScroll, + CdkVirtualForOf, NgClass, Badge, AsyncPipe, @@ -207,8 +206,6 @@ export class BookFilterComponent implements OnInit, OnDestroy { private _selectedFilterMode: BookFilterMode = 'and'; expandedPanels: number[] = [0]; readonly filterLabels: Record = { - library: 'Library', - shelf: 'Shelf', author: 'Author', category: 'Genre', series: 'Series', @@ -221,13 +218,10 @@ export class BookFilterComponent implements OnInit, OnDestroy { tag: 'Tag', language: 'Language', bookType: 'Book Type', - shelfStatus: 'Shelf Status', fileSize: 'File Size', pageCount: 'Page Count', amazonRating: 'Amazon Rating', - goodreadsRating: 'Goodreads Rating', - hardcoverRating: 'Hardcover Rating', - ranobedbRating: 'Ranobedb Rating', + goodreadsRating: 'Goodreads Rating' }; private destroy$ = new Subject(); @@ -253,14 +247,6 @@ export class BookFilterComponent implements OnInit, OnDestroy { .pipe(takeUntil(this.destroy$)) .subscribe(() => { this.filterStreams = { - library: this.getFilterStream( - (book) => (book.libraryId ? [{id: book.libraryId, name: book.libraryName}] : []), - 'id', 'name' - ), - shelf: this.getFilterStream( - (book) => (book.shelves ? book.shelves.map(s => ({id: s.id, name: s.name})) : []), - 'id', 'name' - ), author: this.getFilterStream( (book: Book) => Array.isArray(book.metadata?.authors) ? book.metadata.authors.map(name => ({id: name, name})) : [], 'id', 'name' @@ -297,13 +283,10 @@ export class BookFilterComponent implements OnInit, OnDestroy { ), language: this.getFilterStream(getLanguageFilter, 'id', 'name'), bookType: this.getFilterStream(getBookTypeFilter, 'id', 'name'), - shelfStatus: this.getFilterStream(getShelfStatusFilter, 'id', 'name'), fileSize: this.getFilterStream((book: Book) => getFileSizeRangeFilters(book.fileSizeKb), 'id', 'name', 'sortIndex'), pageCount: this.getFilterStream((book: Book) => getPageCountRangeFilters(book.metadata?.pageCount ?? undefined), 'id', 'name', 'sortIndex'), amazonRating: this.getFilterStream((book: Book) => getRatingRangeFilters(book.metadata?.amazonRating ?? undefined), 'id', 'name', 'sortIndex'), - goodreadsRating: this.getFilterStream((book: Book) => getRatingRangeFilters(book.metadata?.goodreadsRating ?? undefined), 'id', 'name', 'sortIndex'), - hardcoverRating: this.getFilterStream((book: Book) => getRatingRangeFilters(book.metadata?.hardcoverRating ?? undefined), 'id', 'name', 'sortIndex'), - ranobedbRating: this.getFilterStream((book: Book) => getRatingRangeFilters(book.metadata?.ranobedbRating ?? undefined), 'id', 'name', 'sortIndex'), + goodreadsRating: this.getFilterStream((book: Book) => getRatingRangeFilters(book.metadata?.goodreadsRating ?? undefined), 'id', 'name', 'sortIndex') }; this.filterTypes = Object.keys(this.filterStreams) as FilterType[]; @@ -460,18 +443,28 @@ export class BookFilterComponent implements OnInit, OnDestroy { clearActiveFilter() { this.activeFilters = {}; - this.setExpandedPanels(); + this.expandedPanels = [0]; this.filterChangeSubject.next(null); } + onExpandedPanelsChange(value: string | number | string[] | number[] | null | undefined): void { + if (Array.isArray(value)) { + this.expandedPanels = value.map(v => Number(v)); + } + } + + getVirtualScrollHeight(itemCount: number): number { + return Math.min(itemCount * 28, 440); + } + setExpandedPanels(): void { - const indexes = []; + const current = new Set(this.expandedPanels); for (let i = 0; i < this.filterTypes.length; i++) { if (this.activeFilters[this.filterTypes[i]]?.length) { - indexes.push(i); + current.add(i); } } - this.expandedPanels = indexes.length > 0 ? indexes : [0]; + this.expandedPanels = current.size > 0 ? [...current] : [0]; } onFiltersChanged(): void { @@ -484,18 +477,18 @@ export class BookFilterComponent implements OnInit, OnDestroy { trackByFilter(_: number, filter: Filter): unknown { const value = filter.value as { id?: unknown } | unknown; - return (typeof value === 'object' && value !== null && 'id' in value) ? (value as {id: unknown}).id : filter.value; + return (typeof value === 'object' && value !== null && 'id' in value) ? (value as { id: unknown }).id : filter.value; } getFilterValueId(filter: Filter): unknown { const value = filter.value as { id?: unknown } | unknown; - return (typeof value === 'object' && value !== null && 'id' in value) ? (value as {id: unknown}).id : filter.value; + return (typeof value === 'object' && value !== null && 'id' in value) ? (value as { id: unknown }).id : filter.value; } getFilterValueDisplay(filter: Filter): string { const value = filter.value as { name?: string } | string | unknown; if (typeof value === 'object' && value !== null && 'name' in value) { - return String((value as {name: string}).name ?? ''); + return String((value as { name: string }).name ?? ''); } return String(value ?? ''); }