편의 기능(정렬·종류필터·전체선택/단축키·정보 오버레이) + 스냅샷 범위 안전화
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:
@@ -8,6 +8,7 @@ import { ContextMenu } from "./features/fileops/ContextMenu";
|
||||
import { MoveDialog } from "./features/fileops/MoveDialog";
|
||||
import { DedupePanel } from "./features/dedupe/DedupePanel";
|
||||
import { RemoteDialog } from "./features/sources/RemoteDialog";
|
||||
import { ShortcutsOverlay } from "./shell/ShortcutsOverlay";
|
||||
import { onLibraryChanged, onThumbsReady, type UnlistenFn } from "./ipc/commands";
|
||||
import { bumpThumbVersions, loadAppInfo, refreshPendingCounts, refreshSnapshot } from "./state/store";
|
||||
|
||||
@@ -45,6 +46,7 @@ const App: Component = () => {
|
||||
<MoveDialog />
|
||||
<DedupePanel />
|
||||
<RemoteDialog />
|
||||
<ShortcutsOverlay />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -28,6 +28,38 @@ export const ko = {
|
||||
empty: "표시할 항목이 없습니다",
|
||||
items: (n: number) => `${n.toLocaleString("ko-KR")}개 항목`,
|
||||
selected: (n: number) => `${n.toLocaleString("ko-KR")}개 선택됨`,
|
||||
infoToggle: "썸네일에 파일명·크기 표시",
|
||||
},
|
||||
filter: {
|
||||
all: "전체",
|
||||
image: "사진",
|
||||
video: "영상",
|
||||
},
|
||||
sort: {
|
||||
taken: "촬영일",
|
||||
name: "이름",
|
||||
size: "크기",
|
||||
mtime: "수정일",
|
||||
asc: "오름차순",
|
||||
desc: "내림차순",
|
||||
},
|
||||
shortcuts: {
|
||||
title: "키보드 단축키",
|
||||
close: "닫기 (Esc / ?)",
|
||||
items: [
|
||||
["Space / K", "재생·일시정지 (영상)"],
|
||||
["← →", "이전·다음 항목 (그리드) / 5초 이동 (영상)"],
|
||||
["↑ ↓", "위·아래 줄 이동 (그리드) / 볼륨 (영상)"],
|
||||
["Enter / Space", "선택 항목 크게 보기"],
|
||||
["Ctrl+A", "전체 선택"],
|
||||
["Ctrl+Shift+A", "선택 반전"],
|
||||
["Esc", "선택 해제 / 닫기"],
|
||||
["Delete", "휴지통으로"],
|
||||
["Ctrl+Z", "실행 취소"],
|
||||
["F2", "이름 바꾸기"],
|
||||
["Ctrl+휠", "썸네일 크기"],
|
||||
["?", "이 도움말"],
|
||||
] as [string, string][],
|
||||
},
|
||||
details: {
|
||||
title: "상세 정보",
|
||||
|
||||
+36
-17
@@ -88,24 +88,30 @@ export class Snapshot {
|
||||
this.view = new DataView(buf);
|
||||
this.count = Math.floor(buf.byteLength / ITEM_BYTES);
|
||||
}
|
||||
// 모든 접근자는 범위 밖 i에 안전해야 한다. 스냅샷이 줄어들 때(필터/검색/폴더 전환)
|
||||
// For가 초과 셀을 제거하기 전에 셀의 반응형 접근자가 옛 인덱스로 재실행될 수 있는데,
|
||||
// DataView가 범위를 벗어나면 RangeError를 던져 Solid 반응성 그래프 전체가 무너진다.
|
||||
private ok(i: number): boolean {
|
||||
return i >= 0 && i < this.count;
|
||||
}
|
||||
id(i: number): number {
|
||||
return this.view.getUint32(i * ITEM_BYTES, true);
|
||||
return this.ok(i) ? this.view.getUint32(i * ITEM_BYTES, true) : 0;
|
||||
}
|
||||
width(i: number): number {
|
||||
return this.view.getUint16(i * ITEM_BYTES + 4, true);
|
||||
return this.ok(i) ? this.view.getUint16(i * ITEM_BYTES + 4, true) : 0;
|
||||
}
|
||||
height(i: number): number {
|
||||
return this.view.getUint16(i * ITEM_BYTES + 6, true);
|
||||
return this.ok(i) ? this.view.getUint16(i * ITEM_BYTES + 6, true) : 0;
|
||||
}
|
||||
kind(i: number): number {
|
||||
return this.view.getUint8(i * ITEM_BYTES + 8);
|
||||
return this.ok(i) ? this.view.getUint8(i * ITEM_BYTES + 8) : 0;
|
||||
}
|
||||
hasThumb(i: number): boolean {
|
||||
return (this.view.getUint8(i * ITEM_BYTES + 9) & 1) !== 0;
|
||||
return this.ok(i) && (this.view.getUint8(i * ITEM_BYTES + 9) & 1) !== 0;
|
||||
}
|
||||
/** 썸네일 생성을 시도했으나 실패함(state 2) — 대기와 구분해 스켈레톤을 멈추기 위함 */
|
||||
thumbFailed(i: number): boolean {
|
||||
return (this.view.getUint8(i * ITEM_BYTES + 9) & 2) !== 0;
|
||||
return this.ok(i) && (this.view.getUint8(i * ITEM_BYTES + 9) & 2) !== 0;
|
||||
}
|
||||
/** 레이아웃 워커 입력용 종횡비 배열 (미지 치수는 1:1) */
|
||||
aspectRatios(): Float32Array {
|
||||
@@ -119,21 +125,34 @@ export class Snapshot {
|
||||
}
|
||||
}
|
||||
|
||||
export async function folderSnapshot(args: {
|
||||
sourceId?: number;
|
||||
folderId?: number;
|
||||
recursive?: boolean;
|
||||
}): Promise<Snapshot> {
|
||||
/** 정렬 키: taken(촬영/기본) | name | size | mtime */
|
||||
export type SortKey = "taken" | "name" | "size" | "mtime";
|
||||
/** 스냅샷 공통 옵션 — 정렬/종류 필터 */
|
||||
export interface SnapOpts {
|
||||
sortKey?: SortKey;
|
||||
sortDesc?: boolean;
|
||||
kind?: number | null; // 0=사진, 1=영상, null=전체
|
||||
}
|
||||
const snapArgs = (o?: SnapOpts) => ({
|
||||
sortKey: o?.sortKey ?? null,
|
||||
sortDesc: o?.sortDesc ?? null,
|
||||
kind: o?.kind ?? null,
|
||||
});
|
||||
|
||||
export async function folderSnapshot(
|
||||
args: { sourceId?: number; folderId?: number; recursive?: boolean } & SnapOpts,
|
||||
): Promise<Snapshot> {
|
||||
const buf = await invoke<ArrayBuffer>("folder_snapshot", {
|
||||
sourceId: args.sourceId ?? null,
|
||||
folderId: args.folderId ?? null,
|
||||
recursive: args.recursive ?? true,
|
||||
...snapArgs(args),
|
||||
});
|
||||
return new Snapshot(buf);
|
||||
}
|
||||
|
||||
export async function searchSnapshot(query: string): Promise<Snapshot> {
|
||||
const buf = await invoke<ArrayBuffer>("search_snapshot", { query });
|
||||
export async function searchSnapshot(query: string, opts?: SnapOpts): Promise<Snapshot> {
|
||||
const buf = await invoke<ArrayBuffer>("search_snapshot", { query, ...snapArgs(opts) });
|
||||
return new Snapshot(buf);
|
||||
}
|
||||
|
||||
@@ -165,8 +184,8 @@ export const fileTagsOf = (fileId: number) => invoke<Tag[]>("file_tags_of", { fi
|
||||
export const exportTags = (path?: string) =>
|
||||
invoke<string>("export_tags", { path: path ?? null });
|
||||
|
||||
export async function tagSnapshot(tagId: number): Promise<Snapshot> {
|
||||
return new Snapshot(await invoke<ArrayBuffer>("tag_snapshot", { tagId }));
|
||||
export async function tagSnapshot(tagId: number, opts?: SnapOpts): Promise<Snapshot> {
|
||||
return new Snapshot(await invoke<ArrayBuffer>("tag_snapshot", { tagId, ...snapArgs(opts) }));
|
||||
}
|
||||
|
||||
// ── 파일 작업 ───────────────────────────────────────────
|
||||
@@ -227,8 +246,8 @@ export const createSmartFolder = (name: string, query: SmartQuery) =>
|
||||
export const deleteSmartFolder = (id: number) =>
|
||||
invoke<void>("delete_smart_folder", { id });
|
||||
|
||||
export async function smartSnapshot(id: number): Promise<Snapshot> {
|
||||
return new Snapshot(await invoke<ArrayBuffer>("smart_snapshot", { id }));
|
||||
export async function smartSnapshot(id: number, opts?: SnapOpts): Promise<Snapshot> {
|
||||
return new Snapshot(await invoke<ArrayBuffer>("smart_snapshot", { id, ...snapArgs(opts) }));
|
||||
}
|
||||
|
||||
// ── 원격 소스 ───────────────────────────────────────────
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createResource, createSignal, onCleanup, Show, type Component } from "solid-js";
|
||||
import { createResource, createSignal, For, onCleanup, Show, type Component } from "solid-js";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { ko } from "../i18n/ko";
|
||||
import { JustifiedGrid } from "../features/grid/JustifiedGrid";
|
||||
@@ -6,13 +6,22 @@ import {
|
||||
GRID_ROW_MAX,
|
||||
GRID_ROW_MIN,
|
||||
gridRowHeight,
|
||||
kindFilter,
|
||||
refreshSidebar,
|
||||
setGridRowHeight,
|
||||
setKindFilter,
|
||||
setSort,
|
||||
setView,
|
||||
showInfo,
|
||||
sidebarVersion,
|
||||
snapshot,
|
||||
sortDesc,
|
||||
sortKey,
|
||||
toggleShowInfo,
|
||||
updateScanProgress,
|
||||
view,
|
||||
type KindFilter,
|
||||
type SortKey,
|
||||
} from "../state/store";
|
||||
import { addLocalSource, listSources, startScan } from "../ipc/commands";
|
||||
import { openRemoteDialog } from "../features/sources/RemoteDialog";
|
||||
@@ -61,6 +70,58 @@ export const ContentArea: Component = () => {
|
||||
<span class="text-[11px] text-[var(--text-muted)]">
|
||||
{snapshot() ? ko.grid.items(snapshot()!.count) : ""}
|
||||
</span>
|
||||
|
||||
{/* 종류 필터: 전체 / 사진 / 영상 */}
|
||||
<div class="flex overflow-hidden rounded border border-[var(--border)] text-[11px]">
|
||||
<For each={[["all", ko.filter.all], ["image", ko.filter.image], ["video", ko.filter.video]] as [KindFilter, string][]}>
|
||||
{([k, label]) => (
|
||||
<button
|
||||
class="px-2 py-0.5"
|
||||
classList={{
|
||||
"bg-[var(--bg-active)] text-[var(--text-primary)]": kindFilter() === k,
|
||||
"text-[var(--text-muted)] hover:bg-[var(--bg-hover)]": kindFilter() !== k,
|
||||
}}
|
||||
onClick={() => setKindFilter(k)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
{/* 정렬: 키 선택 + 방향 토글 */}
|
||||
<div class="flex items-center gap-1 text-[11px] text-[var(--text-secondary)]">
|
||||
<select
|
||||
class="rounded border border-[var(--border)] bg-[var(--bg-panel)] px-1.5 py-0.5 outline-none"
|
||||
value={sortKey()}
|
||||
onChange={(e) => setSort(e.currentTarget.value as SortKey, sortDesc())}
|
||||
>
|
||||
<For each={[["taken", ko.sort.taken], ["name", ko.sort.name], ["size", ko.sort.size], ["mtime", ko.sort.mtime]] as [SortKey, string][]}>
|
||||
{([k, label]) => <option value={k}>{label}</option>}
|
||||
</For>
|
||||
</select>
|
||||
<button
|
||||
class="rounded border border-[var(--border)] px-1.5 py-0.5 hover:bg-[var(--bg-hover)]"
|
||||
onClick={() => setSort(sortKey(), !sortDesc())}
|
||||
title={sortDesc() ? ko.sort.desc : ko.sort.asc}
|
||||
>
|
||||
{sortDesc() ? "↓" : "↑"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 썸네일 정보 오버레이 토글 */}
|
||||
<button
|
||||
class="rounded border border-[var(--border)] px-1.5 py-0.5 text-[11px]"
|
||||
classList={{
|
||||
"bg-[var(--bg-active)] text-[var(--text-primary)]": showInfo(),
|
||||
"text-[var(--text-muted)] hover:bg-[var(--bg-hover)]": !showInfo(),
|
||||
}}
|
||||
onClick={toggleShowInfo}
|
||||
title={ko.grid.infoToggle}
|
||||
>
|
||||
ⓘ
|
||||
</button>
|
||||
|
||||
{/* 그리드 줌 슬라이더 */}
|
||||
<div class="ml-auto flex items-center gap-1.5 text-[var(--text-muted)]" title="썸네일 크기 (Ctrl+휠)">
|
||||
<span class="text-xs">🔍</span>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// 키보드 단축키 도움말 오버레이 (? 키로 열기).
|
||||
import { For, onCleanup, onMount, Show, type Component } from "solid-js";
|
||||
import { ko } from "../i18n/ko";
|
||||
import { closeShortcuts, shortcutsOpen } from "../state/store";
|
||||
|
||||
export const ShortcutsOverlay: Component = () => {
|
||||
onMount(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (!shortcutsOpen()) return;
|
||||
if (e.key === "Escape" || e.key === "?") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeShortcuts();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", onKey, { capture: true });
|
||||
onCleanup(() => window.removeEventListener("keydown", onKey, { capture: true }));
|
||||
});
|
||||
|
||||
return (
|
||||
<Show when={shortcutsOpen()}>
|
||||
<div
|
||||
class="fixed inset-0 z-50 grid place-items-center bg-black/50"
|
||||
onClick={closeShortcuts}
|
||||
>
|
||||
<div
|
||||
class="max-h-[80vh] w-[min(30rem,92vw)] overflow-y-auto rounded-lg border border-[var(--border)] bg-[var(--bg-panel)] p-5 shadow-xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-sm font-semibold text-[var(--text-primary)]">
|
||||
{ko.shortcuts.title}
|
||||
</span>
|
||||
<button
|
||||
class="rounded px-2 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]"
|
||||
onClick={closeShortcuts}
|
||||
title={ko.shortcuts.close}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<For each={ko.shortcuts.items}>
|
||||
{([keys, desc]) => (
|
||||
<div class="flex items-center justify-between gap-4 text-xs">
|
||||
<kbd class="shrink-0 rounded border border-[var(--border)] bg-[var(--bg-app)] px-1.5 py-0.5 font-mono text-[11px] text-[var(--text-secondary)]">
|
||||
{keys}
|
||||
</kbd>
|
||||
<span class="text-right text-[var(--text-secondary)]">{desc}</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
+49
-6
@@ -15,6 +15,8 @@ import {
|
||||
type DedupeProgress,
|
||||
type PendingCounts,
|
||||
type ScanProgress,
|
||||
type SnapOpts,
|
||||
type SortKey,
|
||||
} from "../ipc/commands";
|
||||
|
||||
// ── 앱 정보 (미디어 서버 포트/토큰 포함) ─────────────────
|
||||
@@ -43,6 +45,36 @@ const [snapshot, setSnapshot] = createSignal<Snapshot | undefined>();
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
export { view, snapshot, loading };
|
||||
|
||||
// ── 정렬 / 종류 필터 (툴바) ──────────────────────────────
|
||||
const [sortKey, setSortKeySig] = createSignal<SortKey>("taken");
|
||||
const [sortDesc, setSortDescSig] = createSignal(true);
|
||||
export type KindFilter = "all" | "image" | "video";
|
||||
const [kindFilter, setKindFilterSig] = createSignal<KindFilter>("all");
|
||||
export { sortKey, sortDesc, kindFilter };
|
||||
export type { SortKey };
|
||||
|
||||
/** 정렬 변경 → 재조회 */
|
||||
export function setSort(key: SortKey, desc: boolean): void {
|
||||
setSortKeySig(key);
|
||||
setSortDescSig(desc);
|
||||
void refreshSnapshot();
|
||||
}
|
||||
/** 종류 필터 변경(전체/사진/영상) → 재조회 */
|
||||
export function setKindFilter(k: KindFilter): void {
|
||||
setKindFilterSig(k);
|
||||
void refreshSnapshot();
|
||||
}
|
||||
|
||||
// 그리드 썸네일에 파일명·크기 오버레이 표시 (localStorage 유지)
|
||||
const INFO_KEY = "archive.grid.infoOverlay";
|
||||
const [showInfo, setShowInfoRaw] = createSignal(localStorage.getItem(INFO_KEY) === "1");
|
||||
export { showInfo };
|
||||
export function toggleShowInfo(): void {
|
||||
const n = !showInfo();
|
||||
setShowInfoRaw(n);
|
||||
localStorage.setItem(INFO_KEY, n ? "1" : "0");
|
||||
}
|
||||
|
||||
let viewSeq = 0;
|
||||
|
||||
export async function setView(v: View): Promise<void> {
|
||||
@@ -55,26 +87,31 @@ export async function refreshSnapshot(): Promise<void> {
|
||||
const v = view();
|
||||
const seq = ++viewSeq;
|
||||
setLoading(true);
|
||||
const opts: SnapOpts = {
|
||||
sortKey: sortKey(),
|
||||
sortDesc: sortDesc(),
|
||||
kind: kindFilter() === "image" ? 0 : kindFilter() === "video" ? 1 : null,
|
||||
};
|
||||
try {
|
||||
let snap: Snapshot;
|
||||
switch (v.type) {
|
||||
case "all":
|
||||
snap = await folderSnapshot({});
|
||||
snap = await folderSnapshot({ ...opts });
|
||||
break;
|
||||
case "source":
|
||||
snap = await folderSnapshot({ sourceId: v.sourceId });
|
||||
snap = await folderSnapshot({ sourceId: v.sourceId, ...opts });
|
||||
break;
|
||||
case "folder":
|
||||
snap = await folderSnapshot({ folderId: v.folderId, recursive: v.recursive });
|
||||
snap = await folderSnapshot({ folderId: v.folderId, recursive: v.recursive, ...opts });
|
||||
break;
|
||||
case "search":
|
||||
snap = await searchSnapshot(v.query);
|
||||
snap = await searchSnapshot(v.query, opts);
|
||||
break;
|
||||
case "tag":
|
||||
snap = await tagSnapshot(v.tagId);
|
||||
snap = await tagSnapshot(v.tagId, opts);
|
||||
break;
|
||||
case "smart":
|
||||
snap = await smartSnapshot(v.smartId);
|
||||
snap = await smartSnapshot(v.smartId, opts);
|
||||
break;
|
||||
}
|
||||
if (seq === viewSeq) setSnapshot(snap);
|
||||
@@ -193,6 +230,12 @@ const [repeatMode, setRepeatMode] = createSignal(false);
|
||||
export { repeatMode };
|
||||
export const toggleRepeat = () => setRepeatMode((r) => !r);
|
||||
|
||||
// ── 단축키 도움말 오버레이 ───────────────────────────────
|
||||
const [shortcutsOpen, setShortcutsOpen] = createSignal(false);
|
||||
export { shortcutsOpen };
|
||||
export const openShortcuts = () => setShortcutsOpen(true);
|
||||
export const closeShortcuts = () => setShortcutsOpen(false);
|
||||
|
||||
// ── 중복 검토 패널 ───────────────────────────────────────
|
||||
const [dedupeOpen, setDedupeOpen] = createSignal(false);
|
||||
export { dedupeOpen };
|
||||
|
||||
Reference in New Issue
Block a user