소스 제외(인덱스에서 제거) 기능 추가
- 사이드바 소스 행 hover 시 ✕ 버튼 → 확인 후 인덱스/폴더/태그/썸네일 캐시 제거. 디스크의 실제 파일은 삭제하지 않음. - 로컬: 파일 워처 해제. 원격: 연결 해제 + 자격증명(키체인/볼트) 삭제. - maintenance::remove_source (defer_foreign_keys로 자기참조 FK 처리), 스캔 중 제외 방지, 제외한 소스를 보던 중이면 전체 뷰로 전환. Rust 테스트: 실제 파일 보존 + 인덱스/썸네일 제거 검증 통과. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,10 @@ export const ko = {
|
||||
dedupe: "중복 항목",
|
||||
addSource: "폴더",
|
||||
noSources: "등록된 소스가 없습니다.\n폴더를 추가해 시작하세요.",
|
||||
removeConfirm: (name: string) =>
|
||||
`소스 "${name}"를 목록에서 제외할까요?\n\n인덱스·태그·썸네일만 제거되며, 디스크의 실제 파일은 삭제되지 않습니다.`,
|
||||
removed: (name: string, n: number) =>
|
||||
`"${name}" 제외됨 (${n.toLocaleString("ko-KR")}개 항목 인덱스에서 제거)`,
|
||||
},
|
||||
onboarding: {
|
||||
title: "Archive에 오신 것을 환영합니다",
|
||||
|
||||
@@ -52,6 +52,9 @@ export const appInfo = () => invoke<AppInfo>("app_info");
|
||||
export const listSources = () => invoke<Source[]>("list_sources");
|
||||
export const addLocalSource = (path: string) =>
|
||||
invoke<Source>("add_local_source", { path });
|
||||
/** 소스를 인덱스에서 제외 (실제 파일은 삭제하지 않음). 반환: 제거된 항목 수 */
|
||||
export const removeSource = (sourceId: number) =>
|
||||
invoke<number>("remove_source", { sourceId });
|
||||
export const cancelScan = () => invoke<void>("cancel_scan");
|
||||
export const folderTree = (sourceId: number) =>
|
||||
invoke<FolderNode[]>("folder_tree", { sourceId });
|
||||
|
||||
+35
-2
@@ -19,6 +19,7 @@ import {
|
||||
listSmartFolders,
|
||||
listSources,
|
||||
listTags,
|
||||
removeSource,
|
||||
startScan,
|
||||
type FolderNode,
|
||||
type Source,
|
||||
@@ -133,10 +134,35 @@ const SourceRow: Component<{ source: Source }> = (props) => {
|
||||
const v = view();
|
||||
return v.type === "source" && v.sourceId === props.source.id;
|
||||
};
|
||||
const onRemove = async (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const yes = await ask(ko.sidebar.removeConfirm(props.source.name), {
|
||||
title: "Archive",
|
||||
kind: "warning",
|
||||
});
|
||||
if (!yes) return;
|
||||
try {
|
||||
const n = await removeSource(props.source.id);
|
||||
toast(ko.sidebar.removed(props.source.name, n));
|
||||
// 제외한 소스를 보고 있었다면 전체 뷰로
|
||||
const v = view();
|
||||
if (
|
||||
(v.type === "source" && v.sourceId === props.source.id) ||
|
||||
v.type === "folder"
|
||||
) {
|
||||
void setView({ type: "all" });
|
||||
}
|
||||
refreshSidebar();
|
||||
await refreshSnapshot();
|
||||
} catch (err) {
|
||||
toast(String(err));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
class="flex cursor-pointer items-center gap-1.5 rounded px-2 py-1 text-xs hover:bg-[var(--bg-hover)]"
|
||||
class="group flex cursor-pointer items-center gap-1.5 rounded px-2 py-1 text-xs hover:bg-[var(--bg-hover)]"
|
||||
classList={{ "bg-[var(--bg-active)]": isActive() }}
|
||||
onClick={() => void setView({ type: "source", sourceId: props.source.id })}
|
||||
>
|
||||
@@ -152,9 +178,16 @@ const SourceRow: Component<{ source: Source }> = (props) => {
|
||||
<span class="truncate font-medium text-[var(--text-primary)]" title={props.source.root}>
|
||||
{props.source.name}
|
||||
</span>
|
||||
<span class="ml-auto shrink-0 text-[10px] text-[var(--text-muted)]">
|
||||
<span class="ml-auto shrink-0 text-[10px] text-[var(--text-muted)] group-hover:hidden">
|
||||
{props.source.fileCount.toLocaleString("ko-KR")}
|
||||
</span>
|
||||
<button
|
||||
class="ml-auto hidden shrink-0 rounded px-1 text-[11px] text-[var(--text-muted)] hover:text-[var(--danger)] group-hover:block"
|
||||
onClick={(e) => void onRemove(e)}
|
||||
title="소스 제외 (파일은 삭제되지 않음)"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<Show when={expanded() && tree()}>
|
||||
<For each={tree()}>{(n) => <FolderRow node={n} depth={1} />}</For>
|
||||
|
||||
Reference in New Issue
Block a user