중복 탐지 진행을 상태바에 표시 — 패널을 닫아도 진행/취소 가능
기존엔 중복 탐지 진행이 전체화면 패널 안에서만 보였고, 패널을 닫으면 진행 상황을 알 수 없었다. - 중복 탐지 실행/진행 상태를 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,
|
createResource,
|
||||||
createSignal,
|
createSignal,
|
||||||
For,
|
For,
|
||||||
|
on,
|
||||||
Show,
|
Show,
|
||||||
type Component,
|
type Component,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { ask } from "@tauri-apps/plugin-dialog";
|
import { ask } from "@tauri-apps/plugin-dialog";
|
||||||
import { ko } from "../../i18n/ko";
|
import { ko } from "../../i18n/ko";
|
||||||
import {
|
import {
|
||||||
cancelDedupe,
|
|
||||||
dismissDupePair,
|
dismissDupePair,
|
||||||
listDupeGroups,
|
listDupeGroups,
|
||||||
resolveDupeGroup,
|
resolveDupeGroup,
|
||||||
startDedupe,
|
|
||||||
thumbUrl,
|
thumbUrl,
|
||||||
trashFiles,
|
trashFiles,
|
||||||
type DedupeProgress,
|
|
||||||
type DupeGroup,
|
type DupeGroup,
|
||||||
} from "../../ipc/commands";
|
} from "../../ipc/commands";
|
||||||
import {
|
import {
|
||||||
closeDedupe,
|
closeDedupe,
|
||||||
dedupeOpen,
|
dedupeOpen,
|
||||||
|
dedupeProgress,
|
||||||
|
dedupeRunning,
|
||||||
info,
|
info,
|
||||||
refreshSidebar,
|
refreshSidebar,
|
||||||
refreshSnapshot,
|
refreshSnapshot,
|
||||||
|
requestCancelDedupe,
|
||||||
|
runDedupe,
|
||||||
toast,
|
toast,
|
||||||
view,
|
view,
|
||||||
} from "../../state/store";
|
} from "../../state/store";
|
||||||
@@ -40,8 +42,6 @@ function fmtSize(bytes: number): string {
|
|||||||
const KIND_LABEL: Record<number, string> = { 0: "완전 동일", 1: "유사 이미지", 2: "유사 영상" };
|
const KIND_LABEL: Record<number, string> = { 0: "완전 동일", 1: "유사 이미지", 2: "유사 영상" };
|
||||||
|
|
||||||
export const DedupePanel: Component = () => {
|
export const DedupePanel: Component = () => {
|
||||||
const [running, setRunning] = createSignal(false);
|
|
||||||
const [progress, setProgress] = createSignal<DedupeProgress | undefined>();
|
|
||||||
const [includeVideo, setIncludeVideo] = createSignal(false);
|
const [includeVideo, setIncludeVideo] = createSignal(false);
|
||||||
const [threshold, setThreshold] = createSignal(5);
|
const [threshold, setThreshold] = createSignal(5);
|
||||||
const [groupsVersion, setGroupsVersion] = createSignal(0);
|
const [groupsVersion, setGroupsVersion] = createSignal(0);
|
||||||
@@ -67,28 +67,25 @@ export const DedupePanel: Component = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const runScan = async () => {
|
const runScan = async () => {
|
||||||
setRunning(true);
|
|
||||||
setProgress(undefined);
|
|
||||||
const scoped = useCurrentScope() ? currentScope() : null;
|
const scoped = useCurrentScope() ? currentScope() : null;
|
||||||
try {
|
try {
|
||||||
await new Promise<void>((resolve, reject) => {
|
await runDedupe({ ...(scoped?.args ?? {}), imageThreshold: threshold(), includeVideo: includeVideo() });
|
||||||
startDedupe(
|
|
||||||
{ ...(scoped?.args ?? {}), imageThreshold: threshold(), includeVideo: includeVideo() },
|
|
||||||
(p) => {
|
|
||||||
setProgress(p);
|
|
||||||
if (p.phase === "done") resolve();
|
|
||||||
},
|
|
||||||
).catch(reject);
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast(String(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 외 전부 휴지통 */
|
/** keeper 외 전부 휴지통 */
|
||||||
const resolveGroup = async (g: DupeGroup) => {
|
const resolveGroup = async (g: DupeGroup) => {
|
||||||
const losers = g.members.filter((m) => !m.isKeeper).map((m) => m.fileId);
|
const losers = g.members.filter((m) => !m.isKeeper).map((m) => m.fileId);
|
||||||
@@ -181,20 +178,20 @@ export const DedupePanel: Component = () => {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-auto flex items-center gap-2">
|
<div class="ml-auto flex items-center gap-2">
|
||||||
<Show when={running()}>
|
<Show when={dedupeRunning()}>
|
||||||
<span class="text-[11px] text-[var(--accent)]">
|
<span class="text-[11px] text-[var(--accent)]">
|
||||||
{progress()
|
{dedupeProgress()
|
||||||
? ko.dedupe.scanning(progress()!.phase, progress()!.current, progress()!.total)
|
? ko.dedupe.scanning(dedupeProgress()!.phase, dedupeProgress()!.current, dedupeProgress()!.total)
|
||||||
: ko.dedupe.starting}
|
: ko.dedupe.starting}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
class="rounded border border-[var(--border)] px-2 py-1 text-xs hover:bg-[var(--bg-hover)]"
|
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}
|
{ko.common.cancel}
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!running()}>
|
<Show when={!dedupeRunning()}>
|
||||||
<button
|
<button
|
||||||
class="rounded bg-[var(--accent)] px-3 py-1 text-xs text-white"
|
class="rounded bg-[var(--accent)] px-3 py-1 text-xs text-white"
|
||||||
onClick={() => void runScan()}
|
onClick={() => void runScan()}
|
||||||
@@ -218,7 +215,7 @@ export const DedupePanel: Component = () => {
|
|||||||
when={groups() && groups()!.length > 0}
|
when={groups() && groups()!.length > 0}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="grid h-full place-items-center text-sm text-[var(--text-muted)]">
|
<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>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
+31
-1
@@ -1,6 +1,17 @@
|
|||||||
import { Show, type Component } from "solid-js";
|
import { Show, type Component } from "solid-js";
|
||||||
import { ko } from "../i18n/ko";
|
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 = () => {
|
export const StatusBar: Component = () => {
|
||||||
return (
|
return (
|
||||||
@@ -25,6 +36,25 @@ export const StatusBar: Component = () => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</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}>
|
<Show when={pending().thumbs > 0 || pending().meta > 0}>
|
||||||
<span class="flex items-center gap-1.5 text-[var(--text-muted)]">
|
<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 { createStore, produce } from "solid-js/store";
|
||||||
import {
|
import {
|
||||||
appInfo,
|
appInfo,
|
||||||
|
cancelDedupe,
|
||||||
folderSnapshot,
|
folderSnapshot,
|
||||||
pendingCounts as fetchPendingCounts,
|
pendingCounts as fetchPendingCounts,
|
||||||
searchSnapshot,
|
searchSnapshot,
|
||||||
smartSnapshot,
|
smartSnapshot,
|
||||||
|
startDedupe,
|
||||||
tagSnapshot,
|
tagSnapshot,
|
||||||
Snapshot,
|
Snapshot,
|
||||||
type AppInfo,
|
type AppInfo,
|
||||||
|
type DedupeProgress,
|
||||||
type PendingCounts,
|
type PendingCounts,
|
||||||
type ScanProgress,
|
type ScanProgress,
|
||||||
} from "../ipc/commands";
|
} from "../ipc/commands";
|
||||||
@@ -196,6 +199,39 @@ export { dedupeOpen };
|
|||||||
export const openDedupe = () => setDedupeOpen(true);
|
export const openDedupe = () => setDedupeOpen(true);
|
||||||
export const closeDedupe = () => setDedupeOpen(false);
|
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>();
|
const [statusMessage, setStatusMessage] = createSignal<string | undefined>();
|
||||||
export { statusMessage };
|
export { statusMessage };
|
||||||
|
|||||||
Reference in New Issue
Block a user