reader: implement Feature Registration Pattern infrastructure
- 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
This commit is contained in:
@@ -1,11 +1,6 @@
|
|||||||
// Parser Manager - Routes files to appropriate parsers
|
// Parser Manager - Routes files to appropriate parsers
|
||||||
// Procedural style: Functions, not classes
|
// Procedural style: Functions, not classes
|
||||||
|
|
||||||
import { parseEPUB } from "../parsers/epub-parsers";
|
|
||||||
import { parseFB2 } from "../parsers/fb2-parser";
|
|
||||||
import { parseTXT } from "../parsers/txt-parser";
|
|
||||||
import { parseHTML } from "../parsers/html-parser";
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Parser Registry
|
// Parser Registry
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -125,14 +120,22 @@ async function parseEbookOnClient(
|
|||||||
format: string,
|
format: string,
|
||||||
): Promise<EbookCIF> {
|
): Promise<EbookCIF> {
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case "epub":
|
case "epub": {
|
||||||
|
const { parseEPUB } = await import("../parsers/epub-parsers");
|
||||||
return parseEPUB(file);
|
return parseEPUB(file);
|
||||||
case "fb2":
|
}
|
||||||
|
case "fb2": {
|
||||||
|
const { parseFB2 } = await import("../parsers/fb2-parser");
|
||||||
return parseFB2(file);
|
return parseFB2(file);
|
||||||
case "txt":
|
}
|
||||||
|
case "txt": {
|
||||||
|
const { parseTXT } = await import("../parsers/txt-parser");
|
||||||
return parseTXT(file);
|
return parseTXT(file);
|
||||||
case "html":
|
}
|
||||||
|
case "html": {
|
||||||
|
const { parseHTML } = await import("../parsers/html-parser");
|
||||||
return parseHTML(file);
|
return parseHTML(file);
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`Client-side parser not implemented for: ${format}`);
|
throw new Error(`Client-side parser not implemented for: ${format}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
// EPUB Parser - Converts EPUB 2/3 to Common Intermediate Format
|
// EPUB Parser - Converts EPUB 2/3 to Common Intermediate Format
|
||||||
// Procedural style: Functions, not classes
|
// Procedural style: Functions, not classes
|
||||||
|
|
||||||
import JSZip from 'jszip';
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Main Parse Function
|
// Main Parse Function
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
export async function parseEPUB(epubBlob: Blob): Promise<EbookCIF> {
|
export async function parseEPUB(epubBlob: Blob): Promise<EbookCIF> {
|
||||||
|
const JSZip = (await import('jszip')).default;
|
||||||
const zip = await JSZip.loadAsync(epubBlob);
|
const zip = await JSZip.loadAsync(epubBlob);
|
||||||
|
|
||||||
// Parse container.xml to find OPF file
|
// Parse container.xml to find OPF file
|
||||||
@@ -18,11 +17,9 @@ export async function parseEPUB(epubBlob: Blob): Promise<EbookCIF> {
|
|||||||
throw new Error('Invalid EPUB: no OPF file found');
|
throw new Error('Invalid EPUB: no OPF file found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse OPF file
|
|
||||||
const opfXml = await getZipFileContent(zip, opfPath);
|
const opfXml = await getZipFileContent(zip, opfPath);
|
||||||
const packageDoc = parseXML(opfXml);
|
const packageDoc = parseXML(opfXml);
|
||||||
|
|
||||||
// Extract all components
|
|
||||||
const metadata = extractMetadata(packageDoc);
|
const metadata = extractMetadata(packageDoc);
|
||||||
const spine = parseSpine(packageDoc);
|
const spine = parseSpine(packageDoc);
|
||||||
const toc = await parseTOC(zip, packageDoc, opfPath);
|
const toc = await parseTOC(zip, packageDoc, opfPath);
|
||||||
@@ -48,7 +45,7 @@ export async function parseEPUB(epubBlob: Blob): Promise<EbookCIF> {
|
|||||||
// Helper Functions
|
// Helper Functions
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
async function getZipFileContent(zip: JSZip, path: string): Promise<string> {
|
async function getZipFileContent(zip: any, path: string): Promise<string> {
|
||||||
const file = zip.file(path);
|
const file = zip.file(path);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
throw new Error(`File not found: ${path}`);
|
throw new Error(`File not found: ${path}`);
|
||||||
@@ -113,7 +110,7 @@ function parseSpine(packageDoc: XMLDocument): EbookCIF['spine'] {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseTOC(zip: JSZip, packageDoc: XMLDocument, opfPath: string): Promise<EbookCIF['toc']> {
|
async function parseTOC(zip: any, packageDoc: XMLDocument, opfPath: string): Promise<EbookCIF['toc']> {
|
||||||
// Try EPUB 3.0 navigation document first
|
// Try EPUB 3.0 navigation document first
|
||||||
const navItem = packageDoc.querySelector('manifest item[properties~="nav"]');
|
const navItem = packageDoc.querySelector('manifest item[properties~="nav"]');
|
||||||
if (navItem) {
|
if (navItem) {
|
||||||
@@ -140,7 +137,7 @@ async function parseTOC(zip: JSZip, packageDoc: XMLDocument, opfPath: string): P
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseNavTOC(zip: JSZip, navPath: string): Promise<EbookCIF['toc']> {
|
async function parseNavTOC(zip: any, navPath: string): Promise<EbookCIF['toc']> {
|
||||||
const navXml = await getZipFileContent(zip, navPath);
|
const navXml = await getZipFileContent(zip, navPath);
|
||||||
const navDoc = parseXML(navXml);
|
const navDoc = parseXML(navXml);
|
||||||
const nav = navDoc.querySelector('nav');
|
const nav = navDoc.querySelector('nav');
|
||||||
@@ -168,7 +165,7 @@ async function parseNavTOC(zip: JSZip, navPath: string): Promise<EbookCIF['toc']
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseNCXTOC(zip: JSZip, ncxPath: string): Promise<EbookCIF['toc']> {
|
async function parseNCXTOC(zip: any, ncxPath: string): Promise<EbookCIF['toc']> {
|
||||||
const ncxXml = await getZipFileContent(zip, ncxPath);
|
const ncxXml = await getZipFileContent(zip, ncxPath);
|
||||||
const ncxDoc = parseXML(ncxXml);
|
const ncxDoc = parseXML(ncxXml);
|
||||||
const navMap = ncxDoc.querySelector('navMap');
|
const navMap = ncxDoc.querySelector('navMap');
|
||||||
@@ -198,7 +195,7 @@ function parseNCXNode(node: Element): EbookCIF['toc'] {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadResources(zip: JSZip): Promise<Map<string, Blob>> {
|
async function loadResources(zip: any): Promise<Map<string, Blob>> {
|
||||||
const resources = new Map<string, Blob>();
|
const resources = new Map<string, Blob>();
|
||||||
const files = Object.keys(zip.files);
|
const files = Object.keys(zip.files);
|
||||||
|
|
||||||
@@ -213,7 +210,7 @@ async function loadResources(zip: JSZip): Promise<Map<string, Blob>> {
|
|||||||
return resources;
|
return resources;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extractCover(zip: JSZip, packageDoc: XMLDocument): Promise<Blob | undefined> {
|
async function extractCover(zip: any, packageDoc: XMLDocument): Promise<Blob | undefined> {
|
||||||
// Try cover-id metadata
|
// Try cover-id metadata
|
||||||
const coverId = packageDoc.querySelector('meta[name="cover"]')?.getAttribute('content');
|
const coverId = packageDoc.querySelector('meta[name="cover"]')?.getAttribute('content');
|
||||||
if (coverId) {
|
if (coverId) {
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
// FB2 is XML-based, similar to EPUB structure
|
// FB2 is XML-based, similar to EPUB structure
|
||||||
// Procedural style: Functions, not classes
|
// Procedural style: Functions, not classes
|
||||||
|
|
||||||
import JSZip from "jszip";
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Main Parse Function
|
// Main Parse Function
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -16,6 +14,7 @@ export async function parseFB2(fb2Blob: Blob): Promise<EbookCIF> {
|
|||||||
fb2Blob.type === "application/zip" ||
|
fb2Blob.type === "application/zip" ||
|
||||||
fb2Blob.type === "application/x-zip-compressed"
|
fb2Blob.type === "application/x-zip-compressed"
|
||||||
) {
|
) {
|
||||||
|
const JSZip = (await import("jszip")).default;
|
||||||
const zip = await JSZip.loadAsync(fb2Blob);
|
const zip = await JSZip.loadAsync(fb2Blob);
|
||||||
const files = Object.keys(zip.files);
|
const files = Object.keys(zip.files);
|
||||||
|
|
||||||
@@ -231,6 +230,7 @@ export async function getFB2Metadata(
|
|||||||
fb2Blob.type === "application/zip" ||
|
fb2Blob.type === "application/zip" ||
|
||||||
fb2Blob.type === "application/x-zip-compressed"
|
fb2Blob.type === "application/x-zip-compressed"
|
||||||
) {
|
) {
|
||||||
|
const JSZip = (await import("jszip")).default;
|
||||||
const zip = await JSZip.loadAsync(fb2Blob);
|
const zip = await JSZip.loadAsync(fb2Blob);
|
||||||
const files = Object.keys(zip.files);
|
const files = Object.keys(zip.files);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { Alpine } from "../alpine";
|
import { Alpine } from "../alpine";
|
||||||
import { getReaderMetadata, updateReadingProgress } from "../api";
|
import { getReaderMetadata } from "../api";
|
||||||
|
import "./comic/panel-editor";
|
||||||
import { createReaderContext, type ReaderContext } from "./core/reader-context";
|
import { createReaderContext, type ReaderContext } from "./core/reader-context";
|
||||||
import { getState, setState } from "./core/reader-state";
|
import { getState, setState } from "./core/reader-state";
|
||||||
import { createNavigationAPI } from "./core/reader-navigation";
|
import { createNavigationAPI } from "./core/reader-navigation";
|
||||||
import { readerEvents } from "./core/reader-events";
|
import { readerEvents } from "./core/reader-events";
|
||||||
|
|
||||||
type ReaderType = "ebook" | "pdf" | "comic" | "manga";
|
|
||||||
|
|
||||||
interface UniversalReader {
|
interface UniversalReader {
|
||||||
type: "ebook";
|
type: "ebook";
|
||||||
cif: any;
|
cif: any;
|
||||||
@@ -35,7 +34,7 @@ interface MangaReader {
|
|||||||
type CurrentReader = UniversalReader | PDFReader | ComicReader | MangaReader;
|
type CurrentReader = UniversalReader | PDFReader | ComicReader | MangaReader;
|
||||||
|
|
||||||
let currentReader: CurrentReader | null = null;
|
let currentReader: CurrentReader | null = null;
|
||||||
let readerMetadata: import("../types/reader").ReaderMetadata | null = null;
|
let readerMetadata: ReaderMetadata | null = null;
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Feature Registry
|
// Feature Registry
|
||||||
@@ -90,41 +89,62 @@ async function initializeFeatures(context: ReaderContext): Promise<void> {
|
|||||||
|
|
||||||
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;
|
console.log("Initializing reader for:", mediaItemId);
|
||||||
|
if (!mediaItemId) {
|
||||||
readerMetadata = await getReaderMetadata(mediaItemId);
|
console.error("No mediaItemId on body");
|
||||||
|
return;
|
||||||
switch (readerMetadata.library_type) {
|
|
||||||
case "ebook":
|
|
||||||
currentReader = await initializeEbookReader(readerMetadata);
|
|
||||||
break;
|
|
||||||
case "pdf":
|
|
||||||
currentReader = await initializePDFReader(readerMetadata);
|
|
||||||
break;
|
|
||||||
case "comic":
|
|
||||||
currentReader = await initializeComicReader(readerMetadata);
|
|
||||||
break;
|
|
||||||
case "manga":
|
|
||||||
currentReader = await initializeMangaReader(readerMetadata);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentReader) 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;
|
||||||
|
}
|
||||||
|
|
||||||
setState({ currentReader, readerMetadata });
|
if (!currentReader) return;
|
||||||
|
|
||||||
const context = createReaderContext(
|
setState({ currentReader, readerMetadata });
|
||||||
getState,
|
|
||||||
setState,
|
|
||||||
createNavigationAPI(),
|
|
||||||
() =>
|
|
||||||
(context.render = () => {
|
|
||||||
// Delegates to navigation module
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await initializeFeatures(context);
|
const context = createReaderContext(
|
||||||
readerEvents.emit("readerReady", currentReader);
|
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> {
|
async function initializeEbookReader(metadata: any): Promise<UniversalReader> {
|
||||||
@@ -196,7 +216,7 @@ function getFileExtension(filepath: string): string {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
Alpine.data("readerShell", () => ({
|
Alpine.data("readerShell", () => ({
|
||||||
init() {
|
initReader() {
|
||||||
initializeReader();
|
initializeReader();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user