summaryrefslogtreecommitdiffstatshomepage
path: root/frontend/src/lib/Filter.svelte.ts
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/lib/Filter.svelte.ts')
-rw-r--r--frontend/src/lib/Filter.svelte.ts334
1 files changed, 334 insertions, 0 deletions
diff --git a/frontend/src/lib/Filter.svelte.ts b/frontend/src/lib/Filter.svelte.ts
new file mode 100644
index 0000000..8c0fa82
--- /dev/null
+++ b/frontend/src/lib/Filter.svelte.ts
@@ -0,0 +1,334 @@
+import {
+ type ArchiveFilter,
+ type ArchiveFilterInput,
+ type ComicFilter,
+ type ComicFilterInput,
+ type StringFilter,
+ type TagFilter,
+ type TagFilterInput
+} from '$gql/graphql';
+import { navigate } from './Navigation';
+import { numKeys, type Key } from './Utils';
+
+interface FilterInput<T> {
+ include?: T | null;
+ exclude?: T | null;
+}
+
+interface BasicFilter {
+ name?: { contains?: string | null } | null;
+}
+
+export type FilterType = 'include' | 'exclude';
+
+type FilterMode = 'any' | 'all' | 'exact';
+
+type Filter<T, K extends Key> = Partial<Record<K, T | null>>;
+
+type AssocFilter<T, K extends Key> = Filter<
+ {
+ any?: T[] | null;
+ all?: T[] | null;
+ exact?: T[] | null;
+ empty?: boolean | null;
+ },
+ K
+>;
+
+type EnumFilter<K extends Key> = Filter<
+ {
+ any?: string[] | null;
+ empty?: boolean | null;
+ },
+ K
+>;
+
+interface Integrateable<F> {
+ integrate(filter: F): void;
+}
+
+class ComplexMember<K extends Key> {
+ values: unknown[] = $state([]);
+ key: K;
+ mode: FilterMode = $state('all');
+ empty?: boolean | null = $state(null);
+
+ constructor(key: K, mode: FilterMode) {
+ this.key = key;
+ this.mode = mode;
+ }
+
+ integrate(filter: AssocFilter<unknown, K>) {
+ if (this.values.length > 0) {
+ filter[this.key] = { [this.mode]: this.values };
+ }
+
+ if (this.empty) {
+ filter[this.key] = { ...filter[this.key], empty: this.empty };
+ }
+ }
+}
+
+export class Association<K extends Key> extends ComplexMember<K> {
+ values: (string | number)[] = $state([]);
+
+ constructor(key: K, mode: FilterMode, filter?: AssocFilter<string | number, K> | null) {
+ super(key, mode);
+
+ if (!filter) {
+ return;
+ }
+
+ const prop = filter[key];
+ this.empty = prop?.empty;
+
+ if (prop?.all && prop.all.length > 0) {
+ this.mode = 'all';
+ this.values = prop.all;
+ } else if (prop?.any && prop.any.length > 0) {
+ this.mode = 'any';
+ this.values = prop.any;
+ } else if (prop?.exact && prop.exact.length > 0) {
+ this.mode = 'exact';
+ this.values = prop.exact;
+ }
+ }
+}
+
+export class Enum<K extends Key> extends ComplexMember<K> {
+ values: string[] = $state([]);
+
+ constructor(key: K, filter?: EnumFilter<K> | null) {
+ super(key, 'any');
+
+ if (!filter) {
+ return;
+ }
+
+ this.empty = filter[key]?.empty;
+
+ const prop = filter[key];
+ if (prop?.any) {
+ this.values = prop.any;
+ }
+ }
+}
+
+class Bool<K extends Key> {
+ key: K;
+ value?: boolean = $state(undefined);
+
+ constructor(key: K, filter?: Filter<boolean, K> | null) {
+ this.key = key;
+
+ if (filter) {
+ this.value = filter[key] ?? undefined;
+ }
+ }
+
+ integrate(filter: Filter<boolean, K>) {
+ if (this.value !== undefined) {
+ filter[this.key] = this.value;
+ }
+ }
+}
+
+class Str<K extends Key> {
+ key: K;
+ contains = $state('');
+
+ constructor(key: K, filter?: Filter<StringFilter, K> | null) {
+ this.key = key;
+
+ if (filter) {
+ this.contains = filter[key]?.contains ?? '';
+ }
+ }
+
+ integrate(filter: Filter<StringFilter, K>) {
+ if (this.contains) {
+ filter[this.key] = { contains: this.contains };
+ }
+ }
+}
+
+abstract class Controls<F> {
+ buildFilter() {
+ const filter = {} as F;
+ Object.values(this).forEach((v: Integrateable<F>) => v.integrate(filter));
+ return filter;
+ }
+}
+
+export class ArchiveFilterControls extends Controls<ArchiveFilter> {
+ path: Str<'path'>;
+ organized: Bool<'organized'>;
+
+ constructor(filter: ArchiveFilter | null | undefined) {
+ super();
+
+ this.path = new Str('path', filter);
+ this.organized = new Bool('organized', filter);
+ }
+}
+
+export class ComicFilterControls extends Controls<ComicFilter> {
+ title: Str<'title'>;
+ categories: Enum<'category'>;
+ censorships: Enum<'censorship'>;
+ ratings: Enum<'rating'>;
+ tags: Association<'tags'>;
+ languages: Enum<'language'>;
+ artists: Association<'artists'>;
+ circles: Association<'circles'>;
+ characters: Association<'characters'>;
+ worlds: Association<'worlds'>;
+ favourite: Bool<'favourite'>;
+ organized: Bool<'organized'>;
+ bookmarked: Bool<'bookmarked'>;
+
+ constructor(filter: ComicFilter | null | undefined, mode: FilterMode);
+ constructor(filter: ComicFilter | null | undefined, mode: FilterMode);
+ constructor(filter: ComicFilter | null | undefined, mode: FilterMode) {
+ super();
+
+ this.title = new Str('title', filter);
+ this.favourite = new Bool('favourite', filter);
+ this.organized = new Bool('organized', filter);
+ this.bookmarked = new Bool('bookmarked', filter);
+ this.tags = new Association('tags', mode, filter);
+ this.languages = new Enum('language', filter);
+ this.categories = new Enum('category', filter);
+ this.censorships = new Enum('censorship', filter);
+ this.ratings = new Enum('rating', filter);
+ this.artists = new Association('artists', mode, filter);
+ this.circles = new Association('circles', mode, filter);
+ this.characters = new Association('characters', mode, filter);
+ this.worlds = new Association('worlds', mode, filter);
+ }
+}
+
+export class BasicFilterControls extends Controls<BasicFilter> {
+ name: Str<'name'>;
+
+ constructor(filter?: BasicFilter | null) {
+ super();
+
+ this.name = new Str('name', filter);
+ }
+}
+
+export class TagFilterControls extends BasicFilterControls {
+ namespaces: Association<'namespaces'>;
+
+ constructor(filter: TagFilter | null | undefined, mode: FilterMode) {
+ super(filter);
+
+ this.namespaces = new Association('namespaces', mode, filter);
+ }
+}
+
+function buildFilterInput<F>(include?: F, exclude?: F) {
+ const input: FilterInput<F> = {};
+
+ if (include && Object.keys(include).length > 0) {
+ input.include = include;
+ }
+
+ if (exclude && Object.keys(exclude).length > 0) {
+ input.exclude = exclude;
+ }
+
+ return input;
+}
+
+abstract class FilterContext<F> {
+ include!: Controls<F>;
+ exclude!: Controls<F>;
+ includes = 0;
+ excludes = 0;
+
+ apply = (params: URLSearchParams) => {
+ navigate(
+ {
+ filter: buildFilterInput(this.include.buildFilter(), this.exclude.buildFilter())
+ },
+ params
+ );
+ };
+}
+
+export class ArchiveFilterContext extends FilterContext<ArchiveFilter> {
+ include: ArchiveFilterControls;
+ exclude: ArchiveFilterControls;
+ private static ignore = ['organized'];
+
+ constructor(filter: ArchiveFilterInput) {
+ super();
+
+ this.include = new ArchiveFilterControls(filter.include);
+ this.exclude = new ArchiveFilterControls(filter.exclude);
+ this.includes = numKeys(filter.include, ArchiveFilterContext.ignore);
+ this.excludes = numKeys(filter.exclude, ArchiveFilterContext.ignore);
+ }
+}
+
+export class ComicFilterContext extends FilterContext<ComicFilter> {
+ include: ComicFilterControls;
+ exclude: ComicFilterControls;
+ private static ignore = ['title', 'favourite', 'organized', 'bookmarked'];
+
+ constructor(filter: ComicFilterInput) {
+ super();
+
+ this.include = new ComicFilterControls(filter.include, 'all');
+ this.exclude = new ComicFilterControls(filter.exclude, 'any');
+ this.includes = numKeys(filter.include, ComicFilterContext.ignore);
+ this.excludes = numKeys(filter.exclude, ComicFilterContext.ignore);
+ }
+}
+
+export class BasicFilterContext extends FilterContext<BasicFilter> {
+ include: BasicFilterControls;
+ exclude: BasicFilterControls;
+
+ constructor(filter: FilterInput<BasicFilter>) {
+ super();
+
+ this.include = new BasicFilterControls(filter.include);
+ this.exclude = new BasicFilterControls();
+ }
+}
+
+export class TagFilterContext extends FilterContext<TagFilter> {
+ include: TagFilterControls;
+ exclude: TagFilterControls;
+ private static ignore = ['name'];
+
+ constructor(filter: TagFilterInput) {
+ super();
+
+ this.include = new TagFilterControls(filter.include, 'all');
+ this.exclude = new TagFilterControls(filter.exclude, 'any');
+ this.includes = numKeys(filter.include, TagFilterContext.ignore);
+ this.excludes = numKeys(filter.exclude, TagFilterContext.ignore);
+ }
+}
+
+export function cycleBooleanFilter(value: boolean | undefined, tristate = true) {
+ if (tristate) {
+ if (value === undefined) {
+ return true;
+ } else if (value) {
+ return false;
+ } else {
+ return undefined;
+ }
+ } else {
+ if (value) {
+ return undefined;
+ } else {
+ return true;
+ }
+ }
+}