diff options
Diffstat (limited to '')
-rw-r--r-- | frontend/src/routes/comics/+page.svelte | 116 | ||||
-rw-r--r-- | frontend/src/routes/comics/+page.ts | 12 | ||||
-rw-r--r-- | frontend/src/routes/comics/[id]/+page.svelte | 176 | ||||
-rw-r--r-- | frontend/src/routes/comics/[id]/+page.ts | 5 |
4 files changed, 309 insertions, 0 deletions
diff --git a/frontend/src/routes/comics/+page.svelte b/frontend/src/routes/comics/+page.svelte new file mode 100644 index 0000000..353d69c --- /dev/null +++ b/frontend/src/routes/comics/+page.svelte @@ -0,0 +1,116 @@ +<script lang="ts"> + import { deleteComics, updateComics } from '$gql/Mutations'; + 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 Card, { comicCard } from '$lib/components/Card.svelte'; + import Empty from '$lib/components/Empty.svelte'; + import Guard from '$lib/components/Guard.svelte'; + import Head from '$lib/components/Head.svelte'; + import Cards from '$lib/containers/Cards.svelte'; + import Column from '$lib/containers/Column.svelte'; + import UpdateComicsDialog from '$lib/dialogs/UpdateComics.svelte'; + import ComicFilterForm from '$lib/filter/ComicFilterForm.svelte'; + import Pagination from '$lib/pagination/Pagination.svelte'; + import ComicPills from '$lib/pills/ComicPills.svelte'; + import Selectable from '$lib/selection/Selectable.svelte'; + import SelectionOverlay from '$lib/selection/SelectionOverlay.svelte'; + import DeleteSelection from '$lib/toolbar/DeleteSelection.svelte'; + import EditSelection from '$lib/toolbar/EditSelection.svelte'; + import FilterBookmarked from '$lib/toolbar/FilterBookmarked.svelte'; + import FilterFavourites from '$lib/toolbar/FilterFavourites.svelte'; + import FilterOrganized from '$lib/toolbar/FilterOrganized.svelte'; + import MarkBookmark from '$lib/toolbar/MarkBookmark.svelte'; + import MarkFavourite from '$lib/toolbar/MarkFavourite.svelte'; + import MarkOrganized from '$lib/toolbar/MarkOrganized.svelte'; + import MarkSelection from '$lib/toolbar/MarkSelection.svelte'; + import Search from '$lib/toolbar/Search.svelte'; + import SelectItems from '$lib/toolbar/SelectItems.svelte'; + import SelectSort from '$lib/toolbar/SelectSort.svelte'; + import SelectionControls from '$lib/toolbar/SelectionControls.svelte'; + import ToggleAdvancedFilters from '$lib/toolbar/ToggleAdvancedFilters.svelte'; + import Toolbar from '$lib/toolbar/Toolbar.svelte'; + import { getContextClient } from '@urql/svelte'; + import type { PageData } from './$types'; + + export let data: PageData; + + const client = getContextClient(); + + $: result = comicsQuery(client, { + pagination: data.pagination, + filter: data.filter, + sort: data.sort + }); + + $: comics = $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; + + const pagination = initPaginationContext(); + $: $pagination.update = data.pagination; +</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 /> + <div class="rounded-group flex"> + <FilterFavourites /> + <FilterBookmarked /> + <FilterOrganized /> + </div> + <SelectSort /> + <SelectItems /> + </svelte:fragment> + <ComicFilterForm /> + </Toolbar> + {#if comics} + <Pagination /> + <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> + {:else} + <Empty /> + {/each} + </Cards> + </main> + <Pagination /> + {:else} + <Guard {result} /> + {/if} +</Column> diff --git a/frontend/src/routes/comics/+page.ts b/frontend/src/routes/comics/+page.ts new file mode 100644 index 0000000..4558804 --- /dev/null +++ b/frontend/src/routes/comics/+page.ts @@ -0,0 +1,12 @@ +import { ComicSort, type ComicFilterInput } from '$gql/graphql'; +import { parseFilter, parsePaginationData, parseSortData } from '$lib/Navigation'; + +export const trailingSlash = 'always'; + +export function load({ url }: { url: URL; params: Record<string, string> }) { + return { + sort: parseSortData(url.searchParams, ComicSort.Title), + filter: parseFilter<ComicFilterInput>(url.searchParams), + pagination: parsePaginationData(url.searchParams, 24) + }; +} diff --git a/frontend/src/routes/comics/[id]/+page.svelte b/frontend/src/routes/comics/[id]/+page.svelte new file mode 100644 index 0000000..cfc5840 --- /dev/null +++ b/frontend/src/routes/comics/[id]/+page.svelte @@ -0,0 +1,176 @@ +<script lang="ts"> + 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 { toastFinally } from '$lib/Toasts'; + import { preventOnPending } from '$lib/Utils'; + import BookmarkButton from '$lib/components/BookmarkButton.svelte'; + import Guard from '$lib/components/Guard.svelte'; + import Head from '$lib/components/Head.svelte'; + import OrganizedButton from '$lib/components/OrganizedButton.svelte'; + import RemovePageButton from '$lib/components/RemovePageButton.svelte'; + import SubmitButton from '$lib/components/SubmitButton.svelte'; + import Titlebar from '$lib/components/Titlebar.svelte'; + import Grid from '$lib/containers/Grid.svelte'; + 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 ComicScrapeForm from '$lib/scraper/ComicScrapeForm.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'; + + 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' + }); + + 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; + } + } + + function toggle(field: keyof Omit<UpdateComicInput, 'cover'>) { + updateComics(client, { ids: comic.id, input: { [field]: !comic[field] } }) + .then(() => (updatePartial = true)) + .catch(toastFinally); + } + + function updateComic(event: CustomEvent<UpdateComicInput>) { + updateComics(client, { ids: comic.id, input: event.detail }).catch(toastFinally); + } + + function updateCover(event: CustomEvent<number>) { + updateComics(client, { ids: comic.id, input: { cover: { id: event.detail } } }) + .then(() => (updatePartial = true)) + .catch(toastFinally); + } + + function removePages() { + updateComics(client, { + ids: comic.id, + input: { pages: { ids: $selection.ids, options: { mode: UpdateMode.Remove } } } + }) + .then(() => { + updatePartial = true; + $selection = $selection.clear(); + }) + .catch(toastFinally); + } + + beforeNavigate((navigation) => preventOnPending(navigation, pending)); +</script> + +<Head section="Comic" title={original?.title} /> + +{#if comic} + <Grid> + <header> + <Titlebar + title={original.title} + subtitle={original.originalTitle} + bind:favourite={comic.favourite} + on:favourite={() => toggle('favourite')} + /> + </header> + + <aside> + <Tabs> + <Tab id="details"> + <ComicDetails comic={original} /> + </Tab> + <Tab id="edit"> + <div class="flex flex-col gap-4"> + <div class="flex gap-2 text-sm"> + <SelectionControls page> + <RemovePageButton on:click={removePages} /> + </SelectionControls> + <div class="grow" /> + <BookmarkButton bookmarked={comic.bookmarked} on:click={() => toggle('bookmarked')} /> + <OrganizedButton organized={comic.organized} on:click={() => toggle('organized')} /> + </div> + <ComicForm bind:comic on:submit={updateComic}> + <div class="flex gap-2"> + <div class="grow" /> + <SubmitButton active={pending} /> + </div> + </ComicForm> + </div> + </Tab> + <Tab id="scrape"> + <ComicScrapeForm {comic} /> + </Tab> + <Tab id="deletion"> + <ComicDelete {comic} /> + </Tab> + </Tabs> + </aside> + + <main class="overflow-auto"> + <Gallery + pages={comic.pages} + on:open={(e) => ($reader = $reader.open(e.detail))} + on:cover={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> + </Reader> +{:else} + <Guard {result} /> +{/if} diff --git a/frontend/src/routes/comics/[id]/+page.ts b/frontend/src/routes/comics/[id]/+page.ts new file mode 100644 index 0000000..d872ba2 --- /dev/null +++ b/frontend/src/routes/comics/[id]/+page.ts @@ -0,0 +1,5 @@ +export function load({ params }: { params: Record<string, string> }) { + return { + id: +params.id + }; +} |