summaryrefslogtreecommitdiffstatshomepage
path: root/frontend/src/lib/toolbar
diff options
context:
space:
mode:
authorWolfgang Müller2024-03-05 18:08:09 +0100
committerWolfgang Müller2024-03-05 19:25:59 +0100
commitd1d654ebac2d51e3841675faeb56480e440f622f (patch)
tree56ef123c1a15a10dfd90836e4038e27efde950c6 /frontend/src/lib/toolbar
downloadhircine-0.1.0.tar.gz
Initial commit0.1.0
Diffstat (limited to '')
-rw-r--r--frontend/src/lib/toolbar/DeleteSelection.svelte26
-rw-r--r--frontend/src/lib/toolbar/EditSelection.svelte29
-rw-r--r--frontend/src/lib/toolbar/FilterBookmarked.svelte24
-rw-r--r--frontend/src/lib/toolbar/FilterFavourites.svelte24
-rw-r--r--frontend/src/lib/toolbar/FilterOrganized.svelte30
-rw-r--r--frontend/src/lib/toolbar/MarkBookmark.svelte27
-rw-r--r--frontend/src/lib/toolbar/MarkFavourite.svelte27
-rw-r--r--frontend/src/lib/toolbar/MarkOrganized.svelte27
-rw-r--r--frontend/src/lib/toolbar/MarkSelection.svelte24
-rw-r--r--frontend/src/lib/toolbar/Search.svelte21
-rw-r--r--frontend/src/lib/toolbar/SelectItems.svelte19
-rw-r--r--frontend/src/lib/toolbar/SelectSort.svelte61
-rw-r--r--frontend/src/lib/toolbar/SelectionControls.svelte57
-rw-r--r--frontend/src/lib/toolbar/ToggleAdvancedFilters.svelte40
-rw-r--r--frontend/src/lib/toolbar/Toolbar.svelte42
15 files changed, 478 insertions, 0 deletions
diff --git a/frontend/src/lib/toolbar/DeleteSelection.svelte b/frontend/src/lib/toolbar/DeleteSelection.svelte
new file mode 100644
index 0000000..7459a87
--- /dev/null
+++ b/frontend/src/lib/toolbar/DeleteSelection.svelte
@@ -0,0 +1,26 @@
+<script lang="ts">
+ import type { DeleteMutation } from '$gql/Mutations';
+ import { getSelectionContext } from '$lib/Selection';
+ import { toastFinally } from '$lib/Toasts';
+ import { confirmDeletion } from '$lib/Utils';
+ import DeleteButton from '$lib/components/DeleteButton.svelte';
+ import { getContextClient } from '@urql/svelte';
+
+ const client = getContextClient();
+ const selection = getSelectionContext();
+
+ export let mutation: DeleteMutation;
+ export let warning: string | undefined = undefined;
+
+ function remove() {
+ const mutate = () => {
+ mutation(client, { ids: $selection.ids })
+ .then(() => ($selection = $selection.clear()))
+ .catch(toastFinally);
+ };
+
+ confirmDeletion($selection.typename, $selection.names, mutate, warning);
+ }
+</script>
+
+<DeleteButton on:click={remove} />
diff --git a/frontend/src/lib/toolbar/EditSelection.svelte b/frontend/src/lib/toolbar/EditSelection.svelte
new file mode 100644
index 0000000..50e6656
--- /dev/null
+++ b/frontend/src/lib/toolbar/EditSelection.svelte
@@ -0,0 +1,29 @@
+<script lang="ts">
+ import { getSelectionContext } from '$lib/Selection';
+ import { accelerator } from '$lib/Shortcuts';
+ import type { SvelteComponent } from 'svelte';
+ import { openModal } from 'svelte-modals';
+
+ const selection = getSelectionContext();
+
+ export let dialog: typeof SvelteComponent<{
+ isOpen: boolean;
+ ids: number[];
+ }>;
+
+ function edit() {
+ openModal(dialog, {
+ ids: $selection.ids
+ });
+ }
+</script>
+
+<button
+ type="button"
+ class="btn-slate hover:bg-blue-700"
+ title="Edit selection"
+ on:click={edit}
+ use:accelerator={'e'}
+>
+ <span class="icon-base icon-[material-symbols--edit]" />
+</button>
diff --git a/frontend/src/lib/toolbar/FilterBookmarked.svelte b/frontend/src/lib/toolbar/FilterBookmarked.svelte
new file mode 100644
index 0000000..bcbe295
--- /dev/null
+++ b/frontend/src/lib/toolbar/FilterBookmarked.svelte
@@ -0,0 +1,24 @@
+<script lang="ts">
+ import { page } from '$app/stores';
+ import { ComicFilterContext, cycleBooleanFilter, getFilterContext } from '$lib/Filter';
+ import { accelerator } from '$lib/Shortcuts';
+ import Bookmark from '$lib/icons/Bookmark.svelte';
+
+ const filter = getFilterContext<ComicFilterContext>();
+ $: bookmarked = $filter.include.controls.bookmarked.value;
+
+ const toggle = () => {
+ $filter.include.controls.bookmarked.value = cycleBooleanFilter(bookmarked, false);
+ $filter.apply($page.url.searchParams);
+ };
+</script>
+
+<button
+ class:toggled={bookmarked}
+ class="btn-slate"
+ title="Filter bookmarked"
+ on:click={toggle}
+ use:accelerator={'b'}
+>
+ <Bookmark {bookmarked} />
+</button>
diff --git a/frontend/src/lib/toolbar/FilterFavourites.svelte b/frontend/src/lib/toolbar/FilterFavourites.svelte
new file mode 100644
index 0000000..6591cef
--- /dev/null
+++ b/frontend/src/lib/toolbar/FilterFavourites.svelte
@@ -0,0 +1,24 @@
+<script lang="ts">
+ import { page } from '$app/stores';
+ import { ComicFilterContext, cycleBooleanFilter, getFilterContext } from '$lib/Filter';
+ import { accelerator } from '$lib/Shortcuts';
+ import Star from '$lib/icons/Star.svelte';
+
+ const filter = getFilterContext<ComicFilterContext>();
+ $: favourite = $filter.include.controls.favourite.value;
+
+ const toggle = () => {
+ $filter.include.controls.favourite.value = cycleBooleanFilter(favourite, false);
+ $filter.apply($page.url.searchParams);
+ };
+</script>
+
+<button
+ class:toggled={favourite}
+ class="btn-slate"
+ title="Filter favourites"
+ on:click={toggle}
+ use:accelerator={'f'}
+>
+ <Star {favourite} />
+</button>
diff --git a/frontend/src/lib/toolbar/FilterOrganized.svelte b/frontend/src/lib/toolbar/FilterOrganized.svelte
new file mode 100644
index 0000000..754e663
--- /dev/null
+++ b/frontend/src/lib/toolbar/FilterOrganized.svelte
@@ -0,0 +1,30 @@
+<script lang="ts">
+ import { page } from '$app/stores';
+ import {
+ ArchiveFilterContext,
+ ComicFilterContext,
+ cycleBooleanFilter,
+ getFilterContext
+ } from '$lib/Filter';
+ import { accelerator } from '$lib/Shortcuts';
+ import Organized from '$lib/icons/Organized.svelte';
+
+ const filter = getFilterContext<ArchiveFilterContext | ComicFilterContext>();
+ $: organized = $filter.include.controls.organized.value;
+
+ const toggle = () => {
+ $filter.include.controls.organized.value = cycleBooleanFilter(organized);
+ $filter.apply($page.url.searchParams);
+ };
+</script>
+
+<button
+ type="button"
+ class:toggled={organized !== undefined}
+ class="btn-slate"
+ title="Filter organized"
+ on:click={toggle}
+ use:accelerator={'o'}
+>
+ <Organized tristate {organized} />
+</button>
diff --git a/frontend/src/lib/toolbar/MarkBookmark.svelte b/frontend/src/lib/toolbar/MarkBookmark.svelte
new file mode 100644
index 0000000..792b84f
--- /dev/null
+++ b/frontend/src/lib/toolbar/MarkBookmark.svelte
@@ -0,0 +1,27 @@
+<script lang="ts">
+ import { getSelectionContext } from '$lib/Selection';
+ import { toastFinally } from '$lib/Toasts';
+ import Bookmark from '$lib/icons/Bookmark.svelte';
+ import { Client, getContextClient } from '@urql/svelte';
+
+ const client = getContextClient();
+ const selection = getSelectionContext();
+
+ export let mutation: (
+ client: Client,
+ args: { ids: number[]; input: { bookmarked: boolean } }
+ ) => Promise<unknown>;
+
+ function mutate(bookmarked: boolean) {
+ mutation(client, { ids: $selection.ids, input: { bookmarked } }).catch(toastFinally);
+ }
+</script>
+
+<button type="button" class="btn-slate flex justify-start gap-1" on:click={() => mutate(true)}>
+ <Bookmark bookmarked={true} />
+ <span>Bookmark</span>
+</button>
+<button type="button" class="btn-slate flex justify-start gap-1" on:click={() => mutate(false)}>
+ <Bookmark bookmarked={false} />
+ <span>Unbookmark</span>
+</button>
diff --git a/frontend/src/lib/toolbar/MarkFavourite.svelte b/frontend/src/lib/toolbar/MarkFavourite.svelte
new file mode 100644
index 0000000..42eaa39
--- /dev/null
+++ b/frontend/src/lib/toolbar/MarkFavourite.svelte
@@ -0,0 +1,27 @@
+<script lang="ts">
+ import { getSelectionContext } from '$lib/Selection';
+ import { toastFinally } from '$lib/Toasts';
+ import Star from '$lib/icons/Star.svelte';
+ import { Client, getContextClient } from '@urql/svelte';
+
+ const client = getContextClient();
+ const selection = getSelectionContext();
+
+ export let mutation: (
+ client: Client,
+ args: { ids: number[]; input: { favourite: boolean } }
+ ) => Promise<unknown>;
+
+ function mutate(favourite: boolean) {
+ mutation(client, { ids: $selection.ids, input: { favourite } }).catch(toastFinally);
+ }
+</script>
+
+<button type="button" class="btn-slate justify-start gap-1" on:click={() => mutate(true)}>
+ <Star favourite={true} />
+ <span>Favourite</span>
+</button>
+<button type="button" class="btn-slate justify-start gap-1" on:click={() => mutate(false)}>
+ <Star favourite={false} />
+ <span>Unfavourite</span>
+</button>
diff --git a/frontend/src/lib/toolbar/MarkOrganized.svelte b/frontend/src/lib/toolbar/MarkOrganized.svelte
new file mode 100644
index 0000000..4dc3a83
--- /dev/null
+++ b/frontend/src/lib/toolbar/MarkOrganized.svelte
@@ -0,0 +1,27 @@
+<script lang="ts">
+ import { getSelectionContext } from '$lib/Selection';
+ import { toastFinally } from '$lib/Toasts';
+ import Organized from '$lib/icons/Organized.svelte';
+ import { Client, getContextClient } from '@urql/svelte';
+
+ const client = getContextClient();
+ const selection = getSelectionContext();
+
+ export let mutation: (
+ client: Client,
+ args: { ids: number[]; input: { organized: boolean } }
+ ) => Promise<unknown>;
+
+ function mutate(organized: boolean) {
+ mutation(client, { ids: $selection.ids, input: { organized } }).catch(toastFinally);
+ }
+</script>
+
+<button type="button" class="btn-slate flex justify-start gap-1" on:click={() => mutate(true)}>
+ <Organized tristate organized={true} />
+ <span>Organized</span>
+</button>
+<button type="button" class="btn-slate flex justify-start gap-1" on:click={() => mutate(false)}>
+ <Organized dim tristate organized={false} />
+ <span>Unorganized</span>
+</button>
diff --git a/frontend/src/lib/toolbar/MarkSelection.svelte b/frontend/src/lib/toolbar/MarkSelection.svelte
new file mode 100644
index 0000000..27eb2c7
--- /dev/null
+++ b/frontend/src/lib/toolbar/MarkSelection.svelte
@@ -0,0 +1,24 @@
+<script lang="ts">
+ import Dropdown from '$lib/components/Dropdown.svelte';
+
+ let visible = false;
+ let button: HTMLElement;
+</script>
+
+<div class="relative">
+ <button
+ type="button"
+ class="btn-slate rounded-inherit relative hover:bg-blue-700 [&:not(:only-child)]:bg-blue-700"
+ title="Set flag..."
+ bind:this={button}
+ on:click={() => (visible = !visible)}
+ >
+ <span class="icon-base icon-[material-symbols--flag] pointer-events-none" />
+ </button>
+
+ <Dropdown parent={button} bind:visible>
+ <div class="grid grid-cols-[min-content_min-content] gap-1">
+ <slot />
+ </div>
+ </Dropdown>
+</div>
diff --git a/frontend/src/lib/toolbar/Search.svelte b/frontend/src/lib/toolbar/Search.svelte
new file mode 100644
index 0000000..f033258
--- /dev/null
+++ b/frontend/src/lib/toolbar/Search.svelte
@@ -0,0 +1,21 @@
+<script lang="ts">
+ import { page } from '$app/stores';
+ import { debounce } from '$lib/Actions';
+ import { BasicFilterContext, getFilterContext } from '$lib/Filter';
+ import { accelerator } from '$lib/Shortcuts';
+
+ const filter = getFilterContext<BasicFilterContext>();
+
+ export let name: string;
+ export let field: string;
+</script>
+
+<input
+ type="text"
+ size={25}
+ class="btn-slate w-min"
+ placeholder="Search {name}..."
+ bind:value={field}
+ use:debounce={{ callback: () => $filter.apply($page.url.searchParams) }}
+ use:accelerator={'F'}
+/>
diff --git a/frontend/src/lib/toolbar/SelectItems.svelte b/frontend/src/lib/toolbar/SelectItems.svelte
new file mode 100644
index 0000000..7ff339e
--- /dev/null
+++ b/frontend/src/lib/toolbar/SelectItems.svelte
@@ -0,0 +1,19 @@
+<script lang="ts">
+ import { page } from '$app/stores';
+ import { getPaginationContext } from '$lib/Pagination';
+
+ const pagination = getPaginationContext();
+
+ $: values = new Set([24, 48, 72, 90, 120, 150, 180, $pagination.items].sort((a, b) => a - b));
+</script>
+
+<select
+ class="btn-slate"
+ bind:value={$pagination.items}
+ on:change={() => $pagination.apply($page.url.searchParams)}
+ title="Limit displayed items to..."
+>
+ {#each values as value}
+ <option {value}>{value}</option>
+ {/each}
+</select>
diff --git a/frontend/src/lib/toolbar/SelectSort.svelte b/frontend/src/lib/toolbar/SelectSort.svelte
new file mode 100644
index 0000000..fdcb057
--- /dev/null
+++ b/frontend/src/lib/toolbar/SelectSort.svelte
@@ -0,0 +1,61 @@
+<script lang="ts">
+ import { page } from '$app/stores';
+ import { SortDirection } from '$gql/graphql';
+
+ import { getSortContext } from '$lib/Sort';
+ import { slideXFast } from '$lib/Transitions';
+ import { getRandomInt } from '$lib/Utils';
+ import { slide } from 'svelte/transition';
+
+ const sort = getSortContext();
+
+ function toggle() {
+ if ($sort.direction === SortDirection.Ascending) {
+ $sort.direction = SortDirection.Descending;
+ } else {
+ $sort.direction = SortDirection.Ascending;
+ }
+
+ apply();
+ }
+
+ function apply() {
+ if ($sort.on === 'RANDOM' && $sort.seed === undefined) {
+ $sort.seed = getRandomInt(0, 1000000000);
+ }
+ $sort.apply($page.url.searchParams);
+ }
+
+ function reshuffle() {
+ $sort.seed = undefined;
+ apply();
+ }
+</script>
+
+<div class="rounded-group flex flex-row">
+ <select class="btn-slate" bind:value={$sort.on} on:change={apply} title="Sort on...">
+ {#each Object.entries($sort.labels) as [value, label]}
+ <option {value}>{label}</option>
+ {/each}
+ </select>
+ <button type="button" class="btn-slate" title="Toggle sort direction" on:click={toggle}>
+ {#if $sort.direction === SortDirection.Ascending}
+ <span class="icon-base icon-[material-symbols--sort] -scale-y-100" />
+ {:else}
+ <span class="icon-base icon-[material-symbols--sort]" />
+ {/if}
+ </button>
+ {#if $sort.on === 'RANDOM'}
+ <button
+ type="button"
+ class="btn-slate"
+ title="Reshuffle"
+ on:click={reshuffle}
+ transition:slide={slideXFast}
+ >
+ <div class="flex">
+ <span class="icon-base icon-[material-symbols--shuffle]" />
+ </div>
+ </button>
+ {/if}
+</div>
diff --git a/frontend/src/lib/toolbar/SelectionControls.svelte b/frontend/src/lib/toolbar/SelectionControls.svelte
new file mode 100644
index 0000000..4d309df
--- /dev/null
+++ b/frontend/src/lib/toolbar/SelectionControls.svelte
@@ -0,0 +1,57 @@
+<script lang="ts">
+ import { getSelectionContext } from '$lib/Selection';
+ import { accelerator } from '$lib/Shortcuts';
+ import { fadeDefault, slideXFast } from '$lib/Transitions';
+ import Badge from '$lib/components/Badge.svelte';
+ import { onDestroy } from 'svelte';
+ import { fade, slide } from 'svelte/transition';
+
+ const selection = getSelectionContext();
+
+ export let page = false;
+
+ const toggle = () => ($selection = $selection.toggle());
+ const all = () => ($selection = $selection.all());
+ const none = () => ($selection = $selection.none());
+
+ onDestroy(() => ($selection = $selection.clear()));
+</script>
+
+<div class="rounded-group flex">
+ <button
+ type="button"
+ class="btn-slate relative"
+ class:toggled={$selection.active}
+ title={`${$selection.active ? 'Exit' : 'Enter'} ${page ? 'page ' : ' '}selection mode`}
+ on:click={toggle}
+ use:accelerator={'s'}
+ >
+ {#if $selection.active}
+ {#if page}
+ <span class="icon-base icon-[material-symbols--edit-document]" />
+ {:else}
+ <span class="icon-base icon-[material-symbols--remove-selection]" />
+ {/if}
+ {:else if page}
+ <span class="icon-base icon-[material-symbols--edit-document-outline]" />
+ {:else}
+ <span class="icon-base icon-[material-symbols--select]" />
+ {/if}
+ <Badge number={$selection.size} />
+ </button>
+ {#if $selection.active}
+ <div class="rounded-group-end flex" transition:slide={slideXFast}>
+ <button type="button" class="btn-slate" title="Select all" on:click={all}>
+ <span class="icon-base icon-[material-symbols--select-all]" />
+ </button>
+ <button type="button" class="btn-slate" title="Select none" on:click={none}>
+ <span class="icon-base icon-[material-symbols--deselect]" />
+ </button>
+ </div>
+ {/if}
+</div>
+{#if $selection.size > 0}
+ <div class="rounded-group flex" transition:fade={fadeDefault}>
+ <slot />
+ </div>
+{/if}
diff --git a/frontend/src/lib/toolbar/ToggleAdvancedFilters.svelte b/frontend/src/lib/toolbar/ToggleAdvancedFilters.svelte
new file mode 100644
index 0000000..2e7869f
--- /dev/null
+++ b/frontend/src/lib/toolbar/ToggleAdvancedFilters.svelte
@@ -0,0 +1,40 @@
+<script lang="ts">
+ import { page } from '$app/stores';
+ import { getFilterContext } from '$lib/Filter';
+ import { navigate } from '$lib/Navigation';
+ import { slideXFast } from '$lib/Transitions';
+ import Badge from '$lib/components/Badge.svelte';
+ import { slide } from 'svelte/transition';
+ import { getToolbarContext } from './Toolbar.svelte';
+
+ const toolbar = getToolbarContext();
+ const filter = getFilterContext();
+</script>
+
+<div class="rounded-group flex">
+ <button
+ class:toggled={$toolbar.expand}
+ class="btn-slate relative"
+ title={`${$toolbar.expand ? 'Hide' : 'Show'} filters`}
+ on:click={() => ($toolbar.expand = !$toolbar.expand)}
+ >
+ {#if $toolbar.expand}
+ <span class="icon-base icon-[material-symbols--filter-alt]" />
+ {:else}
+ <span class="icon-base icon-[material-symbols--filter-alt-outline]" />
+ {/if}
+ <Badge number={$filter.include.size + $filter.exclude.size} />
+ </button>
+ {#if $filter.include.size + $filter.exclude.size > 0}
+ <button
+ class="btn-slate relative hover:bg-rose-700"
+ on:click={() => navigate({ filter: {} }, $page.url.searchParams)}
+ transition:slide={slideXFast}
+ title="Reset filters"
+ >
+ <div class="flex">
+ <span class="icon-base icon-[material-symbols--filter-alt-off]" />
+ </div>
+ </button>
+ {/if}
+</div>
diff --git a/frontend/src/lib/toolbar/Toolbar.svelte b/frontend/src/lib/toolbar/Toolbar.svelte
new file mode 100644
index 0000000..e87d731
--- /dev/null
+++ b/frontend/src/lib/toolbar/Toolbar.svelte
@@ -0,0 +1,42 @@
+<script lang="ts" context="module">
+ import { writable, type Writable } from 'svelte/store';
+
+ interface ToolbarContext {
+ expand: boolean;
+ }
+
+ function initToolbarContext() {
+ return setContext<Writable<ToolbarContext>>('toolbar', writable({ expand: false }));
+ }
+
+ export function getToolbarContext() {
+ return getContext<Writable<ToolbarContext>>('toolbar');
+ }
+</script>
+
+<script lang="ts">
+ import { getContext, setContext } from 'svelte';
+
+ const toolbar = initToolbarContext();
+</script>
+
+<div class="flex flex-col">
+ <div
+ class="flex flex-row flex-wrap gap-4 text-sm xl:grid xl:grid-flow-col xl:grid-cols-[1fr_2fr_1fr]"
+ >
+ <div class="flex flex-row justify-start gap-2">
+ <slot name="start" />
+ </div>
+ <div class="flex flex-row flex-wrap justify-start gap-2 xl:flex-nowrap xl:justify-center">
+ <slot name="center" />
+ </div>
+ <div class="flex flex-row justify-end gap-2">
+ <slot name="end" />
+ </div>
+ </div>
+ {#if $toolbar.expand}
+ <div class="mt-4">
+ <slot />
+ </div>
+ {/if}
+</div>