mirror of
https://github.com/booklore-app/booklore.git
synced 2025-12-23 22:28:11 -05:00
Feature: filter mode preference (#1739)
* Feature: filter mode preference * Flter sorting mode preference * Filter sorting mode preference --------- Co-authored-by: Aditya Chandel <8075870+adityachandelgit@users.noreply.github.com>
This commit is contained in:
@@ -25,9 +25,9 @@ import {ProgressSpinner} from 'primeng/progressspinner';
|
||||
import {Menu} from 'primeng/menu';
|
||||
import {InputText} from 'primeng/inputtext';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {BookFilterComponent, BookFilterMode} from './book-filter/book-filter.component';
|
||||
import {BookFilterComponent} from './book-filter/book-filter.component';
|
||||
import {Tooltip} from 'primeng/tooltip';
|
||||
import {EntityViewPreferences, UserService} from '../../../settings/user-management/user.service';
|
||||
import {BookFilterMode, EntityViewPreferences, UserService} from '../../../settings/user-management/user.service';
|
||||
import {SeriesCollapseFilter} from './filters/SeriesCollapseFilter';
|
||||
import {SideBarFilter} from './filters/SidebarFilter';
|
||||
import {HeaderFilter} from './filters/HeaderFilter';
|
||||
@@ -267,12 +267,12 @@ export class BookBrowserComponent implements OnInit, AfterViewInit {
|
||||
const sortParam = queryParamMap.get(QUERY_PARAMS.SORT);
|
||||
const directionParam = queryParamMap.get(QUERY_PARAMS.DIRECTION);
|
||||
const filterParams = queryParamMap.get(QUERY_PARAMS.FILTER);
|
||||
const filterMode = <BookFilterMode>queryParamMap.get(QUERY_PARAMS.FMODE);
|
||||
const filterMode = queryParamMap.get(QUERY_PARAMS.FMODE) || user.user?.userSettings?.filterMode;
|
||||
|
||||
if (filterMode && filterMode !== this.selectedFilterMode.getValue()) {
|
||||
this.selectedFilterMode.next(filterMode);
|
||||
this.selectedFilterMode.next(<BookFilterMode>filterMode);
|
||||
if (this.bookFilterComponent) {
|
||||
this.bookFilterComponent.selectedFilterMode = filterMode;
|
||||
this.bookFilterComponent.selectedFilterMode = <BookFilterMode>filterMode;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {ChangeDetectionStrategy, Component, EventEmitter, inject, Input, OnDestroy, OnInit, Output} from '@angular/core';
|
||||
import {combineLatest, Observable, of, shareReplay, Subject, takeUntil} from 'rxjs';
|
||||
import {combineLatest, filter, Observable, of, shareReplay, Subject, takeUntil} from 'rxjs';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {BookService} from '../../../service/book.service';
|
||||
import {Library} from '../../../model/library.model';
|
||||
@@ -11,15 +11,13 @@ import {AsyncPipe, NgClass, TitleCasePipe} from '@angular/common';
|
||||
import {Badge} from 'primeng/badge';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {SelectButton} from 'primeng/selectbutton';
|
||||
import {UserService} from '../../../../settings/user-management/user.service';
|
||||
import {BookFilterMode, FilterSortingMode, UserService, UserState} from '../../../../settings/user-management/user.service';
|
||||
import {MagicShelf} from '../../../../magic-shelf/service/magic-shelf.service';
|
||||
import {BookRuleEvaluatorService} from '../../../../magic-shelf/service/book-rule-evaluator.service';
|
||||
import {GroupRule} from '../../../../magic-shelf/component/magic-shelf-component';
|
||||
|
||||
type Filter<T> = { value: T; bookCount: number };
|
||||
|
||||
export type BookFilterMode = 'and' | 'or' | 'single';
|
||||
|
||||
export const ratingRanges = [
|
||||
{id: '0to1', label: '0 to 1', min: 0, max: 1, sortIndex: 0},
|
||||
{id: '1to2', label: '1 to 2', min: 1, max: 2, sortIndex: 1},
|
||||
@@ -200,14 +198,23 @@ export class BookFilterComponent implements OnInit, OnDestroy {
|
||||
bookService = inject(BookService);
|
||||
userService = inject(UserService);
|
||||
bookRuleEvaluatorService = inject(BookRuleEvaluatorService);
|
||||
userData$: Observable<UserState> = this.userService.userState$;
|
||||
filterSortingMode: FilterSortingMode = 'alphabetical';
|
||||
|
||||
ngOnInit(): void {
|
||||
this.userData$.pipe(
|
||||
filter(userState => !!userState?.user && userState.loaded),
|
||||
takeUntil(this.destroy$)
|
||||
).subscribe(userState => {
|
||||
this.filterSortingMode = userState.user!.userSettings.filterSortingMode ?? 'alphabetical';
|
||||
});
|
||||
|
||||
combineLatest([
|
||||
this.entity$ ?? of(null),
|
||||
this.entityType$ ?? of(EntityType.ALL_BOOKS)
|
||||
])
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(([sortMode]) => {
|
||||
.subscribe(() => {
|
||||
this.filterStreams = {
|
||||
author: this.getFilterStream(
|
||||
(book: Book) => Array.isArray(book.metadata?.authors) ? book.metadata.authors.map(name => ({id: name, name})) : [],
|
||||
@@ -270,7 +277,7 @@ export class BookFilterComponent implements OnInit, OnDestroy {
|
||||
extractor: (book: Book) => T[] | undefined,
|
||||
idKey: keyof T,
|
||||
nameKey: keyof T,
|
||||
sortMode: 'count' | 'alphabetical' | 'sortIndex' = 'count'
|
||||
sortMode: FilterSortingMode | 'sortIndex' = this.filterSortingMode
|
||||
): Observable<Filter<T[keyof T]>[]> {
|
||||
return combineLatest([
|
||||
this.bookService.bookState$,
|
||||
@@ -296,11 +303,10 @@ export class BookFilterComponent implements OnInit, OnDestroy {
|
||||
const sorted = result.sort((a, b) => {
|
||||
if (sortMode === 'sortIndex') {
|
||||
return (a.value.sortIndex ?? 999) - (b.value.sortIndex ?? 999);
|
||||
} else if (sortMode === 'count' && b.bookCount !== a.bookCount) {
|
||||
return b.bookCount - a.bookCount;
|
||||
}
|
||||
return (
|
||||
b.bookCount - a.bookCount ||
|
||||
a.value[nameKey].toString().localeCompare(b.value[nameKey].toString())
|
||||
);
|
||||
return a.value[nameKey].toString().localeCompare(b.value[nameKey].toString());
|
||||
});
|
||||
|
||||
const isTruncated = sorted.length > 500;
|
||||
|
||||
@@ -2,8 +2,9 @@ import {Observable, combineLatest, of} from 'rxjs';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {BookFilter} from './BookFilter';
|
||||
import {BookState} from '../../../model/state/book-state.model';
|
||||
import {fileSizeRanges, matchScoreRanges, pageCountRanges, ratingRanges, BookFilterMode} from '../book-filter/book-filter.component';
|
||||
import {fileSizeRanges, matchScoreRanges, pageCountRanges, ratingRanges} from '../book-filter/book-filter.component';
|
||||
import {Book, ReadStatus} from '../../../model/book.model';
|
||||
import {BookFilterMode} from '../../../../settings/user-management/user.service';
|
||||
|
||||
export function isRatingInRange(rating: number | undefined | null, rangeId: string): boolean {
|
||||
if (rating == null) return false;
|
||||
|
||||
@@ -43,6 +43,8 @@ export interface PerBookSetting {
|
||||
}
|
||||
|
||||
export type PageSpread = 'off' | 'even' | 'odd';
|
||||
export type BookFilterMode = 'and' | 'or' | 'single';
|
||||
export type FilterSortingMode = 'alphabetical' | 'count';
|
||||
|
||||
export enum CbxBackgroundColor {
|
||||
GRAY = 'GRAY',
|
||||
@@ -127,7 +129,8 @@ export interface UserSettings {
|
||||
newPdfReaderSetting: NewPdfReaderSetting;
|
||||
sidebarLibrarySorting: SidebarLibrarySorting;
|
||||
sidebarShelfSorting: SidebarShelfSorting;
|
||||
filterSortingMode: 'alphabetical' | 'count';
|
||||
filterMode: BookFilterMode;
|
||||
filterSortingMode: FilterSortingMode;
|
||||
metadataCenterViewMode: 'route' | 'dialog';
|
||||
enableSeriesView: boolean;
|
||||
entityViewPreferences: EntityViewPreferences;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<div class="main-container enclosing-container">
|
||||
<div class="settings-header">
|
||||
<h2 class="settings-title">
|
||||
<i class="pi pi-sort-alt"></i>
|
||||
Filter Preferences
|
||||
</h2>
|
||||
<p class="settings-description">
|
||||
Configure default options for filter controls shown in the sidebar.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="settings-content">
|
||||
<div class="preferences-section">
|
||||
<div class="settings-card">
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">
|
||||
Filter Mode
|
||||
<i class="pi pi-info-circle text-sky-600"
|
||||
pTooltip="Choose the default mode for combining filters selected from the filter sidebar."
|
||||
tooltipPosition="right"
|
||||
style="cursor: pointer;">
|
||||
</i>
|
||||
</label>
|
||||
<p-select
|
||||
size="small"
|
||||
[options]="filterModes"
|
||||
[(ngModel)]="selectedFilterMode"
|
||||
(ngModelChange)="onFilterModeChange()"
|
||||
appendTo="body">
|
||||
</p-select>
|
||||
</div>
|
||||
<p class="setting-description">
|
||||
Choose the default mode for combining filters selected from the filter sidebar.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label-row">
|
||||
<label class="setting-label">
|
||||
Sorting Mode
|
||||
<i class="pi pi-info-circle text-sky-600"
|
||||
pTooltip="Choose how filter options are sorted in the sidebar."
|
||||
tooltipPosition="right"
|
||||
style="cursor: pointer;">
|
||||
</i>
|
||||
</label>
|
||||
<p-select
|
||||
size="small"
|
||||
[options]="filterSortingModes"
|
||||
[(ngModel)]="selectedFilterSortingMode"
|
||||
(ngModelChange)="onFilterSortingModeChange()"
|
||||
appendTo="body">
|
||||
</p-select>
|
||||
</div>
|
||||
<p class="setting-description">
|
||||
Choose how filter options are sorted in the sidebar.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,133 @@
|
||||
.main-container {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.enclosing-container {
|
||||
border-color: var(--p-content-border-color);
|
||||
background: var(--p-content-background);
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.settings-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--p-text-color);
|
||||
margin: 0 0 0.75rem 0;
|
||||
|
||||
.pi {
|
||||
color: var(--p-primary-color);
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-description {
|
||||
color: var(--p-text-muted-color);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.preferences-section {
|
||||
@media (min-width: 768px) {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
border: 1px solid var(--p-content-border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--p-content-background);
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 2rem;
|
||||
padding: 0.25rem 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.setting-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--p-text-color);
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.setting-label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.setting-label {
|
||||
margin-bottom: 0;
|
||||
flex-shrink: 0;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
p-select {
|
||||
min-width: 200px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
min-width: 180px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
color: var(--p-text-muted-color);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-control {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
align-items: flex-end;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import {Component, inject, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Select} from 'primeng/select';
|
||||
import {Tooltip} from 'primeng/tooltip';
|
||||
import {BookFilterMode, FilterSortingMode, SidebarLibrarySorting, SidebarShelfSorting, User, UserService, UserSettings, UserState} from '../../user-management/user.service';
|
||||
import {MessageService} from 'primeng/api';
|
||||
import {Observable, Subject} from 'rxjs';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {filter, takeUntil} from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-filter-preferences',
|
||||
imports: [
|
||||
Select,
|
||||
Tooltip,
|
||||
FormsModule
|
||||
],
|
||||
templateUrl: './filter-preferences.component.html',
|
||||
styleUrl: './filter-preferences.component.scss'
|
||||
})
|
||||
export class FilterPreferencesComponent implements OnInit, OnDestroy {
|
||||
|
||||
readonly filterModes = [
|
||||
{label: 'And', value: 'and'},
|
||||
{label: 'Or', value: 'or'},
|
||||
{label: 'Single', value: 'single'},
|
||||
];
|
||||
|
||||
readonly filterSortingModes = [
|
||||
{label: 'Alphabetical', value: 'alphabetical'},
|
||||
{label: 'By Count', value: 'count'},
|
||||
];
|
||||
|
||||
selectedFilterMode: BookFilterMode = 'and';
|
||||
selectedFilterSortingMode: FilterSortingMode = 'alphabetical';
|
||||
|
||||
private readonly userService = inject(UserService);
|
||||
private readonly messageService = inject(MessageService);
|
||||
private readonly destroy$ = new Subject<void>();
|
||||
|
||||
userData$: Observable<UserState> = this.userService.userState$;
|
||||
private currentUser: User | null = null;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.userData$.pipe(
|
||||
filter(userState => !!userState?.user && userState.loaded),
|
||||
takeUntil(this.destroy$)
|
||||
).subscribe(userState => {
|
||||
this.currentUser = userState.user;
|
||||
this.loadPreferences(userState.user!.userSettings);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
private loadPreferences(settings: UserSettings): void {
|
||||
this.selectedFilterMode = settings.filterMode ?? 'and';
|
||||
this.selectedFilterSortingMode = settings.filterSortingMode ?? 'alphabetical';
|
||||
}
|
||||
|
||||
private updatePreference(path: string[], value: any): void {
|
||||
if (!this.currentUser) return;
|
||||
|
||||
let target: any = this.currentUser.userSettings;
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
target = target[path[i]] ||= {};
|
||||
}
|
||||
target[path.at(-1)!] = value;
|
||||
|
||||
const [rootKey] = path;
|
||||
const updatedValue = this.currentUser.userSettings[rootKey as keyof UserSettings];
|
||||
this.userService.updateUserSetting(this.currentUser.id, rootKey, updatedValue);
|
||||
this.messageService.add({
|
||||
severity: 'success',
|
||||
summary: 'Preferences Updated',
|
||||
detail: 'Your preferences have been saved successfully.',
|
||||
life: 1500
|
||||
});
|
||||
}
|
||||
|
||||
onFilterModeChange() {
|
||||
this.updatePreference(['filterMode'], this.selectedFilterMode);
|
||||
}
|
||||
|
||||
onFilterSortingModeChange() {
|
||||
this.updatePreference(['filterSortingMode'], this.selectedFilterSortingMode);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,10 @@
|
||||
<div class="pt-8">
|
||||
<p-divider></p-divider>
|
||||
</div>
|
||||
<app-filter-preferences></app-filter-preferences>
|
||||
<div class="pt-8">
|
||||
<p-divider></p-divider>
|
||||
</div>
|
||||
<app-sidebar-sorting-preferences></app-sidebar-sorting-preferences>
|
||||
<div class="pt-8">
|
||||
<p-divider></p-divider>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {ToastModule} from 'primeng/toast';
|
||||
import {ViewPreferencesComponent} from './view-preferences/view-preferences.component';
|
||||
import {SidebarSortingPreferencesComponent} from './sidebar-sorting-preferences/sidebar-sorting-preferences.component';
|
||||
import {MetaCenterViewModeComponent} from './meta-center-view-mode/meta-center-view-mode-component';
|
||||
import {FilterPreferencesComponent} from './filter-preferences/filter-preferences.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-view-preferences-parent',
|
||||
@@ -17,7 +18,8 @@ import {MetaCenterViewModeComponent} from './meta-center-view-mode/meta-center-v
|
||||
ToastModule,
|
||||
ViewPreferencesComponent,
|
||||
SidebarSortingPreferencesComponent,
|
||||
MetaCenterViewModeComponent
|
||||
MetaCenterViewModeComponent,
|
||||
FilterPreferencesComponent,
|
||||
],
|
||||
templateUrl: './view-preferences-parent.component.html',
|
||||
styleUrl: './view-preferences-parent.component.scss'
|
||||
|
||||
Reference in New Issue
Block a user