feat(reader): implement gestures and keyboard-shortcuts as feature modules
- Move gestures.ts to features/ with init(context) pattern - Move keyboard-shortcuts.ts to features/ with init(context) pattern - Remove callback-based architecture - Features now subscribe to events via ReaderContext - Support touch gestures (swipe, tap, double-tap, pinch-to-zoom) - Support keyboard shortcuts (navigation, zoom, fullscreen, bookmarks) - Add panel-aware navigation for comics/manga - Keyboard shortcuts include chapter navigation
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import type { ReaderContext } from "../core/reader-context";
|
||||
interface ProgressDisplay {
|
||||
mode: "pages" | "chapter" | "percentage" | "time-left";
|
||||
text: string;
|
||||
}
|
||||
export async function init(context: ReaderContext): Promise<void> {
|
||||
context.events.on("pageChanged", () => {
|
||||
updateProgressDisplay(context);
|
||||
});
|
||||
context.events.on("progressUpdated", () => {
|
||||
updateProgressDisplay(context);
|
||||
});
|
||||
setupProgressModeCycling(context);
|
||||
}
|
||||
export { calculateProgress, cycleProgressMode };
|
||||
export type { ProgressDisplay };
|
||||
function calculateProgress(
|
||||
currentPage: number,
|
||||
totalPages: number,
|
||||
currentChapterPage: number,
|
||||
chapterPages: number,
|
||||
readingSpeed?: ReadingSpeed,
|
||||
): ProgressDisplay {
|
||||
const mode = getCurrentProgressMode();
|
||||
switch (mode) {
|
||||
case "pages":
|
||||
return {
|
||||
mode: "pages",
|
||||
text: `${currentPage}/${totalPages}`,
|
||||
};
|
||||
case "chapter":
|
||||
return {
|
||||
mode: "chapter",
|
||||
text: `${currentChapterPage}/${chapterPages}`,
|
||||
};
|
||||
case "percentage":
|
||||
const percentage = Math.round((currentPage / totalPages) * 100);
|
||||
return {
|
||||
mode: "percentage",
|
||||
text: `${percentage}%`,
|
||||
};
|
||||
case "time-left":
|
||||
if (!readingSpeed || readingSpeed.pages_per_minute === 0) {
|
||||
return { mode: "time-left", text: "--:--" };
|
||||
}
|
||||
const pagesLeft = totalPages - currentPage;
|
||||
const minutesLeft = pagesLeft / readingSpeed.pages_per_minute;
|
||||
const hours = Math.floor(minutesLeft / 60);
|
||||
const mins = Math.round(minutesLeft % 60);
|
||||
return {
|
||||
mode: "time-left",
|
||||
text: `${hours}h ${mins}m`,
|
||||
};
|
||||
default:
|
||||
return { mode: "pages", text: `${currentPage}/${totalPages}` };
|
||||
}
|
||||
}
|
||||
function cycleProgressMode(): void {
|
||||
const modes: Array<"pages" | "chapter" | "percentage" | "time-left"> = [
|
||||
"pages",
|
||||
"chapter",
|
||||
"percentage",
|
||||
"time-left",
|
||||
];
|
||||
const currentMode = getCurrentProgressMode();
|
||||
const currentIndex = modes.indexOf(currentMode);
|
||||
const nextMode = modes[(currentIndex + 1) % modes.length];
|
||||
setProgressMode(nextMode);
|
||||
}
|
||||
function getCurrentProgressMode(): ReaderSettings["progress_mode"] {
|
||||
const settingsStr = localStorage.getItem("reader_settings_progress_mode");
|
||||
if (settingsStr) {
|
||||
try {
|
||||
return JSON.parse(settingsStr);
|
||||
} catch {
|
||||
return "pages";
|
||||
}
|
||||
}
|
||||
return "pages";
|
||||
}
|
||||
function setProgressMode(mode: ReaderSettings["progress_mode"]): void {
|
||||
localStorage.setItem("reader_settings_progress_mode", JSON.stringify(mode));
|
||||
|
||||
const display = document.getElementById("progress-display");
|
||||
if (display) {
|
||||
display.dataset.mode = mode;
|
||||
}
|
||||
}
|
||||
async function getReadingSpeed(): Promise<ReadingSpeed | null> {
|
||||
const speedStr = localStorage.getItem("reader_speed_data");
|
||||
if (speedStr) {
|
||||
try {
|
||||
return JSON.parse(speedStr);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function updateProgressDisplay(context: ReaderContext): void {
|
||||
const state = context.getState();
|
||||
const display = context.elements.progressDisplay;
|
||||
if (!display || !state.currentReader) return;
|
||||
let currentPage = 0;
|
||||
let totalPages = 0;
|
||||
if (state.currentReader.type === "ebook") {
|
||||
currentPage = state.currentReader.currentSpineIndex + 1;
|
||||
totalPages = state.currentReader.cif.spine.length;
|
||||
} else if (state.currentReader.type === "pdf") {
|
||||
currentPage = state.currentReader.currentPage;
|
||||
totalPages = state.readerMetadata?.total_pages || 0;
|
||||
} else {
|
||||
currentPage = state.currentReader.currentPage;
|
||||
totalPages = state.currentReader.images.length;
|
||||
}
|
||||
getReadingSpeed().then((speed) => {
|
||||
const result = calculateProgress(
|
||||
currentPage,
|
||||
totalPages,
|
||||
0,
|
||||
0,
|
||||
speed || undefined,
|
||||
);
|
||||
display.textContent = result.text;
|
||||
display.dataset.mode = result.mode;
|
||||
});
|
||||
}
|
||||
function setupProgressModeCycling(context: ReaderContext): void {
|
||||
const display = context.elements.progressDisplay;
|
||||
if (!display) return;
|
||||
display.style.cursor = "pointer";
|
||||
display.addEventListener("click", () => {
|
||||
cycleProgressMode();
|
||||
updateProgressDisplay(context);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user