[FA-27] Bookmark implementation

This commit is contained in:
gamer147
2026-01-19 00:01:16 -05:00
parent f67c5c610c
commit f8a45ad891
28 changed files with 1036 additions and 20 deletions

View File

@@ -1,29 +1,131 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Popover, PopoverTrigger, PopoverContent } from '$lib/components/ui/popover';
import { Textarea } from '$lib/components/ui/textarea';
import { client } from '$lib/graphql/client';
import { UpsertBookmarkDocument, RemoveBookmarkDocument } from '$lib/graphql/__generated__/graphql';
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import List from '@lucide/svelte/icons/list';
import Bookmark from '@lucide/svelte/icons/bookmark';
import BookmarkCheck from '@lucide/svelte/icons/bookmark-check';
interface Props {
novelId: string;
chapterId?: number;
prevChapterVolumeOrder: number | null | undefined;
prevChapterOrder: number | null | undefined;
nextChapterVolumeOrder: number | null | undefined;
nextChapterOrder: number | null | undefined;
showKeyboardHints?: boolean;
isBookmarked?: boolean;
bookmarkDescription?: string | null;
onBookmarkChange?: (isBookmarked: boolean, description?: string | null) => void;
}
let {
novelId,
chapterId,
prevChapterVolumeOrder,
prevChapterOrder,
nextChapterVolumeOrder,
nextChapterOrder,
showKeyboardHints = true
showKeyboardHints = true,
isBookmarked = false,
bookmarkDescription = null,
onBookmarkChange
}: Props = $props();
const hasPrev = $derived(prevChapterOrder != null && prevChapterVolumeOrder != null);
const hasNext = $derived(nextChapterOrder != null && nextChapterVolumeOrder != null);
// Bookmark state
let popoverOpen = $state(false);
let description = $state(bookmarkDescription ?? '');
let saving = $state(false);
let removing = $state(false);
let error: string | null = $state(null);
// Reset description when popover opens
$effect(() => {
if (popoverOpen) {
description = bookmarkDescription ?? '';
error = null;
}
});
async function saveBookmark() {
if (!chapterId) return;
saving = true;
error = null;
try {
const result = await client
.mutation(UpsertBookmarkDocument, {
input: {
chapterId,
novelId: parseInt(novelId, 10),
description: description.trim() || null
}
})
.toPromise();
if (result.error) {
error = result.error.message;
return;
}
if (result.data?.upsertBookmark?.errors?.length) {
error = result.data.upsertBookmark.errors[0]?.message ?? 'Failed to save bookmark';
return;
}
if (result.data?.upsertBookmark?.bookmarkPayload?.success) {
popoverOpen = false;
onBookmarkChange?.(true, description.trim() || null);
}
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to save bookmark';
} finally {
saving = false;
}
}
async function removeBookmark() {
if (!chapterId) return;
removing = true;
error = null;
try {
const result = await client
.mutation(RemoveBookmarkDocument, {
input: { chapterId }
})
.toPromise();
if (result.error) {
error = result.error.message;
return;
}
if (result.data?.removeBookmark?.errors?.length) {
error = result.data.removeBookmark.errors[0]?.message ?? 'Failed to remove bookmark';
return;
}
if (result.data?.removeBookmark?.bookmarkPayload?.success) {
popoverOpen = false;
description = '';
onBookmarkChange?.(false, null);
}
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to remove bookmark';
} finally {
removing = false;
}
}
</script>
<div class="flex flex-col gap-2">
@@ -38,10 +140,72 @@
<span class="hidden sm:inline">Previous</span>
</Button>
<Button variant="outline" href="/novels/{novelId}" class="gap-2">
<List class="h-4 w-4" />
<span class="hidden sm:inline">Contents</span>
</Button>
<div class="flex items-center gap-2">
<Button variant="outline" href="/novels/{novelId}" class="gap-2">
<List class="h-4 w-4" />
<span class="hidden sm:inline">Contents</span>
</Button>
{#if chapterId}
<Popover bind:open={popoverOpen}>
<PopoverTrigger asChild>
{#snippet child({ props })}
<Button
variant={isBookmarked ? 'default' : 'outline'}
class="gap-2"
{...props}
>
{#if isBookmarked}
<BookmarkCheck class="h-4 w-4" />
{:else}
<Bookmark class="h-4 w-4" />
{/if}
<span class="hidden sm:inline">{isBookmarked ? 'Bookmarked' : 'Bookmark'}</span>
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="w-80">
<div class="space-y-4">
<div class="space-y-2">
<h4 class="font-medium leading-none">
{isBookmarked ? 'Edit bookmark' : 'Bookmark this chapter'}
</h4>
<p class="text-sm text-muted-foreground">
{isBookmarked ? 'Update your note or remove the bookmark.' : 'Add an optional note to remember why you bookmarked this.'}
</p>
</div>
<Textarea
bind:value={description}
placeholder="Add a note..."
class="min-h-[80px] resize-none"
/>
{#if error}
<p class="text-sm text-destructive">{error}</p>
{/if}
<div class="flex justify-end gap-2">
{#if isBookmarked}
<Button
variant="destructive"
size="sm"
onclick={removeBookmark}
disabled={removing || saving}
>
{removing ? 'Removing...' : 'Remove'}
</Button>
{/if}
<Button
size="sm"
onclick={saveBookmark}
disabled={saving || removing}
>
{saving ? 'Saving...' : 'Save'}
</Button>
</div>
</div>
</PopoverContent>
</Popover>
{/if}
</div>
<Button
variant="outline"

View File

@@ -1,13 +1,14 @@
<script lang="ts" module>
import type { GetChapterQuery } from '$lib/graphql/__generated__/graphql';
import type { GetChapterQuery, GetBookmarksQuery } from '$lib/graphql/__generated__/graphql';
export type ChapterData = NonNullable<GetChapterQuery['chapter']>;
export type BookmarkData = GetBookmarksQuery['bookmarks'][number];
</script>
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { client } from '$lib/graphql/client';
import { GetChapterDocument } from '$lib/graphql/__generated__/graphql';
import { GetChapterDocument, GetBookmarksDocument } from '$lib/graphql/__generated__/graphql';
import { Card, CardContent } from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import ChapterNavigation from './ChapterNavigation.svelte';
@@ -28,6 +29,10 @@
let error: string | null = $state(null);
let scrollProgress = $state(0);
// Bookmark state
let isBookmarked = $state(false);
let bookmarkDescription: string | null = $state(null);
// Derived values
const sanitizedBody = $derived(chapter?.body ? sanitizeChapterHtml(chapter.body) : '');
@@ -78,6 +83,8 @@
chapter = result.data.chapter;
// Update the page title with chapter info
document.title = `${chapter.novelName} - ${chapter.order}`;
// Fetch bookmark status
await fetchBookmarks();
} else {
error = 'Chapter not found';
}
@@ -88,6 +95,34 @@
}
}
async function fetchBookmarks() {
if (!novelId || !chapter) return;
try {
const result = await client
.query(GetBookmarksDocument, { novelId: parseInt(novelId, 10) })
.toPromise();
if (result.data?.bookmarks) {
const bookmark = result.data.bookmarks.find((b) => b.chapterId === chapter!.id);
if (bookmark) {
isBookmarked = true;
bookmarkDescription = bookmark.description ?? null;
} else {
isBookmarked = false;
bookmarkDescription = null;
}
}
} catch {
// Silently fail - bookmark status is non-critical
}
}
function handleBookmarkChange(newIsBookmarked: boolean, newDescription?: string | null) {
isBookmarked = newIsBookmarked;
bookmarkDescription = newDescription ?? null;
}
onMount(() => {
fetchChapter();
window.addEventListener('scroll', handleScroll, { passive: true });
@@ -139,10 +174,14 @@
<!-- Navigation (top) -->
<ChapterNavigation
novelId={novelId ?? ''}
chapterId={chapter.id}
prevChapterVolumeOrder={chapter.prevChapterVolumeOrder}
prevChapterOrder={chapter.prevChapterOrder}
nextChapterVolumeOrder={chapter.nextChapterVolumeOrder}
nextChapterOrder={chapter.nextChapterOrder}
{isBookmarked}
{bookmarkDescription}
onBookmarkChange={handleBookmarkChange}
/>
<!-- Chapter Header -->
@@ -173,11 +212,15 @@
<!-- Navigation (bottom) -->
<ChapterNavigation
novelId={novelId ?? ''}
chapterId={chapter.id}
prevChapterVolumeOrder={chapter.prevChapterVolumeOrder}
prevChapterOrder={chapter.prevChapterOrder}
nextChapterVolumeOrder={chapter.nextChapterVolumeOrder}
nextChapterOrder={chapter.nextChapterOrder}
showKeyboardHints={false}
{isBookmarked}
{bookmarkDescription}
onBookmarkChange={handleBookmarkChange}
/>
{/if}
</div>

View File

@@ -1,8 +1,10 @@
<script lang="ts" module>
import type { NovelQuery, NovelStatus, Language } from '$lib/graphql/__generated__/graphql';
import type { NovelQuery, NovelStatus, Language, GetBookmarksQuery } from '$lib/graphql/__generated__/graphql';
import { TagType } from '$lib/graphql/__generated__/graphql';
import { SystemTags } from '$lib/constants/systemTags';
export type BookmarkData = GetBookmarksQuery['bookmarks'][number];
export type NovelNode = NonNullable<NonNullable<NovelQuery['novels']>['nodes']>[number];
const statusColors: Record<NovelStatus, string> = {
@@ -32,7 +34,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { client } from '$lib/graphql/client';
import { NovelDocument, ImportNovelDocument, DeleteNovelDocument } from '$lib/graphql/__generated__/graphql';
import { NovelDocument, ImportNovelDocument, DeleteNovelDocument, GetBookmarksDocument } from '$lib/graphql/__generated__/graphql';
import { isAuthenticated } from '$lib/auth/authStore';
import { Card, CardContent, CardHeader } from '$lib/components/ui/card';
import { Badge } from '$lib/components/ui/badge';
@@ -98,6 +100,11 @@
let activeTab = $state('chapters');
let galleryLoaded = $state(false);
// Bookmarks state
let bookmarks: BookmarkData[] = $state([]);
let bookmarksLoaded = $state(false);
let bookmarksFetching = $state(false);
const DESCRIPTION_PREVIEW_LENGTH = 300;
// Derived values
@@ -128,6 +135,15 @@
const isSingleVolume = $derived(sortedVolumes.length === 1);
// Chapter lookup for bookmarks (maps chapterId to chapter details)
const chapterLookup = $derived(
new Map(
sortedVolumes.flatMap((v) =>
v.chapters.map((c) => [c.id, { ...c, volumeOrder: v.order }])
)
)
);
const chapterCount = $derived(
sortedVolumes.reduce((sum, v) => sum + v.chapters.length, 0)
);
@@ -184,6 +200,34 @@
}
});
// Load bookmarks when tab is first activated
$effect(() => {
if (activeTab === 'bookmarks' && !bookmarksLoaded && novelId) {
fetchBookmarks();
}
});
async function fetchBookmarks() {
if (!novelId || bookmarksFetching) return;
bookmarksFetching = true;
try {
const result = await client
.query(GetBookmarksDocument, { novelId: parseInt(novelId, 10) })
.toPromise();
if (result.data?.bookmarks) {
bookmarks = result.data.bookmarks;
}
} catch {
// Silently fail - bookmarks are non-critical
} finally {
bookmarksFetching = false;
bookmarksLoaded = true;
}
}
// Image viewer functions
function openImageViewer(index: number) {
viewerIndex = index;
@@ -528,10 +572,9 @@
</TabsTrigger>
<TabsTrigger
value="bookmarks"
disabled
class="rounded-md data-[state=active]:bg-background data-[state=active]:shadow-sm px-3 py-1.5 text-sm font-medium transition-all disabled:opacity-50 disabled:cursor-not-allowed"
class="rounded-md data-[state=active]:bg-background data-[state=active]:shadow-sm px-3 py-1.5 text-sm font-medium transition-all"
>
Bookmarks
Bookmarks{bookmarksLoaded ? ` (${bookmarks.length})` : ''}
</TabsTrigger>
</TabsList>
</CardHeader>
@@ -646,9 +689,50 @@
</TabsContent>
<TabsContent value="bookmarks" class="mt-0">
<p class="text-muted-foreground text-sm py-8 text-center">
Bookmarks coming soon.
</p>
{#if bookmarksFetching}
<div class="flex items-center justify-center py-8">
<div
class="border-primary h-8 w-8 animate-spin rounded-full border-2 border-t-transparent"
aria-label="Loading bookmarks"
></div>
</div>
{:else if bookmarks.length === 0}
<p class="text-muted-foreground text-sm py-8 text-center">
No bookmarks yet. Add bookmarks while reading chapters.
</p>
{:else}
<div class="max-h-96 overflow-y-auto -mx-2">
{#each bookmarks as bookmark (bookmark.id)}
{@const chapter = chapterLookup.get(bookmark.chapterId)}
{#if chapter}
{@const bookmarkDate = new Date(bookmark.createdTime)}
<a
href="/novels/{novelId}/volumes/{chapter.volumeOrder}/chapters/{chapter.order}"
class="flex items-center justify-between px-3 py-2.5 hover:bg-muted/50 rounded-md transition-colors group"
>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3">
<span class="text-muted-foreground text-sm font-medium shrink-0 w-14">
Ch. {chapter.order}
</span>
<span class="text-sm truncate group-hover:text-primary transition-colors">
{chapter.name}
</span>
</div>
{#if bookmark.description}
<p class="text-xs text-muted-foreground/70 mt-1 ml-[4.25rem] truncate">
{bookmark.description}
</p>
{/if}
</div>
<span class="text-xs text-muted-foreground/70 shrink-0 ml-2">
{formatRelativeTime(bookmarkDate)}
</span>
</a>
{/if}
{/each}
</div>
{/if}
</TabsContent>
</CardContent>
</Tabs>

View File

@@ -0,0 +1,18 @@
import { Popover as PopoverPrimitive } from 'bits-ui';
import Content from './popover-content.svelte';
const Root = PopoverPrimitive.Root;
const Trigger = PopoverPrimitive.Trigger;
const Close = PopoverPrimitive.Close;
export {
Root,
Trigger,
Content,
Close,
//
Root as Popover,
Trigger as PopoverTrigger,
Content as PopoverContent,
Close as PopoverClose
};

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import { Popover as PopoverPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
sideOffset = 4,
align = 'center',
class: className,
...restProps
}: PopoverPrimitive.ContentProps = $props();
</script>
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
bind:ref
data-slot="popover-content"
{sideOffset}
{align}
class={cn(
'bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 overflow-hidden rounded-md border p-4 shadow-md outline-none',
className
)}
{...restProps}
/>
</PopoverPrimitive.Portal>

View File

@@ -0,0 +1,7 @@
import Root from './textarea.svelte';
export {
Root,
//
Root as Textarea
};

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import type { HTMLTextareaAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '$lib/utils.js';
type Props = WithElementRef<HTMLTextareaAttributes, HTMLTextAreaElement>;
let {
ref = $bindable(null),
value = $bindable(),
class: className,
'data-slot': dataSlot = 'textarea',
...restProps
}: Props = $props();
</script>
<textarea
bind:this={ref}
data-slot={dataSlot}
class={cn(
'border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex min-h-[80px] w-full min-w-0 rounded-md border px-3 py-2 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className
)}
bind:value
{...restProps}
></textarea>

View File

@@ -29,6 +29,19 @@ export const ApplyPolicy = {
} as const;
export type ApplyPolicy = typeof ApplyPolicy[keyof typeof ApplyPolicy];
export type BookmarkDto = {
chapterId: Scalars['UnsignedInt']['output'];
createdTime: Scalars['Instant']['output'];
description: Maybe<Scalars['String']['output']>;
id: Scalars['Int']['output'];
novelId: Scalars['UnsignedInt']['output'];
};
export type BookmarkPayload = {
bookmark: Maybe<BookmarkDto>;
success: Scalars['Boolean']['output'];
};
export type ChapterDto = {
body: Scalars['String']['output'];
createdTime: Scalars['Instant']['output'];
@@ -266,9 +279,11 @@ export type Mutation = {
fetchChapterContents: FetchChapterContentsPayload;
importNovel: ImportNovelPayload;
inviteUser: InviteUserPayload;
removeBookmark: RemoveBookmarkPayload;
runJob: RunJobPayload;
scheduleEventJob: ScheduleEventJobPayload;
translateText: TranslateTextPayload;
upsertBookmark: UpsertBookmarkPayload;
};
@@ -297,6 +312,11 @@ export type MutationInviteUserArgs = {
};
export type MutationRemoveBookmarkArgs = {
input: RemoveBookmarkInput;
};
export type MutationRunJobArgs = {
input: RunJobInput;
};
@@ -311,6 +331,11 @@ export type MutationTranslateTextArgs = {
input: TranslateTextInput;
};
export type MutationUpsertBookmarkArgs = {
input: UpsertBookmarkInput;
};
export type NovelDto = {
author: PersonDto;
coverImage: Maybe<ImageDto>;
@@ -471,6 +496,7 @@ export type PersonDtoSortInput = {
};
export type Query = {
bookmarks: Array<BookmarkDto>;
chapter: Maybe<ChapterReaderDto>;
currentUser: Maybe<UserDto>;
jobs: Array<SchedulerJob>;
@@ -480,6 +506,11 @@ export type Query = {
};
export type QueryBookmarksArgs = {
novelId: Scalars['UnsignedInt']['input'];
};
export type QueryChapterArgs = {
chapterOrder: Scalars['UnsignedInt']['input'];
novelId: Scalars['UnsignedInt']['input'];
@@ -514,6 +545,17 @@ export type QueryTranslationRequestsArgs = {
where?: InputMaybe<TranslationRequestDtoFilterInput>;
};
export type RemoveBookmarkError = InvalidOperationError;
export type RemoveBookmarkInput = {
chapterId: Scalars['UnsignedInt']['input'];
};
export type RemoveBookmarkPayload = {
bookmarkPayload: Maybe<BookmarkPayload>;
errors: Maybe<Array<RemoveBookmarkError>>;
};
export type RunJobError = JobPersistenceError;
export type RunJobInput = {
@@ -739,6 +781,19 @@ export type UnsignedIntOperationFilterInputType = {
nlte?: InputMaybe<Scalars['UnsignedInt']['input']>;
};
export type UpsertBookmarkError = InvalidOperationError;
export type UpsertBookmarkInput = {
chapterId: Scalars['UnsignedInt']['input'];
description?: InputMaybe<Scalars['String']['input']>;
novelId: Scalars['UnsignedInt']['input'];
};
export type UpsertBookmarkPayload = {
bookmarkPayload: Maybe<BookmarkPayload>;
errors: Maybe<Array<UpsertBookmarkError>>;
};
export type UserDto = {
availableInvites: Scalars['Int']['output'];
createdTime: Scalars['Instant']['output'];
@@ -807,6 +862,27 @@ export type InviteUserMutationVariables = Exact<{
export type InviteUserMutation = { inviteUser: { userDto: { id: any, username: string, email: string } | null, errors: Array<{ message: string }> | null } };
export type RemoveBookmarkMutationVariables = Exact<{
input: RemoveBookmarkInput;
}>;
export type RemoveBookmarkMutation = { removeBookmark: { bookmarkPayload: { success: boolean } | null, errors: Array<{ message: string }> | null } };
export type UpsertBookmarkMutationVariables = Exact<{
input: UpsertBookmarkInput;
}>;
export type UpsertBookmarkMutation = { upsertBookmark: { bookmarkPayload: { success: boolean, bookmark: { id: number, chapterId: any, novelId: any, description: string | null, createdTime: any } | null } | null, errors: Array<{ message: string }> | null } };
export type GetBookmarksQueryVariables = Exact<{
novelId: Scalars['UnsignedInt']['input'];
}>;
export type GetBookmarksQuery = { bookmarks: Array<{ id: number, chapterId: any, novelId: any, description: string | null, createdTime: any }> };
export type GetChapterQueryVariables = Exact<{
novelId: Scalars['UnsignedInt']['input'];
volumeOrder: Scalars['UnsignedInt']['input'];
@@ -842,6 +918,9 @@ export type GetSettingsPageDataQuery = { currentUser: { id: any, username: strin
export const DeleteNovelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteNovel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteNovelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteNovel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"boolean"}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<DeleteNovelMutation, DeleteNovelMutationVariables>;
export const ImportNovelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ImportNovel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ImportNovelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"importNovel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novelUpdateRequestedEvent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novelUrl"}}]}}]}}]}}]} as unknown as DocumentNode<ImportNovelMutation, ImportNovelMutationVariables>;
export const InviteUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InviteUser"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"InviteUserInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inviteUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userDto"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InvalidOperationError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<InviteUserMutation, InviteUserMutationVariables>;
export const RemoveBookmarkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveBookmark"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoveBookmarkInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"removeBookmark"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"bookmarkPayload"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<RemoveBookmarkMutation, RemoveBookmarkMutationVariables>;
export const UpsertBookmarkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpsertBookmark"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpsertBookmarkInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"upsertBookmark"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"bookmarkPayload"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"bookmark"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"chapterId"}},{"kind":"Field","name":{"kind":"Name","value":"novelId"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<UpsertBookmarkMutation, UpsertBookmarkMutationVariables>;
export const GetBookmarksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetBookmarks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"bookmarks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"novelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"chapterId"}},{"kind":"Field","name":{"kind":"Name","value":"novelId"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}}]}}]}}]} as unknown as DocumentNode<GetBookmarksQuery, GetBookmarksQueryVariables>;
export const GetChapterDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetChapter"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"volumeOrder"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"chapterOrder"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"novelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"novelId"}}},{"kind":"Argument","name":{"kind":"Name","value":"volumeOrder"},"value":{"kind":"Variable","name":{"kind":"Name","value":"volumeOrder"}}},{"kind":"Argument","name":{"kind":"Name","value":"chapterOrder"},"value":{"kind":"Variable","name":{"kind":"Name","value":"chapterOrder"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"revision"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"images"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"novelId"}},{"kind":"Field","name":{"kind":"Name","value":"novelName"}},{"kind":"Field","name":{"kind":"Name","value":"volumeId"}},{"kind":"Field","name":{"kind":"Name","value":"volumeName"}},{"kind":"Field","name":{"kind":"Name","value":"volumeOrder"}},{"kind":"Field","name":{"kind":"Name","value":"totalChaptersInVolume"}},{"kind":"Field","name":{"kind":"Name","value":"prevChapterVolumeOrder"}},{"kind":"Field","name":{"kind":"Name","value":"prevChapterOrder"}},{"kind":"Field","name":{"kind":"Name","value":"nextChapterVolumeOrder"}},{"kind":"Field","name":{"kind":"Name","value":"nextChapterOrder"}}]}}]}}]} as unknown as DocumentNode<GetChapterQuery, GetChapterQueryVariables>;
export const NovelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Novel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UnsignedInt"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}},{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"rawLanguage"}},{"kind":"Field","name":{"kind":"Name","value":"rawStatus"}},{"kind":"Field","name":{"kind":"Name","value":"statusOverride"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"externalUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"source"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"coverImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"tagType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"volumes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdTime"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"chapters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"images"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<NovelQuery, NovelQueryVariables>;
export const NovelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Novels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"NovelDtoFilterInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"order"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NovelDtoSortInput"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"novels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}},{"kind":"Argument","name":{"kind":"Name","value":"order"},"value":{"kind":"Variable","name":{"kind":"Name","value":"order"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"coverImage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"newPath"}}]}},{"kind":"Field","name":{"kind":"Name","value":"rawStatus"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"volumes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"chapters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"tagType"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode<NovelsQuery, NovelsQueryVariables>;

View File

@@ -0,0 +1,12 @@
mutation RemoveBookmark($input: RemoveBookmarkInput!) {
removeBookmark(input: $input) {
bookmarkPayload {
success
}
errors {
... on Error {
message
}
}
}
}

View File

@@ -0,0 +1,19 @@
mutation UpsertBookmark($input: UpsertBookmarkInput!) {
upsertBookmark(input: $input) {
bookmarkPayload {
success
bookmark {
id
chapterId
novelId
description
createdTime
}
}
errors {
... on Error {
message
}
}
}
}

View File

@@ -0,0 +1,9 @@
query GetBookmarks($novelId: UnsignedInt!) {
bookmarks(novelId: $novelId) {
id
chapterId
novelId
description
createdTime
}
}