- Add Feature Registry system in reader-shell.ts to track features - Update format detection to use format_group with switch statement - Add manga detection via library_type_name or manga_type fields - Import and register features via init() functions on load - Fix parser imports in parser-manager.ts (add missing function imports) - Simplify epub-parsers and fb2-parser by removing unused imports
246 lines
7.4 KiB
TypeScript
246 lines
7.4 KiB
TypeScript
import { Alpine } from "../alpine";
|
|
import { getReaderMetadata } from "../api";
|
|
import "./comic/panel-editor";
|
|
import { createReaderContext, type ReaderContext } from "./core/reader-context";
|
|
import { getState, setState } from "./core/reader-state";
|
|
import { createNavigationAPI } from "./core/reader-navigation";
|
|
import { readerEvents } from "./core/reader-events";
|
|
|
|
interface UniversalReader {
|
|
type: "ebook";
|
|
cif: any;
|
|
currentSpineIndex: number;
|
|
}
|
|
|
|
interface PDFReader {
|
|
type: "pdf";
|
|
doc: any;
|
|
currentPage: number;
|
|
}
|
|
|
|
interface ComicReader {
|
|
type: "comic";
|
|
images: Blob[];
|
|
currentPage: number;
|
|
}
|
|
|
|
interface MangaReader {
|
|
type: "manga";
|
|
images: Blob[];
|
|
currentPage: number;
|
|
readingDirection: "rtl" | "vertical";
|
|
}
|
|
|
|
type CurrentReader = UniversalReader | PDFReader | ComicReader | MangaReader;
|
|
|
|
let currentReader: CurrentReader | null = null;
|
|
let readerMetadata: ReaderMetadata | null = null;
|
|
|
|
// ============================================================
|
|
// 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> {
|
|
const mediaItemId = document.body.dataset.mediaItemId;
|
|
console.log("Initializing reader for:", mediaItemId);
|
|
if (!mediaItemId) {
|
|
console.error("No mediaItemId on body");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
readerMetadata = await getReaderMetadata(mediaItemId);
|
|
console.log("Got metadata:", readerMetadata);
|
|
console.log("Library type:", readerMetadata.library_type_name);
|
|
console.log("Mime type:", readerMetadata.mime_type);
|
|
switch (readerMetadata.format_group) {
|
|
case "reflowable":
|
|
if (
|
|
readerMetadata.mime_type === "application/epub+zip" ||
|
|
readerMetadata.file_path.endsWith(".epub")
|
|
) {
|
|
currentReader = await initializeEbookReader(readerMetadata);
|
|
}
|
|
break;
|
|
case "fixed_layout":
|
|
currentReader = await initializePDFReader(readerMetadata);
|
|
break;
|
|
case "comic_archive":
|
|
// Check manga_type or library_type_name
|
|
if (
|
|
readerMetadata.manga_type === "yes" ||
|
|
readerMetadata.manga_type === "yes_and_right_to_left" ||
|
|
readerMetadata.library_type_name === "manga"
|
|
) {
|
|
currentReader = await initializeMangaReader(readerMetadata);
|
|
} else {
|
|
currentReader = await initializeComicReader(readerMetadata);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (!currentReader) return;
|
|
|
|
setState({ currentReader, readerMetadata });
|
|
|
|
const context = createReaderContext(
|
|
getState,
|
|
setState,
|
|
createNavigationAPI(),
|
|
() =>
|
|
(context.render = () => {
|
|
// Delegates to navigation module
|
|
}),
|
|
);
|
|
|
|
await initializeFeatures(context);
|
|
readerEvents.emit("readerReady", currentReader);
|
|
} catch (error) {
|
|
console.error("Render initialization failed:", error);
|
|
}
|
|
}
|
|
|
|
async function initializeEbookReader(metadata: any): Promise<UniversalReader> {
|
|
const { parseEbook, requiresServerParsing } =
|
|
await import("./core/parser-manager");
|
|
|
|
const needsServer = requiresServerParsing(
|
|
metadata.mime_type,
|
|
getFileExtension(metadata.file_path),
|
|
);
|
|
|
|
let ebookFile: Blob;
|
|
|
|
if (needsServer) {
|
|
const response = await fetch(`/readers/${metadata.media_item_id}/parse`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
mime_type: metadata.mime_type,
|
|
file_path: metadata.file_path,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Server parsing failed: ${response.statusText}`);
|
|
}
|
|
|
|
ebookFile = await response.blob();
|
|
} else {
|
|
const response = await fetch(metadata.file_path);
|
|
ebookFile = await response.blob();
|
|
}
|
|
|
|
const cif = await parseEbook(
|
|
ebookFile,
|
|
metadata.mime_type,
|
|
getFileExtension(metadata.file_path),
|
|
);
|
|
|
|
return {
|
|
type: "ebook",
|
|
cif,
|
|
currentSpineIndex: 0,
|
|
};
|
|
}
|
|
|
|
async function initializePDFReader(metadata: any): Promise<PDFReader> {
|
|
const { initializePDFReader } = await import("./pdf/pdfjs-wrapper");
|
|
return initializePDFReader(metadata);
|
|
}
|
|
|
|
async function initializeComicReader(metadata: any): Promise<ComicReader> {
|
|
const { initializeComicReader } = await import("./comic/image-parser");
|
|
return initializeComicReader(metadata);
|
|
}
|
|
|
|
async function initializeMangaReader(metadata: any): Promise<MangaReader> {
|
|
const { initializeMangaReader } = await import("./comic/image-parser");
|
|
return initializeMangaReader(metadata);
|
|
}
|
|
|
|
function getFileExtension(filepath: string): string {
|
|
const match = filepath.match(/\.([^.]+)$/);
|
|
return match ? `.${match[1]}` : "";
|
|
}
|
|
|
|
// ============================================================
|
|
// Alpine.js Integration
|
|
// ============================================================
|
|
|
|
Alpine.data("readerShell", () => ({
|
|
initReader() {
|
|
initializeReader();
|
|
},
|
|
|
|
get currentPage() {
|
|
const state = getState();
|
|
if (!state.currentReader) return 0;
|
|
|
|
if (state.currentReader.type === "ebook") {
|
|
return state.currentReader.currentSpineIndex + 1;
|
|
}
|
|
return state.currentReader.currentPage;
|
|
},
|
|
|
|
get totalPages() {
|
|
const state = getState();
|
|
if (!state.currentReader || !state.readerMetadata) return 0;
|
|
|
|
if (state.currentReader.type === "ebook") {
|
|
return state.currentReader.cif.spine.length;
|
|
} else if (state.currentReader.type === "pdf") {
|
|
return state.readerMetadata.total_pages || 0;
|
|
} else {
|
|
return (state.currentReader as any).images.length;
|
|
}
|
|
},
|
|
}));
|