Archived
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:
@@ -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 };
|
||||
Reference in New Issue
Block a user