summaryrefslogtreecommitdiffstatshomepage
path: root/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'frontend')
-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
-rw-r--r--frontend/src/routes/artists/+page.svelte2
-rw-r--r--frontend/src/routes/characters/+page.svelte2
-rw-r--r--frontend/src/routes/circles/+page.svelte2
-rw-r--r--frontend/src/routes/namespaces/+page.svelte8
-rw-r--r--frontend/src/routes/tags/+page.svelte2
-rw-r--r--frontend/src/routes/worlds/+page.svelte2
9 files changed, 122 insertions, 8 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>
diff --git a/frontend/src/routes/artists/+page.svelte b/frontend/src/routes/artists/+page.svelte
index 9f0d893..9fff9d2 100644
--- a/frontend/src/routes/artists/+page.svelte
+++ b/frontend/src/routes/artists/+page.svelte
@@ -20,6 +20,7 @@
import { initSelectionContext } from '$lib/selection/Selection.svelte';
import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte';
import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte';
+ import FilterOrphaned from '$lib/toolbar/FilterOrphaned.svelte';
import Search from '$lib/toolbar/Search.svelte';
import SelectItems from '$lib/toolbar/SelectItems.svelte';
import SelectSort from '$lib/toolbar/SelectSort.svelte';
@@ -69,6 +70,7 @@
{/snippet}
{#snippet center()}
<Search name="Artists" {filter} bind:field={filter.include.name.contains} />
+ <FilterOrphaned {filter} />
<SelectSort {sort} labels={ArtistSortLabel} />
<SelectItems {pagination} />
{/snippet}
diff --git a/frontend/src/routes/characters/+page.svelte b/frontend/src/routes/characters/+page.svelte
index 3a4b737..970a8fa 100644
--- a/frontend/src/routes/characters/+page.svelte
+++ b/frontend/src/routes/characters/+page.svelte
@@ -20,6 +20,7 @@
import { initSelectionContext } from '$lib/selection/Selection.svelte';
import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte';
import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte';
+ import FilterOrphaned from '$lib/toolbar/FilterOrphaned.svelte';
import Search from '$lib/toolbar/Search.svelte';
import SelectItems from '$lib/toolbar/SelectItems.svelte';
import SelectSort from '$lib/toolbar/SelectSort.svelte';
@@ -69,6 +70,7 @@
{/snippet}
{#snippet center()}
<Search name="Characters" {filter} bind:field={filter.include.name.contains} />
+ <FilterOrphaned {filter} />
<SelectSort {sort} labels={CharacterSortLabel} />
<SelectItems {pagination} />
{/snippet}
diff --git a/frontend/src/routes/circles/+page.svelte b/frontend/src/routes/circles/+page.svelte
index 8bac7ed..bad1e1d 100644
--- a/frontend/src/routes/circles/+page.svelte
+++ b/frontend/src/routes/circles/+page.svelte
@@ -20,6 +20,7 @@
import { initSelectionContext } from '$lib/selection/Selection.svelte';
import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte';
import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte';
+ import FilterOrphaned from '$lib/toolbar/FilterOrphaned.svelte';
import Search from '$lib/toolbar/Search.svelte';
import SelectItems from '$lib/toolbar/SelectItems.svelte';
import SelectSort from '$lib/toolbar/SelectSort.svelte';
@@ -69,6 +70,7 @@
{/snippet}
{#snippet center()}
<Search name="Circles" {filter} bind:field={filter.include.name.contains} />
+ <FilterOrphaned {filter} />
<SelectSort {sort} labels={CircleSortLabel} />
<SelectItems {pagination} />
{/snippet}
diff --git a/frontend/src/routes/namespaces/+page.svelte b/frontend/src/routes/namespaces/+page.svelte
index d8e728d..429432f 100644
--- a/frontend/src/routes/namespaces/+page.svelte
+++ b/frontend/src/routes/namespaces/+page.svelte
@@ -3,7 +3,7 @@
import { fetchNamespace, namespacesQuery } from '$gql/Queries';
import type { Namespace } from '$gql/graphql';
import { NamespaceSortLabel } from '$lib/Enums';
- import { BasicFilterContext } from '$lib/Filter.svelte';
+ import { NamespaceFilterContext } from '$lib/Filter.svelte';
import { quickComicFilter } from '$lib/Navigation';
import { toastFinally } from '$lib/Toasts';
import AddButton from '$lib/components/AddButton.svelte';
@@ -20,6 +20,7 @@
import { initSelectionContext } from '$lib/selection/Selection.svelte';
import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte';
import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte';
+ import FilterOrphaned from '$lib/toolbar/FilterOrphaned.svelte';
import Search from '$lib/toolbar/Search.svelte';
import SelectItems from '$lib/toolbar/SelectItems.svelte';
import SelectSort from '$lib/toolbar/SelectSort.svelte';
@@ -44,9 +45,9 @@
}
});
- let filter = $state(new BasicFilterContext(data.filter));
+ let filter = $state(new NamespaceFilterContext(data.filter));
$effect(() => {
- filter = new BasicFilterContext(data.filter);
+ filter = new NamespaceFilterContext(data.filter);
});
const edit = (id: number) => {
@@ -69,6 +70,7 @@
{/snippet}
{#snippet center()}
<Search name="Namespaces" {filter} bind:field={filter.include.name.contains} />
+ <FilterOrphaned {filter} />
<SelectSort {sort} labels={NamespaceSortLabel} />
<SelectItems {pagination} />
{/snippet}
diff --git a/frontend/src/routes/tags/+page.svelte b/frontend/src/routes/tags/+page.svelte
index f71267f..2cb6f87 100644
--- a/frontend/src/routes/tags/+page.svelte
+++ b/frontend/src/routes/tags/+page.svelte
@@ -23,6 +23,7 @@
import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte';
import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte';
import EditSelection from '$lib/toolbar/EditSelection.svelte';
+ import FilterOrphaned from '$lib/toolbar/FilterOrphaned.svelte';
import Search from '$lib/toolbar/Search.svelte';
import SelectItems from '$lib/toolbar/SelectItems.svelte';
import SelectSort from '$lib/toolbar/SelectSort.svelte';
@@ -76,6 +77,7 @@
{#snippet center({ expanded, toggle })}
<Search name="Tags" {filter} bind:field={filter.include.name.contains} />
<ToggleAdvancedFilters {expanded} {toggle} {filterSize} />
+ <FilterOrphaned {filter} />
<SelectSort {sort} labels={TagSortLabel} />
<SelectItems {pagination} />
{/snippet}
diff --git a/frontend/src/routes/worlds/+page.svelte b/frontend/src/routes/worlds/+page.svelte
index 6b95142..133dc27 100644
--- a/frontend/src/routes/worlds/+page.svelte
+++ b/frontend/src/routes/worlds/+page.svelte
@@ -20,6 +20,7 @@
import { initSelectionContext } from '$lib/selection/Selection.svelte';
import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte';
import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte';
+ import FilterOrphaned from '$lib/toolbar/FilterOrphaned.svelte';
import Search from '$lib/toolbar/Search.svelte';
import SelectItems from '$lib/toolbar/SelectItems.svelte';
import SelectSort from '$lib/toolbar/SelectSort.svelte';
@@ -69,6 +70,7 @@
{/snippet}
{#snippet center()}
<Search name="Worlds" {filter} bind:field={filter.include.name.contains} />
+ <FilterOrphaned {filter} />
<SelectSort {sort} labels={WorldSortLabel} />
<SelectItems {pagination} />
{/snippet}