diff --git a/booklore-ui/src/app/book/components/book-browser/book-browser.component.html b/booklore-ui/src/app/book/components/book-browser/book-browser.component.html
index 0db905c40..fea36348c 100644
--- a/booklore-ui/src/app/book/components/book-browser/book-browser.component.html
+++ b/booklore-ui/src/app/book/components/book-browser/book-browser.component.html
@@ -44,7 +44,8 @@
+ tooltipPosition="top"
+ style="color: orange;">
}
@@ -83,7 +84,6 @@
}
-
+
@@ -217,15 +226,13 @@
}
-
+
+
+
+
@@ -396,19 +403,23 @@
}
+
@if (this.showFilter) {
+
+
}
-
-
+
diff --git a/booklore-ui/src/app/book/components/book-browser/book-browser.component.ts b/booklore-ui/src/app/book/components/book-browser/book-browser.component.ts
index fa0c7e1b7..7a712172f 100644
--- a/booklore-ui/src/app/book/components/book-browser/book-browser.component.ts
+++ b/booklore-ui/src/app/book/components/book-browser/book-browser.component.ts
@@ -1,4 +1,4 @@
-import {AfterViewInit, ChangeDetectorRef, Component, inject, OnInit, ViewChild} from '@angular/core';
+import {Component, inject, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {ConfirmationService, MenuItem, MessageService, PrimeTemplate} from 'primeng/api';
import {LibraryService} from '../../service/library.service';
@@ -46,6 +46,7 @@ import {BookMenuService} from '../../service/book-menu.service';
import {MagicShelf, MagicShelfService} from '../../../magic-shelf.service';
import {BookRuleEvaluatorService} from '../../../book-rule-evaluator.service';
import {GroupRule} from '../../../magic-shelf-component/magic-shelf-component';
+import {SidebarFilterTogglePrefService} from './filters/sidebar-filter-toggle-pref-service';
export enum EntityType {
LIBRARY = 'Library',
@@ -87,18 +88,19 @@ const SORT_DIRECTION = {
providers: [SeriesCollapseFilter],
animations: [
trigger('slideInOut', [
- state('void', style({ transform: 'translateY(100%)' })),
- state('*', style({ transform: 'translateY(0)' })),
- transition(':enter', [ animate('0.1s ease-in') ]),
- transition(':leave', [ animate('0.1s ease-out') ])
+ state('void', style({transform: 'translateY(100%)'})),
+ state('*', style({transform: 'translateY(0)'})),
+ transition(':enter', [animate('0.1s ease-in')]),
+ transition(':leave', [animate('0.1s ease-out')])
])
]
})
-export class BookBrowserComponent implements OnInit, AfterViewInit {
+export class BookBrowserComponent implements OnInit {
protected userService = inject(UserService);
protected coverScalePreferenceService = inject(CoverScalePreferenceService);
protected filterSortPreferenceService = inject(FilterSortPreferenceService);
protected columnPreferenceService = inject(TableColumnPreferenceService);
+ protected sidebarFilterTogglePrefService = inject(SidebarFilterTogglePrefService);
private activatedRoute = inject(ActivatedRoute);
private messageService = inject(MessageService);
private libraryService = inject(LibraryService);
@@ -108,7 +110,6 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
private bookMenuService = inject(BookMenuService);
private sortService = inject(SortService);
private router = inject(Router);
- private changeDetectorRef = inject(ChangeDetectorRef);
private libraryShelfMenuService = inject(LibraryShelfMenuService);
protected seriesCollapseFilter = inject(SeriesCollapseFilter);
protected confirmationService = inject(ConfirmationService);
@@ -148,14 +149,30 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
private headerFilter = new HeaderFilter(this.searchTerm$);
protected bookSorter = new BookSorter(selectedSort => this.applySortOption(selectedSort));
- @ViewChild(BookTableComponent) bookTableComponent!: BookTableComponent;
- @ViewChild(BookFilterComponent) bookFilterComponent!: BookFilterComponent;
+ @ViewChild(BookTableComponent)
+ bookTableComponent!: BookTableComponent;
+ @ViewChild(BookFilterComponent, { static: false })
+ bookFilterComponent!: BookFilterComponent;
- get currentCardSize() { return this.coverScalePreferenceService.currentCardSize; }
- get gridColumnMinWidth(): string { return this.coverScalePreferenceService.gridColumnMinWidth; }
- get viewIcon(): string { return this.currentViewMode === VIEW_MODES.GRID ? 'pi pi-objects-column' : 'pi pi-table'; }
- get hasSidebarFilters(): boolean { return !!this.selectedFilter.value && Object.keys(this.selectedFilter.value).length > 0; }
- get isFilterActive(): boolean { return this.selectedFilter.value !== null; }
+ get currentCardSize() {
+ return this.coverScalePreferenceService.currentCardSize;
+ }
+
+ get gridColumnMinWidth(): string {
+ return this.coverScalePreferenceService.gridColumnMinWidth;
+ }
+
+ get viewIcon(): string {
+ return this.currentViewMode === VIEW_MODES.GRID ? 'pi pi-objects-column' : 'pi pi-table';
+ }
+
+ get hasSidebarFilters(): boolean {
+ return !!this.selectedFilter.value && Object.keys(this.selectedFilter.value).length > 0;
+ }
+
+ get isFilterActive(): boolean {
+ return this.selectedFilter.value !== null;
+ }
ngOnInit(): void {
this.coverScalePreferenceService.scaleChange$.pipe(debounceTime(1000)).subscribe();
@@ -197,23 +214,22 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
() => this.multiBookEditMetadata()
);
this.tieredMenuItems = this.bookMenuService.getTieredMenuItems(this.selectedBooks);
- }
- ngAfterViewInit(): void {
- combineLatest({
- paramMap: this.activatedRoute.queryParamMap,
- user: this.userService.userState$.pipe(
- filter(userState => !!userState?.user && userState.loaded),
- take(1)
- )
- }).subscribe(({paramMap, user}) => {
+ // --- NEW: Subscribe to query params + user changes for reactive updates ---
+ combineLatest([
+ this.activatedRoute.paramMap,
+ this.activatedRoute.queryParamMap,
+ this.userService.userState$.pipe(filter(u => !!u?.user && u.loaded))
+ ]).subscribe(([paramMap, queryParamMap, user]) => {
- const viewParam = paramMap.get(QUERY_PARAMS.VIEW);
- const sortParam = paramMap.get(QUERY_PARAMS.SORT);
- const directionParam = paramMap.get(QUERY_PARAMS.DIRECTION);
- const filterParams = paramMap.get(QUERY_PARAMS.FILTER);
- const sidebarParam = paramMap.get(QUERY_PARAMS.SIDEBAR);
- this.showFilter = sidebarParam === null ? true : sidebarParam === 'true';
+ const viewParam = queryParamMap.get(QUERY_PARAMS.VIEW);
+ const sortParam = queryParamMap.get(QUERY_PARAMS.SORT);
+ const directionParam = queryParamMap.get(QUERY_PARAMS.DIRECTION);
+ const filterParams = queryParamMap.get(QUERY_PARAMS.FILTER);
+
+ this.sidebarFilterTogglePrefService.showFilter$.subscribe(value => {
+ this.showFilter = value;
+ });
const parsedFilters: Record = {};
@@ -229,8 +245,10 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
});
this.selectedFilter.next(parsedFilters);
- this.bookFilterComponent.setFilters?.(parsedFilters);
- this.bookFilterComponent.onFiltersChanged?.();
+ if(this.bookFilterComponent) {
+ this.bookFilterComponent.setFilters?.(parsedFilters);
+ this.bookFilterComponent.onFiltersChanged?.();
+ }
const firstFilter = filterParams.split(',')[0];
const [key, ...values] = firstFilter.split(':');
@@ -256,7 +274,6 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
this.columnPreferenceService.initPreferences(user.user?.userSettings?.tableColumnPreference);
this.visibleColumns = this.columnPreferenceService.visibleColumns;
-
const override = this.entityViewPreferences?.overrides?.find(o =>
o.entityType?.toUpperCase() === currentEntityTypeStr &&
o.entityId === this.entity?.id
@@ -289,7 +306,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
direction: SortDirection.DESCENDING
};
- const fromParam = paramMap.get(QUERY_PARAMS.FROM);
+ const fromParam = queryParamMap.get(QUERY_PARAMS.FROM);
this.currentViewMode = fromParam === 'toggle'
? (viewParam === VIEW_MODES.TABLE || viewParam === VIEW_MODES.GRID
? viewParam
@@ -306,8 +323,7 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
const queryParams: any = {
[QUERY_PARAMS.VIEW]: this.currentViewMode,
[QUERY_PARAMS.SORT]: this.bookSorter.selectedSort.field,
- [QUERY_PARAMS.DIRECTION]: this.bookSorter.selectedSort.direction === SortDirection.ASCENDING ? SORT_DIRECTION.ASCENDING : SORT_DIRECTION.DESCENDING,
- [QUERY_PARAMS.SIDEBAR]: this.showFilter.toString()
+ [QUERY_PARAMS.DIRECTION]: this.bookSorter.selectedSort.direction === SortDirection.ASCENDING ? SORT_DIRECTION.ASCENDING : SORT_DIRECTION.DESCENDING
};
if (Object.keys(parsedFilters).length > 0) {
@@ -316,29 +332,13 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
const currentParams = this.activatedRoute.snapshot.queryParams;
const changed = Object.keys(queryParams).some(k => currentParams[k] !== queryParams[k]);
-
if (changed) {
+ const mergedParams = {...currentParams, ...queryParams};
this.router.navigate([], {
- queryParams,
+ queryParams: mergedParams,
replaceUrl: true
});
}
-
- this.changeDetectorRef.detectChanges();
- });
-
- this.bookFilterComponent.filterSelected.subscribe((filters: Record | null) => {
- if (this.settingFiltersFromUrl) return;
-
- this.selectedFilter.next(filters);
- this.rawFilterParamFromUrl = null;
-
- const hasSidebarFilters = !!filters && Object.keys(filters).length > 0;
- this.currentFilterLabel = hasSidebarFilters ? 'All Books (Filtered)' : 'All Books';
- });
-
- this.bookFilterComponent.filterModeChanged.subscribe((mode: 'and' | 'or') => {
- this.selectedFilterMode.next(mode);
});
this.searchTerm$.subscribe(term => {
@@ -346,6 +346,25 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
});
}
+ onFilterSelected(filters: Record | null): void {
+ if (this.settingFiltersFromUrl) return;
+
+ this.selectedFilter.next(filters);
+ this.rawFilterParamFromUrl = null;
+
+ const hasSidebarFilters = !!filters && Object.keys(filters).length > 0;
+ this.currentFilterLabel = hasSidebarFilters ? 'All Books (Filtered)' : 'All Books';
+ }
+
+ onFilterModeChanged(mode: 'and' | 'or'): void {
+ this.selectedFilterMode.next(mode);
+ }
+
+ toggleSidebar(): void {
+ this.showFilter = !this.showFilter;
+ this.sidebarFilterTogglePrefService.selectedShowFilter = this.showFilter;
+ }
+
updateScale(): void {
this.coverScalePreferenceService.setScale(this.coverScalePreferenceService.scaleFactor);
}
@@ -476,18 +495,6 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
this.clearSearch();
}
- toggleFilterSidebar() {
- this.showFilter = !this.showFilter;
- const currentParams = this.activatedRoute.snapshot.queryParams;
- this.router.navigate([], {
- queryParams: {
- ...currentParams,
- [QUERY_PARAMS.SIDEBAR]: this.showFilter.toString()
- },
- replaceUrl: true
- });
- }
-
toggleTableGrid(): void {
this.currentViewMode = this.currentViewMode === VIEW_MODES.GRID ? VIEW_MODES.TABLE : VIEW_MODES.GRID;
this.router.navigate([], {
diff --git a/booklore-ui/src/app/book/components/book-browser/book-filter/book-filter.component.ts b/booklore-ui/src/app/book/components/book-browser/book-filter/book-filter.component.ts
index b3a1745ee..4e342f22a 100644
--- a/booklore-ui/src/app/book/components/book-browser/book-filter/book-filter.component.ts
+++ b/booklore-ui/src/app/book/components/book-browser/book-filter/book-filter.component.ts
@@ -1,5 +1,5 @@
-import {Component, EventEmitter, ChangeDetectionStrategy, inject, Input, OnDestroy, OnInit, Output} from '@angular/core';
-import {combineLatest, Observable, of, Subject, debounceTime, distinctUntilChanged, takeUntil, shareReplay} from 'rxjs';
+import {ChangeDetectionStrategy, Component, EventEmitter, inject, Input, OnDestroy, OnInit, Output} from '@angular/core';
+import {combineLatest, distinctUntilChanged, Observable, of, shareReplay, Subject, takeUntil} from 'rxjs';
import {map} from 'rxjs/operators';
import {BookService} from '../../../service/book.service';
import {Library} from '../../../model/library.model';
@@ -152,7 +152,7 @@ export class BookFilterComponent implements OnInit, OnDestroy {
private filterChangeSubject = new Subject | null>();
@Output() filterSelected = new EventEmitter | null>();
- @Output('filterMode') filterModeChanged = new EventEmitter<'and' | 'or'>();
+ @Output() filterModeChanged = new EventEmitter<'and' | 'or'>();
@Input() entity$!: Observable | undefined;
@Input() entityType$!: Observable | undefined;
@@ -204,8 +204,8 @@ export class BookFilterComponent implements OnInit, OnDestroy {
this.filterStreams = {
// Temporarily disabled until we can optimize for large libraries
/*author: this.getFilterStream((book: Book) => book.metadata?.authors!.map(name => ({id: name, name})) || [], 'id', 'name', sortMode),
- category: this.getFilterStream((book: Book) => book.metadata?.categories!.map(name => ({id: name, name})) || [], 'id', 'name', sortMode),
- series: this.getFilterStream((book) => (book.metadata?.seriesName ? [{id: book.metadata.seriesName, name: book.metadata.seriesName}] : []), 'id', 'name', sortMode),*/
+ category: this.getFilterStream((book: Book) => book.metadata?.categories!.map(name => ({id: name, name})) || [], 'id', 'name', sortMode),*/
+ series: this.getFilterStream((book) => (book.metadata?.seriesName ? [{id: book.metadata.seriesName, name: book.metadata.seriesName}] : []), 'id', 'name', sortMode),
publisher: this.getFilterStream((book) => (book.metadata?.publisher ? [{id: book.metadata.publisher, name: book.metadata.publisher}] : []), 'id', 'name', sortMode),
readStatus: this.getFilterStream((book: Book) => {
let status = book.readStatus;
@@ -231,7 +231,6 @@ export class BookFilterComponent implements OnInit, OnDestroy {
});
this.filterChangeSubject.pipe(
- debounceTime(10),
takeUntil(this.destroy$)
).subscribe(value => this.filterSelected.emit(value));
diff --git a/booklore-ui/src/app/book/components/book-browser/filters/HeaderFilter.ts b/booklore-ui/src/app/book/components/book-browser/filters/HeaderFilter.ts
index 6de686043..6fef337ab 100644
--- a/booklore-ui/src/app/book/components/book-browser/filters/HeaderFilter.ts
+++ b/booklore-ui/src/app/book/components/book-browser/filters/HeaderFilter.ts
@@ -1,7 +1,7 @@
import {BookFilter} from './BookFilter';
import {BookState} from '../../../model/state/book-state.model';
-import {Observable} from 'rxjs';
-import {map, debounceTime, distinctUntilChanged} from 'rxjs/operators';
+import {Observable, of} from 'rxjs';
+import {map, debounceTime, distinctUntilChanged, switchMap} from 'rxjs/operators';
export class HeaderFilter implements BookFilter {
@@ -13,30 +13,35 @@ export class HeaderFilter implements BookFilter {
str.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
return this.searchTerm$.pipe(
- debounceTime(500),
distinctUntilChanged(),
- map(term => {
- const normalizedTerm = normalize(term || '');
- if (normalizedTerm && normalizedTerm.trim() !== '') {
- const filteredBooks = bookState.books?.filter(book => {
- const title = book.metadata?.title || '';
- const series = book.metadata?.seriesName || '';
- const authors = book.metadata?.authors || [];
- const categories = book.metadata?.categories || [];
- const isbn = book.metadata?.isbn10 || '';
- const isbn13 = book.metadata?.isbn13 || '';
-
- const matchesTitle = normalize(title).includes(normalizedTerm);
- const matchesSeries = normalize(series).includes(normalizedTerm);
- const matchesAuthor = authors.some(author => normalize(author).includes(normalizedTerm));
- const matchesCategory = categories.some(category => normalize(category).includes(normalizedTerm));
- const matchesIsbn = normalize(isbn).includes(normalizedTerm) || normalize(isbn13).includes(normalizedTerm);
-
- return matchesTitle || matchesSeries || matchesAuthor || matchesCategory || matchesIsbn;
- }) || null;
- return {...bookState, books: filteredBooks};
+ switchMap(term => {
+ const normalizedTerm = normalize(term || '').trim();
+ if (!normalizedTerm) {
+ return of(bookState);
}
- return bookState;
+ return of(normalizedTerm).pipe(
+ debounceTime(500),
+ map(nTerm => {
+ const filteredBooks = bookState.books?.filter(book => {
+ const title = book.metadata?.title || '';
+ const series = book.metadata?.seriesName || '';
+ const authors = book.metadata?.authors || [];
+ const categories = book.metadata?.categories || [];
+ const isbn = book.metadata?.isbn10 || '';
+ const isbn13 = book.metadata?.isbn13 || '';
+
+ const matchesTitle = normalize(title).includes(nTerm);
+ const matchesSeries = normalize(series).includes(nTerm);
+ const matchesAuthor = authors.some(author => normalize(author).includes(nTerm));
+ const matchesCategory = categories.some(category => normalize(category).includes(nTerm));
+ const matchesIsbn = normalize(isbn).includes(nTerm) || normalize(isbn13).includes(nTerm);
+
+ return matchesTitle || matchesSeries || matchesAuthor || matchesCategory || matchesIsbn;
+ }) || null;
+
+ return {...bookState, books: filteredBooks};
+ })
+ );
})
);
}
diff --git a/booklore-ui/src/app/book/components/book-browser/filters/sidebar-filter-toggle-pref-service.ts b/booklore-ui/src/app/book/components/book-browser/filters/sidebar-filter-toggle-pref-service.ts
new file mode 100644
index 000000000..9ce251369
--- /dev/null
+++ b/booklore-ui/src/app/book/components/book-browser/filters/sidebar-filter-toggle-pref-service.ts
@@ -0,0 +1,56 @@
+import {inject, Injectable} from '@angular/core';
+import {BehaviorSubject} from 'rxjs';
+import {MessageService} from 'primeng/api';
+import {LocalStorageService} from '../../../../core/service/local-storage-service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class SidebarFilterTogglePrefService {
+
+ private readonly STORAGE_KEY = 'showSidebarFilter';
+ private readonly messageService = inject(MessageService);
+ private readonly localStorageService = inject(LocalStorageService);
+
+ private readonly showFilterSubject = new BehaviorSubject(true);
+ readonly showFilter$ = this.showFilterSubject.asObservable();
+
+ constructor() {
+ this.loadFromStorage();
+ }
+
+ get selectedShowFilter(): boolean {
+ return this.showFilterSubject.value;
+ }
+
+ set selectedShowFilter(value: boolean) {
+ if (this.showFilterSubject.value !== value) {
+ this.showFilterSubject.next(value);
+ this.savePreference(value);
+ }
+ }
+
+ toggle(): void {
+ this.selectedShowFilter = !this.selectedShowFilter;
+ }
+
+ private savePreference(value: boolean): void {
+ try {
+ this.localStorageService.set(this.STORAGE_KEY, value);
+ } catch (e) {
+ this.messageService.add({
+ severity: 'error',
+ summary: 'Save Failed',
+ detail: 'Could not save sidebar filter preference locally.',
+ life: 3000
+ });
+ }
+ }
+
+ private loadFromStorage(): void {
+ const saved = this.localStorageService.get(this.STORAGE_KEY);
+ if (saved !== null) {
+ this.showFilterSubject.next(saved);
+ }
+ }
+}