시작 시 메타 백필 + 백그라운드 처리 진행 표시(상태바)
- 메타 백필: 이전 스캔이 중단돼 meta_state=0으로 남은 파일을 시작 시 (전체 재스캔 없이) 저우선순위로 이어서 분석. 썸네일 백필(enqueue_pending)과 짝. - pending_counts 커맨드 + 상태바 표시: "썸네일 생성 중 N / 메타 분석 중 M". thumbs-ready·library-changed·시작 시 트리거하고, 남은 작업이 있으면 2초 폴링, 0이 되면 자동으로 숨김. 검증: 영상 200개 스캔 중 상태바가 200→0으로 감소 후 사라짐. 메타 도중 강제 종료 후 재실행 시 재스캔 없이(scanning=false) 메타/썸네일 백필만 재개됨. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,36 @@ pub struct SourceDto {
|
|||||||
pub file_count: i64,
|
pub file_count: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct PendingCounts {
|
||||||
|
/// 썸네일 생성 대기(thumb_state=0) 이미지/영상 수
|
||||||
|
pub thumbs: i64,
|
||||||
|
/// 메타데이터 분석 대기(meta_state=0) 수
|
||||||
|
pub meta: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 백그라운드 처리 진행 표시용 — 대기 중인 썸네일/메타 개수.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn pending_counts(state: tauri::State<'_, AppState>) -> CmdResult<PendingCounts> {
|
||||||
|
state
|
||||||
|
.db
|
||||||
|
.with_read(|conn| {
|
||||||
|
let thumbs: i64 = conn.query_row(
|
||||||
|
"SELECT count(*) FROM files WHERE thumb_state=0 AND kind IN (0,1) AND deleted_at IS NULL",
|
||||||
|
[],
|
||||||
|
|r| r.get(0),
|
||||||
|
)?;
|
||||||
|
let meta: i64 = conn.query_row(
|
||||||
|
"SELECT count(*) FROM files WHERE meta_state=0 AND deleted_at IS NULL",
|
||||||
|
[],
|
||||||
|
|r| r.get(0),
|
||||||
|
)?;
|
||||||
|
Ok(PendingCounts { thumbs, meta })
|
||||||
|
})
|
||||||
|
.map_err(err_str)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn list_sources(state: tauri::State<'_, AppState>) -> CmdResult<Vec<SourceDto>> {
|
pub fn list_sources(state: tauri::State<'_, AppState>) -> CmdResult<Vec<SourceDto>> {
|
||||||
state
|
state
|
||||||
|
|||||||
@@ -218,6 +218,34 @@ fn main() {
|
|||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 시작 시 메타데이터 백필 — 이전 스캔이 중단돼 meta_state=0으로 남은 파일을
|
||||||
|
// (전체 재스캔 없이) 이어서 분석한다. 썸네일 백필(enqueue_pending)과 짝. 저우선순위.
|
||||||
|
{
|
||||||
|
let state = app.state::<AppState>();
|
||||||
|
let db = state.db.clone();
|
||||||
|
let tools = state.tools.clone();
|
||||||
|
std::thread::Builder::new()
|
||||||
|
.name("meta-backfill".into())
|
||||||
|
.spawn(move || {
|
||||||
|
archive_indexer::prio::set_current_thread_low();
|
||||||
|
let cancel = std::sync::atomic::AtomicBool::new(false);
|
||||||
|
let sources: Vec<i64> = db
|
||||||
|
.with_read(|c| {
|
||||||
|
let mut s = c.prepare("SELECT id FROM sources WHERE kind='local'")?;
|
||||||
|
let rows = s.query_map([], |r| r.get(0))?;
|
||||||
|
rows.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
for sid in sources {
|
||||||
|
let _ = archive_indexer::meta::extract_image_meta(&db, sid, &cancel);
|
||||||
|
if let Some(t) = &tools {
|
||||||
|
let _ = archive_indexer::meta::extract_video_meta(&db, sid, &cancel, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
// dev 편의: ARCHIVE_AUTO_SOURCE=<폴더> → 시작 시 소스 등록 + 스캔
|
// dev 편의: ARCHIVE_AUTO_SOURCE=<폴더> → 시작 시 소스 등록 + 스캔
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
if let Ok(auto) = std::env::var("ARCHIVE_AUTO_SOURCE") {
|
if let Ok(auto) = std::env::var("ARCHIVE_AUTO_SOURCE") {
|
||||||
@@ -258,6 +286,7 @@ fn main() {
|
|||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
commands::app_info,
|
commands::app_info,
|
||||||
|
commands::pending_counts,
|
||||||
commands::list_sources,
|
commands::list_sources,
|
||||||
commands::add_local_source,
|
commands::add_local_source,
|
||||||
commands::remove_source,
|
commands::remove_source,
|
||||||
|
|||||||
+10
-3
@@ -9,16 +9,23 @@ import { MoveDialog } from "./features/fileops/MoveDialog";
|
|||||||
import { DedupePanel } from "./features/dedupe/DedupePanel";
|
import { DedupePanel } from "./features/dedupe/DedupePanel";
|
||||||
import { RemoteDialog } from "./features/sources/RemoteDialog";
|
import { RemoteDialog } from "./features/sources/RemoteDialog";
|
||||||
import { onLibraryChanged, onThumbsReady, type UnlistenFn } from "./ipc/commands";
|
import { onLibraryChanged, onThumbsReady, type UnlistenFn } from "./ipc/commands";
|
||||||
import { bumpThumbVersions, loadAppInfo, refreshSnapshot } from "./state/store";
|
import { bumpThumbVersions, loadAppInfo, refreshPendingCounts, refreshSnapshot } from "./state/store";
|
||||||
|
|
||||||
const App: Component = () => {
|
const App: Component = () => {
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
void loadAppInfo();
|
void loadAppInfo();
|
||||||
void refreshSnapshot();
|
void refreshSnapshot();
|
||||||
|
refreshPendingCounts();
|
||||||
|
|
||||||
const unlisteners: Promise<UnlistenFn>[] = [
|
const unlisteners: Promise<UnlistenFn>[] = [
|
||||||
onThumbsReady((ids) => bumpThumbVersions(ids)),
|
onThumbsReady((ids) => {
|
||||||
onLibraryChanged(() => onLibraryChangedRefresh()),
|
bumpThumbVersions(ids);
|
||||||
|
refreshPendingCounts();
|
||||||
|
}),
|
||||||
|
onLibraryChanged(() => {
|
||||||
|
onLibraryChangedRefresh();
|
||||||
|
refreshPendingCounts();
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
for (const u of unlisteners) void u.then((f) => f());
|
for (const u of unlisteners) void u.then((f) => f());
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ export const ko = {
|
|||||||
dataDir: "데이터 위치",
|
dataDir: "데이터 위치",
|
||||||
portable: "포터블 모드",
|
portable: "포터블 모드",
|
||||||
installed: "일반 모드",
|
installed: "일반 모드",
|
||||||
|
thumbPending: (n: number) => `썸네일 생성 중 ${n.toLocaleString("ko-KR")}개`,
|
||||||
|
metaPending: (n: number) => `메타 분석 중 ${n.toLocaleString("ko-KR")}개`,
|
||||||
},
|
},
|
||||||
search: {
|
search: {
|
||||||
placeholder: "파일명 검색…",
|
placeholder: "파일명 검색…",
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ export interface ScanProgress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const appInfo = () => invoke<AppInfo>("app_info");
|
export const appInfo = () => invoke<AppInfo>("app_info");
|
||||||
|
export interface PendingCounts {
|
||||||
|
thumbs: number;
|
||||||
|
meta: number;
|
||||||
|
}
|
||||||
|
export const pendingCounts = () => invoke<PendingCounts>("pending_counts");
|
||||||
export const listSources = () => invoke<Source[]>("list_sources");
|
export const listSources = () => invoke<Source[]>("list_sources");
|
||||||
export const addLocalSource = (path: string) =>
|
export const addLocalSource = (path: string) =>
|
||||||
invoke<Source>("add_local_source", { path });
|
invoke<Source>("add_local_source", { path });
|
||||||
|
|||||||
+11
-1
@@ -1,6 +1,6 @@
|
|||||||
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, scanProgress, selected, snapshot, statusMessage } from "../state/store";
|
import { info, pending, scanProgress, selected, snapshot, statusMessage } from "../state/store";
|
||||||
|
|
||||||
export const StatusBar: Component = () => {
|
export const StatusBar: Component = () => {
|
||||||
return (
|
return (
|
||||||
@@ -25,6 +25,16 @@ export const StatusBar: Component = () => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
{/* 백그라운드 처리 진행 — 썸네일/메타 대기 수 (있을 때만) */}
|
||||||
|
<Show when={pending().thumbs > 0 || pending().meta > 0}>
|
||||||
|
<span class="flex items-center gap-1.5 text-[var(--text-muted)]">
|
||||||
|
<span class="inline-block h-2.5 w-2.5 animate-spin rounded-full border border-[var(--text-muted)] border-t-transparent" />
|
||||||
|
<Show when={pending().thumbs > 0}>{ko.statusBar.thumbPending(pending().thumbs)}</Show>
|
||||||
|
<Show when={pending().meta > 0}>
|
||||||
|
<span class="ml-1">{ko.statusBar.metaPending(pending().meta)}</span>
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
<Show when={info()}>
|
<Show when={info()}>
|
||||||
{(i) => (
|
{(i) => (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import { createStore, produce } from "solid-js/store";
|
|||||||
import {
|
import {
|
||||||
appInfo,
|
appInfo,
|
||||||
folderSnapshot,
|
folderSnapshot,
|
||||||
|
pendingCounts as fetchPendingCounts,
|
||||||
searchSnapshot,
|
searchSnapshot,
|
||||||
smartSnapshot,
|
smartSnapshot,
|
||||||
tagSnapshot,
|
tagSnapshot,
|
||||||
Snapshot,
|
Snapshot,
|
||||||
type AppInfo,
|
type AppInfo,
|
||||||
|
type PendingCounts,
|
||||||
type ScanProgress,
|
type ScanProgress,
|
||||||
} from "../ipc/commands";
|
} from "../ipc/commands";
|
||||||
|
|
||||||
@@ -134,6 +136,29 @@ export function updateScanProgress(p: ScanProgress): void {
|
|||||||
setScanProgress(p.done ? undefined : p);
|
setScanProgress(p.done ? undefined : p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 백그라운드 처리 진행(썸네일/메타 대기 수) ────────────
|
||||||
|
const [pending, setPending] = createSignal<PendingCounts>({ thumbs: 0, meta: 0 });
|
||||||
|
export { pending };
|
||||||
|
|
||||||
|
let pendingTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
async function doRefreshPending(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const c = await fetchPendingCounts();
|
||||||
|
setPending(c);
|
||||||
|
clearTimeout(pendingTimer);
|
||||||
|
// 남은 작업이 있으면 계속 폴링(썸네일은 thumbs-ready로도 갱신되지만 메타는 이벤트가 없어 폴링 필요)
|
||||||
|
if (c.thumbs > 0 || c.meta > 0) pendingTimer = setTimeout(() => void doRefreshPending(), 2000);
|
||||||
|
} catch {
|
||||||
|
/* 무시 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 대기 수 갱신 트리거(디바운스). 시작·thumbs-ready·library-changed에서 호출. */
|
||||||
|
export function refreshPendingCounts(): void {
|
||||||
|
clearTimeout(pendingTimer);
|
||||||
|
pendingTimer = setTimeout(() => void doRefreshPending(), 400);
|
||||||
|
}
|
||||||
|
|
||||||
// ── 사이드바 새로고침 트리거 ─────────────────────────────
|
// ── 사이드바 새로고침 트리거 ─────────────────────────────
|
||||||
const [sidebarVersion, setSidebarVersion] = createSignal(0);
|
const [sidebarVersion, setSidebarVersion] = createSignal(0);
|
||||||
export { sidebarVersion };
|
export { sidebarVersion };
|
||||||
|
|||||||
Reference in New Issue
Block a user