feat: uv config other feats

- add uv configuration for the backend
- update frontend to make auth work
- add new auth endpoints
- add bookmars feat
- add reader feat
This commit is contained in:
2026-06-03 22:06:01 -05:00
parent 730c748f5f
commit 6b4c0c43f8
137 changed files with 20319 additions and 2340 deletions
+151
View File
@@ -0,0 +1,151 @@
import type { ReadingSettings } from "../types/reader";
type EpubSpine = {
get: (target: string | number) => { href: string } | null;
spineItems?: Array<{ href: string }>;
};
type EpubBookForNav = {
ready: Promise<void>;
spine: EpubSpine;
};
type EpubRendition = {
themes: {
register: (name: string, styles: { body: Record<string, string> }) => void;
select: (name: string) => void;
fontSize: (size: string) => void;
override: (property: string, value: string, important?: boolean) => void;
};
book: EpubBookForNav & {
locations: {
generate: (size: number) => Promise<string[]>;
percentageFromCfi: (cfi: string) => number | null;
};
navigation?: {
get: (href: string) => { label?: string } | null;
};
};
currentLocation: () => Promise<{ start?: { href?: string } } | null>;
display: (target: string) => Promise<unknown>;
next: () => void;
prev: () => void;
};
type EpubManager = {
settings: { gap?: number };
isRendered?: () => boolean;
updateLayout?: () => void;
};
type EpubRenditionWithManager = EpubRendition & {
settings?: { gap?: number };
manager?: EpubManager;
};
const FONT_STACKS: Record<ReadingSettings["font_family"], string> = {
serif: 'Georgia, "Times New Roman", serif',
"sans-serif": 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
monospace: 'ui-monospace, "Cascadia Code", monospace',
};
const READER_THEME_KEY = "cloud-reader";
export function isEpubCfi(location: string | number): boolean {
return typeof location === "string" && location.startsWith("epubcfi(");
}
/** Map nav/TOC href to a spine href epub.js can display. */
export function resolveSpineHref(book: EpubBookForNav, href: string): string {
const hashIndex = href.indexOf("#");
const fragment = hashIndex >= 0 ? href.slice(hashIndex) : "";
const path = hashIndex >= 0 ? href.slice(0, hashIndex) : href;
const candidates = [href, path];
try {
candidates.push(decodeURIComponent(path), encodeURI(path));
} catch {
/* ignore malformed URI encoding in nav hrefs */
}
for (const candidate of candidates) {
if (book.spine.get(candidate)) {
return candidate.includes("#") ? candidate : candidate + fragment;
}
}
const normalizedPath = path.replace(/^\/+/, "");
const pathFile = normalizedPath.split("/").pop() ?? normalizedPath;
for (const item of book.spine.spineItems ?? []) {
const spineHref = item.href.replace(/^\/+/, "");
const spineFile = spineHref.split("/").pop() ?? spineHref;
if (
spineHref === normalizedPath ||
spineHref.endsWith(normalizedPath) ||
normalizedPath.endsWith(spineHref) ||
spineFile === pathFile
) {
return item.href + fragment;
}
}
return href;
}
/** epub.js paginated side padding is gap/2; map per-side margin to total gap. */
export function marginWidthToGap(marginWidth: number): number {
return marginWidth * 2;
}
function applyEpubGap(rendition: EpubRendition, marginWidth: number): void {
const gap = marginWidthToGap(marginWidth);
const r = rendition as EpubRenditionWithManager;
if (r.settings) {
r.settings.gap = gap;
}
if (r.manager?.settings) {
r.manager.settings.gap = gap;
}
if (r.manager?.isRendered?.() && r.manager.updateLayout) {
r.manager.updateLayout();
}
}
/** Apply reading preferences inside the epub.js iframe. */
export function applyRenditionSettings(
rendition: EpubRendition,
settings: ReadingSettings,
): void {
const fontStack = FONT_STACKS[settings.font_family] ?? FONT_STACKS.serif;
applyEpubGap(rendition, settings.margin_width);
rendition.themes.register(READER_THEME_KEY, {
body: {
color: `${settings.text_color} !important`,
background: `${settings.background_color} !important`,
"font-family": `${fontStack} !important`,
"line-height": `${settings.line_height} !important`,
},
});
rendition.themes.select(READER_THEME_KEY);
rendition.themes.fontSize(`${settings.font_size}px`);
}
export async function updateChapterTitleFromRendition(
rendition: EpubRendition,
): Promise<string> {
try {
const loc = await rendition.currentLocation();
const href = loc?.start?.href;
if (href && rendition.book.navigation) {
const nav = rendition.book.navigation.get(href);
if (nav?.label) return nav.label;
}
} catch {
/* ignore navigation lookup failures */
}
return "";
}
export type { EpubRendition };