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();
} }
+14 -5
View File
@@ -4,15 +4,23 @@
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(
"navigator:initialize",
(detail: { containerSelector: string; totalPages: number }) => {
state = initializeNavigator(detail.containerSelector); state = initializeNavigator(detail.containerSelector);
state.totalPages = detail.totalPages; state.totalPages = detail.totalPages;
}); },
);
context.events.on("navigator:update", (detail: { currentPage: number; contentImage?: HTMLImageElement }) => { context.events.on(
"navigator:update",
(detail: { currentPage: number; contentImage?: HTMLImageElement }) => {
if (state) { if (state) {
state.currentPage = detail.currentPage; state.currentPage = detail.currentPage;
if (detail.contentImage) { if (detail.contentImage) {
@@ -20,7 +28,8 @@ export function init(context: ReaderContext): void {
updateNavigatorViewport(state); 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) {
+5 -2
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", () => {
+44 -10
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(
"panel:move",
(detail: { panelId: string; side: "left" | "right" }) => {
movePanelToSide(context, state, detail.panelId, detail.side); movePanelToSide(context, state, detail.panelId, detail.side);
}); },
);
} }
interface PanelDockState { interface PanelDockState {
@@ -88,7 +94,11 @@ 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}`;
@@ -106,21 +116,32 @@ function createPanel(context: ReaderContext, panelId: string, panelState: PanelS
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;
@@ -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 }) => {