// 전역 상태 — Solid 시그널 기반 단일 스토어 모듈. import { createSignal } from "solid-js"; import { createStore, produce } from "solid-js/store"; import { appInfo, folderSnapshot, searchSnapshot, smartSnapshot, tagSnapshot, Snapshot, type AppInfo, type ScanProgress, } from "../ipc/commands"; // ── 앱 정보 (미디어 서버 포트/토큰 포함) ───────────────── const [info, setInfo] = createSignal(); export { info }; export async function loadAppInfo(): Promise { try { setInfo(await appInfo()); } catch (e) { console.error("app_info 실패", e); } } // ── 현재 뷰 ───────────────────────────────────────────── export type View = | { type: "all" } | { type: "source"; sourceId: number } | { type: "folder"; folderId: number; recursive: boolean } | { type: "search"; query: string } | { type: "tag"; tagId: number } | { type: "smart"; smartId: number }; const [view, setViewSignal] = createSignal({ type: "all" }); const [snapshot, setSnapshot] = createSignal(); const [loading, setLoading] = createSignal(false); export { view, snapshot, loading }; let viewSeq = 0; export async function setView(v: View): Promise { setViewSignal(v); clearSelection(); await refreshSnapshot(); } export async function refreshSnapshot(): Promise { const v = view(); const seq = ++viewSeq; setLoading(true); try { let snap: Snapshot; switch (v.type) { case "all": snap = await folderSnapshot({}); break; case "source": snap = await folderSnapshot({ sourceId: v.sourceId }); break; case "folder": snap = await folderSnapshot({ folderId: v.folderId, recursive: v.recursive }); break; case "search": snap = await searchSnapshot(v.query); break; case "tag": snap = await tagSnapshot(v.tagId); break; case "smart": snap = await smartSnapshot(v.smartId); break; } if (seq === viewSeq) setSnapshot(snap); } finally { if (seq === viewSeq) setLoading(false); } } // ── 선택 ──────────────────────────────────────────────── const [selected, setSelected] = createSignal>(new Set()); const [anchorIndex, setAnchorIndex] = createSignal(-1); export { selected, anchorIndex }; export function selectSingle(id: number, index: number): void { setSelected(new Set([id])); setAnchorIndex(index); } export function toggleSelect(id: number, index: number): void { const next = new Set(selected()); if (next.has(id)) next.delete(id); else next.add(id); setSelected(next); setAnchorIndex(index); } export function selectRange(snap: Snapshot, toIndex: number): void { const from = anchorIndex() < 0 ? toIndex : anchorIndex(); const [a, b] = from <= toIndex ? [from, toIndex] : [toIndex, from]; const next = new Set(); for (let i = a; i <= b; i++) next.add(snap.id(i)); setSelected(next); } export function clearSelection(): void { setSelected(new Set()); setAnchorIndex(-1); } /** 마퀴(드래그 사각형) 선택 — id 집합으로 직접 설정 */ export function setSelectionFromIds(ids: number[]): void { setSelected(new Set(ids)); } // ── 썸네일 갱신 버전 (thumbs-ready 이벤트로 증가) ──────── const [thumbVersions, setThumbVersions] = createStore>({}); export { thumbVersions }; export function bumpThumbVersions(ids: number[]): void { setThumbVersions( produce((s) => { for (const id of ids) s[id] = (s[id] ?? 0) + 1; }), ); } // ── 스캔 진행 ─────────────────────────────────────────── const [scanProgress, setScanProgress] = createSignal(); export { scanProgress }; export function updateScanProgress(p: ScanProgress): void { setScanProgress(p.done ? undefined : p); } // ── 사이드바 새로고침 트리거 ───────────────────────────── const [sidebarVersion, setSidebarVersion] = createSignal(0); export { sidebarVersion }; export const refreshSidebar = () => setSidebarVersion((v) => v + 1); // ── 그리드 썸네일 크기(줌) ─────────────────────────────── export const GRID_ROW_MIN = 110; export const GRID_ROW_MAX = 420; const GRID_ROW_KEY = "archive.grid.rowHeight"; const initialRow = Number(localStorage.getItem(GRID_ROW_KEY) ?? "200"); const [gridRowHeight, setGridRowHeightRaw] = createSignal( Number.isFinite(initialRow) ? Math.min(GRID_ROW_MAX, Math.max(GRID_ROW_MIN, initialRow)) : 200, ); export { gridRowHeight }; export function setGridRowHeight(px: number): void { const clamped = Math.min(GRID_ROW_MAX, Math.max(GRID_ROW_MIN, Math.round(px))); setGridRowHeightRaw(clamped); localStorage.setItem(GRID_ROW_KEY, String(clamped)); } /** 상대 배율로 줌 (Ctrl+휠) */ export function zoomGrid(factor: number): void { setGridRowHeight(gridRowHeight() * factor); } // ── 영상 재생목록 옵션 ─────────────────────────────────── const [repeatMode, setRepeatMode] = createSignal(false); export { repeatMode }; export const toggleRepeat = () => setRepeatMode((r) => !r); // ── 중복 검토 패널 ─────────────────────────────────────── const [dedupeOpen, setDedupeOpen] = createSignal(false); export { dedupeOpen }; export const openDedupe = () => setDedupeOpen(true); export const closeDedupe = () => setDedupeOpen(false); // ── 상태 메시지 (토스트) ───────────────────────────────── const [statusMessage, setStatusMessage] = createSignal(); export { statusMessage }; let toastTimer: ReturnType | undefined; export function toast(msg: string): void { setStatusMessage(msg); clearTimeout(toastTimer); toastTimer = setTimeout(() => setStatusMessage(undefined), 4000); } // ── 라이트박스 ────────────────────────────────────────── const [lightboxIndex, setLightboxIndex] = createSignal(null); export { lightboxIndex }; export function openLightbox(index: number): void { const snap = snapshot(); if (!snap || index < 0 || index >= snap.count) return; setLightboxIndex(index); } export function closeLightbox(): void { setLightboxIndex(null); } export function navLightbox(delta: number): void { const snap = snapshot(); const cur = lightboxIndex(); if (!snap || cur == null) return; const next = Math.min(snap.count - 1, Math.max(0, cur + delta)); setLightboxIndex(next); // 라이트박스 이동 시 선택도 따라간다 selectSingle(snap.id(next), next); }