편의 기능(정렬·종류필터·전체선택/단축키·정보 오버레이) + 스냅샷 범위 안전화
Snapshot 접근자 범위 안전화(핵심 버그): - id/width/height/kind/hasThumb/thumbFailed가 count 밖 i에서 DataView RangeError를 던져 Solid 반응성 그래프 전체가 무너지던 문제 수정. 스냅샷이 줄어들 때(필터/검색/ 폴더 전환) For가 초과 셀을 제거하기 전에 셀 접근자가 옛 인덱스로 재실행되며 발생. 범위 밖이면 0/false 반환. (증상: 필터/검색이 한 번은 되고 이후 먹통) 기능: - 정렬: 촬영일/이름/크기/수정일 × 오름·내림 (툴바 드롭다운+방향 토글). backend snap_tail로 ORDER BY 구성, 4개 스냅샷 커맨드에 sort/kind 파라미터 추가. - 종류 필터: 전체/사진/영상 툴바 토글 (backend kind 필터). - 전체 선택(Ctrl+A)/선택 반전(Ctrl+Shift+A), ? 키 단축키 도움말 오버레이. - 썸네일 정보 오버레이(ⓘ 토글): 가시 셀에 파일명·크기 지연 로드 표시. 검증(사진120+영상25): 종류필터 120/25/145/120 반복 정상, 정렬 순서 변경, Ctrl+A 145개 선택, ? 오버레이, ⓘ로 파일명 표시 확인. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
Show,
|
||||
type Component,
|
||||
} from "solid-js";
|
||||
import { createStore, produce } from "solid-js/store";
|
||||
import { computeLayout, type LayoutResult } from "./layout";
|
||||
import {
|
||||
anchorIndex,
|
||||
@@ -17,9 +18,11 @@ import {
|
||||
gridRowHeight,
|
||||
info,
|
||||
openLightbox,
|
||||
openShortcuts,
|
||||
selected,
|
||||
selectSingle,
|
||||
setSelectionFromIds,
|
||||
showInfo,
|
||||
toggleSelect,
|
||||
selectRange,
|
||||
snapshot,
|
||||
@@ -38,6 +41,12 @@ import {
|
||||
const GAP = 6;
|
||||
const OVERSCAN_PX = 800;
|
||||
|
||||
function fmtSize(bytes: number): string {
|
||||
if (bytes >= 1 << 20) return `${(bytes / (1 << 20)).toFixed(1)}MB`;
|
||||
if (bytes >= 1 << 10) return `${(bytes / (1 << 10)).toFixed(0)}KB`;
|
||||
return `${bytes}B`;
|
||||
}
|
||||
|
||||
/** rowTops에서 y 이상이 처음 나오는 행 인덱스 (이진탐색) */
|
||||
function findRow(rowTops: Float32Array, y: number): number {
|
||||
let lo = 0;
|
||||
@@ -156,6 +165,35 @@ export const JustifiedGrid: Component = () => {
|
||||
});
|
||||
onCleanup(() => clearTimeout(prioTimer));
|
||||
|
||||
// 정보 오버레이용 파일명/크기 지연 로드 (showInfo 켜졌을 때 가시 범위만)
|
||||
const [detailsCache, setDetailsCache] = createStore<Record<number, { name: string; size: number }>>({});
|
||||
let detTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
createEffect(() => {
|
||||
if (!showInfo()) return;
|
||||
const [first, last] = visibleRange();
|
||||
const snap = snapshot();
|
||||
if (!snap || last < first) return;
|
||||
clearTimeout(detTimer);
|
||||
detTimer = setTimeout(() => {
|
||||
const need: number[] = [];
|
||||
for (let i = first; i <= last && need.length < 512; i++) {
|
||||
const id = snap.id(i);
|
||||
if (detailsCache[id] === undefined) need.push(id);
|
||||
}
|
||||
if (need.length === 0) return;
|
||||
void fileDetails(need)
|
||||
.then((ds) =>
|
||||
setDetailsCache(
|
||||
produce((s) => {
|
||||
for (const d of ds) s[d.id] = { name: d.name, size: d.size };
|
||||
}),
|
||||
),
|
||||
)
|
||||
.catch(() => {});
|
||||
}, 150);
|
||||
});
|
||||
onCleanup(() => clearTimeout(detTimer));
|
||||
|
||||
const indices = createMemo(() => {
|
||||
const [first, last] = visibleRange();
|
||||
const out: number[] = [];
|
||||
@@ -206,6 +244,25 @@ export const JustifiedGrid: Component = () => {
|
||||
void performUndo();
|
||||
return;
|
||||
}
|
||||
// Ctrl+A 전체 선택 / Ctrl+Shift+A 선택 반전
|
||||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a") {
|
||||
e.preventDefault();
|
||||
const all: number[] = [];
|
||||
for (let i = 0; i < snap.count; i++) all.push(snap.id(i));
|
||||
if (e.shiftKey) {
|
||||
const cur = selected();
|
||||
setSelectionFromIds(all.filter((id) => !cur.has(id)));
|
||||
} else {
|
||||
setSelectionFromIds(all);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// ? 단축키 도움말
|
||||
if (e.key === "?") {
|
||||
e.preventDefault();
|
||||
openShortcuts();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case "Enter":
|
||||
@@ -456,6 +513,15 @@ export const JustifiedGrid: Component = () => {
|
||||
영상
|
||||
</span>
|
||||
</Show>
|
||||
{/* 파일명·크기 오버레이 (툴바 ⓘ 토글) */}
|
||||
<Show when={showInfo()}>
|
||||
<div class="pointer-events-none absolute inset-x-0 bottom-0 bg-black/55 px-1 py-0.5 leading-tight">
|
||||
<div class="truncate text-[10px] text-white/90">{detailsCache[id()]?.name ?? ""}</div>
|
||||
<Show when={detailsCache[id()]}>
|
||||
<div class="text-[9px] text-white/60">{fmtSize(detailsCache[id()]!.size)}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user