- reader-shell.ts: Main initialization, Alpine.js integration, media type detection - progress-indicator.ts: Reading progress tracking and display components - settings-manager.ts: User settings persistence and retrieval - panel-dock-system.ts: Dockable panel management with drag/drop and collapse - parser-manager.ts: Parser selection and format detection system These core modules provide the foundation for all reader types with shared functionality for progress tracking, settings management, and the flexible panel docking system.
248 lines
6.7 KiB
TypeScript
248 lines
6.7 KiB
TypeScript
// 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 };
|