reader: convert feature modules to Feature Registration Pattern

- Add init(context) exported function to each feature module
- Store module-level context reference for event handlers
- Fix gestures.ts to use library_type_name instead of library_type
- Features: gestures, keyboard-shortcuts, navigator-panel, offline-manager,
  panel-dock-system, progress-indicator, reading-speed-tracker
This commit is contained in:
2026-04-04 18:17:16 -04:00
parent 4abc814c1b
commit 2b22d7b255
7 changed files with 96 additions and 40 deletions
+2 -2
View File
@@ -101,7 +101,7 @@ function setupGestures() {
function handleSwipeLeft() { function handleSwipeLeft() {
const metadata = context.getState().readerMetadata; const metadata = context.getState().readerMetadata;
if (metadata?.library_type === "manga") { if (metadata?.library_type_name === "manga") {
navigateWithPanels("next"); navigateWithPanels("next");
} else { } else {
context.navigation.previousPage(); context.navigation.previousPage();
@@ -110,7 +110,7 @@ function handleSwipeLeft() {
function handleSwipeRight() { function handleSwipeRight() {
const metadata = context.getState().readerMetadata; const metadata = context.getState().readerMetadata;
if (metadata?.library_type === "manga") { if (metadata?.library_type_name === "manga") {
navigateWithPanels("previous"); navigateWithPanels("previous");
} else { } else {
context.navigation.nextPage(); context.navigation.nextPage();
@@ -1,6 +1,9 @@
import type { ReaderContext } from "../core/reader-context"; import type { ReaderContext } from "../core/reader-context";
export async function init(context: ReaderContext): Promise<void> { let context: ReaderContext;
export async function init(readerContext: ReaderContext): Promise<void> {
context = readerContext;
setupKeyboardShortcuts(); setupKeyboardShortcuts();
} }
+24 -15
View File
@@ -4,23 +4,32 @@
import type { ReaderContext } from "../core/reader-context"; import type { ReaderContext } from "../core/reader-context";
export function init(context: ReaderContext): void { let context: ReaderContext;
export function init(readerContext: ReaderContext): void {
context = readerContext;
let state: NavigatorState | null = null; let state: NavigatorState | null = null;
context.events.on("navigator:initialize", (detail: { containerSelector: string; totalPages: number }) => { context.events.on(
state = initializeNavigator(detail.containerSelector); "navigator:initialize",
state.totalPages = detail.totalPages; (detail: { containerSelector: string; totalPages: number }) => {
}); state = initializeNavigator(detail.containerSelector);
state.totalPages = detail.totalPages;
},
);
context.events.on("navigator:update", (detail: { currentPage: number; contentImage?: HTMLImageElement }) => { context.events.on(
if (state) { "navigator:update",
state.currentPage = detail.currentPage; (detail: { currentPage: number; contentImage?: HTMLImageElement }) => {
if (detail.contentImage) { if (state) {
state.contentImage = detail.contentImage; state.currentPage = detail.currentPage;
updateNavigatorViewport(state); if (detail.contentImage) {
state.contentImage = detail.contentImage;
updateNavigatorViewport(state);
}
} }
} },
}); );
context.events.on("navigator:pan", (detail: { x: number; y: number }) => { context.events.on("navigator:pan", (detail: { x: number; y: number }) => {
if (state) { if (state) {
@@ -124,10 +133,10 @@ function handleNavigatorPan(state: NavigatorState, x: number, y: number): void {
if (!state.contentImage) return; if (!state.contentImage) return;
const imgRect = state.contentImage.getBoundingClientRect(); const imgRect = state.contentImage.getBoundingClientRect();
const viewportX = x * state.container.offsetWidth; const viewportX = x * state.container.offsetWidth;
const viewportY = y * state.container.offsetHeight; const viewportY = y * state.container.offsetHeight;
state.viewport.style.left = `${viewportX}px`; state.viewport.style.left = `${viewportX}px`;
state.viewport.style.top = `${viewportY}px`; state.viewport.style.top = `${viewportY}px`;
} }
+6 -3
View File
@@ -1,9 +1,12 @@
// Offline manager for PWA functionality // Offline manager for PWA functionality
// Feature Registration Pattern implementation // Feature Registration Pattern implementation
import type { ReaderContext } from "./core/reader-context"; import type { ReaderContext } from "../core/reader-context";
export function init(context: ReaderContext): void { let context: ReaderContext;
export function init(readerContext: ReaderContext): void {
context = readerContext;
registerServiceWorker(); registerServiceWorker();
window.addEventListener("online", () => { window.addEventListener("online", () => {
@@ -46,4 +49,4 @@ export function checkOnlineStatus(): boolean {
function syncPendingChanges(context: ReaderContext): void { function syncPendingChanges(context: ReaderContext): void {
context.events.emit("offline:sync", {}); context.events.emit("offline:sync", {});
} }
+49 -15
View File
@@ -3,7 +3,10 @@
import type { ReaderContext } from "../core/reader-context"; import type { ReaderContext } from "../core/reader-context";
export function init(context: ReaderContext): void { let context: ReaderContext;
export function init(readerContext: ReaderContext): void {
context = readerContext;
const dockZones: DockZone[] = [ const dockZones: DockZone[] = [
{ side: "left", x: 0, width: 400, height: window.innerHeight }, { side: "left", x: 0, width: 400, height: window.innerHeight },
{ {
@@ -45,9 +48,12 @@ export function init(context: ReaderContext): void {
} }
}); });
context.events.on("panel:move", (detail: { panelId: string; side: "left" | "right" }) => { context.events.on(
movePanelToSide(context, state, detail.panelId, detail.side); "panel:move",
}); (detail: { panelId: string; side: "left" | "right" }) => {
movePanelToSide(context, state, detail.panelId, detail.side);
},
);
} }
interface PanelDockState { interface PanelDockState {
@@ -88,39 +94,54 @@ async function loadSettings(): Promise<any> {
return await response.json(); return await response.json();
} }
function createPanel(context: ReaderContext, panelId: string, panelState: PanelState): void { function createPanel(
context: ReaderContext,
panelId: string,
panelState: PanelState,
): void {
const panel = document.createElement("div"); const panel = document.createElement("div");
panel.id = `panel-${panelId}`; panel.id = `panel-${panelId}`;
panel.className = `reader-panel panel-${panelState.side}`; panel.className = `reader-panel panel-${panelState.side}`;
panel.dataset.panelId = panelId; panel.dataset.panelId = panelId;
panel.style.width = `${panelState.width_px}px`; panel.style.width = `${panelState.width_px}px`;
if (!panelState.visible) { if (!panelState.visible) {
panel.classList.add("panel-hidden"); panel.classList.add("panel-hidden");
} }
if (panelState.collapsed) { if (panelState.collapsed) {
panel.classList.add("panel-collapsed"); panel.classList.add("panel-collapsed");
} }
context.elements.readerContent.appendChild(panel); context.elements.readerContent.appendChild(panel);
} }
function updatePanelVisibility(context: ReaderContext, panelId: string, visible: boolean): void { function updatePanelVisibility(
context: ReaderContext,
panelId: string,
visible: boolean,
): void {
const panel = document.getElementById(`panel-${panelId}`); const panel = document.getElementById(`panel-${panelId}`);
if (panel) { if (panel) {
panel.classList.toggle("panel-hidden", !visible); panel.classList.toggle("panel-hidden", !visible);
} }
} }
function updatePanelCollapsed(context: ReaderContext, panelId: string, collapsed: boolean): void { function updatePanelCollapsed(
context: ReaderContext,
panelId: string,
collapsed: boolean,
): void {
const panel = document.getElementById(`panel-${panelId}`); const panel = document.getElementById(`panel-${panelId}`);
if (panel) { if (panel) {
panel.classList.toggle("panel-collapsed", collapsed); panel.classList.toggle("panel-collapsed", collapsed);
} }
} }
function setupGlobalDragHandlers(context: ReaderContext, state: PanelDockState): void { function setupGlobalDragHandlers(
context: ReaderContext,
state: PanelDockState,
): void {
document.addEventListener("mousedown", (e) => { document.addEventListener("mousedown", (e) => {
const panelHeader = (e.target as HTMLElement).closest(".panel-header"); const panelHeader = (e.target as HTMLElement).closest(".panel-header");
if (panelHeader) { if (panelHeader) {
@@ -149,9 +170,17 @@ function setupGlobalDragHandlers(context: ReaderContext, state: PanelDockState):
document.addEventListener("mouseup", () => { document.addEventListener("mouseup", () => {
if (state.dragState) { if (state.dragState) {
const nearestZone = findNearestDockZone(state.dragState.currentX, state.dockZones); const nearestZone = findNearestDockZone(
state.dragState.currentX,
state.dockZones,
);
if (nearestZone) { if (nearestZone) {
movePanelToSide(context, state, state.dragState.panelId, nearestZone.side); movePanelToSide(
context,
state,
state.dragState.panelId,
nearestZone.side,
);
} }
state.dragState = null; state.dragState = null;
} }
@@ -173,7 +202,12 @@ function findNearestDockZone(x: number, zones: DockZone[]): DockZone | null {
return nearest; return nearest;
} }
function movePanelToSide(context: ReaderContext, state: PanelDockState, panelId: string, side: "left" | "right"): void { function movePanelToSide(
context: ReaderContext,
state: PanelDockState,
panelId: string,
side: "left" | "right",
): void {
const panelState = state.panels.get(panelId); const panelState = state.panels.get(panelId);
if (!panelState) return; if (!panelState) return;
@@ -187,4 +221,4 @@ function movePanelToSide(context: ReaderContext, state: PanelDockState, panelId:
} }
context.events.emit("panel:moved", { panelId, side }); context.events.emit("panel:moved", { panelId, side });
} }
@@ -3,7 +3,11 @@ interface ProgressDisplay {
mode: "pages" | "chapter" | "percentage" | "time-left"; mode: "pages" | "chapter" | "percentage" | "time-left";
text: string; text: string;
} }
export async function init(context: ReaderContext): Promise<void> {
let context: ReaderContext;
export async function init(readerContext: ReaderContext): Promise<void> {
context = readerContext;
context.events.on("pageChanged", () => { context.events.on("pageChanged", () => {
updateProgressDisplay(context); updateProgressDisplay(context);
}); });
@@ -1,9 +1,12 @@
// Track reading speed and update database // Track reading speed and update database
// Feature Registration Pattern implementation // Feature Registration Pattern implementation
import type { ReaderContext } from "./core/reader-context"; import type { ReaderContext } from "../core/reader-context";
export function init(context: ReaderContext): void { let context: ReaderContext;
export function init(readerContext: ReaderContext): void {
context = readerContext;
let state: ReadingSpeedTrackerState | null = null; let state: ReadingSpeedTrackerState | null = null;
context.events.on("reader:loaded", (detail: { mediaItemId: string }) => { context.events.on("reader:loaded", (detail: { mediaItemId: string }) => {
@@ -122,4 +125,4 @@ async function syncReadingSpeed(
} catch (error) { } catch (error) {
console.error("Failed to sync reading speed:", error); console.error("Failed to sync reading speed:", error);
} }
} }