mirror of
https://github.com/jung-geun/NFD2NFC.git
synced 2026-06-21 12:25:14 +09:00
src/preload + src/renderer: contextBridge + React 19 popover/settings
preload/index.ts: window.api 노출 (dirs/watcher/undo/settings/events/app). renderer/popover: 트레이 팝오버 (300×400, frameless, alwaysOnTop, blur시 자동 닫힘). renderer/settings: 설정창 (720×560, 3탭: 디렉토리/Undo기록/일반). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
113
src/renderer/popover/Popover.tsx
Normal file
113
src/renderer/popover/Popover.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import type { WatchedDir, ActivityEvent } from '../../core/types';
|
||||
|
||||
const api = (window as any).api;
|
||||
|
||||
export function Popover() {
|
||||
const [dirs, setDirs] = useState<WatchedDir[]>([]);
|
||||
const [activity, setActivity] = useState<ActivityEvent[]>([]);
|
||||
const [paused, setPaused] = useState(false);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setDirs(await api.dirs.list());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// 초기 상태: main process에서 직접 읽음 (단일 진실의 원천)
|
||||
api.watcher.status().then(({ paused: p }: { paused: boolean }) => setPaused(p));
|
||||
refresh();
|
||||
|
||||
const unsubActivity = api.events.onActivity((ev: ActivityEvent) => {
|
||||
setActivity((prev) => [ev, ...prev].slice(0, 20));
|
||||
refresh();
|
||||
});
|
||||
|
||||
// main에서 pause 상태가 바뀔 때마다 업데이트 (트레이 컨텍스트 메뉴 포함)
|
||||
const unsubPaused = api.events.onPausedChange((p: boolean) => setPaused(p));
|
||||
|
||||
return () => {
|
||||
unsubActivity();
|
||||
unsubPaused();
|
||||
};
|
||||
}, [refresh]);
|
||||
|
||||
const togglePause = async () => {
|
||||
if (paused) {
|
||||
await api.watcher.resumeAll();
|
||||
} else {
|
||||
await api.watcher.pauseAll();
|
||||
}
|
||||
// 상태는 onPausedChange 이벤트로 자동 반영되므로 직접 setPaused 불필요
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="popover">
|
||||
<div className="popover-header">
|
||||
<h1>NFD → NFC</h1>
|
||||
<div className="popover-actions">
|
||||
<button className="secondary" onClick={togglePause} title={paused ? '재개' : '일시정지'}>
|
||||
{paused ? '▶ 재개' : '⏸ 정지'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dir-list">
|
||||
{dirs.length === 0 && (
|
||||
<div className="empty-state">
|
||||
감시 중인 디렉토리가 없습니다.<br />
|
||||
설정에서 추가하세요.
|
||||
</div>
|
||||
)}
|
||||
{dirs.map((dir) => (
|
||||
<DirRow key={dir.id} dir={dir} paused={paused} onRefresh={refresh} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="activity-log">
|
||||
<h3>최근 활동</h3>
|
||||
{activity.length === 0 && <div className="activity-item">활동 없음</div>}
|
||||
{activity.map((ev, i) => (
|
||||
<div key={i} className={`activity-item ${ev.type === 'error' || ev.type === 'collision' ? 'error' : ''}`}>
|
||||
{ev.message.split('/').pop() ?? ev.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="popover-footer">
|
||||
<button className="secondary" onClick={() => api.undo.revertLastBatch()}>Undo 마지막 배치</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DirRow({ dir, paused, onRefresh }: { dir: WatchedDir; paused: boolean; onRefresh: () => void }) {
|
||||
const [pending, setPending] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (dir.mode === 'manual') {
|
||||
api.dirs.pendingQueue(dir.id).then((q: string[]) => setPending(q.length));
|
||||
}
|
||||
}, [dir]);
|
||||
|
||||
const applyQueue = async () => {
|
||||
await api.dirs.applyQueue(dir.id);
|
||||
setPending(0);
|
||||
onRefresh();
|
||||
};
|
||||
|
||||
const statusClass = paused ? 'paused' : dir.enabled ? 'active' : 'disabled';
|
||||
const dirName = dir.path.split('/').pop() ?? dir.path;
|
||||
|
||||
return (
|
||||
<div className="dir-row">
|
||||
<div className={`status-dot ${statusClass}`} title={statusClass} />
|
||||
<div className="dir-info">
|
||||
<div className="dir-name">{dirName}</div>
|
||||
<div className="dir-path">{dir.path}</div>
|
||||
</div>
|
||||
{dir.mode === 'manual' && pending > 0 && (
|
||||
<button onClick={applyQueue}>{pending}개 변환</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user