Files
genshin-assistant/src/features/scan/hooks/useScanViewController.ts
T

277 lines
11 KiB
TypeScript

import { useCallback, useMemo, useRef, useState } from "react";
import { requiresAdminForAutomation } from "../../../lib/automationPlanner";
import {
countScannerLearningRules,
type ScannerLearningRules,
} from "../../../lib/scannerLearning";
import { type ParsedArtifactCandidate } from "../../../lib/artifactOcrParser";
import { analyzeReviewSamples } from "../../../lib/reviewSampleAnalysis";
import {
parseLearnedArtifact as parseLearnedArtifactHelper,
} from "./scanViewReviewHelpers";
import { useScanRuntimeInfo } from "./useScanRuntimeInfo";
import { useScanSnapshotPublisher } from "./useScanSnapshotPublisher";
import { useScanViewActions } from "./useScanViewActions";
import { useScanViewStateSync } from "./useScanViewStateSync";
import { emptyAutoScanStats, resolveScanTargetCount, type AutoScanStats, type ScanSummary } from "../../../lib/scannerSession";
import type { ScanViewProps, ScanViewControllerResult } from "../types";
import type { CaptureResult, ClickResult, ReviewSampleRecord } from "../../../types/global";
import type { StoredArtifactRecord } from "../../../types/storage";
import { goodDatabaseToStoredArtifacts, type GoodImportDatabase, storedArtifactsToGood } from "../../../lib/goodInterop";
import { createRendererRepositories } from "../../../infrastructure/repositories/rendererBridgeRepositories";
export function useScanViewController({
snapshot,
isScanning,
captureSources,
selectedSourceId,
latestCapture,
captureStatus,
captureSelectedSource,
bridgeReady,
onStoredArtifactsChanged,
}: ScanViewProps): ScanViewControllerResult {
const repositories = useMemo(() => createRendererRepositories(), []);
const artifactRepo = repositories?.artifacts;
const runtimeRepo = repositories?.runtime;
const reviewSamplesRepo = repositories?.reviewSamples;
const learningRepo = repositories?.learning;
const snapshotRepo = repositories?.snapshot;
const automationRepo = repositories?.automation;
const captureRepo = repositories?.capture;
const exportRepo = repositories?.export;
const [detailsOpen, setDetailsOpen] = useState(false);
const [diagnosticsOpen, setDiagnosticsOpen] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
const [reviewQueueOpen, setReviewQueueOpen] = useState(false);
const [reviewSamples, setReviewSamples] = useState<ReviewSampleRecord[]>([]);
const [reviewSampleTotal, setReviewSampleTotal] = useState(0);
const [reviewStatus, setReviewStatus] = useState("");
const [autoScanRunning, setAutoScanRunning] = useState(false);
const stopVisibleScanRef = useRef(false);
const [autoScanStats, setAutoScanStats] = useState<AutoScanStats>(emptyAutoScanStats);
const [scanSummary, setScanSummary] = useState<ScanSummary | null>(null);
const [scanLimit, setScanLimit] = useState(16);
const [scanLimitTouched, setScanLimitTouched] = useState(false);
const [skipRows, setSkipRows] = useState(0);
const [automationLog, setAutomationLog] = useState<string[]>([]);
const [storedTotal, setStoredTotal] = useState<number | null>(null);
const [devMode, setDevMode] = useState(() => localStorage.getItem("gaa-dev-mode") === "1");
const [scannerLearningRules, setScannerLearningRules] = useState<ScannerLearningRules>({ textReplacements: {} });
const [learningRulesLoaded, setLearningRulesLoaded] = useState(false);
const runtimeInfo = useScanRuntimeInfo(runtimeRepo);
const parsedArtifact = useMemo(
() => parseLearnedArtifactHelper(latestCapture, scannerLearningRules),
[latestCapture, scannerLearningRules],
);
const selectedSource = captureSources.find((source) => source.id === selectedSourceId);
const genshinSource = captureSources.find((source) => source.isGenshinCandidate);
const requiresAdminForAutoScan = requiresAdminForAutomation(runtimeInfo);
const hasSourceSelected = selectedSourceId.length > 0;
const canCaptureSource = bridgeReady && Boolean(captureRepo?.captureSource);
const canReadReviewQueue = bridgeReady && Boolean(reviewSamplesRepo?.loadSamples);
const canSaveReviewSample = bridgeReady && Boolean(reviewSamplesRepo?.saveSample);
const canAutoScan = bridgeReady && Boolean(automationRepo?.clickScreen) && Boolean(automationRepo?.scrollScreen);
const reviewAnalysis = useMemo(() => analyzeReviewSamples(reviewSamples), [reviewSamples]);
const learningRuleCount = countScannerLearningRules(scannerLearningRules);
const detectedInventoryCount = latestCapture?.inventoryCount?.current ?? 0;
const activeTargetCount = autoScanRunning
? resolveScanTargetCount(scanLimit, detectedInventoryCount)
: scanSummary?.targetCount ?? resolveScanTargetCount(scanLimit, detectedInventoryCount);
const scanProgressBase = Math.max(autoScanStats.attempted, autoScanStats.parsed, autoScanStats.verified);
const scanProgressPercent = Math.min(100, Math.round((scanProgressBase / Math.max(1, activeTargetCount)) * 100));
const scannerModeLabel = autoScanRunning ? "Scan laeuft" : runtimeInfo?.isElevated ? "Admin bereit" : "Bereit";
const sourceLabel = selectedSource?.name ?? "Keine Quelle";
const gridLabel = latestCapture?.inventoryGrid ? `${latestCapture.inventoryGrid.cols} x ${latestCapture.inventoryGrid.rows}` : "warte auf Capture";
const inventoryLabel = latestCapture?.inventoryCount?.current ? `${latestCapture.inventoryCount.current}/${latestCapture.inventoryCount.total || "?"}` : "nicht erkannt";
const appendAutomationLog = useCallback((line: string) => {
setAutomationLog((previous) => [...previous.slice(-11), `${new Date().toLocaleTimeString()} ${line}`]);
}, []);
const appendClickDiagnostics = useCallback((result: ClickResult, prefix = "input") => {
const cursor = `${result.cursorX ?? "?"},${result.cursorY ?? "?"}`;
const focus = result.focused ? `fg:${result.foregroundProcess || "Genshin"}` : `fg-miss:${result.foregroundProcess || "?"}`;
const blocked = result.inputBlocked ? " input:blocked" : "";
appendAutomationLog(`${prefix}: ${focus}${blocked} cursor ${cursor} moved:${result.moved ? "yes" : "no"} clicked:${result.clicked ? "yes" : "no"}`);
}, [appendAutomationLog]);
const toggleDevMode = useCallback(() => {
setDevMode((previous) => {
const next = !previous;
localStorage.setItem("gaa-dev-mode", next ? "1" : "0");
return next;
});
}, []);
const parseArtifact = useCallback(
(capture: CaptureResult | null) => parseLearnedArtifactHelper(capture, scannerLearningRules),
[scannerLearningRules],
);
const {
requestScanStop,
saveReviewSample: handleSaveReviewSample,
loadReviewQueue: loadReviewQueueAction,
openReviewQueue: openReviewQueueModal,
runAutoReviewScan,
runVisibleGridScan,
} = useScanViewActions({
autoScanRunning,
setAutoScanRunning,
isScanning,
stopVisibleScanRef,
selectedSourceId,
bridgeReady,
automationRepo,
runtimeInfo,
runtimeRepo,
scanLimit,
skipRows,
detectedInventoryCount,
setScanSummary,
setAutoScanStats,
setReviewStatus,
appendAutomationLog,
appendClickDiagnostics,
parseArtifact,
artifactRepo,
reviewSamplesRepo,
learningRepo,
onStoredArtifactsChanged,
setReviewSampleTotal,
setReviewSamples,
setLearningRulesLoaded,
setScannerLearningRules,
setStoredTotal,
scannerLearningRules,
captureSelectedSource,
latestCapture,
parsedArtifact,
canCaptureSource,
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]);
const importGoodFromFile = useCallback(async () => {
if (!exportRepo?.importGoodFile || !artifactRepo?.saveMany) {
return { ok: false, added: 0, updated: 0, count: 0, error: "GOOD import is unavailable." };
}
const fileResult = await exportRepo.importGoodFile();
if (fileResult.canceled) return { ok: false, added: 0, updated: 0, count: 0, canceled: true };
if (!fileResult.ok) {
return { ok: false, added: 0, updated: 0, count: 0, path: fileResult.path, error: fileResult.error };
}
const records = goodDatabaseToStoredArtifacts(fileResult.database as GoodImportDatabase);
if (records.length === 0) {
return { ok: false, added: 0, updated: 0, count: 0, path: fileResult.path, error: "No valid GOOD artifacts found." };
}
const saved = await importGoodArtifacts(records);
return { ...saved, count: records.length, path: fileResult.path };
}, [artifactRepo, exportRepo, importGoodArtifacts]);
useScanViewStateSync({
artifactRepo,
latestCapture,
scanLimitTouched,
setScanLimit,
setStoredTotal,
});
useScanSnapshotPublisher({
autoScanRunning,
reviewStatus,
captureStatus,
selectedSourceName: selectedSource?.name ?? null,
autoScanStats,
scanSummary,
snapshot,
latestInventoryGrid: latestCapture?.inventoryGrid ?? null,
automationLog,
runtimeInfo,
storedTotal,
learningRuleCount,
snapshotRepo,
});
return {
detailsOpen,
diagnosticsOpen,
settingsOpen,
reviewQueueOpen,
reviewSamples,
reviewSampleTotal,
reviewStatus,
autoScanRunning,
autoScanStats,
scanSummary,
scanLimit,
scanLimitTouched,
skipRows,
automationLog,
storedTotal,
devMode,
scannerLearningRules,
learningRulesLoaded,
runtimeInfo,
parsedArtifact,
reviewAnalysis,
learningRuleCount,
detectedInventoryCount,
scanProgressPercent,
activeTargetCount,
requiresAdminForAutoScan,
hasSourceSelected,
canCaptureSource,
canReadReviewQueue,
canSaveReviewSample,
canAutoScan,
scannerModeLabel,
sourceLabel,
gridLabel,
inventoryLabel,
selectedSource,
genshinSource,
setDetailsOpen,
setDiagnosticsOpen,
setSettingsOpen,
setReviewQueueOpen,
setScanSummary,
setScanLimit,
setScanLimitTouched,
setSkipRows,
toggleDevMode,
requestScanStop,
saveReviewSample: handleSaveReviewSample,
loadReviewQueue: loadReviewQueueAction,
openReviewQueue: openReviewQueueModal,
runAutoReviewScan,
runVisibleGridScan,
canGoodInterop,
exportGoodFromStore,
importGoodFromFile,
importGoodArtifacts,
};
}