Files
bookhoard/web/src/reader/ui/keyboard-shortcuts.ts
T
john-okeefe d65ac1362c refactor(reader): create format-agnostic UI component layer
Extract UI components from format-specific code into dedicated ui/ directory.
This creates a clean separation between functionality and presentation, making
it easier to maintain and share UI elements across different reader formats.

## New UI Components

### Core UI Elements
- **gestures.ts**: Touch and mouse gesture handling
- **keyboard-shortcuts.ts**: Keyboard navigation and shortcuts
- **navigator-panel.ts**: Table of contents and navigation panel
- **offline-manager.ts**: Offline reading support and caching

### Progress Display
- **progress-indicator.ts**: Reading progress bar and percentage
- **reading-speed-tracker.ts**: Words per minute calculation
- **page-display.ts**: Current page / total pages display

### Panel System
- **panel-dock-system.ts**: Draggable, resizable panel dock interface

## Architecture Benefits

1. **Format Independence**: UI components work with any format
2. **Reusability**: Same components work for PDF, EPUB, comic, manga
3. **Maintainability**: UI logic separated from format-specific code
4. **Testability**: UI can be tested independently of readers
5. **Consistency**: Uniform UX across all format types

## Migration Notes

- Moved from reader/features/ to reader/ui/
- Components use ReaderContext interface for format-agnostic access
- Maintains all existing functionality during transition
- Feature registration pattern preserved for backward compatibility

This creates the foundation for a unified user interface that works seamlessly
across all reader format types while maintaining format-specific flexibility.
2026-04-09 14:53:22 -04:00

211 lines
5.8 KiB
TypeScript

import type { ReaderContext } from "../core/reader-context";
let context: ReaderContext;
export async function init(readerContext: ReaderContext): Promise<void> {
context = readerContext;
setupKeyboardShortcuts();
}
function setupKeyboardShortcuts() {
const container = context.elements.readerContent;
if (!container) return;
const state = context.getState();
let maxPage = 0;
if (state.currentReader?.type === "ebook") {
maxPage = state.currentReader.cif.spine.length;
} else if (state.currentReader?.type === "pdf") {
maxPage = state.readerMetadata?.total_pages || 0;
} else if (
state.currentReader?.type === "comic" ||
state.currentReader?.type === "manga"
) {
const reader = state.currentReader as any;
maxPage = reader.images.length;
}
container.addEventListener("keydown", (e) => {
if (
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement
) {
return;
}
switch (e.key) {
case "ArrowRight":
case "PageDown":
case "l":
e.preventDefault();
context.navigation.nextPage();
break;
case "ArrowLeft":
case "PageUp":
case "h":
e.preventDefault();
context.navigation.previousPage();
break;
case "ArrowUp":
case "k":
e.preventDefault();
context.navigation.previousPage();
break;
case "ArrowDown":
case "j":
e.preventDefault();
context.navigation.nextPage();
break;
case " ":
e.preventDefault();
context.navigation.nextPage();
break;
case "Home":
e.preventDefault();
context.navigation.goToPage(1);
break;
case "End":
e.preventDefault();
context.navigation.goToPage(maxPage);
break;
case "b":
if (!e.ctrlKey && !e.metaKey) {
e.preventDefault();
toggleBookmark();
}
break;
case "+":
case "=":
e.preventDefault();
zoomIn();
break;
case "-":
case "_":
e.preventDefault();
zoomOut();
break;
case "0":
e.preventDefault();
zoomReset();
break;
case "?":
e.preventDefault();
showShortcutHelp();
break;
case "f":
if (!e.ctrlKey && !e.metaKey) {
e.preventDefault();
toggleFullscreen();
}
break;
case "Escape":
e.preventDefault();
exitFullscreen();
break;
default:
if (e.key >= "1" && e.key <= "9") {
const targetPage = Math.floor((parseInt(e.key) / 10) * maxPage);
e.preventDefault();
context.navigation.goToPage(targetPage);
}
}
});
}
function toggleBookmark() {
// TODO: Implement bookmark toggle
console.log("Toggle bookmark");
}
function zoomIn() {
const container = context.elements.readerContent;
if (container) {
const currentTransform = container.style.transform || "";
const currentScale = currentTransform.match(/scale\(([\d.]+)\)/);
const scale = currentScale ? parseFloat(currentScale[1]) : 1;
const newScale = Math.min(scale + 0.25, 3);
container.style.transform = `scale(${newScale})`;
container.style.transformOrigin = "center center";
context.events.emit("zoomChanged", newScale);
}
}
function zoomOut() {
const container = context.elements.readerContent;
if (container) {
const currentTransform = container.style.transform || "";
const currentScale = currentTransform.match(/scale\(([\d.]+)\)/);
const scale = currentScale ? parseFloat(currentScale[1]) : 1;
const newScale = Math.max(scale - 0.25, 0.5);
container.style.transform = `scale(${newScale})`;
container.style.transformOrigin = "center center";
context.events.emit("zoomChanged", newScale);
}
}
function zoomReset() {
const container = context.elements.readerContent;
if (container) {
container.style.transform = "scale(1)";
container.style.transformOrigin = "center center";
context.events.emit("zoomChanged", 1);
}
}
function toggleFullscreen() {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
document.documentElement.requestFullscreen();
}
}
function exitFullscreen() {
if (document.fullscreenElement) {
document.exitFullscreen();
}
}
function showShortcutHelp() {
const help = document.createElement("div");
help.className =
"keyboard-shortcut-help fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50";
help.innerHTML = `
<div class="bg-gray-800 rounded-lg p-6 max-w-md">
<h2 class="text-xl font-bold mb-4">Keyboard Shortcuts</h2>
<div class="grid grid-cols-2 gap-4 text-sm">
<div><kbd class="bg-gray-700 px-2 py-1 rounded">→</kbd> / <kbd class="bg-gray-700 px-2 py-1 rounded">Space</kbd> Next page</div>
<div><kbd class="bg-gray-700 px-2 py-1 rounded">←</kbd> Previous page</div>
<div><kbd class="bg-gray-700 px-2 py-1 rounded">Home</kbd> First page</div>
<div><kbd class="bg-gray-700 px-2 py-1 rounded">End</kbd> Last page</div>
<div><kbd class="bg-gray-700 px-2 py-1 rounded">+</kbd> / <kbd class="bg-gray-700 px-2 py-1 rounded">-</kbd> Zoom</div>
<div><kbd class="bg-gray-700 px-2 py-1 rounded">B</kbd> Toggle bookmark</div>
<div><kbd class="bg-gray-700 px-2 py-1 rounded">?</kbd> Show help</div>
<div><kbd class="bg-gray-700 px-2 py-1 rounded">Esc</kbd> Exit fullscreen</div>
</div>
<button class="mt-4 px-4 py-2 bg-blue-600 rounded" onclick="this.closest('.keyboard-shortcut-help').remove()">
Close
</button>
</div>
`;
document.body.appendChild(help);
help.addEventListener("click", (e) => {
if (e.target === help) help.remove();
});
}