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([]); const [reviewSampleTotal, setReviewSampleTotal] = useState(0); const [reviewStatus, setReviewStatus] = useState(""); const [autoScanRunning, setAutoScanRunning] = useState(false); const stopVisibleScanRef = useRef(false); const [autoScanStats, setAutoScanStats] = useState(emptyAutoScanStats); const [scanSummary, setScanSummary] = useState(null); const [scanLimit, setScanLimit] = useState(16); const [scanLimitTouched, setScanLimitTouched] = useState(false); const [skipRows, setSkipRows] = useState(0); const [automationLog, setAutomationLog] = useState([]); const [storedTotal, setStoredTotal] = useState(null); const [devMode, setDevMode] = useState(() => localStorage.getItem("gaa-dev-mode") === "1"); const [scannerLearningRules, setScannerLearningRules] = useState({ 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, }; }