refactor(reader): create modular format-specific architecture
Implement complete modularization of reader code by separating format-specific functionality into dedicated modules. This replaces the monolithic structure with a clean, maintainable architecture that separates concerns by format type. ## New Architecture ### Format-Specific Modules - **formats/reflowable/**: EPUB, FB2, TXT, HTML (page-based pagination) - types.ts: Shared type definitions for reflowable formats - page-calculator.ts: Word-count based pagination with HTML slicing - navigation.ts: Page-based navigation logic - progress-tracker.ts: CFI-based progress tracking - content-renderer.ts: DOM rendering for page content - parser.ts: Unified parser interface for all reflowable formats - ebook/**: Migrated ebook-specific features - **formats/pdf/**: PDF format support - Core PDF functionality (navigation, text selection, annotations) - Advanced features (bookmarks, search, outlines, dual-page) - Page cache and rendering optimizations - **formats/comic/**: Comic format support - Background color, chapter markers, page caching - Page ordering, gap adjustments - **formats/manga/**: Manga format support - RTL navigation, vertical scrolling, reading direction ## Key Improvements 1. **Separation of Concerns**: Each format has its own dedicated module 2. **No Circular Dependencies**: Clean import structure 3. **Type Safety**: Comprehensive TypeScript types throughout 4. **Functional Programming**: Pure functions, no OOP complexity 5. **Scalability**: Easy to add new formats without touching core code ## Migration Path - Old format-specific code in reader/, ebook/, pdf/, comic/, manga/ - New code in formats/[format]/ structure - Maintains backward compatibility during transition - Core reader logic remains format-agnostic This change enables the implementation of page-based pagination for reflowable formats while keeping PDF, comic, and manga functionality unchanged.
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
// Adjustable panel gap controls
|
||||
// Feature Registration Pattern implementation
|
||||
|
||||
import type { ReaderContext } from "../core/reader-context";
|
||||
|
||||
export function init(context: ReaderContext): void {
|
||||
const state = createPanelGapState();
|
||||
applyPanelGap(state.gapSize, state.showBorders);
|
||||
|
||||
context.events.on("panel-gap:set", (detail: { gap: number }) => {
|
||||
setPanelGap(state, detail.gap);
|
||||
});
|
||||
|
||||
context.events.on("panel-gap:increase", (detail?: { amount: number }) => {
|
||||
increasePanelGap(state, detail?.amount);
|
||||
});
|
||||
|
||||
context.events.on("panel-gap:decrease", (detail?: { amount: number }) => {
|
||||
decreasePanelGap(state, detail?.amount);
|
||||
});
|
||||
|
||||
context.events.on("panel-gap:borders:toggle", () => {
|
||||
togglePanelBorders(state);
|
||||
});
|
||||
|
||||
context.events.on("ui:show-settings", (detail: { container: HTMLElement }) => {
|
||||
renderPanelGapControls(detail.container, state);
|
||||
});
|
||||
|
||||
context.events.on("reader:unload", () => {
|
||||
const controls = document.querySelector(".panel-gap-controls");
|
||||
controls?.remove();
|
||||
});
|
||||
}
|
||||
|
||||
interface PanelGapState {
|
||||
gapSize: number;
|
||||
showBorders: boolean;
|
||||
}
|
||||
|
||||
function createPanelGapState(initialGap: number = 4): PanelGapState {
|
||||
const saved = localStorage.getItem("reader-panel-gap");
|
||||
return {
|
||||
gapSize: saved ? parseInt(saved) : initialGap,
|
||||
showBorders: false,
|
||||
};
|
||||
}
|
||||
|
||||
function applyPanelGap(gap: number, showBorders: boolean): void {
|
||||
document.documentElement.style.setProperty("--panel-gap", `${gap}px`);
|
||||
document.documentElement.style.setProperty(
|
||||
"--panel-border-width",
|
||||
showBorders ? "1px" : "0px",
|
||||
);
|
||||
localStorage.setItem("reader-panel-gap", String(gap));
|
||||
}
|
||||
|
||||
function setPanelGap(state: PanelGapState, gap: number): PanelGapState {
|
||||
const clampedGap = Math.max(0, Math.min(20, gap));
|
||||
state.gapSize = clampedGap;
|
||||
|
||||
document.documentElement.style.setProperty("--panel-gap", `${clampedGap}px`);
|
||||
localStorage.setItem("reader-panel-gap", String(clampedGap));
|
||||
|
||||
const controls = document.querySelector(".panel-gap-controls");
|
||||
if (controls) {
|
||||
updatePanelGapUI(controls as HTMLElement, state);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function increasePanelGap(state: PanelGapState, amount: number = 2): PanelGapState {
|
||||
return setPanelGap(state, state.gapSize + amount);
|
||||
}
|
||||
|
||||
function decreasePanelGap(state: PanelGapState, amount: number = 2): PanelGapState {
|
||||
return setPanelGap(state, state.gapSize - amount);
|
||||
}
|
||||
|
||||
function togglePanelBorders(state: PanelGapState): PanelGapState {
|
||||
state.showBorders = !state.showBorders;
|
||||
|
||||
document.documentElement.style.setProperty(
|
||||
"--panel-border-width",
|
||||
state.showBorders ? "1px" : "0px",
|
||||
);
|
||||
|
||||
const controls = document.querySelector(".panel-gap-controls");
|
||||
if (controls) {
|
||||
updatePanelGapUI(controls as HTMLElement, state);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function renderPanelGapControls(
|
||||
container: HTMLElement,
|
||||
state: PanelGapState,
|
||||
): void {
|
||||
const existing = container.querySelector(".panel-gap-controls");
|
||||
existing?.remove();
|
||||
|
||||
const controls = document.createElement("div");
|
||||
controls.className =
|
||||
"panel-gap-controls fixed bottom-24 right-4 bg-gray-900 bg-opacity-90 rounded-lg p-2 flex flex-col gap-2 z-40";
|
||||
controls.innerHTML = `
|
||||
<button class="panel-gap-increase p-2 hover:bg-gray-700 rounded" title="Increase gap">+</button>
|
||||
<span class="text-center text-sm">${state.gapSize}px</span>
|
||||
<button class="panel-gap-decrease p-2 hover:bg-gray-700 rounded" title="Decrease gap">-</button>
|
||||
<button class="panel-gap-borders p-2 hover:bg-gray-700 rounded" title="Toggle borders">
|
||||
${state.showBorders ? "▦" : "▢"}
|
||||
</button>
|
||||
`;
|
||||
|
||||
controls
|
||||
.querySelector(".panel-gap-increase")
|
||||
?.addEventListener("click", () => {
|
||||
increasePanelGap(state);
|
||||
updatePanelGapUI(controls, state);
|
||||
});
|
||||
|
||||
controls
|
||||
.querySelector(".panel-gap-decrease")
|
||||
?.addEventListener("click", () => {
|
||||
decreasePanelGap(state);
|
||||
updatePanelGapUI(controls, state);
|
||||
});
|
||||
|
||||
controls
|
||||
.querySelector(".panel-gap-borders")
|
||||
?.addEventListener("click", () => {
|
||||
togglePanelBorders(state);
|
||||
updatePanelGapUI(controls, state);
|
||||
});
|
||||
|
||||
container.appendChild(controls);
|
||||
}
|
||||
|
||||
function updatePanelGapUI(container: HTMLElement, state: PanelGapState): void {
|
||||
const gapLabel = container.querySelector("span");
|
||||
if (gapLabel) {
|
||||
gapLabel.textContent = `${state.gapSize}px`;
|
||||
}
|
||||
|
||||
const bordersBtn = container.querySelector(".panel-gap-borders");
|
||||
if (bordersBtn) {
|
||||
bordersBtn.textContent = state.showBorders ? "▦" : "▢";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user