summaryrefslogtreecommitdiffstatshomepage
path: root/frontend/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/lib')
-rw-r--r--frontend/src/lib/Filter.svelte.ts73
-rw-r--r--frontend/src/lib/icons/Orphan.svelte15
-rw-r--r--frontend/src/lib/toolbar/FilterOrphaned.svelte24
3 files changed, 107 insertions, 5 deletions
diff --git a/frontend/src/lib/Filter.svelte.ts b/frontend/src/lib/Filter.svelte.ts
index e73f497..390b98a 100644
--- a/frontend/src/lib/Filter.svelte.ts
+++ b/frontend/src/lib/Filter.svelte.ts
@@ -4,6 +4,8 @@ import {
type ArchiveFilterInput,
type ComicFilter,
type ComicFilterInput,
+ type NamespaceFilter,
+ type NamespaceFilterInput,
type StringFilter,
type TagFilter,
type TagFilterInput
@@ -16,10 +18,18 @@ interface FilterInput<T> {
exclude?: T | null;
}
-interface BasicFilter {
+interface NameFilter {
name?: { contains?: string | null } | null;
}
+interface AssociationCount {
+ count: { value: number; operator?: Operator | null };
+}
+
+interface BasicFilter extends NameFilter {
+ comics?: AssociationCount | null;
+}
+
export type FilterType = 'include' | 'exclude';
type FilterMode = 'any' | 'all' | 'exact';
@@ -146,6 +156,27 @@ class Bool<K extends Key> {
}
}
+class Orphan<K extends Key> {
+ key: K;
+ value?: boolean = false;
+
+ constructor(key: K, filter?: Filter<AssociationCount, K> | null) {
+ this.key = key;
+
+ if (filter) {
+ this.value =
+ filter[key]?.count?.value === 0 &&
+ (filter[key].count.operator === undefined || filter[key].count.operator === Operator.Equal);
+ }
+ }
+
+ integrate(filter: Filter<AssociationCount, K>) {
+ if (this.value) {
+ filter[this.key] = { count: { value: 0, operator: Operator.Equal } };
+ }
+ }
+}
+
class Str<K extends Key> {
key: K;
contains = $state('');
@@ -177,7 +208,7 @@ export class ArchiveFilterControls extends Controls<ArchiveFilter> {
path: Str<'path'>;
organized: Bool<'organized'>;
- constructor(filter: ArchiveFilter | null | undefined) {
+ constructor(filter?: ArchiveFilter | null) {
super();
this.path = new Str('path', filter);
@@ -223,16 +254,26 @@ export class ComicFilterControls extends Controls<ComicFilter> {
}
}
-export class BasicFilterControls extends Controls<BasicFilter> {
+export class NameFilterControls extends Controls<NameFilter> {
name: Str<'name'>;
- constructor(filter?: BasicFilter | null) {
+ constructor(filter?: NameFilter | null) {
super();
this.name = new Str('name', filter);
}
}
+export class BasicFilterControls extends NameFilterControls {
+ orphan: Orphan<'comics'>;
+
+ constructor(filter?: BasicFilter | null) {
+ super(filter);
+
+ this.orphan = new Orphan('comics', filter);
+ }
+}
+
export class TagFilterControls extends BasicFilterControls {
namespaces: Association<'namespaces'>;
@@ -243,6 +284,16 @@ export class TagFilterControls extends BasicFilterControls {
}
}
+export class NamespaceFilterControls extends NameFilterControls {
+ orphan: Orphan<'tags'>;
+
+ constructor(filter?: NamespaceFilter | null) {
+ super(filter);
+
+ this.orphan = new Orphan('tags', filter);
+ }
+}
+
function buildFilterInput<F>(include?: F, exclude?: F) {
const input: FilterInput<F> = {};
@@ -318,7 +369,7 @@ export class BasicFilterContext extends FilterContext<BasicFilter> {
export class TagFilterContext extends FilterContext<TagFilter> {
include: TagFilterControls;
exclude: TagFilterControls;
- private static ignore = ['name'];
+ private static ignore = ['name', 'comics'];
constructor(filter: TagFilterInput) {
super();
@@ -330,6 +381,18 @@ export class TagFilterContext extends FilterContext<TagFilter> {
}
}
+export class NamespaceFilterContext extends FilterContext<NamespaceFilter> {
+ include: NamespaceFilterControls;
+ exclude: NamespaceFilterControls;
+
+ constructor(filter: NamespaceFilterInput) {
+ super();
+
+ this.include = new NamespaceFilterControls(filter.include);
+ this.exclude = new NamespaceFilterControls();
+ }
+}
+
export function cycleBooleanFilter(value: boolean | undefined, tristate = true) {
if (tristate) {
if (value === undefined) {
diff --git a/frontend/src/lib/icons/Orphan.svelte b/frontend/src/lib/icons/Orphan.svelte
new file mode 100644
index 0000000..7d947d2
--- /dev/null
+++ b/frontend/src/lib/icons/Orphan.svelte
@@ -0,0 +1,15 @@
+<script lang="ts">
+ interface Props {
+ orphaned?: boolean;
+ hoverable?: boolean;
+ }
+
+ let { orphaned, hoverable = false }: Props = $props();
+</script>
+
+{#if orphaned}
+ <span class:hoverable class="icon-gray icon-base icon-[material-symbols--fmd-bad]"></span>
+{:else}
+ <span class:hoverable class="icon-gray icon-base dim icon-[material-symbols--fmd-bad-outline]"
+ ></span>
+{/if}
diff --git a/frontend/src/lib/toolbar/FilterOrphaned.svelte b/frontend/src/lib/toolbar/FilterOrphaned.svelte
new file mode 100644
index 0000000..7e79be1
--- /dev/null
+++ b/frontend/src/lib/toolbar/FilterOrphaned.svelte
@@ -0,0 +1,24 @@
+<script lang="ts">
+ import { page } from '$app/state';
+ import { BasicFilterContext, NamespaceFilterContext } from '$lib/Filter.svelte';
+ import { accelerator } from '$lib/Shortcuts';
+ import Orphan from '$lib/icons/Orphan.svelte';
+
+ let { filter }: { filter: BasicFilterContext | NamespaceFilterContext } = $props();
+ let orphaned = $derived(filter.include.orphan.value);
+
+ const toggle = () => {
+ filter.include.orphan.value = !orphaned;
+ filter.apply(page.url.searchParams);
+ };
+</script>
+
+<button
+ class:toggled={orphaned}
+ class="btn-slate"
+ title="Filter orphaned"
+ onclick={toggle}
+ use:accelerator={'r'}
+>
+ <Orphan {orphaned} />
+</button>