refactor: Remove duplicate feature files from reader root
Delete old versions of features that are now in /reader/features/: - offline-manager.ts (moved to features/) - reading-speed-tracker.ts (moved to features/) - panel-dock-system.ts (replaced by converted version in features/) - navigator-panel.ts (replaced by converted version in features/) All imports now use the /reader/features/ directory. This eliminates duplication and makes it clear which files are the active ones.
This commit is contained in:
@@ -1,152 +0,0 @@
|
||||
// Navigator panel - shows full page with draggable viewport box
|
||||
// Affinity/Photoshop-style mini-map for page navigation
|
||||
// Procedural implementation (no OOP)
|
||||
|
||||
import { loadSettings } from "./settings-manager";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Setup draggable viewport box within navigator
|
||||
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();
|
||||
|
||||
// Calculate position relative to scaled image
|
||||
const relX = (e.clientX - imgRect.left) / imgRect.width;
|
||||
const relY = (e.clientY - imgRect.top) / imgRect.height;
|
||||
|
||||
// Update main viewer's position (call external handler)
|
||||
const mainViewer = document.getElementById("reader-content");
|
||||
if (mainViewer) {
|
||||
mainViewer.dataset.panX = relX.toString();
|
||||
mainViewer.dataset.panY = relY.toString();
|
||||
// Dispatch event for main viewer to handle
|
||||
mainViewer.dispatchEvent(
|
||||
new CustomEvent("navigator-pan", {
|
||||
detail: { x: relX, y: relY },
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("mouseup", () => {
|
||||
state.isDragging = false;
|
||||
state.viewport.style.cursor = "move";
|
||||
});
|
||||
}
|
||||
|
||||
// Update navigator with current page image
|
||||
async function updateNavigatorContent(
|
||||
state: NavigatorState,
|
||||
pageNumber: number,
|
||||
): Promise<void> {
|
||||
state.currentPage = pageNumber;
|
||||
|
||||
// Get current page image (from PDF viewer, comic reader, or manga reader)
|
||||
const contentArea = document.getElementById("reader-content");
|
||||
const img = contentArea?.querySelector("img, canvas") as
|
||||
| HTMLImageElement
|
||||
| HTMLCanvasElement
|
||||
| null;
|
||||
|
||||
if (!img) return;
|
||||
|
||||
// Create thumbnail version for navigator
|
||||
const thumb = document.createElement("img");
|
||||
thumb.src = img.src || (img as HTMLCanvasElement).toDataURL();
|
||||
thumb.style.cssText = `
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
// Clear and populate container
|
||||
state.container.innerHTML = "";
|
||||
state.container.appendChild(thumb);
|
||||
state.contentImage = thumb;
|
||||
|
||||
// Recreate viewport box
|
||||
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;
|
||||
width: ${100 / state.scale}%;
|
||||
height: ${100 / state.scale}%;
|
||||
`;
|
||||
state.container.appendChild(viewport);
|
||||
state.viewport = viewport;
|
||||
|
||||
// Re-attach drag handler
|
||||
setupNavigatorDragHandler(state);
|
||||
|
||||
// Calculate viewport size relative to container
|
||||
const containerRect = state.container.getBoundingClientRect();
|
||||
const viewportWidth = (containerRect.width / img.width) * 100;
|
||||
const viewportHeight = (containerRect.height / img.height) * 100;
|
||||
|
||||
viewport.style.width = `${viewportWidth}%`;
|
||||
viewport.style.height = `${viewportHeight}%`;
|
||||
}
|
||||
|
||||
// Handle window resize
|
||||
function handleNavigatorResize(state: NavigatorState): void {
|
||||
if (state.contentImage) {
|
||||
updateNavigatorContent(state, state.currentPage);
|
||||
}
|
||||
}
|
||||
|
||||
export { initializeNavigator, updateNavigatorContent, handleNavigatorResize };
|
||||
@@ -1,49 +0,0 @@
|
||||
// Offline manager for PWA functionality
|
||||
// Feature Registration Pattern implementation
|
||||
|
||||
import type { ReaderContext } from "./core/reader-context";
|
||||
|
||||
export function init(context: ReaderContext): void {
|
||||
registerServiceWorker();
|
||||
|
||||
window.addEventListener("online", () => {
|
||||
const isOnline = checkOnlineStatus();
|
||||
if (isOnline) {
|
||||
context.events.emit("offline:online", {});
|
||||
syncPendingChanges(context);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("offline", () => {
|
||||
context.events.emit("offline:offline", {});
|
||||
});
|
||||
|
||||
context.events.on("offline:check", () => {
|
||||
const isOnline = checkOnlineStatus();
|
||||
context.events.emit("offline:status", { isOnline });
|
||||
});
|
||||
}
|
||||
|
||||
export function registerServiceWorker(): void {
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker
|
||||
.register("/static/service-worker.js")
|
||||
.then((registration) => {
|
||||
console.log("Service worker registered:", registration);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Service worker registration failed:", error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function checkOnlineStatus(): boolean {
|
||||
if (typeof navigator !== "undefined" && navigator.onLine) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function syncPendingChanges(context: ReaderContext): void {
|
||||
context.events.emit("offline:sync", {});
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
// Modular dockable panel system - handles drag, lock, snap-back, window-shade
|
||||
// Procedural style: Functions, not classes
|
||||
|
||||
import {
|
||||
saveSettings,
|
||||
loadSettings,
|
||||
getDefaultSettings,
|
||||
} from "./settings-manager";
|
||||
|
||||
interface PanelDockState {
|
||||
panels: Map<string, PanelState>;
|
||||
dragState: DragState | null;
|
||||
dockZones: DockZone[];
|
||||
}
|
||||
|
||||
interface DragState {
|
||||
panelId: string;
|
||||
startX: number;
|
||||
startY: number;
|
||||
currentX: number;
|
||||
currentY: number;
|
||||
isLocked: boolean;
|
||||
}
|
||||
|
||||
interface DockZone {
|
||||
side: "left" | "right";
|
||||
x: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const state: PanelDockState = {
|
||||
panels: new Map(),
|
||||
dragState: null,
|
||||
dockZones: [
|
||||
{ side: "left", x: 0, width: 400, height: window.innerHeight },
|
||||
{
|
||||
side: "right",
|
||||
x: window.innerWidth - 400,
|
||||
width: 400,
|
||||
height: window.innerHeight,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Initialize all panels from settings
|
||||
function initializePanelDockSystem(): void {
|
||||
const settings = loadSettings();
|
||||
|
||||
for (const [panelId, panelState] of Object.entries(settings.panel_layout)) {
|
||||
registerPanel(panelId, panelState);
|
||||
}
|
||||
|
||||
setupDragHandlers();
|
||||
setupWindowShadeHandlers();
|
||||
setupLockHandlers();
|
||||
}
|
||||
|
||||
// Register a panel with the dock system
|
||||
function registerPanel(panelId: string, panelState: PanelState): void {
|
||||
state.panels.set(panelId, panelState);
|
||||
applyPanelState(panelId, panelState);
|
||||
}
|
||||
|
||||
// Apply panel state to DOM
|
||||
function applyPanelState(panelId: string, panelState: PanelState): void {
|
||||
const panel = document.querySelector(`[data-panel="${panelId}"]`);
|
||||
if (!panel) return;
|
||||
|
||||
const container = panel.parentElement;
|
||||
if (!container) return;
|
||||
|
||||
// Apply side positioning
|
||||
if (panelState.side === "left") {
|
||||
container.style.left = "0";
|
||||
container.style.right = "auto";
|
||||
} else if (panelState.side === "right") {
|
||||
container.style.right = "0";
|
||||
container.style.left = "auto";
|
||||
} else {
|
||||
container.style.left = "-9999px";
|
||||
}
|
||||
|
||||
// Apply width
|
||||
panel.style.width = `${panelState.width_px}px`;
|
||||
|
||||
// Apply collapsed (window-shade) state
|
||||
if (panelState.collapsed) {
|
||||
panel.classList.add("panel-collapsed");
|
||||
panel.querySelector(".panel-content")?.classList.add("hidden");
|
||||
} else {
|
||||
panel.classList.remove("panel-collapsed");
|
||||
panel.querySelector(".panel-content")?.classList.remove("hidden");
|
||||
}
|
||||
|
||||
// Apply lock state
|
||||
const lockBtn = panel.querySelector(".panel-lock");
|
||||
if (lockBtn) {
|
||||
lockBtn.textContent = panelState.locked ? "🔒" : "🔓";
|
||||
}
|
||||
}
|
||||
|
||||
// Setup mouse/touch drag handlers
|
||||
function setupDragHandlers(): void {
|
||||
document
|
||||
.querySelectorAll(".dockable-panel .panel-header")
|
||||
.forEach((header) => {
|
||||
header.addEventListener("mousedown", handleDragStart);
|
||||
header.addEventListener("touchstart", handleDragStart, {
|
||||
passive: false,
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("mousemove", handleDragMove);
|
||||
document.addEventListener("touchmove", handleDragMove, { passive: false });
|
||||
document.addEventListener("mouseup", handleDragEnd);
|
||||
document.addEventListener("touchend", handleDragEnd);
|
||||
}
|
||||
|
||||
function handleDragStart(e: MouseEvent | TouchEvent): void {
|
||||
const header = e.target.closest(".panel-header") as HTMLElement;
|
||||
const panel = header?.closest(".dockable-panel") as HTMLElement;
|
||||
if (!panel) return;
|
||||
|
||||
const panelId = panel.dataset.panel;
|
||||
const panelState = state.panels.get(panelId);
|
||||
|
||||
// Check if locked
|
||||
if (panelState?.locked) return;
|
||||
|
||||
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
|
||||
const clientY = "touches" in e ? e.touches[0].clientY : e.clientY;
|
||||
|
||||
state.dragState = {
|
||||
panelId: panelId!,
|
||||
startX: clientX,
|
||||
startY: clientY,
|
||||
currentX: clientX,
|
||||
currentY: clientY,
|
||||
isLocked: panelState?.locked || false,
|
||||
};
|
||||
|
||||
panel.classList.add("dragging");
|
||||
}
|
||||
|
||||
function handleDragMove(e: MouseEvent | TouchEvent): void {
|
||||
if (!state.dragState) return;
|
||||
|
||||
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
|
||||
const clientY = "touches" in e ? e.touches[0].clientY : e.clientY;
|
||||
|
||||
state.dragState.currentX = clientX;
|
||||
state.dragState.currentY = clientY;
|
||||
|
||||
const panel = document.querySelector(
|
||||
`[data-panel="${state.dragState.panelId}"]`,
|
||||
);
|
||||
const container = panel?.parentElement;
|
||||
if (container) {
|
||||
container.style.transform = `translateX(${clientX - state.dragState.startX}px)`;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragEnd(e: MouseEvent | TouchEvent): void {
|
||||
if (!state.dragState) return;
|
||||
|
||||
const { panelId, currentX } = state.dragState;
|
||||
const panel = document.querySelector(`[data-panel="${panelId}"]`);
|
||||
const container = panel?.parentElement;
|
||||
|
||||
// Reset transform
|
||||
container.style.transform = "";
|
||||
panel?.classList.remove("dragging");
|
||||
|
||||
// Determine drop zone
|
||||
const newSide = currentX < window.innerWidth / 2 ? "left" : "right";
|
||||
const panelState = state.panels.get(panelId);
|
||||
|
||||
if (panelState) {
|
||||
// Check if dropped in valid zone
|
||||
const isValidDrop = newSide === "left" || newSide === "right";
|
||||
|
||||
if (isValidDrop) {
|
||||
panelState.last_valid_side = panelState.side; // Save previous valid position
|
||||
panelState.side = newSide;
|
||||
} else {
|
||||
// Snap back to last valid position
|
||||
panelState.side = panelState.last_valid_side;
|
||||
}
|
||||
|
||||
applyPanelState(panelId, panelState);
|
||||
savePanelState(panelId, panelState);
|
||||
}
|
||||
|
||||
state.dragState = null;
|
||||
}
|
||||
|
||||
// Setup window-shade (collapse/expand) handlers
|
||||
function setupWindowShadeHandlers(): void {
|
||||
document.querySelectorAll(".window-shade-toggle").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
const panel = (e.target as HTMLElement).closest(
|
||||
".dockable-panel",
|
||||
) as HTMLElement;
|
||||
const panelId = panel.dataset.panel;
|
||||
const panelState = state.panels.get(panelId);
|
||||
|
||||
if (panelState) {
|
||||
panelState.collapsed = !panelState.collapsed;
|
||||
applyPanelState(panelId, panelState);
|
||||
savePanelState(panelId, panelState);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Setup lock toggle handlers
|
||||
function setupLockHandlers(): void {
|
||||
document.querySelectorAll(".panel-lock").forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
const panel = (e.target as HTMLElement).closest(
|
||||
".dockable-panel",
|
||||
) as HTMLElement;
|
||||
const panelId = panel.dataset.panel;
|
||||
const panelState = state.panels.get(panelId);
|
||||
|
||||
if (panelState) {
|
||||
panelState.locked = !panelState.locked;
|
||||
applyPanelState(panelId, panelState);
|
||||
savePanelState(panelId, panelState);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Persist panel state to settings
|
||||
async function savePanelState(
|
||||
panelId: string,
|
||||
panelState: PanelState,
|
||||
): Promise<void> {
|
||||
const settings = loadSettings();
|
||||
settings.panel_layout[panelId as keyof typeof settings.panel_layout] =
|
||||
panelState;
|
||||
await saveSettings(settings);
|
||||
}
|
||||
|
||||
export { initializePanelDockSystem, registerPanel, applyPanelState };
|
||||
@@ -1,125 +0,0 @@
|
||||
// Track reading speed and update database
|
||||
// Feature Registration Pattern implementation
|
||||
|
||||
import type { ReaderContext } from "./core/reader-context";
|
||||
|
||||
export function init(context: ReaderContext): void {
|
||||
let state: ReadingSpeedTrackerState | null = null;
|
||||
|
||||
context.events.on("reader:loaded", (detail: { mediaItemId: string }) => {
|
||||
state = createReadingSpeedTracker(detail.mediaItemId);
|
||||
});
|
||||
|
||||
context.events.on("reading-session:start", () => {
|
||||
if (state) {
|
||||
startReadingSession(state);
|
||||
}
|
||||
});
|
||||
|
||||
context.events.on("page-changed", () => {
|
||||
if (state) {
|
||||
recordPageTurn(state);
|
||||
}
|
||||
});
|
||||
|
||||
context.events.on("words-read", (detail: { wordCount: number }) => {
|
||||
if (state) {
|
||||
recordWordsRead(state, detail.wordCount);
|
||||
}
|
||||
});
|
||||
|
||||
context.events.on("reading-session:end", async () => {
|
||||
if (state) {
|
||||
await syncReadingSpeed(state);
|
||||
}
|
||||
});
|
||||
|
||||
context.events.on("reader:unload", async () => {
|
||||
if (state) {
|
||||
await syncReadingSpeed(state);
|
||||
state = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
interface ReadingSpeedTrackerState {
|
||||
startTime: number | null;
|
||||
pagesRead: number;
|
||||
wordsRead: number;
|
||||
lastSync: number;
|
||||
mediaItemId: string;
|
||||
}
|
||||
|
||||
function createReadingSpeedTracker(
|
||||
mediaItemId: string,
|
||||
): ReadingSpeedTrackerState {
|
||||
return {
|
||||
startTime: null,
|
||||
pagesRead: 0,
|
||||
wordsRead: 0,
|
||||
lastSync: Date.now(),
|
||||
mediaItemId,
|
||||
};
|
||||
}
|
||||
|
||||
function startReadingSession(
|
||||
state: ReadingSpeedTrackerState,
|
||||
): ReadingSpeedTrackerState {
|
||||
state.startTime = Date.now();
|
||||
state.pagesRead = 0;
|
||||
state.wordsRead = 0;
|
||||
return state;
|
||||
}
|
||||
|
||||
function recordPageTurn(
|
||||
state: ReadingSpeedTrackerState,
|
||||
): ReadingSpeedTrackerState {
|
||||
if (!state.startTime) return state;
|
||||
|
||||
state.pagesRead += 1;
|
||||
const now = Date.now();
|
||||
|
||||
if (state.pagesRead % 5 === 0 || now - state.lastSync > 5 * 60 * 1000) {
|
||||
syncReadingSpeed(state);
|
||||
state.lastSync = now;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function recordWordsRead(
|
||||
state: ReadingSpeedTrackerState,
|
||||
wordCount: number,
|
||||
): ReadingSpeedTrackerState {
|
||||
state.wordsRead += wordCount;
|
||||
return state;
|
||||
}
|
||||
|
||||
async function syncReadingSpeed(
|
||||
state: ReadingSpeedTrackerState,
|
||||
): Promise<void> {
|
||||
if (!state.startTime) return;
|
||||
|
||||
const minutesElapsed = (Date.now() - state.startTime) / (1000 * 60);
|
||||
const pagesPerMinute = state.pagesRead / minutesElapsed;
|
||||
const wordsPerMinute = state.wordsRead / minutesElapsed;
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem("token");
|
||||
await fetch(`/readers/${state.mediaItemId}/reading-speed`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
pages_per_minute: pagesPerMinute,
|
||||
words_per_minute: wordsPerMinute,
|
||||
pages_read: state.pagesRead,
|
||||
total_reading_minutes: minutesElapsed,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to sync reading speed:", error);
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user