Release OpenScreen 1.4.1
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
# Copy to .env.signing.local for a local signing machine. Do not commit real values.
|
||||||
|
AZURE_TENANT_ID=
|
||||||
|
AZURE_CLIENT_ID=
|
||||||
|
AZURE_CLIENT_SECRET=
|
||||||
|
AZURE_TRUSTED_SIGNING_ENDPOINT=https://<region>.codesigning.azure.net/
|
||||||
|
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME=
|
||||||
|
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME=
|
||||||
|
AZURE_TRUSTED_SIGNING_PUBLISHER_NAME=
|
||||||
@@ -13,6 +13,7 @@ dist-electron
|
|||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
.env
|
.env
|
||||||
|
.env.signing.local
|
||||||
|
|
||||||
# Native helper build outputs
|
# Native helper build outputs
|
||||||
/electron/native/wgc-capture/build/
|
/electron/native/wgc-capture/build/
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Windows Private Trust Signing
|
||||||
|
|
||||||
|
OpenScreen supports Microsoft Trusted Signing private trust profiles for Windows
|
||||||
|
builds. Secrets and signing resource names are read from environment variables;
|
||||||
|
no certificate, client secret, or API key should be committed.
|
||||||
|
|
||||||
|
For a local signing machine, copy `.env.signing.example` to
|
||||||
|
`.env.signing.local` and fill in values there. `.env.signing.local` is ignored
|
||||||
|
by Git. Explicit shell environment variables override values in that local file.
|
||||||
|
|
||||||
|
## Required Azure Resource Variables
|
||||||
|
|
||||||
|
Set these values for the Trusted Signing account and certificate profile:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:AZURE_TRUSTED_SIGNING_ENDPOINT = "https://<region>.codesigning.azure.net/"
|
||||||
|
$env:AZURE_TRUSTED_SIGNING_ACCOUNT_NAME = "<trusted-signing-account-name>"
|
||||||
|
$env:AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME = "<private-trust-profile-name>"
|
||||||
|
$env:AZURE_TRUSTED_SIGNING_PUBLISHER_NAME = "<certificate-common-name>"
|
||||||
|
```
|
||||||
|
|
||||||
|
`AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME` must point to a certificate
|
||||||
|
profile created with the `PrivateTrust` profile type.
|
||||||
|
|
||||||
|
## Required Azure Auth Variables
|
||||||
|
|
||||||
|
Electron Builder uses Azure environment credentials. Set the tenant and client:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:AZURE_TENANT_ID = "<tenant-id>"
|
||||||
|
$env:AZURE_CLIENT_ID = "<app-registration-client-id>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then set one authentication mode. Service principal secret is the simplest for
|
||||||
|
local signing:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:AZURE_CLIENT_SECRET = "<client-secret>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Certificate auth is also supported:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:AZURE_CLIENT_CERTIFICATE_PATH = "C:\secure\signing-auth.pfx"
|
||||||
|
$env:AZURE_CLIENT_CERTIFICATE_PASSWORD = "<pfx-password>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sign Existing Installer
|
||||||
|
|
||||||
|
This signs the installer already built at
|
||||||
|
`release/<version>/Openscreen Setup <version>.exe`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run sign:win:private-trust
|
||||||
|
```
|
||||||
|
|
||||||
|
To sign a specific file:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run sign:win:private-trust -- --file "D:\Code\OpenScreen\release\1.4.0\Openscreen Setup 1.4.0.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build And Sign
|
||||||
|
|
||||||
|
This signs the packaged app executable, bundled OCR service executable, and NSIS
|
||||||
|
installer during the Windows build:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run build:win:private-trust
|
||||||
|
```
|
||||||
|
|
||||||
|
The regular `npm run build:win` remains unsigned for local development builds.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
After signing:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-AuthenticodeSignature "release\1.4.0\Openscreen Setup 1.4.0.exe" | Format-List
|
||||||
|
```
|
||||||
|
|
||||||
|
Private trust signatures are valid only on machines that trust the private trust
|
||||||
|
certificate chain/publisher. For public downloads that must be trusted on any
|
||||||
|
Windows machine, use a public trust certificate profile instead.
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
const JSON5 = require("json5");
|
||||||
|
|
||||||
|
function loadLocalSigningEnv() {
|
||||||
|
const envPath = path.join(__dirname, ".env.signing.local");
|
||||||
|
if (!fs.existsSync(envPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = fs.readFileSync(envPath, "utf8").split(/\r?\n/);
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith("#")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
||||||
|
if (!match || process.env[match[1]]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
process.env[match[1]] = match[2].replace(/^['"]|['"]$/g, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readBaseConfig() {
|
||||||
|
const configPath = path.join(__dirname, "electron-builder.json5");
|
||||||
|
return JSON5.parse(fs.readFileSync(configPath, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireEnv(name) {
|
||||||
|
const value = process.env[name]?.trim();
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`Missing required environment variable: ${name}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireAnyEnv(names) {
|
||||||
|
for (const name of names) {
|
||||||
|
const value = process.env[name]?.trim();
|
||||||
|
if (value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`Missing required environment variable: ${names.join(" or ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadLocalSigningEnv();
|
||||||
|
|
||||||
|
const config = readBaseConfig();
|
||||||
|
|
||||||
|
config.win = {
|
||||||
|
...config.win,
|
||||||
|
signAndEditExecutable: true,
|
||||||
|
azureSignOptions: {
|
||||||
|
publisherName: requireAnyEnv([
|
||||||
|
"AZURE_TRUSTED_SIGNING_PUBLISHER_NAME",
|
||||||
|
"OPENSCREEN_SIGNING_PUBLISHER_NAME",
|
||||||
|
]),
|
||||||
|
endpoint: requireEnv("AZURE_TRUSTED_SIGNING_ENDPOINT"),
|
||||||
|
certificateProfileName: requireEnv("AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME"),
|
||||||
|
codeSigningAccountName: requireEnv("AZURE_TRUSTED_SIGNING_ACCOUNT_NAME"),
|
||||||
|
fileDigest: process.env.AZURE_TRUSTED_SIGNING_FILE_DIGEST?.trim() || "SHA256",
|
||||||
|
timestampRfc3161:
|
||||||
|
process.env.AZURE_TRUSTED_SIGNING_TIMESTAMP_RFC3161?.trim() ||
|
||||||
|
"http://timestamp.acs.microsoft.com",
|
||||||
|
timestampDigest: process.env.AZURE_TRUSTED_SIGNING_TIMESTAMP_DIGEST?.trim() || "SHA256",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
delete config.win.signExts;
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
Vendored
+5
@@ -48,6 +48,11 @@ interface Window {
|
|||||||
event: import("../src/guide/contracts").GuideEvent;
|
event: import("../src/guide/contracts").GuideEvent;
|
||||||
}>
|
}>
|
||||||
>;
|
>;
|
||||||
|
capturePointerMarker: () => Promise<
|
||||||
|
import("../src/guide/contracts").GuideIpcResult<
|
||||||
|
import("../src/guide/contracts").CaptureGuidePointerMarkerResult
|
||||||
|
>
|
||||||
|
>;
|
||||||
finalizeEvents: (
|
finalizeEvents: (
|
||||||
input: import("../src/guide/contracts").FinalizeGuideEventsInput,
|
input: import("../src/guide/contracts").FinalizeGuideEventsInput,
|
||||||
) => Promise<
|
) => Promise<
|
||||||
|
|||||||
@@ -18,15 +18,25 @@ import type {
|
|||||||
import type { DeepSeekSettingsStore } from "./ai/deepseekSettingsStore";
|
import type { DeepSeekSettingsStore } from "./ai/deepseekSettingsStore";
|
||||||
import { GuideStore, GuideStoreError } from "./guideStore";
|
import { GuideStore, GuideStoreError } from "./guideStore";
|
||||||
|
|
||||||
|
export interface GuideIpcLifecycle {
|
||||||
|
onSessionStarted?: (session: GuideSession) => void;
|
||||||
|
onSessionEnded?: (recordingId: unknown) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export function registerGuideIpcHandlers(
|
export function registerGuideIpcHandlers(
|
||||||
ipcMain: IpcMain,
|
ipcMain: IpcMain,
|
||||||
store: GuideStore,
|
store: GuideStore,
|
||||||
aiSettingsStore?: DeepSeekSettingsStore,
|
aiSettingsStore?: DeepSeekSettingsStore,
|
||||||
|
lifecycle: GuideIpcLifecycle = {},
|
||||||
): void {
|
): void {
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"guide:start-session",
|
"guide:start-session",
|
||||||
async (_, recordingId): Promise<GuideIpcResult<GuideSession>> => {
|
async (_, recordingId): Promise<GuideIpcResult<GuideSession>> => {
|
||||||
return await toGuideResult(() => store.startSession(recordingId));
|
const result = await toGuideResult(() => store.startSession(recordingId));
|
||||||
|
if (result.success) {
|
||||||
|
lifecycle.onSessionStarted?.(result.data);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -50,7 +60,11 @@ export function registerGuideIpcHandlers(
|
|||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"guide:finalize-events",
|
"guide:finalize-events",
|
||||||
async (_, input: FinalizeGuideEventsInput): Promise<GuideIpcResult<GuideSession>> => {
|
async (_, input: FinalizeGuideEventsInput): Promise<GuideIpcResult<GuideSession>> => {
|
||||||
return await toGuideResult(() => store.finalizeEvents(input));
|
const result = await toGuideResult(() => store.finalizeEvents(input));
|
||||||
|
if (result.success) {
|
||||||
|
lifecycle.onSessionEnded?.(input.recordingId);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -110,10 +124,14 @@ export function registerGuideIpcHandlers(
|
|||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"guide:discard-session",
|
"guide:discard-session",
|
||||||
async (_, input: DiscardGuideSessionInput): Promise<GuideIpcResult<{ discarded: true }>> => {
|
async (_, input: DiscardGuideSessionInput): Promise<GuideIpcResult<{ discarded: true }>> => {
|
||||||
return await toGuideResult(async () => {
|
const result = await toGuideResult(async () => {
|
||||||
await store.discardSession(input);
|
await store.discardSession(input);
|
||||||
return { discarded: true };
|
return { discarded: true as const };
|
||||||
});
|
});
|
||||||
|
if (result.success) {
|
||||||
|
lifecycle.onSessionEnded?.(input.recordingId);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,9 +42,17 @@ describe("GuideStore", () => {
|
|||||||
kind: "hotkey",
|
kind: "hotkey",
|
||||||
timeMs: 500,
|
timeMs: 500,
|
||||||
label: "First",
|
label: "First",
|
||||||
|
normalizedX: 0.25,
|
||||||
|
normalizedY: 0.75,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.event.kind).toBe("hotkey");
|
expect(result.event.kind).toBe("hotkey");
|
||||||
|
expect(result.event).toMatchObject({
|
||||||
|
x: 0.25,
|
||||||
|
y: 0.75,
|
||||||
|
normalizedX: 0.25,
|
||||||
|
normalizedY: 0.75,
|
||||||
|
});
|
||||||
expect(result.session.events.map((event) => event.timeMs)).toEqual([500, 2000]);
|
expect(result.session.events.map((event) => event.timeMs)).toEqual([500, 2000]);
|
||||||
expect(result.session.events[0]?.source).toBe("guide-hotkey");
|
expect(result.session.events[0]?.source).toBe("guide-hotkey");
|
||||||
expect(result.session.events[1]?.source).toBe("review-ui");
|
expect(result.session.events[1]?.source).toBe("review-ui");
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ export class GuideStore {
|
|||||||
kind: input.kind,
|
kind: input.kind,
|
||||||
source: input.kind === "hotkey" ? "guide-hotkey" : "review-ui",
|
source: input.kind === "hotkey" ? "guide-hotkey" : "review-ui",
|
||||||
timeMs: Math.max(0, input.timeMs),
|
timeMs: Math.max(0, input.timeMs),
|
||||||
|
...normalizeMarkerPoint(input),
|
||||||
label: normalizeOptionalString(input.label),
|
label: normalizeOptionalString(input.label),
|
||||||
screenshotOffsetMs: 500,
|
screenshotOffsetMs: 500,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
@@ -813,6 +814,31 @@ function normalizeOptionalNumber(value: unknown): number | undefined {
|
|||||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeMarkerPoint(
|
||||||
|
input: AddGuideMarkerInput,
|
||||||
|
): Pick<GuideEvent, "x" | "y" | "normalizedX" | "normalizedY"> {
|
||||||
|
const normalizedX = normalizeOptionalNormalizedNumber(input.normalizedX ?? input.x);
|
||||||
|
const normalizedY = normalizeOptionalNormalizedNumber(input.normalizedY ?? input.y);
|
||||||
|
if (normalizedX === undefined || normalizedY === undefined) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: normalizedX,
|
||||||
|
y: normalizedY,
|
||||||
|
normalizedX,
|
||||||
|
normalizedY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOptionalNormalizedNumber(value: unknown): number | undefined {
|
||||||
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(1, Math.max(0, value));
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePositiveInteger(value: unknown): number | null {
|
function normalizePositiveInteger(value: unknown): number | null {
|
||||||
return typeof value === "number" && Number.isFinite(value) && value > 0
|
return typeof value === "number" && Number.isFinite(value) && value > 0
|
||||||
? Math.round(value)
|
? Math.round(value)
|
||||||
|
|||||||
+204
-5
@@ -11,6 +11,7 @@ import {
|
|||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
desktopCapturer,
|
desktopCapturer,
|
||||||
dialog,
|
dialog,
|
||||||
|
globalShortcut,
|
||||||
ipcMain,
|
ipcMain,
|
||||||
screen,
|
screen,
|
||||||
shell,
|
shell,
|
||||||
@@ -428,6 +429,19 @@ let nativeMacCursorRecordingStartMs = 0;
|
|||||||
let nativeMacPauseStartedAtMs: number | null = null;
|
let nativeMacPauseStartedAtMs: number | null = null;
|
||||||
let nativeMacPauseRanges: Array<{ startMs: number; endMs: number }> = [];
|
let nativeMacPauseRanges: Array<{ startMs: number; endMs: number }> = [];
|
||||||
let nativeMacIsPaused = false;
|
let nativeMacIsPaused = false;
|
||||||
|
const GUIDE_MARKER_HOTKEY = "Control+F12";
|
||||||
|
const GUIDE_MARKER_HOTKEY_LABEL = "Ctrl+F12";
|
||||||
|
type GuideHotkeyBounds = { x: number; y: number; width: number; height: number };
|
||||||
|
type GuideHotkeyRecordingState = {
|
||||||
|
recordingId: number;
|
||||||
|
startedAtMs: number;
|
||||||
|
accumulatedPausedMs: number;
|
||||||
|
pausedAtMs: number | null;
|
||||||
|
bounds: GuideHotkeyBounds;
|
||||||
|
};
|
||||||
|
let activeGuideHotkeyRecording: GuideHotkeyRecordingState | null = null;
|
||||||
|
let activeGuideHotkeySessionId: number | null = null;
|
||||||
|
let guideMarkerHotkeyRegistered = false;
|
||||||
|
|
||||||
function normalizeCursorSample(sample: unknown): CursorRecordingSample | null {
|
function normalizeCursorSample(sample: unknown): CursorRecordingSample | null {
|
||||||
if (!sample || typeof sample !== "object") {
|
if (!sample || typeof sample !== "object") {
|
||||||
@@ -585,6 +599,160 @@ function getSelectedSourceBounds() {
|
|||||||
return (sourceDisplay ?? screen.getDisplayNearestPoint(cursor)).bounds;
|
return (sourceDisplay ?? screen.getDisplayNearestPoint(cursor)).bounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeGuideHotkeyRecordingId(recordingId: unknown): number | null {
|
||||||
|
if (typeof recordingId === "number" && Number.isFinite(recordingId)) {
|
||||||
|
return Math.trunc(recordingId);
|
||||||
|
}
|
||||||
|
if (typeof recordingId === "string" && recordingId.trim()) {
|
||||||
|
const numeric = Number(recordingId);
|
||||||
|
return Number.isFinite(numeric) ? Math.trunc(numeric) : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeGuideHotkeyBounds(bounds: GuideHotkeyBounds): GuideHotkeyBounds {
|
||||||
|
return {
|
||||||
|
x: Number.isFinite(bounds.x) ? bounds.x : 0,
|
||||||
|
y: Number.isFinite(bounds.y) ? bounds.y : 0,
|
||||||
|
width: Number.isFinite(bounds.width) && bounds.width > 0 ? bounds.width : 1,
|
||||||
|
height: Number.isFinite(bounds.height) && bounds.height > 0 ? bounds.height : 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGuideHotkeyRecording(
|
||||||
|
recordingIdInput: unknown,
|
||||||
|
bounds: GuideHotkeyBounds = getSelectedSourceBounds(),
|
||||||
|
) {
|
||||||
|
const recordingId = normalizeGuideHotkeyRecordingId(recordingIdInput);
|
||||||
|
if (recordingId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeGuideHotkeyRecording = {
|
||||||
|
recordingId,
|
||||||
|
startedAtMs: Date.now(),
|
||||||
|
accumulatedPausedMs: 0,
|
||||||
|
pausedAtMs: null,
|
||||||
|
bounds: sanitizeGuideHotkeyBounds(bounds),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearGuideHotkeyRecording() {
|
||||||
|
activeGuideHotkeyRecording = null;
|
||||||
|
activeGuideHotkeySessionId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateGuideHotkeySession(recordingIdInput: unknown) {
|
||||||
|
const recordingId = normalizeGuideHotkeyRecordingId(recordingIdInput);
|
||||||
|
if (recordingId !== null) {
|
||||||
|
activeGuideHotkeySessionId = recordingId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivateGuideHotkeySession(recordingIdInput: unknown) {
|
||||||
|
const recordingId = normalizeGuideHotkeyRecordingId(recordingIdInput);
|
||||||
|
if (recordingId === null || activeGuideHotkeySessionId === recordingId) {
|
||||||
|
activeGuideHotkeySessionId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pauseGuideHotkeyRecording() {
|
||||||
|
if (activeGuideHotkeyRecording && activeGuideHotkeyRecording.pausedAtMs === null) {
|
||||||
|
activeGuideHotkeyRecording.pausedAtMs = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resumeGuideHotkeyRecording() {
|
||||||
|
if (!activeGuideHotkeyRecording || activeGuideHotkeyRecording.pausedAtMs === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeGuideHotkeyRecording.accumulatedPausedMs += Math.max(
|
||||||
|
0,
|
||||||
|
Date.now() - activeGuideHotkeyRecording.pausedAtMs,
|
||||||
|
);
|
||||||
|
activeGuideHotkeyRecording.pausedAtMs = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGuideHotkeyRecordingTimeMs(recording: GuideHotkeyRecordingState): number {
|
||||||
|
const now = recording.pausedAtMs ?? Date.now();
|
||||||
|
return Math.max(0, now - recording.startedAtMs - recording.accumulatedPausedMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGuideHotkeyPoint(boundsInput: GuideHotkeyBounds) {
|
||||||
|
const bounds = sanitizeGuideHotkeyBounds(boundsInput);
|
||||||
|
const cursor = screen.getCursorScreenPoint();
|
||||||
|
return {
|
||||||
|
normalizedX: clampGuideHotkey01((cursor.x - bounds.x) / bounds.width),
|
||||||
|
normalizedY: clampGuideHotkey01((cursor.y - bounds.y) / bounds.height),
|
||||||
|
rawX: cursor.x,
|
||||||
|
rawY: cursor.y,
|
||||||
|
bounds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampGuideHotkey01(value: number): number {
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Math.min(1, Math.max(0, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureGuideHotkeyMarker(guideStore: GuideStore) {
|
||||||
|
const recording = activeGuideHotkeyRecording;
|
||||||
|
if (!recording || activeGuideHotkeySessionId !== recording.recordingId) {
|
||||||
|
return { captured: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const point = getGuideHotkeyPoint(recording.bounds);
|
||||||
|
try {
|
||||||
|
const result = await guideStore.addMarker({
|
||||||
|
recordingId: recording.recordingId,
|
||||||
|
kind: "hotkey",
|
||||||
|
timeMs: getGuideHotkeyRecordingTimeMs(recording),
|
||||||
|
x: point.normalizedX,
|
||||||
|
y: point.normalizedY,
|
||||||
|
normalizedX: point.normalizedX,
|
||||||
|
normalizedY: point.normalizedY,
|
||||||
|
label: `${GUIDE_MARKER_HOTKEY_LABEL} marker`,
|
||||||
|
});
|
||||||
|
console.info("[guide-hotkey] marker captured", {
|
||||||
|
recordingId: recording.recordingId,
|
||||||
|
timeMs: result.event.timeMs,
|
||||||
|
normalizedX: result.event.normalizedX,
|
||||||
|
normalizedY: result.event.normalizedY,
|
||||||
|
rawX: point.rawX,
|
||||||
|
rawY: point.rawY,
|
||||||
|
bounds: point.bounds,
|
||||||
|
});
|
||||||
|
return { captured: true, ...result };
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
console.warn("[guide-hotkey] failed to capture marker:", message);
|
||||||
|
return { captured: false, error: message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerGuideMarkerHotkey(guideStore: GuideStore) {
|
||||||
|
if (guideMarkerHotkeyRegistered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
guideMarkerHotkeyRegistered = globalShortcut.register(GUIDE_MARKER_HOTKEY, () => {
|
||||||
|
void captureGuideHotkeyMarker(guideStore);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!guideMarkerHotkeyRegistered) {
|
||||||
|
console.warn(`[guide-hotkey] failed to register ${GUIDE_MARKER_HOTKEY_LABEL}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.once("will-quit", () => {
|
||||||
|
globalShortcut.unregister(GUIDE_MARKER_HOTKEY);
|
||||||
|
guideMarkerHotkeyRegistered = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getSelectedSourceId() {
|
function getSelectedSourceId() {
|
||||||
return typeof selectedSource?.id === "string" ? selectedSource.id : null;
|
return typeof selectedSource?.id === "string" ? selectedSource.id : null;
|
||||||
}
|
}
|
||||||
@@ -1666,6 +1834,7 @@ export function registerIpcHandlers(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const source = selectedSource || { name: "Screen" };
|
const source = selectedSource || { name: "Screen" };
|
||||||
|
startGuideHotkeyRecording(recordingId, bounds);
|
||||||
if (onRecordingStateChange) {
|
if (onRecordingStateChange) {
|
||||||
onRecordingStateChange(true, source.name);
|
onRecordingStateChange(true, source.name);
|
||||||
}
|
}
|
||||||
@@ -1689,6 +1858,7 @@ export function registerIpcHandlers(
|
|||||||
nativeWindowsPauseStartedAtMs = null;
|
nativeWindowsPauseStartedAtMs = null;
|
||||||
nativeWindowsPauseRanges = [];
|
nativeWindowsPauseRanges = [];
|
||||||
nativeWindowsIsPaused = false;
|
nativeWindowsIsPaused = false;
|
||||||
|
clearGuideHotkeyRecording();
|
||||||
await stopCursorRecording();
|
await stopCursorRecording();
|
||||||
return { success: false, error: String(error) };
|
return { success: false, error: String(error) };
|
||||||
}
|
}
|
||||||
@@ -1811,6 +1981,7 @@ export function registerIpcHandlers(
|
|||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const source = selectedSource || { name: "Screen" };
|
const source = selectedSource || { name: "Screen" };
|
||||||
|
startGuideHotkeyRecording(recordingId, bounds);
|
||||||
if (onRecordingStateChange) {
|
if (onRecordingStateChange) {
|
||||||
onRecordingStateChange(true, source.name);
|
onRecordingStateChange(true, source.name);
|
||||||
}
|
}
|
||||||
@@ -1833,6 +2004,7 @@ export function registerIpcHandlers(
|
|||||||
nativeMacPauseStartedAtMs = null;
|
nativeMacPauseStartedAtMs = null;
|
||||||
nativeMacPauseRanges = [];
|
nativeMacPauseRanges = [];
|
||||||
nativeMacIsPaused = false;
|
nativeMacIsPaused = false;
|
||||||
|
clearGuideHotkeyRecording();
|
||||||
await stopCursorRecording();
|
await stopCursorRecording();
|
||||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||||
}
|
}
|
||||||
@@ -1858,6 +2030,7 @@ export function registerIpcHandlers(
|
|||||||
proc.stdin.write("pause\n");
|
proc.stdin.write("pause\n");
|
||||||
nativeMacIsPaused = true;
|
nativeMacIsPaused = true;
|
||||||
nativeMacPauseStartedAtMs = Date.now();
|
nativeMacPauseStartedAtMs = Date.now();
|
||||||
|
pauseGuideHotkeyRecording();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||||
@@ -1884,6 +2057,7 @@ export function registerIpcHandlers(
|
|||||||
proc.stdin.write("resume\n");
|
proc.stdin.write("resume\n");
|
||||||
completeNativeMacCursorPauseRange();
|
completeNativeMacCursorPauseRange();
|
||||||
nativeMacIsPaused = false;
|
nativeMacIsPaused = false;
|
||||||
|
resumeGuideHotkeyRecording();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||||
@@ -1906,6 +2080,7 @@ export function registerIpcHandlers(
|
|||||||
proc.stdin.write("pause\n");
|
proc.stdin.write("pause\n");
|
||||||
nativeWindowsIsPaused = true;
|
nativeWindowsIsPaused = true;
|
||||||
nativeWindowsPauseStartedAtMs = Date.now();
|
nativeWindowsPauseStartedAtMs = Date.now();
|
||||||
|
pauseGuideHotkeyRecording();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||||
@@ -1928,6 +2103,7 @@ export function registerIpcHandlers(
|
|||||||
proc.stdin.write("resume\n");
|
proc.stdin.write("resume\n");
|
||||||
completeNativeWindowsCursorPauseRange();
|
completeNativeWindowsCursorPauseRange();
|
||||||
nativeWindowsIsPaused = false;
|
nativeWindowsIsPaused = false;
|
||||||
|
resumeGuideHotkeyRecording();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||||
@@ -2017,6 +2193,7 @@ export function registerIpcHandlers(
|
|||||||
nativeWindowsPauseStartedAtMs = null;
|
nativeWindowsPauseStartedAtMs = null;
|
||||||
nativeWindowsPauseRanges = [];
|
nativeWindowsPauseRanges = [];
|
||||||
nativeWindowsIsPaused = false;
|
nativeWindowsIsPaused = false;
|
||||||
|
clearGuideHotkeyRecording();
|
||||||
const source = selectedSource || { name: "Screen" };
|
const source = selectedSource || { name: "Screen" };
|
||||||
if (onRecordingStateChange) {
|
if (onRecordingStateChange) {
|
||||||
onRecordingStateChange(false, source.name);
|
onRecordingStateChange(false, source.name);
|
||||||
@@ -2102,6 +2279,7 @@ export function registerIpcHandlers(
|
|||||||
nativeMacPauseStartedAtMs = null;
|
nativeMacPauseStartedAtMs = null;
|
||||||
nativeMacPauseRanges = [];
|
nativeMacPauseRanges = [];
|
||||||
nativeMacIsPaused = false;
|
nativeMacIsPaused = false;
|
||||||
|
clearGuideHotkeyRecording();
|
||||||
const source = selectedSource || { name: "Screen" };
|
const source = selectedSource || { name: "Screen" };
|
||||||
if (onRecordingStateChange) {
|
if (onRecordingStateChange) {
|
||||||
onRecordingStateChange(false, source.name);
|
onRecordingStateChange(false, source.name);
|
||||||
@@ -2178,11 +2356,27 @@ export function registerIpcHandlers(
|
|||||||
const guideAiSettingsStore = new DeepSeekSettingsStore(
|
const guideAiSettingsStore = new DeepSeekSettingsStore(
|
||||||
path.join(app.getPath("userData"), "guide-ai-settings.json"),
|
path.join(app.getPath("userData"), "guide-ai-settings.json"),
|
||||||
);
|
);
|
||||||
registerGuideIpcHandlers(
|
const guideStore = new GuideStore(RECORDINGS_DIR, {
|
||||||
ipcMain,
|
deepSeekConfigProvider: guideAiSettingsStore,
|
||||||
new GuideStore(RECORDINGS_DIR, { deepSeekConfigProvider: guideAiSettingsStore }),
|
});
|
||||||
guideAiSettingsStore,
|
registerGuideMarkerHotkey(guideStore);
|
||||||
);
|
registerGuideIpcHandlers(ipcMain, guideStore, guideAiSettingsStore, {
|
||||||
|
onSessionStarted: (session) => activateGuideHotkeySession(session.recordingId),
|
||||||
|
onSessionEnded: (recordingId) => deactivateGuideHotkeySession(recordingId),
|
||||||
|
});
|
||||||
|
ipcMain.handle("guide:capture-pointer-marker", async () => {
|
||||||
|
const result = await captureGuideHotkeyMarker(guideStore);
|
||||||
|
if (result.error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
code: "guide-internal-error",
|
||||||
|
error: result.error,
|
||||||
|
retryable: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, data: result };
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle("store-recorded-session", async (_, payload: StoreRecordedSessionInput) => {
|
ipcMain.handle("store-recorded-session", async (_, payload: StoreRecordedSessionInput) => {
|
||||||
try {
|
try {
|
||||||
@@ -2315,6 +2509,11 @@ export function registerIpcHandlers(
|
|||||||
} else {
|
} else {
|
||||||
await stopCursorRecording();
|
await stopCursorRecording();
|
||||||
}
|
}
|
||||||
|
if (recording) {
|
||||||
|
startGuideHotkeyRecording(recordingId, getSelectedSourceBounds());
|
||||||
|
} else {
|
||||||
|
clearGuideHotkeyRecording();
|
||||||
|
}
|
||||||
|
|
||||||
const source = selectedSource || { name: "Screen" };
|
const source = selectedSource || { name: "Screen" };
|
||||||
if (onRecordingStateChange) {
|
if (onRecordingStateChange) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { contextBridge, ipcRenderer } from "electron";
|
import { contextBridge, ipcRenderer } from "electron";
|
||||||
import type {
|
import type {
|
||||||
AddGuideMarkerInput,
|
AddGuideMarkerInput,
|
||||||
|
CaptureGuidePointerMarkerResult,
|
||||||
DiscardGuideSessionInput,
|
DiscardGuideSessionInput,
|
||||||
ExportGuideInput,
|
ExportGuideInput,
|
||||||
FinalizeGuideEventsInput,
|
FinalizeGuideEventsInput,
|
||||||
@@ -37,6 +38,11 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
addMarker: (input: AddGuideMarkerInput) => {
|
addMarker: (input: AddGuideMarkerInput) => {
|
||||||
return ipcRenderer.invoke("guide:add-marker", input);
|
return ipcRenderer.invoke("guide:add-marker", input);
|
||||||
},
|
},
|
||||||
|
capturePointerMarker: () => {
|
||||||
|
return ipcRenderer.invoke("guide:capture-pointer-marker") as Promise<
|
||||||
|
import("../src/guide/contracts").GuideIpcResult<CaptureGuidePointerMarkerResult>
|
||||||
|
>;
|
||||||
|
},
|
||||||
finalizeEvents: (input: FinalizeGuideEventsInput) => {
|
finalizeEvents: (input: FinalizeGuideEventsInput) => {
|
||||||
return ipcRenderer.invoke("guide:finalize-events", input);
|
return ipcRenderer.invoke("guide:finalize-events", input);
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "openscreen",
|
"name": "openscreen",
|
||||||
"version": "1.4.0",
|
"version": "1.4.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "openscreen",
|
"name": "openscreen",
|
||||||
"version": "1.4.0",
|
"version": "1.4.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fix-webm-duration/fix": "^1.0.1",
|
"@fix-webm-duration/fix": "^1.0.1",
|
||||||
"@pixi/filter-drop-shadow": "^5.2.0",
|
"@pixi/filter-drop-shadow": "^5.2.0",
|
||||||
|
|||||||
+3
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "openscreen",
|
"name": "openscreen",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.4.0",
|
"version": "1.4.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "npm@10.9.4",
|
"packageManager": "npm@10.9.4",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -25,6 +25,8 @@
|
|||||||
"build:native:win": "node scripts/build-windows-wgc-helper.mjs",
|
"build:native:win": "node scripts/build-windows-wgc-helper.mjs",
|
||||||
"build:ocr:win": "node scripts/build-windows-ocr-service.mjs",
|
"build:ocr:win": "node scripts/build-windows-ocr-service.mjs",
|
||||||
"build:win": "npm run build:native:win && npm run build:ocr:win && tsc && vite build && electron-builder --win --config electron-builder.json5 --config.npmRebuild=false",
|
"build:win": "npm run build:native:win && npm run build:ocr:win && tsc && vite build && electron-builder --win --config electron-builder.json5 --config.npmRebuild=false",
|
||||||
|
"build:win:private-trust": "npm run build:native:win && npm run build:ocr:win && tsc && vite build && electron-builder --win --config electron-builder.private-trust.cjs --config.npmRebuild=false",
|
||||||
|
"sign:win:private-trust": "node scripts/sign-windows-private-trust.mjs",
|
||||||
"build:linux": "tsc && vite build && electron-builder --linux AppImage deb pacman --config electron-builder.json5 --config.npmRebuild=false",
|
"build:linux": "tsc && vite build && electron-builder --linux AppImage deb pacman --config electron-builder.json5 --config.npmRebuild=false",
|
||||||
"test": "vitest --run",
|
"test": "vitest --run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import process from "node:process";
|
||||||
|
|
||||||
|
const rootDir = process.cwd();
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||||
|
|
||||||
|
function loadLocalSigningEnv() {
|
||||||
|
const envPath = path.join(rootDir, ".env.signing.local");
|
||||||
|
if (!fs.existsSync(envPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = fs.readFileSync(envPath, "utf8").split(/\r?\n/);
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith("#")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
||||||
|
if (!match || process.env[match[1]]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
process.env[match[1]] = match[2].replace(/^['"]|['"]$/g, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function usage() {
|
||||||
|
return [
|
||||||
|
"Usage:",
|
||||||
|
" node scripts/sign-windows-private-trust.mjs [--file <path>]",
|
||||||
|
"",
|
||||||
|
"Defaults to release/<version>/Openscreen Setup <version>.exe",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = { file: null };
|
||||||
|
for (let i = 0; i < argv.length; i += 1) {
|
||||||
|
const arg = argv[i];
|
||||||
|
if (arg === "--help" || arg === "-h") {
|
||||||
|
console.log(usage());
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
if (arg === "--file") {
|
||||||
|
args.file = argv[i + 1];
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new Error(`Unknown argument: ${arg}\n${usage()}`);
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireEnv(name) {
|
||||||
|
const value = process.env[name]?.trim();
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`Missing required environment variable: ${name}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAnyAuthMode() {
|
||||||
|
const hasClientSecret = Boolean(process.env.AZURE_CLIENT_SECRET?.trim());
|
||||||
|
const hasClientCertificate = Boolean(process.env.AZURE_CLIENT_CERTIFICATE_PATH?.trim());
|
||||||
|
const hasUsernamePassword = Boolean(
|
||||||
|
process.env.AZURE_USERNAME?.trim() && process.env.AZURE_PASSWORD?.trim(),
|
||||||
|
);
|
||||||
|
return hasClientSecret || hasClientCertificate || hasUsernamePassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
function psQuote(value) {
|
||||||
|
return `'${String(value).replaceAll("'", "''")}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runPowerShell(command) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const candidates = ["pwsh.exe", "powershell.exe"];
|
||||||
|
const tryCandidate = (index, lastError) => {
|
||||||
|
if (index >= candidates.length) {
|
||||||
|
reject(lastError ?? new Error("Unable to find PowerShell"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const child = spawn(
|
||||||
|
candidates[index],
|
||||||
|
["-NoProfile", "-NonInteractive", "-Command", command],
|
||||||
|
{
|
||||||
|
stdio: "inherit",
|
||||||
|
windowsHide: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
child.on("error", (error) => tryCandidate(index + 1, error));
|
||||||
|
child.on("exit", (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reject(new Error(`${candidates[index]} exited with code ${code}`));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
tryCandidate(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
const defaultInstaller = path.join(
|
||||||
|
rootDir,
|
||||||
|
"release",
|
||||||
|
packageJson.version,
|
||||||
|
`Openscreen Setup ${packageJson.version}.exe`,
|
||||||
|
);
|
||||||
|
const fileToSign = path.resolve(rootDir, args.file ?? defaultInstaller);
|
||||||
|
|
||||||
|
if (!fs.existsSync(fileToSign)) {
|
||||||
|
throw new Error(`Installer not found: ${fileToSign}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
requireEnv("AZURE_TENANT_ID");
|
||||||
|
requireEnv("AZURE_CLIENT_ID");
|
||||||
|
if (!hasAnyAuthMode()) {
|
||||||
|
throw new Error(
|
||||||
|
"Missing Azure auth mode. Set AZURE_CLIENT_SECRET, or AZURE_CLIENT_CERTIFICATE_PATH, or AZURE_USERNAME/AZURE_PASSWORD.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = requireEnv("AZURE_TRUSTED_SIGNING_ENDPOINT");
|
||||||
|
const accountName = requireEnv("AZURE_TRUSTED_SIGNING_ACCOUNT_NAME");
|
||||||
|
const profileName = requireEnv("AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME");
|
||||||
|
const timestampUrl =
|
||||||
|
process.env.AZURE_TRUSTED_SIGNING_TIMESTAMP_RFC3161?.trim() ||
|
||||||
|
"http://timestamp.acs.microsoft.com";
|
||||||
|
|
||||||
|
const installCommand = [
|
||||||
|
"Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser",
|
||||||
|
"Install-Module -Name TrustedSigning -MinimumVersion 0.5.0 -Force -Repository PSGallery -Scope CurrentUser",
|
||||||
|
].join("; ");
|
||||||
|
|
||||||
|
const signCommand = [
|
||||||
|
"Invoke-TrustedSigning",
|
||||||
|
`-Endpoint ${psQuote(endpoint)}`,
|
||||||
|
`-CertificateProfileName ${psQuote(profileName)}`,
|
||||||
|
`-CodeSigningAccountName ${psQuote(accountName)}`,
|
||||||
|
`-TimestampRfc3161 ${psQuote(timestampUrl)}`,
|
||||||
|
"-TimestampDigest SHA256",
|
||||||
|
"-FileDigest SHA256",
|
||||||
|
`-Files ${psQuote(fileToSign)}`,
|
||||||
|
].join(" ");
|
||||||
|
|
||||||
|
const verifyCommand = [
|
||||||
|
"$signature = Get-AuthenticodeSignature -FilePath",
|
||||||
|
psQuote(fileToSign),
|
||||||
|
"; $signature | Format-List Status,StatusMessage,SignerCertificate,TimeStamperCertificate",
|
||||||
|
].join(" ");
|
||||||
|
|
||||||
|
console.log(`Signing ${fileToSign}`);
|
||||||
|
await runPowerShell(installCommand);
|
||||||
|
await runPowerShell(signCommand);
|
||||||
|
await runPowerShell(verifyCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadLocalSigningEnv();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await main();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error instanceof Error ? error.message : String(error));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -143,6 +143,7 @@ export function LaunchWindow() {
|
|||||||
top: 12,
|
top: 12,
|
||||||
maxHeight: 240,
|
maxHeight: 240,
|
||||||
});
|
});
|
||||||
|
const guideCtrlMarkerArmedRef = useRef(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
devices: micDevices,
|
devices: micDevices,
|
||||||
@@ -247,6 +248,47 @@ export function LaunchWindow() {
|
|||||||
};
|
};
|
||||||
}, [isLanguageMenuOpen]);
|
}, [isLanguageMenuOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!recording || !guideModeEnabled) {
|
||||||
|
guideCtrlMarkerArmedRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCtrlKey = (event: KeyboardEvent) =>
|
||||||
|
event.key === "Control" || event.code === "ControlLeft" || event.code === "ControlRight";
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (!isCtrlKey(event) || event.repeat || guideCtrlMarkerArmedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
guideCtrlMarkerArmedRef.current = true;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
addGuideMarker();
|
||||||
|
};
|
||||||
|
|
||||||
|
const releaseCtrlMarker = (event?: KeyboardEvent) => {
|
||||||
|
if (event && !isCtrlKey(event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
guideCtrlMarkerArmedRef.current = false;
|
||||||
|
};
|
||||||
|
const handleWindowBlur = () => {
|
||||||
|
guideCtrlMarkerArmedRef.current = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown, { capture: true });
|
||||||
|
window.addEventListener("keyup", releaseCtrlMarker, { capture: true });
|
||||||
|
window.addEventListener("blur", handleWindowBlur);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown, { capture: true });
|
||||||
|
window.removeEventListener("keyup", releaseCtrlMarker, { capture: true });
|
||||||
|
window.removeEventListener("blur", handleWindowBlur);
|
||||||
|
};
|
||||||
|
}, [addGuideMarker, guideModeEnabled, recording]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLanguageMenuOpen || !languageTriggerRef.current) return;
|
if (!isLanguageMenuOpen || !languageTriggerRef.current) return;
|
||||||
|
|
||||||
|
|||||||
@@ -115,11 +115,21 @@ export interface GuideSession {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CaptureGuidePointerMarkerResult {
|
||||||
|
captured: boolean;
|
||||||
|
session?: GuideSession;
|
||||||
|
event?: GuideEvent;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AddGuideMarkerInput {
|
export interface AddGuideMarkerInput {
|
||||||
recordingId: GuideRecordingIdInput;
|
recordingId: GuideRecordingIdInput;
|
||||||
timeMs: number;
|
timeMs: number;
|
||||||
kind: "hotkey" | "manual";
|
kind: "hotkey" | "manual";
|
||||||
label?: string;
|
label?: string;
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
normalizedX?: number;
|
||||||
|
normalizedY?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FinalizeGuideEventsInput {
|
export interface FinalizeGuideEventsInput {
|
||||||
|
|||||||
@@ -90,6 +90,25 @@ describe("buildGuideStepCandidates", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("treats hotkey markers with coordinates like clicks", () => {
|
||||||
|
const session = createSession();
|
||||||
|
session.events[0] = {
|
||||||
|
...session.events[0],
|
||||||
|
kind: "hotkey",
|
||||||
|
source: "guide-hotkey",
|
||||||
|
normalizedX: 0.5,
|
||||||
|
normalizedY: 0.5,
|
||||||
|
};
|
||||||
|
|
||||||
|
const candidates = buildGuideStepCandidates(session);
|
||||||
|
|
||||||
|
expect(candidates[0]).toMatchObject({
|
||||||
|
action: "click",
|
||||||
|
targetText: "Save",
|
||||||
|
targetRole: "button",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("prefers a nearby line phrase over a single OCR word", () => {
|
it("prefers a nearby line phrase over a single OCR word", () => {
|
||||||
const session = createSession();
|
const session = createSession();
|
||||||
session.events[0] = {
|
session.events[0] = {
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ function pointInsideExpandedBox(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function inferAction(event: GuideEvent): GuideAction {
|
function inferAction(event: GuideEvent): GuideAction {
|
||||||
if (event.kind === "click") {
|
if (event.kind === "click" || (event.kind === "hotkey" && getEventPoint(event))) {
|
||||||
return "click";
|
return "click";
|
||||||
}
|
}
|
||||||
return "manual";
|
return "manual";
|
||||||
|
|||||||
@@ -209,18 +209,27 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void window.electronAPI.guide
|
void (async () => {
|
||||||
.addMarker({
|
if (window.electronAPI?.guide.capturePointerMarker) {
|
||||||
|
const captureResult = await window.electronAPI.guide.capturePointerMarker();
|
||||||
|
if (captureResult.success && captureResult.data.captured) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!captureResult.success) {
|
||||||
|
console.warn("Failed to capture guide pointer marker:", captureResult.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await window.electronAPI.guide.addMarker({
|
||||||
recordingId: activeRecordingId,
|
recordingId: activeRecordingId,
|
||||||
kind: "manual",
|
kind: "manual",
|
||||||
timeMs: getRecordingDurationMs(),
|
timeMs: getRecordingDurationMs(),
|
||||||
label: "Manual marker",
|
label: "Manual marker",
|
||||||
})
|
|
||||||
.then((result) => {
|
|
||||||
if (!result.success) {
|
|
||||||
console.warn("Failed to add guide marker:", result.error);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
if (!result.success) {
|
||||||
|
console.warn("Failed to add guide marker:", result.error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
}, [getRecordingDurationMs, recording]);
|
}, [getRecordingDurationMs, recording]);
|
||||||
|
|
||||||
const selectMimeType = () => {
|
const selectMimeType = () => {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
"guide": {
|
"guide": {
|
||||||
"enableGuideMode": "Enable guide mode",
|
"enableGuideMode": "Enable guide mode",
|
||||||
"disableGuideMode": "Disable guide mode",
|
"disableGuideMode": "Disable guide mode",
|
||||||
"addMarker": "Add guide marker"
|
"addMarker": "Capture guide marker (Ctrl or Ctrl+F12)"
|
||||||
},
|
},
|
||||||
"sourceSelector": {
|
"sourceSelector": {
|
||||||
"loading": "Loading sources...",
|
"loading": "Loading sources...",
|
||||||
|
|||||||
@@ -47,6 +47,6 @@
|
|||||||
"guide": {
|
"guide": {
|
||||||
"enableGuideMode": "Bật chế độ tạo hướng dẫn",
|
"enableGuideMode": "Bật chế độ tạo hướng dẫn",
|
||||||
"disableGuideMode": "Tắt chế độ tạo hướng dẫn",
|
"disableGuideMode": "Tắt chế độ tạo hướng dẫn",
|
||||||
"addMarker": "Thêm mốc hướng dẫn"
|
"addMarker": "Chụp mốc hướng dẫn (Ctrl hoặc Ctrl+F12)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user