diff options
Diffstat (limited to 'frontend/src/routes/comics')
-rw-r--r-- | frontend/src/routes/comics/+page.svelte | 107 | ||||
-rw-r--r-- | frontend/src/routes/comics/[id]/+page.svelte | 177 |
2 files changed, 135 insertions, 149 deletions
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} /> |