Files
genshin-assistant/src/features/scan/hooks/useScanViewController.ts
T
2026-07-09 08:44:50 +02:00

276 lines
10 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 { useScanGoodInterop } from "./useScanGoodInterop";
import { useScanSnapshotPublisher } from "./useScanSnapshotPublisher";
import { useScanViewActions } from "./useScanViewActions";
import { useScanViewStateSync } from "./useScanViewStateSync";
import { emptyAutoScanStats, resolveScanTargetCount, type AutoScanStats, type ScanSummary } from "../../../lib/scannerSession";
import { createScanDiagnosticEvent, summarizeClickResult, type ScanDiagnosticEvent } from "../../../lib/scanDiagnosticsLog";
import type { ScanViewProps, ScanViewControllerResult } from "../types";
import type { CaptureResult, ClickResult, ReviewSampleRecord } from "../../../types/global";
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 [diagnosticEvents, setDiagnosticEvents] = useState<ScanDiagnosticEvent[]>([]);
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?.total ?? 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 appendDiagnosticEvent = useCallback((event: Omit<Parameters<typeof createScanDiagnosticEvent>[0], "includeFullScreenshot">) => {
setDiagnosticEvents((previous) => [
...previous.slice(-23),
createScanDiagnosticEvent({
...event,
includeFullScreenshot: false,
}),
]);
}, []);
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"}`);
appendDiagnosticEvent({
phase: "input",
severity: result.inputBlocked || result.clicked === false || result.moved === false ? "warn" : "ok",
message: `${prefix}: click ${result.clicked ? "sent" : "not sent"}`,
details: summarizeClickResult(result),
});
}, [appendAutomationLog, appendDiagnosticEvent]);
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,
runGuidedAutoScan,
runVisibleGridScan,
} = useScanViewActions({
autoScanRunning,
setAutoScanRunning,
isScanning,
stopVisibleScanRef,
selectedSourceId,
bridgeReady,
automationRepo,
runtimeInfo,
runtimeRepo,
scanLimit,
skipRows,
detectedInventoryCount,
setScanSummary,
setAutoScanStats,
setReviewStatus,
appendAutomationLog,
appendClickDiagnostics,
appendDiagnosticEvent,
parseArtifact,
artifactRepo,
reviewSamplesRepo,
learningRepo,
onStoredArtifactsChanged,
setReviewSampleTotal,
setReviewSamples,
setLearningRulesLoaded,
setScannerLearningRules,
setStoredTotal,
scannerLearningRules,
captureSelectedSource,
latestCapture,
parsedArtifact,
canCaptureSource,
setReviewQueueOpen,
});
const {
canGoodInterop,
exportGoodFromStore,
importGoodFromFile,
importGoodArtifacts,
} = useScanGoodInterop({
artifactRepo,
exportRepo,
onStoredArtifactsChanged,
setStoredTotal,
bridgeReady,
});
useScanViewStateSync({
artifactRepo,
latestCapture,
scanLimitTouched,
setScanLimit,
setStoredTotal,
});
useScanSnapshotPublisher({
autoScanRunning,
reviewStatus,
captureStatus,
selectedSourceName: selectedSource?.name ?? null,
autoScanStats,
scanSummary,
snapshot,
latestInventoryGrid: latestCapture?.inventoryGrid ?? null,
automationLog,
diagnosticEvents,
runtimeInfo,
storedTotal,
learningRuleCount,
snapshotRepo,
});
return {
detailsOpen,
diagnosticsOpen,
settingsOpen,
reviewQueueOpen,
reviewSamples,
reviewSampleTotal,
reviewStatus,
autoScanRunning,
autoScanStats,
scanSummary,
scanLimit,
scanLimitTouched,
skipRows,
automationLog,
diagnosticEvents,
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,
runGuidedAutoScan,
runVisibleGridScan,
canGoodInterop,
exportGoodFromStore,
importGoodFromFile,
importGoodArtifacts,
};
}