refactor(reader): implement unified reader shell with feature orchestration
- Replace callback-based architecture with Feature Registration Pattern - Add feature registry for modular feature initialization - Create createNavigationAPI() for unified page/chapter navigation - Implement renderCurrentPage() for PDF/comic/manga rendering - Add updateProgress() to sync reading progress to backend - Support ebook, PDF, comic, and manga readers in single interface - Remove 600+ lines of callback wiring code - reader-shell now acts as lightweight orchestrator - Delegates specific functionality to feature modules - Parse ebooks using parser-manager with format routing - Initialize readers based on library type from metadata
This commit is contained in:
@@ -1,160 +0,0 @@
|
|||||||
// Parser Manager - Routes files to appropriate parsers
|
|
||||||
// Procedural style: Functions, not classes
|
|
||||||
|
|
||||||
import JSZip from "jszip";
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Parser Registry
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
const PARSER_REGISTRY: ParserEntry[] = [
|
|
||||||
{
|
|
||||||
format: "epub",
|
|
||||||
mimeType: "application/epub+zip",
|
|
||||||
extensions: [".epub"],
|
|
||||||
side: "client",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
format: "fb2",
|
|
||||||
mimeType: "application/fb2",
|
|
||||||
extensions: [".fb2", ".fb2.zip"],
|
|
||||||
side: "client",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
format: "txt",
|
|
||||||
mimeType: "text/plain",
|
|
||||||
extensions: [".txt"],
|
|
||||||
side: "client",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
format: "html",
|
|
||||||
mimeType: "text/html",
|
|
||||||
extensions: [".html", ".htm"],
|
|
||||||
side: "client",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
format: "mobi",
|
|
||||||
mimeType: "application/x-mobipocket-ebook",
|
|
||||||
extensions: [".mobi", ".azw"],
|
|
||||||
side: "server",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
format: "azw3",
|
|
||||||
mimeType: "application/vnd.amazon.mobi8-ebook",
|
|
||||||
extensions: [".azw3"],
|
|
||||||
side: "server",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
format: "docx",
|
|
||||||
mimeType:
|
|
||||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
||||||
extensions: [".docx"],
|
|
||||||
side: "server",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
format: "rtf",
|
|
||||||
mimeType: "application/rtf",
|
|
||||||
extensions: [".rtf"],
|
|
||||||
side: "server",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
interface ParserEntry {
|
|
||||||
format: string;
|
|
||||||
mimeType: string;
|
|
||||||
extensions: string[];
|
|
||||||
side: "client" | "server";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Parser Detection
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
export function detectParserFormat(
|
|
||||||
mimeType: string,
|
|
||||||
extension: string,
|
|
||||||
): ParserEntry | null {
|
|
||||||
return (
|
|
||||||
PARSER_REGISTRY.find(
|
|
||||||
(entry) =>
|
|
||||||
entry.mimeType === mimeType ||
|
|
||||||
entry.extensions.includes(extension.toLowerCase()),
|
|
||||||
) || null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function requiresServerParsing(
|
|
||||||
mimeType: string,
|
|
||||||
extension: string,
|
|
||||||
): boolean {
|
|
||||||
const entry = detectParserFormat(mimeType, extension);
|
|
||||||
return entry?.side === "server" || false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Main Parse Function (Router)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
export async function parseEbook(
|
|
||||||
file: Blob,
|
|
||||||
mimeType: string,
|
|
||||||
extension: string,
|
|
||||||
): Promise<EbookCIF> {
|
|
||||||
const entry = detectParserFormat(mimeType, extension);
|
|
||||||
|
|
||||||
if (!entry) {
|
|
||||||
throw new Error(`Unsupported ebook format: ${mimeType}, ${extension}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.side === "server") {
|
|
||||||
return parseEbookOnServer(file, entry.format);
|
|
||||||
} else {
|
|
||||||
return parseEbookOnClient(file, entry.format);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Client-Side Parsing
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
async function parseEbookOnClient(
|
|
||||||
file: Blob,
|
|
||||||
format: string,
|
|
||||||
): Promise<EbookCIF> {
|
|
||||||
switch (format) {
|
|
||||||
case "epub":
|
|
||||||
return parseEPUB(file);
|
|
||||||
case "fb2":
|
|
||||||
return parseFB2(file);
|
|
||||||
case "txt":
|
|
||||||
return parseTXT(file);
|
|
||||||
case "html":
|
|
||||||
return parseHTML(file);
|
|
||||||
default:
|
|
||||||
throw new Error(`Client-side parser not implemented for: ${format}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Server-Side Parsing (API Call)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
async function parseEbookOnServer(
|
|
||||||
file: Blob,
|
|
||||||
format: string,
|
|
||||||
): Promise<EbookCIF> {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("file", file);
|
|
||||||
formData.append("format", format);
|
|
||||||
|
|
||||||
const response = await fetch("/readers/parse", {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Server parsing failed: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
+104
-271
@@ -1,29 +1,15 @@
|
|||||||
// Universal Reader Shell - Routes to appropriate reader
|
|
||||||
// Procedural style: Functions, not classes
|
|
||||||
|
|
||||||
import { Alpine } from "../alpine";
|
import { Alpine } from "../alpine";
|
||||||
import { getReaderMetadata, updateReadingProgress } from "./api";
|
import { getReaderMetadata, updateReadingProgress } from "../api";
|
||||||
import { SettingsManager } from "./settings-manager";
|
import { createReaderContext, type ReaderContext } from "./core/reader-context";
|
||||||
import { ProgressIndicator } from "./progress-indicator";
|
import { getState, setState } from "./core/reader-state";
|
||||||
import { parseEbook, requiresServerParsing } from "./parser-manager";
|
import { createNavigationAPI } from "./core/reader-navigation";
|
||||||
import { initializePDFReader } from "./pdf/pdfjs-wrapper";
|
import { readerEvents } from "./core/reader-events";
|
||||||
import { initializeComicReader } from "./comic/image-parser";
|
|
||||||
|
|
||||||
// ============================================================
|
type ReaderType = "ebook" | "pdf" | "comic" | "manga";
|
||||||
// Reader State
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
let currentReader:
|
|
||||||
| UniversalReader
|
|
||||||
| PDFReader
|
|
||||||
| ComicReader
|
|
||||||
| MangaReader
|
|
||||||
| null = null;
|
|
||||||
let readerMetadata: ReaderMetadata | null = null;
|
|
||||||
|
|
||||||
interface UniversalReader {
|
interface UniversalReader {
|
||||||
type: "ebook";
|
type: "ebook";
|
||||||
cif: EbookCIF;
|
cif: any;
|
||||||
currentSpineIndex: number;
|
currentSpineIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,18 +32,68 @@ interface MangaReader {
|
|||||||
readingDirection: "rtl" | "vertical";
|
readingDirection: "rtl" | "vertical";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CurrentReader = UniversalReader | PDFReader | ComicReader | MangaReader;
|
||||||
|
|
||||||
|
let currentReader: CurrentReader | null = null;
|
||||||
|
let readerMetadata: import("../types/reader").ReaderMetadata | null = null;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Initialization
|
// Feature Registry
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
type FeatureInit = (context: ReaderContext) => void | Promise<void>;
|
||||||
|
|
||||||
|
const featureModules: FeatureInit[] = [
|
||||||
|
// Core features
|
||||||
|
(ctx) => import("./features/gestures").then((m) => m.init(ctx)),
|
||||||
|
(ctx) => import("./features/keyboard-shortcuts").then((m) => m.init(ctx)),
|
||||||
|
(ctx) => import("./features/panel-dock-system").then((m) => m.init(ctx)),
|
||||||
|
(ctx) => import("./features/navigator-panel").then((m) => m.init(ctx)),
|
||||||
|
(ctx) => import("./features/offline-manager").then((m) => m.init(ctx)),
|
||||||
|
(ctx) => import("./features/reading-speed-tracker").then((m) => m.init(ctx)),
|
||||||
|
(ctx) => import("./features/progress-indicator").then((m) => m.init(ctx)),
|
||||||
|
|
||||||
|
// Comic features
|
||||||
|
(ctx) => import("./comic/background-color").then((m) => m.init?.(ctx)),
|
||||||
|
(ctx) => import("./comic/chapter-markers").then((m) => m.init?.(ctx)),
|
||||||
|
(ctx) => import("./comic/page-cache").then((m) => m.init?.(ctx)),
|
||||||
|
|
||||||
|
// PDF features
|
||||||
|
(ctx) => import("./pdf/pdf-navigation").then((m) => m.init?.(ctx)),
|
||||||
|
(ctx) => import("./pdf/pdf-text-selection").then((m) => m.init?.(ctx)),
|
||||||
|
(ctx) => import("./pdf/annotation-layer").then((m) => m.init?.(ctx)),
|
||||||
|
|
||||||
|
// Ebook features
|
||||||
|
(ctx) => import("./ebook/copy-handler").then((m) => m.init?.(ctx)),
|
||||||
|
(ctx) => import("./ebook/font-loader").then((m) => m.init?.(ctx)),
|
||||||
|
|
||||||
|
// Manga features
|
||||||
|
(ctx) => import("./manga/reading-direction").then((m) => m.init?.(ctx)),
|
||||||
|
(ctx) => import("./manga/vertical-scroll-mode").then((m) => m.init?.(ctx)),
|
||||||
|
];
|
||||||
|
|
||||||
|
async function initializeFeatures(context: ReaderContext): Promise<void> {
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
featureModules.map((getInit) => getInit(context)),
|
||||||
|
);
|
||||||
|
|
||||||
|
results.forEach((result, index) => {
|
||||||
|
if (result.status === "rejected") {
|
||||||
|
console.warn(`Feature ${index} failed to initialize:`, result.reason);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Reader Initialization
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
async function initializeReader(): Promise<void> {
|
async function initializeReader(): Promise<void> {
|
||||||
const mediaItemId = document.body.dataset.mediaItemId;
|
const mediaItemId = document.body.dataset.mediaItemId;
|
||||||
if (!mediaItemId) return;
|
if (!mediaItemId) return;
|
||||||
|
|
||||||
// Fetch metadata
|
|
||||||
readerMetadata = await getReaderMetadata(mediaItemId);
|
readerMetadata = await getReaderMetadata(mediaItemId);
|
||||||
|
|
||||||
// Initialize appropriate reader based on type
|
|
||||||
switch (readerMetadata.library_type) {
|
switch (readerMetadata.library_type) {
|
||||||
case "ebook":
|
case "ebook":
|
||||||
currentReader = await initializeEbookReader(readerMetadata);
|
currentReader = await initializeEbookReader(readerMetadata);
|
||||||
@@ -73,15 +109,28 @@ async function initializeReader(): Promise<void> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentReader) {
|
if (!currentReader) return;
|
||||||
setupReaderUI();
|
|
||||||
}
|
setState({ currentReader, readerMetadata });
|
||||||
|
|
||||||
|
const context = createReaderContext(
|
||||||
|
getState,
|
||||||
|
setState,
|
||||||
|
createNavigationAPI(),
|
||||||
|
() =>
|
||||||
|
(context.render = () => {
|
||||||
|
// Delegates to navigation module
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await initializeFeatures(context);
|
||||||
|
readerEvents.emit("readerReady", currentReader);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initializeEbookReader(
|
async function initializeEbookReader(metadata: any): Promise<UniversalReader> {
|
||||||
metadata: ReaderMetadata,
|
const { parseEbook, requiresServerParsing } =
|
||||||
): Promise<UniversalReader> {
|
await import("./core/parser-manager");
|
||||||
// Check if server-side parsing is needed
|
|
||||||
const needsServer = requiresServerParsing(
|
const needsServer = requiresServerParsing(
|
||||||
metadata.mime_type,
|
metadata.mime_type,
|
||||||
getFileExtension(metadata.file_path),
|
getFileExtension(metadata.file_path),
|
||||||
@@ -90,7 +139,6 @@ async function initializeEbookReader(
|
|||||||
let ebookFile: Blob;
|
let ebookFile: Blob;
|
||||||
|
|
||||||
if (needsServer) {
|
if (needsServer) {
|
||||||
// Fetch parsed CIF from server
|
|
||||||
const response = await fetch(`/readers/${metadata.media_item_id}/parse`, {
|
const response = await fetch(`/readers/${metadata.media_item_id}/parse`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -106,12 +154,10 @@ async function initializeEbookReader(
|
|||||||
|
|
||||||
ebookFile = await response.blob();
|
ebookFile = await response.blob();
|
||||||
} else {
|
} else {
|
||||||
// Fetch original file for client-side parsing
|
|
||||||
const response = await fetch(metadata.file_path);
|
const response = await fetch(metadata.file_path);
|
||||||
ebookFile = await response.blob();
|
ebookFile = await response.blob();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse ebook to CIF
|
|
||||||
const cif = await parseEbook(
|
const cif = await parseEbook(
|
||||||
ebookFile,
|
ebookFile,
|
||||||
metadata.mime_type,
|
metadata.mime_type,
|
||||||
@@ -125,186 +171,24 @@ async function initializeEbookReader(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
async function initializePDFReader(metadata: any): Promise<PDFReader> {
|
||||||
// UI Setup
|
const { initializePDFReader } = await import("./pdf/pdfjs-wrapper");
|
||||||
// ============================================================
|
return initializePDFReader(metadata);
|
||||||
|
|
||||||
function setupReaderUI(): void {
|
|
||||||
if (!currentReader || !readerMetadata) return;
|
|
||||||
|
|
||||||
// Setup chrome
|
|
||||||
setupChromeBehavior();
|
|
||||||
|
|
||||||
// Setup progress indicator
|
|
||||||
setupProgressIndicator();
|
|
||||||
|
|
||||||
// Setup annotations
|
|
||||||
setupAnnotations();
|
|
||||||
|
|
||||||
// Setup keyboard navigation
|
|
||||||
setupKeyboardNavigation();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupChromeBehavior(): void {
|
async function initializeComicReader(metadata: any): Promise<ComicReader> {
|
||||||
const chrome = document.getElementById("reader-chrome");
|
const { initializeComicReader } = await import("./comic/image-parser");
|
||||||
if (!chrome) return;
|
return initializeComicReader(metadata);
|
||||||
|
|
||||||
// Auto-hide on scroll
|
|
||||||
let hideTimeout: NodeJS.Timeout;
|
|
||||||
|
|
||||||
window.addEventListener("scroll", () => {
|
|
||||||
chrome.classList.add("visible");
|
|
||||||
|
|
||||||
clearTimeout(hideTimeout);
|
|
||||||
hideTimeout = setTimeout(() => {
|
|
||||||
chrome.classList.remove("visible");
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Toggle on tap (for touch devices)
|
|
||||||
chrome.addEventListener("click", () => {
|
|
||||||
chrome.classList.toggle("visible");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupProgressIndicator(): void {
|
async function initializeMangaReader(metadata: any): Promise<MangaReader> {
|
||||||
// Update progress based on reader type
|
const { initializeMangaReader } = await import("./comic/image-parser");
|
||||||
if (!currentReader) return;
|
return initializeMangaReader(metadata);
|
||||||
|
|
||||||
if (currentReader.type === "ebook") {
|
|
||||||
updateEbookProgress(currentReader.cif, currentReader.currentSpineIndex);
|
|
||||||
} else if (currentReader.type === "pdf") {
|
|
||||||
updatePDFProgress(
|
|
||||||
currentReader.currentPage,
|
|
||||||
readerMetadata.total_pages || 0,
|
|
||||||
);
|
|
||||||
} else if (currentReader.type === "comic" || currentReader.type === "manga") {
|
|
||||||
updateComicProgress(currentReader.currentPage, currentReader.images.length);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupAnnotations(): void {
|
function getFileExtension(filepath: string): string {
|
||||||
// Load existing highlights and notes
|
const match = filepath.match(/\.([^.]+)$/);
|
||||||
// Implementation depends on annotation system
|
return match ? `.${match[1]}` : "";
|
||||||
}
|
|
||||||
|
|
||||||
function setupKeyboardNavigation(): void {
|
|
||||||
document.addEventListener("keydown", (e) => {
|
|
||||||
if (!currentReader) return;
|
|
||||||
|
|
||||||
switch (e.key) {
|
|
||||||
case "ArrowRight":
|
|
||||||
case "ArrowDown":
|
|
||||||
e.preventDefault();
|
|
||||||
nextPage();
|
|
||||||
break;
|
|
||||||
case "ArrowLeft":
|
|
||||||
case "ArrowUp":
|
|
||||||
e.preventDefault();
|
|
||||||
previousPage();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Navigation Functions
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
function nextPage(): void {
|
|
||||||
if (!currentReader) return;
|
|
||||||
|
|
||||||
if (currentReader.type === "ebook") {
|
|
||||||
nextSpineItem();
|
|
||||||
} else if (currentReader.type === "pdf") {
|
|
||||||
nextPDFPage();
|
|
||||||
} else if (currentReader.type === "comic" || currentReader.type === "manga") {
|
|
||||||
nextComicPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function previousPage(): void {
|
|
||||||
if (!currentReader) return;
|
|
||||||
|
|
||||||
if (currentReader.type === "ebook") {
|
|
||||||
previousSpineItem();
|
|
||||||
} else if (currentReader.type === "pdf") {
|
|
||||||
previousPDFPage();
|
|
||||||
} else if (currentReader.type === "comic" || currentReader.type === "manga") {
|
|
||||||
previousComicPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextSpineItem(): void {
|
|
||||||
if (currentReader?.type !== "ebook") return;
|
|
||||||
|
|
||||||
if (currentReader.currentSpineIndex < currentReader.cif.spine.length - 1) {
|
|
||||||
currentReader.currentSpineIndex++;
|
|
||||||
renderCurrentSpineItem();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function previousSpineItem(): void {
|
|
||||||
if (currentReader?.type !== "ebook") return;
|
|
||||||
|
|
||||||
if (currentReader.currentSpineIndex > 0) {
|
|
||||||
currentReader.currentSpineIndex--;
|
|
||||||
renderCurrentSpineItem();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCurrentSpineItem(): void {
|
|
||||||
if (currentReader?.type !== "ebook") return;
|
|
||||||
|
|
||||||
const spineItem = currentReader.cif.spine[currentReader.currentSpineIndex];
|
|
||||||
const container = document.getElementById("reader-content");
|
|
||||||
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
// Render spine item content
|
|
||||||
container.innerHTML = spineItem.content;
|
|
||||||
|
|
||||||
// Apply theme and typography
|
|
||||||
applyReaderTheme();
|
|
||||||
applyTypography();
|
|
||||||
|
|
||||||
// Update progress
|
|
||||||
updateProgress();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Progress Tracking
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
function updateProgress(): void {
|
|
||||||
if (!currentReader || !readerMetadata) return;
|
|
||||||
|
|
||||||
let percentage = 0;
|
|
||||||
let currentPosition = "";
|
|
||||||
|
|
||||||
if (currentReader.type === "ebook") {
|
|
||||||
const totalSpine = currentReader.cif.spine.length;
|
|
||||||
percentage = (currentReader.currentSpineIndex + 1) / totalSpine;
|
|
||||||
currentPosition = `spine:${currentReader.currentSpineIndex}`;
|
|
||||||
} else if (currentReader.type === "pdf") {
|
|
||||||
const totalPages = readerMetadata.total_pages || 1;
|
|
||||||
percentage = currentReader.currentPage / totalPages;
|
|
||||||
currentPosition = `page:${currentReader.currentPage}`;
|
|
||||||
} else if (currentReader.type === "comic" || currentReader.type === "manga") {
|
|
||||||
const totalPages = currentReader.images.length;
|
|
||||||
percentage = currentReader.currentPage / totalPages;
|
|
||||||
currentPosition = `page:${currentReader.currentPage}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send to backend
|
|
||||||
updateReadingProgress(readerMetadata.media_item_id, {
|
|
||||||
percentage,
|
|
||||||
current_page:
|
|
||||||
currentReader.type === "ebook"
|
|
||||||
? currentReader.currentSpineIndex
|
|
||||||
: currentReader.currentPage,
|
|
||||||
position: currentPosition,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -316,77 +200,26 @@ Alpine.data("readerShell", () => ({
|
|||||||
initializeReader();
|
initializeReader();
|
||||||
},
|
},
|
||||||
|
|
||||||
nextPage,
|
|
||||||
previousPage,
|
|
||||||
|
|
||||||
get currentPage() {
|
get currentPage() {
|
||||||
if (!currentReader) return 0;
|
const state = getState();
|
||||||
|
if (!state.currentReader) return 0;
|
||||||
|
|
||||||
if (currentReader.type === "ebook") {
|
if (state.currentReader.type === "ebook") {
|
||||||
return currentReader.currentSpineIndex + 1;
|
return state.currentReader.currentSpineIndex + 1;
|
||||||
} else {
|
|
||||||
return currentReader.currentPage;
|
|
||||||
}
|
}
|
||||||
|
return state.currentReader.currentPage;
|
||||||
},
|
},
|
||||||
|
|
||||||
get totalPages() {
|
get totalPages() {
|
||||||
if (!currentReader || !readerMetadata) return 0;
|
const state = getState();
|
||||||
|
if (!state.currentReader || !state.readerMetadata) return 0;
|
||||||
|
|
||||||
if (currentReader.type === "ebook") {
|
if (state.currentReader.type === "ebook") {
|
||||||
return currentReader.cif.spine.length;
|
return state.currentReader.cif.spine.length;
|
||||||
} else if (currentReader.type === "pdf") {
|
} else if (state.currentReader.type === "pdf") {
|
||||||
return readerMetadata.total_pages || 0;
|
return state.readerMetadata.total_pages || 0;
|
||||||
} else {
|
} else {
|
||||||
return currentReader.images.length;
|
return (state.currentReader as any).images.length;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Utility Functions
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
function getFileExtension(filepath: string): string {
|
|
||||||
const match = filepath.match(/\.([^.]+)$/);
|
|
||||||
return match ? `.${match[1]}` : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyReaderTheme(): void {
|
|
||||||
// Apply reading theme from settings
|
|
||||||
const settings = getReaderSettings();
|
|
||||||
|
|
||||||
const container = document.getElementById("reader-content");
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
container.className = `ebook-content theme-${settings.reading_theme}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyTypography(): void {
|
|
||||||
const settings = getReaderSettings();
|
|
||||||
const container = document.getElementById("reader-content");
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
container.style.fontSize = `${settings.font_size}px`;
|
|
||||||
container.style.lineHeight = settings.line_height.toString();
|
|
||||||
container.style.fontFamily = getFontStack(settings.reading_font);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFontStack(font: string): string {
|
|
||||||
const stacks: Record<string, string> = {
|
|
||||||
literata: '"Literata", serif',
|
|
||||||
crimson: '"Crimson Text", serif',
|
|
||||||
"source-serif": '"Source Serif 4", serif',
|
|
||||||
"eb-garamond": '"EB Garamond", serif',
|
|
||||||
libertinus: '"Libertinus Serif", serif',
|
|
||||||
"noto-serif": '"Noto Serif", serif',
|
|
||||||
"charis-sil": '"Charis SIL", serif',
|
|
||||||
"ibm-plex": '"IBM Plex Serif", serif',
|
|
||||||
};
|
|
||||||
|
|
||||||
return stacks[font] || stacks["literata"];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getReaderSettings(): ReaderSettings {
|
|
||||||
// Load from settings manager
|
|
||||||
return {} as ReaderSettings; // Simplified
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user