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:
@@ -10,7 +10,8 @@
|
||||
--radius: 0.625rem;
|
||||
|
||||
/* Typography — system stacks, so nothing depends on a CDN or a webfont build. */
|
||||
--app-font-sans: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--app-font-sans:
|
||||
system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--app-font-serif: Georgia, 'Iowan Old Style', 'Times New Roman', serif;
|
||||
--app-font-mono: ui-monospace, 'SF Mono', 'Cascadia Mono', Menlo, Consolas, monospace;
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||
import * as Empty from '$lib/components/ui/empty/index.js'
|
||||
import { Button } from '$lib/components/ui/button/index.js'
|
||||
import * as Empty from '$lib/components/ui/empty/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import BookList from '$lib/components/view/book-list.svelte';
|
||||
import { FolderX, Upload } from '@lucide/svelte';
|
||||
import { getBookOperationsState } from '$lib/state/bookOperations.svelte.js';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let bookOps = getBookOperationsState()
|
||||
|
||||
let bookOps = getBookOperationsState();
|
||||
</script>
|
||||
|
||||
<ScrollArea class="h-full w-full">
|
||||
@@ -22,7 +21,7 @@
|
||||
|
||||
<!-- Empty when no preset returned anything, i.e. the library has no books -->
|
||||
{#if data.presets.every((preset) => data.shelves[preset.id].length === 0)}
|
||||
<div class="flex flex-col items-center justify-center w-full h-full">
|
||||
<div class="flex h-full w-full flex-col items-center justify-center">
|
||||
<!-- Show a CTA to upload books if the library is empty -->
|
||||
<Empty.Root class="mb-[12vh]">
|
||||
<Empty.Header>
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<div class="[--header-height:calc(--spacing(14))]">
|
||||
<Sidebar.Provider class="flex flex-col">
|
||||
<div class="flex h-screen">
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
|
||||
<!-- Colours -->
|
||||
<section class="flex flex-col gap-3">
|
||||
<Label class="text-xs font-medium tracking-wider text-muted-foreground uppercase">Colours</Label>
|
||||
<Label class="text-xs font-medium tracking-wider text-muted-foreground uppercase">Colours</Label
|
||||
>
|
||||
<div class="grid gap-x-6 gap-y-1 sm:grid-cols-2">
|
||||
{#each COLOR_TOKENS as token (token.key)}
|
||||
<div class="flex items-center gap-3 rounded-md px-2 py-2 hover:bg-muted">
|
||||
@@ -94,7 +95,8 @@
|
||||
|
||||
<!-- Radius -->
|
||||
<section class="flex flex-col gap-3">
|
||||
<Label class="text-xs font-medium tracking-wider text-muted-foreground uppercase">Corners</Label>
|
||||
<Label class="text-xs font-medium tracking-wider text-muted-foreground uppercase">Corners</Label
|
||||
>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each RADII as option (option.value)}
|
||||
<button
|
||||
@@ -138,7 +140,8 @@
|
||||
|
||||
<!-- Preview -->
|
||||
<section class="flex flex-col gap-3">
|
||||
<Label class="text-xs font-medium tracking-wider text-muted-foreground uppercase">Preview</Label>
|
||||
<Label class="text-xs font-medium tracking-wider text-muted-foreground uppercase">Preview</Label
|
||||
>
|
||||
<div class="rounded-lg border bg-sidebar p-4">
|
||||
<div class="flex flex-col gap-4 rounded-lg border bg-card p-4">
|
||||
<div class="flex items-baseline justify-between gap-4">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { listDevices } from "$lib/api/device.remote";
|
||||
import { listDevices } from '$lib/api/device.remote';
|
||||
|
||||
export async function load() {
|
||||
const devices = await listDevices();
|
||||
const devices = await listDevices();
|
||||
|
||||
return {
|
||||
devices
|
||||
};
|
||||
return {
|
||||
devices
|
||||
};
|
||||
}
|
||||
|
||||
@@ -109,9 +109,7 @@
|
||||
<Smartphone />
|
||||
</Empty.Media>
|
||||
<Empty.Title>No devices</Empty.Title>
|
||||
<Empty.Description>
|
||||
Add a device to sync your KOReader reading progress.
|
||||
</Empty.Description>
|
||||
<Empty.Description>Add a device to sync your KOReader reading progress.</Empty.Description>
|
||||
</Empty.Header>
|
||||
<Empty.Content>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
@@ -224,12 +222,14 @@
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>Regenerate API key?</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
This will invalidate the current API key for "{regenerateConfirmDevice?.name}".
|
||||
You will need to update the key in your KOReader device settings.
|
||||
This will invalidate the current API key for "{regenerateConfirmDevice?.name}". You will
|
||||
need to update the key in your KOReader device settings.
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
<AlertDialog.Footer>
|
||||
<AlertDialog.Cancel onclick={() => (regenerateConfirmDevice = null)}>Cancel</AlertDialog.Cancel>
|
||||
<AlertDialog.Cancel onclick={() => (regenerateConfirmDevice = null)}
|
||||
>Cancel</AlertDialog.Cancel
|
||||
>
|
||||
<AlertDialog.Action
|
||||
onclick={() => regenerateConfirmDevice && handleRegenerate(regenerateConfirmDevice)}
|
||||
>
|
||||
|
||||
@@ -38,7 +38,9 @@
|
||||
>
|
||||
<Table.Cell class="font-medium">
|
||||
<a
|
||||
href={resolve('/(root)/(library)/library/[libraryId]', { libraryId: String(library.id) })}
|
||||
href={resolve('/(root)/(library)/library/[libraryId]', {
|
||||
libraryId: String(library.id)
|
||||
})}
|
||||
class="hover:underline">{library.name}</a
|
||||
>
|
||||
</Table.Cell>
|
||||
|
||||
+1
-3
@@ -10,9 +10,7 @@ export async function load({ params, depends }) {
|
||||
// identifiers, description, publisher — so fetch them in one go rather than per
|
||||
// card, and let the dialog pick out the books for its own group.
|
||||
const ids = [...new Set(groups.flatMap((group) => group.books.map((book) => book.book_id)))];
|
||||
const books = ids.length
|
||||
? await listBooks({ ids, pageSize: ids.length })
|
||||
: { items: [] };
|
||||
const books = ids.length ? await listBooks({ ids, pageSize: ids.length }) : { items: [] };
|
||||
|
||||
return { groups, books: books.items };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user