277 lines
11 KiB
TypeScript
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,
|
|
};
|
|
}
|