- 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
143 lines
4.0 KiB
TypeScript
143 lines
4.0 KiB
TypeScript
// Navigator panel - shows full page with draggable viewport box
|
|
// Affinity/Photoshop-style mini-map for page navigation
|
|
// Feature Registration Pattern implementation
|
|
|
|
import type { ReaderContext } from "../core/reader-context";
|
|
|
|
let context: ReaderContext;
|
|
|
|
export function init(readerContext: ReaderContext): void {
|
|
context = readerContext;
|
|
let state: NavigatorState | null = null;
|
|
|
|
context.events.on(
|
|
"navigator:initialize",
|
|
(detail: { containerSelector: string; totalPages: number }) => {
|
|
state = initializeNavigator(detail.containerSelector);
|
|
state.totalPages = detail.totalPages;
|
|
},
|
|
);
|
|
|
|
context.events.on(
|
|
"navigator:update",
|
|
(detail: { currentPage: number; contentImage?: HTMLImageElement }) => {
|
|
if (state) {
|
|
state.currentPage = detail.currentPage;
|
|
if (detail.contentImage) {
|
|
state.contentImage = detail.contentImage;
|
|
updateNavigatorViewport(state);
|
|
}
|
|
}
|
|
},
|
|
);
|
|
|
|
context.events.on("navigator:pan", (detail: { x: number; y: number }) => {
|
|
if (state) {
|
|
handleNavigatorPan(state, detail.x, detail.y);
|
|
}
|
|
});
|
|
}
|
|
|
|
interface NavigatorState {
|
|
panelId: string;
|
|
container: HTMLElement;
|
|
viewport: HTMLElement;
|
|
currentPage: number;
|
|
totalPages: number;
|
|
scale: number;
|
|
contentImage: HTMLImageElement | null;
|
|
isDragging: boolean;
|
|
}
|
|
|
|
function initializeNavigator(containerSelector: string): NavigatorState {
|
|
const container = document.querySelector(containerSelector) as HTMLElement;
|
|
if (!container) throw new Error("Navigator container not found");
|
|
|
|
const viewport = document.createElement("div");
|
|
viewport.className = "navigator-viewport-box";
|
|
viewport.style.cssText = `
|
|
position: absolute;
|
|
border: 2px solid var(--accent-color, #3b82f6);
|
|
background: rgba(59, 130, 246, 0.1);
|
|
cursor: move;
|
|
z-index: 10;
|
|
`;
|
|
|
|
container.appendChild(viewport);
|
|
|
|
const state: NavigatorState = {
|
|
panelId: "navigator",
|
|
container,
|
|
viewport,
|
|
currentPage: 1,
|
|
totalPages: 1,
|
|
scale: 0.1,
|
|
contentImage: null,
|
|
isDragging: false,
|
|
};
|
|
|
|
setupNavigatorDragHandler(state);
|
|
return state;
|
|
}
|
|
|
|
function setupNavigatorDragHandler(state: NavigatorState): void {
|
|
state.viewport.addEventListener("mousedown", (e) => {
|
|
e.preventDefault();
|
|
state.isDragging = true;
|
|
state.viewport.style.cursor = "grabbing";
|
|
});
|
|
|
|
document.addEventListener("mousemove", (e) => {
|
|
if (!state.isDragging || !state.contentImage) return;
|
|
|
|
const containerRect = state.container.getBoundingClientRect();
|
|
const imgRect = state.contentImage.getBoundingClientRect();
|
|
|
|
const relX = (e.clientX - imgRect.left) / imgRect.width;
|
|
const relY = (e.clientY - imgRect.top) / imgRect.height;
|
|
|
|
const mainViewer = document.getElementById("reader-content");
|
|
if (mainViewer) {
|
|
mainViewer.dispatchEvent(
|
|
new CustomEvent("navigator-pan", {
|
|
detail: { x: relX, y: relY },
|
|
}),
|
|
);
|
|
}
|
|
});
|
|
|
|
document.addEventListener("mouseup", () => {
|
|
if (state.isDragging) {
|
|
state.isDragging = false;
|
|
state.viewport.style.cursor = "move";
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateNavigatorViewport(state: NavigatorState): void {
|
|
if (!state.contentImage) return;
|
|
|
|
const containerRect = state.container.getBoundingClientRect();
|
|
const imgRect = state.contentImage.getBoundingClientRect();
|
|
|
|
state.scale = containerRect.width / imgRect.width;
|
|
|
|
state.viewport.style.width = `${containerRect.width}px`;
|
|
state.viewport.style.height = `${containerRect.height * state.scale}px`;
|
|
|
|
state.viewport.style.left = "0";
|
|
state.viewport.style.top = `${(state.currentPage - 1) * imgRect.height * state.scale}px`;
|
|
}
|
|
|
|
function handleNavigatorPan(state: NavigatorState, x: number, y: number): void {
|
|
if (!state.contentImage) return;
|
|
|
|
const imgRect = state.contentImage.getBoundingClientRect();
|
|
|
|
const viewportX = x * state.container.offsetWidth;
|
|
const viewportY = y * state.container.offsetHeight;
|
|
|
|
state.viewport.style.left = `${viewportX}px`;
|
|
state.viewport.style.top = `${viewportY}px`;
|
|
}
|