chore: format the tree and clear ruff's findings

Applies ruff format, ruff check --fix and prettier, so CI can gate on all three.
The surviving unused imports were all re-exports in __init__.py, now covered by a
per-file ignore; the pdf.js viewer and the generated openapi types join the
vendored code that eslint and prettier already skip.
This commit is contained in:
2026-08-17 15:17:14 -04:00
parent b70ed5cb51
commit 45b03764d2
85 changed files with 847 additions and 709 deletions
+34 -24
View File
@@ -1,5 +1,10 @@
import { command, getRequestEvent, query } from '$app/server';
import { bookshelfCreate, bookshelfQuerySchema, modifyBooksInShelf, type Bookshelf } from '$lib/schema/bookshelf';
import {
bookshelfCreate,
bookshelfQuerySchema,
modifyBooksInShelf,
type Bookshelf
} from '$lib/schema/bookshelf';
import { createQueryParams } from '$lib/utils';
import { error } from '@sveltejs/kit';
@@ -19,46 +24,51 @@ export const listBookshelves = query(bookshelfQuerySchema, async (data) => {
return await response.json();
});
export const addBooksToShelf = command(modifyBooksInShelf, async ({ shelf_id, ...data }): Promise<Bookshelf> => {
const { locals } = getRequestEvent();
export const addBooksToShelf = command(
modifyBooksInShelf,
async ({ shelf_id, ...data }): Promise<Bookshelf> => {
const { locals } = getRequestEvent();
const params = createQueryParams(data);
const params = createQueryParams(data);
const response = await locals.api.post(`/shelves/${shelf_id}/books?${params.toString()}`, {});
const response = await locals.api.post(`/shelves/${shelf_id}/books?${params.toString()}`, {});
if (!response.ok) {
const message = await response.text();
error(response.status, message);
if (!response.ok) {
const message = await response.text();
error(response.status, message);
}
return await response.json();
}
);
return await response.json()
});
export const removeBooksFromShelf = command(
modifyBooksInShelf,
async ({ shelf_id, ...data }): Promise<Bookshelf> => {
const { locals } = getRequestEvent();
export const removeBooksFromShelf = command(modifyBooksInShelf, async ({ shelf_id, ...data }): Promise<Bookshelf> => {
const { locals } = getRequestEvent();
const params = createQueryParams(data);
const params = createQueryParams(data);
const response = await locals.api.delete(`/shelves/${shelf_id}/books?${params.toString()}`);
const response = await locals.api.delete(`/shelves/${shelf_id}/books?${params.toString()}`);
if (!response.ok) {
const message = await response.text();
error(response.status, message);
}
if (!response.ok) {
const message = await response.text();
error(response.status, message);
return await response.json();
}
return await response.json()
});
);
export const createBookshelf = command(bookshelfCreate, async (data) => {
const { locals } = getRequestEvent();
const response = await locals.api.post(`/shelves`, data)
const response = await locals.api.post(`/shelves`, data);
if (!response.ok) {
const message = await response.text();
error(response.status, message);
}
return await response.json()
})
return await response.json();
});
+17 -17
View File
@@ -4,40 +4,40 @@ import { error } from '@sveltejs/kit';
import z from 'zod';
export const listDevices = query(async (): Promise<Device[]> => {
const { locals } = getRequestEvent();
const { locals } = getRequestEvent();
const response = await locals.api.get(`/devices`);
const response = await locals.api.get(`/devices`);
if (!response.ok) error(500, 'An unkown error occurred');
if (!response.ok) error(500, 'An unkown error occurred');
const deviceResult = await response.json();
return deviceResult.items
const deviceResult = await response.json();
return deviceResult.items;
});
export const createDevice = form(createDeviceSchema, async (data): Promise<Device> => {
const { locals } = getRequestEvent();
const { locals } = getRequestEvent();
const response = await locals.api.post(`/devices`, data);
const response = await locals.api.post(`/devices`, data);
if (!response.ok) error(500, 'An unknown error occurred');
if (!response.ok) error(500, 'An unknown error occurred');
return await response.json();
return await response.json();
});
export const regenerateDeviceApiKey = command(z.string(), async (deviceId): Promise<Device> => {
const { locals } = getRequestEvent();
const { locals } = getRequestEvent();
const response = await locals.api.get(`/devices/${deviceId}/regenerate`);
const response = await locals.api.get(`/devices/${deviceId}/regenerate`);
if (!response.ok) error(500, 'An unknown error occurred');
if (!response.ok) error(500, 'An unknown error occurred');
return await response.json();
})
return await response.json();
});
export const deleteDevice = command(z.string(), async (deviceId): Promise<void> => {
const { locals } = getRequestEvent();
const { locals } = getRequestEvent();
const response = await locals.api.delete(`/devices/${deviceId}`);
const response = await locals.api.delete(`/devices/${deviceId}`);
if (!response.ok) error(500, 'An unknown error occurred');
if (!response.ok) error(500, 'An unknown error occurred');
});
@@ -1,35 +1,36 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog/index.js';
import { Button } from "$lib/components/ui/button/index.js";
import { Input } from "$lib/components/ui/input/index.js";
import { Label } from "$lib/components/ui/label/index.js";
let { open = $bindable(), onSubmit }: { open?: boolean, onSubmit: (name: string) => Promise<undefined> } = $props()
let shelfName = $state('')
import { Button } from '$lib/components/ui/button/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import { Label } from '$lib/components/ui/label/index.js';
let {
open = $bindable(),
onSubmit
}: { open?: boolean; onSubmit: (name: string) => Promise<undefined> } = $props();
let shelfName = $state('');
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>Create bookshelf</Dialog.Title>
</Dialog.Header>
<div class="flex items-center gap-4">
<Label>Name</Label>
<Input bind:value={shelfName}/>
</div>
<div class="flex items-center gap-4">
<Label>Name</Label>
<Input bind:value={shelfName} />
</div>
<!-- Buttons -->
<Dialog.Footer class="ml-auto flex">
<Button variant="outline" onclick={() => (open = false)}>Cancel</Button>
<Button
onclick={async () => {
await onSubmit(shelfName);
await onSubmit(shelfName);
}}
variant="default">Create</Button
>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
</Dialog.Root>
@@ -28,7 +28,9 @@
// directly in the markup keeps it static.
const header = $derived({
title: 'chitai',
url: resolve('/(root)/(library)/library/[libraryId]', { libraryId: String(libraryState.activeLibrary!.id) })
url: resolve('/(root)/(library)/library/[libraryId]', {
libraryId: String(libraryState.activeLibrary!.id)
})
});
</script>
@@ -2,7 +2,7 @@
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import { useSidebar } from '$lib/components/ui/sidebar/index.js';
import { Badge } from "$lib/components/ui/badge/index.js";
import { Badge } from '$lib/components/ui/badge/index.js';
import { getLibraryState, LibraryState } from '$lib/state/library.svelte';
import { LIBRARY_ICONS } from '$lib/components/ui/icon-picker/index.js';
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
@@ -12,7 +12,9 @@
const libraryState = getLibraryState();
const sidebar = useSidebar();
const activeIcon = $derived(LIBRARY_ICONS[libraryState.activeLibrary?.icon ?? 'library'] ?? LIBRARY_ICONS['library']);
const activeIcon = $derived(
LIBRARY_ICONS[libraryState.activeLibrary?.icon ?? 'library'] ?? LIBRARY_ICONS['library']
);
</script>
<Sidebar.Menu>
@@ -35,7 +37,7 @@
<span class="ml-1 truncate font-semibold">
{libraryState.activeLibrary!.name}
</span>
<span class="ml-1 truncate font-mono text-xs tabular-nums text-muted-foreground">
<span class="ml-1 truncate font-mono text-xs text-muted-foreground tabular-nums">
{libraryState.activeLibrary!.total ?? 0} books
</span>
</div>
@@ -51,15 +53,15 @@
>
<DropdownMenu.Label class="text-xs text-muted-foreground">Libraries</DropdownMenu.Label>
{#each libraryState.libraries as library (library.name)}
{@const LibraryIcon = (LIBRARY_ICONS[library.icon ?? 'library'] ?? LIBRARY_ICONS['library']).component}
{@const LibraryIcon = (
LIBRARY_ICONS[library.icon ?? 'library'] ?? LIBRARY_ICONS['library']
).component}
<DropdownMenu.Item onSelect={() => libraryState.setActive(library.id)} class="gap-2 p-2">
<div class="flex size-6 items-center justify-center rounded-md border">
<LibraryIcon class="size-3.5 shrink-0" />
</div>
{library.name}
<Badge
variant="outline"
class="font-semibold ml-auto">
<Badge variant="outline" class="ml-auto font-semibold">
{library.total ?? 0}
</Badge>
</DropdownMenu.Item>
@@ -1,6 +1,6 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
@@ -14,10 +14,10 @@
bind:this={ref}
data-slot="avatar-badge"
class={cn(
"bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none",
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
'absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none',
'group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden',
'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2',
'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
className
)}
{...restProps}
@@ -1,6 +1,6 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { Avatar as AvatarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
@@ -13,7 +13,7 @@
bind:ref
data-slot="avatar-fallback"
class={cn(
"rounded-full bg-muted text-muted-foreground flex size-full items-center justify-center text-sm group-data-[size=sm]/avatar:text-xs",
'flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs',
className
)}
{...restProps}
@@ -1,6 +1,6 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="avatar-group-count"
class={cn(
"size-8 rounded-full bg-muted text-sm text-muted-foreground group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 relative flex shrink-0 items-center justify-center ring-2 ring-background",
'relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
className
)}
{...restProps}
@@ -1,6 +1,6 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from '$lib/utils.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
@@ -14,7 +14,7 @@
bind:this={ref}
data-slot="avatar-group"
class={cn(
"cn-avatar-group group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
'cn-avatar-group group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background',
className
)}
{...restProps}
@@ -1,6 +1,6 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { Avatar as AvatarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
@@ -12,6 +12,6 @@
<AvatarPrimitive.Image
bind:ref
data-slot="avatar-image"
class={cn("rounded-full aspect-square size-full object-cover", className)}
class={cn('aspect-square size-full rounded-full object-cover', className)}
{...restProps}
/>
@@ -1,15 +1,15 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { Avatar as AvatarPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
loadingStatus = $bindable("loading"),
size = "default",
loadingStatus = $bindable('loading'),
size = 'default',
class: className,
...restProps
}: AvatarPrimitive.RootProps & {
size?: "default" | "sm" | "lg";
size?: 'default' | 'sm' | 'lg';
} = $props();
</script>
@@ -19,7 +19,7 @@
data-slot="avatar"
data-size={size}
class={cn(
"size-8 rounded-full after:rounded-full data-[size=lg]:size-10 data-[size=sm]:size-6 group/avatar relative flex shrink-0 select-none after:absolute after:inset-0 after:border after:border-border after:mix-blend-darken dark:after:mix-blend-lighten",
'group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten',
className
)}
{...restProps}
@@ -1,9 +1,9 @@
import Badge from "./avatar-badge.svelte";
import Fallback from "./avatar-fallback.svelte";
import GroupCount from "./avatar-group-count.svelte";
import Group from "./avatar-group.svelte";
import Image from "./avatar-image.svelte";
import Root from "./avatar.svelte";
import Badge from './avatar-badge.svelte';
import Fallback from './avatar-fallback.svelte';
import GroupCount from './avatar-group-count.svelte';
import Group from './avatar-group.svelte';
import Image from './avatar-image.svelte';
import Root from './avatar.svelte';
export {
Root,
@@ -18,5 +18,5 @@ export {
Fallback as AvatarFallback,
Badge as AvatarBadge,
Group as AvatarGroup,
GroupCount as AvatarGroupCount,
GroupCount as AvatarGroupCount
};
@@ -15,8 +15,7 @@
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
accent:
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90'
accent: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90'
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
@@ -45,11 +45,7 @@
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-64 p-3" align="start">
<Input
bind:value={search}
placeholder="Search icons..."
class="mb-3 h-8"
/>
<Input bind:value={search} placeholder="Search icons..." class="mb-3 h-8" />
<ScrollArea class="h-48">
<div class="grid grid-cols-6 gap-1">
{#each filteredIcons as [key, icon] (key)}
@@ -58,7 +54,10 @@
<Tooltip.Trigger>
<button
type="button"
class="flex size-8 items-center justify-center rounded-md hover:bg-accent {value === key ? 'bg-accent' : ''}"
class="flex size-8 items-center justify-center rounded-md hover:bg-accent {value ===
key
? 'bg-accent'
: ''}"
onclick={() => selectIcon(key)}
>
<IconComponent class="size-4" />
@@ -59,7 +59,10 @@ import type { Component } from 'svelte';
export type IconName = keyof typeof LIBRARY_ICONS;
export const LIBRARY_ICONS: Record<string, { component: Component; label: string; category: string }> = {
export const LIBRARY_ICONS: Record<
string,
{ component: Component; label: string; category: string }
> = {
// Generic
library: { component: Library, label: 'Library', category: 'Generic' },
'book-open': { component: BookOpen, label: 'Book Open', category: 'Generic' },
@@ -1,8 +1,8 @@
import Root from "./popover.svelte";
import Close from "./popover-close.svelte";
import Content from "./popover-content.svelte";
import Trigger from "./popover-trigger.svelte";
import Portal from "./popover-portal.svelte";
import Root from './popover.svelte';
import Close from './popover-close.svelte';
import Content from './popover-content.svelte';
import Trigger from './popover-trigger.svelte';
import Portal from './popover-portal.svelte';
export {
Root,
@@ -15,5 +15,5 @@ export {
Content as PopoverContent,
Trigger as PopoverTrigger,
Close as PopoverClose,
Portal as PopoverPortal,
Portal as PopoverPortal
};
@@ -1,5 +1,5 @@
<script lang="ts">
import { Popover as PopoverPrimitive } from "bits-ui";
import { Popover as PopoverPrimitive } from 'bits-ui';
let { ref = $bindable(null), ...restProps }: PopoverPrimitive.CloseProps = $props();
</script>
@@ -1,14 +1,14 @@
<script lang="ts">
import { Popover as PopoverPrimitive } from "bits-ui";
import PopoverPortal from "./popover-portal.svelte";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
import { Popover as PopoverPrimitive } from 'bits-ui';
import PopoverPortal from './popover-portal.svelte';
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
import type { ComponentProps } from 'svelte';
let {
ref = $bindable(null),
class: className,
sideOffset = 4,
align = "center",
align = 'center',
portalProps,
...restProps
}: PopoverPrimitive.ContentProps & {
@@ -23,7 +23,7 @@
{sideOffset}
{align}
class={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--bits-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
'z-50 w-72 origin-(--bits-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
className
)}
{...restProps}
@@ -1,5 +1,5 @@
<script lang="ts">
import { Popover as PopoverPrimitive } from "bits-ui";
import { Popover as PopoverPrimitive } from 'bits-ui';
let { ...restProps }: PopoverPrimitive.PortalProps = $props();
</script>
@@ -1,6 +1,6 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { Popover as PopoverPrimitive } from "bits-ui";
import { cn } from '$lib/utils.js';
import { Popover as PopoverPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
@@ -12,6 +12,6 @@
<PopoverPrimitive.Trigger
bind:ref
data-slot="popover-trigger"
class={cn("", className)}
class={cn('', className)}
{...restProps}
/>
@@ -1,5 +1,5 @@
<script lang="ts">
import { Popover as PopoverPrimitive } from "bits-ui";
import { Popover as PopoverPrimitive } from 'bits-ui';
let { open = $bindable(false), ...restProps }: PopoverPrimitive.RootProps = $props();
</script>
@@ -1,6 +1,6 @@
import { Tooltip as TooltipPrimitive } from "bits-ui";
import Trigger from "./tooltip-trigger.svelte";
import Content from "./tooltip-content.svelte";
import { Tooltip as TooltipPrimitive } from 'bits-ui';
import Trigger from './tooltip-trigger.svelte';
import Content from './tooltip-content.svelte';
const Root = TooltipPrimitive.Root;
const Provider = TooltipPrimitive.Provider;
@@ -17,5 +17,5 @@ export {
Content as TooltipContent,
Trigger as TooltipTrigger,
Provider as TooltipProvider,
Portal as TooltipPortal,
Portal as TooltipPortal
};
@@ -1,12 +1,12 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import { Tooltip as TooltipPrimitive } from 'bits-ui';
import { cn } from '$lib/utils.js';
let {
ref = $bindable(null),
class: className,
sideOffset = 0,
side = "top",
side = 'top',
children,
arrowClasses,
...restProps
@@ -22,7 +22,7 @@
{sideOffset}
{side}
class={cn(
"bg-primary text-primary-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 origin-(--bits-tooltip-content-transform-origin) z-50 w-fit text-balance rounded-md px-3 py-1.5 text-xs",
'z-50 w-fit origin-(--bits-tooltip-content-transform-origin) animate-in rounded-md bg-primary px-3 py-1.5 text-xs text-balance text-primary-foreground fade-in-0 zoom-in-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 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
className
)}
{...restProps}
@@ -32,11 +32,11 @@
{#snippet child({ props })}
<div
class={cn(
"bg-primary z-50 size-2.5 rotate-45 rounded-[2px]",
"data-[side=top]:translate-x-1/2 data-[side=top]:translate-y-[calc(-50%_+_2px)]",
"data-[side=bottom]:-translate-x-1/2 data-[side=bottom]:-translate-y-[calc(-50%_+_1px)]",
"data-[side=right]:translate-x-[calc(50%_+_2px)] data-[side=right]:translate-y-1/2",
"data-[side=left]:-translate-y-[calc(50%_-_3px)]",
'z-50 size-2.5 rotate-45 rounded-[2px] bg-primary',
'data-[side=top]:translate-x-1/2 data-[side=top]:translate-y-[calc(-50%_+_2px)]',
'data-[side=bottom]:-translate-x-1/2 data-[side=bottom]:-translate-y-[calc(-50%_+_1px)]',
'data-[side=right]:translate-x-[calc(50%_+_2px)] data-[side=right]:translate-y-1/2',
'data-[side=left]:-translate-y-[calc(50%_-_3px)]',
arrowClasses
)}
{...props}
@@ -1,5 +1,5 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from "bits-ui";
import { Tooltip as TooltipPrimitive } from 'bits-ui';
let { ref = $bindable(null), ...restProps }: TooltipPrimitive.TriggerProps = $props();
</script>
@@ -49,7 +49,8 @@
Read
</DropdownMenu.Item>
<DropdownMenu.Item
onclick={() => bookOps.downloadBookFile(book.id, book.files[0].id, book.files[0].filename)}
onclick={() =>
bookOps.downloadBookFile(book.id, book.files[0].id, book.files[0].filename)}
>
<Download class="size-4" />
Download
@@ -111,7 +111,11 @@
<Spinner />
</div>
{:else if bookCollection.books.length > 0}
<ScrollArea bind:viewportRef={scrollContainer} orientation="both" class="h-[calc(100vh-11rem)] w-full px-5 pb-5">
<ScrollArea
bind:viewportRef={scrollContainer}
orientation="both"
class="h-[calc(100vh-11rem)] w-full px-5 pb-5"
>
{#if bookCollection.view === 'grid'}
<BookGrid books={bookCollection.books} />
{:else if bookCollection.view === 'list'}
@@ -170,7 +174,7 @@
</Sidebar.Inset>
<!-- Filter Sidebar (right-side) -->
<FilterSidebar class="m-2 pb-5 h-full pt-20" />
<FilterSidebar class="m-2 h-full pt-20 pb-5" />
</Sidebar.Provider>
</div>
</div>
@@ -73,8 +73,8 @@
bookOps.deleteDialogTitle = `Delete "${book.title}"?`;
bookOps.deleteFn = async (deleteFiles: boolean) => {
await bookOps.deleteBooks([book.id], deleteFiles);
libraryState.activeLibrary!.total!--
bookshelfState.deletedBooks([book])
libraryState.activeLibrary!.total!--;
bookshelfState.deletedBooks([book]);
};
bookOps.deleteDialogOpen = true;
}}
@@ -121,7 +121,10 @@
? 'cursor-pointer'
: ''}"
>
<a href={resolve('/(root)/(library)/book/[bookId]', { bookId: String(book.id) })} class="shrink-0">
<a
href={resolve('/(root)/(library)/book/[bookId]', { bookId: String(book.id) })}
class="shrink-0"
>
<BookCover {book} height={110} />
</a>
@@ -18,7 +18,7 @@
aria-pressed={active}
onclick={() => (active ? bookCollection.clearView() : bookCollection.applyPreset(preset))}
class="shrink-0 rounded-full border px-3 py-1 text-xs whitespace-nowrap transition-colors {active
? 'border-primary bg-primary text-primary-foreground font-medium'
? 'border-primary bg-primary font-medium text-primary-foreground'
: 'border-border text-muted-foreground hover:border-muted-foreground hover:text-foreground'}"
>
{preset.label}
@@ -19,9 +19,7 @@
class="{buttonVariants({ variant: 'ghost', size: 'icon' })} relative"
>
{#if bookCollection.hasActiveSort}
<div
class="absolute top-[5px] right-[5px] h-1.5 w-1.5 rounded-full bg-flag"
></div>
<div class="absolute top-[5px] right-[5px] h-1.5 w-1.5 rounded-full bg-flag"></div>
{/if}
<ArrowUpDown />
</DropdownMenu.Trigger>
+1 -1
View File
@@ -19,7 +19,7 @@ export const bookshelfCreate = z.object({
title: z.string().min(1, 'Must have a title'),
library_id: stringCoerce.optional(),
book_ids: stringArrayCoerce.optional()
})
});
export type BookshelfQuerySchema = typeof bookshelfQuerySchema;
export type ModifyBooksInShelf = typeof modifyBooksInShelf;
+3 -3
View File
@@ -1,8 +1,8 @@
import { z } from 'zod';
import type { components } from './openapi/schema';
export type Device = components['schemas']['KosyncDeviceRead']
export type Device = components['schemas']['KosyncDeviceRead'];
export const createDeviceSchema = z.object({
name: z.string().min(1, 'Name cannot be empty')
})
name: z.string().min(1, 'Name cannot be empty')
});
@@ -1,16 +1,11 @@
import { invalidate } from '$app/navigation';
import { page } from '$app/state';
import {
deleteBookFiles,
deleteBooks,
listBooks,
updateBookProgress
} from '$lib/api';
import { deleteBookFiles, deleteBooks, listBooks, updateBookProgress } from '$lib/api';
import {
type Book,
type UpdateBookProgress,
type BookQuery,
type PaginatedResponse,
type PaginatedResponse
} from '$lib/schema';
import { getContext, setContext } from 'svelte';
import { toast } from 'svelte-sonner';
+7 -11
View File
@@ -7,12 +7,9 @@ export class BookSelectionState {
readonly selectionModeActive = $derived(this.selectedBooks.size !== 0);
toggleSelection(book: Book) {
const bookId = book.id.toString()
if (this.selectedBooks.has(bookId))
this.selectedBooks.delete(bookId);
else
this.selectedBooks.set(bookId, book);
const bookId = book.id.toString();
if (this.selectedBooks.has(bookId)) this.selectedBooks.delete(bookId);
else this.selectedBooks.set(bookId, book);
}
isSelected(id: number | string): boolean {
@@ -20,11 +17,11 @@ export class BookSelectionState {
}
getSelectedBooks(): Book[] {
return Array.from(this.selectedBooks.values())
return Array.from(this.selectedBooks.values());
}
getSelectedIds(): string[] {
return Array.from(this.selectedBooks.keys())
return Array.from(this.selectedBooks.keys());
}
numSelected() {
@@ -33,9 +30,8 @@ export class BookSelectionState {
selectAll(books: Book[]) {
books.forEach((book) => {
const bookId = book.id.toString()
if (!this.selectedBooks.has(bookId))
this.selectedBooks.set(bookId, book)
const bookId = book.id.toString();
if (!this.selectedBooks.has(bookId)) this.selectedBooks.set(bookId, book);
});
}
+98 -97
View File
@@ -9,16 +9,16 @@ export class BookshelfState {
readonly libraryBookshelves = new SvelteMap<string, Bookshelf[]>();
getBookshelves(libraryId: string | number): Bookshelf[] | undefined {
const id = libraryId.toString();
if (!this.libraryBookshelves.has(id)) {
this.fetchBookshelves(id).then(shelves => {
this.libraryBookshelves.set(id, shelves);
});
}
return this.libraryBookshelves.get(id);
}
const id = libraryId.toString();
if (!this.libraryBookshelves.has(id)) {
this.fetchBookshelves(id).then((shelves) => {
this.libraryBookshelves.set(id, shelves);
});
}
return this.libraryBookshelves.get(id);
}
async fetchBookshelves(libraryId: string) {
try {
@@ -38,108 +38,109 @@ export class BookshelfState {
title: name,
library_id: libraryId,
book_ids: booksToAdd
})
});
if (bookshelf.library_id) {
const currentShelves = this.libraryBookshelves.get(bookshelf.library_id.toString()) || [];
this.libraryBookshelves.set(bookshelf.library_id.toString(), [...currentShelves, bookshelf]);
this.libraryBookshelves.set(bookshelf.library_id.toString(), [
...currentShelves,
bookshelf
]);
}
invalidate('app:books')
invalidate('app:books');
if (booksToAdd?.length)
toast.success(`Added ${booksToAdd.length} books to '${name}'`)
else
toast.success(`Created shelf '${name}'`)
return bookshelf
if (booksToAdd?.length) toast.success(`Added ${booksToAdd.length} books to '${name}'`);
else toast.success(`Created shelf '${name}'`);
return bookshelf;
} catch (error) {
toast.error(`Failed to create bookshelf '${name}'`)
console.error(`Failed to create bookshelf: `, error)
toast.error(`Failed to create bookshelf '${name}'`);
console.error(`Failed to create bookshelf: `, error);
}
}
async addBooksToShelf(shelfId: number | string, bookIds: number[]) {
try {
const shelf = await addBooksToShelf({
shelf_id: shelfId,
book_ids: bookIds
});
this.updateShelf(shelf)
if (bookIds.length === 1) toast.success(`Added book to shelf!`);
else toast.success(`Added ${bookIds.length} books to shelf!`);
} catch (error) {
console.error('Failed to add book(s) to shelf: ', error);
toast.error('Failed to add book(s) to shelf.');
}
}
async removeBooksFromShelf(shelfId: number | string, bookIds: number[]) {
try {
const shelf = await removeBooksFromShelf({
shelf_id: shelfId,
book_ids: bookIds
});
this.updateShelf(shelf)
if (bookIds.length === 1) toast.success(`Removed book from shelf.`);
else toast.success(`Removed ${bookIds.length} books from shelf!`);
} catch (error) {
console.error('Failed to remove book(s) from shelf: ', error);
toast.error('Failed to remove book(s) from shelf.');
}
}
async updateShelf(shelf: Bookshelf) {
if (!shelf.library_id) return;
const bookshelves = this.libraryBookshelves.get(shelf.library_id.toString())
if (!bookshelves) return;
const index = bookshelves.findIndex(bookshelf => bookshelf.id === shelf.id)
if (index !== -1) {
this.libraryBookshelves.set(
shelf.library_id.toString(),
[...bookshelves.slice(0, index), shelf, ...bookshelves.slice(index + 1)]
);
}
}
deletedBooks(books: Book[]) {
// Assume all books are in the same library
const libraryId = books[0].library_id.toString()
const bookshelves = this.libraryBookshelves.get(libraryId);
if (!bookshelves) return;
// Create a Set of shelf IDs that need updating for efficient lookup
const shelfIdsToUpdate = new Set<number>();
books.forEach(book => {
book.lists.forEach(shelf => {
shelfIdsToUpdate.add(shelf.id);
});
try {
const shelf = await addBooksToShelf({
shelf_id: shelfId,
book_ids: bookIds
});
const updatedBookshelves = bookshelves.map(shelf => {
if (shelfIdsToUpdate.has(shelf.id)) {
// Count how many times this shelf appears across all deleted books
let decrementBy = 0;
books.forEach(book => {
if (book.lists.some(s => s.id === shelf.id)) {
decrementBy++;
}
});
return { ...shelf, total: shelf.total - decrementBy };
}
return shelf;
});
this.libraryBookshelves.set(libraryId, updatedBookshelves);
this.updateShelf(shelf);
if (bookIds.length === 1) toast.success(`Added book to shelf!`);
else toast.success(`Added ${bookIds.length} books to shelf!`);
} catch (error) {
console.error('Failed to add book(s) to shelf: ', error);
toast.error('Failed to add book(s) to shelf.');
}
}
async removeBooksFromShelf(shelfId: number | string, bookIds: number[]) {
try {
const shelf = await removeBooksFromShelf({
shelf_id: shelfId,
book_ids: bookIds
});
this.updateShelf(shelf);
if (bookIds.length === 1) toast.success(`Removed book from shelf.`);
else toast.success(`Removed ${bookIds.length} books from shelf!`);
} catch (error) {
console.error('Failed to remove book(s) from shelf: ', error);
toast.error('Failed to remove book(s) from shelf.');
}
}
async updateShelf(shelf: Bookshelf) {
if (!shelf.library_id) return;
const bookshelves = this.libraryBookshelves.get(shelf.library_id.toString());
if (!bookshelves) return;
const index = bookshelves.findIndex((bookshelf) => bookshelf.id === shelf.id);
if (index !== -1) {
this.libraryBookshelves.set(shelf.library_id.toString(), [
...bookshelves.slice(0, index),
shelf,
...bookshelves.slice(index + 1)
]);
}
}
deletedBooks(books: Book[]) {
// Assume all books are in the same library
const libraryId = books[0].library_id.toString();
const bookshelves = this.libraryBookshelves.get(libraryId);
if (!bookshelves) return;
// Create a Set of shelf IDs that need updating for efficient lookup
const shelfIdsToUpdate = new Set<number>();
books.forEach((book) => {
book.lists.forEach((shelf) => {
shelfIdsToUpdate.add(shelf.id);
});
});
const updatedBookshelves = bookshelves.map((shelf) => {
if (shelfIdsToUpdate.has(shelf.id)) {
// Count how many times this shelf appears across all deleted books
let decrementBy = 0;
books.forEach((book) => {
if (book.lists.some((s) => s.id === shelf.id)) {
decrementBy++;
}
});
return { ...shelf, total: shelf.total - decrementBy };
}
return shelf;
});
this.libraryBookshelves.set(libraryId, updatedBookshelves);
}
}
const BOOKSHELF_KEY = Symbol('BOOKSHELF');
export function setBookshelfState() {
+6 -3
View File
@@ -32,9 +32,12 @@ export class LibraryState {
this.activeLibrary = this.libraries.find((lib) => lib.id === libraryId) || this.libraries[0];
if (browser) {
localStorage.setItem('previousLibraryId', this.activeLibrary.id.toString());
await goto(resolve('/(root)/(library)/library/[libraryId]/view', { libraryId: String(libraryId) }), {
invalidate: ['app:libraries']
});
await goto(
resolve('/(root)/(library)/library/[libraryId]/view', { libraryId: String(libraryId) }),
{
invalidate: ['app:libraries']
}
);
}
}
+8 -2
View File
@@ -44,7 +44,10 @@ export const FONT_STACKS: { label: string; value: string }[] = [
{ label: 'Palatino', value: "'Palatino Linotype', Palatino, 'Book Antiqua', serif" },
{ label: 'Helvetica', value: "'Helvetica Neue', Helvetica, Arial, sans-serif" },
{ label: 'Verdana', value: 'Verdana, Geneva, sans-serif' },
{ label: 'Monospace', value: "ui-monospace, 'SF Mono', 'Cascadia Mono', Menlo, Consolas, monospace" }
{
label: 'Monospace',
value: "ui-monospace, 'SF Mono', 'Cascadia Mono', Menlo, Consolas, monospace"
}
];
export type Palette = Record<string, string>;
@@ -299,7 +302,10 @@ export function parseThemeCookie(raw: string | undefined | null): ThemeConfig {
try {
const parsed = JSON.parse(decodeURIComponent(raw));
if (!parsed || typeof parsed !== 'object') return { preset: DEFAULT_PRESET_ID };
return { ...parsed, preset: typeof parsed.preset === 'string' ? parsed.preset : DEFAULT_PRESET_ID };
return {
...parsed,
preset: typeof parsed.preset === 'string' ? parsed.preset : DEFAULT_PRESET_ID
};
} catch {
return { preset: DEFAULT_PRESET_ID };
}
+3 -3
View File
@@ -35,9 +35,9 @@ export class ThemeState {
readonly isCustomised = $derived(
Boolean(
this.config.radius ||
this.config.fonts ||
Object.keys(this.config.light ?? {}).length ||
Object.keys(this.config.dark ?? {}).length
this.config.fonts ||
Object.keys(this.config.light ?? {}).length ||
Object.keys(this.config.dark ?? {}).length
)
);