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

377 lines
13 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef } from "react";
import type { Dispatch, MutableRefObject, SetStateAction } from "react";
import { runAutoReviewScan as runAutoReviewScanAction, runVisibleGridScan as runVisibleGridScanAction, type VisibleGridScanOptions } from "./scanViewScanActions";
import {
initializeLearningState,
loadReviewQueue as loadReviewQueueFromRepo,
persistParsedArtifact as persistParsedArtifactHelper,
persistParsedArtifactsBatch as persistParsedArtifactsBatchHelper,
saveReviewSample as saveReviewSampleHelper,
} from "./scanViewReviewHelpers";
import { createReviewContext, createScanActionContext } from "./scanViewControllerService";
import { useScanCommandListener } from "./useScanCommandListener";
import type { ParsedArtifactCandidate } from "../../../lib/artifactOcrParser";
import type { ScannerLearningRules } from "../../../lib/scannerLearning";
import type { BooleanResult, CaptureOptions, CaptureResult, ClickResult, ReviewSampleRecord } from "../../../types/global";
import type {
ArtifactRepositoryPort,
ReviewSampleRepositoryPort,
LearningRepositoryPort,
RuntimeRepositoryPort,
AutomationRepositoryPort,
} from "../../../infrastructure/repositories/rendererBridgeRepositoryTypes";
import type { AutoScanStats, ScanSummary } from "../../../lib/scannerSession";
import type { RuntimeInfo } from "../../../types/global";
import type { createScanDiagnosticEvent } from "../../../lib/scanDiagnosticsLog";
import { validateAutoScanEntryPreflight } from "../../../lib/autoScanEntry";
type BooleanSetter = Dispatch<SetStateAction<boolean>>;
type NumberSetter = Dispatch<SetStateAction<number>>;
type StringSetter = Dispatch<SetStateAction<string>>;
type NumberOrNullSetter = Dispatch<SetStateAction<number | null>>;
type ScannerRulesSetter = Dispatch<SetStateAction<ScannerLearningRules>>;
type ReviewSamplesSetter = Dispatch<SetStateAction<ReviewSampleRecord[]>>;
type ScanSummarySetter = Dispatch<SetStateAction<ScanSummary | null>>;
type AutoScanStatsSetter = Dispatch<SetStateAction<AutoScanStats>>;
interface ScanViewActionInput {
autoScanRunning: boolean;
setAutoScanRunning: BooleanSetter;
isScanning: boolean;
stopVisibleScanRef: MutableRefObject<boolean>;
selectedSourceId: string;
bridgeReady: boolean;
automationRepo?: AutomationRepositoryPort;
runtimeInfo?: RuntimeInfo | null;
runtimeRepo?: RuntimeRepositoryPort;
scanLimit: number;
skipRows: number;
detectedInventoryCount: number;
setScanSummary: ScanSummarySetter;
setAutoScanStats: AutoScanStatsSetter;
setReviewStatus: StringSetter;
appendAutomationLog: (line: string) => void;
appendClickDiagnostics: (result: ClickResult, prefix?: string) => void;
appendDiagnosticEvent: (event: Omit<Parameters<typeof createScanDiagnosticEvent>[0], "includeFullScreenshot">) => void;
parseArtifact: (capture: CaptureResult | null) => ParsedArtifactCandidate | null;
artifactRepo?: ArtifactRepositoryPort;
reviewSamplesRepo?: ReviewSampleRepositoryPort;
learningRepo?: LearningRepositoryPort;
onStoredArtifactsChanged?: (() => Promise<void>) | (() => void);
setReviewSampleTotal: NumberSetter;
setReviewSamples: ReviewSamplesSetter;
setLearningRulesLoaded: BooleanSetter;
setScannerLearningRules: ScannerRulesSetter;
setStoredTotal: NumberOrNullSetter;
scannerLearningRules: ScannerLearningRules;
captureSelectedSource: (delayMs?: number, focusGenshin?: boolean, options?: CaptureOptions) => Promise<CaptureResult | null>;
latestCapture: CaptureResult | null;
parsedArtifact: ParsedArtifactCandidate | null;
canCaptureSource: boolean;
setReviewQueueOpen: BooleanSetter;
}
export interface ScanViewActionResult {
requestScanStop: (reason?: string) => void;
saveReviewSample: (
capture?: CaptureResult | null,
parsed?: ParsedArtifactCandidate | null,
reason?: string,
) => Promise<BooleanResult | null>;
loadReviewQueue: () => Promise<void>;
openReviewQueue: () => Promise<void>;
runAutoReviewScan: () => Promise<void>;
runGuidedAutoScan: (options?: { scanLimit?: number; ocrEngine?: CaptureOptions["ocrEngine"] }) => Promise<void>;
runVisibleGridScan: (options?: VisibleGridScanOptions) => Promise<void>;
}
export function useScanViewActions(input: ScanViewActionInput): ScanViewActionResult {
const {
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,
} = input;
const learningInitializedRef = useRef(false);
const requestScanStop = useCallback((reason = "Stop angefordert.") => {
stopVisibleScanRef.current = true;
appendAutomationLog(`stop requested: ${reason}`);
setReviewStatus(`${reason} Der aktuelle Klick/Capture-Schritt wird noch sauber beendet.`);
}, [appendAutomationLog, setReviewStatus, stopVisibleScanRef]);
const reviewContext = useMemo(
() =>
createReviewContext({
artifactRepo,
reviewSamplesRepo,
learningRepo,
onStoredArtifactsChanged,
setReviewSampleTotal,
setReviewSamples,
setLearningRulesLoaded,
setScannerLearningRules,
setReviewStatus,
setStoredTotal,
appendAutomationLog,
}),
[
artifactRepo,
reviewSamplesRepo,
learningRepo,
onStoredArtifactsChanged,
setReviewSampleTotal,
setReviewSamples,
setLearningRulesLoaded,
setScannerLearningRules,
setReviewStatus,
setStoredTotal,
appendAutomationLog,
],
);
useEffect(() => {
if (learningInitializedRef.current) return;
if (!learningRepo && !reviewSamplesRepo && !artifactRepo) return;
learningInitializedRef.current = true;
void initializeLearningState(reviewContext);
}, [artifactRepo, learningRepo, reviewContext, reviewSamplesRepo]);
const focusDashboard = useCallback(async () => {
try {
await automationRepo?.focusMainWindow?.();
} catch {
// Best-effort fallback; state remains in renderer.
}
}, [automationRepo?.focusMainWindow]);
const parseArtifactAndPersist = useCallback(
async function parseArtifactAndPersist(
capture: CaptureResult | null,
parsed: ParsedArtifactCandidate,
source: string,
needsReview: boolean,
) {
return persistParsedArtifactHelper(capture, parsed, source, needsReview, reviewContext);
},
[reviewContext],
);
const parseArtifactsAndPersistBatch = useCallback(
async function parseArtifactsAndPersistBatch(
items: Array<{ capture: CaptureResult | null; parsed: ParsedArtifactCandidate; source: string; needsReview: boolean }>,
) {
return persistParsedArtifactsBatchHelper(items, reviewContext);
},
[reviewContext],
);
const handleSaveReviewSample = useCallback(
async function handleSaveReviewSample(
capture: CaptureResult | null = latestCapture,
parsed: ParsedArtifactCandidate | null = parsedArtifact,
reason = "manual",
): Promise<BooleanResult | null> {
const result = await saveReviewSampleHelper(
capture,
parsed,
reason,
scannerLearningRules,
reviewContext,
);
return result.result ? { ok: result.result.ok } : null;
},
[latestCapture, parsedArtifact, reviewContext, scannerLearningRules],
);
const scanActionContext = useMemo(
() =>
createScanActionContext({
autoScanRunning,
setAutoScanRunning,
isScanning,
stopVisibleScanRef,
selectedSourceId,
bridgeReady,
automationRepo,
runtimeInfo,
runtimeRepo,
scanLimit,
skipRows,
detectedInventoryCount,
setScanSummary,
setAutoScanStats,
setReviewStatus,
appendAutomationLog,
appendClickDiagnostics,
appendDiagnosticEvent,
parseArtifact,
persistParsedArtifact: parseArtifactAndPersist,
persistParsedArtifactsBatch: parseArtifactsAndPersistBatch,
saveReviewSample: handleSaveReviewSample,
focusDashboard,
captureSelectedSource: (delayMs = 0, focusGenshin = false, options) => captureSelectedSource(delayMs, focusGenshin, {
ocrMode: "artifact",
omitFullFrame: true,
omitInventoryPreview: true,
...options,
}),
captureFastSelectedSource: (delayMs = 0, focusGenshin = false, options) => captureSelectedSource(delayMs, focusGenshin, {
skipOcr: true,
omitFullFrame: true,
omitCrops: true,
omitCropImages: true,
omitLockState: true,
...options,
}),
}),
[
autoScanRunning,
setAutoScanRunning,
isScanning,
stopVisibleScanRef,
selectedSourceId,
bridgeReady,
automationRepo,
runtimeInfo,
runtimeRepo,
scanLimit,
skipRows,
detectedInventoryCount,
setScanSummary,
setAutoScanStats,
setReviewStatus,
appendAutomationLog,
appendClickDiagnostics,
appendDiagnosticEvent,
parseArtifact,
parseArtifactAndPersist,
parseArtifactsAndPersistBatch,
handleSaveReviewSample,
focusDashboard,
captureSelectedSource,
],
);
const loadReviewQueueAction = useCallback(async () => {
await loadReviewQueueFromRepo(reviewContext);
}, [reviewContext]);
const openReviewQueueModal = useCallback(async () => {
await loadReviewQueueAction();
setReviewQueueOpen(true);
}, [loadReviewQueueAction, setReviewQueueOpen]);
const runAutoReviewScan = useCallback(async () => {
if (autoScanRunning || !selectedSourceId || !canCaptureSource) return;
await runAutoReviewScanAction(scanActionContext);
}, [autoScanRunning, canCaptureSource, selectedSourceId, scanActionContext]);
const runVisibleGridScan = useCallback(async (options: VisibleGridScanOptions = {}) => {
if (autoScanRunning || !bridgeReady || !selectedSourceId || !automationRepo?.clickScreen || !automationRepo?.scrollScreen) {
return;
}
await runVisibleGridScanAction(scanActionContext, options);
}, [
autoScanRunning,
bridgeReady,
selectedSourceId,
automationRepo?.clickScreen,
automationRepo?.scrollScreen,
scanActionContext,
]);
const runGuidedAutoScan = useCallback(async (options: { scanLimit?: number; ocrEngine?: CaptureOptions["ocrEngine"] } = {}) => {
if (autoScanRunning || !bridgeReady || !selectedSourceId || !automationRepo?.clickScreen || !automationRepo?.scrollScreen) {
return;
}
setReviewStatus("Auto-Scan prueft den Startzustand ohne OCR...");
const preflightCapture = await captureSelectedSource(0, true, {
skipOcr: true,
omitFullFrame: true,
omitCrops: true,
omitCropImages: true,
omitLockState: true,
});
const visibleInventoryReady = validateAutoScanEntryPreflight(preflightCapture).ok;
appendDiagnosticEvent({
phase: "guided-start",
severity: visibleInventoryReady ? "ok" : "info",
message: visibleInventoryReady
? "Artifact inventory detail view already visible; starting scan directly."
: "Artifact detail view is not ready; guided scan waits for a visible artifact detail card instead of navigating.",
capture: preflightCapture,
});
if (!visibleInventoryReady) {
setReviewStatus("Auto-Scan wartet: Bitte Artifact-Inventar mit sichtbarer Detailkarte oeffnen und erneut starten.");
return;
}
await runVisibleGridScanAction(scanActionContext, {
scanLimit: options.scanLimit,
scanEntryMode: "visible-inventory",
processInitialSelection: true,
ocrEngine: options.ocrEngine,
});
}, [
autoScanRunning,
bridgeReady,
selectedSourceId,
automationRepo?.clickScreen,
automationRepo?.scrollScreen,
setReviewStatus,
captureSelectedSource,
appendDiagnosticEvent,
scanActionContext,
]);
useScanCommandListener({
automationRepo,
autoScanRunning,
isScanning,
selectedSourceId,
requestScanStop,
runGuidedAutoScan,
runVisibleGridScan,
});
return {
requestScanStop,
saveReviewSample: handleSaveReviewSample,
loadReviewQueue: loadReviewQueueAction,
openReviewQueue: openReviewQueueModal,
runAutoReviewScan,
runGuidedAutoScan,
runVisibleGridScan,
};
}