summaryrefslogtreecommitdiffstatshomepage
path: root/frontend/src/routes
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/routes')
-rw-r--r--frontend/src/routes/+layout.svelte60
-rw-r--r--frontend/src/routes/+page.svelte8
-rw-r--r--frontend/src/routes/archives/+page.svelte131
-rw-r--r--frontend/src/routes/archives/[id]/+page.svelte65
-rw-r--r--frontend/src/routes/artists/+page.svelte88
-rw-r--r--frontend/src/routes/characters/+page.svelte88
-rw-r--r--frontend/src/routes/circles/+page.svelte88
-rw-r--r--frontend/src/routes/comics/+page.svelte107
-rw-r--r--frontend/src/routes/comics/[id]/+page.svelte177
-rw-r--r--frontend/src/routes/namespaces/+page.svelte88
-rw-r--r--frontend/src/routes/statistics/+page.svelte4
-rw-r--r--frontend/src/routes/tags/+page.svelte100
-rw-r--r--frontend/src/routes/worlds/+page.svelte91
13 files changed, 528 insertions, 567 deletions
diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte
index 6af3b88..29a1c16 100644
--- a/frontend/src/routes/+layout.svelte
+++ b/frontend/src/routes/+layout.svelte
@@ -1,5 +1,6 @@
<script lang="ts">
import { addShortcut, handleShortcuts } from '$lib/Shortcuts';
+ import { toastFinally } from '$lib/Toasts';
import { fadeDefault } from '$lib/Transitions';
import AddArtist from '$lib/dialogs/AddArtist.svelte';
import AddCharacter from '$lib/dialogs/AddCharacter.svelte';
@@ -11,7 +12,7 @@
import Navigation from '$lib/navigation/Navigation.svelte';
import { cacheExchange, fetchExchange, initContextClient } from '@urql/svelte';
import { SvelteToast } from '@zerodevx/svelte-toast';
- import { Modals, closeModal, openModal } from 'svelte-modals';
+ import { Modals, modals, type ModalComponent } from 'svelte-modals';
import { fade } from 'svelte/transition';
import '../app.css';
@@ -20,12 +21,16 @@
exchanges: [cacheExchange, fetchExchange]
});
- addShortcut('na', () => openModal(AddArtist));
- addShortcut('nh', () => openModal(AddCharacter));
- addShortcut('ni', () => openModal(AddCircle));
- addShortcut('nn', () => openModal(AddNamespace));
- addShortcut('nt', () => openModal(AddTag));
- addShortcut('nw', () => openModal(AddWorld));
+ function open(modal: ModalComponent) {
+ modals.open(modal).catch(toastFinally);
+ }
+
+ addShortcut('na', () => open(AddArtist));
+ addShortcut('nh', () => open(AddCharacter));
+ addShortcut('ni', () => open(AddCircle));
+ addShortcut('nn', () => open(AddNamespace));
+ addShortcut('nt', () => open(AddTag));
+ addShortcut('nw', () => open(AddWorld));
function keydown(event: KeyboardEvent) {
handleShortcuts(event);
@@ -36,38 +41,38 @@
<Navigation>
<Link matchExact href="/" title="Home" accel="go">
- <span class="icon-base icon-[material-symbols--home]" />
+ <span class="icon-base icon-[material-symbols--home]"></span>
</Link>
<Link href="/comics/" title="Comics" accel="gc">
- <span class="icon-base icon-[material-symbols--menu-book]" />
+ <span class="icon-base icon-[material-symbols--menu-book]"></span>
</Link>
<Link href="/namespaces/" title="Namespaces" accel="gn">
- <span class="icon-base icon-[material-symbols--inbox]" />
+ <span class="icon-base icon-[material-symbols--inbox]"></span>
</Link>
<Link href="/tags/" title="Tags" accel="gt">
- <span class="icon-base icon-[material-symbols--label]" />
+ <span class="icon-base icon-[material-symbols--label]"></span>
</Link>
<Link href="/artists/" title="Artists" accel="ga">
- <span class="icon-base icon-[material-symbols--person]" />
+ <span class="icon-base icon-[material-symbols--person]"></span>
</Link>
<Link href="/circles/" title="Circles" accel="gi">
- <span class="icon-base icon-[material-symbols--group]" />
+ <span class="icon-base icon-[material-symbols--group]"></span>
</Link>
<Link href="/characters/" title="Characters" accel="gh">
- <span class="icon-base icon-[material-symbols--face]" />
+ <span class="icon-base icon-[material-symbols--face]"></span>
</Link>
<Link href="/worlds/" title="Worlds" accel="gw">
- <span class="icon-base icon-[material-symbols--public]" />
+ <span class="icon-base icon-[material-symbols--public]"></span>
</Link>
<Link href="/archives/" title="Archives" accel="gz">
- <span class="icon-base icon-[material-symbols--folder-zip]" />
+ <span class="icon-base icon-[material-symbols--folder-zip]"></span>
</Link>
- <div class="mb-auto" />
+ <div class="mb-auto"></div>
<Link href="/statistics/" title="Statistics" accel="gs">
- <span class="icon-base icon-[material-symbols--bar-chart]" />
+ <span class="icon-base icon-[material-symbols--bar-chart]"></span>
</Link>
<Link href="/help/" title="Help" accel="?" target="_blank">
- <span class="icon-base icon-[material-symbols--help]" />
+ <span class="icon-base icon-[material-symbols--help]"></span>
</Link>
</Navigation>
@@ -76,14 +81,15 @@
</div>
<Modals>
- <!-- svelte-ignore a11y-no-static-element-interactions -->
- <!-- svelte-ignore a11y-click-events-have-key-events -->
- <div
- slot="backdrop"
- on:click={closeModal}
- transition:fade={fadeDefault}
- class="fixed bottom-0 left-0 right-0 top-0 z-20 bg-stone-800/80"
- />
+ {#snippet backdrop({ close })}
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
+ <div
+ onclick={() => close()}
+ transition:fade={fadeDefault}
+ class="fixed bottom-0 left-0 right-0 top-0 z-20 bg-stone-800/80"
+ ></div>
+ {/snippet}
</Modals>
<SvelteToast options={{ reversed: true, intro: { y: 192 } }} />
diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte
index 97a7a60..32e4e07 100644
--- a/frontend/src/routes/+page.svelte
+++ b/frontend/src/routes/+page.svelte
@@ -19,10 +19,10 @@
});
const favouriteLink = href('comics', { filter: { include: { favourite: true } } });
- $: query = frontpageQuery(getContextClient());
- $: recent = $query.data?.recent;
- $: favourites = $query.data?.favourites;
- $: bookmarked = $query.data?.bookmarked;
+ let query = $derived(frontpageQuery(getContextClient()));
+ let recent = $derived($query.data?.recent);
+ let favourites = $derived($query.data?.favourites);
+ let bookmarked = $derived($query.data?.bookmarked);
</script>
<Head section="Home" />
diff --git a/frontend/src/routes/archives/+page.svelte b/frontend/src/routes/archives/+page.svelte
index 545058a..3fc4ed4 100644
--- a/frontend/src/routes/archives/+page.svelte
+++ b/frontend/src/routes/archives/+page.svelte
@@ -3,10 +3,7 @@
import { archivesQuery } from '$gql/Queries';
import type { ArchiveFragment } from '$gql/graphql';
import { ArchiveSortLabel } from '$lib/Enums';
- import { ArchiveFilterContext, initFilterContext } from '$lib/Filter';
- import { initPaginationContext } from '$lib/Pagination';
- import { initSelectionContext } from '$lib/Selection';
- import { initSortContext } from '$lib/Sort';
+ import { ArchiveFilterContext } from '$lib/Filter.svelte';
import Card from '$lib/components/Card.svelte';
import Empty from '$lib/components/Empty.svelte';
import Guard from '$lib/components/Guard.svelte';
@@ -17,6 +14,7 @@
import Pagination from '$lib/pagination/Pagination.svelte';
import Pill from '$lib/pills/Pill.svelte';
import Selectable from '$lib/selection/Selectable.svelte';
+ import { initSelectionContext } from '$lib/selection/Selection.svelte';
import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte';
import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte';
import FilterOrganized from '$lib/toolbar/FilterOrganized.svelte';
@@ -29,90 +27,91 @@
import Toolbar from '$lib/toolbar/Toolbar.svelte';
import { getContextClient } from '@urql/svelte';
import { filesize } from 'filesize';
- import type { PageData } from './$types';
+ import type { PageProps } from './$types';
- let client = getContextClient();
+ let { data }: PageProps = $props();
+ let pagination = $derived(data.pagination);
+ let sort = $derived(data.sort);
- export let data: PageData;
+ const client = getContextClient();
+ let result = $derived(archivesQuery(client, { ...data }));
+ let archives = $derived($result.data?.archives);
- $: result = archivesQuery(client, {
- pagination: data.pagination,
- filter: data.filter,
- sort: data.sort
+ let selection = initSelectionContext<ArchiveFragment>('Archive', (a) => a.name);
+ $effect(() => {
+ if (archives) {
+ selection.view = archives.edges;
+ }
});
- $: archives = $result.data?.archives;
-
- const selection = initSelectionContext<ArchiveFragment>('Archive', (a) => a.name);
- $: if (archives) {
- $selection.view = archives.edges;
- $pagination.total = archives.count;
- }
-
- const pagination = initPaginationContext();
- $: $pagination.update = data.pagination;
-
- const filter = initFilterContext<ArchiveFilterContext>();
- $: $filter = new ArchiveFilterContext(data.filter);
-
- const sort = initSortContext(data.sort, ArchiveSortLabel);
- $: $sort.update = data.sort;
-
- function refresh() {
- result.reexecute({ requestPolicy: 'network-only' });
- }
+ let filter = $state(new ArchiveFilterContext(data.filter));
+ $effect(() => {
+ filter = new ArchiveFilterContext(data.filter);
+ });
</script>
<Head section="Archives" />
<Column>
<Toolbar>
- <SelectionControls slot="start">
- <MarkSelection>
- <MarkOrganized mutation={updateArchives} />
- </MarkSelection>
- <DeleteSelection
- mutation={deleteArchives}
- warning="Deleting an archive will also delete its archive file on disk as well as all comics that belong to it."
- />
- </SelectionControls>
- <svelte:fragment slot="center">
- <Search name="Archives" bind:field={$filter.include.controls.path.contains} />
- <FilterOrganized />
- <SelectSort />
- <SelectItems />
- </svelte:fragment>
- <RefreshButton slot="end" on:click={refresh} />
+ {#snippet start()}
+ <SelectionControls>
+ <MarkSelection>
+ <MarkOrganized mutation={updateArchives} />
+ </MarkSelection>
+ <DeleteSelection
+ mutation={deleteArchives}
+ warning="Deleting an archive will also delete its archive file on disk as well as all comics that belong to it."
+ />
+ </SelectionControls>
+ {/snippet}
+ {#snippet center()}
+ <Search name="Archives" {filter} bind:field={filter.include.path.contains} />
+ <FilterOrganized {filter} />
+ <SelectSort {sort} labels={ArchiveSortLabel} />
+ <SelectItems {pagination} />
+ {/snippet}
+ {#snippet end()}
+ <RefreshButton onclick={() => result.reexecute({ requestPolicy: 'network-only' })} />
+ {/snippet}
</Toolbar>
{#if archives}
- <Pagination />
+ <Pagination {pagination} total={archives.count} />
<main>
<Cards>
{#each archives.edges as { id, name, cover, size, pageCount }, index (id)}
- <Selectable {index} {id} let:handle let:selected>
- <Card
- ellipsis={false}
- href={id.toString()}
- details={{ title: name, cover: cover }}
- on:click={handle}
- >
- <SelectionOverlay position="left" {selected} slot="overlay" />
- <div class="flex gap-1 text-xs">
- <Pill name={`${pageCount} pages`}>
- <span class="icon-[material-symbols--note] mr-0.5" slot="icon" />
- </Pill>
- <Pill name={filesize(size, { base: 2 })}>
- <span class="icon-[material-symbols--hard-drive] mr-0.5" slot="icon" />
- </Pill>
- </div>
- </Card>
+ <Selectable {index} {id}>
+ {#snippet children({ onclick, selected })}
+ <Card
+ ellipsis={false}
+ href={id.toString()}
+ details={{ title: name, cover }}
+ {onclick}
+ >
+ {#snippet overlay()}
+ <SelectionOverlay position="left" {selected} />
+ {/snippet}
+ <div class="flex gap-1 text-xs">
+ <Pill name={`${pageCount} pages`}>
+ {#snippet icon()}
+ <span class="icon-[material-symbols--note] mr-0.5"></span>
+ {/snippet}
+ </Pill>
+ <Pill name={filesize(size, { base: 2 })}>
+ {#snippet icon()}
+ <span class="icon-[material-symbols--hard-drive] mr-0.5"></span>
+ {/snippet}
+ </Pill>
+ </div>
+ </Card>
+ {/snippet}
</Selectable>
{:else}
<Empty />
{/each}
</Cards>
</main>
- <Pagination />
+ <Pagination {pagination} total={archives.count} />
{:else}
<Guard {result} />
{/if}
diff --git a/frontend/src/routes/archives/[id]/+page.svelte b/frontend/src/routes/archives/[id]/+page.svelte
index 50a2940..56c3273 100644
--- a/frontend/src/routes/archives/[id]/+page.svelte
+++ b/frontend/src/routes/archives/[id]/+page.svelte
@@ -2,9 +2,6 @@
import { updateArchives } from '$gql/Mutations';
import { archiveQuery } from '$gql/Queries';
import { Direction, Layout, type FullArchiveFragment, type PageFragment } from '$gql/graphql';
- import { initReaderContext } from '$lib/Reader';
- import { initSelectionContext } from '$lib/Selection';
- import { setTabContext } from '$lib/Tabs';
import { toastFinally } from '$lib/Toasts';
import Guard from '$lib/components/Guard.svelte';
import Head from '$lib/components/Head.svelte';
@@ -12,52 +9,40 @@
import Grid from '$lib/containers/Grid.svelte';
import Gallery from '$lib/gallery/Gallery.svelte';
import PageView from '$lib/reader/PageView.svelte';
- import Reader from '$lib/reader/Reader.svelte';
+ import Reader, { initReaderContext } from '$lib/reader/Reader.svelte';
+ import { initSelectionContext } from '$lib/selection/Selection.svelte';
import ArchiveDelete from '$lib/tabs/ArchiveDelete.svelte';
import ArchiveDetails from '$lib/tabs/ArchiveDetails.svelte';
import ArchiveEdit from '$lib/tabs/ArchiveEdit.svelte';
import Tab from '$lib/tabs/Tab.svelte';
import Tabs from '$lib/tabs/Tabs.svelte';
import { getContextClient } from '@urql/svelte';
- import type { PageData } from './$types';
+ import type { PageProps } from './$types';
- export let data: PageData;
+ let { data }: PageProps = $props();
const client = getContextClient();
const reader = initReaderContext();
- setTabContext({
- tabs: {
- details: { title: 'Details' },
- edit: { title: 'Edit' },
- deletion: { title: 'Delete' }
- },
- current: 'details'
- });
-
- $: result = archiveQuery(client, { id: data.id });
- function updateCover(event: CustomEvent<number>) {
- updateArchives(client, { ids: archive.id, input: { cover: { id: event.detail } } }).catch(
- toastFinally
- );
+ function updateCover(id: number) {
+ updateArchives(client, { ids: data.id, input: { cover: { id } } }).catch(toastFinally);
}
- let archive: FullArchiveFragment;
+ let selection = initSelectionContext<PageFragment>(
+ 'Page',
+ (p) => p.path,
+ (p) => p.comicId === null
+ );
- $: $result, update();
- function update() {
- if (!$result.stale && $result.data?.archive.__typename === 'FullArchive') {
- archive = structuredClone($result.data.archive);
+ let result = $derived(archiveQuery(client, { id: data.id }));
+ let archive: FullArchiveFragment | undefined = $state();
- $reader.pages = archive.pages;
+ $effect(() => {
+ if (!$result.stale && $result.data?.archive.__typename === 'FullArchive') {
+ archive = $result.data.archive;
+ reader.pages = $result.data.archive.pages;
+ selection.view = $result.data.archive.pages;
}
- }
-
- const selection = initSelectionContext<PageFragment>('Page', (p) => p.path);
- $selection.selectable = (p) => p.comicId === null;
-
- $: if (archive) {
- $selection.view = archive.pages;
- }
+ });
</script>
<Head section="Archive" title={archive?.name} />
@@ -70,24 +55,20 @@
<aside>
<Tabs>
- <Tab id="details">
+ <Tab initial id="details" title="Details">
<ArchiveDetails {archive} />
</Tab>
- <Tab id="edit">
+ <Tab id="edit" title="Edit">
<ArchiveEdit {archive} />
</Tab>
- <Tab id="deletion">
+ <Tab id="deletion" title="Delete">
<ArchiveDelete {archive} />
</Tab>
</Tabs>
</aside>
<main class="overflow-auto">
- <Gallery
- pages={archive.pages}
- on:open={(e) => ($reader = $reader.open(e.detail))}
- on:cover={updateCover}
- />
+ <Gallery pages={archive.pages} open={reader.open} {updateCover} />
</main>
</Grid>
{:else}
diff --git a/frontend/src/routes/artists/+page.svelte b/frontend/src/routes/artists/+page.svelte
index e07338c..c907470 100644
--- a/frontend/src/routes/artists/+page.svelte
+++ b/frontend/src/routes/artists/+page.svelte
@@ -3,10 +3,7 @@
import { artistsQuery, fetchArtist } from '$gql/Queries';
import type { Artist } from '$gql/graphql';
import { ArtistSortLabel } from '$lib/Enums';
- import { BasicFilterContext, initFilterContext } from '$lib/Filter';
- import { initPaginationContext } from '$lib/Pagination';
- import { initSelectionContext } from '$lib/Selection';
- import { initSortContext } from '$lib/Sort';
+ import { BasicFilterContext } from '$lib/Filter.svelte';
import { toastFinally } from '$lib/Toasts';
import AddButton from '$lib/components/AddButton.svelte';
import Cardlet from '$lib/components/Cardlet.svelte';
@@ -19,6 +16,7 @@
import EditArtist from '$lib/dialogs/EditArtist.svelte';
import Pagination from '$lib/pagination/Pagination.svelte';
import Selectable from '$lib/selection/Selectable.svelte';
+ import { initSelectionContext } from '$lib/selection/Selection.svelte';
import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte';
import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte';
import Search from '$lib/toolbar/Search.svelte';
@@ -27,38 +25,32 @@
import SelectionControls from '$lib/toolbar/SelectionControls.svelte';
import Toolbar from '$lib/toolbar/Toolbar.svelte';
import { getContextClient } from '@urql/svelte';
- import { openModal } from 'svelte-modals';
- import type { PageData } from './$types';
+ import { modals } from 'svelte-modals';
+ import type { PageProps } from './$types';
+
+ let { data }: PageProps = $props();
+ let pagination = $derived(data.pagination);
+ let sort = $derived(data.sort);
const client = getContextClient();
- export let data: PageData;
+ let result = $derived(artistsQuery(client, { ...data }));
+ let artists = $derived($result.data?.artists);
- $: result = artistsQuery(client, {
- pagination: data.pagination,
- filter: data.filter,
- sort: data.sort
+ let selection = initSelectionContext<Artist>('Artist', (a) => a.name);
+ $effect(() => {
+ if (artists) {
+ selection.view = artists.edges;
+ }
});
- $: artists = $result.data?.artists;
-
- const selection = initSelectionContext<Artist>('Artist', (a) => a.name);
- $: if (artists) {
- $selection.view = artists.edges;
- $pagination.total = artists.count;
- }
-
- const filter = initFilterContext<BasicFilterContext>();
- $: $filter = new BasicFilterContext(data.filter);
-
- const sort = initSortContext(data.sort, ArtistSortLabel);
- $: $sort.update = data.sort;
-
- const pagination = initPaginationContext();
- $: $pagination.update = data.pagination;
+ let filter = $state(new BasicFilterContext(data.filter));
+ $effect(() => {
+ filter = new BasicFilterContext(data.filter);
+ });
const edit = (id: number) => {
fetchArtist(client, id)
- .then((artist) => openModal(EditArtist, { artist }))
+ .then((artist) => modals.open(EditArtist, { artist }))
.catch(toastFinally);
};
</script>
@@ -67,34 +59,40 @@
<Column>
<Toolbar>
- <SelectionControls slot="start">
- <DeleteSelection mutation={deleteArtists} />
- </SelectionControls>
- <svelte:fragment slot="center">
- <Search name="Artists" bind:field={$filter.include.controls.name.contains} />
- <SelectSort />
- <SelectItems />
- </svelte:fragment>
- <svelte:fragment slot="end">
- <AddButton title="Add Artist" on:click={() => openModal(AddArtist)} />
- </svelte:fragment>
+ {#snippet start()}
+ <SelectionControls>
+ <DeleteSelection mutation={deleteArtists} />
+ </SelectionControls>
+ {/snippet}
+ {#snippet center()}
+ <Search name="Artists" {filter} bind:field={filter.include.name.contains} />
+ <SelectSort {sort} labels={ArtistSortLabel} />
+ <SelectItems {pagination} />
+ {/snippet}
+ {#snippet end()}
+ <AddButton title="Add Artist" onclick={() => modals.open(AddArtist)} />
+ {/snippet}
</Toolbar>
{#if artists}
- <Pagination />
+ <Pagination {pagination} total={artists.count} />
<main>
<Cardlets>
{#each artists.edges as { id, name }, index (id)}
- <Selectable {index} {id} {edit} let:handle let:selected>
- <Cardlet {name} on:click={handle} filter="artists" {id}>
- <SelectionOverlay slot="overlay" position="right" centered {selected} />
- </Cardlet>
+ <Selectable {index} {id} {edit}>
+ {#snippet children({ onclick, selected })}
+ <Cardlet {name} {onclick} filter="artists" {id}>
+ {#snippet overlay()}
+ <SelectionOverlay position="right" centered {selected} />
+ {/snippet}
+ </Cardlet>
+ {/snippet}
</Selectable>
{:else}
<Empty />
{/each}
</Cardlets>
</main>
- <Pagination />
+ <Pagination {pagination} total={artists.count} />
{:else}
<Guard {result} />
{/if}
diff --git a/frontend/src/routes/characters/+page.svelte b/frontend/src/routes/characters/+page.svelte
index 0934bab..04c72cb 100644
--- a/frontend/src/routes/characters/+page.svelte
+++ b/frontend/src/routes/characters/+page.svelte
@@ -3,10 +3,7 @@
import { charactersQuery, fetchCharacter } from '$gql/Queries';
import type { Character } from '$gql/graphql';
import { CharacterSortLabel } from '$lib/Enums';
- import { BasicFilterContext, initFilterContext } from '$lib/Filter';
- import { initPaginationContext } from '$lib/Pagination';
- import { initSelectionContext } from '$lib/Selection';
- import { initSortContext } from '$lib/Sort';
+ import { BasicFilterContext } from '$lib/Filter.svelte';
import { toastFinally } from '$lib/Toasts';
import AddButton from '$lib/components/AddButton.svelte';
import Cardlet from '$lib/components/Cardlet.svelte';
@@ -19,6 +16,7 @@
import EditCharacter from '$lib/dialogs/EditCharacter.svelte';
import Pagination from '$lib/pagination/Pagination.svelte';
import Selectable from '$lib/selection/Selectable.svelte';
+ import { initSelectionContext } from '$lib/selection/Selection.svelte';
import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte';
import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte';
import Search from '$lib/toolbar/Search.svelte';
@@ -27,38 +25,32 @@
import SelectionControls from '$lib/toolbar/SelectionControls.svelte';
import Toolbar from '$lib/toolbar/Toolbar.svelte';
import { getContextClient } from '@urql/svelte';
- import { openModal } from 'svelte-modals';
- import type { PageData } from './$types';
+ import { modals } from 'svelte-modals';
+ import type { PageProps } from './$types';
+
+ let { data }: PageProps = $props();
+ let pagination = $derived(data.pagination);
+ let sort = $derived(data.sort);
const client = getContextClient();
- export let data: PageData;
+ let result = $derived(charactersQuery(client, { ...data }));
+ let characters = $derived($result.data?.characters);
- $: result = charactersQuery(client, {
- pagination: data.pagination,
- filter: data.filter,
- sort: data.sort
+ let selection = initSelectionContext<Character>('Character', (a) => a.name);
+ $effect(() => {
+ if (characters) {
+ selection.view = characters.edges;
+ }
});
- $: characters = $result.data?.characters;
-
- const selection = initSelectionContext<Character>('Character', (c) => c.name);
- $: if (characters) {
- $selection.view = characters.edges;
- $pagination.total = characters.count;
- }
-
- const filter = initFilterContext<BasicFilterContext>();
- $: $filter = new BasicFilterContext(data.filter);
-
- const sort = initSortContext(data.sort, CharacterSortLabel);
- $: $sort.update = data.sort;
-
- const pagination = initPaginationContext();
- $: $pagination.update = data.pagination;
+ let filter = $state(new BasicFilterContext(data.filter));
+ $effect(() => {
+ filter = new BasicFilterContext(data.filter);
+ });
const edit = (id: number) => {
fetchCharacter(client, id)
- .then((character) => openModal(EditCharacter, { character }))
+ .then((character) => modals.open(EditCharacter, { character }))
.catch(toastFinally);
};
</script>
@@ -67,34 +59,40 @@
<Column>
<Toolbar>
- <SelectionControls slot="start">
- <DeleteSelection mutation={deleteCharacters} />
- </SelectionControls>
- <svelte:fragment slot="center">
- <Search name="Characters" bind:field={$filter.include.controls.name.contains} />
- <SelectSort />
- <SelectItems />
- </svelte:fragment>
- <svelte:fragment slot="end">
- <AddButton title="Add Character" on:click={() => openModal(AddCharacter)} />
- </svelte:fragment>
+ {#snippet start()}
+ <SelectionControls>
+ <DeleteSelection mutation={deleteCharacters} />
+ </SelectionControls>
+ {/snippet}
+ {#snippet center()}
+ <Search name="Characters" {filter} bind:field={filter.include.name.contains} />
+ <SelectSort {sort} labels={CharacterSortLabel} />
+ <SelectItems {pagination} />
+ {/snippet}
+ {#snippet end()}
+ <AddButton title="Add Character" onclick={() => modals.open(AddCharacter)} />
+ {/snippet}
</Toolbar>
{#if characters}
- <Pagination />
+ <Pagination {pagination} total={characters.count} />
<main>
<Cardlets>
{#each characters.edges as { id, name }, index (id)}
- <Selectable {index} {id} {edit} let:handle let:selected>
- <Cardlet {name} on:click={handle} filter="characters" {id}>
- <SelectionOverlay slot="overlay" position="right" centered {selected} />
- </Cardlet>
+ <Selectable {index} {id} {edit}>
+ {#snippet children({ onclick, selected })}
+ <Cardlet {name} {onclick} filter="characters" {id}>
+ {#snippet overlay()}
+ <SelectionOverlay position="right" centered {selected} />
+ {/snippet}
+ </Cardlet>
+ {/snippet}
</Selectable>
{:else}
<Empty />
{/each}
</Cardlets>
</main>
- <Pagination />
+ <Pagination {pagination} total={characters.count} />
{:else}
<Guard {result} />
{/if}
diff --git a/frontend/src/routes/circles/+page.svelte b/frontend/src/routes/circles/+page.svelte
index 14b0866..57520f8 100644
--- a/frontend/src/routes/circles/+page.svelte
+++ b/frontend/src/routes/circles/+page.svelte
@@ -3,10 +3,7 @@
import { circlesQuery, fetchCircle } from '$gql/Queries';
import type { Circle } from '$gql/graphql';
import { CircleSortLabel } from '$lib/Enums';
- import { BasicFilterContext, initFilterContext } from '$lib/Filter';
- import { initPaginationContext } from '$lib/Pagination';
- import { initSelectionContext } from '$lib/Selection';
- import { initSortContext } from '$lib/Sort';
+ import { BasicFilterContext } from '$lib/Filter.svelte';
import { toastFinally } from '$lib/Toasts';
import AddButton from '$lib/components/AddButton.svelte';
import Cardlet from '$lib/components/Cardlet.svelte';
@@ -19,6 +16,7 @@
import EditCircle from '$lib/dialogs/EditCircle.svelte';
import Pagination from '$lib/pagination/Pagination.svelte';
import Selectable from '$lib/selection/Selectable.svelte';
+ import { initSelectionContext } from '$lib/selection/Selection.svelte';
import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte';
import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte';
import Search from '$lib/toolbar/Search.svelte';
@@ -27,38 +25,32 @@
import SelectionControls from '$lib/toolbar/SelectionControls.svelte';
import Toolbar from '$lib/toolbar/Toolbar.svelte';
import { getContextClient } from '@urql/svelte';
- import { openModal } from 'svelte-modals';
- import type { PageData } from './$types';
+ import { modals } from 'svelte-modals';
+ import type { PageProps } from './$types';
+
+ let { data }: PageProps = $props();
+ let pagination = $derived(data.pagination);
+ let sort = $derived(data.sort);
const client = getContextClient();
- export let data: PageData;
+ let result = $derived(circlesQuery(client, { ...data }));
+ let circles = $derived($result.data?.circles);
- $: result = circlesQuery(client, {
- pagination: data.pagination,
- filter: data.filter,
- sort: data.sort
+ let selection = initSelectionContext<Circle>('Circle', (a) => a.name);
+ $effect(() => {
+ if (circles) {
+ selection.view = circles.edges;
+ }
});
- $: circles = $result.data?.circles;
-
- const selection = initSelectionContext<Circle>('Circle', (c) => c.name);
- $: if (circles) {
- $selection.view = circles.edges;
- $pagination.total = circles.count;
- }
-
- const filter = initFilterContext<BasicFilterContext>();
- $: $filter = new BasicFilterContext(data.filter);
-
- const sort = initSortContext(data.sort, CircleSortLabel);
- $: $sort.update = data.sort;
-
- const pagination = initPaginationContext();
- $: $pagination.update = data.pagination;
+ let filter = $state(new BasicFilterContext(data.filter));
+ $effect(() => {
+ filter = new BasicFilterContext(data.filter);
+ });
const edit = (id: number) => {
fetchCircle(client, id)
- .then((circle) => openModal(EditCircle, { circle }))
+ .then((circle) => modals.open(EditCircle, { circle }))
.catch(toastFinally);
};
</script>
@@ -67,34 +59,40 @@
<Column>
<Toolbar>
- <SelectionControls slot="start">
- <DeleteSelection mutation={deleteCircles} />
- </SelectionControls>
- <svelte:fragment slot="center">
- <Search name="Circles" bind:field={$filter.include.controls.name.contains} />
- <SelectSort />
- <SelectItems />
- </svelte:fragment>
- <svelte:fragment slot="end">
- <AddButton title="Add Circle" on:click={() => openModal(AddCircle)} />
- </svelte:fragment>
+ {#snippet start()}
+ <SelectionControls>
+ <DeleteSelection mutation={deleteCircles} />
+ </SelectionControls>
+ {/snippet}
+ {#snippet center()}
+ <Search name="Circles" {filter} bind:field={filter.include.name.contains} />
+ <SelectSort {sort} labels={CircleSortLabel} />
+ <SelectItems {pagination} />
+ {/snippet}
+ {#snippet end()}
+ <AddButton title="Add Circle" onclick={() => modals.open(AddCircle)} />
+ {/snippet}
</Toolbar>
{#if circles}
- <Pagination />
+ <Pagination {pagination} total={circles.count} />
<main>
<Cardlets>
{#each circles.edges as { id, name }, index (id)}
- <Selectable {index} {id} {edit} let:handle let:selected>
- <Cardlet {name} on:click={handle} filter="circles" {id}>
- <SelectionOverlay slot="overlay" position="right" centered {selected} />
- </Cardlet>
+ <Selectable {index} {id} {edit}>
+ {#snippet children({ onclick, selected })}
+ <Cardlet {name} {onclick} filter="circles" {id}>
+ {#snippet overlay()}
+ <SelectionOverlay position="right" centered {selected} />
+ {/snippet}
+ </Cardlet>
+ {/snippet}
</Selectable>
{:else}
<Empty />
{/each}
</Cardlets>
</main>
- <Pagination />
+ <Pagination {pagination} total={circles.count} />
{:else}
<Guard {result} />
{/if}
diff --git a/frontend/src/routes/comics/+page.svelte b/frontend/src/routes/comics/+page.svelte
index 353d69c..372fd1a 100644
--- a/frontend/src/routes/comics/+page.svelte
+++ b/frontend/src/routes/comics/+page.svelte
@@ -3,10 +3,7 @@
import { comicsQuery } from '$gql/Queries';
import { type ComicFragment } from '$gql/graphql';
import { ComicSortLabel } from '$lib/Enums';
- import { ComicFilterContext, initFilterContext } from '$lib/Filter';
- import { initPaginationContext } from '$lib/Pagination';
- import { initSelectionContext } from '$lib/Selection';
- import { initSortContext } from '$lib/Sort';
+ import { ComicFilterContext } from '$lib/Filter.svelte';
import Card, { comicCard } from '$lib/components/Card.svelte';
import Empty from '$lib/components/Empty.svelte';
import Guard from '$lib/components/Guard.svelte';
@@ -18,6 +15,7 @@
import Pagination from '$lib/pagination/Pagination.svelte';
import ComicPills from '$lib/pills/ComicPills.svelte';
import Selectable from '$lib/selection/Selectable.svelte';
+ import { initSelectionContext } from '$lib/selection/Selection.svelte';
import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte';
import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte';
import EditSelection from '$lib/toolbar/EditSelection.svelte';
@@ -35,81 +33,82 @@
import ToggleAdvancedFilters from '$lib/toolbar/ToggleAdvancedFilters.svelte';
import Toolbar from '$lib/toolbar/Toolbar.svelte';
import { getContextClient } from '@urql/svelte';
- import type { PageData } from './$types';
+ import type { PageProps } from './$types';
- export let data: PageData;
+ let { data }: PageProps = $props();
+ let pagination = $derived(data.pagination);
+ let sort = $derived(data.sort);
const client = getContextClient();
-
- $: result = comicsQuery(client, {
- pagination: data.pagination,
- filter: data.filter,
- sort: data.sort
- });
-
- $: comics = $result.data?.comics;
+ let result = $derived(comicsQuery(client, { ...data }));
+ let comics = $derived($result.data?.comics);
const selection = initSelectionContext<ComicFragment>('Comic', (c) => c.title);
- $: if (comics) {
- $selection.view = comics.edges;
- $pagination.total = comics.count;
- }
-
- const filter = initFilterContext<ComicFilterContext>();
- $: $filter = new ComicFilterContext(data.filter);
-
- const sort = initSortContext(data.sort, ComicSortLabel);
- $: $sort.update = data.sort;
+ $effect(() => {
+ if (comics) {
+ selection.view = comics.edges;
+ }
+ });
- const pagination = initPaginationContext();
- $: $pagination.update = data.pagination;
+ let filter = $state(new ComicFilterContext(data.filter));
+ $effect(() => {
+ filter = new ComicFilterContext(data.filter);
+ });
</script>
<Head section="Comics" />
<Column>
<Toolbar>
- <SelectionControls slot="start">
- <MarkSelection>
- <MarkFavourite mutation={updateComics} />
- <hr class="col-span-2 border-slate-600" />
- <MarkBookmark mutation={updateComics} />
- <hr class="col-span-2 border-slate-600" />
- <MarkOrganized mutation={updateComics} />
- </MarkSelection>
- <EditSelection dialog={UpdateComicsDialog} />
- <DeleteSelection mutation={deleteComics} />
- </SelectionControls>
- <svelte:fragment slot="center">
- <Search name="Comics" bind:field={$filter.include.controls.title.contains} />
- <ToggleAdvancedFilters />
+ {#snippet start()}
+ <SelectionControls>
+ <MarkSelection>
+ <MarkFavourite mutation={updateComics} />
+ <hr class="col-span-2 border-slate-600" />
+ <MarkBookmark mutation={updateComics} />
+ <hr class="col-span-2 border-slate-600" />
+ <MarkOrganized mutation={updateComics} />
+ </MarkSelection>
+ <EditSelection dialog={UpdateComicsDialog} />
+ <DeleteSelection mutation={deleteComics} />
+ </SelectionControls>
+ {/snippet}
+ {#snippet center({ expanded, toggle })}
+ <Search name="Comics" {filter} bind:field={filter.include.title.contains} />
+ <ToggleAdvancedFilters {expanded} {toggle} filterSize={filter.includes + filter.excludes} />
<div class="rounded-group flex">
- <FilterFavourites />
- <FilterBookmarked />
- <FilterOrganized />
+ <FilterFavourites {filter} />
+ <FilterBookmarked {filter} />
+ <FilterOrganized {filter} />
</div>
- <SelectSort />
- <SelectItems />
- </svelte:fragment>
- <ComicFilterForm />
+ <SelectSort {sort} labels={ComicSortLabel} />
+ <SelectItems {pagination} />
+ {/snippet}
+ {#snippet expansion()}
+ <ComicFilterForm {filter} />
+ {/snippet}
</Toolbar>
{#if comics}
- <Pagination />
+ <Pagination {pagination} total={comics.count} />
<main>
<Cards>
{#each comics.edges as comic, index (comic.id)}
- <Selectable {index} id={comic.id} let:handle let:selected>
- <Card {...comicCard(comic)} on:click={handle}>
- <SelectionOverlay position="left" {selected} slot="overlay" />
- <ComicPills {comic} />
- </Card>
+ <Selectable {index} id={comic.id}>
+ {#snippet children({ onclick, selected })}
+ <Card {...comicCard(comic)} {onclick}>
+ {#snippet overlay()}
+ <SelectionOverlay position="left" {selected} />
+ {/snippet}
+ <ComicPills {comic} />
+ </Card>
+ {/snippet}
</Selectable>
{:else}
<Empty />
{/each}
</Cards>
</main>
- <Pagination />
+ <Pagination {pagination} total={comics.count} />
{:else}
<Guard {result} />
{/if}
diff --git a/frontend/src/routes/comics/[id]/+page.svelte b/frontend/src/routes/comics/[id]/+page.svelte
index cfc5840..48c588f 100644
--- a/frontend/src/routes/comics/[id]/+page.svelte
+++ b/frontend/src/routes/comics/[id]/+page.svelte
@@ -2,12 +2,14 @@
import { beforeNavigate } from '$app/navigation';
import { updateComics } from '$gql/Mutations';
import { comicQuery } from '$gql/Queries';
- import { comicEquals } from '$gql/Utils';
- import { UpdateMode, type FullComicFragment, type UpdateComicInput } from '$gql/graphql';
- import { initReaderContext } from '$lib/Reader';
- import { initScraperContext } from '$lib/Scraper';
- import { initSelectionContext } from '$lib/Selection';
- import { setTabContext } from '$lib/Tabs';
+ import { omitIdentifiers, type OmitIdentifiers } from '$gql/Utils';
+ import {
+ UpdateMode,
+ type FullComicFragment,
+ type PageFragment,
+ type UpdateComicInput
+ } from '$gql/graphql';
+ import { comicPending } from '$lib/Form';
import { toastFinally } from '$lib/Toasts';
import { preventOnPending } from '$lib/Utils';
import BookmarkButton from '$lib/components/BookmarkButton.svelte';
@@ -21,155 +23,140 @@
import ComicForm from '$lib/forms/ComicForm.svelte';
import Gallery from '$lib/gallery/Gallery.svelte';
import PageView from '$lib/reader/PageView.svelte';
- import Reader from '$lib/reader/Reader.svelte';
+ import Reader, { initReaderContext } from '$lib/reader/Reader.svelte';
import ComicScrapeForm from '$lib/scraper/ComicScrapeForm.svelte';
+ import { initScraperContext } from '$lib/scraper/Scraper.svelte';
+ import { initSelectionContext } from '$lib/selection/Selection.svelte';
import ComicDelete from '$lib/tabs/ComicDelete.svelte';
import ComicDetails from '$lib/tabs/ComicDetails.svelte';
import Tab from '$lib/tabs/Tab.svelte';
import Tabs from '$lib/tabs/Tabs.svelte';
import SelectionControls from '$lib/toolbar/SelectionControls.svelte';
import { getContextClient } from '@urql/svelte';
- import type { PageData } from './$types';
+ import { untrack } from 'svelte';
+ import type { PageProps } from './$types';
+ let { data }: PageProps = $props();
const client = getContextClient();
const reader = initReaderContext();
- const selection = initSelectionContext();
const scraper = initScraperContext();
- const tabContext = setTabContext({
- tabs: {
- details: { title: 'Details' },
- edit: { title: 'Edit' },
- scrape: { title: 'Scrape' },
- deletion: { title: 'Delete' }
- },
- current: 'details'
- });
+ const selection = initSelectionContext<PageFragment>('Page', (p) => p.path);
- export let data: PageData;
- $: result = comicQuery(client, { id: data.id });
-
- let comic: FullComicFragment;
- let original: Readonly<FullComicFragment>;
- let updatePartial = false;
-
- $: $result, update();
- $: pending = !comicEquals(comic, original);
- $: $tabContext.tabs.edit.badge = pending;
-
- function update() {
- if (!$result.stale && $result.data?.comic.__typename === 'FullComic') {
- original = $result.data.comic;
- if (updatePartial) {
- comic.pages = structuredClone(original.pages);
- comic.favourite = original.favourite;
- comic.bookmarked = original.bookmarked;
- comic.organized = original.organized;
- comic.updatedAt = original.updatedAt;
- updatePartial = false;
- } else {
- comic = structuredClone(original);
- }
-
- $reader.pages = original.pages;
- $selection.view = comic.pages;
- $scraper.selector = undefined;
- }
+ let comic: FullComicFragment | undefined = $state();
+ let input: OmitIdentifiers<FullComicFragment> | undefined = $state();
+ let updateInput = $state(true);
+
+ function invalidateInput() {
+ updateInput = true;
}
- function toggle(field: keyof Omit<UpdateComicInput, 'cover'>) {
- updateComics(client, { ids: comic.id, input: { [field]: !comic[field] } })
- .then(() => (updatePartial = true))
- .catch(toastFinally);
+ function submit(input: UpdateComicInput) {
+ updateComics(client, { ids: data.id, input }).then(invalidateInput).catch(toastFinally);
}
- function updateComic(event: CustomEvent<UpdateComicInput>) {
- updateComics(client, { ids: comic.id, input: event.detail }).catch(toastFinally);
+ function toggle(field: keyof Pick<UpdateComicInput, 'bookmarked' | 'favourite' | 'organized'>) {
+ if (!comic) return;
+ updateComics(client, { ids: data.id, input: { [field]: !comic[field] } }).catch(toastFinally);
}
- function updateCover(event: CustomEvent<number>) {
- updateComics(client, { ids: comic.id, input: { cover: { id: event.detail } } })
- .then(() => (updatePartial = true))
- .catch(toastFinally);
+ function updateCover(id: number) {
+ updateComics(client, { ids: data.id, input: { cover: { id } } }).catch(toastFinally);
}
function removePages() {
updateComics(client, {
- ids: comic.id,
- input: { pages: { ids: $selection.ids, options: { mode: UpdateMode.Remove } } }
+ ids: data.id,
+ input: { pages: { ids: selection.ids, options: { mode: UpdateMode.Remove } } }
})
- .then(() => {
- updatePartial = true;
- $selection = $selection.clear();
- })
+ .then(selection.clear)
.catch(toastFinally);
}
beforeNavigate((navigation) => preventOnPending(navigation, pending));
+ let result = $derived(comicQuery(client, { id: data.id }));
+ let pending = $derived(comicPending(comic, input));
+
+ $effect(() => {
+ if (!$result.stale) {
+ untrack(() => {
+ if ($result.data?.comic.__typename === 'FullComic') {
+ comic = $result.data.comic;
+
+ if (updateInput) {
+ input = omitIdentifiers($result.data.comic);
+ updateInput = false;
+ }
+
+ reader.pages = comic.pages;
+ selection.view = comic.pages;
+ scraper.reset();
+ }
+ });
+ }
+ });
</script>
-<Head section="Comic" title={original?.title} />
+<Head section="Comic" title={comic?.title} />
-{#if comic}
+{#if comic && input}
<Grid>
<header>
<Titlebar
- title={original.title}
- subtitle={original.originalTitle}
- bind:favourite={comic.favourite}
- on:favourite={() => toggle('favourite')}
+ title={comic.title}
+ subtitle={comic.originalTitle}
+ favourite={comic.favourite}
+ onfavourite={() => toggle('favourite')}
/>
</header>
<aside>
- <Tabs>
- <Tab id="details">
- <ComicDetails comic={original} />
+ <Tabs badges={{ edit: pending }}>
+ <Tab initial id="details" title="Details">
+ <ComicDetails {comic} />
</Tab>
- <Tab id="edit">
+ <Tab id="edit" title="Edit">
<div class="flex flex-col gap-4">
<div class="flex gap-2 text-sm">
<SelectionControls page>
- <RemovePageButton on:click={removePages} />
+ <RemovePageButton onclick={removePages} />
</SelectionControls>
- <div class="grow" />
- <BookmarkButton bookmarked={comic.bookmarked} on:click={() => toggle('bookmarked')} />
- <OrganizedButton organized={comic.organized} on:click={() => toggle('organized')} />
+ <div class="grow"></div>
+ <BookmarkButton bookmarked={comic.bookmarked} onclick={() => toggle('bookmarked')} />
+ <OrganizedButton organized={comic.organized} onclick={() => toggle('organized')} />
</div>
- <ComicForm bind:comic on:submit={updateComic}>
+ <ComicForm bind:input {submit}>
<div class="flex gap-2">
- <div class="grow" />
- <SubmitButton active={pending} />
+ <div class="grow"></div>
+ <SubmitButton {pending} />
</div>
</ComicForm>
</div>
</Tab>
- <Tab id="scrape">
- <ComicScrapeForm {comic} />
+ <Tab id="scrape" title="Scrape">
+ <ComicScrapeForm {comic} onupsert={invalidateInput} />
</Tab>
- <Tab id="deletion">
+ <Tab id="deletion" title="Delete">
<ComicDelete {comic} />
</Tab>
</Tabs>
</aside>
<main class="overflow-auto">
- <Gallery
- pages={comic.pages}
- on:open={(e) => ($reader = $reader.open(e.detail))}
- on:cover={updateCover}
- />
+ <Gallery pages={comic.pages} open={reader.open} {updateCover} />
</main>
</Grid>
<Reader>
<PageView layout={comic.layout} direction={comic.direction} />
- <svelte:fragment slot="sidebar">
- <ComicForm bind:comic on:submit={updateComic}>
- <div class="flex justify-end gap-2">
- <SubmitButton active={pending} />
- </div>
- </ComicForm>
- </svelte:fragment>
+ {#snippet sidebar()}
+ {#if input}
+ <ComicForm bind:input {submit}>
+ <div class="flex justify-end gap-2">
+ <SubmitButton {pending} />
+ </div>
+ </ComicForm>
+ {/if}
+ {/snippet}
</Reader>
{:else}
<Guard {result} />
diff --git a/frontend/src/routes/namespaces/+page.svelte b/frontend/src/routes/namespaces/+page.svelte
index f6568f9..04f7737 100644
--- a/frontend/src/routes/namespaces/+page.svelte
+++ b/frontend/src/routes/namespaces/+page.svelte
@@ -3,10 +3,7 @@
import { fetchNamespace, namespacesQuery } from '$gql/Queries';
import type { Namespace } from '$gql/graphql';
import { NamespaceSortLabel } from '$lib/Enums';
- import { BasicFilterContext, initFilterContext } from '$lib/Filter';
- import { initPaginationContext } from '$lib/Pagination';
- import { initSelectionContext } from '$lib/Selection';
- import { initSortContext } from '$lib/Sort';
+ import { BasicFilterContext } from '$lib/Filter.svelte';
import { toastFinally } from '$lib/Toasts';
import AddButton from '$lib/components/AddButton.svelte';
import Cardlet from '$lib/components/Cardlet.svelte';
@@ -19,6 +16,7 @@
import EditNamespace from '$lib/dialogs/EditNamespace.svelte';
import Pagination from '$lib/pagination/Pagination.svelte';
import Selectable from '$lib/selection/Selectable.svelte';
+ import { initSelectionContext } from '$lib/selection/Selection.svelte';
import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte';
import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte';
import Search from '$lib/toolbar/Search.svelte';
@@ -27,38 +25,32 @@
import SelectionControls from '$lib/toolbar/SelectionControls.svelte';
import Toolbar from '$lib/toolbar/Toolbar.svelte';
import { getContextClient } from '@urql/svelte';
- import { openModal } from 'svelte-modals';
- import type { PageData } from './$types';
+ import { modals } from 'svelte-modals';
+ import type { PageProps } from './$types';
+
+ let { data }: PageProps = $props();
+ let pagination = $derived(data.pagination);
+ let sort = $derived(data.sort);
const client = getContextClient();
- export let data: PageData;
+ let result = $derived(namespacesQuery(client, { ...data }));
+ let namespaces = $derived($result.data?.namespaces);
- $: result = namespacesQuery(client, {
- pagination: data.pagination,
- filter: data.filter,
- sort: data.sort
+ let selection = initSelectionContext<Namespace>('Namespace', (n) => n.name);
+ $effect(() => {
+ if (namespaces) {
+ selection.view = namespaces.edges;
+ }
});
- $: namespaces = $result.data?.namespaces;
-
- const selection = initSelectionContext<Namespace>('Namespace', (n) => n.name);
- $: if (namespaces) {
- $selection.view = namespaces.edges;
- $pagination.total = namespaces.count;
- }
-
- const filter = initFilterContext<BasicFilterContext>();
- $: $filter = new BasicFilterContext(data.filter);
-
- const sort = initSortContext(data.sort, NamespaceSortLabel);
- $: $sort.update = data.sort;
-
- const pagination = initPaginationContext();
- $: $pagination.update = data.pagination;
+ let filter = $state(new BasicFilterContext(data.filter));
+ $effect(() => {
+ filter = new BasicFilterContext(data.filter);
+ });
const edit = (id: number) => {
fetchNamespace(client, id)
- .then((namespace) => openModal(EditNamespace, { namespace }))
+ .then((namespace) => modals.open(EditNamespace, { namespace }))
.catch(toastFinally);
};
</script>
@@ -67,34 +59,40 @@
<Column>
<Toolbar>
- <SelectionControls slot="start">
- <DeleteSelection mutation={deleteNamespaces} />
- </SelectionControls>
- <svelte:fragment slot="center">
- <Search name="Namespaces" bind:field={$filter.include.controls.name.contains} />
- <SelectSort />
- <SelectItems />
- </svelte:fragment>
- <svelte:fragment slot="end">
- <AddButton title="Add Namespace" on:click={() => openModal(AddNamespace)} />
- </svelte:fragment>
+ {#snippet start()}
+ <SelectionControls>
+ <DeleteSelection mutation={deleteNamespaces} />
+ </SelectionControls>
+ {/snippet}
+ {#snippet center()}
+ <Search name="Namespaces" {filter} bind:field={filter.include.name.contains} />
+ <SelectSort {sort} labels={NamespaceSortLabel} />
+ <SelectItems {pagination} />
+ {/snippet}
+ {#snippet end()}
+ <AddButton title="Add Namespace" onclick={() => modals.open(AddNamespace)} />
+ {/snippet}
</Toolbar>
{#if namespaces}
- <Pagination />
+ <Pagination {pagination} total={namespaces.count} />
<main>
<Cardlets>
{#each namespaces.edges as { id, name }, index (id)}
- <Selectable {index} {id} {edit} let:handle let:selected>
- <Cardlet {name} on:click={handle} filter="tags" id={`${id}:`}>
- <SelectionOverlay slot="overlay" position="right" centered {selected} />
- </Cardlet>
+ <Selectable {index} {id} {edit}>
+ {#snippet children({ onclick, selected })}
+ <Cardlet {name} {onclick} filter="tags" id={`${id}:`}>
+ {#snippet overlay()}
+ <SelectionOverlay position="right" centered {selected} />
+ {/snippet}
+ </Cardlet>
+ {/snippet}
</Selectable>
{:else}
<Empty />
{/each}
</Cardlets>
</main>
- <Pagination />
+ <Pagination {pagination} total={namespaces.count} />
{:else}
<Guard {result} />
{/if}
diff --git a/frontend/src/routes/statistics/+page.svelte b/frontend/src/routes/statistics/+page.svelte
index 7497bcf..1a5bb27 100644
--- a/frontend/src/routes/statistics/+page.svelte
+++ b/frontend/src/routes/statistics/+page.svelte
@@ -5,8 +5,8 @@
import StatGroup from '$lib/statistics/StatGroup.svelte';
import { getContextClient } from '@urql/svelte';
- $: query = statisticsQuery(getContextClient());
- $: totals = $query.data?.statistics.total;
+ let query = $derived(statisticsQuery(getContextClient()));
+ let totals = $derived($query.data?.statistics.total);
</script>
<Head section="Statistics" />
diff --git a/frontend/src/routes/tags/+page.svelte b/frontend/src/routes/tags/+page.svelte
index e0909ad..30554c7 100644
--- a/frontend/src/routes/tags/+page.svelte
+++ b/frontend/src/routes/tags/+page.svelte
@@ -3,10 +3,7 @@
import { fetchTag, tagsQuery } from '$gql/Queries';
import { type Tag } from '$gql/graphql';
import { TagSortLabel } from '$lib/Enums';
- import { TagFilterContext, initFilterContext } from '$lib/Filter';
- import { initPaginationContext } from '$lib/Pagination';
- import { initSelectionContext } from '$lib/Selection';
- import { initSortContext } from '$lib/Sort';
+ import { TagFilterContext } from '$lib/Filter.svelte';
import { toastFinally } from '$lib/Toasts';
import AddButton from '$lib/components/AddButton.svelte';
import Cardlet from '$lib/components/Cardlet.svelte';
@@ -17,10 +14,11 @@
import Column from '$lib/containers/Column.svelte';
import AddTag from '$lib/dialogs/AddTag.svelte';
import EditTag from '$lib/dialogs/EditTag.svelte';
- import UpdateTagsDialog from '$lib/dialogs/UpdateTags.svelte';
+ import UpdateTags from '$lib/dialogs/UpdateTags.svelte';
import TagFilterForm from '$lib/filter/TagFilterForm.svelte';
import Pagination from '$lib/pagination/Pagination.svelte';
import Selectable from '$lib/selection/Selectable.svelte';
+ import { initSelectionContext } from '$lib/selection/Selection.svelte';
import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte';
import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte';
import EditSelection from '$lib/toolbar/EditSelection.svelte';
@@ -31,39 +29,33 @@
import ToggleAdvancedFilters from '$lib/toolbar/ToggleAdvancedFilters.svelte';
import Toolbar from '$lib/toolbar/Toolbar.svelte';
import { getContextClient } from '@urql/svelte';
- import { openModal } from 'svelte-modals';
- import type { PageData } from './$types';
+ import { modals } from 'svelte-modals';
+ import type { PageProps } from './$types';
- const client = getContextClient();
-
- export let data: PageData;
+ let { data }: PageProps = $props();
+ let pagination = $derived(data.pagination);
+ let sort = $derived(data.sort);
- $: result = tagsQuery(client, {
- pagination: data.pagination,
- filter: data.filter,
- sort: data.sort
- });
-
- $: tags = $result.data?.tags;
+ const client = getContextClient();
+ let result = $derived(tagsQuery(client, { ...data }));
+ let tags = $derived($result.data?.tags);
const selection = initSelectionContext<Tag>('Tag', (t) => t.name);
- $: if (tags) {
- $selection.view = tags.edges;
- $pagination.total = tags.count;
- }
-
- const filter = initFilterContext<TagFilterContext>();
- $: $filter = new TagFilterContext(data.filter);
-
- const sort = initSortContext(data.sort, TagSortLabel);
- $: $sort.update = data.sort;
+ $effect(() => {
+ if (tags) {
+ selection.view = tags.edges;
+ }
+ });
- const pagination = initPaginationContext();
- $: $pagination.update = data.pagination;
+ let filter = $state(new TagFilterContext(data.filter));
+ let filterSize = $derived(filter.includes + filter.excludes);
+ $effect(() => {
+ filter = new TagFilterContext(data.filter);
+ });
const edit = (id: number) => {
fetchTag(client, id)
- .then((tag) => openModal(EditTag, { tag }))
+ .then((tag) => modals.open(EditTag, { tag }))
.catch(toastFinally);
};
</script>
@@ -72,37 +64,45 @@
<Column>
<Toolbar>
- <SelectionControls slot="start">
- <EditSelection dialog={UpdateTagsDialog} />
- <DeleteSelection mutation={deleteTags} />
- </SelectionControls>
- <svelte:fragment slot="center">
- <Search name="Tags" bind:field={$filter.include.controls.name.contains} />
- <ToggleAdvancedFilters />
- <SelectSort />
- <SelectItems />
- </svelte:fragment>
- <svelte:fragment slot="end">
- <AddButton title="Add Tag" on:click={() => openModal(AddTag)} />
- </svelte:fragment>
- <TagFilterForm />
+ {#snippet start()}
+ <SelectionControls>
+ <EditSelection dialog={UpdateTags} />
+ <DeleteSelection mutation={deleteTags} />
+ </SelectionControls>
+ {/snippet}
+ {#snippet center({ expanded, toggle })}
+ <Search name="Tags" {filter} bind:field={filter.include.name.contains} />
+ <ToggleAdvancedFilters {expanded} {toggle} {filterSize} />
+ <SelectSort {sort} labels={TagSortLabel} />
+ <SelectItems {pagination} />
+ {/snippet}
+ {#snippet end()}
+ <AddButton title="Add Tag" onclick={() => modals.open(AddTag)} />
+ {/snippet}
+ {#snippet expansion()}
+ <TagFilterForm {filter} />
+ {/snippet}
</Toolbar>
{#if tags}
- <Pagination />
+ <Pagination {pagination} total={tags.count} />
<main>
<Cardlets>
{#each tags.edges as { id, name, description }, index (id)}
- <Selectable {index} {id} {edit} let:handle let:selected>
- <Cardlet {name} title={description} on:click={handle} filter="tags" id={`:${id}`}>
- <SelectionOverlay slot="overlay" position="right" centered {selected} />
- </Cardlet>
+ <Selectable {index} {id} {edit}>
+ {#snippet children({ onclick, selected })}
+ <Cardlet {name} title={description} {onclick} filter="tags" id={`:${id}`}>
+ {#snippet overlay()}
+ <SelectionOverlay position="right" centered {selected} />
+ {/snippet}
+ </Cardlet>
+ {/snippet}
</Selectable>
{:else}
<Empty />
{/each}
</Cardlets>
</main>
- <Pagination />
+ <Pagination {pagination} total={tags.count} />
{:else}
<Guard {result} />
{/if}
diff --git a/frontend/src/routes/worlds/+page.svelte b/frontend/src/routes/worlds/+page.svelte
index e0366e9..f223a61 100644
--- a/frontend/src/routes/worlds/+page.svelte
+++ b/frontend/src/routes/worlds/+page.svelte
@@ -3,10 +3,7 @@
import { fetchWorld, worldsQuery } from '$gql/Queries';
import type { World } from '$gql/graphql';
import { WorldSortLabel } from '$lib/Enums';
- import { BasicFilterContext, initFilterContext } from '$lib/Filter';
- import { initPaginationContext } from '$lib/Pagination';
- import { initSelectionContext } from '$lib/Selection';
- import { initSortContext } from '$lib/Sort';
+ import { BasicFilterContext } from '$lib/Filter.svelte';
import { toastFinally } from '$lib/Toasts';
import AddButton from '$lib/components/AddButton.svelte';
import Cardlet from '$lib/components/Cardlet.svelte';
@@ -19,6 +16,7 @@
import EditWorld from '$lib/dialogs/EditWorld.svelte';
import Pagination from '$lib/pagination/Pagination.svelte';
import Selectable from '$lib/selection/Selectable.svelte';
+ import { initSelectionContext } from '$lib/selection/Selection.svelte';
import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte';
import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte';
import Search from '$lib/toolbar/Search.svelte';
@@ -27,75 +25,74 @@
import SelectionControls from '$lib/toolbar/SelectionControls.svelte';
import Toolbar from '$lib/toolbar/Toolbar.svelte';
import { getContextClient } from '@urql/svelte';
- import { openModal } from 'svelte-modals';
- import type { PageData } from './$types';
+ import { modals } from 'svelte-modals';
+ import type { PageProps } from './$types';
- const client = getContextClient();
+ let { data }: PageProps = $props();
+ let pagination = $derived(data.pagination);
+ let sort = $derived(data.sort);
- export let data: PageData;
+ const client = getContextClient();
+ let result = $derived(worldsQuery(client, { ...data }));
+ let worlds = $derived($result.data?.worlds);
- $: result = worldsQuery(client, {
- pagination: data.pagination,
- filter: data.filter,
- sort: data.sort
+ let selection = initSelectionContext<World>('World', (a) => a.name);
+ $effect(() => {
+ if (worlds) {
+ selection.view = worlds.edges;
+ }
});
- $: worlds = $result.data?.worlds;
-
- const selection = initSelectionContext<World>('World', (w) => w.name);
- $: if (worlds) {
- $selection.view = worlds.edges;
- $pagination.total = worlds.count;
- }
-
- const filter = initFilterContext<BasicFilterContext>();
- $: $filter = new BasicFilterContext(data.filter);
-
- const sort = initSortContext(data.sort, WorldSortLabel);
- $: $sort.update = data.sort;
-
- const pagination = initPaginationContext();
- $: $pagination.update = data.pagination;
+ let filter = $state(new BasicFilterContext(data.filter));
+ $effect(() => {
+ filter = new BasicFilterContext(data.filter);
+ });
const edit = (id: number) => {
fetchWorld(client, id)
- .then((world) => openModal(EditWorld, { world }))
+ .then((world) => modals.open(EditWorld, { world }))
.catch(toastFinally);
};
</script>
-<Head section="Worlds" />
+<Head section="worlds" />
<Column>
<Toolbar>
- <SelectionControls slot="start">
- <DeleteSelection mutation={deleteWorlds} />
- </SelectionControls>
- <svelte:fragment slot="center">
- <Search name="Worlds" bind:field={$filter.include.controls.name.contains} />
- <SelectSort />
- <SelectItems />
- </svelte:fragment>
- <svelte:fragment slot="end">
- <AddButton title="Add World" on:click={() => openModal(AddWorld)} />
- </svelte:fragment>
+ {#snippet start()}
+ <SelectionControls>
+ <DeleteSelection mutation={deleteWorlds} />
+ </SelectionControls>
+ {/snippet}
+ {#snippet center()}
+ <Search name="Worlds" {filter} bind:field={filter.include.name.contains} />
+ <SelectSort {sort} labels={WorldSortLabel} />
+ <SelectItems {pagination} />
+ {/snippet}
+ {#snippet end()}
+ <AddButton title="Add World" onclick={() => modals.open(AddWorld)} />
+ {/snippet}
</Toolbar>
{#if worlds}
- <Pagination />
+ <Pagination {pagination} total={worlds.count} />
<main>
<Cardlets>
{#each worlds.edges as { id, name }, index (id)}
- <Selectable {index} {id} {edit} let:handle let:selected>
- <Cardlet {name} on:click={handle} filter="worlds" {id}>
- <SelectionOverlay slot="overlay" position="right" centered {selected} />
- </Cardlet>
+ <Selectable {index} {id} {edit}>
+ {#snippet children({ onclick, selected })}
+ <Cardlet {name} {onclick} filter="worlds" {id}>
+ {#snippet overlay()}
+ <SelectionOverlay position="right" centered {selected} />
+ {/snippet}
+ </Cardlet>
+ {/snippet}
</Selectable>
{:else}
<Empty />
{/each}
</Cardlets>
</main>
- <Pagination />
+ <Pagination {pagination} total={worlds.count} />
{:else}
<Guard {result} />
{/if}