[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

@@ -7,6 +7,13 @@
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="..\.dockerignore"> <Content Include="..\.dockerignore">
<Link>.dockerignore</Link> <Link>.dockerignore</Link>

View File

@@ -1,6 +1,109 @@
using System.Security.Claims;
using FictionArchive.Service.UserNovelDataService.Models.Database;
using FictionArchive.Service.UserNovelDataService.Models.DTOs;
using FictionArchive.Service.UserNovelDataService.Services;
using HotChocolate.Authorization;
using HotChocolate.Types;
using Microsoft.EntityFrameworkCore;
namespace FictionArchive.Service.UserNovelDataService.GraphQL; namespace FictionArchive.Service.UserNovelDataService.GraphQL;
public class Mutation public class Mutation
{ {
[Authorize]
} [Error<InvalidOperationException>]
public async Task<BookmarkPayload> UpsertBookmark(
UserNovelDataServiceDbContext dbContext,
ClaimsPrincipal claimsPrincipal,
UpsertBookmarkInput input)
{
var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(oAuthProviderId))
{
throw new InvalidOperationException("Unable to determine current user identity");
}
var user = await dbContext.Users
.FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId);
if (user == null)
{
// Auto-create user if not exists
user = new User { OAuthProviderId = oAuthProviderId };
dbContext.Users.Add(user);
await dbContext.SaveChangesAsync();
}
var existingBookmark = await dbContext.Bookmarks
.FirstOrDefaultAsync(b => b.UserId == user.Id && b.ChapterId == input.ChapterId);
if (existingBookmark != null)
{
// Update existing
existingBookmark.Description = input.Description;
}
else
{
// Create new
existingBookmark = new Bookmark
{
UserId = user.Id,
NovelId = input.NovelId,
ChapterId = input.ChapterId,
Description = input.Description
};
dbContext.Bookmarks.Add(existingBookmark);
}
await dbContext.SaveChangesAsync();
return new BookmarkPayload
{
Success = true,
Bookmark = new BookmarkDto
{
Id = existingBookmark.Id,
ChapterId = existingBookmark.ChapterId,
NovelId = existingBookmark.NovelId,
Description = existingBookmark.Description,
CreatedTime = existingBookmark.CreatedTime
}
};
}
[Authorize]
[Error<InvalidOperationException>]
public async Task<BookmarkPayload> RemoveBookmark(
UserNovelDataServiceDbContext dbContext,
ClaimsPrincipal claimsPrincipal,
uint chapterId)
{
var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(oAuthProviderId))
{
throw new InvalidOperationException("Unable to determine current user identity");
}
var user = await dbContext.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId);
if (user == null)
{
return new BookmarkPayload { Success = false };
}
var bookmark = await dbContext.Bookmarks
.FirstOrDefaultAsync(b => b.UserId == user.Id && b.ChapterId == chapterId);
if (bookmark == null)
{
return new BookmarkPayload { Success = false };
}
dbContext.Bookmarks.Remove(bookmark);
await dbContext.SaveChangesAsync();
return new BookmarkPayload { Success = true };
}
}

View File

@@ -1,6 +1,45 @@
using System.Security.Claims;
using FictionArchive.Service.UserNovelDataService.Models.DTOs;
using FictionArchive.Service.UserNovelDataService.Services;
using HotChocolate.Authorization;
using Microsoft.EntityFrameworkCore;
namespace FictionArchive.Service.UserNovelDataService.GraphQL; namespace FictionArchive.Service.UserNovelDataService.GraphQL;
public class Query public class Query
{ {
[Authorize]
} public async Task<IQueryable<BookmarkDto>> GetBookmarks(
UserNovelDataServiceDbContext dbContext,
ClaimsPrincipal claimsPrincipal,
uint novelId)
{
var oAuthProviderId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(oAuthProviderId))
{
return new List<BookmarkDto>().AsQueryable();
}
var user = await dbContext.Users
.AsNoTracking()
.FirstOrDefaultAsync(u => u.OAuthProviderId == oAuthProviderId);
if (user == null)
{
return new List<BookmarkDto>().AsQueryable();
}
return dbContext.Bookmarks
.AsNoTracking()
.Where(b => b.UserId == user.Id && b.NovelId == novelId)
.OrderByDescending(b => b.CreatedTime)
.Select(b => new BookmarkDto
{
Id = b.Id,
ChapterId = b.ChapterId,
NovelId = b.NovelId,
Description = b.Description,
CreatedTime = b.CreatedTime
});
}
}

