feat(good): wire GOOD import/export UI into the Diagnose view
Completes the GOOD interop point end-to-end (the conversion engine landed earlier in goodInterop.ts). No new IPC needed - reuses the existing artifacts:load / artifacts:saveMany / good:export bridge. - Scan controller gains exportGoodFromStore (loadArtifacts -> storedArtifactsToGood -> exportGood) and importGoodArtifacts (saveMany + snapshot refresh), plus a canGoodInterop flag. - DiagnosticsView adds a GOOD Interop card: export the scan store as GOOD, or import a GOOD file. The file is read in the renderer via a file input + goodDatabaseToStoredArtifacts, so no file-dialog IPC is required. Verified in the browser preview after a clean restart: the Diagnose view renders all cards (Status, Last scan, GOOD Interop, Automation log, Crops/OCR), the Scan<->Diagnose switch works with no console errors (the earlier hook-order warnings were stale-HMR artifacts from deleting files mid-session). 120 tests + build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import { AlertTriangle, Play, Wrench } from "lucide-react";
|
import { useRef, useState, type ChangeEvent } from "react";
|
||||||
|
import { AlertTriangle, Download, Play, Upload, Wrench } from "lucide-react";
|
||||||
import type { CaptureResult } from "../../../types/global";
|
import type { CaptureResult } from "../../../types/global";
|
||||||
import type { ScanViewControllerResult } from "../types";
|
import type { ScanViewControllerResult } from "../types";
|
||||||
|
import { goodDatabaseToStoredArtifacts, type GoodImportDatabase } from "../../../lib/goodInterop";
|
||||||
import { FieldConfidenceList } from "./ScanResultCards";
|
import { FieldConfidenceList } from "./ScanResultCards";
|
||||||
import { useScanDiagnosticsModalModel } from "./modals/hooks/useScanDiagnosticsModalModel";
|
import { useScanDiagnosticsModalModel } from "./modals/hooks/useScanDiagnosticsModalModel";
|
||||||
import { useScanDetailsModalModel } from "./modals/hooks/useScanDetailsModalModel";
|
import { useScanDetailsModalModel } from "./modals/hooks/useScanDetailsModalModel";
|
||||||
@@ -56,6 +58,42 @@ export function DiagnosticsView({ controller, latestCapture, captureStatus, onDe
|
|||||||
latestCapture,
|
latestCapture,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [interopStatus, setInteropStatus] = useState("");
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleExportGood = async () => {
|
||||||
|
setInteropStatus("Exportiere GOOD...");
|
||||||
|
const result = await controller.exportGoodFromStore();
|
||||||
|
setInteropStatus(
|
||||||
|
result.ok
|
||||||
|
? `GOOD exportiert: ${result.count} Artifacts${result.path ? ` -> ${result.path}` : ""}`
|
||||||
|
: "GOOD-Export fehlgeschlagen (App im Electron-Fenster oeffnen).",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImportGood = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
event.target.value = "";
|
||||||
|
if (!file) return;
|
||||||
|
try {
|
||||||
|
const database = JSON.parse(await file.text()) as GoodImportDatabase;
|
||||||
|
const records = goodDatabaseToStoredArtifacts(database);
|
||||||
|
if (records.length === 0) {
|
||||||
|
setInteropStatus("Keine gueltigen Artifacts in der Datei gefunden.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInteropStatus(`Importiere ${records.length} Artifacts...`);
|
||||||
|
const result = await controller.importGoodArtifacts(records);
|
||||||
|
setInteropStatus(
|
||||||
|
result.ok
|
||||||
|
? `Importiert: ${result.added} neu, ${result.updated} aktualisiert.`
|
||||||
|
: "Import fehlgeschlagen (App im Electron-Fenster oeffnen).",
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
setInteropStatus("Datei ist kein gueltiges GOOD/JSON.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="diagnose-view">
|
<section className="diagnose-view">
|
||||||
<div className="diagnose-header">
|
<div className="diagnose-header">
|
||||||
@@ -136,6 +174,27 @@ export function DiagnosticsView({ controller, latestCapture, captureStatus, onDe
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="diagnose-card">
|
||||||
|
<div className="diagnose-card-heading">
|
||||||
|
<p className="eyebrow">GOOD Interop</p>
|
||||||
|
<div className="diagnose-header-actions">
|
||||||
|
<button className="ghost-button" onClick={handleExportGood} disabled={!controller.canGoodInterop}>
|
||||||
|
<Download size={15} />
|
||||||
|
GOOD exportieren
|
||||||
|
</button>
|
||||||
|
<button className="ghost-button" onClick={() => fileInputRef.current?.click()} disabled={!controller.canGoodInterop}>
|
||||||
|
<Upload size={15} />
|
||||||
|
GOOD importieren
|
||||||
|
</button>
|
||||||
|
<input ref={fileInputRef} type="file" accept="application/json,.json" hidden onChange={handleImportGood} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="scanner-subcopy">
|
||||||
|
Exportiert den Scan-Store als GOOD (Genshin Optimizer / Inventory Kamera / Akasha) oder importiert eine GOOD-Datei in den Store.
|
||||||
|
</p>
|
||||||
|
{interopStatus && <p className="review-status">{interopStatus}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="diagnose-card">
|
<div className="diagnose-card">
|
||||||
<p className="eyebrow">Automation log</p>
|
<p className="eyebrow">Automation log</p>
|
||||||
<div className="automation-log-lines">
|
<div className="automation-log-lines">
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { useScanViewStateSync } from "./useScanViewStateSync";
|
|||||||
import { emptyAutoScanStats, resolveScanTargetCount, type AutoScanStats, type ScanSummary } from "../../../lib/scannerSession";
|
import { emptyAutoScanStats, resolveScanTargetCount, type AutoScanStats, type ScanSummary } from "../../../lib/scannerSession";
|
||||||
import type { ScanViewProps, ScanViewControllerResult } from "../types";
|
import type { ScanViewProps, ScanViewControllerResult } from "../types";
|
||||||
import type { CaptureResult, ClickResult, ReviewSampleRecord } from "../../../types/global";
|
import type { CaptureResult, ClickResult, ReviewSampleRecord } from "../../../types/global";
|
||||||
|
import type { StoredArtifactRecord } from "../../../types/storage";
|
||||||
|
import { storedArtifactsToGood } from "../../../lib/goodInterop";
|
||||||
import { createRendererRepositories } from "../../../infrastructure/repositories/rendererBridgeRepositories";
|
import { createRendererRepositories } from "../../../infrastructure/repositories/rendererBridgeRepositories";
|
||||||
|
|
||||||
export function useScanViewController({
|
export function useScanViewController({
|
||||||
@@ -37,6 +39,7 @@ export function useScanViewController({
|
|||||||
const snapshotRepo = repositories?.snapshot;
|
const snapshotRepo = repositories?.snapshot;
|
||||||
const automationRepo = repositories?.automation;
|
const automationRepo = repositories?.automation;
|
||||||
const captureRepo = repositories?.capture;
|
const captureRepo = repositories?.capture;
|
||||||
|
const exportRepo = repositories?.export;
|
||||||
|
|
||||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||||
const [diagnosticsOpen, setDiagnosticsOpen] = useState(false);
|
const [diagnosticsOpen, setDiagnosticsOpen] = useState(false);
|
||||||
@@ -152,6 +155,25 @@ export function useScanViewController({
|
|||||||
setReviewQueueOpen,
|
setReviewQueueOpen,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const canGoodInterop = bridgeReady && Boolean(artifactRepo?.loadAll) && Boolean(artifactRepo?.saveMany);
|
||||||
|
|
||||||
|
const exportGoodFromStore = useCallback(async () => {
|
||||||
|
if (!artifactRepo?.loadAll || !exportRepo?.exportGood) return { ok: false, count: 0 };
|
||||||
|
const loaded = await artifactRepo.loadAll();
|
||||||
|
const records = loaded.artifacts ?? [];
|
||||||
|
const good = storedArtifactsToGood(records);
|
||||||
|
const result = await exportRepo.exportGood(good);
|
||||||
|
return { ok: Boolean(result.ok), path: result.path, count: good.artifacts.length };
|
||||||
|
}, [artifactRepo, exportRepo]);
|
||||||
|
|
||||||
|
const importGoodArtifacts = useCallback(async (records: StoredArtifactRecord[]) => {
|
||||||
|
if (!artifactRepo?.saveMany || records.length === 0) return { ok: false, added: 0, updated: 0 };
|
||||||
|
const result = await artifactRepo.saveMany(records);
|
||||||
|
if (typeof result.total === "number") setStoredTotal(result.total);
|
||||||
|
await onStoredArtifactsChanged?.();
|
||||||
|
return { ok: Boolean(result.ok), added: result.added ?? 0, updated: result.updated ?? 0 };
|
||||||
|
}, [artifactRepo, onStoredArtifactsChanged]);
|
||||||
|
|
||||||
useScanViewStateSync({
|
useScanViewStateSync({
|
||||||
artifactRepo,
|
artifactRepo,
|
||||||
latestCapture,
|
latestCapture,
|
||||||
@@ -229,5 +251,8 @@ export function useScanViewController({
|
|||||||
openReviewQueue: openReviewQueueModal,
|
openReviewQueue: openReviewQueueModal,
|
||||||
runAutoReviewScan,
|
runAutoReviewScan,
|
||||||
runVisibleGridScan,
|
runVisibleGridScan,
|
||||||
|
canGoodInterop,
|
||||||
|
exportGoodFromStore,
|
||||||
|
importGoodArtifacts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { AppSnapshot } from "../../types/domain";
|
import type { AppSnapshot } from "../../types/domain";
|
||||||
import type { BooleanResult, CaptureOptions, CaptureResult, CaptureSourceInfo, RuntimeInfo, ReviewSampleRecord } from "../../types/global";
|
import type { BooleanResult, CaptureOptions, CaptureResult, CaptureSourceInfo, RuntimeInfo, ReviewSampleRecord } from "../../types/global";
|
||||||
|
import type { StoredArtifactRecord } from "../../types/storage";
|
||||||
import type { AutoScanStats, ScanSummary } from "../../lib/scannerSession";
|
import type { AutoScanStats, ScanSummary } from "../../lib/scannerSession";
|
||||||
import type { ParsedArtifactCandidate } from "../../lib/artifactOcrParser";
|
import type { ParsedArtifactCandidate } from "../../lib/artifactOcrParser";
|
||||||
import type { ScannerLearningRules } from "../../lib/scannerLearning";
|
import type { ScannerLearningRules } from "../../lib/scannerLearning";
|
||||||
@@ -79,4 +80,7 @@ export interface ScanViewControllerResult {
|
|||||||
openReviewQueue: () => Promise<void>;
|
openReviewQueue: () => Promise<void>;
|
||||||
runAutoReviewScan: () => Promise<void>;
|
runAutoReviewScan: () => Promise<void>;
|
||||||
runVisibleGridScan: () => Promise<void>;
|
runVisibleGridScan: () => Promise<void>;
|
||||||
|
canGoodInterop: boolean;
|
||||||
|
exportGoodFromStore: () => Promise<{ ok: boolean; path?: string; count: number }>;
|
||||||
|
importGoodArtifacts: (records: StoredArtifactRecord[]) => Promise<{ ok: boolean; added: number; updated: number }>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user