Skip to content

Commit d4ed7da

Browse files
authored
add NOT filter mode for excluding books by filter values (#3099)
1 parent 11dea38 commit d4ed7da

File tree

5 files changed

+30
-16
lines changed

5 files changed

+30
-16
lines changed

booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@
4242
*cdkVirtualFor="let filter of filters; trackBy: trackByFilter"
4343
class="filter-row"
4444
[ngClass]="{
45-
'active': activeFilters[filterType]?.includes(getFilterValueId(filter))
45+
'active': activeFilters[filterType]?.includes(getFilterValueId(filter)),
46+
'active-not': activeFilters[filterType]?.includes(getFilterValueId(filter)) && selectedFilterMode === 'not'
4647
}"
4748
(click)="handleFilterClick(filterType, getFilterValueId(filter))">
4849
<span class="filter-value-text">{{ getFilterValueDisplay(filter) }}</span>

booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.scss

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
}
44

55
:host ::ng-deep .p-togglebutton {
6-
--p-togglebutton-content-padding: 0.25rem 0.5rem;
6+
--p-togglebutton-content-padding: 0.25rem 0.35rem;
77
}
88

99
:host ::ng-deep .cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper {
@@ -34,7 +34,11 @@
3434
}
3535

3636
.filter-mode-select {
37-
font-size: 0.875rem;
37+
font-size: 0.75rem;
38+
}
39+
40+
:host ::ng-deep .filter-mode-select .p-togglebutton-label {
41+
font-size: 0.75rem;
3842
}
3943

4044
.filter-content {
@@ -84,6 +88,11 @@
8488
&.active {
8589
color: var(--primary-color);
8690
}
91+
92+
&.active-not {
93+
color: var(--p-red-400);
94+
text-decoration: line-through;
95+
}
8796
}
8897

8998
.filter-value-text {

booklore-ui/src/app/features/book/components/book-browser/book-filter/book-filter.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export class BookFilterComponent implements OnInit, OnDestroy {
6767
readonly filterModeOptions: FilterModeOption[] = [
6868
{label: 'AND', value: 'and'},
6969
{label: 'OR', value: 'or'},
70+
{label: 'NOT', value: 'not'},
7071
{label: '1', value: 'single'}
7172
];
7273

booklore-ui/src/app/features/book/components/book-browser/filters/sidebar-filter.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,19 @@ export function doesBookMatchFilter(
6060
return mode === 'or';
6161
}
6262

63+
const effectiveMode = mode === 'not' ? 'or' : mode;
64+
6365
switch (filterType) {
6466
case 'author':
65-
return mode === 'or'
67+
return effectiveMode === 'or'
6668
? filterValues.some(val => book.metadata?.authors?.includes(val as string))
6769
: filterValues.every(val => book.metadata?.authors?.includes(val as string));
6870
case 'category':
69-
return mode === 'or'
71+
return effectiveMode === 'or'
7072
? filterValues.some(val => book.metadata?.categories?.includes(val as string))
7173
: filterValues.every(val => book.metadata?.categories?.includes(val as string));
7274
case 'series':
73-
return mode === 'or'
75+
return effectiveMode === 'or'
7476
? filterValues.some(val => book.metadata?.seriesName?.trim() === val)
7577
: filterValues.every(val => book.metadata?.seriesName?.trim() === val);
7678
case 'bookType':
@@ -80,24 +82,24 @@ export function doesBookMatchFilter(
8082
case 'personalRating':
8183
return filterValues.some(range => isRatingInRange10(book.personalRating, range as string | number));
8284
case 'publisher':
83-
return mode === 'or'
85+
return effectiveMode === 'or'
8486
? filterValues.some(val => book.metadata?.publisher === val)
8587
: filterValues.every(val => book.metadata?.publisher === val);
8688
case 'matchScore':
8789
return filterValues.some(range => isMatchScoreInRange(book.metadataMatchScore, range as string | number));
8890
case 'library':
89-
return mode === 'or'
91+
return effectiveMode === 'or'
9092
? filterValues.some(val => val == book.libraryId)
9193
: filterValues.every(val => val == book.libraryId);
9294
case 'shelf':
93-
return mode === 'or'
95+
return effectiveMode === 'or'
9496
? filterValues.some(val => book.shelves?.some(s => s.id == val))
9597
: filterValues.every(val => book.shelves?.some(s => s.id == val));
9698
case 'shelfStatus':
9799
const shelved = book.shelves && book.shelves.length > 0 ? 'shelved' : 'unshelved';
98100
return filterValues.includes(shelved);
99101
case 'tag':
100-
return mode === 'or'
102+
return effectiveMode === 'or'
101103
? filterValues.some(val => book.metadata?.tags?.includes(val as string))
102104
: filterValues.every(val => book.metadata?.tags?.includes(val as string));
103105
case 'publishedDate':
@@ -118,7 +120,7 @@ export function doesBookMatchFilter(
118120
case 'pageCount':
119121
return filterValues.some(range => isPageCountInRange(book.metadata?.pageCount!, range as string | number));
120122
case 'mood':
121-
return mode === 'or'
123+
return effectiveMode === 'or'
122124
? filterValues.some(val => book.metadata?.moods?.includes(val as string))
123125
: filterValues.every(val => book.metadata?.moods?.includes(val as string));
124126
case 'ageRating':
@@ -131,15 +133,15 @@ export function doesBookMatchFilter(
131133
case 'narrator':
132134
return filterValues.includes(book.metadata?.narrator);
133135
case 'comicCharacter':
134-
return mode === 'or'
136+
return effectiveMode === 'or'
135137
? filterValues.some(val => book.metadata?.comicMetadata?.characters?.includes(val as string))
136138
: filterValues.every(val => book.metadata?.comicMetadata?.characters?.includes(val as string));
137139
case 'comicTeam':
138-
return mode === 'or'
140+
return effectiveMode === 'or'
139141
? filterValues.some(val => book.metadata?.comicMetadata?.teams?.includes(val as string))
140142
: filterValues.every(val => book.metadata?.comicMetadata?.teams?.includes(val as string));
141143
case 'comicLocation':
142-
return mode === 'or'
144+
return effectiveMode === 'or'
143145
? filterValues.some(val => book.metadata?.comicMetadata?.locations?.includes(val as string))
144146
: filterValues.every(val => book.metadata?.comicMetadata?.locations?.includes(val as string));
145147
case 'comicCreator': {
@@ -161,7 +163,7 @@ export function doesBookMatchFilter(
161163
}
162164
}
163165
}
164-
return mode === 'or'
166+
return effectiveMode === 'or'
165167
? filterValues.some(val => allCreators.includes(val as string))
166168
: filterValues.every(val => allCreators.includes(val as string));
167169
}
@@ -187,6 +189,7 @@ export function filterBooksByFilters(
187189
const matches = filterEntries.map(([filterType, filterValues]) =>
188190
doesBookMatchFilter(book, filterType, filterValues, mode)
189191
);
192+
if (mode === 'not') return matches.every(m => !m);
190193
return mode === 'or' ? matches.some(m => m) : matches.every(m => m);
191194
});
192195
}

booklore-ui/src/app/features/settings/user-management/user.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export interface PerBookSetting {
5656
}
5757

5858
export type PageSpread = 'off' | 'even' | 'odd';
59-
export type BookFilterMode = 'and' | 'or' | 'single';
59+
export type BookFilterMode = 'and' | 'or' | 'single' | 'not';
6060

6161

6262
export enum CbxPageViewMode {

0 commit comments

Comments
 (0)