Prepare scanner branch for merge
This commit is contained in:
+103
-163
@@ -1,5 +1,4 @@
|
||||
import { app, BrowserWindow, Menu, desktopCapturer, dialog, globalShortcut, nativeImage, screen, type NativeImage } from "electron";
|
||||
import fs from "node:fs/promises";
|
||||
import { app, desktopCapturer, globalShortcut, nativeImage, screen, type NativeImage } from "electron";
|
||||
import { existsSync } from "node:fs";
|
||||
import type { Server } from "node:http";
|
||||
import { cpus } from "node:os";
|
||||
@@ -7,18 +6,19 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createWorker, PSM } from "tesseract.js";
|
||||
import { createInputHelperService, type InputHelperService } from "./services/inputHelper.js";
|
||||
import { pngBufferToBitmap } from "./services/pngBitmap.js";
|
||||
import { createRepositoryContext, type RepositoryContext } from "./bootstrap/repositoryContext.js";
|
||||
import { registerIpcHandlers } from "./bootstrap/ipcBootstrap.js";
|
||||
import { createDevControlServer } from "./devControlServer.js";
|
||||
import { createAppWindowManager, type AppWindowManager } from "./appWindowManager.js";
|
||||
import { createGoodFileService, type GoodFileService } from "./services/goodFileService.js";
|
||||
import type { AppSnapshot } from "../src/types/domain.js";
|
||||
import type {
|
||||
CaptureOptions,
|
||||
CaptureResult,
|
||||
GoodDatabase,
|
||||
GoodImportFileResult,
|
||||
OcrResult,
|
||||
AppRuntimeInfo,
|
||||
SaveResultWithPath,
|
||||
ScannerCommand,
|
||||
ScannerLearningRulePayload,
|
||||
ScannerStatusPayload,
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
profileDetailRect,
|
||||
} from "../src/lib/layoutProfile.js";
|
||||
import { binarizeForOcr } from "../src/lib/ocrPreprocess.js";
|
||||
import { detectLockState, lockIconCropRect } from "../src/lib/lockDetection.js";
|
||||
import { DEFAULT_LOCK_THRESHOLD, isLocked, lockIconCropRect, lockSignalRatio } from "../src/lib/lockDetection.js";
|
||||
|
||||
// Chromium's renderer sandbox can refuse to fully initialize (or silently
|
||||
// crash the GPU/renderer process) when the hosting process runs with a full
|
||||
@@ -54,12 +54,11 @@ app.commandLine.appendSwitch("disable-gpu-sandbox");
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const isDev = Boolean(process.env.VITE_DEV_SERVER_URL);
|
||||
const APP_RUNTIME_STARTED_AT = new Date().toISOString();
|
||||
const APP_RUNTIME_SIGNATURE = "2026-07-07-ik32-fastsubstats-active-timing";
|
||||
const APP_RUNTIME_SIGNATURE = "2026-07-08-direct-gdi-reviewfix";
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let overlayWindow: BrowserWindow | null = null;
|
||||
let registeredHotkeys: Record<string, boolean> = {};
|
||||
let devControlServer: Server | null = null;
|
||||
const captureSourceNameCache = new Map<string, string>();
|
||||
let scannerDevStatus: ScannerStatusPayload = {
|
||||
running: false,
|
||||
reviewStatus: "",
|
||||
@@ -83,6 +82,8 @@ let artifactStoreRepository: ArtifactStoreRepositoryPort | null = null;
|
||||
let reviewSamplesRepository: ReviewSamplesRepositoryPort | null = null;
|
||||
let scannerLearningRepository: ScannerLearningRepositoryPort | null = null;
|
||||
let inputHelperService: InputHelperService | null = null;
|
||||
let appWindowManager: AppWindowManager | null = null;
|
||||
let goodFileService: GoodFileService | null = null;
|
||||
|
||||
function getInputHelperService() {
|
||||
if (!inputHelperService) {
|
||||
@@ -97,6 +98,7 @@ function resolveInputHelperExePath(): string | null {
|
||||
const candidates = [
|
||||
process.env.INPUT_HELPER_EXE,
|
||||
path.join(process.resourcesPath, "input-helper", "InputHelper.exe"),
|
||||
path.join(process.cwd(), "native", "input-helper", "bin", "publish", "InputHelper.exe"),
|
||||
path.join(app.getAppPath(), "native", "input-helper", "bin", "publish", "InputHelper.exe"),
|
||||
].filter((candidate): candidate is string => Boolean(candidate));
|
||||
|
||||
@@ -163,7 +165,7 @@ function getScannerLearningRepository() {
|
||||
async function writeScannerLearningRules(rules: ScannerLearningRulePayload) {
|
||||
const safeRules = rules && typeof rules === "object" ? rules : {};
|
||||
try {
|
||||
return await getScannerLearningRepository().save(safeRules as { textReplacements?: Record<string, string> });
|
||||
return await getScannerLearningRepository().save(safeRules);
|
||||
} catch {
|
||||
return { ok: true, path: scannerLearningPath(), rules: { textReplacements: {} }, total: 0 };
|
||||
}
|
||||
@@ -184,8 +186,11 @@ function getSnapshotRepository() {
|
||||
return context.snapshotRepository;
|
||||
}
|
||||
|
||||
function exportPath(fileName: string) {
|
||||
return path.join(app.getPath("userData"), "exports", fileName);
|
||||
function getGoodFileService() {
|
||||
if (!goodFileService) {
|
||||
throw new Error("GOOD file service is not initialized.");
|
||||
}
|
||||
return goodFileService;
|
||||
}
|
||||
|
||||
async function loadSnapshotFromDisk() {
|
||||
@@ -264,6 +269,7 @@ async function getGenshinWindowBounds() {
|
||||
// second display exists and isn't the one Genshin occupies, move the
|
||||
// dashboard there so it can never cover the grid we're about to click.
|
||||
async function moveMainWindowOffGenshin() {
|
||||
const mainWindow = getAppWindowManager().getMainWindow();
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||
const genshinBounds = await getGenshinWindowBounds();
|
||||
const displays = screen.getAllDisplays();
|
||||
@@ -304,8 +310,7 @@ async function showOverlayWindow() {
|
||||
}
|
||||
|
||||
async function hideOverlayWindow() {
|
||||
overlayWindow?.close();
|
||||
return { ok: true };
|
||||
return getAppWindowManager().hideOverlayWindow();
|
||||
}
|
||||
|
||||
async function listCaptureSources() {
|
||||
@@ -324,12 +329,15 @@ async function listCaptureSources() {
|
||||
fetchWindowIcons: true,
|
||||
});
|
||||
|
||||
return sources.map((source) => ({
|
||||
id: source.id,
|
||||
name: source.name,
|
||||
isGenshinCandidate: isLikelyGenshinSourceName(source.name),
|
||||
thumbnailDataUrl: source.thumbnail.resize({ width: 420 }).toDataURL(),
|
||||
}));
|
||||
return sources.map((source) => {
|
||||
captureSourceNameCache.set(source.id, source.name);
|
||||
return {
|
||||
id: source.id,
|
||||
name: source.name,
|
||||
isGenshinCandidate: isLikelyGenshinSourceName(source.name),
|
||||
thumbnailDataUrl: source.thumbnail.resize({ width: 420 }).toDataURL(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function toScreenPoint(x: number, y: number) {
|
||||
@@ -394,69 +402,45 @@ async function capturePrimaryScreenViaGdi() {
|
||||
|
||||
async function captureSourceFromGdi(sourceId: string, sourceName: string, options: CaptureOptions = {}) {
|
||||
const gdi = await capturePrimaryScreenViaGdi();
|
||||
const sourceImage = nativeImage.createFromDataURL(gdi.dataUrl);
|
||||
const sourceImage = nativeImageFromGdiCapture(gdi);
|
||||
return await buildCaptureResult(sourceImage, sourceId, sourceName, gdi.captureTarget, options);
|
||||
}
|
||||
|
||||
function createMainWindow() {
|
||||
Menu.setApplicationMenu(null);
|
||||
function nativeImageFromGdiCapture(gdi: Awaited<ReturnType<InputHelperService["capturePrimaryScreenViaGdi"]>>) {
|
||||
return nativeImage.createFromDataURL(gdi.dataUrl);
|
||||
}
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1320,
|
||||
height: 860,
|
||||
minWidth: 1120,
|
||||
minHeight: 720,
|
||||
backgroundColor: "#090711",
|
||||
title: "Genshin Artifact Assistant",
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "preload.cjs"),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
function shouldUseDirectGdiHotPath(options: CaptureOptions = {}) {
|
||||
return Boolean(
|
||||
options.ocrMode === "artifact" ||
|
||||
options.skipOcrUnlessArtifactDetail ||
|
||||
options.skipOcr ||
|
||||
options.omitCrops,
|
||||
);
|
||||
}
|
||||
|
||||
mainWindow.setMenuBarVisibility(false);
|
||||
mainWindow.on("closed", () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
mainWindow.once("ready-to-show", () => {
|
||||
void moveMainWindowOffGenshin();
|
||||
focusMainWindow();
|
||||
});
|
||||
mainWindow.webContents.once("did-finish-load", () => {
|
||||
setTimeout(() => focusMainWindow(), 350);
|
||||
});
|
||||
|
||||
if (isDev) {
|
||||
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL!);
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname, "../../dist/index.html"));
|
||||
function getAppWindowManager() {
|
||||
if (!appWindowManager) {
|
||||
appWindowManager = createAppWindowManager({
|
||||
preloadPath: path.join(__dirname, "preload.cjs"),
|
||||
rendererUrl: process.env.VITE_DEV_SERVER_URL,
|
||||
rendererFilePath: path.join(__dirname, "../../dist/index.html"),
|
||||
onMainReadyToShow: moveMainWindowOffGenshin,
|
||||
});
|
||||
}
|
||||
return appWindowManager;
|
||||
}
|
||||
|
||||
function createMainWindow() {
|
||||
getAppWindowManager().createMainWindow();
|
||||
}
|
||||
|
||||
function focusMainWindow() {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return { ok: false };
|
||||
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.show();
|
||||
// Genshin often keeps foreground focus after a scan click. Toggling
|
||||
// always-on-top for one tick nudges Windows to surface the dashboard again
|
||||
// without leaving it pinned above other apps.
|
||||
mainWindow.setAlwaysOnTop(true, "screen-saver");
|
||||
mainWindow.focus();
|
||||
setTimeout(() => {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||
mainWindow.setAlwaysOnTop(false);
|
||||
mainWindow.focus();
|
||||
}, 250);
|
||||
return { ok: true };
|
||||
return getAppWindowManager().focusMainWindow();
|
||||
}
|
||||
|
||||
function sendScannerCommand(command: ScannerCommand | "probe-click") {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return;
|
||||
mainWindow.webContents.send("scanner:command", command);
|
||||
getAppWindowManager().sendScannerCommand(command);
|
||||
}
|
||||
|
||||
function registerScannerHotkeys() {
|
||||
@@ -474,7 +458,7 @@ function startDevControlServer() {
|
||||
devControlServer = createDevControlServer({
|
||||
registeredHotkeys,
|
||||
appBuild: appRuntimeInfo(),
|
||||
hasMainWindow: () => Boolean(mainWindow && !mainWindow.isDestroyed()),
|
||||
hasMainWindow: () => getAppWindowManager().hasMainWindow(),
|
||||
sendScannerCommand,
|
||||
clickScreen: clickScreenCommand,
|
||||
scannerStatus: () => ({ ...scannerDevStatus, appBuild: appRuntimeInfo(), ocrWarmup: getOcrWarmupStatus() }),
|
||||
@@ -490,43 +474,7 @@ function startDevControlServer() {
|
||||
}
|
||||
|
||||
function createOverlayWindow() {
|
||||
if (overlayWindow) {
|
||||
overlayWindow.show();
|
||||
return;
|
||||
}
|
||||
|
||||
const display = screen.getPrimaryDisplay();
|
||||
overlayWindow = new BrowserWindow({
|
||||
x: display.workArea.x,
|
||||
y: display.workArea.y,
|
||||
width: display.workArea.width,
|
||||
height: display.workArea.height,
|
||||
transparent: true,
|
||||
frame: false,
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true,
|
||||
resizable: false,
|
||||
focusable: false,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "preload.cjs"),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
|
||||
overlayWindow.setIgnoreMouseEvents(true, { forward: true });
|
||||
|
||||
if (isDev) {
|
||||
overlayWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL!}?overlay=1`);
|
||||
} else {
|
||||
overlayWindow.loadFile(path.join(__dirname, "../../dist/index.html"), {
|
||||
query: { overlay: "1" },
|
||||
});
|
||||
}
|
||||
|
||||
overlayWindow.on("closed", () => {
|
||||
overlayWindow = null;
|
||||
});
|
||||
getAppWindowManager().createOverlayWindow();
|
||||
}
|
||||
|
||||
// Inventory Kamera keeps a pool of native Tesseract engines and scans artifact
|
||||
@@ -818,8 +766,8 @@ async function runOcrOnCropsWithTimeout(crops: OcrCropPayload[], engine: OcrWork
|
||||
|
||||
function cleanOcrText(cropId: string, text: string) {
|
||||
const normalized = text
|
||||
.replace(/[“â€]/g, '"')
|
||||
.replace(/[’]/g, "'")
|
||||
.replace(/[\u201c\u201d]/g, '"')
|
||||
.replace(/[\u2019]/g, "'")
|
||||
.replace(/\r/g, "")
|
||||
.split("\n")
|
||||
.map((line) => line.replace(/\s+/g, " ").trim())
|
||||
@@ -1126,6 +1074,7 @@ async function getAllSources() {
|
||||
|
||||
async function findCaptureSourceById(sourceId: string) {
|
||||
const sources = await getAllSources();
|
||||
for (const source of sources) captureSourceNameCache.set(source.id, source.name);
|
||||
return sources.find((source) => source.id === sourceId) ?? null;
|
||||
}
|
||||
|
||||
@@ -1172,7 +1121,7 @@ function imageCropFingerprint(sourceImage: NativeImage, rect: Electron.Rectangle
|
||||
function preprocessedCropPngBuffer(sourceImage: NativeImage, rect: Electron.Rectangle, imageSize: { width: number; height: number }, cropId = "") {
|
||||
const safeRect = clampCaptureRect(rect, imageSize);
|
||||
const scale = cropId === "artifact-level" ? 3 : 2;
|
||||
const upscaled = sourceImage.crop(safeRect).resize({ width: Math.max(1, safeRect.width * scale), quality: "best" });
|
||||
const upscaled = sourceImage.crop(safeRect).resize({ width: Math.max(1, Math.round(safeRect.width * scale)), quality: "best" });
|
||||
const size = upscaled.getSize();
|
||||
if (!size.width || !size.height) return upscaled.toPNG();
|
||||
const binarized = binarizeForOcr(
|
||||
@@ -1201,10 +1150,9 @@ function createCrops(
|
||||
.filter((template) => {
|
||||
if (fastArtifactProfile && (
|
||||
template.id === "artifact-set-effects" ||
|
||||
template.id === "artifact-slot" ||
|
||||
template.id === "artifact-main-stat-value"
|
||||
)) return false;
|
||||
if (template.id === "artifact-footer" && (options.omitEquippedOcr || fastArtifactProfile)) return false;
|
||||
if (template.id === "artifact-footer" && options.omitEquippedOcr) return false;
|
||||
if (!isArtifactScanMode || template.id !== "artifact-footer" || !bitmap) return true;
|
||||
return hasEquippedFooterMarker(bitmap, imageSize, template.rect);
|
||||
});
|
||||
@@ -1314,16 +1262,35 @@ async function buildCaptureResult(
|
||||
const crops = omitCrops
|
||||
? []
|
||||
: createCrops(sourceImage, size, detailRect, inventoryRect, options, bitmap, { sanctified, skipOcr: shouldSkipOcr });
|
||||
const locked = options.omitLockState
|
||||
const lockSignal = options.omitLockState
|
||||
? undefined
|
||||
: (() => {
|
||||
const lockRect = clampCaptureRect(lockIconCropRect(detailRect, size), size);
|
||||
const lockImage = sourceImage.crop(lockRect);
|
||||
const lockSize = lockImage.getSize();
|
||||
return lockSize.width > 0 && lockSize.height > 0
|
||||
? detectLockState({ data: lockImage.getBitmap(), width: lockSize.width, height: lockSize.height })
|
||||
const ratio = lockSize.width > 0 && lockSize.height > 0
|
||||
? (() => {
|
||||
try {
|
||||
return lockSignalRatio(pngBufferToBitmap(lockImage.toPNG()));
|
||||
} catch {
|
||||
return lockSignalRatio({ data: lockImage.getBitmap(), width: lockSize.width, height: lockSize.height });
|
||||
}
|
||||
})()
|
||||
: undefined;
|
||||
return typeof ratio === "number"
|
||||
? {
|
||||
ratio,
|
||||
threshold: DEFAULT_LOCK_THRESHOLD,
|
||||
rect: {
|
||||
x: lockRect.x,
|
||||
y: lockRect.y,
|
||||
width: lockRect.width,
|
||||
height: lockRect.height,
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
})();
|
||||
const locked = lockSignal ? isLocked(lockSignal.ratio, lockSignal.threshold) : undefined;
|
||||
const omitFullFrame = Boolean(options.omitFullFrame || options.ocrMode === "artifact");
|
||||
const omitDetailPreview = Boolean(options.omitDetailPreview);
|
||||
const omitInventoryPreview = Boolean(options.omitInventoryPreview || options.ocrMode === "artifact");
|
||||
@@ -1383,6 +1350,7 @@ async function buildCaptureResult(
|
||||
paimonMenu,
|
||||
inventoryCount: count,
|
||||
locked,
|
||||
lockSignal,
|
||||
sanctified,
|
||||
layout: {
|
||||
aspect: aspectRatioLabel(size),
|
||||
@@ -1407,16 +1375,27 @@ async function buildCaptureResult(
|
||||
}
|
||||
|
||||
async function captureSource(sourceId: string, delayMs = 0, focusGenshin = false, options?: CaptureOptions): Promise<CaptureResult> {
|
||||
const captureStartedAt = Date.now();
|
||||
if (!Number.isFinite(delayMs) || delayMs < 0) {
|
||||
delayMs = 0;
|
||||
}
|
||||
|
||||
const withElapsed = (capture: CaptureResult): CaptureResult => ({
|
||||
...capture,
|
||||
elapsedMs: Math.max(0, Date.now() - captureStartedAt),
|
||||
});
|
||||
|
||||
await waitDelay(Math.floor(delayMs));
|
||||
|
||||
if (focusGenshin) {
|
||||
await focusGenshinForScanStart();
|
||||
}
|
||||
|
||||
if (shouldUseDirectGdiHotPath(options ?? {})) {
|
||||
const cachedName = captureSourceNameCache.get(sourceId) ?? "Genshin GDI Capture";
|
||||
return withElapsed(await captureSourceFromGdi(sourceId, cachedName, options ?? {}));
|
||||
}
|
||||
|
||||
const source = await findCaptureSourceById(sourceId);
|
||||
if (!source) {
|
||||
throw new Error("Capture source not found.");
|
||||
@@ -1425,7 +1404,7 @@ async function captureSource(sourceId: string, delayMs = 0, focusGenshin = false
|
||||
const isGenshinCandidate = isLikelyGenshinSourceName(source.name);
|
||||
if (isGenshinCandidate) {
|
||||
try {
|
||||
return await captureSourceFromGdi(sourceId, source.name, options ?? {});
|
||||
return withElapsed(await captureSourceFromGdi(sourceId, source.name, options ?? {}));
|
||||
} catch {
|
||||
// Fall back to desktop thumbnail capture for robustness in low-permission
|
||||
// or transient capture failures. OCR will still produce a best-effort result.
|
||||
@@ -1434,56 +1413,16 @@ async function captureSource(sourceId: string, delayMs = 0, focusGenshin = false
|
||||
|
||||
const sourceImage = source.thumbnail;
|
||||
if (sourceImage.isEmpty()) {
|
||||
return await captureSourceFromGdi(sourceId, source.name, options ?? {});
|
||||
return withElapsed(await captureSourceFromGdi(sourceId, source.name, options ?? {}));
|
||||
}
|
||||
|
||||
return await buildCaptureResult(
|
||||
return withElapsed(await buildCaptureResult(
|
||||
sourceImage,
|
||||
sourceId,
|
||||
source.name,
|
||||
sourceId.startsWith("screen:") ? "desktop-source" : "genshin-client",
|
||||
options ?? {},
|
||||
);
|
||||
}
|
||||
|
||||
async function exportGood(payload: GoodDatabase): Promise<SaveResultWithPath> {
|
||||
const fileNameSafe = `good-export-${new Date().toISOString().replace(/[\\/:]/g, "-").replace(/\..+?$/, "").replace(/\s+/g, "-")}.json`;
|
||||
const filePath = exportPath(fileNameSafe);
|
||||
try {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fs.writeFile(filePath, JSON.stringify(payload, null, 2), "utf8");
|
||||
return { ok: true, path: filePath };
|
||||
} catch {
|
||||
return { ok: false, path: filePath };
|
||||
}
|
||||
}
|
||||
|
||||
async function importGoodFile(): Promise<GoodImportFileResult> {
|
||||
const dialogOptions = {
|
||||
title: "GOOD-Datei importieren",
|
||||
properties: ["openFile"],
|
||||
filters: [{ name: "GOOD JSON", extensions: ["json"] }],
|
||||
} satisfies Electron.OpenDialogOptions;
|
||||
const dialogResult = mainWindow && !mainWindow.isDestroyed()
|
||||
? await dialog.showOpenDialog(mainWindow, dialogOptions)
|
||||
: await dialog.showOpenDialog(dialogOptions);
|
||||
|
||||
if (dialogResult.canceled || dialogResult.filePaths.length === 0) {
|
||||
return { ok: false, canceled: true, path: "" };
|
||||
}
|
||||
|
||||
const filePath = dialogResult.filePaths[0];
|
||||
try {
|
||||
const text = await fs.readFile(filePath, "utf8");
|
||||
return { ok: true, canceled: false, path: filePath, database: JSON.parse(text) };
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
canceled: false,
|
||||
path: filePath,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
function initializeAppLifecycle() {
|
||||
@@ -1494,6 +1433,7 @@ function initializeAppLifecycle() {
|
||||
reviewSamplesRepository = repositoryContext.reviewSamplesRepository;
|
||||
scannerLearningRepository = repositoryContext.scannerLearningRepository;
|
||||
inputHelperService = createInputHelperService({ userDataPath, exePath: resolveInputHelperExePath() });
|
||||
goodFileService = createGoodFileService(path.join(userDataPath, "exports"));
|
||||
|
||||
registerIpcHandlers({
|
||||
focusMainWindow: () => focusMainWindow(),
|
||||
@@ -1513,8 +1453,8 @@ function initializeAppLifecycle() {
|
||||
loadReviewSamples: (limit?: number) => loadReviewSamples(limit),
|
||||
loadScannerLearningRules: () => loadScannerLearningRules(),
|
||||
writeScannerLearningRules: (rules: ScannerLearningRulePayload) => writeScannerLearningRules(rules),
|
||||
exportGood: (exportPayload: GoodDatabase) => exportGood(exportPayload),
|
||||
importGoodFile: () => importGoodFile(),
|
||||
exportGood: (exportPayload: GoodDatabase) => getGoodFileService().exportGood(exportPayload),
|
||||
importGoodFile: () => getGoodFileService().importGoodFile(getAppWindowManager().getMainWindow()),
|
||||
listSources: () => listCaptureSources(),
|
||||
captureSource: (
|
||||
id: string,
|
||||
@@ -1535,7 +1475,7 @@ function initializeAppLifecycle() {
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
if (!getAppWindowManager().hasMainWindow()) {
|
||||
createMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user