중복 탐지 진행을 상태바에 표시 — 패널을 닫아도 진행/취소 가능
기존엔 중복 탐지 진행이 전체화면 패널 안에서만 보였고, 패널을 닫으면 진행 상황을 알 수 없었다. - 중복 탐지 실행/진행 상태를 store로 이관(runDedupe/dedupeRunning/dedupeProgress) → 패널 열림 여부와 무관하게 유지. - DedupePanel은 store 상태를 사용(로컬 running/progress 제거), 완료 시 결과 갱신. - StatusBar에 진행 표시: "완전 동일 검사 N/M" + 스피너 + 취소(✕), 텍스트 클릭 시 패널 다시 열기. 검증(대용량 3000장): 패널을 닫은 상태에서 상태바가 완전 동일 검사 517→3,000 진행을 실시간 표시, 취소 버튼 노출 확인. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,28 +5,30 @@ import {
|
||||
createResource,
|
||||
createSignal,
|
||||
For,
|
||||
on,
|
||||
Show,
|
||||
type Component,
|
||||
} from "solid-js";
|
||||
import { ask } from "@tauri-apps/plugin-dialog";
|
||||
import { ko } from "../../i18n/ko";
|
||||
import {
|
||||
cancelDedupe,
|
||||
dismissDupePair,
|
||||
listDupeGroups,
|
||||
resolveDupeGroup,
|
||||
startDedupe,
|
||||
thumbUrl,
|
||||
trashFiles,
|
||||
type DedupeProgress,
|
||||
type DupeGroup,
|
||||
} from "../../ipc/commands";
|
||||
import {
|
||||
closeDedupe,
|
||||
dedupeOpen,
|
||||
dedupeProgress,
|
||||
dedupeRunning,
|
||||
info,
|
||||
refreshSidebar,
|
||||
refreshSnapshot,
|
||||
requestCancelDedupe,
|
||||
runDedupe,
|
||||
toast,
|
||||
view,
|
||||
} from "../../state/store";
|
||||
@@ -40,8 +42,6 @@ function fmtSize(bytes: number): string {
|
||||
const KIND_LABEL: Record<number, string> = { 0: "완전 동일", 1: "유사 이미지", 2: "유사 영상" };
|
||||
|
||||
export const DedupePanel: Component = () => {
|
||||
const [running, setRunning] = createSignal(false);
|
||||
const [progress, setProgress] = createSignal<DedupeProgress | undefined>();
|
||||
const [includeVideo, setIncludeVideo] = createSignal(false);
|
||||
const [threshold, setThreshold] = createSignal(5);
|
||||
const [groupsVersion, setGroupsVersion] = createSignal(0);
|
||||
@@ -67,28 +67,25 @@ export const DedupePanel: Component = () => {
|
||||
});
|
||||
|
||||
const runScan = async () => {
|
||||
setRunning(true);
|
||||
setProgress(undefined);
|
||||
const scoped = useCurrentScope() ? currentScope() : null;
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
startDedupe(
|
||||
{ ...(scoped?.args ?? {}), imageThreshold: threshold(), includeVideo: includeVideo() },
|
||||
(p) => {
|
||||
setProgress(p);
|
||||
if (p.phase === "done") resolve();
|
||||
},
|
||||
).catch(reject);
|
||||
});
|
||||
await runDedupe({ ...(scoped?.args ?? {}), imageThreshold: threshold(), includeVideo: includeVideo() });
|
||||
} catch (e) {
|
||||
toast(String(e));
|
||||
} finally {
|
||||
setRunning(false);
|
||||
setProgress(undefined);
|
||||
setGroupsVersion((v) => v + 1);
|
||||
}
|
||||
};
|
||||
|
||||
// 탐지 완료(실행중 true→false) 시 결과 목록 갱신 — 패널이 닫혀 있다 열려도 재조회됨.
|
||||
createEffect(
|
||||
on(
|
||||
dedupeRunning,
|
||||
(running, prev) => {
|
||||
if (prev && !running) setGroupsVersion((v) => v + 1);
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
);
|
||||
|
||||
/** keeper 외 전부 휴지통 */
|
||||
const resolveGroup = async (g: DupeGroup) => {
|
||||
const losers = g.members.filter((m) => !m.isKeeper).map((m) => m.fileId);
|
||||
@@ -181,20 +178,20 @@ export const DedupePanel: Component = () => {
|
||||
</Show>
|
||||
</div>
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<Show when={running()}>
|
||||
<Show when={dedupeRunning()}>
|
||||
<span class="text-[11px] text-[var(--accent)]">
|
||||
{progress()
|
||||
? ko.dedupe.scanning(progress()!.phase, progress()!.current, progress()!.total)
|
||||
{dedupeProgress()
|
||||
? ko.dedupe.scanning(dedupeProgress()!.phase, dedupeProgress()!.current, dedupeProgress()!.total)
|
||||
: ko.dedupe.starting}
|
||||
</span>
|
||||
<button
|
||||
class="rounded border border-[var(--border)] px-2 py-1 text-xs hover:bg-[var(--bg-hover)]"
|
||||
onClick={() => void cancelDedupe()}
|
||||
onClick={requestCancelDedupe}
|
||||
>
|
||||
{ko.common.cancel}
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={!running()}>
|
||||
<Show when={!dedupeRunning()}>
|
||||
<button
|
||||
class="rounded bg-[var(--accent)] px-3 py-1 text-xs text-white"
|
||||
onClick={() => void runScan()}
|
||||
@@ -218,7 +215,7 @@ export const DedupePanel: Component = () => {
|
||||
when={groups() && groups()!.length > 0}
|
||||
fallback={
|
||||
<div class="grid h-full place-items-center text-sm text-[var(--text-muted)]">
|
||||
{running() ? ko.dedupe.starting : ko.dedupe.empty}
|
||||
{dedupeRunning() ? ko.dedupe.starting : ko.dedupe.empty}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
||||
+31
-1
@@ -1,6 +1,17 @@
|
||||
import { Show, type Component } from "solid-js";
|
||||
import { ko } from "../i18n/ko";
|
||||
import { info, pending, scanProgress, selected, snapshot, statusMessage } from "../state/store";
|
||||
import {
|
||||
dedupeProgress,
|
||||
dedupeRunning,
|
||||
info,
|
||||
openDedupe,
|
||||
pending,
|
||||
requestCancelDedupe,
|
||||
scanProgress,
|
||||
selected,
|
||||
snapshot,
|
||||
statusMessage,
|
||||
} from "../state/store";
|
||||
|
||||
export const StatusBar: Component = () => {
|
||||
return (
|
||||
@@ -25,6 +36,25 @@ export const StatusBar: Component = () => {
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
{/* 중복 탐지 진행 — 패널을 닫아도 상태바에 표시 (클릭 시 패널 열기 / 취소) */}
|
||||
<Show when={dedupeRunning()}>
|
||||
<span class="flex items-center gap-1.5 text-[var(--accent)]">
|
||||
<span class="inline-block h-2.5 w-2.5 animate-spin rounded-full border border-[var(--accent)] border-t-transparent" />
|
||||
<button class="hover:underline" onClick={openDedupe} title={ko.dedupe.open}>
|
||||
{dedupeProgress()
|
||||
? ko.dedupe.scanning(dedupeProgress()!.phase, dedupeProgress()!.current, dedupeProgress()!.total)
|
||||
: ko.dedupe.starting}
|
||||
</button>
|
||||
<button
|
||||
class="rounded px-1 text-[var(--text-muted)] hover:text-[var(--danger)]"
|
||||
onClick={requestCancelDedupe}
|
||||
title={ko.common.cancel}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</span>
|
||||
</Show>
|
||||
|
||||
{/* 백그라운드 처리 진행 — 썸네일/메타 대기 수 (있을 때만) */}
|
||||
<Show when={pending().thumbs > 0 || pending().meta > 0}>
|
||||
<span class="flex items-center gap-1.5 text-[var(--text-muted)]">
|
||||
|
||||
@@ -3,13 +3,16 @@ import { createSignal } from "solid-js";
|
||||
import { createStore, produce } from "solid-js/store";
|
||||
import {
|
||||
appInfo,
|
||||
cancelDedupe,
|
||||
folderSnapshot,
|
||||
pendingCounts as fetchPendingCounts,
|
||||
searchSnapshot,
|
||||
smartSnapshot,
|
||||
startDedupe,
|
||||
tagSnapshot,
|
||||
Snapshot,
|
||||
type AppInfo,
|
||||
type DedupeProgress,
|
||||
type PendingCounts,
|
||||
type ScanProgress,
|
||||
} from "../ipc/commands";
|
||||
@@ -196,6 +199,39 @@ export { dedupeOpen };
|
||||
export const openDedupe = () => setDedupeOpen(true);
|
||||
export const closeDedupe = () => setDedupeOpen(false);
|
||||
|
||||
// ── 중복 탐지 실행(전역) — 패널을 닫아도 상태바에서 진행이 보이도록 store가 소유 ──
|
||||
const [dedupeProgress, setDedupeProgress] = createSignal<DedupeProgress | undefined>();
|
||||
const [dedupeRunning, setDedupeRunning] = createSignal(false);
|
||||
export { dedupeProgress, dedupeRunning };
|
||||
|
||||
export interface DedupeRunArgs {
|
||||
sourceId?: number;
|
||||
folderId?: number;
|
||||
recursive?: boolean;
|
||||
imageThreshold?: number;
|
||||
includeVideo?: boolean;
|
||||
}
|
||||
|
||||
/** 중복 탐지 실행. 진행 상태는 store에 유지되어 패널을 닫아도 상태바에서 보인다. */
|
||||
export async function runDedupe(args: DedupeRunArgs): Promise<void> {
|
||||
if (dedupeRunning()) return;
|
||||
setDedupeRunning(true);
|
||||
setDedupeProgress(undefined);
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
startDedupe(args, (p) => {
|
||||
setDedupeProgress(p);
|
||||
if (p.phase === "done") resolve();
|
||||
}).catch(reject);
|
||||
});
|
||||
} finally {
|
||||
setDedupeRunning(false);
|
||||
setDedupeProgress(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
export const requestCancelDedupe = () => void cancelDedupe();
|
||||
|
||||
// ── 상태 메시지 (토스트) ─────────────────────────────────
|
||||
const [statusMessage, setStatusMessage] = createSignal<string | undefined>();
|
||||
export { statusMessage };
|
||||
|
||||
Reference in New Issue
Block a user