Chore: Restructure Repository for SvelteKit

This commit is contained in:
Youwes09
2026-05-22 04:04:59 -05:00
parent bf071dcfc7
commit 8cef74bb98
266 changed files with 5093 additions and 396 deletions
+23
View File
@@ -0,0 +1,23 @@
import { store } from "@store/state.svelte";
const IDLE_EVENTS = ["mousemove", "mousedown", "keydown", "touchstart", "wheel"] as const;
export function mountIdleDetection(onIdle: () => void, onActive: () => void): () => void {
let timer: ReturnType<typeof setTimeout> | null = null;
function reset() {
if (timer) clearTimeout(timer);
const ms = (store.settings.idleTimeoutMin ?? 5) * 60 * 1000;
if (ms === 0) return;
timer = setTimeout(onIdle, ms);
onActive();
}
IDLE_EVENTS.forEach(e => window.addEventListener(e, reset, { passive: true }));
reset();
return () => {
if (timer) clearTimeout(timer);
IDLE_EVENTS.forEach(e => window.removeEventListener(e, reset));
};
}
+3
View File
@@ -0,0 +1,3 @@
export * from './idle';
export * from './zoom';
export * from './touchscreen';
+234
View File
@@ -0,0 +1,234 @@
export interface LongPressOptions {
onLongPress: (e: PointerEvent) => void;
duration?: number;
moveThreshold?: number;
}
export function longPress(node: HTMLElement, opts: LongPressOptions) {
const { onLongPress, duration = 500, moveThreshold = 8 } = opts;
let timer: ReturnType<typeof setTimeout> | null = null;
let startX = 0, startY = 0;
let fired = false;
function start(e: PointerEvent) {
if (e.button !== 0 && e.pointerType === "mouse") return;
startX = e.clientX; startY = e.clientY; fired = false;
timer = setTimeout(() => { timer = null; fired = true; onLongPress(e); }, duration);
}
function move(e: PointerEvent) {
if (!timer) return;
const dx = e.clientX - startX, dy = e.clientY - startY;
if (Math.sqrt(dx * dx + dy * dy) > moveThreshold) cancel();
}
function cancel() { if (timer) { clearTimeout(timer); timer = null; } }
node.addEventListener("pointerdown", start);
node.addEventListener("pointermove", move);
node.addEventListener("pointerup", cancel);
node.addEventListener("pointerleave", cancel);
node.addEventListener("pointercancel",cancel);
return {
get fired() { return fired; },
destroy() {
cancel();
node.removeEventListener("pointerdown", start);
node.removeEventListener("pointermove", move);
node.removeEventListener("pointerup", cancel);
node.removeEventListener("pointerleave", cancel);
node.removeEventListener("pointercancel",cancel);
},
};
}
export interface TapOptions {
onTap: (e: PointerEvent) => void;
onDoubleTap?: (e: PointerEvent) => void;
doubleTapGap?: number;
}
export function tap(node: HTMLElement, opts: TapOptions) {
const { onTap, onDoubleTap, doubleTapGap = 300 } = opts;
let lastTap = 0;
let pending: ReturnType<typeof setTimeout> | null = null;
let startX = 0, startY = 0;
const SLOP = 8;
function down(e: PointerEvent) { startX = e.clientX; startY = e.clientY; }
function up(e: PointerEvent) {
const dx = e.clientX - startX, dy = e.clientY - startY;
if (Math.sqrt(dx * dx + dy * dy) > SLOP) return;
const now = Date.now();
if (onDoubleTap && now - lastTap < doubleTapGap) {
if (pending) { clearTimeout(pending); pending = null; }
onDoubleTap(e);
lastTap = 0;
} else {
lastTap = now;
if (onDoubleTap) {
pending = setTimeout(() => { pending = null; onTap(e); }, doubleTapGap);
} else {
onTap(e);
}
}
}
node.addEventListener("pointerdown", down);
node.addEventListener("pointerup", up);
return { destroy() {
node.removeEventListener("pointerdown", down);
node.removeEventListener("pointerup", up);
}};
}
export interface SwipeOptions {
onSwipeLeft?: (e: PointerEvent) => void;
onSwipeRight?: (e: PointerEvent) => void;
onSwipeUp?: (e: PointerEvent) => void;
onSwipeDown?: (e: PointerEvent) => void;
threshold?: number;
lockAxis?: boolean;
}
export function swipe(node: HTMLElement, opts: SwipeOptions) {
const { onSwipeLeft, onSwipeRight, onSwipeUp, onSwipeDown, threshold = 40, lockAxis = true } = opts;
let startX = 0, startY = 0, active = false;
function down(e: PointerEvent) {
if (e.pointerType === "mouse") return;
startX = e.clientX; startY = e.clientY; active = true;
node.setPointerCapture(e.pointerId);
}
function up(e: PointerEvent) {
if (!active) return; active = false;
const dx = e.clientX - startX, dy = e.clientY - startY;
const ax = Math.abs(dx), ay = Math.abs(dy);
if (Math.max(ax, ay) < threshold) return;
if (lockAxis && ax > ay) {
if (dx < 0) onSwipeLeft?.(e); else onSwipeRight?.(e);
} else if (lockAxis && ay >= ax) {
if (dy < 0) onSwipeUp?.(e); else onSwipeDown?.(e);
} else {
if (ax >= ay) { if (dx < 0) onSwipeLeft?.(e); else onSwipeRight?.(e); }
else { if (dy < 0) onSwipeUp?.(e); else onSwipeDown?.(e); }
}
}
function cancel() { active = false; }
node.addEventListener("pointerdown", down);
node.addEventListener("pointerup", up);
node.addEventListener("pointercancel", cancel);
return { destroy() {
node.removeEventListener("pointerdown", down);
node.removeEventListener("pointerup", up);
node.removeEventListener("pointercancel", cancel);
}};
}
export interface PinchOptions {
onPinch: (scale: number, origin: { x: number; y: number }) => void;
onPinchEnd?: (scale: number) => void;
}
export interface PinchGestureOptions {
onPinch: (scale: number, origin: { x: number; y: number }) => void;
onPinchEnd?: (scale: number) => void;
}
export interface PinchGesture {
onPointerDown: (e: PointerEvent) => void;
onPointerMove: (e: PointerEvent) => void;
onPointerUp: (e: PointerEvent) => void;
isPinching: () => boolean;
}
export function createPinchGesture(opts: PinchGestureOptions): PinchGesture {
const { onPinch, onPinchEnd } = opts;
const pointers = new Map<number, PointerEvent>();
let initDist = 0;
function pdist(a: PointerEvent, b: PointerEvent) {
const dx = a.clientX - b.clientX, dy = a.clientY - b.clientY;
return Math.sqrt(dx * dx + dy * dy);
}
function pmid(a: PointerEvent, b: PointerEvent) {
return { x: (a.clientX + b.clientX) / 2, y: (a.clientY + b.clientY) / 2 };
}
function onPointerDown(e: PointerEvent) {
pointers.set(e.pointerId, e);
if (pointers.size === 2) {
const [a, b] = [...pointers.values()];
initDist = pdist(a, b);
}
}
function onPointerMove(e: PointerEvent) {
if (!pointers.has(e.pointerId)) return;
pointers.set(e.pointerId, e);
if (pointers.size !== 2 || initDist === 0) return;
const [a, b] = [...pointers.values()];
onPinch(pdist(a, b) / initDist, pmid(a, b));
}
function onPointerUp(e: PointerEvent) {
if (pointers.size === 2 && onPinchEnd) {
const [a, b] = [...pointers.values()];
onPinchEnd(pdist(a, b) / initDist);
}
pointers.delete(e.pointerId);
initDist = 0;
}
return { onPointerDown, onPointerMove, onPointerUp, isPinching: () => pointers.size >= 2 };
}
export function pinch(node: HTMLElement, opts: PinchOptions) {
const gesture = createPinchGesture(opts);
function down(e: PointerEvent) { node.setPointerCapture(e.pointerId); gesture.onPointerDown(e); }
node.addEventListener("pointerdown", down);
node.addEventListener("pointermove", gesture.onPointerMove);
node.addEventListener("pointerup", gesture.onPointerUp);
node.addEventListener("pointercancel", gesture.onPointerUp);
return { destroy() {
node.removeEventListener("pointerdown", down);
node.removeEventListener("pointermove", gesture.onPointerMove);
node.removeEventListener("pointerup", gesture.onPointerUp);
node.removeEventListener("pointercancel", gesture.onPointerUp);
}};
}
export interface DragScrollOptions {
direction?: "x" | "y" | "both";
onDragStart?: () => void;
onDragEnd?: () => void;
}
export function dragScroll(node: HTMLElement, opts: DragScrollOptions = {}) {
const { direction = "both", onDragStart, onDragEnd } = opts;
let active = false, startX = 0, startY = 0, scrollX = 0, scrollY = 0;
function down(e: PointerEvent) {
if (e.pointerType === "mouse") return;
active = true;
startX = e.clientX; startY = e.clientY;
scrollX = node.scrollLeft; scrollY = node.scrollTop;
node.setPointerCapture(e.pointerId);
onDragStart?.();
}
function move(e: PointerEvent) {
if (!active) return;
if (direction !== "x") node.scrollTop = scrollY - (e.clientY - startY);
if (direction !== "y") node.scrollLeft = scrollX - (e.clientX - startX);
}
function up() { if (active) { active = false; onDragEnd?.(); } }
node.addEventListener("pointerdown", down);
node.addEventListener("pointermove", move);
node.addEventListener("pointerup", up);
node.addEventListener("pointercancel", up);
return { destroy() {
node.removeEventListener("pointerdown", down);
node.removeEventListener("pointermove", move);
node.removeEventListener("pointerup", up);
node.removeEventListener("pointercancel", up);
}};
}
+61
View File
@@ -0,0 +1,61 @@
import { store } from "@store/state.svelte";
let _appliedZoom: number = -1;
let _vhRafId: number | null = null;
export function applyZoom() {
const uiZoom = store.settings.uiZoom ?? 1.0;
if (uiZoom === _appliedZoom) return;
_appliedZoom = uiZoom;
document.documentElement.style.setProperty("--ui-zoom", String(uiZoom));
document.documentElement.style.setProperty("--ui-scale", String(uiZoom));
document.documentElement.style.zoom = `${uiZoom * 100}%`;
if (_vhRafId !== null) cancelAnimationFrame(_vhRafId);
_vhRafId = requestAnimationFrame(() => {
_vhRafId = null;
document.documentElement.style.setProperty("--visual-vh", `${window.innerHeight / uiZoom}px`);
});
}
export function handleZoomKey(e: KeyboardEvent) {
if (!e.ctrlKey) return;
const current = store.settings.uiZoom ?? 1.0;
if (e.key === "=" || e.key === "+") { e.preventDefault(); store.settings.uiZoom = Math.min(2.0, Math.round((current + 0.1) * 10) / 10); }
else if (e.key === "-") { e.preventDefault(); store.settings.uiZoom = Math.max(0.5, Math.round((current - 0.1) * 10) / 10); }
else if (e.key === "0") { e.preventDefault(); store.settings.uiZoom = 1.0; }
}
export function mountZoomKey(): () => void {
window.addEventListener("keydown", handleZoomKey);
return () => window.removeEventListener("keydown", handleZoomKey);
}
export function clampZoom(z: number, min: number, max: number): number {
return Math.round(Math.min(max, Math.max(min, z)) * 1000) / 1000;
}
export function captureZoomAnchor(
containerEl: HTMLElement | null,
style: string,
out: { el: HTMLElement | null; offset: number },
) {
if (!containerEl || style !== "longstrip") return;
const containerTop = containerEl.getBoundingClientRect().top;
for (const img of containerEl.querySelectorAll<HTMLElement>("img[data-local-page]")) {
const rect = img.getBoundingClientRect();
if (rect.bottom > containerTop) { out.el = img; out.offset = rect.top - containerTop; return; }
}
}
export function restoreZoomAnchor(
containerEl: HTMLElement | null,
out: { el: HTMLElement | null; offset: number },
) {
if (!out.el || !containerEl) return;
const el = out.el;
out.el = null;
requestAnimationFrame(() => {
const containerTop = containerEl!.getBoundingClientRect().top;
containerEl!.scrollTop += (el.getBoundingClientRect().top - containerTop) - out.offset;
});
}