View File

@@ -0,0 +1,99 @@
// <auto-generated />
using System;
using FictionArchive.Service.UserNovelDataService.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NodaTime;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace FictionArchive.Service.UserNovelDataService.Migrations
{
[DbContext(typeof(UserNovelDataServiceDbContext))]
[Migration("20251230181559_AddBookmarks")]
partial class AddBookmarks
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.Bookmark", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<long>("ChapterId")
.HasColumnType("bigint");
b.Property<Instant>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<Instant>("LastUpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<long>("NovelId")
.HasColumnType("bigint");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId", "ChapterId")
.IsUnique();
b.HasIndex("UserId", "NovelId");
b.ToTable("Bookmarks");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Instant>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<Instant>("LastUpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("OAuthProviderId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.Bookmark", b =>
{
b.HasOne("FictionArchive.Service.UserNovelDataService.Models.Database.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,76 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using NodaTime;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace FictionArchive.Service.UserNovelDataService.Migrations
{
/// <inheritdoc />
public partial class AddBookmarks : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
OAuthProviderId = table.Column<string>(type: "text", nullable: false),
CreatedTime = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
LastUpdatedTime = table.Column<Instant>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Bookmarks",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
UserId = table.Column<Guid>(type: "uuid", nullable: false),
ChapterId = table.Column<long>(type: "bigint", nullable: false),
NovelId = table.Column<long>(type: "bigint", nullable: false),
Description = table.Column<string>(type: "text", nullable: true),
CreatedTime = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
LastUpdatedTime = table.Column<Instant>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Bookmarks", x => x.Id);
table.ForeignKey(
name: "FK_Bookmarks_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Bookmarks_UserId_ChapterId",
table: "Bookmarks",
columns: new[] { "UserId", "ChapterId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Bookmarks_UserId_NovelId",
table: "Bookmarks",
columns: new[] { "UserId", "NovelId" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Bookmarks");
migrationBuilder.DropTable(
name: "Users");
}
}
}

View File

@@ -0,0 +1,96 @@
// <auto-generated />
using System;
using FictionArchive.Service.UserNovelDataService.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NodaTime;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace FictionArchive.Service.UserNovelDataService.Migrations
{
[DbContext(typeof(UserNovelDataServiceDbContext))]
partial class UserNovelDataServiceDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.Bookmark", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<long>("ChapterId")
.HasColumnType("bigint");
b.Property<Instant>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<Instant>("LastUpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<long>("NovelId")
.HasColumnType("bigint");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId", "ChapterId")
.IsUnique();
b.HasIndex("UserId", "NovelId");
b.ToTable("Bookmarks");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Instant>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<Instant>("LastUpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("OAuthProviderId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("FictionArchive.Service.UserNovelDataService.Models.Database.Bookmark", b =>
{
b.HasOne("FictionArchive.Service.UserNovelDataService.Models.Database.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,12 @@
using NodaTime;
namespace FictionArchive.Service.UserNovelDataService.Models.DTOs;
public class BookmarkDto
{
public int Id { get; init; }
public uint ChapterId { get; init; }
public uint NovelId { get; init; }
public string? Description { get; init; }
public Instant CreatedTime { get; init; }
}

View File

@@ -0,0 +1,7 @@
namespace FictionArchive.Service.UserNovelDataService.Models.DTOs;
public class BookmarkPayload
{
public BookmarkDto? Bookmark { get; init; }
public bool Success { get; init; }
}

View File

@@ -0,0 +1,3 @@
namespace FictionArchive.Service.UserNovelDataService.Models.DTOs;
public record UpsertBookmarkInput(uint NovelId, uint ChapterId, string? Description);

View File

@@ -0,0 +1,14 @@
using FictionArchive.Service.Shared.Models;
namespace FictionArchive.Service.UserNovelDataService.Models.Database;
public class Bookmark : BaseEntity<int>
{
public Guid UserId { get; set; }
public virtual User User { get; set; } = null!;
public uint ChapterId { get; set; }
public uint NovelId { get; set; }
public string? Description { get; set; }
}

View File

@@ -0,0 +1,8 @@
using FictionArchive.Service.Shared.Models;
namespace FictionArchive.Service.UserNovelDataService.Models.Database;
public class Chapter : BaseEntity<uint>
{
}

View File

@@ -0,0 +1,8 @@
using FictionArchive.Service.Shared.Models;
namespace FictionArchive.Service.UserNovelDataService.Models.Database;
public class Novel : BaseEntity<uint>
{
public virtual ICollection<Volume> Volumes { get; set; }
}

View File

@@ -0,0 +1,8 @@
using FictionArchive.Service.Shared.Models;
namespace FictionArchive.Service.UserNovelDataService.Models.Database;
public class User : BaseEntity<Guid>
{
public required string OAuthProviderId { get; set; }
}

View File

@@ -0,0 +1,8 @@
using FictionArchive.Service.Shared.Models;
namespace FictionArchive.Service.UserNovelDataService.Models.Database;
public class Volume : BaseEntity<uint>
{
public virtual ICollection<Chapter> Chapters { get; set; }
}

View File

@@ -1,11 +1,35 @@
using FictionArchive.Service.Shared.Services.Database; using FictionArchive.Service.Shared.Services.Database;
using FictionArchive.Service.UserNovelDataService.Models.Database;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace FictionArchive.Service.UserNovelDataService.Services; namespace FictionArchive.Service.UserNovelDataService.Services;
public class UserNovelDataServiceDbContext : FictionArchiveDbContext public class UserNovelDataServiceDbContext : FictionArchiveDbContext
{ {
public DbSet<User> Users { get; set; }
public DbSet<Bookmark> Bookmarks { get; set; }
public UserNovelDataServiceDbContext(DbContextOptions options, ILogger<UserNovelDataServiceDbContext> logger) : base(options, logger) public UserNovelDataServiceDbContext(DbContextOptions options, ILogger<UserNovelDataServiceDbContext> logger) : base(options, logger)
{ {
} }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Bookmark>(entity =>
{
// Unique constraint: one bookmark per chapter per user
entity.HasIndex(b => new { b.UserId, b.ChapterId }).IsUnique();
// Index for efficient "get bookmarks for novel" queries
entity.HasIndex(b => new { b.UserId, b.NovelId });
// User relationship
entity.HasOne(b => b.User)
.WithMany()
.HasForeignKey(b => b.UserId)
.OnDelete(DeleteBehavior.Cascade);
});
}
}

View File

@@ -12,5 +12,15 @@
"ConnectionString": "amqp://localhost", "ConnectionString": "amqp://localhost",
"ClientIdentifier": "UserNovelDataService" "ClientIdentifier": "UserNovelDataService"
}, },
"OIDC": {
"Authority": "https://auth.orfl.xyz/application/o/fiction-archive/",
"ClientId": "ldi5IpEidq2WW0Ka1lehVskb2SOBjnYRaZCpEyBh",
"Audience": "ldi5IpEidq2WW0Ka1lehVskb2SOBjnYRaZCpEyBh",
"ValidIssuer": "https://auth.orfl.xyz/application/o/fiction-archive/",
"ValidateIssuer": true,
"ValidateAudience": true,
"ValidateLifetime": true,
"ValidateIssuerSigningKey": true
},
"AllowedHosts": "*" "AllowedHosts": "*"
} }

View File

@@ -0,0 +1,6 @@
{
"subgraph": "UserNovelData",
"http": {
"baseAddress": "https://localhost:7298/graphql"
}
}

View File

@@ -1,29 +1,131 @@
<script lang="ts"> <script lang="ts">
import { Button } from '$lib/components/ui/button'; 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 ChevronLeft from '@lucide/svelte/icons/chevron-left';
import ChevronRight from '@lucide/svelte/icons/chevron-right'; import ChevronRight from '@lucide/svelte/icons/chevron-right';
import List from '@lucide/svelte/icons/list'; import List from '@lucide/svelte/icons/list';
import Bookmark from '@lucide/svelte/icons/bookmark';
import BookmarkCheck from '@lucide/svelte/icons/bookmark-check';
interface Props { interface Props {
novelId: string; novelId: string;
chapterId?: number;
prevChapterVolumeOrder: number | null | undefined; prevChapterVolumeOrder: number | null | undefined;
prevChapterOrder: number | null | undefined; prevChapterOrder: number | null | undefined;
nextChapterVolumeOrder: number | null | undefined; nextChapterVolumeOrder: number | null | undefined;
nextChapterOrder: number | null | undefined; nextChapterOrder: number | null | undefined;
showKeyboardHints?: boolean; showKeyboardHints?: boolean;
isBookmarked?: boolean;
bookmarkDescription?: string | null;
onBookmarkChange?: (isBookmarked: boolean, description?: string | null) => void;
} }
let { let {
novelId, novelId,
chapterId,
prevChapterVolumeOrder, prevChapterVolumeOrder,
prevChapterOrder, prevChapterOrder,
nextChapterVolumeOrder, nextChapterVolumeOrder,
nextChapterOrder, nextChapterOrder,
showKeyboardHints = true showKeyboardHints = true,
isBookmarked = false,
bookmarkDescription = null,
onBookmarkChange
}: Props = $props(); }: Props = $props();
const hasPrev = $derived(prevChapterOrder != null && prevChapterVolumeOrder != null); const hasPrev = $derived(prevChapterOrder != null && prevChapterVolumeOrder != null);
const hasNext = $derived(nextChapterOrder != null && nextChapterVolumeOrder != 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> </script>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@@ -38,10 +140,72 @@
<span class="hidden sm:inline">Previous</span> <span class="hidden sm:inline">Previous</span>
</Button> </Button>
<Button variant="outline" href="/novels/{novelId}" class="gap-2"> <div class="flex items-center gap-2">
<List class="h-4 w-4" /> <Button variant="outline" href="/novels/{novelId}" class="gap-2">
<span class="hidden sm:inline">Contents</span> <List class="h-4 w-4" />
</Button> <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 <Button
variant="outline" variant="outline"

View File

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

View File

@@ -1,8 +1,10 @@
<script lang="ts" module> <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 { TagType } from '$lib/graphql/__generated__/graphql';
import { SystemTags } from '$lib/constants/systemTags'; import { SystemTags } from '$lib/constants/systemTags';
export type BookmarkData = GetBookmarksQuery['bookmarks'][number];
export type NovelNode = NonNullable<NonNullable<NovelQuery['novels']>['nodes']>[number]; export type NovelNode = NonNullable<NonNullable<NovelQuery['novels']>['nodes']>[number];
const statusColors: Record<NovelStatus, string> = { const statusColors: Record<NovelStatus, string> = {
@@ -32,7 +34,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { client } from '$lib/graphql/client'; 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 { isAuthenticated } from '$lib/auth/authStore';
import { Card, CardContent, CardHeader } from '$lib/components/ui/card'; import { Card, CardContent, CardHeader } from '$lib/components/ui/card';
import { Badge } from '$lib/components/ui/badge'; import { Badge } from '$lib/components/ui/badge';
@@ -98,6 +100,11 @@
let activeTab = $state('chapters'); let activeTab = $state('chapters');
let galleryLoaded = $state(false); let galleryLoaded = $state(false);
// Bookmarks state
let bookmarks: BookmarkData[] = $state([]);
let bookmarksLoaded = $state(false);
let bookmarksFetching = $state(false);
const DESCRIPTION_PREVIEW_LENGTH = 300; const DESCRIPTION_PREVIEW_LENGTH = 300;
// Derived values // Derived values
@@ -128,6 +135,15 @@
const isSingleVolume = $derived(sortedVolumes.length === 1); 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( const chapterCount = $derived(
sortedVolumes.reduce((sum, v) => sum + v.chapters.length, 0) 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 // Image viewer functions
function openImageViewer(index: number) { function openImageViewer(index: number) {
viewerIndex = index; viewerIndex = index;
@@ -528,10 +572,9 @@
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="bookmarks" 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"
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"
> >
Bookmarks Bookmarks{bookmarksLoaded ? ` (${bookmarks.length})` : ''}
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
</CardHeader> </CardHeader>
@@ -646,9 +689,50 @@
</TabsContent> </TabsContent>
<TabsContent value="bookmarks" class="mt-0"> <TabsContent value="bookmarks" class="mt-0">
<p class="text-muted-foreground text-sm py-8 text-center"> {#if bookmarksFetching}
Bookmarks coming soon. <div class="flex items-center justify-center py-8">
</p> <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> </TabsContent>
</CardContent> </CardContent>
</Tabs> </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; } as const;
export type ApplyPolicy = typeof ApplyPolicy[keyof typeof ApplyPolicy]; 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 = { export type ChapterDto = {
body: Scalars['String']['output']; body: Scalars['String']['output'];
createdTime: Scalars['Instant']['output']; createdTime: Scalars['Instant']['output'];
@@ -266,9 +279,11 @@ export type Mutation = {
fetchChapterContents: FetchChapterContentsPayload; fetchChapterContents: FetchChapterContentsPayload;
importNovel: ImportNovelPayload; importNovel: ImportNovelPayload;
inviteUser: InviteUserPayload; inviteUser: InviteUserPayload;
removeBookmark: RemoveBookmarkPayload;
runJob: RunJobPayload; runJob: RunJobPayload;
scheduleEventJob: ScheduleEventJobPayload; scheduleEventJob: ScheduleEventJobPayload;
translateText: TranslateTextPayload; translateText: TranslateTextPayload;
upsertBookmark: UpsertBookmarkPayload;
}; };
@@ -297,6 +312,11 @@ export type MutationInviteUserArgs = {
}; };
export type MutationRemoveBookmarkArgs = {
input: RemoveBookmarkInput;
};
export type MutationRunJobArgs = { export type MutationRunJobArgs = {
input: RunJobInput; input: RunJobInput;
}; };
@@ -311,6 +331,11 @@ export type MutationTranslateTextArgs = {
input: TranslateTextInput; input: TranslateTextInput;
}; };
export type MutationUpsertBookmarkArgs = {
input: UpsertBookmarkInput;
};
export type NovelDto = { export type NovelDto = {
author: PersonDto; author: PersonDto;
coverImage: Maybe<ImageDto>; coverImage: Maybe<ImageDto>;
@@ -471,6 +496,7 @@ export type PersonDtoSortInput = {
}; };
export type Query = { export type Query = {
bookmarks: Array<BookmarkDto>;
chapter: Maybe<ChapterReaderDto>; chapter: Maybe<ChapterReaderDto>;
currentUser: Maybe<UserDto>; currentUser: Maybe<UserDto>;
jobs: Array<SchedulerJob>; jobs: Array<SchedulerJob>;
@@ -480,6 +506,11 @@ export type Query = {
}; };
export type QueryBookmarksArgs = {
novelId: Scalars['UnsignedInt']['input'];
};
export type QueryChapterArgs = { export type QueryChapterArgs = {
chapterOrder: Scalars['UnsignedInt']['input']; chapterOrder: Scalars['UnsignedInt']['input'];
novelId: Scalars['UnsignedInt']['input']; novelId: Scalars['UnsignedInt']['input'];
@@ -514,6 +545,17 @@ export type QueryTranslationRequestsArgs = {
where?: InputMaybe<TranslationRequestDtoFilterInput>; 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 RunJobError = JobPersistenceError;
export type RunJobInput = { export type RunJobInput = {
@@ -739,6 +781,19 @@ export type UnsignedIntOperationFilterInputType = {
nlte?: InputMaybe<Scalars['UnsignedInt']['input']>; 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 = { export type UserDto = {
availableInvites: Scalars['Int']['output']; availableInvites: Scalars['Int']['output'];
createdTime: Scalars['Instant']['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 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<{ export type GetChapterQueryVariables = Exact<{
novelId: Scalars['UnsignedInt']['input']; novelId: Scalars['UnsignedInt']['input'];
volumeOrder: 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 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 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 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 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 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>; 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
}
}